Skip to content

Commit 5280aa1

Browse files
committed
Implement Xbox Ima Ad PCM algorithm. Closes #4
1 parent 44c8d5c commit 5280aa1

File tree

1 file changed

+98
-25
lines changed

1 file changed

+98
-25
lines changed

Fmod5Sharp/CodecRebuilders/FmodImaAdPcmRebuilder.cs

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ namespace Fmod5Sharp.CodecRebuilders
77
{
88
public static class FmodImaAdPcmRebuilder
99
{
10-
public const int SamplesPerFramePerChannel = 64;
11-
12-
static readonly int[] ADPCMTable = {
10+
public const int SamplesPerFramePerChannel = 0x40;
11+
12+
static readonly int[] ADPCMTable =
13+
{
1314
7, 8, 9, 10, 11, 12, 13, 14,
1415
16, 17, 19, 21, 23, 25, 28, 31,
1516
34, 37, 41, 45, 50, 55, 60, 66,
@@ -24,89 +25,161 @@ public static class FmodImaAdPcmRebuilder
2425
32767
2526
};
2627

27-
private static readonly int[] IMA_IndexTable = {
28+
private static readonly int[] IMA_IndexTable =
29+
{
2830
-1, -1, -1, -1, 2, 4, 6, 8,
2931
-1, -1, -1, -1, 2, 4, 6, 8,
3032
};
31-
33+
3234
private static void ExpandNibble(MemoryStream stream, long byteOffset, int nibbleShift, ref int hist, ref int stepIndex)
3335
{
36+
//Read the raw nibble
3437
stream.Seek(byteOffset, SeekOrigin.Begin);
3538
var sampleNibble = (stream.ReadByte() >> nibbleShift) & 0xf;
39+
40+
//Initial value for the sample is the previous value
3641
var sampleDecoded = hist;
42+
43+
//Apply the step from the table of values above
3744
var step = ADPCMTable[stepIndex];
3845

3946
var delta = step >> 3;
4047
if ((sampleNibble & 1) != 0) delta += step >> 2;
4148
if ((sampleNibble & 2) != 0) delta += step >> 1;
4249
if ((sampleNibble & 4) != 0) delta += step;
4350
if ((sampleNibble & 8) != 0) delta = -delta;
51+
52+
//Sample changes by the delta
4453
sampleDecoded += delta;
4554

55+
//New sample becomes the previous value, but clamped to a short.
4656
hist = Utils.Clamp((short)sampleDecoded, short.MinValue, short.MaxValue);
57+
58+
//Step index changes based on what was stored in the file, clamped to fit in the array
4759
stepIndex += IMA_IndexTable[sampleNibble];
4860
stepIndex = Utils.Clamp((short)stepIndex, 0, 88);
4961
}
5062

51-
private static short[] GetPcm(FmodSample sample)
63+
private static short[] DecodeSamplesFsbIma(FmodSample sample)
5264
{
53-
var blockSamples = 0x40;
65+
const int blockSamples = 0x40;
66+
5467
var numChannels = (int)sample.Metadata.Channels;
55-
68+
5669
using var stream = new MemoryStream(sample.SampleBytes);
5770
using var reader = new BinaryReader(stream);
5871

59-
short[] ret = new short[sample.Metadata.SampleCount * 2];
60-
var sampleIndex = 0;
61-
72+
var ret = new short[sample.Metadata.SampleCount * 2];
73+
74+
// Calculate frame count from sample count
75+
var numFrames = (int)sample.Metadata.SampleCount / SamplesPerFramePerChannel;
76+
6277
for (var channel = 0; channel < numChannels; channel++)
6378
{
64-
sampleIndex = channel;
65-
66-
var numFrames = (int) sample.Metadata.SampleCount / SamplesPerFramePerChannel;
79+
var sampleIndex = channel;
6780

6881
for (var frameNum = 0; frameNum < numFrames; frameNum++)
6982
{
83+
//Offset of this frame in the entire sample data
7084
var frameOffset = 0x24 * numChannels * frameNum;
71-
72-
//Read header
73-
var headerIndex = frameOffset + 4 * channel;
7485

86+
//Read frame header
87+
var headerIndex = frameOffset + 4 * channel;
7588
stream.Seek(headerIndex, SeekOrigin.Begin);
7689
int hist = reader.ReadInt16();
7790
stream.Seek(headerIndex + 2, SeekOrigin.Begin);
7891
int stepIndex = reader.ReadByte();
7992

93+
//Calculate initial sample value for this frame
8094
stepIndex = Utils.Clamp((short)stepIndex, 0, 88);
8195
ret[sampleIndex] = (short)hist;
8296
sampleIndex += numChannels;
8397

8498
for (var sampleNum = 1; sampleNum <= SamplesPerFramePerChannel; sampleNum++)
8599
{
86-
// var byteOffset = relativePos + 4 * numChannels + 2 * channel + (i - 1) / 4 * 2 * numChannels + ((i - 1) % 4) / 2;
100+
//Offset of this sample in the entire sample data
101+
//Note that this is, slightly confusingly, two different definitions of the word sample.
102+
//What i mean is "index of this value within the current frame which is part of one of the channels in the FMOD 'sample', which should really be called a sound file"
87103
var byteOffset = frameOffset + 4 * 2 + 4 * (channel % 2) + 4 * 2 * ((sampleNum - 1) / 8) + ((sampleNum - 1) % 8) / 2;
88104
if (numChannels == 0)
89105
byteOffset = frameOffset + 4 + (sampleNum - 1) / 2;
90106

107+
//Each sample is only half a byte, so odd samples use the upper half of the byte, and even samples use the lower half.
91108
var nibbleShift = ((sampleNum - 1) & 1) != 0 ? 4 : 0;
92109

93110
if (sampleNum < blockSamples)
94111
{
112+
//Apply the IMA algorithm to convert this nibble into a full byte of data.
95113
ExpandNibble(stream, byteOffset, nibbleShift, ref hist, ref stepIndex);
114+
115+
//Move to next sample
96116
ret[sampleIndex] = ((short)hist);
97117
sampleIndex += numChannels;
98118
}
99-
else
100-
{
101-
// relativePos += 0x24 * numChannels;
102-
}
103119
}
104120
}
105121
}
106122

107123
return ret;
108124
}
109-
125+
126+
private static short[] DecodeSamplesXboxIma(FmodSample sample)
127+
{
128+
//This is a simplified version of the algorithm, because we know that this will only ever be called if we have one channel.
129+
130+
const int frameSize = 0x24;
131+
132+
using var stream = new MemoryStream(sample.SampleBytes);
133+
using var reader = new BinaryReader(stream);
134+
135+
var numFrames = (int)sample.Metadata.SampleCount / SamplesPerFramePerChannel;
136+
137+
var ret = new short[sample.Metadata.SampleCount];
138+
var sampleIndex = 0;
139+
140+
for (var frameNum = 0; frameNum < numFrames; frameNum++)
141+
{
142+
143+
144+
//Offset of this frame in the entire sample data
145+
var frameOffset = frameSize * frameNum;
146+
147+
//Read frame header
148+
stream.Seek(frameOffset, SeekOrigin.Begin);
149+
int hist = reader.ReadInt16();
150+
stream.Seek(frameOffset + 2, SeekOrigin.Begin);
151+
int stepIndex = reader.ReadByte();
152+
153+
//Calculate initial sample value for this frame
154+
stepIndex = Utils.Clamp((short)stepIndex, 0, 88);
155+
ret[sampleIndex] = (short)hist;
156+
sampleIndex ++;
157+
158+
for (var sampleNum = 1; sampleNum <= SamplesPerFramePerChannel; sampleNum++)
159+
{
160+
//Offset of this sample in the entire sample data
161+
//Note that this is, slightly confusingly, two different definitions of the word sample.
162+
//What i mean is "index of this value within the current frame which is part of one of the channels in the FMOD 'sample', which should really be called a sound file"
163+
var byteOffset = frameOffset + 4 + (sampleNum - 1) / 2;
164+
165+
//Each sample is only half a byte, so odd samples use the upper half of the byte, and even samples use the lower half.
166+
var nibbleShift = ((sampleNum - 1) & 1) != 0 ? 4 : 0;
167+
168+
if (sampleNum < SamplesPerFramePerChannel)
169+
{
170+
//Apply the IMA algorithm to convert this nibble into a full byte of data.
171+
ExpandNibble(stream, byteOffset, nibbleShift, ref hist, ref stepIndex);
172+
173+
//Move to next sample
174+
ret[sampleIndex] = ((short)hist);
175+
sampleIndex ++;
176+
}
177+
}
178+
}
179+
180+
return ret;
181+
}
182+
110183
public static byte[] Rebuild(FmodSample sample)
111184
{
112185
var numChannels = sample.Metadata.IsStereo ? 2 : 1;
@@ -121,8 +194,8 @@ public static byte[] Rebuild(FmodSample sample)
121194
using var stream = new MemoryStream();
122195
using var writer = new WaveFileWriter(stream, format);
123196

124-
var pcmShorts = GetPcm(sample);
125-
197+
var pcmShorts = numChannels == 1 ? DecodeSamplesXboxIma(sample) : DecodeSamplesFsbIma(sample);
198+
126199
writer.WriteSamples(pcmShorts, 0, pcmShorts.Length);
127200

128201
return stream.ToArray();

0 commit comments

Comments
 (0)