diff --git a/SoundFont2/Chunks/Main Chunks/InfoListChunk.cs b/SoundFont2/Chunks/Main Chunks/InfoListChunk.cs new file mode 100644 index 0000000..a27a2a2 --- /dev/null +++ b/SoundFont2/Chunks/Main Chunks/InfoListChunk.cs @@ -0,0 +1,390 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public sealed class InfoListChunk : SF2ListChunk + { + private const string DEFAULT_ENGINE = "EMU8000"; + private const string DEFAULT_BANK = "General MIDI"; + private const int COMMENT_MAX_SIZE = 0x10000; + + private readonly List _subChunks; + public string Engine + { + get + { + if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) + { + return chunk.Field; + } + _subChunks.Add(new HeaderSubChunk(_sf2, "isng", DEFAULT_ENGINE)); + return DEFAULT_ENGINE; + } + set + { + if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "isng", value)); + } + } + } + public string Bank + { + get + { + if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) + { + return chunk.Field; + } + _subChunks.Add(new HeaderSubChunk(_sf2, "INAM", DEFAULT_BANK)); + return DEFAULT_BANK; + } + set + { + if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "INAM", value)); + } + } + } + + public string? ROM + { + get + { + if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) + { + return chunk.Field; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "irom") as HeaderSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "irom", value)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + public SF2VersionTag? ROMVersion + { + get + { + if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) + { + return chunk.Version; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "iver") as VersionSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Version = value; + } + else + { + _subChunks.Add(new VersionSubChunk(_sf2, "iver", value)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + public string? Date + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) + { + return chunk.Field; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "ICRD") as HeaderSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICRD", value)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + public string? Designer + { + get + { + if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) + { + return chunk.Field; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "IENG") as HeaderSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "IENG", value)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + public string? Products + { + get + { + if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) + { + return chunk.Field; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "IPRD") as HeaderSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "IPRD", value)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + public string? Copyright + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk icop) + { + return icop.Field; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "ICOP") as HeaderSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICOP", value)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + + public string? Comment + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) + { + return chunk.Field; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "ICMT") as HeaderSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICMT", value, maxSize: COMMENT_MAX_SIZE)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + public string? Tools + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) + { + return chunk.Field; + } + return null; + } + set + { + var chunk = _subChunks.Find(s => s.ChunkName == "ISFT") as HeaderSubChunk; + if (value is not null) + { + if (chunk is not null) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ISFT", value)); + } + } + else + { + if (chunk is not null) + { + _subChunks.Remove(chunk); + } + } + } + } + + internal InfoListChunk(SF2 inSf2) : base(inSf2, "INFO") + { + _subChunks = new List() + { + // Mandatory sub-chunks + new VersionSubChunk(inSf2, "ifil", new SF2VersionTag(2, 1)), + new HeaderSubChunk(inSf2, "isng", DEFAULT_ENGINE), + new HeaderSubChunk(inSf2, "INAM", DEFAULT_BANK), + }; + inSf2.UpdateSize(); + } + internal InfoListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + _subChunks = new List(); + long startOffset = reader.Stream.Position; + while (reader.Stream.Position < startOffset + Size - 4) // The 4 represents the INFO that was already read + { + // Peek 4 chars for the chunk name + string name = reader.ReadString_Count(4); + reader.Stream.Position -= 4; + switch (name) + { + case "ICMT": _subChunks.Add(new HeaderSubChunk(inSf2, reader, maxSize: COMMENT_MAX_SIZE)); break; + case "ifil": + case "iver": _subChunks.Add(new VersionSubChunk(inSf2, reader)); break; + case "isng": + case "INAM": + case "ICRD": + case "IENG": + case "IPRD": + case "ICOP": + case "ISFT": + case "irom": _subChunks.Add(new HeaderSubChunk(inSf2, reader)); break; + default: throw new NotSupportedException($"Unsupported chunk name at 0x{reader.Stream.Position:X}: \"{name}\""); + } + } + } + + internal override uint UpdateSize() + { + Size = 4; + foreach (SF2Chunk sub in _subChunks) + { + Size += sub.Size + 8; + } + + return Size; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + foreach (SF2Chunk sub in _subChunks) + { + sub.Write(writer); + } + } + + public override string ToString() + { + return $"Info List Chunk - Sub-chunk count = {_subChunks.Count}"; + } + } +} diff --git a/SoundFont2/Chunks/Main Chunks/PdtaListChunk.cs b/SoundFont2/Chunks/Main Chunks/PdtaListChunk.cs new file mode 100644 index 0000000..2b646f0 --- /dev/null +++ b/SoundFont2/Chunks/Main Chunks/PdtaListChunk.cs @@ -0,0 +1,76 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public sealed class PdtaListChunk : SF2ListChunk + { + public PHDRSubChunk PHDRSubChunk { get; } + public BAGSubChunk PBAGSubChunk { get; } + public MODSubChunk PMODSubChunk { get; } + public GENSubChunk PGENSubChunk { get; } + public INSTSubChunk INSTSubChunk { get; } + public BAGSubChunk IBAGSubChunk { get; } + public MODSubChunk IMODSubChunk { get; } + public GENSubChunk IGENSubChunk { get; } + public SHDRSubChunk SHDRSubChunk { get; } + + internal PdtaListChunk(SF2 inSf2) : base(inSf2, "pdta") + { + PHDRSubChunk = new PHDRSubChunk(inSf2); + PBAGSubChunk = new BAGSubChunk(inSf2, true); + PMODSubChunk = new MODSubChunk(inSf2, true); + PGENSubChunk = new GENSubChunk(inSf2, true); + INSTSubChunk = new INSTSubChunk(inSf2); + IBAGSubChunk = new BAGSubChunk(inSf2, false); + IMODSubChunk = new MODSubChunk(inSf2, false); + IGENSubChunk = new GENSubChunk(inSf2, false); + SHDRSubChunk = new SHDRSubChunk(inSf2); + inSf2.UpdateSize(); + } + internal PdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + PHDRSubChunk = new PHDRSubChunk(inSf2, reader); + PBAGSubChunk = new BAGSubChunk(inSf2, reader); + PMODSubChunk = new MODSubChunk(inSf2, reader); + PGENSubChunk = new GENSubChunk(inSf2, reader); + INSTSubChunk = new INSTSubChunk(inSf2, reader); + IBAGSubChunk = new BAGSubChunk(inSf2, reader); + IMODSubChunk = new MODSubChunk(inSf2, reader); + IGENSubChunk = new GENSubChunk(inSf2, reader); + SHDRSubChunk = new SHDRSubChunk(inSf2, reader); + } + + internal override uint UpdateSize() + { + return Size = 4 + + PHDRSubChunk.Size + 8 + + PBAGSubChunk.Size + 8 + + PMODSubChunk.Size + 8 + + PGENSubChunk.Size + 8 + + INSTSubChunk.Size + 8 + + IBAGSubChunk.Size + 8 + + IMODSubChunk.Size + 8 + + IGENSubChunk.Size + 8 + + SHDRSubChunk.Size + 8; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + PHDRSubChunk.Write(writer); + PBAGSubChunk.Write(writer); + PMODSubChunk.Write(writer); + PGENSubChunk.Write(writer); + INSTSubChunk.Write(writer); + IBAGSubChunk.Write(writer); + IMODSubChunk.Write(writer); + IGENSubChunk.Write(writer); + SHDRSubChunk.Write(writer); + } + + public override string ToString() + { + return $"Hydra List Chunk"; + } + } +} diff --git a/SoundFont2/Chunks/Main Chunks/SdtaListChunk.cs b/SoundFont2/Chunks/Main Chunks/SdtaListChunk.cs new file mode 100644 index 0000000..b5101d1 --- /dev/null +++ b/SoundFont2/Chunks/Main Chunks/SdtaListChunk.cs @@ -0,0 +1,36 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public sealed class SdtaListChunk : SF2ListChunk + { + public SMPLSubChunk SMPLSubChunk { get; } + + internal SdtaListChunk(SF2 inSf2) : base(inSf2, "sdta") + { + SMPLSubChunk = new SMPLSubChunk(inSf2); + inSf2.UpdateSize(); + } + internal SdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + SMPLSubChunk = new SMPLSubChunk(inSf2, reader); + } + + internal override uint UpdateSize() + { + return Size = 4 + + SMPLSubChunk.Size + 8; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + SMPLSubChunk.Write(writer); + } + + public override string ToString() + { + return $"Sample Data List Chunk"; + } + } +} diff --git a/SoundFont2/Chunks/SF2Bag.cs b/SoundFont2/Chunks/SF2Bag.cs new file mode 100644 index 0000000..687a2d3 --- /dev/null +++ b/SoundFont2/Chunks/SF2Bag.cs @@ -0,0 +1,46 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + /// Covers sfPresetBag and sfInstBag + public sealed class SF2Bag + { + public const uint SIZE = 4; + + /// Index in list of generators + public ushort GeneratorIndex { get; set; } + /// Index in list of modulators + public ushort ModulatorIndex { get; set; } + + internal SF2Bag(SF2 inSf2, bool isPresetBag) + { + if (isPresetBag) + { + GeneratorIndex = (ushort)inSf2.HydraChunk.PGENSubChunk.Count; + ModulatorIndex = (ushort)inSf2.HydraChunk.PMODSubChunk.Count; + } + else + { + GeneratorIndex = (ushort)inSf2.HydraChunk.IGENSubChunk.Count; + ModulatorIndex = (ushort)inSf2.HydraChunk.IMODSubChunk.Count; + } + } + internal SF2Bag(EndianBinaryReader reader) + { + GeneratorIndex = reader.ReadUInt16(); + ModulatorIndex = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.WriteUInt16(GeneratorIndex); + writer.WriteUInt16(ModulatorIndex); + } + + public override string ToString() + { + return $"Bag - Generator index = {GeneratorIndex}" + + $",\nModulator index = {ModulatorIndex}"; + } + } +} diff --git a/SoundFont2/Chunks/SF2Chunk.cs b/SoundFont2/Chunks/SF2Chunk.cs new file mode 100644 index 0000000..cd48a53 --- /dev/null +++ b/SoundFont2/Chunks/SF2Chunk.cs @@ -0,0 +1,32 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public class SF2Chunk + { + protected readonly SF2 _sf2; + + /// Length 4 + public string ChunkName { get; } + /// Size in bytes + public uint Size { get; protected set; } + + protected SF2Chunk(SF2 inSf2, string name) + { + _sf2 = inSf2; + ChunkName = name; + } + protected SF2Chunk(SF2 inSf2, EndianBinaryReader reader) + { + _sf2 = inSf2; + ChunkName = reader.ReadString_Count(4); + Size = reader.ReadUInt32(); + } + + internal virtual void Write(EndianBinaryWriter writer) + { + writer.WriteChars_Count(ChunkName, 4); + writer.WriteUInt32(Size); + } + } +} diff --git a/SoundFont2/Chunks/SF2GeneratorList.cs b/SoundFont2/Chunks/SF2GeneratorList.cs new file mode 100644 index 0000000..c32acb0 --- /dev/null +++ b/SoundFont2/Chunks/SF2GeneratorList.cs @@ -0,0 +1,36 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public sealed class SF2GeneratorList + { + public const uint SIZE = 4; + + public SF2Generator Generator { get; set; } + public SF2GeneratorAmount GeneratorAmount { get; set; } + + internal SF2GeneratorList() { } + internal SF2GeneratorList(SF2Generator generator, SF2GeneratorAmount amount) + { + Generator = generator; + GeneratorAmount = amount; + } + internal SF2GeneratorList(EndianBinaryReader reader) + { + Generator = reader.ReadEnum(); + GeneratorAmount = new SF2GeneratorAmount { Amount = reader.ReadInt16() }; + } + + public void Write(EndianBinaryWriter writer) + { + writer.WriteEnum(Generator); + writer.WriteInt16(GeneratorAmount.Amount); + } + + public override string ToString() + { + return $"Generator List - Generator = {Generator}" + + $",\nGenerator amount = \"{GeneratorAmount}\""; + } + } +} diff --git a/SoundFont2/Chunks/SF2Instrument.cs b/SoundFont2/Chunks/SF2Instrument.cs new file mode 100644 index 0000000..bd6603a --- /dev/null +++ b/SoundFont2/Chunks/SF2Instrument.cs @@ -0,0 +1,35 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public sealed class SF2Instrument + { + public const uint SIZE = 22; + + /// Length 20 + public string InstrumentName { get; set; } + public ushort InstrumentBagIndex { get; set; } + + internal SF2Instrument(string name, ushort index) + { + InstrumentName = name; + InstrumentBagIndex = index; + } + internal SF2Instrument(EndianBinaryReader reader) + { + InstrumentName = reader.ReadString_Count_TrimNullTerminators(20); + InstrumentBagIndex = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.WriteChars_Count(InstrumentName, 20); + writer.WriteUInt16(InstrumentBagIndex); + } + + public override string ToString() + { + return $"Instrument - Name = \"{InstrumentName}\""; + } + } +} diff --git a/SoundFont2/Chunks/SF2ListChunk.cs b/SoundFont2/Chunks/SF2ListChunk.cs new file mode 100644 index 0000000..6b8d09f --- /dev/null +++ b/SoundFont2/Chunks/SF2ListChunk.cs @@ -0,0 +1,28 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public abstract class SF2ListChunk : SF2Chunk + { + ///Length 4 + public string ListChunkName { get; } + + protected SF2ListChunk(SF2 inSf2, string name) : base(inSf2, "LIST") + { + ListChunkName = name; + Size = 4; + } + protected SF2ListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + ListChunkName = reader.ReadString_Count(4); + } + + internal abstract uint UpdateSize(); + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.WriteChars_Count(ListChunkName, 4); + } + } +} diff --git a/SoundFont2/Chunks/SF2ModulatorList.cs b/SoundFont2/Chunks/SF2ModulatorList.cs new file mode 100644 index 0000000..0bf3444 --- /dev/null +++ b/SoundFont2/Chunks/SF2ModulatorList.cs @@ -0,0 +1,44 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + /// Covers sfModList and sfInstModList + public sealed class SF2ModulatorList + { + public const uint SIZE = 10; + + public SF2Modulator ModulatorSource { get; set; } + public SF2Generator ModulatorDestination { get; set; } + public short ModulatorAmount { get; set; } + public SF2Modulator ModulatorAmountSource { get; set; } + public SF2Transform ModulatorTransform { get; set; } + + internal SF2ModulatorList() { } + internal SF2ModulatorList(EndianBinaryReader reader) + { + ModulatorSource = reader.ReadEnum(); + ModulatorDestination = reader.ReadEnum(); + ModulatorAmount = reader.ReadInt16(); + ModulatorAmountSource = reader.ReadEnum(); + ModulatorTransform = reader.ReadEnum(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.WriteEnum(ModulatorSource); + writer.WriteEnum(ModulatorDestination); + writer.WriteInt16(ModulatorAmount); + writer.WriteEnum(ModulatorAmountSource); + writer.WriteEnum(ModulatorTransform); + } + + public override string ToString() + { + return $"Modulator List - Modulator source = {ModulatorSource}" + + $",\nModulator destination = {ModulatorDestination}" + + $",\nModulator amount = {ModulatorAmount}" + + $",\nModulator amount source = {ModulatorAmountSource}" + + $",\nModulator transform = {ModulatorTransform}"; + } + } +} diff --git a/SoundFont2/Chunks/SF2PresetHeader.cs b/SoundFont2/Chunks/SF2PresetHeader.cs new file mode 100644 index 0000000..1b4bfce --- /dev/null +++ b/SoundFont2/Chunks/SF2PresetHeader.cs @@ -0,0 +1,55 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public sealed class SF2PresetHeader + { + public const uint SIZE = 38; + + /// Length 20 + public string PresetName { get; set; } + public ushort Preset { get; set; } + public ushort Bank { get; set; } + public ushort PresetBagIndex { get; set; } + // Reserved for future implementations + private readonly uint _library; + private readonly uint _genre; + private readonly uint _morphology; + + internal SF2PresetHeader(string name, ushort preset, ushort bank, ushort index) + { + PresetName = name; + Preset = preset; + Bank = bank; + PresetBagIndex = index; + } + internal SF2PresetHeader(EndianBinaryReader reader) + { + PresetName = reader.ReadString_Count_TrimNullTerminators(20); + Preset = reader.ReadUInt16(); + Bank = reader.ReadUInt16(); + PresetBagIndex = reader.ReadUInt16(); + _library = reader.ReadUInt32(); + _genre = reader.ReadUInt32(); + _morphology = reader.ReadUInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.WriteChars_Count(PresetName, 20); + writer.WriteUInt16(Preset); + writer.WriteUInt16(Bank); + writer.WriteUInt16(PresetBagIndex); + writer.WriteUInt32(_library); + writer.WriteUInt32(_genre); + writer.WriteUInt32(_morphology); + } + + public override string ToString() + { + return $"Preset Header - Bank = {Bank}" + + $",\nPreset = {Preset}" + + $",\nName = \"{PresetName}\""; + } + } +} diff --git a/SoundFont2/Chunks/SF2SampleHeader.cs b/SoundFont2/Chunks/SF2SampleHeader.cs new file mode 100644 index 0000000..c811e2c --- /dev/null +++ b/SoundFont2/Chunks/SF2SampleHeader.cs @@ -0,0 +1,67 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public sealed class SF2SampleHeader + { + public const uint SIZE = 46; + + /// Length 20 + public string SampleName { get; set; } + public uint Start { get; set; } + public uint End { get; set; } + public uint LoopStart { get; set; } + public uint LoopEnd { get; set; } + public uint SampleRate { get; set; } + public byte OriginalKey { get; set; } + public sbyte PitchCorrection { get; set; } + public ushort SampleLink { get; set; } + public SF2SampleLink SampleType { get; set; } + + internal SF2SampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + SampleName = name; + Start = start; + End = end; + LoopStart = loopStart; + LoopEnd = loopEnd; + SampleRate = sampleRate; + OriginalKey = originalKey; + PitchCorrection = pitchCorrection; + SampleType = SF2SampleLink.MonoSample; + } + internal SF2SampleHeader(EndianBinaryReader reader) + { + SampleName = reader.ReadString_Count_TrimNullTerminators(20); + Start = reader.ReadUInt32(); + End = reader.ReadUInt32(); + LoopStart = reader.ReadUInt32(); + LoopEnd = reader.ReadUInt32(); + SampleRate = reader.ReadUInt32(); + OriginalKey = reader.ReadByte(); + PitchCorrection = reader.ReadSByte(); + SampleLink = reader.ReadUInt16(); + SampleType = reader.ReadEnum(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.WriteChars_Count(SampleName, 20); + writer.WriteUInt32(Start); + writer.WriteUInt32(End); + writer.WriteUInt32(LoopStart); + writer.WriteUInt32(LoopEnd); + writer.WriteUInt32(SampleRate); + writer.WriteByte(OriginalKey); + writer.WriteSByte(PitchCorrection); + writer.WriteUInt16(SampleLink); + writer.WriteEnum(SampleType); + } + + public override string ToString() + { + return $"Sample - Name = \"{SampleName}\"" + + $",\nType = {SampleType}"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/BAGSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/BAGSubChunk.cs new file mode 100644 index 0000000..c74ca27 --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/BAGSubChunk.cs @@ -0,0 +1,42 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public sealed class BAGSubChunk : SF2Chunk + { + private readonly List _bags = new(); + public uint Count => (uint)_bags.Count; + + internal BAGSubChunk(SF2 inSf2, bool isPreset) : base(inSf2, isPreset ? "pbag" : "ibag") { } + internal BAGSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2Bag.SIZE; i++) + { + _bags.Add(new SF2Bag(reader)); + } + } + + internal void AddBag(SF2Bag bag) + { + _bags.Add(bag); + Size = Count * SF2Bag.SIZE; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _bags[i].Write(writer); + } + } + + public override string ToString() + { + return $"Bag Chunk - Name = \"{ChunkName}\"" + + $",\nBag count = {Count}"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/GENSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/GENSubChunk.cs new file mode 100644 index 0000000..47de1b2 --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/GENSubChunk.cs @@ -0,0 +1,42 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public sealed class GENSubChunk : SF2Chunk + { + private readonly List _generators = new(); + public uint Count => (uint)_generators.Count; + + internal GENSubChunk(SF2 inSf2, bool isPreset) : base(inSf2, isPreset ? "pgen" : "igen") { } + internal GENSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2GeneratorList.SIZE; i++) + { + _generators.Add(new SF2GeneratorList(reader)); + } + } + + internal void AddGenerator(SF2GeneratorList generator) + { + _generators.Add(generator); + Size = Count * SF2GeneratorList.SIZE; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _generators[i].Write(writer); + } + } + + public override string ToString() + { + return $"Generator Chunk - Name = \"{ChunkName}\"" + + $",\nGenerator count = {Count}"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/HeaderSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/HeaderSubChunk.cs new file mode 100644 index 0000000..beaa804 --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/HeaderSubChunk.cs @@ -0,0 +1,60 @@ +using Kermalis.EndianBinaryIO; +using System.Diagnostics.CodeAnalysis; + +namespace Kermalis.SoundFont2 +{ + public sealed class HeaderSubChunk : SF2Chunk + { + public int MaxSize { get; } + private int _fieldTargetLength; + private string _field; + /// Length + public string Field + { + get => _field; + [MemberNotNull(nameof(_field))] + set + { + if (value.Length >= MaxSize) // Input too long; cut it down + { + _fieldTargetLength = MaxSize; + } + else if (value.Length % 2 == 0) // Even amount of characters + { + _fieldTargetLength = value.Length + 2; // Add two null-terminators to keep the byte count even + } + else // Odd amount of characters + { + _fieldTargetLength = value.Length + 1; // Add one null-terminator since that would make the byte count even + } + _field = value; + Size = (uint)_fieldTargetLength; + _sf2.UpdateSize(); + } + } + + internal HeaderSubChunk(SF2 inSf2, string subChunkName, string field, int maxSize = 0x100) : base(inSf2, subChunkName) + { + MaxSize = maxSize; + Field = field; + } + internal HeaderSubChunk(SF2 inSf2, EndianBinaryReader reader, int maxSize = 0x100) : base(inSf2, reader) + { + MaxSize = maxSize; + Field = reader.ReadString_Count_TrimNullTerminators((int)Size); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.WriteChars_Count(_field, _fieldTargetLength); + } + + public override string ToString() + { + return $"Header Chunk - Name = \"{ChunkName}\"" + + $",\nField Max Size = {MaxSize}" + + $",\nField = \"{Field}\""; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/INSTSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/INSTSubChunk.cs new file mode 100644 index 0000000..0c0dfbd --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/INSTSubChunk.cs @@ -0,0 +1,42 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public sealed class INSTSubChunk : SF2Chunk + { + private readonly List _instruments = new(); + public uint Count => (uint)_instruments.Count; + + internal INSTSubChunk(SF2 inSf2) : base(inSf2, "inst") { } + internal INSTSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2Instrument.SIZE; i++) + { + _instruments.Add(new SF2Instrument(reader)); + } + } + + internal uint AddInstrument(SF2Instrument instrument) + { + _instruments.Add(instrument); + Size = Count * SF2Instrument.SIZE; + _sf2.UpdateSize(); + return Count - 1; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _instruments[i].Write(writer); + } + } + + public override string ToString() + { + return $"Instrument Chunk - Instrument count = {Count}"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/MODSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/MODSubChunk.cs new file mode 100644 index 0000000..ff2f657 --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/MODSubChunk.cs @@ -0,0 +1,42 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public sealed class MODSubChunk : SF2Chunk + { + private readonly List _modulators = new(); + public uint Count => (uint)_modulators.Count; + + internal MODSubChunk(SF2 inSf2, bool isPreset) : base(inSf2, isPreset ? "pmod" : "imod") { } + internal MODSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2ModulatorList.SIZE; i++) + { + _modulators.Add(new SF2ModulatorList(reader)); + } + } + + internal void AddModulator(SF2ModulatorList modulator) + { + _modulators.Add(modulator); + Size = Count * SF2ModulatorList.SIZE; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _modulators[i].Write(writer); + } + } + + public override string ToString() + { + return $"Modulator Chunk - Name = \"{ChunkName}\"" + + $",\nModulator count = {Count}"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/PHDRSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/PHDRSubChunk.cs new file mode 100644 index 0000000..2080676 --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/PHDRSubChunk.cs @@ -0,0 +1,41 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public sealed class PHDRSubChunk : SF2Chunk + { + private readonly List _presets = new(); + public uint Count => (uint)_presets.Count; + + internal PHDRSubChunk(SF2 inSf2) : base(inSf2, "phdr") { } + internal PHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2PresetHeader.SIZE; i++) + { + _presets.Add(new SF2PresetHeader(reader)); + } + } + + internal void AddPreset(SF2PresetHeader preset) + { + _presets.Add(preset); + Size = Count * SF2PresetHeader.SIZE; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _presets[i].Write(writer); + } + } + + public override string ToString() + { + return $"Preset Header Chunk - Preset count = {Count}"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/SHDRSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/SHDRSubChunk.cs new file mode 100644 index 0000000..34e5187 --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/SHDRSubChunk.cs @@ -0,0 +1,42 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public sealed class SHDRSubChunk : SF2Chunk + { + private readonly List _samples = new(); + public uint Count => (uint)_samples.Count; + + internal SHDRSubChunk(SF2 inSf2) : base(inSf2, "shdr") { } + internal SHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2SampleHeader.SIZE; i++) + { + _samples.Add(new SF2SampleHeader(reader)); + } + } + + internal uint AddSample(SF2SampleHeader sample) + { + _samples.Add(sample); + Size = Count * SF2SampleHeader.SIZE; + _sf2.UpdateSize(); + return Count - 1; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _samples[i].Write(writer); + } + } + + public override string ToString() + { + return $"Sample Header Chunk - Sample header count = {Count}"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/SMPLSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/SMPLSubChunk.cs new file mode 100644 index 0000000..a2758ff --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/SMPLSubChunk.cs @@ -0,0 +1,64 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.SoundFont2 +{ + public sealed class SMPLSubChunk : SF2Chunk + { + private short[] _samples; // Block of sample data + + internal SMPLSubChunk(SF2 inSf2) : base(inSf2, "smpl") + { + _samples = Array.Empty(); + } + internal SMPLSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + _samples = new short[Size / sizeof(short)]; + reader.ReadInt16s(_samples); + } + + // Returns index of the start of the sample + internal uint AddSample(ReadOnlySpan pcm16, bool bLoop, uint loopPos) + { + int start = _samples.Length; + uint sampleIndex = (uint)start; + int numNewSamples = start + + pcm16.Length + + (bLoop ? 8 : 0) + + 46; + Array.Resize(ref _samples, numNewSamples); + + // Write wave + pcm16.CopyTo(_samples.AsSpan(start)); + start += pcm16.Length; + + // If looping is enabled, write 8 samples from the loop point + if (bLoop) + { + // In case (loopPos + i) is greater than the sample length + uint max = (uint)pcm16.Length - loopPos; + for (uint i = 0; i < 8; i++) + { + _samples[start++] = pcm16[(int)(loopPos + (i % max))]; + } + } + + // 46 empty samples are remaining at the end + + Size = (uint)_samples.Length * sizeof(short); + _sf2.UpdateSize(); + return sampleIndex; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.WriteInt16s(_samples); + } + + public override string ToString() + { + return $"Sample Data Chunk"; + } + } +} diff --git a/SoundFont2/Chunks/Sub Chunks/VersionSubChunk.cs b/SoundFont2/Chunks/Sub Chunks/VersionSubChunk.cs new file mode 100644 index 0000000..26eb515 --- /dev/null +++ b/SoundFont2/Chunks/Sub Chunks/VersionSubChunk.cs @@ -0,0 +1,31 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + public sealed class VersionSubChunk : SF2Chunk + { + public SF2VersionTag Version { get; set; } + + internal VersionSubChunk(SF2 inSf2, string subChunkName, SF2VersionTag version) : base(inSf2, subChunkName) + { + Size = SF2VersionTag.SIZE; + inSf2.UpdateSize(); + Version = version; + } + internal VersionSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + Version = new SF2VersionTag(reader); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + Version.Write(writer); + } + + public override string ToString() + { + return $"Version Chunk - Revision = {Version}"; + } + } +} diff --git a/SoundFont2/Enums/SF2Generator.cs b/SoundFont2/Enums/SF2Generator.cs new file mode 100644 index 0000000..3494f1a --- /dev/null +++ b/SoundFont2/Enums/SF2Generator.cs @@ -0,0 +1,60 @@ +namespace Kermalis.SoundFont2 +{ + /// SF2 v2.1 spec page 38 + public enum SF2Generator : ushort + { + StartAddrsOffset = 0, + EndAddrsOffset = 1, + StartloopAddrsOffset = 2, + EndloopAddrsOffset = 3, + StartAddrsCoarseOffset = 4, + ModLfoToPitch = 5, + VibLfoToPitch = 6, + ModEnvToPitch = 7, + InitialFilterFc = 8, + InitialFilterQ = 9, + ModLfoToFilterFc = 10, + ModEnvToFilterFc = 11, + EndAddrsCoarseOffset = 12, + ModLfoToVolume = 13, + ChorusEffectsSend = 15, + ReverbEffectsSend = 16, + Pan = 17, + DelayModLFO = 21, + FreqModLFO = 22, + DelayVibLFO = 23, + FreqVibLFO = 24, + DelayModEnv = 25, + AttackModEnv = 26, + HoldModEnv = 27, + DecayModEnv = 28, + SustainModEnv = 29, + ReleaseModEnv = 30, + KeynumToModEnvHold = 31, + KeynumToModEnvDecay = 32, + DelayVolEnv = 33, + AttackVolEnv = 34, + HoldVolEnv = 35, + DecayVolEnv = 36, + SustainVolEnv = 37, + ReleaseVolEnv = 38, + KeynumToVolEnvHold = 39, + KeynumToVolEnvDecay = 40, + Instrument = 41, + KeyRange = 43, + VelRange = 44, + StartloopAddrsCoarseOffset = 45, + Keynum = 46, + Velocity = 47, + InitialAttenuation = 48, + EndloopAddrsCoarseOffset = 50, + CoarseTune = 51, + FineTune = 52, + SampleID = 53, + SampleModes = 54, + ScaleTuning = 56, + ExclusiveClass = 57, + OverridingRootKey = 58, + EndOper = 60, + } +} diff --git a/SoundFont2/Enums/SF2Modulator.cs b/SoundFont2/Enums/SF2Modulator.cs new file mode 100644 index 0000000..26c9cfc --- /dev/null +++ b/SoundFont2/Enums/SF2Modulator.cs @@ -0,0 +1,14 @@ +namespace Kermalis.SoundFont2 +{ + /// SF2 v2.1 spec page 50 + public enum SF2Modulator : ushort + { + None = 0, + NoteOnVelocity = 1, + NoteOnKey = 2, + PolyPressure = 10, + ChnPressure = 13, + PitchWheel = 14, + PitchWheelSensivity = 16, + } +} diff --git a/SoundFont2/Enums/SF2SampleLink.cs b/SoundFont2/Enums/SF2SampleLink.cs new file mode 100644 index 0000000..3a72fc9 --- /dev/null +++ b/SoundFont2/Enums/SF2SampleLink.cs @@ -0,0 +1,15 @@ +namespace Kermalis.SoundFont2 +{ + /// SF2 v2.1 spec page 20 + public enum SF2SampleLink : ushort + { + MonoSample = 1, + RightSample = 2, + LeftSample = 4, + LinkedSample = 8, + RomMonoSample = 0x8001, + RomRightSample = 0x8002, + RomLeftSample = 0x8004, + RomLinkedSample = 0x8008, + } +} diff --git a/SoundFont2/Enums/SF2Transform.cs b/SoundFont2/Enums/SF2Transform.cs new file mode 100644 index 0000000..508969f --- /dev/null +++ b/SoundFont2/Enums/SF2Transform.cs @@ -0,0 +1,10 @@ +namespace Kermalis.SoundFont2 +{ + /// SF2 v2.1 spec page 52 + public enum SF2Transform : ushort + { + Linear = 0, + Concave = 1, + Convex = 2, + } +} diff --git a/SoundFont2/SF2.cs b/SoundFont2/SF2.cs index 1811dc1..0ede422 100644 --- a/SoundFont2/SF2.cs +++ b/SoundFont2/SF2.cs @@ -1,152 +1,157 @@ using Kermalis.EndianBinaryIO; +using System; using System.IO; namespace Kermalis.SoundFont2 { - public sealed class SF2 - { - private uint _size; - public InfoListChunk InfoChunk { get; } - public SdtaListChunk SoundChunk { get; } - public PdtaListChunk HydraChunk { get; } + public sealed class SF2 + { + private uint _size; + public InfoListChunk InfoChunk { get; } + public SdtaListChunk SoundChunk { get; } + public PdtaListChunk HydraChunk { get; } - /// For creating - public SF2() - { - InfoChunk = new InfoListChunk(this); - SoundChunk = new SdtaListChunk(this); - HydraChunk = new PdtaListChunk(this); - } + /// For creating + public SF2() + { + InfoChunk = new InfoListChunk(this); + SoundChunk = new SdtaListChunk(this); + HydraChunk = new PdtaListChunk(this); + } - /// For reading - public SF2(string path) - { - using (var reader = new EndianBinaryReader(File.Open(path, FileMode.Open))) - { - string str = reader.ReadString(4, false); - if (str != "RIFF") - { - throw new InvalidDataException("RIFF header was not found at the start of the file."); - } - _size = reader.ReadUInt32(); - str = reader.ReadString(4, false); - if (str != "sfbk") - { - throw new InvalidDataException("sfbk header was not found at the expected offset."); - } - InfoChunk = new InfoListChunk(this, reader); - SoundChunk = new SdtaListChunk(this, reader); - HydraChunk = new PdtaListChunk(this, reader); - } - } + /// For reading + public SF2(string path) + { + using (FileStream stream = File.Open(path, FileMode.Open)) + { + var reader = new EndianBinaryReader(stream, ascii: true); + string str = reader.ReadString_Count(4); + if (str != "RIFF") + { + throw new InvalidDataException("RIFF header was not found at the start of the file."); + } - public void Save(string path) - { - using (var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create))) - { - AddTerminals(); + _size = reader.ReadUInt32(); + str = reader.ReadString_Count(4); + if (str != "sfbk") + { + throw new InvalidDataException("sfbk header was not found at the expected offset."); + } - writer.Write("RIFF", 4); - writer.Write(_size); - writer.Write("sfbk", 4); + InfoChunk = new InfoListChunk(this, reader); + SoundChunk = new SdtaListChunk(this, reader); + HydraChunk = new PdtaListChunk(this, reader); + } + } - InfoChunk.Write(writer); - SoundChunk.Write(writer); - HydraChunk.Write(writer); - } - } + public void Save(string path) + { + using (FileStream stream = File.Open(path, FileMode.Create)) + { + var writer = new EndianBinaryWriter(stream, ascii: true); + AddTerminals(); + writer.WriteChars_Count("RIFF", 4); + writer.WriteUInt32(_size); + writer.WriteChars_Count("sfbk", 4); - /// Returns sample index - public uint AddSample(short[] pcm16, string name, bool bLoop, uint loopPos, uint sampleRate, byte originalKey, sbyte pitchCorrection) - { - uint start = SoundChunk.SMPLSubChunk.AddSample(pcm16, bLoop, loopPos); - // If the sample is looped the standard requires us to add the 8 bytes from the start of the loop to the end - uint end, loopEnd, loopStart; + InfoChunk.Write(writer); + SoundChunk.Write(writer); + HydraChunk.Write(writer); + } + } - uint len = (uint)pcm16.Length; - if (bLoop) - { - end = start + len + 8; - loopStart = start + loopPos; loopEnd = start + len; - } - else - { - end = start + len; - loopStart = 0; loopEnd = 0; - } - return AddSampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection); - } - /// Returns instrument index - public uint AddInstrument(string name) - { - return HydraChunk.INSTSubChunk.AddInstrument(new SF2Instrument(name, (ushort)HydraChunk.IBAGSubChunk.Count)); - } - public void AddInstrumentBag() - { - HydraChunk.IBAGSubChunk.AddBag(new SF2Bag(this, false)); - } - public void AddInstrumentModulator() - { - HydraChunk.IMODSubChunk.AddModulator(new SF2ModulatorList()); - } - public void AddInstrumentGenerator() - { - HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList()); - } - public void AddInstrumentGenerator(SF2Generator generator, SF2GeneratorAmount amount) - { - HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); - } - public void AddPreset(string name, ushort preset, ushort bank) - { - HydraChunk.PHDRSubChunk.AddPreset(new SF2PresetHeader(name, preset, bank, (ushort)HydraChunk.PBAGSubChunk.Count)); - } - public void AddPresetBag() - { - HydraChunk.PBAGSubChunk.AddBag(new SF2Bag(this, true)); - } - public void AddPresetModulator() - { - HydraChunk.PMODSubChunk.AddModulator(new SF2ModulatorList()); - } - public void AddPresetGenerator() - { - HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList()); - } - public void AddPresetGenerator(SF2Generator generator, SF2GeneratorAmount amount) - { - HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); - } + /// Returns sample index + public uint AddSample(ReadOnlySpan pcm16, string name, bool bLoop, uint loopPos, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + uint start = SoundChunk.SMPLSubChunk.AddSample(pcm16, bLoop, loopPos); + // If the sample is looped the standard requires us to add the 8 bytes from the start of the loop to the end + uint end, loopEnd, loopStart; - private uint AddSampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) - { - return HydraChunk.SHDRSubChunk.AddSample(new SF2SampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection)); - } - private void AddTerminals() - { - AddSampleHeader("EOS", 0, 0, 0, 0, 0, 0, 0); - AddInstrument("EOI"); - AddInstrumentBag(); - AddInstrumentGenerator(); - AddInstrumentModulator(); - AddPreset("EOP", 0xFF, 0xFF); - AddPresetBag(); - AddPresetGenerator(); - AddPresetModulator(); - } + uint len = (uint)pcm16.Length; + if (bLoop) + { + end = start + len + 8; + loopStart = start + loopPos; loopEnd = start + len; + } + else + { + end = start + len; + loopStart = 0; loopEnd = 0; + } - internal void UpdateSize() - { - if (InfoChunk == null || SoundChunk == null || HydraChunk == null) - { - return; - } - _size = 4 - + InfoChunk.UpdateSize() + 8 - + SoundChunk.UpdateSize() + 8 - + HydraChunk.UpdateSize() + 8; - } - } + return AddSampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection); + } + /// Returns instrument index + public uint AddInstrument(string name) + { + return HydraChunk.INSTSubChunk.AddInstrument(new SF2Instrument(name, (ushort)HydraChunk.IBAGSubChunk.Count)); + } + public void AddInstrumentBag() + { + HydraChunk.IBAGSubChunk.AddBag(new SF2Bag(this, false)); + } + public void AddInstrumentModulator() + { + HydraChunk.IMODSubChunk.AddModulator(new SF2ModulatorList()); + } + public void AddInstrumentGenerator() + { + HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList()); + } + public void AddInstrumentGenerator(SF2Generator generator, SF2GeneratorAmount amount) + { + HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); + } + public void AddPreset(string name, ushort preset, ushort bank) + { + HydraChunk.PHDRSubChunk.AddPreset(new SF2PresetHeader(name, preset, bank, (ushort)HydraChunk.PBAGSubChunk.Count)); + } + public void AddPresetBag() + { + HydraChunk.PBAGSubChunk.AddBag(new SF2Bag(this, true)); + } + public void AddPresetModulator() + { + HydraChunk.PMODSubChunk.AddModulator(new SF2ModulatorList()); + } + public void AddPresetGenerator() + { + HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList()); + } + public void AddPresetGenerator(SF2Generator generator, SF2GeneratorAmount amount) + { + HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); + } + + private uint AddSampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + return HydraChunk.SHDRSubChunk.AddSample(new SF2SampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection)); + } + private void AddTerminals() + { + AddSampleHeader("EOS", 0, 0, 0, 0, 0, 0, 0); + AddInstrument("EOI"); + AddInstrumentBag(); + AddInstrumentGenerator(); + AddInstrumentModulator(); + AddPreset("EOP", 0xFF, 0xFF); + AddPresetBag(); + AddPresetGenerator(); + AddPresetModulator(); + } + + internal void UpdateSize() + { + if (InfoChunk is null || SoundChunk is null || HydraChunk is null) + { + return; // The object may not be finished constructing yet + } + _size = 4 + + InfoChunk.UpdateSize() + 8 + + SoundChunk.UpdateSize() + 8 + + HydraChunk.UpdateSize() + 8; + } + } } diff --git a/SoundFont2/SF2Chunks.cs b/SoundFont2/SF2Chunks.cs deleted file mode 100644 index 9601304..0000000 --- a/SoundFont2/SF2Chunks.cs +++ /dev/null @@ -1,1105 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections.Generic; - -namespace Kermalis.SoundFont2 -{ - public class SF2Chunk - { - protected readonly SF2 _sf2; - - /// Length 4 - public string ChunkName { get; } - /// Size in bytes - public uint Size { get; protected set; } - - protected SF2Chunk(SF2 inSf2, string name) - { - _sf2 = inSf2; - ChunkName = name; - } - protected SF2Chunk(SF2 inSf2, EndianBinaryReader reader) - { - _sf2 = inSf2; - ChunkName = reader.ReadString(4, false); - Size = reader.ReadUInt32(); - } - - internal virtual void Write(EndianBinaryWriter writer) - { - writer.Write(ChunkName, 4); - writer.Write(Size); - } - } - - public abstract class SF2ListChunk : SF2Chunk - { - ///Length 4 - public string ListChunkName { get; } - - protected SF2ListChunk(SF2 inSf2, string name) : base(inSf2, "LIST") - { - ListChunkName = name; - Size = 4; - } - protected SF2ListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - ListChunkName = reader.ReadString(4, false); - } - - internal abstract uint UpdateSize(); - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(ListChunkName, 4); - } - } - - public sealed class SF2PresetHeader - { - public const uint Size = 38; - - /// Length 20 - public string PresetName { get; set; } - public ushort Preset { get; set; } - public ushort Bank { get; set; } - public ushort PresetBagIndex { get; set; } - // Reserved for future implementations - private readonly uint _library; - private readonly uint _genre; - private readonly uint _morphology; - - internal SF2PresetHeader(string name, ushort preset, ushort bank, ushort index) - { - PresetName = name; - Preset = preset; - Bank = bank; - PresetBagIndex = index; - } - internal SF2PresetHeader(EndianBinaryReader reader) - { - PresetName = reader.ReadString(20, true); - Preset = reader.ReadUInt16(); - Bank = reader.ReadUInt16(); - PresetBagIndex = reader.ReadUInt16(); - _library = reader.ReadUInt32(); - _genre = reader.ReadUInt32(); - _morphology = reader.ReadUInt32(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(PresetName, 20); - writer.Write(Preset); - writer.Write(Bank); - writer.Write(PresetBagIndex); - writer.Write(_library); - writer.Write(_genre); - writer.Write(_morphology); - } - - public override string ToString() - { - return $"Preset Header - Bank = {Bank}" + - $",\nPreset = {Preset}" + - $",\nName = \"{PresetName}\""; - } - } - - /// Covers sfPresetBag and sfInstBag - public sealed class SF2Bag - { - public const uint Size = 4; - - /// Index in list of generators - public ushort GeneratorIndex { get; set; } - /// Index in list of modulators - public ushort ModulatorIndex { get; set; } - - internal SF2Bag(SF2 inSf2, bool isPresetBag) - { - if (isPresetBag) - { - GeneratorIndex = (ushort)inSf2.HydraChunk.PGENSubChunk.Count; - ModulatorIndex = (ushort)inSf2.HydraChunk.PMODSubChunk.Count; - } - else - { - GeneratorIndex = (ushort)inSf2.HydraChunk.IGENSubChunk.Count; - ModulatorIndex = (ushort)inSf2.HydraChunk.IMODSubChunk.Count; - } - } - internal SF2Bag(EndianBinaryReader reader) - { - GeneratorIndex = reader.ReadUInt16(); - ModulatorIndex = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(GeneratorIndex); - writer.Write(ModulatorIndex); - } - - public override string ToString() - { - return $"Bag - Generator index = {GeneratorIndex}" + - $",\nModulator index = {ModulatorIndex}"; - } - } - - /// Covers sfModList and sfInstModList - public sealed class SF2ModulatorList - { - public const uint Size = 10; - - public SF2Modulator ModulatorSource { get; set; } - public SF2Generator ModulatorDestination { get; set; } - public short ModulatorAmount { get; set; } - public SF2Modulator ModulatorAmountSource { get; set; } - public SF2Transform ModulatorTransform { get; set; } - - internal SF2ModulatorList() { } - internal SF2ModulatorList(EndianBinaryReader reader) - { - ModulatorSource = reader.ReadEnum(); - ModulatorDestination = reader.ReadEnum(); - ModulatorAmount = reader.ReadInt16(); - ModulatorAmountSource = reader.ReadEnum(); - ModulatorTransform = reader.ReadEnum(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(ModulatorSource); - writer.Write(ModulatorDestination); - writer.Write(ModulatorAmount); - writer.Write(ModulatorAmountSource); - writer.Write(ModulatorTransform); - } - - public override string ToString() - { - return $"Modulator List - Modulator source = {ModulatorSource}" + - $",\nModulator destination = {ModulatorDestination}" + - $",\nModulator amount = {ModulatorAmount}" + - $",\nModulator amount source = {ModulatorAmountSource}" + - $",\nModulator transform = {ModulatorTransform}"; - } - } - - public sealed class SF2GeneratorList - { - public const uint Size = 4; - - public SF2Generator Generator { get; set; } - public SF2GeneratorAmount GeneratorAmount { get; set; } - - internal SF2GeneratorList() { } - internal SF2GeneratorList(SF2Generator generator, SF2GeneratorAmount amount) - { - Generator = generator; - GeneratorAmount = amount; - } - internal SF2GeneratorList(EndianBinaryReader reader) - { - Generator = reader.ReadEnum(); - GeneratorAmount = new SF2GeneratorAmount { Amount = reader.ReadInt16() }; - } - - public void Write(EndianBinaryWriter writer) - { - writer.Write(Generator); - writer.Write(GeneratorAmount.Amount); - } - - public override string ToString() - { - return $"Generator List - Generator = {Generator}" + - $",\nGenerator amount = \"{GeneratorAmount}\""; - } - } - - public sealed class SF2Instrument - { - public const uint Size = 22; - - /// Length 20 - public string InstrumentName { get; set; } - public ushort InstrumentBagIndex { get; set; } - - internal SF2Instrument(string name, ushort index) - { - InstrumentName = name; - InstrumentBagIndex = index; - } - internal SF2Instrument(EndianBinaryReader reader) - { - InstrumentName = reader.ReadString(20, true); - InstrumentBagIndex = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(InstrumentName, 20); - writer.Write(InstrumentBagIndex); - } - - public override string ToString() - { - return $"Instrument - Name = \"{InstrumentName}\""; - } - } - - public sealed class SF2SampleHeader - { - public const uint Size = 46; - - /// Length 20 - public string SampleName { get; set; } - public uint Start { get; set; } - public uint End { get; set; } - public uint LoopStart { get; set; } - public uint LoopEnd { get; set; } - public uint SampleRate { get; set; } - public byte OriginalKey { get; set; } - public sbyte PitchCorrection { get; set; } - public ushort SampleLink { get; set; } - public SF2SampleLink SampleType { get; set; } - - internal SF2SampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) - { - SampleName = name; - Start = start; - End = end; - LoopStart = loopStart; - LoopEnd = loopEnd; - SampleRate = sampleRate; - OriginalKey = originalKey; - PitchCorrection = pitchCorrection; - SampleType = SF2SampleLink.MonoSample; - } - internal SF2SampleHeader(EndianBinaryReader reader) - { - SampleName = reader.ReadString(20, true); - Start = reader.ReadUInt32(); - End = reader.ReadUInt32(); - LoopStart = reader.ReadUInt32(); - LoopEnd = reader.ReadUInt32(); - SampleRate = reader.ReadUInt32(); - OriginalKey = reader.ReadByte(); - PitchCorrection = reader.ReadSByte(); - SampleLink = reader.ReadUInt16(); - SampleType = reader.ReadEnum(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(SampleName, 20); - writer.Write(Start); - writer.Write(End); - writer.Write(LoopStart); - writer.Write(LoopEnd); - writer.Write(SampleRate); - writer.Write(OriginalKey); - writer.Write(PitchCorrection); - writer.Write(SampleLink); - writer.Write(SampleType); - } - - public override string ToString() - { - return $"Sample - Name = \"{SampleName}\"" + - $",\nType = {SampleType}"; - } - } - - #region Sub-Chunks - - public sealed class VersionSubChunk : SF2Chunk - { - public SF2VersionTag Version { get; set; } - - internal VersionSubChunk(SF2 inSf2, string subChunkName) : base(inSf2, subChunkName) - { - Size = SF2VersionTag.Size; - inSf2.UpdateSize(); - } - internal VersionSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - Version = new SF2VersionTag(reader); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - Version.Write(writer); - } - - public override string ToString() - { - return $"Version Chunk - Revision = {Version}"; - } - } - - public sealed class HeaderSubChunk : SF2Chunk - { - public int MaxSize { get; } - private int _fieldTargetLength; - private string _field; - /// Length - public string Field - { - get => _field; - set - { - if (value.Length >= MaxSize) // Input too long; cut it down - { - _fieldTargetLength = MaxSize; - } - else if (value.Length % 2 == 0) // Even amount of characters - { - _fieldTargetLength = value.Length + 2; // Add two null-terminators to keep the byte count even - } - else // Odd amount of characters - { - _fieldTargetLength = value.Length + 1; // Add one null-terminator since that would make byte the count even - } - _field = value; - Size = (uint)_fieldTargetLength; - _sf2.UpdateSize(); - } - } - - internal HeaderSubChunk(SF2 inSf2, string subChunkName, int maxSize = 0x100) : base(inSf2, subChunkName) - { - MaxSize = maxSize; - } - internal HeaderSubChunk(SF2 inSf2, EndianBinaryReader reader, int maxSize = 0x100) : base(inSf2, reader) - { - MaxSize = maxSize; - _field = reader.ReadString((int)Size, true); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(_field, _fieldTargetLength); - } - - public override string ToString() - { - return $"Header Chunk - Name = \"{ChunkName}\"" + - $",\nField Max Size = {MaxSize}" + - $",\nField = \"{Field}\""; - } - } - - public sealed class SMPLSubChunk : SF2Chunk - { - private readonly List _samples = new List(); // Block of sample data - - internal SMPLSubChunk(SF2 inSf2) : base(inSf2, "smpl") { } - internal SMPLSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / sizeof(short); i++) - { - _samples.Add(reader.ReadInt16()); - } - } - - // Returns index of the start of the sample - internal uint AddSample(short[] pcm16, bool bLoop, uint loopPos) - { - uint start = (uint)_samples.Count; - - // Write wave - _samples.AddRange(pcm16); - - // If looping is enabled, write 8 samples from the loop point - if (bLoop) - { - // In case (loopPos + i) is greater than the sample length - uint max = (uint)pcm16.Length - loopPos; - for (uint i = 0; i < 8; i++) - { - _samples.Add(pcm16[loopPos + (i % max)]); - } - } - - // Write 46 empty samples - _samples.AddRange(new short[46]); - - Size = (uint)_samples.Count * sizeof(short); - _sf2.UpdateSize(); - return start; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - foreach (short s in _samples) - { - writer.Write(s); - } - } - - public override string ToString() - { - return $"Sample Data Chunk"; - } - } - - public sealed class PHDRSubChunk : SF2Chunk - { - private readonly List _presets = new List(); - public uint Count => (uint)_presets.Count; - - internal PHDRSubChunk(SF2 inSf2) : base(inSf2, "phdr") { } - internal PHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2PresetHeader.Size; i++) - { - _presets.Add(new SF2PresetHeader(reader)); - } - } - - internal void AddPreset(SF2PresetHeader preset) - { - _presets.Add(preset); - Size = Count * SF2PresetHeader.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _presets[i].Write(writer); - } - } - - public override string ToString() - { - return $"Preset Header Chunk - Preset count = {Count}"; - } - } - - public sealed class INSTSubChunk : SF2Chunk - { - private readonly List _instruments = new List(); - public uint Count => (uint)_instruments.Count; - - internal INSTSubChunk(SF2 inSf2) : base(inSf2, "inst") { } - internal INSTSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2Instrument.Size; i++) - { - _instruments.Add(new SF2Instrument(reader)); - } - } - - internal uint AddInstrument(SF2Instrument instrument) - { - _instruments.Add(instrument); - Size = Count * SF2Instrument.Size; - _sf2.UpdateSize(); - return Count - 1; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _instruments[i].Write(writer); - } - } - - public override string ToString() - { - return $"Instrument Chunk - Instrument count = {Count}"; - } - } - - public sealed class BAGSubChunk : SF2Chunk - { - private readonly List _bags = new List(); - public uint Count => (uint)_bags.Count; - - internal BAGSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pbag" : "ibag") { } - internal BAGSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2Bag.Size; i++) - { - _bags.Add(new SF2Bag(reader)); - } - } - - internal void AddBag(SF2Bag bag) - { - _bags.Add(bag); - Size = Count * SF2Bag.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _bags[i].Write(writer); - } - } - - public override string ToString() - { - return $"Bag Chunk - Name = \"{ChunkName}\"" + - $",\nBag count = {Count}"; - } - } - - public sealed class MODSubChunk : SF2Chunk - { - private readonly List _modulators = new List(); - public uint Count => (uint)_modulators.Count; - - internal MODSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pmod" : "imod") { } - internal MODSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2ModulatorList.Size; i++) - { - _modulators.Add(new SF2ModulatorList(reader)); - } - } - - internal void AddModulator(SF2ModulatorList modulator) - { - _modulators.Add(modulator); - Size = Count * SF2ModulatorList.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _modulators[i].Write(writer); - } - } - - public override string ToString() - { - return $"Modulator Chunk - Name = \"{ChunkName}\"" + - $",\nModulator count = {Count}"; - } - } - - public sealed class GENSubChunk : SF2Chunk - { - private readonly List _generators = new List(); - public uint Count => (uint)_generators.Count; - - internal GENSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pgen" : "igen") { } - internal GENSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2GeneratorList.Size; i++) - { - _generators.Add(new SF2GeneratorList(reader)); - } - } - - internal void AddGenerator(SF2GeneratorList generator) - { - _generators.Add(generator); - Size = Count * SF2GeneratorList.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _generators[i].Write(writer); - } - } - - public override string ToString() - { - return $"Generator Chunk - Name = \"{ChunkName}\"" + - $",\nGenerator count = {Count}"; - } - } - - public sealed class SHDRSubChunk : SF2Chunk - { - private readonly List _samples = new List(); - public uint Count => (uint)_samples.Count; - - internal SHDRSubChunk(SF2 inSf2) : base(inSf2, "shdr") { } - internal SHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2SampleHeader.Size; i++) - { - _samples.Add(new SF2SampleHeader(reader)); - } - } - - internal uint AddSample(SF2SampleHeader sample) - { - _samples.Add(sample); - Size = Count * SF2SampleHeader.Size; - _sf2.UpdateSize(); - return Count - 1; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _samples[i].Write(writer); - } - } - - public override string ToString() - { - return $"Sample Header Chunk - Sample header count = {Count}"; - } - } - - #endregion - - #region Main Chunks - - public sealed class InfoListChunk : SF2ListChunk - { - private readonly List _subChunks = new List(); - private const string DefaultEngine = "EMU8000"; - public string Engine - { - get - { - if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = DefaultEngine }); - return DefaultEngine; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = value }); - } - } - } - - private const string DefaultBank = "General MIDI"; - public string Bank - { - get - { - if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = DefaultBank }); - return DefaultBank; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = value }); - } - } - } - public string ROM - { - get - { - if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "irom") { Field = value }); - } - } - } - public SF2VersionTag ROMVersion - { - get - { - if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) - { - return chunk.Version; - } - else - { - return null; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) - { - chunk.Version = value; - } - else - { - _subChunks.Add(new VersionSubChunk(_sf2, "iver") { Version = value }); - } - } - } - public string Date - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ICRD") { Field = value }); - } - } - } - public string Designer - { - get - { - if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "IENG") { Field = value }); - } - } - } - public string Products - { - get - { - if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "IPRD") { Field = value }); - } - } - } - public string Copyright - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk icop) - { - return icop.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ICOP") { Field = value }); - } - } - } - - private const int CommentMaxSize = 0x10000; - public string Comment - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ICMT", maxSize: CommentMaxSize) { Field = value }); - } - } - } - public string Tools - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ISFT") { Field = value }); - } - } - } - - internal InfoListChunk(SF2 inSf2) : base(inSf2, "INFO") - { - // Mandatory sub-chunks - _subChunks.Add(new VersionSubChunk(inSf2, "ifil") { Version = new SF2VersionTag(2, 1) }); - _subChunks.Add(new HeaderSubChunk(inSf2, "isng") { Field = DefaultEngine }); - _subChunks.Add(new HeaderSubChunk(inSf2, "INAM") { Field = DefaultBank }); - inSf2.UpdateSize(); - } - internal InfoListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - long startOffset = reader.BaseStream.Position; - while (reader.BaseStream.Position < startOffset + Size - 4) // The 4 represents the INFO that was already read - { - // Peek 4 chars for the chunk name - string name = reader.ReadString(4, false); - reader.BaseStream.Position -= 4; - switch (name) - { - case "ICMT": _subChunks.Add(new HeaderSubChunk(inSf2, reader, maxSize: CommentMaxSize)); break; - case "ifil": - case "iver": _subChunks.Add(new VersionSubChunk(inSf2, reader)); break; - case "isng": - case "INAM": - case "ICRD": - case "IENG": - case "IPRD": - case "ICOP": - case "ISFT": - case "irom": _subChunks.Add(new HeaderSubChunk(inSf2, reader)); break; - default: throw new NotSupportedException($"Unsupported chunk name at 0x{reader.BaseStream.Position:X}: \"{name}\""); - } - } - } - - internal override uint UpdateSize() - { - Size = 4; - foreach (SF2Chunk sub in _subChunks) - { - Size += sub.Size + 8; - } - - return Size; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - foreach (SF2Chunk sub in _subChunks) - { - sub.Write(writer); - } - } - - public override string ToString() - { - return $"Info List Chunk - Sub-chunk count = {_subChunks.Count}"; - } - } - - public sealed class SdtaListChunk : SF2ListChunk - { - public SMPLSubChunk SMPLSubChunk { get; } - - internal SdtaListChunk(SF2 inSf2) : base(inSf2, "sdta") - { - SMPLSubChunk = new SMPLSubChunk(inSf2); - inSf2.UpdateSize(); - } - internal SdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - SMPLSubChunk = new SMPLSubChunk(inSf2, reader); - } - - internal override uint UpdateSize() - { - return Size = 4 - + SMPLSubChunk.Size + 8; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - SMPLSubChunk.Write(writer); - } - - public override string ToString() - { - return $"Sample Data List Chunk"; - } - } - - public sealed class PdtaListChunk : SF2ListChunk - { - public PHDRSubChunk PHDRSubChunk { get; } - public BAGSubChunk PBAGSubChunk { get; } - public MODSubChunk PMODSubChunk { get; } - public GENSubChunk PGENSubChunk { get; } - public INSTSubChunk INSTSubChunk { get; } - public BAGSubChunk IBAGSubChunk { get; } - public MODSubChunk IMODSubChunk { get; } - public GENSubChunk IGENSubChunk { get; } - public SHDRSubChunk SHDRSubChunk { get; } - - internal PdtaListChunk(SF2 inSf2) : base(inSf2, "pdta") - { - PHDRSubChunk = new PHDRSubChunk(inSf2); - PBAGSubChunk = new BAGSubChunk(inSf2, true); - PMODSubChunk = new MODSubChunk(inSf2, true); - PGENSubChunk = new GENSubChunk(inSf2, true); - INSTSubChunk = new INSTSubChunk(inSf2); - IBAGSubChunk = new BAGSubChunk(inSf2, false); - IMODSubChunk = new MODSubChunk(inSf2, false); - IGENSubChunk = new GENSubChunk(inSf2, false); - SHDRSubChunk = new SHDRSubChunk(inSf2); - inSf2.UpdateSize(); - } - internal PdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - PHDRSubChunk = new PHDRSubChunk(inSf2, reader); - PBAGSubChunk = new BAGSubChunk(inSf2, reader); - PMODSubChunk = new MODSubChunk(inSf2, reader); - PGENSubChunk = new GENSubChunk(inSf2, reader); - INSTSubChunk = new INSTSubChunk(inSf2, reader); - IBAGSubChunk = new BAGSubChunk(inSf2, reader); - IMODSubChunk = new MODSubChunk(inSf2, reader); - IGENSubChunk = new GENSubChunk(inSf2, reader); - SHDRSubChunk = new SHDRSubChunk(inSf2, reader); - } - - internal override uint UpdateSize() - { - return Size = 4 - + PHDRSubChunk.Size + 8 - + PBAGSubChunk.Size + 8 - + PMODSubChunk.Size + 8 - + PGENSubChunk.Size + 8 - + INSTSubChunk.Size + 8 - + IBAGSubChunk.Size + 8 - + IMODSubChunk.Size + 8 - + IGENSubChunk.Size + 8 - + SHDRSubChunk.Size + 8; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - PHDRSubChunk.Write(writer); - PBAGSubChunk.Write(writer); - PMODSubChunk.Write(writer); - PGENSubChunk.Write(writer); - INSTSubChunk.Write(writer); - IBAGSubChunk.Write(writer); - IMODSubChunk.Write(writer); - IGENSubChunk.Write(writer); - SHDRSubChunk.Write(writer); - } - - public override string ToString() - { - return $"Hydra List Chunk"; - } - } - - #endregion -} diff --git a/SoundFont2/SF2Types.cs b/SoundFont2/SF2Types.cs deleted file mode 100644 index b297d80..0000000 --- a/SoundFont2/SF2Types.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.Runtime.InteropServices; - -namespace Kermalis.SoundFont2 -{ - /// SF2 v2.1 spec page 16 - public sealed class SF2VersionTag - { - public const uint Size = 4; - - public ushort Major { get; } - public ushort Minor { get; } - - public SF2VersionTag(ushort major, ushort minor) - { - Major = major; - Minor = minor; - } - internal SF2VersionTag(EndianBinaryReader reader) - { - Major = reader.ReadUInt16(); - Minor = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(Major); - writer.Write(Minor); - } - - public override string ToString() - { - return $"v{Major}.{Minor}"; - } - } - - /// SF2 spec v2.1 page 19 - Two bytes that can handle either two 8-bit values or a single 16-bit value - [StructLayout(LayoutKind.Explicit)] - public struct SF2GeneratorAmount - { - [FieldOffset(0)] public byte LowByte; - [FieldOffset(1)] public byte HighByte; - [FieldOffset(0)] public short Amount; - [FieldOffset(0)] public ushort UAmount; - - public override string ToString() - { - return $"BLo = {LowByte}, BHi = {HighByte}, Sh = {Amount}, U = {UAmount}"; - } - } - - /// SF2 v2.1 spec page 20 - public enum SF2SampleLink : ushort - { - MonoSample = 1, - RightSample = 2, - LeftSample = 4, - LinkedSample = 8, - RomMonoSample = 0x8001, - RomRightSample = 0x8002, - RomLeftSample = 0x8004, - RomLinkedSample = 0x8008 - } - - /// SF2 v2.1 spec page 38 - public enum SF2Generator : ushort - { - StartAddrsOffset = 0, - EndAddrsOffset = 1, - StartloopAddrsOffset = 2, - EndloopAddrsOffset = 3, - StartAddrsCoarseOffset = 4, - ModLfoToPitch = 5, - VibLfoToPitch = 6, - ModEnvToPitch = 7, - InitialFilterFc = 8, - InitialFilterQ = 9, - ModLfoToFilterFc = 10, - ModEnvToFilterFc = 11, - EndAddrsCoarseOffset = 12, - ModLfoToVolume = 13, - ChorusEffectsSend = 15, - ReverbEffectsSend = 16, - Pan = 17, - DelayModLFO = 21, - FreqModLFO = 22, - DelayVibLFO = 23, - FreqVibLFO = 24, - DelayModEnv = 25, - AttackModEnv = 26, - HoldModEnv = 27, - DecayModEnv = 28, - SustainModEnv = 29, - ReleaseModEnv = 30, - KeynumToModEnvHold = 31, - KeynumToModEnvDecay = 32, - DelayVolEnv = 33, - AttackVolEnv = 34, - HoldVolEnv = 35, - DecayVolEnv = 36, - SustainVolEnv = 37, - ReleaseVolEnv = 38, - KeynumToVolEnvHold = 39, - KeynumToVolEnvDecay = 40, - Instrument = 41, - KeyRange = 43, - VelRange = 44, - StartloopAddrsCoarseOffset = 45, - Keynum = 46, - Velocity = 47, - InitialAttenuation = 48, - EndloopAddrsCoarseOffset = 50, - CoarseTune = 51, - FineTune = 52, - SampleID = 53, - SampleModes = 54, - ScaleTuning = 56, - ExclusiveClass = 57, - OverridingRootKey = 58, - EndOper = 60 - } - - /// SF2 v2.1 spec page 50 - public enum SF2Modulator : ushort - { - None = 0, - NoteOnVelocity = 1, - NoteOnKey = 2, - PolyPressure = 10, - ChnPressure = 13, - PitchWheel = 14, - PitchWheelSensivity = 16 - } - - /// SF2 v2.1 spec page 52 - public enum SF2Transform : ushort - { - Linear = 0, - Concave = 1, - Convex = 2 - } -} diff --git a/SoundFont2/SoundFont2.csproj b/SoundFont2/SoundFont2.csproj index 3ed4200..955dcc7 100644 --- a/SoundFont2/SoundFont2.csproj +++ b/SoundFont2/SoundFont2.csproj @@ -1,26 +1,28 @@ - - netstandard2.0 - Release - Kermalis - - SoundFont2 - SoundFont2 - SoundFont2 - Kermalis.SoundFont2 - 1.0.0.0 - + + net6.0 + Library + latest + Kermalis.SoundFont2 + enable + ..\Build - - ..\Build - Auto - none - false - + Kermalis + Kermalis + SoundFont2 + SoundFont2 + SoundFont2 + 1.1.0 - - - + Release + Auto + none + false + + + + + diff --git a/SoundFont2/Structs/SF2GeneratorAmount.cs b/SoundFont2/Structs/SF2GeneratorAmount.cs new file mode 100644 index 0000000..c6974c7 --- /dev/null +++ b/SoundFont2/Structs/SF2GeneratorAmount.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace Kermalis.SoundFont2 +{ + /// SF2 spec v2.1 page 19 - Two bytes that can handle either two 8-bit values or a single 16-bit value + [StructLayout(LayoutKind.Explicit)] + public struct SF2GeneratorAmount + { + [FieldOffset(0)] public byte LowByte; + [FieldOffset(1)] public byte HighByte; + [FieldOffset(0)] public short Amount; + [FieldOffset(0)] public ushort UAmount; + + public override string ToString() + { + return $"BLo = {LowByte}, BHi = {HighByte}, Sh = {Amount}, U = {UAmount}"; + } + } +} diff --git a/SoundFont2/Structs/SF2VersionTag.cs b/SoundFont2/Structs/SF2VersionTag.cs new file mode 100644 index 0000000..b4d5064 --- /dev/null +++ b/SoundFont2/Structs/SF2VersionTag.cs @@ -0,0 +1,35 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.SoundFont2 +{ + /// SF2 v2.1 spec page 16 + public sealed class SF2VersionTag + { + public const uint SIZE = 4; + + public ushort Major { get; } + public ushort Minor { get; } + + public SF2VersionTag(ushort major, ushort minor) + { + Major = major; + Minor = minor; + } + internal SF2VersionTag(EndianBinaryReader reader) + { + Major = reader.ReadUInt16(); + Minor = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.WriteUInt16(Major); + writer.WriteUInt16(Minor); + } + + public override string ToString() + { + return $"v{Major}.{Minor}"; + } + } +}