source: OniSplit/Sound/WavExporter.cs @ 1154

Last change on this file since 1154 was 1154, checked in by geyser, 14 months ago

Implemented import/export of "fact" chunk for .wav files. Added a -demo tag for storing sounds in the PC demo format (if compatible).

File size: 18.5 KB
Line 
1using System;
2using System.IO;
3
4namespace Oni.Sound
5{
6    internal class WavExporter : SoundExporter
7    {
8        #region Private data
9        private bool convert_to_PCM;
10        private bool do_pc_demo_test;
11
12        private const int fcc_RIFF = 0x46464952;
13        private const int fcc_WAVE = 0x45564157;
14        private const int fcc_fmt = 0x20746d66;
15        private const int fcc_fact = 0x74636166;
16        private const int fcc_data = 0x61746164;
17
18        private static readonly byte[] formatTemplate_ADPCM = new byte[50]
19        {
20            0x02, 0,    // format ID (2 for ADPCM)
21            0, 0,       // ChannelCount (overwritten)
22            0x22, 0x56, 0, 0, // SampleRate (usually 22050, can be 44100)
23            0, 0, 0, 0, // average data rate (computed and overwritten)
24            0, 0x02,    // block alignment (default 512, can be 1024)
25            0x04, 0,    // bits per sample (always 4)
26            0x20, 0,    // size of extended ADPCM header block
27            0xf4, 0x03, // samples per block (usually 1012, can be 2036)
28            0x07, 0,    // standard ADPCM coefficient table (always the same)
29            0, 0x01, 0, 0,
30            0, 0x02, 0, 0xff,
31            0, 0, 0, 0,
32            0xc0, 0, 0x40, 0,
33            0xf0, 0, 0, 0,
34            0xcc, 0x01, 0x30, 0xff,
35            0x88, 0x01, 0x18, 0xff
36        };
37
38        private static readonly byte[] formatTemplate_PCM = new byte[16]
39        {
40            0x01, 0,    // format ID (1 for linear PCM)
41            0, 0,       // ChannelCount (overwritten)
42            0x22, 0x56, 0, 0, // SampleRate (usually 22050, can be 44100)
43            0, 0, 0, 0, // data rate in bytes/s (computed and overwritten)
44            0x02, 0,    // block size (2 bytes for mono, 4 for stereo)
45            0x10, 0     // bits per sample (always 16)
46        };
47
48        private static readonly byte[] factTemplate = new byte[4]
49        {
50            0, 0, 0, 0  // sample count (computed and overwritten)
51        };
52
53        private static readonly int[] ima_index_table = new int[16]
54        {
55           -1, -1, -1, -1, 2, 4, 6, 8,
56           -1, -1, -1, -1, 2, 4, 6, 8
57        };
58
59        private static readonly int[] ima_step_table = new int[89]
60        {
61            7, 8, 9, 10, 11, 12, 13, 14, 16, 17,
62            19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
63            50, 55, 60, 66, 73, 80, 88, 97, 107, 118,
64            130, 143, 157, 173, 190, 209, 230, 253, 279, 307,
65            337, 371, 408, 449, 494, 544, 598, 658, 724, 796,
66            876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
67            2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358,
68            5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
69            15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
70        };
71
72        private static readonly int[] msadpcm_adapt_table = new int[16]
73        {
74            230, 230, 230, 230, 307, 409, 512, 614,
75            768, 614, 512, 409, 307, 230, 230, 230
76        };
77
78        private static readonly int[] msadpcm_coeff_table1 = new int[7]
79        {
80            256, 512, 0, 192, 240, 460, 392
81        };
82
83        private static readonly int[] msadpcm_coeff_table2 = new int[7]
84        {
85            0, -256, 0, 64, 0, -208, -232
86        };
87        #endregion
88
89        public WavExporter(InstanceFileManager fileManager, string outputDirPath, bool convertToPCM = false, bool noDemo = false)
90            : base(fileManager, outputDirPath)
91        {
92            convert_to_PCM = convertToPCM;
93            do_pc_demo_test = !noDemo;
94        }
95
96        private static void ClampToRange(ref int value, int lower, int upper)
97        {
98            if (value > upper)
99                value = upper;
100            if (value < lower)
101                value = lower;
102        }
103
104        protected Int16 NibbletoSampleIMA4(ref int predictor, ref int step_index, Byte nibble)
105        {
106            int step = ima_step_table[step_index];
107
108            step_index += ima_index_table[nibble];
109            ClampToRange(ref step_index, 0, 88);
110
111            int diff = step >> 3;
112
113            if ((nibble & 0x04) != 0) diff +=  step;
114            if ((nibble & 0x02) != 0) diff += (step >> 1);
115            if ((nibble & 0x01) != 0) diff += (step >> 2);
116            if ((nibble & 0x08) != 0)
117                predictor -= diff;
118            else
119                predictor += diff;
120
121            ClampToRange(ref predictor, -32768, 32767);
122            return (Int16)predictor;
123        }
124
125        protected Int16 NibbletoSampleMSADPCM(ref Int16 sample1, ref Int16 sample2, ref UInt16 delta, Byte pred_index, Byte nibble)
126        {
127            int coeff1 = msadpcm_coeff_table1[pred_index];
128            int coeff2 = msadpcm_coeff_table2[pred_index];
129
130            int prediction = ((int)sample1 * (int)coeff1 + (int)sample2 * (int)coeff2) >> 8;
131
132            int snibble = (nibble < 8) ? nibble : (nibble - 16);
133            int correction = snibble * (int)delta;
134
135            int sample = prediction + correction;
136            ClampToRange(ref sample, -32768, 32767);
137
138            sample2 = sample1;
139            sample1 = (Int16)sample;
140
141            int newDelta = delta * msadpcm_adapt_table[nibble];
142            newDelta >>= 8;
143            ClampToRange(ref newDelta, 16, 65535);
144            delta = (UInt16)newDelta;
145
146            return (Int16)sample;
147        }
148
149        protected override void ExportInstance(InstanceDescriptor descriptor)
150        {
151            var sound = SoundData.Read(descriptor, do_pc_demo_test);
152
153            using (var stream = File.Create(Path.Combine(OutputDirPath, descriptor.FullName + ".wav")))
154            using (var writer = new BinaryWriter(stream))
155            {
156                var blockSizeADPCM = sound.BlockAlignment;
157               
158                int wholeBlocks = sound.Data.Length / blockSizeADPCM;
159                int leftoverBytes = sound.Data.Length - (wholeBlocks * blockSizeADPCM);
160                int leftoverSamples = 0;
161                if(leftoverBytes > 14)
162                    leftoverSamples = 2 + (leftoverBytes - 7 * sound.ChannelCount)
163                                    * 8 / sound.BitsPerSample / sound.ChannelCount;
164                int paddingBytes = 0;
165                if (leftoverBytes > 0) // incomplete trailing block
166                    paddingBytes = blockSizeADPCM - leftoverBytes;
167                var samplesPerBlock = 2 + (blockSizeADPCM - sound.ChannelCount * 7) * 8 / sound.ChannelCount / sound.BitsPerSample;
168
169                Int32 sampleCount = wholeBlocks * samplesPerBlock + leftoverSamples;
170
171                if (sound.IsIMA4) // IMA4 ADPCM format
172                {
173                    blockSizeADPCM = 34 * sound.ChannelCount;
174                    samplesPerBlock = 64;
175                    sampleCount = (sound.Data.Length / blockSizeADPCM) * samplesPerBlock;
176                }
177                if (!convert_to_PCM)
178                {
179                    if (sound.IsIMA4)
180                    {
181                        throw new NotSupportedException("Transcoding from IMA4 ADPCM (Mac) to MS ADPCM (PC) not supported! Please use -extract:pcm");
182                    }
183                    var format = (byte[])formatTemplate_ADPCM.Clone();
184                    var fact = (byte[])factTemplate.Clone(); // needed for ADPCM (to specify the actual sample count)
185
186                    var averageRate = sound.SampleRate * blockSizeADPCM / samplesPerBlock;
187                    Array.Copy(BitConverter.GetBytes(sound.ChannelCount), 0, format, 2, 2);
188                    Array.Copy(BitConverter.GetBytes(sound.SampleRate), 0, format, 4, 4);
189                    Array.Copy(BitConverter.GetBytes(averageRate), 0, format, 8, 4);
190                    Array.Copy(BitConverter.GetBytes(blockSizeADPCM), 0, format, 12, 2);
191                    Array.Copy(BitConverter.GetBytes(samplesPerBlock), 0, format, 18, 2);
192
193                    Array.Copy(BitConverter.GetBytes(sampleCount), 0, fact, 0, 4);
194
195                    writer.Write(fcc_RIFF);
196                    writer.Write(8 + format.Length + 8 + fact.Length + 8 + sound.Data.Length + paddingBytes);
197                    writer.Write(fcc_WAVE);
198
199                    //
200                    // write format chunk
201                    //
202                    writer.Write(fcc_fmt);
203                    writer.Write(format.Length);
204                    writer.Write(format);
205
206                    //
207                    // write fact chunk
208                    //
209                    writer.Write(fcc_fact);
210                    writer.Write(fact.Length);
211                    writer.Write(fact);
212
213                    //
214                    // write data chunk
215                    //
216                    writer.Write(fcc_data);
217                    writer.Write(sound.Data.Length + paddingBytes);
218                    writer.Write(sound.Data);
219
220                    Byte c = 0;
221                    for (int i = 0; i < paddingBytes; i++)
222                        writer.Write(c);
223                }
224                else
225                {
226                    var format = (byte[])formatTemplate_PCM.Clone();
227
228                    var blockSizePCM = 2 * sound.ChannelCount; // 16-bit samples or sample pairs
229                    samplesPerBlock = 2;
230                    var averageRate = sound.SampleRate * blockSizePCM / samplesPerBlock;
231                    Array.Copy(BitConverter.GetBytes(sound.ChannelCount), 0, format, 2, 2);
232                    Array.Copy(BitConverter.GetBytes(sound.SampleRate), 0, format, 4, 4);
233                    Array.Copy(BitConverter.GetBytes(averageRate), 0, format, 8, 4);
234                    Array.Copy(BitConverter.GetBytes(blockSizePCM), 0, format, 12, 2);
235
236                    int dataSize = blockSizePCM * sampleCount;
237
238                    writer.Write(fcc_RIFF);
239                    writer.Write(8 + format.Length + 8 + dataSize);
240                    writer.Write(fcc_WAVE);
241
242                    //
243                    // write format chunk
244                    //
245
246                    writer.Write(fcc_fmt);
247                    writer.Write(format.Length);
248                    writer.Write(format);
249
250                    //
251                    // write data chunk
252                    //
253                    var samplesL = new Int16[sampleCount];
254                    var samplesR = new Int16[sampleCount];
255                    if (sound.IsIMA4) // decode IMA4 into linear signed 16-bit PCM
256                    {
257                        int pos = 0;
258
259                        int iSampleL = 0;
260                        int predictorL = 0;
261                        int stepIndexL = 0;
262                        int iSampleR = 0;
263                        int predictorR = 0;
264                        int stepIndexR = 0;
265
266                        int nBlocks = sound.Data.Length / blockSizeADPCM;
267                        for (int block = 0; block < nBlocks; block++)
268                        {
269                            byte headerHiL = sound.Data[pos++];
270                            byte headerLoL = sound.Data[pos++];
271                            if (block == 0) // non-standard decoding: predictor initialization ignored after start
272                            {
273                                predictorL = ((((headerHiL << 1) | (headerLoL >> 7))) << 7);
274                                if (predictorL > 32767) predictorL -= 65536;
275                            }
276                            stepIndexL = headerLoL & 0x7f;
277                            if (stepIndexL > 88)
278                                Console.WriteLine("Block {0} (L): Initial IMA4 step index is {1}, clamping to 88.", block, stepIndexL);
279                            ClampToRange(ref stepIndexL, 0, 88);
280
281                            for (int b = 0; b < 32; b++)
282                            {
283                                Byte nibblesL = sound.Data[pos++];
284                                Byte nibbleHiL = (Byte)(nibblesL >> 4);
285                                Byte nibbleLoL = (Byte)(nibblesL & 0xF);
286
287                                samplesL[iSampleL++] = NibbletoSampleIMA4(ref predictorL, ref stepIndexL, nibbleLoL);
288                                samplesL[iSampleL++] = NibbletoSampleIMA4(ref predictorL, ref stepIndexL, nibbleHiL);
289                            }
290
291                            if (sound.ChannelCount == 2)
292                            {
293                                byte headerHiR = sound.Data[pos++];
294                                byte headerLoR = sound.Data[pos++];
295                                if (block == 0) // non-standard decoding: predictor initialization ignored after start
296                                {
297                                    predictorR = ((((headerHiR << 1) | (headerLoR >> 7))) << 7);
298                                    if (predictorR > 32767) predictorR -= 65536;
299                                }
300                                stepIndexR = headerLoR & 0x7f;
301                                if (stepIndexR > 88)
302                                    Console.WriteLine("Block {0} (R): Initial IMA4 step index is {1}, clamping to 88.", block, stepIndexR);
303                                ClampToRange(ref stepIndexR, 0, 88);
304
305                                for (int b = 0; b < 32; b++)
306                                {
307                                    Byte nibblesR = sound.Data[pos++];
308                                    Byte nibbleHiR = (Byte)(nibblesR >> 4);
309                                    Byte nibbleLoR = (Byte)(nibblesR & 0xF);
310
311                                    samplesR[iSampleR++] = NibbletoSampleIMA4(ref predictorR, ref stepIndexR, nibbleLoR);
312                                    samplesR[iSampleR++] = NibbletoSampleIMA4(ref predictorR, ref stepIndexR, nibbleHiR);
313                                }
314                            }
315                        }
316                    }
317                    else // decode MSADPCM into linear signed 16-bit PCM
318                    {
319                        int pos = 0;
320                        Byte pred_indexL = 0, pred_indexR = 0;
321                        UInt16 deltaL = 0, deltaR = 0;
322                        int iSampleL = 0;
323                        int iSampleR = 0;
324                        Int16 sample1L = 0, sample2L = 0;
325                        Int16 sample1R = 0, sample2R = 0;
326
327                        while (pos < sound.Data.Length)
328                        {
329                            if ((pos % blockSizeADPCM) == 0) // read block header
330                            {
331                                pred_indexL = sound.Data[pos++];
332                                if (sound.ChannelCount == 2)
333                                    pred_indexR = sound.Data[pos++];
334                                Byte deltaLo = sound.Data[pos++];
335                                Byte deltaHi = sound.Data[pos++];
336                                deltaL = (UInt16)(deltaLo + 256 * deltaHi);
337                                if (sound.ChannelCount == 2)
338                                {
339                                    deltaLo = sound.Data[pos++];
340                                    deltaHi = sound.Data[pos++];
341                                    deltaR = (UInt16)(deltaLo + 256 * deltaHi);
342                                }
343                                Byte sampleLo = sound.Data[pos++];
344                                Byte sampleHi = sound.Data[pos++];
345                                UInt16 usample = (UInt16)(sampleLo + 256 * sampleHi);
346                                sample1L = (Int16)((usample < 32767) ? usample : (usample - 65536));
347                                if (sound.ChannelCount == 2)
348                                {
349                                    sampleLo = sound.Data[pos++];
350                                    sampleHi = sound.Data[pos++];
351                                    usample = (UInt16)(sampleLo + 256 * sampleHi);
352                                    sample1R = (Int16)((usample < 32767) ? usample : (usample - 65536));
353                                }
354                                sampleLo = sound.Data[pos++];
355                                sampleHi = sound.Data[pos++];
356                                usample = (UInt16)(sampleLo + 256 * sampleHi);
357                                sample2L = (Int16)((usample < 32767) ? usample : (usample - 65536));
358                                if (sound.ChannelCount == 2)
359                                {
360                                    sampleLo = sound.Data[pos++];
361                                    sampleHi = sound.Data[pos++];
362                                    usample = (UInt16)(sampleLo + 256 * sampleHi);
363                                    sample2R = (Int16)((usample < 32767) ? usample : (usample - 65536));
364                                }
365                                samplesL[iSampleL++] = sample2L;
366                                samplesL[iSampleL++] = sample1L;
367                                if (sound.ChannelCount == 2)
368                                {
369                                    samplesR[iSampleR++] = sample2R;
370                                    samplesR[iSampleR++] = sample1R;
371                                }
372                            }
373                            else // read pair of nibbles
374                            {
375                                Byte nibbles = sound.Data[pos++];
376                                Byte nibbleHi = (Byte)(nibbles >> 4);
377                                Byte nibbleLo = (Byte)(nibbles & 0xF);
378                                samplesL[iSampleL++] = NibbletoSampleMSADPCM(ref sample1L, ref sample2L, ref deltaL, pred_indexL, nibbleHi);
379                                if (sound.ChannelCount == 2)
380                                    samplesR[iSampleR++] = NibbletoSampleMSADPCM(ref sample1R, ref sample2R, ref deltaR, pred_indexR, nibbleLo);
381                                else
382                                    samplesL[iSampleL++] = NibbletoSampleMSADPCM(ref sample1L, ref sample2L, ref deltaL, pred_indexL, nibbleLo);
383                            }
384                        }
385                    }
386                    writer.Write(fcc_data);
387                    writer.Write(dataSize);
388                    for (int smp = 0; smp < sampleCount; smp++)
389                    {
390                        writer.Write(samplesL[smp]);
391                        if(sound.ChannelCount == 2)
392                            writer.Write(samplesR[smp]);
393                    }
394                }
395            }
396        }
397    }
398}
Note: See TracBrowser for help on using the repository browser.