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:
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">