Skip to content

Commit

Permalink
Add MSADPCM audio format decoding support
Browse files Browse the repository at this point in the history
  • Loading branch information
ocdi authored and reaperrr committed Feb 19, 2021
1 parent 5aa67a4 commit 536c130
Showing 1 changed file with 103 additions and 7 deletions.
110 changes: 103 additions & 7 deletions OpenRA.Mods.Common/FileFormats/WavReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Stream> result, out short channels, out int sampleBits, out int sampleRate)
{
Expand All @@ -39,8 +39,8 @@ public static bool LoadSound(Stream s, out Func<Stream> 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)
Expand All @@ -65,7 +65,6 @@ public static bool LoadSound(Stream s, out Func<Stream> result, out short channe
s.ReadInt32(); // Byte Rate
blockAlign = s.ReadInt16();
sampleBits = s.ReadInt16();

s.ReadBytes(fmtChunkSize - 16);
break;
case "fact":
Expand All @@ -89,15 +88,18 @@ public static bool LoadSound(Stream s, out Func<Stream> 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;
result = () =>
{
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.
};
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -206,5 +208,99 @@ protected override bool BufferData(Stream baseStream, Queue<byte> 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<byte> 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<byte> 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;
}
}
}
}

0 comments on commit 536c130

Please sign in to comment.