|
| 1 | +using Kermalis.DLS2; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Diagnostics; |
| 4 | + |
| 5 | +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream |
| 6 | +{ |
| 7 | + internal sealed class SoundFontSaver_DLS |
| 8 | + { |
| 9 | + // Since every key will use the same articulation data, just store one instance |
| 10 | + private static readonly Level2ArticulatorChunk _art2 = new Level2ArticulatorChunk() |
| 11 | + { |
| 12 | + new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.LFOFrequency, Scale = 2786 }, |
| 13 | + new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.VIBFrequency, Scale = 2786 }, |
| 14 | + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.KeyNumber, Destination = Level2ArticulatorDestination.Pitch }, |
| 15 | + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.Modulation_CC1, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, |
| 16 | + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.ChannelPressure, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, |
| 17 | + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Pan_CC10, Destination = Level2ArticulatorDestination.Pan, BipolarSource = true, Scale = 0xFE0000 }, |
| 18 | + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.ChorusSend_CC91, Destination = Level2ArticulatorDestination.Reverb, Scale = 0xC80000 }, |
| 19 | + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Reverb_SendCC93, Destination = Level2ArticulatorDestination.Chorus, Scale = 0xC80000 } |
| 20 | + }; |
| 21 | + |
| 22 | + public static void Save(Config config, string path) |
| 23 | + { |
| 24 | + var dls = new DLS(); |
| 25 | + AddInfo(config, dls); |
| 26 | + Dictionary<int, (WaveSampleChunk, int)> sampleDict = AddSamples(config, dls); |
| 27 | + AddInstruments(config, dls, sampleDict); |
| 28 | + dls.Save(path); |
| 29 | + } |
| 30 | + |
| 31 | + private static void AddInfo(Config config, DLS dls) |
| 32 | + { |
| 33 | + var info = new ListChunk("INFO"); |
| 34 | + dls.Add(info); |
| 35 | + info.Add(new InfoSubChunk("INAM", config.Name)); |
| 36 | + //info.Add(new InfoSubChunk("ICOP", config.Creator)); |
| 37 | + info.Add(new InfoSubChunk("IENG", "Kermalis")); |
| 38 | + info.Add(new InfoSubChunk("ISFT", Util.Utils.ProgramName)); |
| 39 | + } |
| 40 | + |
| 41 | + private static Dictionary<int, (WaveSampleChunk, int)> AddSamples(Config config, DLS dls) |
| 42 | + { |
| 43 | + ListChunk waves = dls.WavePool; |
| 44 | + var sampleDict = new Dictionary<int, (WaveSampleChunk, int)>((int)config.SampleTableSize); |
| 45 | + for (int i = 0; i < config.SampleTableSize; i++) |
| 46 | + { |
| 47 | + int ofs = config.Reader.ReadInt32(config.SampleTableOffset + (i * 4)); |
| 48 | + if (ofs == 0) |
| 49 | + { |
| 50 | + continue; // Skip null samples |
| 51 | + } |
| 52 | + |
| 53 | + ofs += config.SampleTableOffset; |
| 54 | + SampleHeader sh = config.Reader.ReadObject<SampleHeader>(ofs); |
| 55 | + |
| 56 | + // Create format chunk |
| 57 | + var fmt = new FormatChunk(WaveFormat.PCM); |
| 58 | + fmt.WaveInfo.Channels = 1; |
| 59 | + fmt.WaveInfo.SamplesPerSec = (uint)(sh.SampleRate >> 10); |
| 60 | + fmt.WaveInfo.AvgBytesPerSec = fmt.WaveInfo.SamplesPerSec; |
| 61 | + fmt.WaveInfo.BlockAlign = 1; |
| 62 | + fmt.FormatInfo.BitsPerSample = 8; |
| 63 | + // Create wave sample chunk and add loop if there is one |
| 64 | + var wsmp = new WaveSampleChunk() |
| 65 | + { |
| 66 | + UnityNote = 60, |
| 67 | + Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression |
| 68 | + }; |
| 69 | + if (sh.DoesLoop == 0x40000000) |
| 70 | + { |
| 71 | + wsmp.Loop = new WaveSampleLoop |
| 72 | + { |
| 73 | + LoopStart = (uint)sh.LoopOffset, |
| 74 | + LoopLength = (uint)(sh.Length - sh.LoopOffset), |
| 75 | + LoopType = LoopType.Forward |
| 76 | + }; |
| 77 | + } |
| 78 | + // Get PCM sample |
| 79 | + byte[] pcm = new byte[sh.Length]; |
| 80 | + System.Array.Copy(config.ROM, ofs + 0x10, pcm, 0, sh.Length); |
| 81 | + |
| 82 | + // Add |
| 83 | + int dlsIndex = waves.Count; |
| 84 | + waves.Add(new ListChunk("wave") |
| 85 | + { |
| 86 | + fmt, |
| 87 | + wsmp, |
| 88 | + new DataChunk(pcm), |
| 89 | + new ListChunk("INFO") |
| 90 | + { |
| 91 | + new InfoSubChunk("INAM", $"Sample {i}") |
| 92 | + } |
| 93 | + }); |
| 94 | + sampleDict.Add(i, (wsmp, dlsIndex)); |
| 95 | + } |
| 96 | + return sampleDict; |
| 97 | + } |
| 98 | + |
| 99 | + private static void AddInstruments(Config config, DLS dls, Dictionary<int, (WaveSampleChunk, int)> sampleDict) |
| 100 | + { |
| 101 | + ListChunk lins = dls.InstrumentList; |
| 102 | + for (int v = 0; v < 256; v++) |
| 103 | + { |
| 104 | + short off = config.Reader.ReadInt16(config.VoiceTableOffset + (v * 2)); |
| 105 | + short nextOff = config.Reader.ReadInt16(config.VoiceTableOffset + ((v + 1) * 2)); |
| 106 | + int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes |
| 107 | + if (numEntries == 0) |
| 108 | + { |
| 109 | + continue; // Skip empty entries |
| 110 | + } |
| 111 | + |
| 112 | + var ins = new ListChunk("ins "); |
| 113 | + ins.Add(new InstrumentHeaderChunk |
| 114 | + { |
| 115 | + NumRegions = (uint)numEntries, |
| 116 | + Locale = new MIDILocale(0, (byte)(v / 128), false, (byte)(v % 128)) |
| 117 | + }); |
| 118 | + var lrgn = new ListChunk("lrgn"); |
| 119 | + ins.Add(lrgn); |
| 120 | + ins.Add(new ListChunk("INFO") |
| 121 | + { |
| 122 | + new InfoSubChunk("INAM", $"Instrument {v}") |
| 123 | + }); |
| 124 | + lins.Add(ins); |
| 125 | + for (int e = 0; e < numEntries; e++) |
| 126 | + { |
| 127 | + VoiceEntry entry = config.Reader.ReadObject<VoiceEntry>(config.VoiceTableOffset + off + (e * 8)); |
| 128 | + // Sample |
| 129 | + if (entry.Sample >= config.SampleTableSize) |
| 130 | + { |
| 131 | + Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); |
| 132 | + continue; |
| 133 | + } |
| 134 | + if (!sampleDict.TryGetValue(entry.Sample, out (WaveSampleChunk, int) value)) |
| 135 | + { |
| 136 | + Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); |
| 137 | + continue; |
| 138 | + } |
| 139 | + void Add(ushort low, ushort high, ushort baseKey) |
| 140 | + { |
| 141 | + var rgnh = new RegionHeaderChunk(); |
| 142 | + rgnh.KeyRange.Low = low; |
| 143 | + rgnh.KeyRange.High = high; |
| 144 | + lrgn.Add(new ListChunk("rgn2") |
| 145 | + { |
| 146 | + rgnh, |
| 147 | + new WaveSampleChunk() |
| 148 | + { |
| 149 | + UnityNote = baseKey, |
| 150 | + Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, |
| 151 | + Loop = value.Item1.Loop |
| 152 | + }, |
| 153 | + new WaveLinkChunk() |
| 154 | + { |
| 155 | + Channels = WaveLinkChannels.Left, |
| 156 | + TableIndex = (uint)value.Item2 |
| 157 | + }, |
| 158 | + new ListChunk("lar2") |
| 159 | + { |
| 160 | + _art2 |
| 161 | + } |
| 162 | + }); |
| 163 | + } |
| 164 | + // Fixed frequency - Since DLS does not support it, we need to manually add every key with its own base note |
| 165 | + if (entry.IsFixedFrequency == 0x80) |
| 166 | + { |
| 167 | + for (ushort i = entry.MinKey; i <= entry.MaxKey; i++) |
| 168 | + { |
| 169 | + Add(i, i, i); |
| 170 | + } |
| 171 | + } |
| 172 | + else |
| 173 | + { |
| 174 | + Add(entry.MinKey, entry.MaxKey, 60); |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + } |
| 180 | +} |
0 commit comments