private void ParseRiff(Stream st) { // Whenever parse the RIFF, reset everything. Init(); // The first three blocks do not follow the rest. // (WAVE is not followed by size, and there is no hard definition of "WAVEfmt " are always together) // The smallest wave is 44 bytes. Sanity.Requires(st.Length >= 44, $"The stream length {st.Length} is too short, should be at least 44."); // For safety, 2G file will not be supported. Sanity.Requires(st.Length <= int.MaxValue, $"The stream length {st.Length} is too long, at most 2G."); // 0-3: RIFF. st.Read(ChunkNameBytes, 0, 4); Sanity.Requires(GetName() == "RIFF", "Invalid header, should be RIFF."); // 4-7: int 32 for RIFF size. st.Read(Int32Bytes, 0, 4); Sanity.Requires(GetInt32() + 8 == st.Length, "Stream length mismatch, in RIFF chunk."); // 8-11: WAVE. st.Read(ChunkNameBytes, 0, 4); Sanity.Requires(GetName() == "WAVE", "Invalid header, should be WAVE"); // Clear the format chunk and data chunk. FormatChunk = InitEmptyChunk(); DataChunk = InitEmptyChunk(); // Recursively parse the remaining chunks. ParseChunks(st); // Validate the rest values. PostCheck(st); }
private void ParseRecursively(Stream st) { if (st.Position == st.Length) { return; } Sanity.Requires(st.Position + 8 <= st.Length, $"Audio length too short, at 0x{st.Position:x}."); string chunkName = st.ReadBytesToString(4, Encoding.ASCII); int chunkSize = st.ReadBytesToInt32(); Chunk chunk = new Chunk { Name = chunkName, Size = chunkSize, Offset = (int)st.Position }; switch (chunkName) { case "fmt ": Sanity.Requires(FormatChunk.Name != "", "Dupelicate format chunk."); FormatChunk = chunk; return; case "data": Sanity.Requires(DataChunk.Name != "", "Duplicate data chunk."); DataChunk = chunk; return; default: break; } Sanity.Requires(st.Position + chunkSize <= st.Length, $"Audio length too short, at {chunkName}, at 0x{st.Position:x}."); st.Seek(chunkSize, SeekOrigin.Current); ParseRecursively(st); }
private void ParseHeader(Stream st) { Sanity.Requires(st.Length >= 44, $"Audio size is too small. Expected at least 44, which is actually {st.Length} "); Sanity.Requires(st.Length <= int.MaxValue, $"Audio size is too large, more than 2GB."); Sanity.Requires(st.ReadBytesToString(4, Encoding.ASCII) == "RIFF", $"File type error, not RIFF."); Sanity.Requires(st.ReadBytesToInt32() + 8 == st.Length, $"File length error, in RIFF chunk."); Sanity.Requires(st.ReadBytesToString(4, Encoding.ASCII) == "WAVE", $"File type error, not WAVE"); }
/// <summary> /// Shallow parse the audio from a certain path. /// </summary> /// <param name="filePath">The path of the audio file.</param> public void ShallowParse(string filePath) { Sanity.Requires(File.Exists(filePath), $"File not exist: {filePath}."); using (FileStream st = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { // Call the shallow parse stream to keep the core algorithm identical. ShallowParse(st); } }
private void CheckData(Stream st) { Sanity.Requires(DataChunk.Name != "", "No data chunk."); if (IsDeep) { DataBytes = new byte[DataChunk.Size]; st.Seek(DataChunk.Offset, 0); st.Read(DataBytes, 0, DataChunk.Size); } }
private void PostCheck(Stream st) { // The format chunk and data chunk have to exist. Sanity.Requires(FormatChunk.ChunkName != null, "Format error, missing format chunk."); Sanity.Requires(DataChunk.ChunkName != null, "Format error, missing data chunk."); // Go to the format chunk. st.Seek(FormatChunk.ChunkOffset + 8, SeekOrigin.Begin); // First two bytes for wave type. st.Read(Int16Bytes, 0, 2); TypeId = GetInt16(); // Following two bytes for the number of channels. st.Read(Int16Bytes, 0, 2); NumChannels = GetInt16(); Sanity.Requires(NumChannels != 0, "Channel cannot be zero."); // Following four bytes for the sample rate. st.Read(Int32Bytes, 0, 4); SampleRate = GetInt32(); Sanity.Requires(SampleRate != 0, "Sample rate cannot be zeror."); // Following four bytes for the byte rate(bytes per second). // Audio length(seconds) can be calculated by this value and the length of data bytes directly. st.Read(Int32Bytes, 0, 4); ByteRate = GetInt32(); Sanity.Requires(ByteRate != 0, "Byte rate cannot be zero."); AudioTime = (double)DataChunk.ChunkLength / ByteRate; // Block align, how many bytes for every "block", e.g. a single sample from all channels. st.Read(Int16Bytes, 0, 2); BlockAlign = GetInt16(); Sanity.Requires(BlockAlign != 0, "Block align cannot be zero."); // Bits per sample, the audio precision, e.g. 8 bit or 16 bit. st.Read(Int16Bytes, 0, 2); BitsPerSample = GetInt16(); Sanity.Requires(BitsPerSample != 0, "Bits per sample cannot be zero."); // The values of all the wave format are redundant, two extra equations should be followed. Sanity.Requires(ByteRate == BitsPerSample / 8 * SampleRate * NumChannels, "Error in byte rate equation."); Sanity.Requires(BlockAlign == BitsPerSample / 8 * NumChannels, "Error in block align equation."); // In practical, invalid format is seldom seen. // So just let it be. // Set the wave type string. SetWaveType(); }
private void CheckFormat(Stream st) { Sanity.Requires(FormatChunk.Name != "", "No format chunk."); Sanity.Requires(FormatChunk.Size >= 16, $"Format chunk too small. Expected at least 16, which is actually {FormatChunk.Size}."); st.Seek(FormatChunk.Offset, SeekOrigin.Begin); WaveTypeId = st.ReadBytesToInt16(); NumChannels = st.ReadBytesToInt16(); SampleRate = st.ReadBytesToInt32(); ByteRate = st.ReadBytesToInt32(); BlockAlign = st.ReadBytesToInt16(); BitsPerSample = st.ReadBytesToInt16(); Sanity.Requires(ByteRate == SampleRate * BlockAlign, $"Wave format error: byte rate: {ByteRate}, sample rate: {SampleRate}, block align: {BlockAlign}."); Sanity.Requires(BitsPerSample * NumChannels == 8 * BlockAlign, $"Wave format error: bits per sample: {BitsPerSample}, channel: {NumChannels}, block align: {BlockAlign}."); AudioLength = (double)DataChunk.Size / ByteRate; }
private void ParseChunks(Stream st) { // When reaches the end, stop. if (st.Position == st.Length) { return; } // If it is a valid chunk, it has to be at least 8 Sanity.Requires(st.Position + 8 <= st.Length, $"Stream length mismatch, at position {st.Position}."); // Offset is the current position. int offset = (int)st.Position; // The first four bytes(of the 8 bytes) is the name. st.Read(ChunkNameBytes, 0, 4); string name = GetName(); // The next four bytes(of the 8 bytes) is the chunk siz/length. st.Read(Int32Bytes, 0, 4); int length = GetInt32(); // The chunk length should not exceed the end of the file. Sanity.Requires(st.Position + length <= st.Length, $"Stream length mismatch, at position {st.Position}, in chunk {name}."); // Move forward. st.Seek(length, SeekOrigin.Current); // Set the chunk. WaveChunk chunk = new WaveChunk { ChunkName = name, ChunkLength = length, ChunkOffset = offset }; // Format chunk is special. if (chunk.ChunkName == "fmt ") { // There should be only 1 format chunk. Sanity.Requires(FormatChunk.ChunkName == null, $"Format error, at most 1 format chunk."); // The smallest format chunk for PCM is 16. Sanity.Requires(chunk.ChunkLength >= 16, $"Format error, format chunk length {FormatChunk.ChunkLength}, should be at least 16."); FormatChunk = chunk; } // Data chunk is special. if (chunk.ChunkName == "data") { // There should be only 1 data chunk. // NOTE: I'M NOT SURE ABOUT THIS, THERE IS NOWHERE SAY ONLY 1 DATA CHUNK IS ALLOWED. // JUST FOR SIMPLE HERE. Sanity.Requires(DataChunk.ChunkName == null, $"Format error, at most 1 data chunk."); DataChunk = chunk; } // Add this chunk into the list. ChunkList.Add(chunk); // Parse the next chunk recursively. ParseChunks(st); }