@@ -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