source: OniSplit/Sound/WavExporter.cs@ 1154

Last change on this file since 1154 was 1154, checked in by geyser, 4 years 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.