using System; using System.IO; namespace Oni.Sound { internal class WavExporter : SoundExporter { #region Private data private bool convert_to_PCM; private bool do_pc_demo_test; private const int fcc_RIFF = 0x46464952; private const int fcc_WAVE = 0x45564157; private const int fcc_fmt = 0x20746d66; private const int fcc_fact = 0x74636166; private const int fcc_data = 0x61746164; private static readonly byte[] formatTemplate_ADPCM = new byte[50] { 0x02, 0, // format ID (2 for ADPCM) 0, 0, // ChannelCount (overwritten) 0x22, 0x56, 0, 0, // SampleRate (usually 22050, can be 44100) 0, 0, 0, 0, // average data rate (computed and overwritten) 0, 0x02, // block alignment (default 512, can be 1024) 0x04, 0, // bits per sample (always 4) 0x20, 0, // size of extended ADPCM header block 0xf4, 0x03, // samples per block (usually 1012, can be 2036) 0x07, 0, // standard ADPCM coefficient table (always the same) 0, 0x01, 0, 0, 0, 0x02, 0, 0xff, 0, 0, 0, 0, 0xc0, 0, 0x40, 0, 0xf0, 0, 0, 0, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff }; private static readonly byte[] formatTemplate_PCM = new byte[16] { 0x01, 0, // format ID (1 for linear PCM) 0, 0, // ChannelCount (overwritten) 0x22, 0x56, 0, 0, // SampleRate (usually 22050, can be 44100) 0, 0, 0, 0, // data rate in bytes/s (computed and overwritten) 0x02, 0, // block size (2 bytes for mono, 4 for stereo) 0x10, 0 // bits per sample (always 16) }; private static readonly byte[] factTemplate = new byte[4] { 0, 0, 0, 0 // sample count (computed and overwritten) }; private static readonly int[] ima_index_table = new int[16] { -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 }; private static readonly int[] ima_step_table = new int[89] { 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 }; private static readonly int[] msadpcm_adapt_table = new int[16] { 230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230 }; private static readonly int[] msadpcm_coeff_table1 = new int[7] { 256, 512, 0, 192, 240, 460, 392 }; private static readonly int[] msadpcm_coeff_table2 = new int[7] { 0, -256, 0, 64, 0, -208, -232 }; #endregion public WavExporter(InstanceFileManager fileManager, string outputDirPath, bool convertToPCM = false, bool noDemo = false) : base(fileManager, outputDirPath) { convert_to_PCM = convertToPCM; do_pc_demo_test = !noDemo; } private static void ClampToRange(ref int value, int lower, int upper) { if (value > upper) value = upper; if (value < lower) value = lower; } protected Int16 NibbletoSampleIMA4(ref int predictor, ref int step_index, Byte nibble) { int step = ima_step_table[step_index]; step_index += ima_index_table[nibble]; ClampToRange(ref step_index, 0, 88); int diff = step >> 3; if ((nibble & 0x04) != 0) diff += step; if ((nibble & 0x02) != 0) diff += (step >> 1); if ((nibble & 0x01) != 0) diff += (step >> 2); if ((nibble & 0x08) != 0) predictor -= diff; else predictor += diff; ClampToRange(ref predictor, -32768, 32767); return (Int16)predictor; } protected Int16 NibbletoSampleMSADPCM(ref Int16 sample1, ref Int16 sample2, ref UInt16 delta, Byte pred_index, Byte nibble) { int coeff1 = msadpcm_coeff_table1[pred_index]; int coeff2 = msadpcm_coeff_table2[pred_index]; int prediction = ((int)sample1 * (int)coeff1 + (int)sample2 * (int)coeff2) >> 8; int snibble = (nibble < 8) ? nibble : (nibble - 16); int correction = snibble * (int)delta; int sample = prediction + correction; ClampToRange(ref sample, -32768, 32767); sample2 = sample1; sample1 = (Int16)sample; int newDelta = delta * msadpcm_adapt_table[nibble]; newDelta >>= 8; ClampToRange(ref newDelta, 16, 65535); delta = (UInt16)newDelta; return (Int16)sample; } protected override void ExportInstance(InstanceDescriptor descriptor) { var sound = SoundData.Read(descriptor, do_pc_demo_test); using (var stream = File.Create(Path.Combine(OutputDirPath, descriptor.FullName + ".wav"))) using (var writer = new BinaryWriter(stream)) { int blockSizeADPCM, samplesPerBlock, sampleCount, paddingBytes = 0; if (sound.IsIMA4) // IMA4 ADPCM format { blockSizeADPCM = 34 * sound.ChannelCount; samplesPerBlock = 64; sampleCount = (sound.Data.Length / blockSizeADPCM) * samplesPerBlock; } else { blockSizeADPCM = sound.BlockAlignment; int wholeBlocks = sound.Data.Length / blockSizeADPCM; int leftoverBytes = sound.Data.Length - (wholeBlocks * blockSizeADPCM); int leftoverSamples = 0; if (leftoverBytes > 7 * sound.ChannelCount) leftoverSamples = 2 + (leftoverBytes - 7 * sound.ChannelCount) * 8 / sound.BitsPerSample / sound.ChannelCount; else Console.Error.WriteLine("Improper trailing bytes/samples!"); if (leftoverBytes > 0) // incomplete trailing block paddingBytes = blockSizeADPCM - leftoverBytes; samplesPerBlock = 2 + (blockSizeADPCM - sound.ChannelCount * 7) * 8 / sound.ChannelCount / sound.BitsPerSample; sampleCount = wholeBlocks * samplesPerBlock + leftoverSamples; } if (!convert_to_PCM) { if (sound.IsIMA4) { throw new NotSupportedException("Transcoding from IMA4 ADPCM (Mac) to MS ADPCM (PC) not supported! Please use -extract:pcm"); } var format = (byte[])formatTemplate_ADPCM.Clone(); var fact = (byte[])factTemplate.Clone(); // needed for ADPCM (to specify the actual sample count) var averageRate = sound.SampleRate * blockSizeADPCM / samplesPerBlock; Array.Copy(BitConverter.GetBytes(sound.ChannelCount), 0, format, 2, 2); Array.Copy(BitConverter.GetBytes(sound.SampleRate), 0, format, 4, 4); Array.Copy(BitConverter.GetBytes(averageRate), 0, format, 8, 4); Array.Copy(BitConverter.GetBytes(blockSizeADPCM), 0, format, 12, 2); Array.Copy(BitConverter.GetBytes(samplesPerBlock), 0, format, 18, 2); Array.Copy(BitConverter.GetBytes(sampleCount), 0, fact, 0, 4); writer.Write(fcc_RIFF); writer.Write(8 + format.Length + 8 + fact.Length + 8 + sound.Data.Length + paddingBytes); writer.Write(fcc_WAVE); // // write format chunk // writer.Write(fcc_fmt); writer.Write(format.Length); writer.Write(format); // // write fact chunk // writer.Write(fcc_fact); writer.Write(fact.Length); writer.Write(fact); // // write data chunk // writer.Write(fcc_data); writer.Write(sound.Data.Length + paddingBytes); writer.Write(sound.Data); Byte c = 0; for (int i = 0; i < paddingBytes; i++) writer.Write(c); } else { var format = (byte[])formatTemplate_PCM.Clone(); var blockSizePCM = 2 * sound.ChannelCount; // 16-bit samples or sample pairs samplesPerBlock = 2; var averageRate = sound.SampleRate * blockSizePCM / samplesPerBlock; Array.Copy(BitConverter.GetBytes(sound.ChannelCount), 0, format, 2, 2); Array.Copy(BitConverter.GetBytes(sound.SampleRate), 0, format, 4, 4); Array.Copy(BitConverter.GetBytes(averageRate), 0, format, 8, 4); Array.Copy(BitConverter.GetBytes(blockSizePCM), 0, format, 12, 2); int dataSize = blockSizePCM * sampleCount; writer.Write(fcc_RIFF); writer.Write(8 + format.Length + 8 + dataSize); writer.Write(fcc_WAVE); // // write format chunk // writer.Write(fcc_fmt); writer.Write(format.Length); writer.Write(format); // // write data chunk // var samplesL = new Int16[sampleCount]; var samplesR = new Int16[sampleCount]; if (sound.IsIMA4) // decode IMA4 into linear signed 16-bit PCM { int pos = 0; int iSampleL = 0; int predictorL = 0; int stepIndexL = 0; int iSampleR = 0; int predictorR = 0; int stepIndexR = 0; int nBlocks = sound.Data.Length / blockSizeADPCM; for (int block = 0; block < nBlocks; block++) { byte headerHiL = sound.Data[pos++]; byte headerLoL = sound.Data[pos++]; if (block == 0) // non-standard decoding: predictor initialization ignored after start { predictorL = ((((headerHiL << 1) | (headerLoL >> 7))) << 7); if (predictorL > 32767) predictorL -= 65536; } stepIndexL = headerLoL & 0x7f; if (stepIndexL > 88) Console.WriteLine("Block {0} (L): Initial IMA4 step index is {1}, clamping to 88.", block, stepIndexL); ClampToRange(ref stepIndexL, 0, 88); for (int b = 0; b < 32; b++) { Byte nibblesL = sound.Data[pos++]; Byte nibbleHiL = (Byte)(nibblesL >> 4); Byte nibbleLoL = (Byte)(nibblesL & 0xF); samplesL[iSampleL++] = NibbletoSampleIMA4(ref predictorL, ref stepIndexL, nibbleLoL); samplesL[iSampleL++] = NibbletoSampleIMA4(ref predictorL, ref stepIndexL, nibbleHiL); } if (sound.ChannelCount == 2) { byte headerHiR = sound.Data[pos++]; byte headerLoR = sound.Data[pos++]; if (block == 0) // non-standard decoding: predictor initialization ignored after start { predictorR = ((((headerHiR << 1) | (headerLoR >> 7))) << 7); if (predictorR > 32767) predictorR -= 65536; } stepIndexR = headerLoR & 0x7f; if (stepIndexR > 88) Console.WriteLine("Block {0} (R): Initial IMA4 step index is {1}, clamping to 88.", block, stepIndexR); ClampToRange(ref stepIndexR, 0, 88); for (int b = 0; b < 32; b++) { Byte nibblesR = sound.Data[pos++]; Byte nibbleHiR = (Byte)(nibblesR >> 4); Byte nibbleLoR = (Byte)(nibblesR & 0xF); samplesR[iSampleR++] = NibbletoSampleIMA4(ref predictorR, ref stepIndexR, nibbleLoR); samplesR[iSampleR++] = NibbletoSampleIMA4(ref predictorR, ref stepIndexR, nibbleHiR); } } } } else // decode MSADPCM into linear signed 16-bit PCM { int pos = 0; Byte pred_indexL = 0, pred_indexR = 0; UInt16 deltaL = 0, deltaR = 0; int iSampleL = 0; int iSampleR = 0; Int16 sample1L = 0, sample2L = 0; Int16 sample1R = 0, sample2R = 0; while (pos < sound.Data.Length) { if ((pos % blockSizeADPCM) == 0) // read block header { pred_indexL = sound.Data[pos++]; if (sound.ChannelCount == 2) pred_indexR = sound.Data[pos++]; Byte deltaLo = sound.Data[pos++]; Byte deltaHi = sound.Data[pos++]; deltaL = (UInt16)(deltaLo + 256 * deltaHi); if (sound.ChannelCount == 2) { deltaLo = sound.Data[pos++]; deltaHi = sound.Data[pos++]; deltaR = (UInt16)(deltaLo + 256 * deltaHi); } Byte sampleLo = sound.Data[pos++]; Byte sampleHi = sound.Data[pos++]; UInt16 usample = (UInt16)(sampleLo + 256 * sampleHi); sample1L = (Int16)((usample < 32767) ? usample : (usample - 65536)); if (sound.ChannelCount == 2) { sampleLo = sound.Data[pos++]; sampleHi = sound.Data[pos++]; usample = (UInt16)(sampleLo + 256 * sampleHi); sample1R = (Int16)((usample < 32767) ? usample : (usample - 65536)); } sampleLo = sound.Data[pos++]; sampleHi = sound.Data[pos++]; usample = (UInt16)(sampleLo + 256 * sampleHi); sample2L = (Int16)((usample < 32767) ? usample : (usample - 65536)); if (sound.ChannelCount == 2) { sampleLo = sound.Data[pos++]; sampleHi = sound.Data[pos++]; usample = (UInt16)(sampleLo + 256 * sampleHi); sample2R = (Int16)((usample < 32767) ? usample : (usample - 65536)); } samplesL[iSampleL++] = sample2L; samplesL[iSampleL++] = sample1L; if (sound.ChannelCount == 2) { samplesR[iSampleR++] = sample2R; samplesR[iSampleR++] = sample1R; } } else // read pair of nibbles { Byte nibbles = sound.Data[pos++]; Byte nibbleHi = (Byte)(nibbles >> 4); Byte nibbleLo = (Byte)(nibbles & 0xF); samplesL[iSampleL++] = NibbletoSampleMSADPCM(ref sample1L, ref sample2L, ref deltaL, pred_indexL, nibbleHi); if (sound.ChannelCount == 2) samplesR[iSampleR++] = NibbletoSampleMSADPCM(ref sample1R, ref sample2R, ref deltaR, pred_indexR, nibbleLo); else samplesL[iSampleL++] = NibbletoSampleMSADPCM(ref sample1L, ref sample2L, ref deltaL, pred_indexL, nibbleLo); } } } writer.Write(fcc_data); writer.Write(dataSize); for (int smp = 0; smp < sampleCount; smp++) { writer.Write(samplesL[smp]); if(sound.ChannelCount == 2) writer.Write(samplesR[smp]); } } } } } }