source: OniSplit/Sound/WavExporter.cs

Last change on this file was 1156, checked in by geyser, 4 years ago

Fixed importing of "fact" chunk, as well as an unhandled fatal when attempting to transcode IMA4 to MS ADPCM. Also added missing padding to SNDD template and fixed the duration calculation for WAV-to-SNDD.

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