--- OniSplit/Sound/SoundData.cs 2020/05/28 21:28:44 1130 +++ OniSplit/Sound/SoundData.cs 2021/05/03 15:26:34 1155 @@ -1,17 +1,35 @@ using System; using System.Collections.Generic; using System.Text; +using System.IO; namespace Oni.Sound { internal class SoundData { + public bool IsPCM; public bool IsIMA4; public int SampleRate; public int ChannelCount; public byte[] Data; - public static SoundData Read(InstanceDescriptor sndd) + public UInt16 nGameTicks; + + public int SampleCount; // calculated + + //MS WAV variables + public int AverageDataRate; + public int BlockAlignment; + public int BitsPerSample; + public int HeaderSizeADPCM; + + // optional MSADPCM values + public UInt16 SamplesPerBlock; + public UInt16 nCoefPairs; + public Int16[] CoefADPCMa; + public Int16[] CoefADPCMb; + + public static SoundData Read(InstanceDescriptor sndd, bool do_pc_demo_test) { if (sndd.Template.Tag != TemplateTag.SNDD) throw new ArgumentException("descriptor"); @@ -23,19 +41,49 @@ namespace Oni.Sound using (var reader = sndd.OpenRead()) { +// sample rate +// duration in frames + +// IMA4 or +// MS ADPCM: block size (default 512 for mono, 1024 for stereo, interruptible) + +// sample count (automatic for IMA4 and PC demo) + + +// size in raw / offset in raw if (sndd.IsMacFile) { - sound.ChannelCount = (reader.ReadInt32() >> 1) + 1; + sound.ChannelCount = (reader.ReadInt32() >> 1) + 1; // TODO: interpret the low bit? (uncompressed/compressed?) sound.SampleRate = 22050; - reader.Skip(4); + sound.nGameTicks = (UInt16)reader.ReadInt32(); + sound.IsIMA4 = true; + sound.IsPCM = false; } else { - reader.Skip(6); // ADPCM format identifiers (change to support PCM?) + sound.IsPCM = false; + reader.Skip(4); // flags (1=?, 2=?, 4=?, 8=compressed) TODO: Try uncompressed (even for demo?) + reader.Skip(2); // Int16; format ID (2= ADPCM, 1=PCM?) TODO: Try uncompressed (even for demo?) sound.ChannelCount = reader.ReadInt16(); sound.SampleRate = reader.ReadInt32(); - reader.Skip(44); + sound.AverageDataRate = reader.ReadInt32(); // in B/s (2 or 4 for PCM; 11155, 22311 or 22179 for Vanilla ADPCM) + sound.BlockAlignment = reader.ReadInt16(); // 2 or 4 bytes per block for PCM; 512 or 1024 for Vanilla ADPCM + sound.BitsPerSample = reader.ReadInt16(); // bits per sample per channel (4 bits per sample for ADPCM, 16 bits for PCM) + sound.HeaderSizeADPCM = reader.ReadInt16(); // size of additional (ADPCM) header (zero for PCM); typically 32 bytes + + sound.SamplesPerBlock = reader.ReadUInt16(); // UInt16; samples per block (can be inferred from block alignment etc) + + sound.nCoefPairs = reader.ReadUInt16(); // usually 7 + sound.CoefADPCMa = new Int16[sound.nCoefPairs]; // usually 256 512 0 192 240 460 392 + sound.CoefADPCMb = new Int16[sound.nCoefPairs]; // usually 0 -256 0 64 0 -208 -232 + for (int coefPair = 0; coefPair < sound.nCoefPairs; ++coefPair) + { + sound.CoefADPCMa[coefPair] = reader.ReadInt16(); + sound.CoefADPCMb[coefPair] = reader.ReadInt16(); + } + + sound.nGameTicks = reader.ReadUInt16(); // UInt16; number of game ticks (truncated to lower value) sound.IsIMA4 = false; } @@ -46,6 +94,72 @@ namespace Oni.Sound using (var rawReader = sndd.GetRawReader(dataOffset)) sound.Data = rawReader.ReadBytes(dataSize); + if (sound.IsIMA4 && do_pc_demo_test) // check if the raw data actually looks like IMA4 + { + int nIMABlocks = sound.Data.Length / 34; + int remainder = sound.Data.Length - nIMABlocks * 34; + if (remainder == 0 && (nIMABlocks % sound.ChannelCount) == 0) + { + bool stepIndexAbove88 = false; + for (int ii = 0; ii < nIMABlocks; ii++) + { + Byte cc = sound.Data[ii * 34 + 1]; + if ((cc & 0x7F) > 88) + stepIndexAbove88 = true; + } + if (stepIndexAbove88) + sound.IsIMA4 = false; + } + else + sound.IsIMA4 = false; + if (!(sound.IsIMA4)) // fill in default values of WAV (MS ADCPM) header + { + sound.IsPCM = false; + Console.WriteLine("PC-demo MS ADPCM detected; use -nodemo flag to treat as Mac IMA4."); + sound.AverageDataRate = (sound.ChannelCount == 1) ? 11155 : 22311; + sound.BlockAlignment = (sound.ChannelCount == 1) ? 512 : 1024; + sound.BitsPerSample = 4; + sound.SamplesPerBlock = 1012; + sound.HeaderSizeADPCM = 32; + sound.nCoefPairs = 7; + sound.CoefADPCMa = new Int16[7]; // usually 256 512 0 192 240 460 392 + sound.CoefADPCMb = new Int16[7]; // usually 0 -256 0 64 0 -208 -232 + sound.CoefADPCMa[0] = 256; sound.CoefADPCMb[0] = 0; + sound.CoefADPCMa[1] = 512; sound.CoefADPCMb[1] = -256; + sound.CoefADPCMa[2] = 0; sound.CoefADPCMb[2] = 0; + sound.CoefADPCMa[3] = 192; sound.CoefADPCMb[3] = 64; + sound.CoefADPCMa[4] = 240; sound.CoefADPCMb[4] = 0; + sound.CoefADPCMa[5] = 460; sound.CoefADPCMb[5] = -208; + sound.CoefADPCMa[6] = 392; sound.CoefADPCMb[6] = -232; + } + } + // validate data and calculate sample count + if(sound.IsIMA4) // get the sample count + { + int nIMABlocks = sound.Data.Length / 34 / sound.ChannelCount; + if(sound.Data.Length - nIMABlocks * 34 * sound.ChannelCount > 0) + throw new InvalidDataException("IMA4 data shouldn't have incomplete blocks."); + sound.SampleCount = nIMABlocks * 64; + } + else + { + // TODO: validate all the parameters: resolve conflicts if any, or bail out + // TODO: handle PCM (the following assumes MS ADPCM) + if (sound.IsPCM) + { + } + else + { + int wholeBlocks = sound.Data.Length / sound.BlockAlignment; + int leftoverBytes = sound.Data.Length - (wholeBlocks * sound.BlockAlignment); + int leftoverSamples = 8 * (leftoverBytes - 7 * sound.ChannelCount) + / sound.BitsPerSample / sound.ChannelCount + 2; // assuming 4 bits per sample + sound.SampleCount = wholeBlocks * sound.SamplesPerBlock + leftoverSamples; + } + } +// Console.WriteLine("Sample count:"); +// Console.WriteLine(sound.SampleCount); + return sound; } }