From 536c130a39409925ff7c913c330c67de8ef81bb2 Mon Sep 17 00:00:00 2001 From: Trevor Nichols Date: Thu, 30 Jul 2020 09:09:00 +1000 Subject: [PATCH] Add MSADPCM audio format decoding support --- OpenRA.Mods.Common/FileFormats/WavReader.cs | 110 ++++++++++++++++++-- 1 file changed, 103 insertions(+), 7 deletions(-) diff --git a/OpenRA.Mods.Common/FileFormats/WavReader.cs b/OpenRA.Mods.Common/FileFormats/WavReader.cs index 1ebddb372182..6bebb15f8970 100644 --- a/OpenRA.Mods.Common/FileFormats/WavReader.cs +++ b/OpenRA.Mods.Common/FileFormats/WavReader.cs @@ -18,7 +18,7 @@ namespace OpenRA.Mods.Common.FileFormats { public static class WavReader { - enum WaveType { Pcm = 0x1, ImaAdpcm = 0x11 } + enum WaveType { Pcm = 0x1, MsAdpcm = 0x2, ImaAdpcm = 0x11 } public static bool LoadSound(Stream s, out Func result, out short channels, out int sampleBits, out int sampleRate) { @@ -39,8 +39,8 @@ public static bool LoadSound(Stream s, out Func result, out short channe WaveType audioType = 0; var dataOffset = -1L; var dataSize = -1; + var uncompressedSize = -1; short blockAlign = -1; - int uncompressedSize = -1; while (s.Position < s.Length) { if ((s.Position & 1) == 1) @@ -65,7 +65,6 @@ public static bool LoadSound(Stream s, out Func result, out short channe s.ReadInt32(); // Byte Rate blockAlign = s.ReadInt16(); sampleBits = s.ReadInt16(); - s.ReadBytes(fmtChunkSize - 16); break; case "fact": @@ -89,7 +88,8 @@ public static bool LoadSound(Stream s, out Func result, out short channe } } - if (audioType == WaveType.ImaAdpcm) + // sampleBits refers to the output bitrate, which is always 16 for adpcm. + if (audioType != WaveType.Pcm) sampleBits = 16; var chan = channels; @@ -97,7 +97,9 @@ public static bool LoadSound(Stream s, out Func result, out short channe { var audioStream = SegmentStream.CreateWithoutOwningStream(s, dataOffset, dataSize); if (audioType == WaveType.ImaAdpcm) - return new WavStream(audioStream, dataSize, blockAlign, chan, uncompressedSize); + return new WavStreamImaAdpcm(audioStream, dataSize, blockAlign, chan, uncompressedSize); + if (audioType == WaveType.MsAdpcm) + return new WavStreamMsAdpcm(audioStream, dataSize, blockAlign, chan); return audioStream; // Data is already PCM format. }; @@ -124,7 +126,7 @@ public static float WaveLength(Stream s) return length / (channels * sampleRate * bitsPerSample); } - sealed class WavStream : ReadOnlyAdapterStream + sealed class WavStreamImaAdpcm : ReadOnlyAdapterStream { readonly short channels; readonly int numBlocks; @@ -137,7 +139,7 @@ sealed class WavStream : ReadOnlyAdapterStream int outOffset; int currentBlock; - public WavStream(Stream stream, int dataSize, short blockAlign, short channels, int uncompressedSize) + public WavStreamImaAdpcm(Stream stream, int dataSize, short blockAlign, short channels, int uncompressedSize) : base(stream) { this.channels = channels; @@ -206,5 +208,99 @@ protected override bool BufferData(Stream baseStream, Queue data) return ++currentBlock >= numBlocks; } } + + // Format docs https://wiki.multimedia.cx/index.php/Microsoft_ADPCM + public sealed class WavStreamMsAdpcm : ReadOnlyAdapterStream + { + static readonly int[] AdaptationTable = + { + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 + }; + + static readonly int[] AdaptCoeff1 = { 256, 512, 0, 192, 240, 460, 392 }; + + static readonly int[] AdaptCoeff2 = { 0, -256, 0, 64, 0, -208, -232 }; + + readonly short channels; + readonly int blockDataSize; + readonly int numBlocks; + + int currentBlock; + + public WavStreamMsAdpcm(Stream stream, int dataSize, short blockAlign, short channels) + : base(stream) + { + this.channels = channels; + blockDataSize = blockAlign - channels * 7; + numBlocks = dataSize / blockAlign; + } + + protected override bool BufferData(Stream baseStream, Queue data) + { + var bpred = new byte[channels]; + var chanIdelta = new short[channels]; + + var s1 = new short[channels]; + var s2 = new short[channels]; + + for (var c = 0; c < channels; c++) + bpred[c] = baseStream.ReadUInt8(); + + for (var c = 0; c < channels; c++) + chanIdelta[c] = baseStream.ReadInt16(); + + for (var c = 0; c < channels; c++) + s1[c] = baseStream.ReadInt16(); + + for (var c = 0; c < channels; c++) + s2[c] = WriteSample(baseStream.ReadInt16(), data); + + for (var c = 0; c < channels; c++) + WriteSample(s1[c], data); + + var channelNumber = channels > 1 ? 1 : 0; + + for (var blockindx = 0; blockindx < blockDataSize; blockindx++) + { + var bytecode = baseStream.ReadUInt8(); + + // Decode the first nibble, this is always left channel + WriteSample(DecodeNibble((short)((bytecode >> 4) & 0x0F), bpred[0], ref chanIdelta[0], ref s1[0], ref s2[0]), data); + + // Decode the second nibble, for stereo this will be the right channel + WriteSample(DecodeNibble((short)(bytecode & 0x0F), bpred[channelNumber], ref chanIdelta[channelNumber], ref s1[channelNumber], ref s2[channelNumber]), data); + } + + return ++currentBlock >= numBlocks; + } + + short WriteSample(short t, Queue data) + { + data.Enqueue((byte)t); + data.Enqueue((byte)(t >> 8)); + return t; + } + + // This code contains elements from libsndfile + short DecodeNibble(short nibble, byte bpred, ref short idelta, ref short s1, ref short s2) + { + var predict = ((s1 * AdaptCoeff1[bpred]) + (s2 * AdaptCoeff2[bpred])) >> 8; + + var twosCompliment = (nibble & 0x8) > 0 + ? nibble - 0x10 + : nibble; + + s2 = s1; + s1 = (short)(twosCompliment * idelta + predict).Clamp(-32768, 32767); + + // Compute next Adaptive Scale Factor (ASF), saturating to lower bound of 16 + idelta = (short)((AdaptationTable[nibble] * idelta) >> 8); + if (idelta < 16) + idelta = 16; + + return s1; + } + } } }