Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 62 additions & 28 deletions dsp/ImpulseResponse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<float> 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;
Expand All @@ -31,62 +42,85 @@ 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<const Eigen::VectorXf>(&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];

this->_AdvanceHistoryIndex(numFrames);
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<float> 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<float>(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<float>& rawAudio) {
std::vector<float> resampled;
if (mRawAudioSampleRate == mSampleRate)
resampled = rawAudio;
else
{
std::vector<float> padded(rawAudio.size() + 2, 0.0f);
std::copy(rawAudio.begin(), rawAudio.end(), padded.begin() + 1);
dsp::ResampleCubic<float>(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<size_t>(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;
}
24 changes: 22 additions & 2 deletions dsp/ImpulseResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; };
Expand All @@ -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<float> mRawAudio;
std::vector<float> mRawAudioRight;
double mRawAudioSampleRate;
// Resampled to the required sample rate.
std::vector<float> 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<float> 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<float> mRawAudioRight;
};

}; // namespace dsp
2 changes: 1 addition & 1 deletion dsp/dsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};
Expand Down
24 changes: 14 additions & 10 deletions dsp/wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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<float>& audio, double& sampleRate)
dsp::wav::LoadReturnCode dsp::wav::Load(const char* fileName, std::vector<float>& audio,
double& sampleRate, size_t& numChannels)
{
// FYI: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
// Open the WAV file for reading
Expand Down Expand Up @@ -399,6 +402,7 @@ dsp::wav::LoadReturnCode dsp::wav::Load(const char* fileName, std::vector<float>
return returnCode;
}
}
numChannels = wfd.fmtChunk.valid ? static_cast<size_t>(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
Expand Down
15 changes: 9 additions & 6 deletions dsp/wav.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<float>& 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<float>& audio, double& sampleRate, size_t& numChannels);

// Load samples, 16-bit
void _LoadSamples16(std::ifstream& wavFile, const int chunkSize, std::vector<float>& samples);
Expand Down
6 changes: 4 additions & 2 deletions tools/test_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ int main()

std::vector<float> 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;
Expand Down
Loading