NeuralAmpModelerPlugin

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

commit e7dd8b5f16394edba278f9c20aa233a312ec73c6
parent 0c96daaa206bce0dd9b8561a9fc3ab4f086e657d
Author: Steven Atkinson <[email protected]>
Date:   Fri, 22 Nov 2024 17:23:38 -0800

[FEATURE] Output calibration (#531)

* Implement output mode

Raw/Normalized/Calibrated
Control layout and style needs work
Need to work on "disabling" based on what's available from the model's metadata.

* Radio button style

* Bit of style and layout improvement

* Set radio labels for normalized and calibrated based on whether the model supports them
Diffstat:
MNeuralAmpModeler/NeuralAmpModeler.cpp | 91++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
MNeuralAmpModeler/NeuralAmpModeler.h | 12++++++------
MNeuralAmpModeler/NeuralAmpModelerControls.h | 49+++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 104 insertions(+), 48 deletions(-)

diff --git a/NeuralAmpModeler/NeuralAmpModeler.cpp b/NeuralAmpModeler/NeuralAmpModeler.cpp @@ -50,9 +50,13 @@ const IVStyle style = DEFAULT_SHADOW_OFFSET, DEFAULT_WIDGET_FRAC, DEFAULT_WIDGET_ANGLE}; - const IVStyle titleStyle = DEFAULT_STYLE.WithValueText(IText(30, COLOR_WHITE, "Michroma-Regular")).WithDrawFrame(false).WithShadowOffset(2.f); +const IVStyle radioButtonStyle = + style + .WithColor(EVColor::kON, PluginColors::NAM_THEMECOLOR) // Pressed buttons and their labels + .WithColor(EVColor::kOFF, PluginColors::NAM_THEMECOLOR.WithOpacity(0.1f)) // Unpressed buttons + .WithColor(EVColor::kX1, PluginColors::NAM_THEMECOLOR.WithOpacity(0.6f)); // Unpressed buttons' labels EMsgBoxResult _ShowMessageBox(iplug::igraphics::IGraphics* pGraphics, const char* str, const char* caption, EMsgBoxType type) @@ -79,10 +83,9 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info) GetParam(kNoiseGateThreshold)->InitGain("Threshold", -80.0, -100.0, 0.0, 0.1); GetParam(kNoiseGateActive)->InitBool("NoiseGateActive", true); GetParam(kEQActive)->InitBool("ToneStack", true); - GetParam(kOutNorm)->InitBool("OutNorm", true); + GetParam(kOutputMode)->InitEnum("OutputMode", 1, {"Raw", "Normalized", "Calibrated"}); // TODO DRY w/ control GetParam(kIRToggle)->InitBool("IRToggle", true); GetParam(kCalibrateInput)->InitBool("CalibrateInput", false); - // TODO Double, label "dBu" GetParam(kInputCalibrationLevel)->InitDouble("InputCalibrationLevel", 12.5, -30.0, 30.0, 0.1, "dBu"); mNoiseGateTrigger.AddListener(&mNoiseGateGain); @@ -227,8 +230,6 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info) pGraphics->AttachControl( new NAMSwitchControl(ngToggleArea, kNoiseGateActive, "Noise Gate", style, switchHandleBitmap)); pGraphics->AttachControl(new NAMSwitchControl(eqToggleArea, kEQActive, "EQ", style, switchHandleBitmap)); - pGraphics->AttachControl( - new NAMSwitchControl(outNormToggleArea, kOutNorm, "Normalize", style, switchHandleBitmap), kCtrlTagOutNorm); // The knobs pGraphics->AttachControl(new NAMKnobControl(inputKnobArea, kInputLevel, "", style, knobBackgroundBitmap)); @@ -254,8 +255,8 @@ NeuralAmpModeler::NeuralAmpModeler(const InstanceInfo& info) gearSVG)); pGraphics - ->AttachControl(new NAMSettingsPageControl( - b, backgroundBitmap, inputLevelBackgroundBitmap, switchHandleBitmap, crossSVG, style), + ->AttachControl(new NAMSettingsPageControl(b, backgroundBitmap, inputLevelBackgroundBitmap, switchHandleBitmap, + crossSVG, style, radioButtonStyle), kCtrlTagSettingsBox) ->Hide(true); @@ -312,20 +313,13 @@ void NeuralAmpModeler::ProcessBlock(iplug::sample** inputs, iplug::sample** outp if (mModel != nullptr) { - // TODO multi-channel processing; Issue - // Make sure it's multi-threaded or else this won't perform well! mModel->process(triggerOutput[0], mOutputPointers[0], nFrames); - // Normalize loudness - if (GetParam(kOutNorm)->Value()) - { - _NormalizeModelOutput(mOutputPointers, numChannelsInternal, numFrames); - } } else { _FallbackDSP(triggerOutput, mOutputPointers, numChannelsInternal, numFrames); } - // Apply the noise gate + // Apply the noise gate after the NAM sample** gateGainOutput = noiseGateActive ? mNoiseGateGain.Process(mOutputPointers, numChannelsInternal, numFrames) : mOutputPointers; @@ -386,7 +380,6 @@ void NeuralAmpModeler::OnIdle() { if (auto* pGraphics = GetUI()) { - pGraphics->GetControlWithTag(kCtrlTagOutNorm)->SetDisabled(!mModel->HasLoudness()); _UpdateControlsFromModel(); mNewModelLoadedInDSP = false; } @@ -395,7 +388,8 @@ void NeuralAmpModeler::OnIdle() { if (auto* pGraphics = GetUI()) { - pGraphics->GetControlWithTag(kCtrlTagOutNorm)->SetDisabled(false); + // FIXME -- need to disable only the "normalized" model + // pGraphics->GetControlWithTag(kCtrlTagOutputMode)->SetDisabled(false); static_cast<NAMSettingsPageControl*>(pGraphics->GetControlWithTag(kCtrlTagSettingsBox))->ClearModelInfo(); mModelCleared = false; } @@ -478,6 +472,9 @@ void NeuralAmpModeler::OnParamChange(int paramIdx) case kCalibrateInput: case kInputCalibrationLevel: case kInputLevel: _SetInputGain(); break; + // Changes to the output gain + case kOutputLevel: + case kOutputMode: _SetOutputGain(); break; // Tone stack: case kToneBass: mToneStack->SetParam("bass", GetParam(paramIdx)->Value()); break; case kToneMid: mToneStack->SetParam("middle", GetParam(paramIdx)->Value()); break; @@ -563,6 +560,7 @@ void NeuralAmpModeler::_ApplyDSPStaging() mModelCleared = true; _UpdateLatency(); _SetInputGain(); + _SetOutputGain(); } if (mShouldRemoveIR) { @@ -573,12 +571,12 @@ void NeuralAmpModeler::_ApplyDSPStaging() // Move things from staged to live if (mStagedModel != nullptr) { - // Move from staged to active DSP mModel = std::move(mStagedModel); mStagedModel = nullptr; mNewModelLoadedInDSP = true; _UpdateLatency(); _SetInputGain(); + _SetOutputGain(); } if (mStagedIR != nullptr) { @@ -613,24 +611,6 @@ void NeuralAmpModeler::_FallbackDSP(iplug::sample** inputs, iplug::sample** outp mOutputArray[c][s] = mInputArray[c][s]; } -void NeuralAmpModeler::_NormalizeModelOutput(iplug::sample** buffer, const size_t numChannels, const size_t numFrames) -{ - if (!mModel) - return; - if (!mModel->HasLoudness()) - return; - const double loudness = mModel->GetLoudness(); - const double targetLoudness = -18.0; - const double gain = pow(10.0, (targetLoudness - loudness) / 20.0); - for (size_t c = 0; c < numChannels; c++) - { - for (size_t f = 0; f < numFrames; f++) - { - buffer[c][f] *= gain; - } - } -} - void NeuralAmpModeler::_ResetModelAndIR(const double sampleRate, const int maxBlockSize) { // Model @@ -675,6 +655,37 @@ void NeuralAmpModeler::_SetInputGain() mInputGain = DBToAmp(inputGainDB); } +void NeuralAmpModeler::_SetOutputGain() +{ + double gainDB = GetParam(kOutputLevel)->Value(); + if (mModel != nullptr) + { + const int outputMode = GetParam(kOutputMode)->Int(); + switch (outputMode) + { + case 1: // Normalized + if (mModel->HasLoudness()) + { + const double loudness = mModel->GetLoudness(); + const double targetLoudness = -18.0; + gainDB += (targetLoudness - loudness); + } + break; + case 2: // Calibrated + if (mModel->HasOutputLevel()) + { + const double inputLevel = GetParam(kInputCalibrationLevel)->Value(); + const double outputLevel = mModel->GetOutputLevel(); + gainDB += (outputLevel - inputLevel); + } + break; + case 0: // Raw + default: break; + } + } + mOutputGain = DBToAmp(gainDB); +} + std::string NeuralAmpModeler::_StageModel(const WDL_String& modelPath) { WDL_String previousNAMPath = mNAMPath; @@ -830,7 +841,7 @@ void NeuralAmpModeler::_ProcessInput(iplug::sample** inputs, const size_t nFrame void NeuralAmpModeler::_ProcessOutput(iplug::sample** inputs, iplug::sample** outputs, const size_t nFrames, const size_t nChansIn, const size_t nChansOut) { - const double gain = pow(10.0, GetParam(kOutputLevel)->Value() / 20.0); + const double gain = mOutputGain; // Assume _PrepareBuffers() was already called if (nChansIn != 1) throw std::runtime_error("Plugin is supposed to process in mono."); @@ -940,9 +951,13 @@ void NeuralAmpModeler::_UpdateControlsFromModel() static_cast<NAMSettingsPageControl*>(pGraphics->GetControlWithTag(kCtrlTagSettingsBox))->SetModelInfo(modelInfo); const bool disableInputCalibrationControls = !mModel->HasInputLevel(); - pGraphics->GetControlWithTag(kCtrlTagOutNorm)->SetDisabled(!mModel->HasLoudness()); pGraphics->GetControlWithTag(kCtrlTagCalibrateInput)->SetDisabled(disableInputCalibrationControls); pGraphics->GetControlWithTag(kCtrlTagInputCalibrationLevel)->SetDisabled(disableInputCalibrationControls); + { + auto* c = static_cast<OutputModeControl*>(pGraphics->GetControlWithTag(kCtrlTagOutputMode)); + c->SetNormalizedDisable(!mModel->HasLoudness()); + c->SetCalibratedDisable(!mModel->HasOutputLevel()); + } } } diff --git a/NeuralAmpModeler/NeuralAmpModeler.h b/NeuralAmpModeler/NeuralAmpModeler.h @@ -39,11 +39,11 @@ enum EParams // The rest is fine though. kNoiseGateActive, kEQActive, - kOutNorm, kIRToggle, // Input calibration kCalibrateInput, kInputCalibrationLevel, + kOutputMode, kNumParams }; @@ -56,7 +56,7 @@ enum ECtrlTags kCtrlTagInputMeter, kCtrlTagOutputMeter, kCtrlTagSettingsBox, - kCtrlTagOutNorm, + kCtrlTagOutputMode, kCtrlTagCalibrateInput, kCtrlTagInputCalibrationLevel, kNumCtrlTags @@ -216,8 +216,6 @@ private: size_t _GetBufferNumChannels() const; size_t _GetBufferNumFrames() const; void _InitToneStack(); - // Apply the normalization for the model output (if possible) - void _NormalizeModelOutput(iplug::sample** buffer, const size_t numChannels, const size_t numFrames); // Loads a NAM model and stores it to mStagedNAM // Returns an empty string on success, or an error message on failure. std::string _StageModel(const WDL_String& dspFile); @@ -244,6 +242,7 @@ private: void _ResetModelAndIR(const double sampleRate, const int maxBlockSize); void _SetInputGain(); + void _SetOutputGain(); // Unserialize current-version plug-in data: int _UnserializeStateCurrent(const iplug::IByteChunk& chunk, int startPos); @@ -273,8 +272,9 @@ private: iplug::sample** mInputPointers = nullptr; iplug::sample** mOutputPointers = nullptr; - // Input and (soon) output gain - iplug::sample mInputGain = 1.0; + // Input and output gain + double mInputGain = 1.0; + double mOutputGain = 1.0; // Noise gates dsp::noise_gate::Trigger mNoiseGateTrigger; diff --git a/NeuralAmpModeler/NeuralAmpModelerControls.h b/NeuralAmpModeler/NeuralAmpModelerControls.h @@ -304,7 +304,8 @@ public: auto clearFileFunc = [&](IControl* pCaller) { pCaller->GetDelegate()->SendArbitraryMsgFromUI(mClearMsgTag); mFileNameControl->SetLabelAndTooltip(mDefaultLabelStr.Get()); - pCaller->GetUI()->GetControlWithTag(kCtrlTagOutNorm)->SetDisabled(false); + // FIXME disabling output mode... + // pCaller->GetUI()->GetControlWithTag(kCtrlTagOutputMode)->SetDisabled(false); }; auto chooseFileFunc = [&, loadFileFunc](IControl* pCaller) { @@ -555,17 +556,50 @@ private: bool mHasInfo = false; }; +class OutputModeControl : public IVRadioButtonControl +{ +public: + OutputModeControl(const IRECT& bounds, int paramIdx, const IVStyle& style, float buttonSize) + : IVRadioButtonControl( + bounds, paramIdx, {}, "Output Mode", style, EVShape::Ellipse, EDirection::Vertical, buttonSize) {}; + + void SetNormalizedDisable(const bool disable) + { + // HACK non-DRY string and hard-coded indices + std::stringstream ss; + ss << "Normalized"; + if (disable) + { + ss << " [Not supported by model]"; + } + mTabLabels.Get(1)->Set(ss.str().c_str()); + }; + void SetCalibratedDisable(const bool disable) + { + // HACK non-DRY string and hard-coded indices + std::stringstream ss; + ss << "Calibrated"; + if (disable) + { + ss << " [Not supported by model]"; + } + mTabLabels.Get(2)->Set(ss.str().c_str()); + }; +}; + class NAMSettingsPageControl : public IContainerBaseWithNamedChildren { public: NAMSettingsPageControl(const IRECT& bounds, const IBitmap& bitmap, const IBitmap& inputLevelBackgroundBitmap, - const IBitmap& switchBitmap, ISVG closeSVG, const IVStyle& style) + const IBitmap& switchBitmap, ISVG closeSVG, const IVStyle& style, + const IVStyle& radioButtonStyle) : IContainerBaseWithNamedChildren(bounds) , mAnimationTime(0) , mBitmap(bitmap) , mInputLevelBackgroundBitmap(inputLevelBackgroundBitmap) , mSwitchBitmap(switchBitmap) , mStyle(style) + , mRadioButtonStyle(radioButtonStyle) , mCloseSVG(closeSVG) { mIgnoreMouse = false; @@ -645,7 +679,7 @@ public: const float width = titleArea.W(); const auto inputOutputArea = titleArea.GetFromBottom(height).GetTranslated(0.0f, height); const auto inputArea = inputOutputArea.GetFromLeft(0.5f * width); - // const auto outputArea = inputOutputArea.GetFromRight(0.5f * width); + const auto outputArea = inputOutputArea.GetFromRight(0.5f * width); const float knobWidth = 87.0f; // HACK based on looking at the main page knobs. const auto inputLevelArea = @@ -662,7 +696,12 @@ public: new NAMSwitchControl(inputSwitchArea, kCalibrateInput, "Calibrate Input", mStyle, mSwitchBitmap), mControlNames.calibrateInput, kCtrlTagCalibrateInput); - // TODO output--raw, normalized, calibrated + // Same-ish height & width as input controls + const auto outputRadioArea = outputArea.GetFromBottom( + 1.1f * (inputLevelArea.H() + inputSwitchArea.H())); // .GetMidHPadded(0.55f * knobWidth); + const float buttonSize = 10.0f; + AddNamedChildControl(new OutputModeControl(outputRadioArea, kOutputMode, mRadioButtonStyle, buttonSize), + mControlNames.outputMode, kCtrlTagOutputMode); } const float halfWidth = PLUG_WIDTH / 2.0f - pad; @@ -694,6 +733,7 @@ private: IBitmap mInputLevelBackgroundBitmap; IBitmap mSwitchBitmap; IVStyle mStyle; + IVStyle mRadioButtonStyle; ISVG mCloseSVG; int mAnimationTime = 200; bool mWillHide = false; @@ -708,6 +748,7 @@ private: const std::string close = "Close"; const std::string inputCalibrationLevel = "InputCalibrationLevel"; const std::string modelInfo = "ModelInfo"; + const std::string outputMode = "OutputMode"; const std::string title = "Title"; } mControlNames;