source: OniSplit/Sound/WavExporter.cs@ 1147

Last change on this file since 1147 was 1131, checked in by geyser, 4 years ago

Yay, another SNDD fix that no one cares about: PC demo SNDDs! (short .dat headers, but the .raw data is MS ADPCM; use -nodemo to treat as Mac/IMA4 anyway).

File size: 18.4 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 = 512 * sound.ChannelCount * sound.SampleRate / 22050;
157 int wholeBlocks = sound.Data.Length / blockSizeADPCM;
158 int leftoverBytes = sound.Data.Length - (wholeBlocks * blockSizeADPCM);
159 int leftoverSamples = 8 * (leftoverBytes - 7 * sound.ChannelCount)
160 / 4 / sound.ChannelCount + 2; // 4 bits per sample
161 int paddingBytes = 0;
162 if (leftoverBytes > 0) // incomplete trailing block
163 paddingBytes = blockSizeADPCM - leftoverBytes;
164 var samplesPerBlock = 2 + (blockSizeADPCM - sound.ChannelCount * 7) * 8 / sound.ChannelCount / 4;
165
166 Int32 sampleCount = sampleCount = wholeBlocks * samplesPerBlock + leftoverSamples;
167
168 if (sound.IsIMA4) // IMA4 ADPCM format
169 {
170 blockSizeADPCM = 34 * sound.ChannelCount;
171 samplesPerBlock = 64;
172 sampleCount = (sound.Data.Length / blockSizeADPCM) * samplesPerBlock;
173 }
174 if (!convert_to_PCM)
175 {
176 if (sound.IsIMA4)
177 {
178 throw new NotSupportedException("Transcoding from IMA4 ADPCM (Mac) to MS ADPCM (PC) not supported! Please use -extract:pcm");
179 }
180 var format = (byte[])formatTemplate_ADPCM.Clone();
181 var fact = (byte[])factTemplate.Clone(); // needed for ADPCM (to specify the actual sample count)
182
183 var averageRate = sound.SampleRate * blockSizeADPCM / samplesPerBlock;
184 Array.Copy(BitConverter.GetBytes(sound.ChannelCount), 0, format, 2, 2);
185 Array.Copy(BitConverter.GetBytes(sound.SampleRate), 0, format, 4, 4);
186 Array.Copy(BitConverter.GetBytes(averageRate), 0, format, 8, 4);
187 Array.Copy(BitConverter.GetBytes(blockSizeADPCM), 0, format, 12, 2);
188 Array.Copy(BitConverter.GetBytes(samplesPerBlock), 0, format, 18, 2);
189
190 Array.Copy(BitConverter.GetBytes(sampleCount), 0, fact, 0, 4);
191
192 writer.Write(fcc_RIFF);
193 writer.Write(8 + format.Length + 8 + fact.Length + 8 + sound.Data.Length + paddingBytes);
194 writer.Write(fcc_WAVE);
195
196 //
197 // write format chunk
198 //
199 writer.Write(fcc_fmt);
200 writer.Write(format.Length);
201 writer.Write(format);
202
203 //
204 // write fact chunk
205 //
206 writer.Write(fcc_fact);
207 writer.Write(fact.Length);
208 writer.Write(fact);
209
210 //
211 // write data chunk
212 //
213 writer.Write(fcc_data);
214 writer.Write(sound.Data.Length + paddingBytes);
215 writer.Write(sound.Data);
216
217 Byte c = 0;
218 for (int i = 0; i < paddingBytes; i++)
219 writer.Write(c);
220 }
221 else
222 {
223 var format = (byte[])formatTemplate_PCM.Clone();
224
225 var blockSizePCM = 2 * sound.ChannelCount; // 16-bit samples or sample pairs
226 samplesPerBlock = 2;
227 var averageRate = sound.SampleRate * blockSizePCM / samplesPerBlock;
228 Array.Copy(BitConverter.GetBytes(sound.ChannelCount), 0, format, 2, 2);
229 Array.Copy(BitConverter.GetBytes(sound.SampleRate), 0, format, 4, 4);
230 Array.Copy(BitConverter.GetBytes(averageRate), 0, format, 8, 4);
231 Array.Copy(BitConverter.GetBytes(blockSizePCM), 0, format, 12, 2);
232
233 int dataSize = blockSizePCM * sampleCount;
234
235 writer.Write(fcc_RIFF);
236 writer.Write(8 + format.Length + 8 + dataSize);
237 writer.Write(fcc_WAVE);
238
239 //
240 // write format chunk
241 //
242
243 writer.Write(fcc_fmt);
244 writer.Write(format.Length);
245 writer.Write(format);
246
247 //
248 // write data chunk
249 //
250 var samplesL = new Int16[sampleCount];
251 var samplesR = new Int16[sampleCount];
252 if (sound.IsIMA4) // decode IMA4 into linear signed 16-bit PCM
253 {
254 int pos = 0;
255
256 int iSampleL = 0;
257 int predictorL = 0;
258 int stepIndexL = 0;
259 int iSampleR = 0;
260 int predictorR = 0;
261 int stepIndexR = 0;
262
263 int nBlocks = sound.Data.Length / blockSizeADPCM;
264 for (int block = 0; block < nBlocks; block++)
265 {
266 byte headerHiL = sound.Data[pos++];
267 byte headerLoL = sound.Data[pos++];
268 if (block == 0) // non-standard decoding: predictor initialization ignored after start
269 {
270 predictorL = ((((headerHiL << 1) | (headerLoL >> 7))) << 7);
271 if (predictorL > 32767) predictorL -= 65536;
272 }
273 stepIndexL = headerLoL & 0x7f;
274 if (stepIndexL > 88)
275 Console.WriteLine("Block {0} (L): Initial IMA4 step index is {1}, clamping to 88.", block, stepIndexL);
276 ClampToRange(ref stepIndexL, 0, 88);
277
278 for (int b = 0; b < 32; b++)
279 {
280 Byte nibblesL = sound.Data[pos++];
281 Byte nibbleHiL = (Byte)(nibblesL >> 4);
282 Byte nibbleLoL = (Byte)(nibblesL & 0xF);
283
284 samplesL[iSampleL++] = NibbletoSampleIMA4(ref predictorL, ref stepIndexL, nibbleLoL);
285 samplesL[iSampleL++] = NibbletoSampleIMA4(ref predictorL, ref stepIndexL, nibbleHiL);
286 }
287
288 if (sound.ChannelCount == 2)
289 {
290 byte headerHiR = sound.Data[pos++];
291 byte headerLoR = sound.Data[pos++];
292 if (block == 0) // non-standard decoding: predictor initialization ignored after start
293 {
294 predictorR = ((((headerHiR << 1) | (headerLoR >> 7))) << 7);
295 if (predictorR > 32767) predictorR -= 65536;
296 }
297 stepIndexR = headerLoR & 0x7f;
298 if (stepIndexR > 88)
299 Console.WriteLine("Block {0} (R): Initial IMA4 step index is {1}, clamping to 88.", block, stepIndexR);
300 ClampToRange(ref stepIndexR, 0, 88);
301
302 for (int b = 0; b < 32; b++)
303 {
304 Byte nibblesR = sound.Data[pos++];
305 Byte nibbleHiR = (Byte)(nibblesR >> 4);
306 Byte nibbleLoR = (Byte)(nibblesR & 0xF);
307
308 samplesR[iSampleR++] = NibbletoSampleIMA4(ref predictorR, ref stepIndexR, nibbleLoR);
309 samplesR[iSampleR++] = NibbletoSampleIMA4(ref predictorR, ref stepIndexR, nibbleHiR);
310 }
311 }
312 }
313 }
314 else // decode MSADPCM into linear signed 16-bit PCM
315 {
316 int pos = 0;
317 Byte pred_indexL = 0, pred_indexR = 0;
318 UInt16 deltaL = 0, deltaR = 0;
319 int iSampleL = 0;
320 int iSampleR = 0;
321 Int16 sample1L = 0, sample2L = 0;
322 Int16 sample1R = 0, sample2R = 0;
323
324 while (pos < sound.Data.Length)
325 {
326 if ((pos % blockSizeADPCM) == 0) // read block header
327 {
328 pred_indexL = sound.Data[pos++];
329 if (sound.ChannelCount == 2)
330 pred_indexR = sound.Data[pos++];
331 Byte deltaLo = sound.Data[pos++];
332 Byte deltaHi = sound.Data[pos++];
333 deltaL = (UInt16)(deltaLo + 256 * deltaHi);
334 if (sound.ChannelCount == 2)
335 {
336 deltaLo = sound.Data[pos++];
337 deltaHi = sound.Data[pos++];
338 deltaR = (UInt16)(deltaLo + 256 * deltaHi);
339 }
340 Byte sampleLo = sound.Data[pos++];
341 Byte sampleHi = sound.Data[pos++];
342 UInt16 usample = (UInt16)(sampleLo + 256 * sampleHi);
343 sample1L = (Int16)((usample < 32767) ? usample : (usample - 65536));
344 if (sound.ChannelCount == 2)
345 {
346 sampleLo = sound.Data[pos++];
347 sampleHi = sound.Data[pos++];
348 usample = (UInt16)(sampleLo + 256 * sampleHi);
349 sample1R = (Int16)((usample < 32767) ? usample : (usample - 65536));
350 }
351 sampleLo = sound.Data[pos++];
352 sampleHi = sound.Data[pos++];
353 usample = (UInt16)(sampleLo + 256 * sampleHi);
354 sample2L = (Int16)((usample < 32767) ? usample : (usample - 65536));
355 if (sound.ChannelCount == 2)
356 {
357 sampleLo = sound.Data[pos++];
358 sampleHi = sound.Data[pos++];
359 usample = (UInt16)(sampleLo + 256 * sampleHi);
360 sample2R = (Int16)((usample < 32767) ? usample : (usample - 65536));
361 }
362 samplesL[iSampleL++] = sample2L;
363 samplesL[iSampleL++] = sample1L;
364 if (sound.ChannelCount == 2)
365 {
366 samplesR[iSampleR++] = sample2R;
367 samplesR[iSampleR++] = sample1R;
368 }
369 }
370 else // read pair of nibbles
371 {
372 Byte nibbles = sound.Data[pos++];
373 Byte nibbleHi = (Byte)(nibbles >> 4);
374 Byte nibbleLo = (Byte)(nibbles & 0xF);
375 samplesL[iSampleL++] = NibbletoSampleMSADPCM(ref sample1L, ref sample2L, ref deltaL, pred_indexL, nibbleHi);
376 if (sound.ChannelCount == 2)
377 samplesR[iSampleR++] = NibbletoSampleMSADPCM(ref sample1R, ref sample2R, ref deltaR, pred_indexR, nibbleLo);
378 else
379 samplesL[iSampleL++] = NibbletoSampleMSADPCM(ref sample1L, ref sample2L, ref deltaL, pred_indexL, nibbleLo);
380 }
381 }
382 }
383 writer.Write(fcc_data);
384 writer.Write(dataSize);
385 for (int smp = 0; smp < sampleCount; smp++)
386 {
387 writer.Write(samplesL[smp]);
388 if(sound.ChannelCount == 2)
389 writer.Write(samplesR[smp]);
390 }
391 }
392 }
393 }
394 }
395}
Note: See TracBrowser for help on using the repository browser.