diff --git a/dsp/NoiseGate.h b/dsp/NoiseGate.h index 691e3cd..c3af01c 100644 --- a/dsp/NoiseGate.h +++ b/dsp/NoiseGate.h @@ -60,7 +60,7 @@ class TriggerParams , mRatio(ratio) , mOpenTime(openTime) , mHoldTime(holdTime) - , mCloseTime(closeTime){}; + , mCloseTime(closeTime) {}; double GetTime() const { return this->mTime; }; double GetThreshold() const { return this->mThreshold; }; diff --git a/dsp/RecursiveLinearFilter.h b/dsp/RecursiveLinearFilter.h index 737d297..291c268 100644 --- a/dsp/RecursiveLinearFilter.h +++ b/dsp/RecursiveLinearFilter.h @@ -56,7 +56,7 @@ class LevelParams : public dsp::Params public: LevelParams(const double gain) : Params() - , mGain(gain){}; + , mGain(gain) {}; double GetGain() const { return this->mGain; }; private: @@ -68,7 +68,7 @@ class Level : public Base { public: Level() - : Base(1, 0){}; + : Base(1, 0) {}; // Invalid usage: require a pointer to recursive_linear_filter::Params so // that SetCoefficients() is defined. void SetParams(const LevelParams& params) { this->mInputCoefficients[0] = params.GetGain(); }; @@ -85,7 +85,7 @@ class BiquadParams : public dsp::Params , mFrequency(frequency) , mGainDB(gainDB) , mQuality(quality) - , mSampleRate(sampleRate){}; + , mSampleRate(sampleRate) {}; // Parameters defined in // https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html @@ -105,7 +105,7 @@ class Biquad : public Base { public: Biquad() - : Base(3, 3){}; + : Base(3, 3) {}; virtual void SetParams(const BiquadParams& params) = 0; protected: @@ -139,7 +139,7 @@ class HighPassParams : public dsp::Params HighPassParams(const double sampleRate, const double frequency) : dsp::Params() , mFrequency(frequency) - , mSampleRate(sampleRate){}; + , mSampleRate(sampleRate) {}; double GetAlpha() const { @@ -156,7 +156,7 @@ class HighPass : public Base { public: HighPass() - : Base(2, 2){}; + : Base(2, 2) {}; void SetParams(const HighPassParams& params) { const double alpha = params.GetAlpha(); @@ -174,7 +174,7 @@ class LowPassParams : public dsp::Params LowPassParams(const double sampleRate, const double frequency) : dsp::Params() , mFrequency(frequency) - , mSampleRate(sampleRate){}; + , mSampleRate(sampleRate) {}; double GetAlpha() const { @@ -191,7 +191,7 @@ class LowPass : public Base { public: LowPass() - : Base(1, 2){}; + : Base(1, 2) {}; void SetParams(const LowPassParams& params) { const double alpha = params.GetAlpha(); diff --git a/dsp/version.h b/dsp/version.h index ab41709..6ad7ced 100644 --- a/dsp/version.h +++ b/dsp/version.h @@ -2,7 +2,7 @@ #define version_h #define AUDIO_DSP_TOOLS_VERSION_MAJOR 0 -#define AUDIO_DSP_TOOLS_VERSION_MINOR 0 +#define AUDIO_DSP_TOOLS_VERSION_MINOR 1 #define AUDIO_DSP_TOOLS_VERSION_PATCH 0 #endif diff --git a/dsp/wav.cpp b/dsp/wav.cpp index eca231e..c72fe81 100644 --- a/dsp/wav.cpp +++ b/dsp/wav.cpp @@ -16,27 +16,92 @@ #include "wav.h" +struct WaveFileData +{ + // TODO use types like uint32_t, etc + struct RiffChunk + { + bool valid = false; // Have we gotten this info yet? + int size; // NB: Of the rest of the file + char format[4]; + } riffChunk; + + struct FmtChunk + { + bool valid = false; + int size; + // PCM: 1 + // IEEE: 3 + // A-law: 6 + // mu-law: 7 + // Extensible: 65534 + unsigned short audioFormat; + short numChannels; + int sampleRate; + int byteRate; + short blockAlign; + short bitsPerSample; + struct Extensible + { + uint16_t validBitsPerSample; + uint16_t channelMask; + uint32_t subFormat; // PCM, IEEE + } extensible; + } fmtChunk; + + struct FactChunk + { + bool valid = false; + int size; + int numSamples; + } factChunk; + + struct DataChunk + { + bool valid = false; + char id[4]; + int size; + } dataChunk; +}; + +const unsigned short AUDIO_FORMAT_PCM = 1; +const unsigned short AUDIO_FORMAT_IEEE = 3; +const unsigned short AUDIO_FORMAT_ALAW = 6; +const unsigned short AUDIO_FORMAT_MULAW = 7; +const unsigned short AUDIO_FORMAT_EXTENSIBLE = 65534; + bool idIsNotJunk(char* id) { return strncmp(id, "RIFF", 4) == 0 || strncmp(id, "WAVE", 4) == 0 || strncmp(id, "fmt ", 4) == 0 || strncmp(id, "data", 4) == 0; } -bool ReadChunkAndSkipJunk(std::ifstream& file, char* chunkID) +int ReadInt(std::ifstream& file) { - file.read(chunkID, 4); - while (!idIsNotJunk(chunkID) && file.good()) - { - int junkSize; - file.read(reinterpret_cast(&junkSize), 4); - file.ignore(junkSize); - // Unused byte if junkSize is odd - if ((junkSize % 2) == 1) - file.ignore(1); - // And now we should be ready for data... - file.read(chunkID, 4); - } - return file.good(); + int value; + file.read(reinterpret_cast(&value), 4); + return value; +} + +short ReadShort(std::ifstream& file) +{ + short value; + file.read(reinterpret_cast(&value), 2); + return value; +} + +unsigned short ReadUnsignedShort(std::ifstream& file) +{ + unsigned short value; + file.read(reinterpret_cast(&value), 2); + return value; +} + +dsp::wav::LoadReturnCode ReadJunk(std::ifstream& file) +{ + int chunkSize = ReadInt(file); + file.ignore(chunkSize + (chunkSize % 2)); // Pad to 2 bytes at a time + return file.good() ? dsp::wav::LoadReturnCode::SUCCESS : dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } std::string dsp::wav::GetMsgForLoadReturnCode(LoadReturnCode retCode) @@ -55,9 +120,7 @@ std::string dsp::wav::GetMsgForLoadReturnCode(LoadReturnCode retCode) case (LoadReturnCode::ERROR_INVALID_FILE): message << "WAV file contents are invalid."; break; case (LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_ALAW): message << "Unsupported file format \"A-law\""; break; case (LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_MULAW): message << "Unsupported file format \"mu-law\""; break; - case (LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_EXTENSIBLE): - message << "Unsupported file format \"extensible\""; - break; + case (LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_OTHER): message << "Unsupported file format."; break; case (LoadReturnCode::ERROR_NOT_MONO): message << "File is not mono."; break; case (LoadReturnCode::ERROR_UNSUPPORTED_BITS_PER_SAMPLE): message << "Unsupported bits per sample"; break; case (dsp::wav::LoadReturnCode::ERROR_OTHER): message << "???"; break; @@ -67,175 +130,274 @@ std::string dsp::wav::GetMsgForLoadReturnCode(LoadReturnCode retCode) return message.str(); } -dsp::wav::LoadReturnCode dsp::wav::Load(const char* fileName, std::vector& audio, double& sampleRate) +dsp::wav::LoadReturnCode ReadRiffChunk(std::ifstream& wavFile, WaveFileData::RiffChunk& chunk) { - // FYI: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html - // Open the WAV file for reading - std::ifstream wavFile(fileName, std::ios::binary); - - // Check if the file was opened successfully - if (!wavFile.is_open()) - { - std::cerr << "Error opening WAV file" << std::endl; - return dsp::wav::LoadReturnCode::ERROR_OPENING; - } - - // WAV file has 3 "chunks": RIFF ("RIFF"), format ("fmt ") and data ("data"). - // Read the WAV file header - char chunkId[4]; - if (!ReadChunkAndSkipJunk(wavFile, chunkId)) + if (chunk.valid) { - std::cerr << "Error while reading for next chunk." << std::endl; + std::cerr << "Error: RIFF chunk already read." << std::endl; return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } - - if (strncmp(chunkId, "RIFF", 4) != 0) + chunk.size = ReadInt(wavFile); + wavFile.read(chunk.format, 4); + if (strncmp(chunk.format, "WAVE", 4) != 0) { - std::cerr << "Error: File does not start with expected RIFF chunk. Got" << chunkId << " instead." << std::endl; - return dsp::wav::LoadReturnCode::ERROR_NOT_RIFF; - } - - int chunkSize; - wavFile.read(reinterpret_cast(&chunkSize), 4); - - char format[4]; - wavFile.read(format, 4); - if (strncmp(format, "WAVE", 4) != 0) - { - std::cerr << "Error: Files' second chunk (format) is not expected WAV. Got" << format << " instead." << std::endl; + std::cerr << "Error: File format is not expected 'WAVE'. Got '" << chunk.format << "' instead." << std::endl; return dsp::wav::LoadReturnCode::ERROR_NOT_WAVE; } + chunk.valid = true; + return dsp::wav::LoadReturnCode::SUCCESS; +} - // Read the format chunk - char subchunk1Id[4]; - if (!ReadChunkAndSkipJunk(wavFile, subchunk1Id)) +dsp::wav::LoadReturnCode ReadFmtChunk(std::ifstream& wavFile, WaveFileData& wfd, double& sampleRate) +{ + if (wfd.fmtChunk.valid) { - std::cerr << "Error while reading for next chunk." << std::endl; + std::cerr << "Error: Format chunk already read." << std::endl; return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } - if (strncmp(subchunk1Id, "fmt ", 4) != 0) + if (!wfd.riffChunk.valid) { - std::cerr << "Error: Invalid WAV file missing expected fmt section; got " << subchunk1Id << " instead." - << std::endl; - return dsp::wav::LoadReturnCode::ERROR_MISSING_FMT; + std::cerr << "Error: Missing RIFF chunk." << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } - int subchunk1Size; - wavFile.read(reinterpret_cast(&subchunk1Size), 4); - if (subchunk1Size < 16) + wfd.fmtChunk.size = ReadInt(wavFile); + if (wfd.fmtChunk.size < 16) { - std::cerr << "WAV chunk 1 size is " << subchunk1Size + std::cerr << "WAV chunk 1 size is " << wfd.fmtChunk.size << ", which is smaller than the requried 16 to fit the expected " "information." << std::endl; return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } - unsigned short audioFormat; - wavFile.read(reinterpret_cast(&audioFormat), 2); - const short AUDIO_FORMAT_PCM = 1; - const short AUDIO_FORMAT_IEEE = 3; - std::unordered_set supportedFormats{AUDIO_FORMAT_PCM, AUDIO_FORMAT_IEEE}; - if (supportedFormats.find(audioFormat) == supportedFormats.end()) + wfd.fmtChunk.audioFormat = ReadUnsignedShort(wavFile); + std::unordered_set supportedFormats{AUDIO_FORMAT_PCM, AUDIO_FORMAT_IEEE, AUDIO_FORMAT_EXTENSIBLE}; + if (supportedFormats.find(wfd.fmtChunk.audioFormat) == supportedFormats.end()) { std::cerr << "Error: Unsupported WAV format detected. "; - switch (audioFormat) + switch (wfd.fmtChunk.audioFormat) { - case 6: std::cerr << "(Got: A-law)" << std::endl; return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_ALAW; - case 7: + case AUDIO_FORMAT_ALAW: + std::cerr << "(Got: A-law)" << std::endl; + return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_ALAW; + case AUDIO_FORMAT_MULAW: std::cerr << "(Got: mu-law)" << std::endl; return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_MULAW; - case 65534: - std::cerr << "(Got: Extensible)" << std::endl; - return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_EXTENSIBLE; default: - std::cerr << "(Got unknown format " << audioFormat << ")" << std::endl; + std::cerr << "(Got unknown format " << wfd.fmtChunk.audioFormat << ")" << std::endl; return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } } - short numChannels; - wavFile.read(reinterpret_cast(&numChannels), 2); + wfd.fmtChunk.numChannels = ReadShort(wavFile); // HACK - if (numChannels != 1) + // Note for future: for multi-channel files, samples are laid out with channel in the inner loop. + if (wfd.fmtChunk.numChannels != 1) { std::cerr << "Require mono (using for IR loading)" << std::endl; return dsp::wav::LoadReturnCode::ERROR_NOT_MONO; } - int iSampleRate; - wavFile.read(reinterpret_cast(&iSampleRate), 4); - // Store in format we assume (SR is double) - sampleRate = (double)iSampleRate; + wfd.fmtChunk.sampleRate = ReadInt(wavFile); + wfd.fmtChunk.byteRate = ReadInt(wavFile); + wfd.fmtChunk.blockAlign = ReadShort(wavFile); + wfd.fmtChunk.bitsPerSample = ReadShort(wavFile); + int bytesRead = 16; - int byteRate; - wavFile.read(reinterpret_cast(&byteRate), 4); + if (wfd.fmtChunk.audioFormat == AUDIO_FORMAT_EXTENSIBLE) + { + unsigned short cbSize = ReadUnsignedShort(wavFile); + // Do we need to assert or modify the data loading below if this doesn't match bitsPerSample? + wfd.fmtChunk.extensible.validBitsPerSample = ReadUnsignedShort(wavFile); + auto read_u32 = [&]() -> uint32_t { + uint8_t b[4]; + wavFile.read((char*)b, 4); + return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24); + }; + wfd.fmtChunk.extensible.channelMask = read_u32(); + uint8_t guid[16]; + wavFile.read((char*)guid, 16); + wfd.fmtChunk.extensible.subFormat = guid[1] << 8 | guid[0]; + bytesRead += cbSize + 2; // Don't forget the 2 for the cbSize itself! + } + + // Skip any extra bytes in the fmt chunk + // This should probably be a remainder of a dword so that we're mod-4 + if (wfd.fmtChunk.size > bytesRead) + { + const int extraBytes = wfd.fmtChunk.size - bytesRead; + if (extraBytes >= 4) + { + std::cerr << "More than 4 extra bytes in fmt chunk." << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; + } + wavFile.ignore(extraBytes); + } - short blockAlign; - wavFile.read(reinterpret_cast(&blockAlign), 2); + // Store SR for final return + sampleRate = (double)wfd.fmtChunk.sampleRate; - short bitsPerSample; - wavFile.read(reinterpret_cast(&bitsPerSample), 2); + wfd.fmtChunk.valid = true; + return dsp::wav::LoadReturnCode::SUCCESS; +} - // The default is for there to be 16 bytes in the fmt chunk, but sometimes - // it's different. - if (subchunk1Size > 16) +dsp::wav::LoadReturnCode ReadFactChunk(std::ifstream& wavFile, WaveFileData& wfd) +{ + if (wfd.factChunk.valid) { - const int extraBytes = subchunk1Size - 16; - const int skipChars = extraBytes / 4 * 4; // truncate to dword size - wavFile.ignore(skipChars); - const int remainder = extraBytes % 4; - wavFile.read(reinterpret_cast(&byteRate), remainder); + std::cerr << "Error: Duplicate fact chunk." << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } + if (!wfd.riffChunk.valid) + { + std::cerr << "Error: Missing RIFF chunk." << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; + } + // We could assert that the fmt chunk was also read first but I'm not sure that's necessary for the file to be valid. + + wfd.factChunk.size = ReadInt(wavFile); + if (wfd.factChunk.size != 4) + { + std::cerr << "Error: Invalid fact chunk size. Only 4 is supported; got " << wfd.factChunk.size << " instead." + << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; + } + wfd.factChunk.numSamples = ReadInt(wavFile); - // Read the data chunk - char subchunk2Id[4]; - if (!ReadChunkAndSkipJunk(wavFile, subchunk2Id)) + wfd.factChunk.valid = true; + return dsp::wav::LoadReturnCode::SUCCESS; +} + +int GetAudioFormat(WaveFileData& wfd) +{ + return wfd.fmtChunk.audioFormat == AUDIO_FORMAT_EXTENSIBLE ? wfd.fmtChunk.extensible.subFormat + : wfd.fmtChunk.audioFormat; +} + +dsp::wav::LoadReturnCode ReadDataChunk(std::ifstream& wavFile, WaveFileData& wfd, std::vector& audio) +{ + if (wfd.dataChunk.valid) { - std::cerr << "Error while reading for next chunk." << std::endl; + std::cerr << "Error: Already read data chunk." << std::endl; return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } - if (strncmp(subchunk2Id, "data", 4) != 0) + if (!wfd.riffChunk.valid) { - std::cerr << "Error: Invalid WAV file" << std::endl; + std::cerr << "Error: Missing RIFF chunk." << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; + } + if (!wfd.fmtChunk.valid) // fmt chunk must come before data chunk + { + std::cerr << "Error: Tried to read data chunk before fmt chunk." << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; + } + if (wfd.fmtChunk.audioFormat == AUDIO_FORMAT_EXTENSIBLE + && !wfd.factChunk.valid) // fact chunk must come before data chunk + { + std::cerr << "Error: Tried to read data chunk before fact chunk for extensible format WAVE file." << std::endl; return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } // Size of the data chunk, in bits. - int subchunk2Size; - wavFile.read(reinterpret_cast(&subchunk2Size), 4); + wfd.dataChunk.size = ReadInt(wavFile); + const int audioFormat = GetAudioFormat(wfd); if (audioFormat == AUDIO_FORMAT_IEEE) { - if (bitsPerSample == 32) - dsp::wav::_LoadSamples32(wavFile, subchunk2Size, audio); + if (wfd.fmtChunk.bitsPerSample == 32) + dsp::wav::_LoadSamples32(wavFile, wfd.dataChunk.size, audio); else { - std::cerr << "Error: Unsupported bits per sample for IEEE files: " << bitsPerSample << std::endl; + std::cerr << "Error: Unsupported bits per sample for IEEE files: " << wfd.fmtChunk.bitsPerSample << std::endl; return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_BITS_PER_SAMPLE; } } else if (audioFormat == AUDIO_FORMAT_PCM) { - if (bitsPerSample == 16) - dsp::wav::_LoadSamples16(wavFile, subchunk2Size, audio); - else if (bitsPerSample == 24) - dsp::wav::_LoadSamples24(wavFile, subchunk2Size, audio); - else if (bitsPerSample == 32) - dsp::wav::_LoadSamples32(wavFile, subchunk2Size, audio); + if (wfd.fmtChunk.bitsPerSample == 16) + dsp::wav::_LoadSamples16(wavFile, wfd.dataChunk.size, audio); + else if (wfd.fmtChunk.bitsPerSample == 24) + dsp::wav::_LoadSamples24(wavFile, wfd.dataChunk.size, audio); + else if (wfd.fmtChunk.bitsPerSample == 32) + dsp::wav::_LoadSamples32(wavFile, wfd.dataChunk.size, audio); else { - std::cerr << "Error: Unsupported bits per sample for PCM files: " << bitsPerSample << std::endl; + std::cerr << "Error: Unsupported bits per sample for PCM files: " << wfd.fmtChunk.bitsPerSample << std::endl; return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_BITS_PER_SAMPLE; } } + else + { + std::cerr << "Error: Unsupported audio format: " << audioFormat << std::endl; + return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_OTHER; + } + wfd.dataChunk.valid = true; + return dsp::wav::LoadReturnCode::SUCCESS; +} - // Close the WAV file - wavFile.close(); +dsp::wav::LoadReturnCode dsp::wav::Load(const char* fileName, std::vector& audio, double& sampleRate) +{ + // FYI: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html + // Open the WAV file for reading + std::ifstream wavFile(fileName, std::ios::binary); - // Print the number of samples - // std::cout << "Number of samples: " << samples.size() << std::endl; + // Check if the file was opened successfully + if (!wavFile.is_open()) + { + std::cerr << "Error opening WAV file" << std::endl; + return dsp::wav::LoadReturnCode::ERROR_OPENING; + } + char chunkId[4]; + auto ReadChunkID = [&]() { wavFile.read(chunkId, 4); }; + + WaveFileData wfd; + dsp::wav::LoadReturnCode returnCode; + while (!wfd.dataChunk.valid && !wavFile.eof()) + { + ReadChunkID(); + if (!wfd.riffChunk.valid && strncmp(chunkId, "RIFF", 4) != 0) + { + { + std::cerr << "Error: File does not start with expected RIFF chunk. Got" << chunkId << " instead." << std::endl; + wavFile.close(); + return dsp::wav::LoadReturnCode::ERROR_NOT_RIFF; + } + } + // Read the various chunks + if (strncmp(chunkId, "RIFF", 4) == 0) + { + returnCode = ReadRiffChunk(wavFile, wfd.riffChunk); + } + else if (strncmp(chunkId, "fmt ", 4) == 0) + { + returnCode = ReadFmtChunk(wavFile, wfd, sampleRate); + } + else if (strncmp(chunkId, "fact", 4) == 0) + { + returnCode = ReadFactChunk(wavFile, wfd); + } + else if (strncmp(chunkId, "data", 4) == 0) + { + returnCode = ReadDataChunk(wavFile, wfd, audio); + } + else + { // There might be junk chunks; just ignore them. + returnCode = ReadJunk(wavFile); + } + if (returnCode != dsp::wav::LoadReturnCode::SUCCESS) + { + wavFile.close(); + return returnCode; + } + } + wavFile.close(); + if (!wfd.dataChunk.valid) + { // This implicitly asserts that the fmt chunk was read and gave us the sample rate + std::cerr << "Error: File does not contain expected data chunk." << std::endl; + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; + } return dsp::wav::LoadReturnCode::SUCCESS; } diff --git a/dsp/wav.h b/dsp/wav.h index 336eddf..772565e 100644 --- a/dsp/wav.h +++ b/dsp/wav.h @@ -25,7 +25,7 @@ enum class LoadReturnCode ERROR_INVALID_FILE, ERROR_UNSUPPORTED_FORMAT_ALAW, ERROR_UNSUPPORTED_FORMAT_MULAW, - ERROR_UNSUPPORTED_FORMAT_EXTENSIBLE, + ERROR_UNSUPPORTED_FORMAT_OTHER, ERROR_UNSUPPORTED_BITS_PER_SAMPLE, ERROR_NOT_MONO, ERROR_OTHER