// Copyright (c) 2023 Xiaomi Corporation (authors: Fangjun Kuang) using System; using System.IO; using System.Runtime.InteropServices; namespace SherpaNcnn { [StructLayout(LayoutKind.Sequential)] public struct WaveHeader { public Int32 ChunkID; public Int32 ChunkSize; public Int32 Format; public Int32 SubChunk1ID; public Int32 SubChunk1Size; public Int16 AudioFormat; public Int16 NumChannels; public Int32 SampleRate; public Int32 ByteRate; public Int16 BlockAlign; public Int16 BitsPerSample; public Int32 SubChunk2ID; public Int32 SubChunk2Size; public bool Validate() { if (ChunkID != 0x46464952) { Console.WriteLine($"Invalid chunk ID: 0x{ChunkID:X}. Expect 0x46464952"); return false; } // E V A W if (Format != 0x45564157) { Console.WriteLine($"Invalid format: 0x{Format:X}. Expect 0x45564157"); return false; } // t m f if (SubChunk1ID != 0x20746d66) { Console.WriteLine($"Invalid SubChunk1ID: 0x{SubChunk1ID:X}. Expect 0x20746d66"); return false; } if (SubChunk1Size != 16) { Console.WriteLine($"Invalid SubChunk1Size: {SubChunk1Size}. Expect 16"); return false; } if (AudioFormat != 1) { Console.WriteLine($"Invalid AudioFormat: {AudioFormat}. Expect 1"); return false; } if (NumChannels != 1) { Console.WriteLine($"Invalid NumChannels: {NumChannels}. Expect 1"); return false; } if (ByteRate != (SampleRate * NumChannels * BitsPerSample / 8)) { Console.WriteLine($"Invalid byte rate: {ByteRate}."); return false; } if (BlockAlign != (NumChannels * BitsPerSample / 8)) { Console.WriteLine($"Invalid block align: {ByteRate}."); return false; } if (BitsPerSample != 16) { // we support only 16 bits per sample Console.WriteLine($"Invalid bits per sample: {BitsPerSample}. Expect 16"); return false; } return true; } } // It supports only 16-bit, single channel WAVE format. // The sample rate can be any value. public class WaveReader { public WaveReader(String fileName) { if (!File.Exists(fileName)) { throw new ApplicationException($"{fileName} does not exist!"); } using (var stream = File.Open(fileName, FileMode.Open)) { using (var reader = new BinaryReader(stream)) { _header = ReadHeader(reader); if (!_header.Validate()) { throw new ApplicationException($"Invalid wave file ${fileName}"); } SkipMetaData(reader); // now read samples // _header.SubChunk2Size contains number of bytes in total. // we assume each sample is of type int16 byte[] buffer = reader.ReadBytes(_header.SubChunk2Size); short[] samples_int16 = new short[_header.SubChunk2Size / 2]; Buffer.BlockCopy(buffer, 0, samples_int16, 0, buffer.Length); _samples = new float[samples_int16.Length]; for (var i = 0; i < samples_int16.Length; ++i) { _samples[i] = samples_int16[i] / 32768.0F; } } } } private static WaveHeader ReadHeader(BinaryReader reader) { byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(WaveHeader))); GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); WaveHeader header = (WaveHeader)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(WaveHeader)); handle.Free(); return header; } private void SkipMetaData(BinaryReader reader) { var bs = reader.BaseStream; Int32 subChunk2ID = _header.SubChunk2ID; Int32 subChunk2Size = _header.SubChunk2Size; while (bs.Position != bs.Length && subChunk2ID != 0x61746164) { bs.Seek(subChunk2Size, SeekOrigin.Current); subChunk2ID = reader.ReadInt32(); subChunk2Size = reader.ReadInt32(); } _header.SubChunk2ID = subChunk2ID; _header.SubChunk2Size = subChunk2Size; } private WaveHeader _header; // Samples are normalized to the range [-1, 1] private float[] _samples; public int SampleRate => _header.SampleRate; public float[] Samples => _samples; public static void Test(String fileName) { WaveReader reader = new WaveReader(fileName); Console.WriteLine($"samples length: {reader.Samples.Length}"); Console.WriteLine($"samples rate: {reader.SampleRate}"); } } }