source: OniSplit/Sound/WavExporter.cs@ 1130

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

Minor fixes and features in SNDD export: "transcoding" forbidden, standard-compliant MSADPCM (with padding and "fact" section), PCM export (with -extract:pcm).

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