diff --git a/dsp/ImpulseResponse.cpp b/dsp/ImpulseResponse.cpp index 6850f53..367974f 100644 --- a/dsp/ImpulseResponse.cpp +++ b/dsp/ImpulseResponse.cpp @@ -15,7 +15,18 @@ dsp::ImpulseResponse::ImpulseResponse(const char* fileName, const double sampleR , mSampleRate(sampleRate) { // Try to load the WAV - this->mWavState = dsp::wav::Load(fileName, this->mRawAudio, this->mRawAudioSampleRate); + size_t channels = 0; + std::vector interleaved; + this->mWavState = dsp::wav::Load(fileName, interleaved, this->mRawAudioSampleRate, channels); + if (this->mWavState == dsp::wav::LoadReturnCode::SUCCESS) + { + for (size_t i = 0; i < interleaved.size(); i += channels) + { + mRawAudio.push_back(interleaved[i]); + if (channels == 2) + mRawAudioRight.push_back(interleaved[i + 1]); + } + } if (this->mWavState != dsp::wav::LoadReturnCode::SUCCESS) { std::stringstream ss; @@ -31,22 +42,28 @@ dsp::ImpulseResponse::ImpulseResponse(const IRData& irData, const double sampleR , mSampleRate(sampleRate) { this->mRawAudio = irData.mRawAudio; + this->mRawAudioRight = irData.mRawAudioRight; this->mRawAudioSampleRate = irData.mRawAudioSampleRate; this->_SetWeights(); } double** dsp::ImpulseResponse::Process(double** inputs, const size_t numChannels, const size_t numFrames) { - this->_PrepareBuffers(numChannels, numFrames); + if (numChannels == 0 || mWeight.size() == 0) + throw std::runtime_error("IR processing requires input and a loaded IR"); + const auto outputChannels = std::max(numChannels, GetNumIRChannels()); + this->_PrepareBuffers(outputChannels, numFrames); this->_UpdateHistory(inputs, numChannels, numFrames); for (size_t i = 0, j = this->mHistoryIndex - this->mHistoryRequired; i < numFrames; i++, j++) { auto input = Eigen::Map(&this->mHistory[j], this->mHistoryRequired + 1); this->mOutputs[0][i] = (double)this->mWeight.dot(input); + if (GetNumIRChannels() == 2) + this->mOutputs[1][i] = (double)this->mWeightRight.dot(input); } // Copy out for more-than-mono. - for (size_t c = 1; c < numChannels; c++) + for (size_t c = GetNumIRChannels(); c < outputChannels; c++) for (size_t i = 0; i < numFrames; i++) this->mOutputs[c][i] = this->mOutputs[0][i]; @@ -54,39 +71,56 @@ double** dsp::ImpulseResponse::Process(double** inputs, const size_t numChannels return this->_GetPointers(); } +void dsp::ImpulseResponse::Reset(size_t maxFrames, size_t numOutputChannels) +{ + _PrepareBuffers(std::max(numOutputChannels, GetNumIRChannels()), maxFrames); + _EnsureHistorySize(std::max(size_t{1}, maxFrames)); + std::fill(mHistory.begin(), mHistory.end(), 0.0f); + mHistoryIndex = mHistoryRequired; +} + void dsp::ImpulseResponse::_SetWeights() { - if (this->mRawAudioSampleRate == mSampleRate) - { - this->mResampled.resize(this->mRawAudio.size()); - memcpy(this->mResampled.data(), this->mRawAudio.data(), sizeof(float) * this->mResampled.size()); - } - else - { - // Cubic resampling - std::vector padded; - padded.resize(this->mRawAudio.size() + 2); - padded[0] = 0.0f; - padded[padded.size() - 1] = 0.0f; - memcpy(padded.data() + 1, this->mRawAudio.data(), sizeof(float) * this->mRawAudio.size()); - dsp::ResampleCubic(padded, this->mRawAudioSampleRate, mSampleRate, 0.0, this->mResampled); - } - // Simple implementation w/ no resample... - const size_t irLength = std::min(this->mResampled.size(), this->mMaxLength); - this->mWeight.resize(irLength); - // Gain reduction. - // https://github.com/sdatkinson/NeuralAmpModelerPlugin/issues/100#issuecomment-1455273839 - // Add sample rate-dependence - const float gain = pow(10, -18 * 0.05) * 48000 / mSampleRate; - for (size_t i = 0, j = irLength - 1; i < irLength; i++, j--) - this->mWeight[j] = gain * this->mResampled[i]; - this->mHistoryRequired = irLength - 1; + if (!std::isfinite(mSampleRate) || mSampleRate <= 0.0 + || !std::isfinite(mRawAudioSampleRate) || mRawAudioSampleRate <= 0.0 + || mRawAudio.empty() + || (!mRawAudioRight.empty() && mRawAudioRight.size() != mRawAudio.size())) + throw std::runtime_error("Invalid IR samples or sample rate"); + for (const auto* channel : { &mRawAudio, &mRawAudioRight }) + for (const auto sample : *channel) + if (!std::isfinite(sample)) + throw std::runtime_error("Non-finite IR sample"); + const auto makeWeights = [this](const std::vector& rawAudio) { + std::vector resampled; + if (mRawAudioSampleRate == mSampleRate) + resampled = rawAudio; + else + { + std::vector padded(rawAudio.size() + 2, 0.0f); + std::copy(rawAudio.begin(), rawAudio.end(), padded.begin() + 1); + dsp::ResampleCubic(padded, mRawAudioSampleRate, mSampleRate, 0.0, resampled); + } + const size_t irLength = std::min(resampled.size(), mMaxLength); + if (irLength == 0) + throw std::runtime_error("Empty resampled IR"); + Eigen::VectorXf weights(irLength); + // Preserve the existing -18 dB and sample-rate-dependent gain law. + const float gain = pow(10, -18 * 0.05) * 48000 / mSampleRate; + for (size_t i = 0; i < irLength; ++i) + weights[irLength - 1 - i] = gain * resampled[i]; + return weights; + }; + mWeight = makeWeights(mRawAudio); + if (!mRawAudioRight.empty()) + mWeightRight = makeWeights(mRawAudioRight); + mHistoryRequired = static_cast(mWeight.size()) - 1; } dsp::ImpulseResponse::IRData dsp::ImpulseResponse::GetData() { IRData irData; irData.mRawAudio = this->mRawAudio; + irData.mRawAudioRight = this->mRawAudioRight; irData.mRawAudioSampleRate = this->mRawAudioSampleRate; return irData; } diff --git a/dsp/ImpulseResponse.h b/dsp/ImpulseResponse.h index fad1ee0..1501866 100644 --- a/dsp/ImpulseResponse.h +++ b/dsp/ImpulseResponse.h @@ -17,14 +17,29 @@ namespace dsp { +// Mono-input convolution with a mono or stereo IR, loaded from a WAV or IRData. +// Each IR channel uses cubic resampling, an 8192-tap limit, and the gain law +// pow(10.0, -18.0 / 20.0) * 48000 / sampleRate, where sampleRate is the processing rate. class ImpulseResponse : public History { public: struct IRData; ImpulseResponse(const char* fileName, const double sampleRate); ImpulseResponse(const IRData& irData, const double sampleRate); + // Only inputs[0] is read; independent stereo inputs are not processed. + // Call Process(inputs, 1, frames) for mono input. The returned buffer contains + // max(numChannels, GetNumIRChannels()) outputs, with stereo IR channels ordered + // left then right. A mono IR is duplicated to all requested outputs. double** Process(double** inputs, const size_t numChannels, const size_t numFrames) override; + // Return both original IR channels and their source sample rate, before + // resampling, truncation, or gain adjustment, for storage or reconstruction. IRData GetData(); + // Number of channels in the IR: one for mono, two for stereo. + size_t GetNumIRChannels() const { return mRawAudioRight.empty() ? 1 : 2; } + // Call off the audio thread to reserve processing storage and clear history. + // numOutputChannels should match numChannels in subsequent Process calls. + // Blocks up to maxFrames reuse that storage, including variable block sizes. + void Reset(size_t maxFrames, size_t numOutputChannels = 2); double GetSampleRate() const { return mSampleRate; }; // TODO states for the IR class dsp::wav::LoadReturnCode GetWavState() const { return this->mWavState; }; @@ -38,20 +53,25 @@ class ImpulseResponse : public History dsp::wav::LoadReturnCode mWavState; // Keep a copy of the raw audio that was loaded so that it can be resampled std::vector mRawAudio; + std::vector mRawAudioRight; double mRawAudioSampleRate; - // Resampled to the required sample rate. - std::vector mResampled; double mSampleRate; const size_t mMaxLength = 8192; // The weights Eigen::VectorXf mWeight; + Eigen::VectorXf mWeightRight; }; struct dsp::ImpulseResponse::IRData { + // Original mono/left samples, before resampling or gain adjustment. std::vector mRawAudio; + // Source sample rate in Hz. double mRawAudioSampleRate; + // Original right samples. Leave empty for mono behavior; stereo IRs require + // the same number of frames as mRawAudio. + std::vector mRawAudioRight; }; }; // namespace dsp diff --git a/dsp/dsp.h b/dsp/dsp.h index 3ba754e..ea522d7 100644 --- a/dsp/dsp.h +++ b/dsp/dsp.h @@ -100,9 +100,9 @@ class History : public DSP // Shall always be in the range [mHistoryRequired, mHistory.size()). size_t mHistoryIndex; -private: // Make sure that the history array is long enough. void _EnsureHistorySize(const size_t bufferSize); +private: // Copy the end of the history back to the fron and reset mHistoryIndex void _RewindHistory(); }; diff --git a/dsp/wav.cpp b/dsp/wav.cpp index f77b31a..f4d57c9 100644 --- a/dsp/wav.cpp +++ b/dsp/wav.cpp @@ -123,7 +123,7 @@ std::string dsp::wav::GetMsgForLoadReturnCode(LoadReturnCode retCode) 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_OTHER): message << "Unsupported file format."; break; - case (LoadReturnCode::ERROR_NOT_MONO): message << "File is not mono."; break; + case (LoadReturnCode::ERROR_UNSUPPORTED_CHANNEL_COUNT): message << "Only mono and stereo WAV files are supported."; break; case (LoadReturnCode::ERROR_UNSUPPORTED_BITS_PER_SAMPLE): message << "Unsupported bits per sample"; break; case (dsp::wav::LoadReturnCode::ERROR_OTHER): message << "???"; break; default: message << "???"; break; @@ -193,13 +193,8 @@ dsp::wav::LoadReturnCode ReadFmtChunk(std::ifstream& wavFile, WaveFileData& wfd, } wfd.fmtChunk.numChannels = ReadShort(wavFile); - // HACK - // 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; - } + if (wfd.fmtChunk.numChannels < 1 || wfd.fmtChunk.numChannels > 2) + return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_CHANNEL_COUNT; wfd.fmtChunk.sampleRate = ReadInt(wavFile); wfd.fmtChunk.byteRate = ReadInt(wavFile); @@ -301,9 +296,14 @@ dsp::wav::LoadReturnCode ReadDataChunk(std::ifstream& wavFile, WaveFileData& wfd return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; } - // Size of the data chunk, in bits. + // Size of the data chunk, in bytes. wfd.dataChunk.size = ReadInt(wavFile); + const auto bytesPerFrame = wfd.fmtChunk.numChannels * (wfd.fmtChunk.bitsPerSample / 8); + if (bytesPerFrame <= 0 || wfd.dataChunk.size <= 0 + || wfd.dataChunk.size % bytesPerFrame != 0 || wfd.fmtChunk.sampleRate <= 0) + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; + const int audioFormat = GetAudioFormat(wfd); if (audioFormat == AUDIO_FORMAT_IEEE) { @@ -334,11 +334,14 @@ dsp::wav::LoadReturnCode ReadDataChunk(std::ifstream& wavFile, WaveFileData& wfd std::cerr << "Error: Unsupported audio format: " << audioFormat << std::endl; return dsp::wav::LoadReturnCode::ERROR_UNSUPPORTED_FORMAT_OTHER; } + if (!wavFile.good()) + return dsp::wav::LoadReturnCode::ERROR_INVALID_FILE; wfd.dataChunk.valid = true; return dsp::wav::LoadReturnCode::SUCCESS; } -dsp::wav::LoadReturnCode dsp::wav::Load(const char* fileName, std::vector& audio, double& sampleRate) +dsp::wav::LoadReturnCode dsp::wav::Load(const char* fileName, std::vector& audio, + double& sampleRate, size_t& numChannels) { // FYI: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html // Open the WAV file for reading @@ -399,6 +402,7 @@ dsp::wav::LoadReturnCode dsp::wav::Load(const char* fileName, std::vector return returnCode; } } + numChannels = wfd.fmtChunk.valid ? static_cast(wfd.fmtChunk.numChannels) : 0; wavFile.close(); if (!wfd.dataChunk.valid) { // This implicitly asserts that the fmt chunk was read and gave us the sample rate diff --git a/dsp/wav.h b/dsp/wav.h index 83f3ef8..14711f8 100644 --- a/dsp/wav.h +++ b/dsp/wav.h @@ -27,18 +27,21 @@ enum class LoadReturnCode ERROR_UNSUPPORTED_FORMAT_MULAW, ERROR_UNSUPPORTED_FORMAT_OTHER, ERROR_UNSUPPORTED_BITS_PER_SAMPLE, - ERROR_NOT_MONO, - ERROR_OTHER + ERROR_OTHER, + ERROR_UNSUPPORTED_CHANNEL_COUNT }; // Get a string describing the error std::string GetMsgForLoadReturnCode(LoadReturnCode rc); -// Load a WAV file into a provided array of doubles, -// And note the sample rate. +// Load mono or stereo WAV samples. On success, audio contains interleaved +// samples (L0, R0, L1, R1, ... for stereo), sampleRate is the source rate in Hz, +// and numChannels is 1 or 2. Mono samples remain sequential. // -// Returns: as per return cases above -LoadReturnCode Load(const char* fileName, std::vector& audio, double& sampleRate); +// This replaces the three-argument, mono-only API: callers must supply a size_t +// channel-count output and handle interleaved stereo data. ERROR_NOT_MONO has +// been removed; unsupported channel counts return ERROR_UNSUPPORTED_CHANNEL_COUNT. +LoadReturnCode Load(const char* fileName, std::vector& audio, double& sampleRate, size_t& numChannels); // Load samples, 16-bit void _LoadSamples16(std::ifstream& wavFile, const int chunkSize, std::vector& samples); diff --git a/tools/test_wav.cpp b/tools/test_wav.cpp index 964c56f..79bd8b6 100644 --- a/tools/test_wav.cpp +++ b/tools/test_wav.cpp @@ -40,12 +40,14 @@ int main() std::vector audio; double sampleRate = 0.0; + size_t numChannels = 0; const auto utf8Path = ToUTF8(wavPath); - const auto result = dsp::wav::Load(utf8Path.c_str(), audio, sampleRate); + const auto result = dsp::wav::Load(utf8Path.c_str(), audio, sampleRate, numChannels); std::filesystem::remove_all(testDirectory); - if (result != dsp::wav::LoadReturnCode::SUCCESS || audio.size() != 1 || sampleRate != 48000.0) + if (result != dsp::wav::LoadReturnCode::SUCCESS || audio.size() != 1 || sampleRate != 48000.0 + || numChannels != 1) { std::cerr << "Failed to load WAV from UTF-8 path" << std::endl; return 1;