NeuralAmpModelerPlugin

Plugin for Neural Amp Modeler
Log | Files | Refs | Submodules | README | LICENSE

commit 13f066ed56bb664bf5060c22e68c92e9a1045285
parent a5725c10034ec0295b1be5d1439dabf0db456b0a
Author: Steven Atkinson <steven@atkinson.mn>
Date:   Sun,  3 Dec 2023 19:23:37 -0800

[ENHANCEMENT] Support for different sample rates (#397)

* Update on core, NAM models expose expected sample rates

* Add resample files to XCode project

* Implement structure of the resampler. Dummy algorithm in place

* Implement placeholder for finalize_()

* Implement dummy resampling that transfers data to encapsulated buffer arrays.

* Rearrange files

* Format

* Update iPlug2

* Implement resampling using iPlug2 tools

* Fix XCode project

* Format

* Fix iPlug2 commit, add aax and app filters to VS solution

* Update core
Diffstat:
MNeuralAmpModeler/NeuralAmpModeler.cpp | 20+++++++++++++++-----
MNeuralAmpModeler/NeuralAmpModeler.h | 141++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
MNeuralAmpModeler/projects/NeuralAmpModeler-aax.vcxproj | 2++
MNeuralAmpModeler/projects/NeuralAmpModeler-aax.vcxproj.filters | 9+++++++++
MNeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj | 2++
MNeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj.filters | 9+++++++++
MNeuralAmpModeler/projects/NeuralAmpModeler-iOS.xcodeproj/project.pbxproj | 24++++++++++++++----------
MNeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj | 2++
MNeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj.filters | 9+++++++++
9 files changed, 199 insertions(+), 19 deletions(-)

diff --git a/NeuralAmpModeler/NeuralAmpModeler.cpp b/NeuralAmpModeler/NeuralAmpModeler.cpp @@ -53,6 +53,7 @@ const IVStyle style = const IVStyle titleStyle = DEFAULT_STYLE.WithValueText(IText(30, COLOR_WHITE, "Michroma-Regular")).WithDrawFrame(false).WithShadowOffset(2.f); + NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info) : Plugin(info, MakeConfig(kNumParams, kNumPresets)) { @@ -382,7 +383,7 @@ void NeuralAmpModeler::OnReset() mOutputSender.Reset(sampleRate); mCheckSampleRateWarning = true; // If there is a model or IR loaded, they need to be checked for resampling. - _ResampleModelAndIR(); + _ResetModelAndIR(sampleRate, GetBlockSize()); } void NeuralAmpModeler::OnIdle() @@ -612,11 +613,17 @@ void NeuralAmpModeler::_NormalizeModelOutput(iplug::sample** buffer, const size_ } } -void NeuralAmpModeler::_ResampleModelAndIR() +void NeuralAmpModeler::_ResetModelAndIR(const double sampleRate, const int maxBlockSize) { - const auto sampleRate = GetSampleRate(); // Model - // TODO + if (mStagedModel != nullptr) + { + mStagedModel->Reset(sampleRate, maxBlockSize); + } + else if (mModel != nullptr) + { + mModel->Reset(sampleRate, maxBlockSize); + } // IR if (mStagedIR != nullptr) @@ -645,7 +652,10 @@ std::string NeuralAmpModeler::_StageModel(const WDL_String& modelPath) try { auto dspPath = std::filesystem::u8path(modelPath.Get()); - mStagedModel = nam::get_dsp(dspPath); + std::unique_ptr<nam::DSP> model = nam::get_dsp(dspPath); + std::unique_ptr<ResamplingNAM> temp = std::make_unique<ResamplingNAM>(std::move(model), GetSampleRate()); + temp->Reset(GetSampleRate(), GetBlockSize()); + mStagedModel = std::move(temp); mNAMPath = modelPath; SendControlMsgFromDelegate(kCtrlTagModelFileBrowser, kMsgTagLoadedModel, mNAMPath.GetLength(), mNAMPath.Get()); } diff --git a/NeuralAmpModeler/NeuralAmpModeler.h b/NeuralAmpModeler/NeuralAmpModeler.h @@ -9,6 +9,8 @@ #include "IPlug_include_in_plug_hdr.h" #include "ISender.h" +#define M_PI 3.141592653589793238462643383279 // Needed by NonIntegerResampler.h +#include "NonIntegerResampler.h" const int kNumPresets = 1; // The plugin is mono inside @@ -68,6 +70,137 @@ enum EMsgTags kNumMsgTags }; +// Get the sample rate of a NAM model. +// Sometimes, the model doesn't know its own sample rate; this wrapper guesses 48k based on the way that most +// people have used NAM in the past. +double GetNAMSampleRate(const std::unique_ptr<nam::DSP>& model) +{ + // Some models are from when we didn't have sample rate in the model. + // For those, this wraps with the assumption that they're 48k models, which is probably true. + const double assumedSampleRate = 48000.0; + const double reportedEncapsulatedSampleRate = model->GetExpectedSampleRate(); + const double encapsulatedSampleRate = + reportedEncapsulatedSampleRate <= 0.0 ? assumedSampleRate : reportedEncapsulatedSampleRate; + return encapsulatedSampleRate; +}; + +class ResamplingNAM : public nam::DSP +{ +public: + // Resampling wrapper around the NAM models + ResamplingNAM(std::unique_ptr<nam::DSP> encapsulated, const double expected_sample_rate) + : nam::DSP(expected_sample_rate) + , mEncapsulated(std::move(encapsulated)) + , mResampler(GetNAMSampleRate(mEncapsulated), iplug::ESRCMode::kLancsoz) + { + // Assign the encapsulated object's processing function to this object's member so that the resampler can use it: + auto ProcessBlockFunc = [&](NAM_SAMPLE** input, NAM_SAMPLE** output, int numFrames) { + mEncapsulated->process(input[0], output[0], numFrames); + mEncapsulated->finalize_(numFrames); + }; + mBlockProcessFunc = ProcessBlockFunc; + + // Get the other information from the encapsulated NAM so that we can tell the outside world about what we're + // holding. + if (mEncapsulated->HasLoudness()) + SetLoudness(mEncapsulated->GetLoudness()); + + // NOTE: prewarm samples doesn't mean anything--we can prewarm the encapsulated model as it likes and be good to + // go. + // _prewarm_samples = 0; + + // And be ready + int maxBlockSize = 2048; // Conservative + Reset(expected_sample_rate, maxBlockSize); + }; + + ~ResamplingNAM() = default; + + void prewarm() override { mEncapsulated->prewarm(); }; + + void process(NAM_SAMPLE* input, NAM_SAMPLE* output, const int num_frames) override + { + if (!mFinalized) + throw std::runtime_error("Processing was called before the last block was finalized!"); + if (num_frames > mMaxExternalBlockSize) + // We can afford to be careful + throw std::runtime_error("More frames were provided than the max expected!"); + + if (GetExpectedSampleRate() == GetEncapsulatedSampleRate()) + { + mEncapsulated->process(input, output, num_frames); + mEncapsulated->finalize_(num_frames); + } + else + { + mResampler.ProcessBlock(&input, &output, num_frames, mBlockProcessFunc); + } + + // Prepare for external call to .finalize_() + lastNumExternalFramesProcessed = num_frames; + mFinalized = false; + }; + + void finalize_(const int num_frames) + { + if (mFinalized) + throw std::runtime_error("Call to ResamplingNAM.finalize_() when the object is already in a finalized state!"); + if (num_frames != lastNumExternalFramesProcessed) + throw std::runtime_error( + "finalize_() called on ResamplingNAM with a different number of frames from what was just processed. Something " + "is probably going wrong."); + + // We don't actually do anything--it was taken care of during BlockProcessFunc()! + + // prepare for next call to `.process()` + mFinalized = true; + }; + + void Reset(const double sampleRate, const int maxBlockSize) + { + mExpectedSampleRate = sampleRate; + mMaxExternalBlockSize = maxBlockSize; + mResampler.Reset(sampleRate, maxBlockSize); + + // Allocations in the encapsulated model (HACK) + // Stolen some code from the resampler; it'd be nice to have these exposed as methods? :) + const double mUpRatio = sampleRate / GetEncapsulatedSampleRate(); + const auto maxEncapsulatedBlockSize = static_cast<int>(std::ceil(static_cast<double>(maxBlockSize) / mUpRatio)); + std::vector<NAM_SAMPLE> input, output; + for (int i = 0; i < maxEncapsulatedBlockSize; i++) + input.push_back((NAM_SAMPLE)0.0); + output.resize(maxEncapsulatedBlockSize); // Doesn't matter what's in here + mEncapsulated->process(input.data(), output.data(), maxEncapsulatedBlockSize); + mEncapsulated->finalize_(maxEncapsulatedBlockSize); + + mFinalized = true; // prepare for `.process()` + }; + + // So that we can let the world know if we're resampling (useful for debugging) + double GetEncapsulatedSampleRate() const { return GetNAMSampleRate(mEncapsulated); }; + +private: + // The encapsulated NAM + std::unique_ptr<nam::DSP> mEncapsulated; + // The processing for NAM is a little weird--there's a call to .finalize_() that's expected. + // This flag makes sure that the NAM sees alternating instances of .process() and .finalize_() + // A value of `true` means that we expect the ResamplingNAM object to see .process() next; + // `false` means we expect .finalize_() next. + bool mFinalized = true; + + // The resampling wrapper + iplug::NonIntegerResampler<NAM_SAMPLE, 1> mResampler; + + // Used to check that we don't get too large a block to process. + int mMaxExternalBlockSize = 0; + // Keep track of how many frames were processed so that we can be sure that finalize_() is being used correctly. + // This is kind of hacky, but I'm not sure I want to rethink the core right now. + int lastNumExternalFramesProcessed = -1; + + // This function is defined to conform to the interface expected by the iPlug2 resampler. + std::function<void(NAM_SAMPLE**, NAM_SAMPLE**, int)> mBlockProcessFunc; +}; + class NeuralAmpModeler final : public iplug::Plugin { public: @@ -128,8 +261,8 @@ private: // :param nChansOut: Out to external void _ProcessOutput(iplug::sample** inputs, iplug::sample** outputs, const size_t nFrames, const size_t nChansIn, const size_t nChansOut); - // Checks the loaded model and IR against the current sample rate and resamples them if needed - void _ResampleModelAndIR(); + // Resetting for models and IRs, called by OnReset + void _ResetModelAndIR(const double sampleRate, const int maxBlockSize); // Update level meters // Called within ProcessBlock(). @@ -151,11 +284,11 @@ private: dsp::noise_gate::Trigger mNoiseGateTrigger; dsp::noise_gate::Gain mNoiseGateGain; // The model actually being used: - std::unique_ptr<nam::DSP> mModel; + std::unique_ptr<ResamplingNAM> mModel; // And the IR std::unique_ptr<dsp::ImpulseResponse> mIR; // Manages switching what DSP is being used. - std::unique_ptr<nam::DSP> mStagedModel; + std::unique_ptr<ResamplingNAM> mStagedModel; std::unique_ptr<dsp::ImpulseResponse> mStagedIR; // Flags to take away the modules at a safe time. std::atomic<bool> mShouldRemoveModel = false; diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-aax.vcxproj b/NeuralAmpModeler/projects/NeuralAmpModeler-aax.vcxproj @@ -491,6 +491,8 @@ <ClInclude Include="..\..\iPlug2\IPlug\AAX\IPlugAAX_Parameters.h" /> <ClInclude Include="..\..\iPlug2\IPlug\AAX\IPlugAAX_TaperDelegate.h" /> <ClInclude Include="..\..\iPlug2\IPlug\AAX\IPlugAAX.h" /> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\LanczosResampler.h" /> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\NonIntegerResampler.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugAPIBase.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugConstants.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugDelegate_select.h" /> diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-aax.vcxproj.filters b/NeuralAmpModeler/projects/NeuralAmpModeler-aax.vcxproj.filters @@ -290,6 +290,12 @@ <ClInclude Include="..\AudioDSPTools\dsp\wav.h"> <Filter>dsp</Filter> </ClInclude> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\NonIntegerResampler.h"> + <Filter>IPlug\Extras</Filter> + </ClInclude> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\LanczosResampler.h"> + <Filter>IPlug\Extras</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Filter Include="resources"> @@ -322,6 +328,9 @@ <Filter Include="dsp"> <UniqueIdentifier>{29388c48-c6f4-4b62-9284-b09d44ad1e40}</UniqueIdentifier> </Filter> + <Filter Include="IPlug\Extras"> + <UniqueIdentifier>{db86886b-3f03-4c59-acdb-b4a137147b3c}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ResourceCompile Include="..\resources\main.rc"> diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj b/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj @@ -302,6 +302,8 @@ <ClInclude Include="..\..\iPlug2\IGraphics\Platforms\IGraphicsWin.h" /> <ClInclude Include="..\..\iPlug2\IPlug\APP\IPlugAPP.h" /> <ClInclude Include="..\..\iPlug2\IPlug\APP\IPlugAPP_host.h" /> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\LanczosResampler.h" /> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\NonIntegerResampler.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugAPIBase.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugConstants.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugEditorDelegate.h" /> diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj.filters b/NeuralAmpModeler/projects/NeuralAmpModeler-app.vcxproj.filters @@ -338,6 +338,12 @@ <ClInclude Include="..\AudioDSPTools\dsp\wav.h"> <Filter>dsp</Filter> </ClInclude> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\NonIntegerResampler.h"> + <Filter>IPlug\Extras</Filter> + </ClInclude> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\LanczosResampler.h"> + <Filter>IPlug\Extras</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Filter Include="resources"> @@ -376,6 +382,9 @@ <Filter Include="NAM"> <UniqueIdentifier>{3adb1e7a-68f8-4b41-8563-9bcf2bb0c8da}</UniqueIdentifier> </Filter> + <Filter Include="IPlug\Extras"> + <UniqueIdentifier>{91ab74a9-d5f2-42db-9c8f-3bef7ee22bd3}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ResourceCompile Include="..\resources\main.rc"> diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-iOS.xcodeproj/project.pbxproj b/NeuralAmpModeler/projects/NeuralAmpModeler-iOS.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ 4FE0DEE829A183B700DDBCC8 /* NeuralAmpModelerAU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FC6982F293BA47F0076EC33 /* NeuralAmpModelerAU.framework */; }; 4FE0DEF029A2E0F100DDBCC8 /* IPlugAUViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4FFF105A20A0E57100D3092F /* IPlugAUViewController.mm */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; 91236D811B08F59300734C5E /* NeuralAmpModelerAppExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 91236D771B08F59300734C5E /* NeuralAmpModelerAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + AA8CA7772A452EF500F5BEF0 /* resample.cpp in Sources */ = {isa = PBXBuildFile; fileRef = AA8CA7752A452EF500F5BEF0 /* resample.cpp */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -335,6 +336,8 @@ 4FFF108820A1036200D3092F /* NeuralAmpModeler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NeuralAmpModeler.h; path = ../NeuralAmpModeler.h; sourceTree = "<group>"; }; 91236D0D1B08F42B00734C5E /* NeuralAmpModeler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NeuralAmpModeler.app; sourceTree = BUILT_PRODUCTS_DIR; }; 91236D771B08F59300734C5E /* NeuralAmpModelerAppExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NeuralAmpModelerAppExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + AA8CA7752A452EF500F5BEF0 /* resample.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = resample.cpp; sourceTree = "<group>"; }; + AA8CA7762A452EF500F5BEF0 /* resample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = resample.h; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -636,20 +639,20 @@ 4FBDC93E29FFF143004FF203 /* NAM */ = { isa = PBXGroup; children = ( - 4FBDC93F29FFF143004FF203 /* util.cpp */, - 4FBDC94029FFF143004FF203 /* version.h */, - 4FBDC94129FFF143004FF203 /* dsp.cpp */, - 4FBDC94229FFF143004FF203 /* convnet.h */, - 4FBDC94329FFF143004FF203 /* lstm.h */, + 4FBDC94A29FFF143004FF203 /* activations.cpp */, + 4FBDC94929FFF143004FF203 /* activations.h */, 4FBDC94429FFF143004FF203 /* convnet.cpp */, - 4FBDC94529FFF143004FF203 /* wavenet.h */, + 4FBDC94229FFF143004FF203 /* convnet.h */, + 4FBDC94129FFF143004FF203 /* dsp.cpp */, + 4FBDC94829FFF143004FF203 /* dsp.h */, + 4FBDC94C29FFF143004FF203 /* get_dsp.cpp */, 4FBDC94629FFF143004FF203 /* lstm.cpp */, + 4FBDC94329FFF143004FF203 /* lstm.h */, + 4FBDC93F29FFF143004FF203 /* util.cpp */, 4FBDC94729FFF143004FF203 /* util.h */, - 4FBDC94829FFF143004FF203 /* dsp.h */, - 4FBDC94929FFF143004FF203 /* activations.h */, - 4FBDC94A29FFF143004FF203 /* activations.cpp */, + 4FBDC94029FFF143004FF203 /* version.h */, 4FBDC94B29FFF143004FF203 /* wavenet.cpp */, - 4FBDC94C29FFF143004FF203 /* get_dsp.cpp */, + 4FBDC94529FFF143004FF203 /* wavenet.h */, ); name = NAM; path = ../NeuralAmpModelerCore/NAM; @@ -1003,6 +1006,7 @@ 4FBDC95329FFF143004FF203 /* ImpulseResponse.cpp in Sources */, 4FC69843293BA5C50076EC33 /* IPlugParameter.cpp in Sources */, 4FC69844293BA5C50076EC33 /* IPlugTimer.cpp in Sources */, + AA8CA7772A452EF500F5BEF0 /* resample.cpp in Sources */, 4FC69845293BA5C50076EC33 /* IPlugPaths.mm in Sources */, 4FBDC96329FFF143004FF203 /* activations.cpp in Sources */, 4FC69847293BA5F90076EC33 /* IPopupMenuControl.cpp in Sources */, diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj b/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj @@ -304,6 +304,8 @@ <ClInclude Include="..\..\iPlug2\IGraphics\Platforms\IGraphicsMac_view.h" /> <ClInclude Include="..\..\iPlug2\IGraphics\Platforms\IGraphicsWeb.h" /> <ClInclude Include="..\..\iPlug2\IGraphics\Platforms\IGraphicsWin.h" /> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\LanczosResampler.h" /> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\NonIntegerResampler.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugAPIBase.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugConstants.h" /> <ClInclude Include="..\..\iPlug2\IPlug\IPlugEditorDelegate.h" /> diff --git a/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj.filters b/NeuralAmpModeler/projects/NeuralAmpModeler-vst3.vcxproj.filters @@ -437,6 +437,12 @@ <ClInclude Include="..\AudioDSPTools\dsp\wav.h"> <Filter>dsp</Filter> </ClInclude> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\NonIntegerResampler.h"> + <Filter>IPlug\Extras</Filter> + </ClInclude> + <ClInclude Include="..\..\iPlug2\IPlug\Extras\LanczosResampler.h"> + <Filter>IPlug\Extras</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Filter Include="resources"> @@ -502,6 +508,9 @@ <Filter Include="dsp"> <UniqueIdentifier>{ed745419-3921-4abe-8286-170fa316a5ac}</UniqueIdentifier> </Filter> + <Filter Include="IPlug\Extras"> + <UniqueIdentifier>{4b6a01ff-796f-4098-a581-3b22035cc8be}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ResourceCompile Include="..\resources\main.rc">