1+ using System ;
2+ using System . IO ;
3+ using System . Linq ;
4+ using Fmod5Sharp . ChunkData ;
5+ using NAudio . Wave ;
6+
7+ namespace Fmod5Sharp
8+ {
9+ public static class FmodGcadPcmRebuilder
10+ {
11+ private const int BytesPerFrame = 8 ;
12+ private const int SamplesPerFrame = 14 ;
13+ private const int NibblesPerFrame = 16 ;
14+
15+ private static short [ ] GetPcmData ( FmodSample sample )
16+ {
17+ //Constants for this sample
18+ var sampleCount = ByteCountToSampleCount ( sample . SampleBytes . Length ) ;
19+ var frameCount = Math . Ceiling ( ( double ) sampleCount / SamplesPerFrame ) ;
20+
21+ //Result array
22+ var pcmData = new short [ sampleCount ] ;
23+
24+ //Read the data we need from the stream
25+ var adpcm = sample . SampleBytes ;
26+ var coeffChunk = ( DspCoefficientsBlockData ) sample . Metadata . Chunks . First ( c => c . ChunkType == FmodSampleChunkType . DSPCOEFF ) . ChunkData ;
27+ var coeffs = coeffChunk . ChannelData [ 0 ] ;
28+
29+ //Initialize indices
30+ var currentSample = 0 ;
31+ var outIndex = 0 ;
32+ var inIndex = 0 ;
33+
34+ //History values - current value is based on previous ones
35+ short hist1 = 0 ;
36+ short hist2 = 0 ;
37+
38+ for ( var i = 0 ; i < frameCount ; i ++ )
39+ {
40+ //Each byte is a scale and a predictor
41+ var combined = adpcm [ inIndex ++ ] ;
42+ var scale = ( 1 << ( combined & 0xF ) ) ;
43+ var predictor = combined >> 4 ;
44+
45+ //Coefficients are based on the predictor value
46+ var coeff1 = coeffs [ predictor * 2 ] ;
47+ var coeff2 = coeffs [ predictor * 2 + 1 ] ;
48+
49+ //Either read 14 - all the samples in this frame - or however many are left, if this is a partial frame
50+ var samplesToRead = Math . Min ( SamplesPerFrame , sampleCount - currentSample ) ;
51+
52+ for ( var s = 0 ; s < samplesToRead ; s ++ )
53+ {
54+ //Raw value
55+ var adpcmSample = ( int ) ( s % 2 == 0 ? GetHighNibbleSigned ( adpcm [ inIndex ] ) : GetLowNibbleSigned ( adpcm [ inIndex ++ ] ) ) ;
56+
57+ //Adaptive processing
58+ adpcmSample = ( ( adpcmSample * scale ) << 11 ) ;
59+ adpcmSample = ( adpcmSample + 1024 + coeff1 * hist1 + coeff2 * hist2 ) >> 11 ;
60+ var clampedSample = Clamp16 ( adpcmSample ) ;
61+
62+ //Bump history along
63+ hist2 = hist1 ;
64+ hist1 = clampedSample ;
65+
66+ //Set result
67+ pcmData [ outIndex ++ ] = clampedSample ;
68+
69+ //Move to next sample
70+ currentSample ++ ;
71+ }
72+ }
73+
74+ return pcmData ;
75+ }
76+
77+ public static byte [ ] Rebuild ( FmodSample sample )
78+ {
79+ var numChannels = sample . Metadata . IsStereo ? 2 : 1 ;
80+ var format = WaveFormat . CreateCustomFormat (
81+ WaveFormatEncoding . Pcm ,
82+ sample . Metadata . Frequency ,
83+ numChannels ,
84+ sample . Metadata . Frequency * numChannels * 2 ,
85+ numChannels * 2 ,
86+ 16
87+ ) ;
88+ using var stream = new MemoryStream ( ) ;
89+ using var writer = new WaveFileWriter ( stream , format ) ;
90+
91+ var pcmShorts = GetPcmData ( sample ) ;
92+
93+ writer . WriteSamples ( pcmShorts , 0 , pcmShorts . Length ) ;
94+
95+ return stream . GetBuffer ( ) ;
96+ }
97+
98+ private static int NibbleCountToSampleCount ( int nibbleCount )
99+ {
100+ var frames = nibbleCount / NibblesPerFrame ;
101+ var extraNibbles = nibbleCount % NibblesPerFrame ;
102+ var extraSamples = extraNibbles < 2 ? 0 : extraNibbles - 2 ;
103+
104+ return SamplesPerFrame * frames + extraSamples ;
105+ }
106+
107+ private static int ByteCountToSampleCount ( int byteCount ) => NibbleCountToSampleCount ( byteCount * 2 ) ;
108+
109+ private static readonly sbyte [ ] SignedNibbles = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , - 8 , - 7 , - 6 , - 5 , - 4 , - 3 , - 2 , - 1 } ;
110+ private static sbyte GetHighNibbleSigned ( byte value ) => SignedNibbles [ ( value >> 4 ) & 0xF ] ;
111+ private static sbyte GetLowNibbleSigned ( byte value ) => SignedNibbles [ value & 0xF ] ;
112+
113+ private static short Clamp16 ( int value )
114+ {
115+ if ( value > short . MaxValue )
116+ return short . MaxValue ;
117+ if ( value < short . MinValue )
118+ return short . MinValue ;
119+ return ( short ) value ;
120+ }
121+ }
122+ }
0 commit comments