Blend up to 3 NAM models in one instance - #662
Open
abelptvts wants to merge 1 commit into
Open
Conversation
abelptvts
marked this pull request as ready for review
August 2, 2026 10:51
Adds three model slots that all receive the same gated mono input, are summed, and then feed the existing shared noise gate, tone stack, IR and output stage. The motivating case is having several captures of one amp through different microphones: previously that meant three plugin instances on parallel tracks, with three copies of the shared chain and nowhere to balance the mics against each other. Slot 1 is the model row already on the main page, and it stays the reference for input calibration and output normalization: the global gains keep their existing formulas and the other slots get only the relative trim needed to match. A session using a single model therefore takes the same code path and produces the same numbers as before. DSP: - Slot arrays for the model, staging, removal flags and paths. - Per-slot level (mixer semantics, -40 dB mutes) and polarity invert, plus mute and solo with console behaviour: any solo silences what isn't soloed, an explicit mute still wins, and a solo on an empty slot is ignored. - Blend gains ramp across the block so polarity flips and mute/solo toggles don't click. - Slots are time-aligned to the largest reported latency, since models at different sample rates would otherwise comb-filter when summed. The delay lines are sized once in OnReset so the audio-thread latency update never allocates. - Input trims apply before the model and output trims after, as the model is nonlinear and they don't commute. UI: - New NAMBlendPageControl, a full-window overlay modelled on the settings page, with one row per slot: browser, level, and compact polarity/mute/solo toggles. Reached from a new icon beside the model row; no window resize and no changes to the artwork. - Loading a model hands its folder to slots that are still empty so their arrows work without picking the same folder again. Those rows stay blank and silent until an arrow is pressed. - NAMFileBrowserControl reasserts its Get/Clear visibility when shown, because IContainerBase::Hide un-hides every descendant and would otherwise resurrect the Get button on a loaded browser. State: three paths are serialized and a 0.7.16 reader added; older sessions load into slot 1 with the rest empty and every blend control at its neutral default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abelptvts
force-pushed
the
multi-model-blend
branch
from
August 2, 2026 17:08
008f3fd to
6cdc1e1
Compare
abelptvts
marked this pull request as draft
August 2, 2026 17:08
abelptvts
marked this pull request as ready for review
August 3, 2026 08:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Lets one instance load up to three models, run them all on the same input, and sum them. Closes #661.
Opening as a draft because it touches the parameter list and serialization, so I'd rather check the shape suits your plans before polishing. The code is complete, and the VST3 now builds and passes the SDK validator — see Testing below for what has and hasn't been checked.
The motivating case is several captures of one amp through different mics. Today that needs three instances on three tracks, with three copies of the shared chain and nowhere to balance the mics.
What it looks like
Slot 1 is the model row that's already on the main page — nothing moves. The other two slots live on an overlay page reached from a new icon beside that row, modelled on the existing settings page. No window resize, no changes to
Background.jpgorLines.png.Each row has a browser, a level, and polarity / mute / solo:
You only pick the folder once. Loading a model hands its folder to any slot that's still empty, so those rows' arrows work immediately — they stay blank and silent until you press one. For a folder of mic captures the whole blend is one file dialog plus one arrow press per row:
How it works
Every loaded slot gets the same gated mono input; their outputs are summed and fed to the existing, unchanged noise-gate-gain → tone stack → IR → DC blocker → output stage. Only the model call in
ProcessBlockchanged.Four things that needed care rather than being obvious:
Slot 1 is the levelling reference.
_SetInputGainand_SetOutputGainkeep their exact current bodies and simply read the first loaded slot. A new_SetSlotTrimsthen computes only the relative correction for the other slots, so the blend balances the models rather than their metadata. The point of doing it this way is that a single-model session takes the same path with the same numbers as before — the reference slot's trims are exactly1.0, and it's handed the shared buffer without a copy.Input trims go before the model, output trims after. The models are nonlinear, so those don't commute.
Slots are time-aligned.
ResamplingNAM::GetLatency()is only non-zero when it resamples, so a 44.1k and a 48k model in a 96k session would sum offset in time and comb-filter._UpdateLatencynow reports the largest latency and delays the others up to match. The delay lines are sized once inOnResetso the latency update inside_ApplyDSPStaging— which runs on the audio thread — never allocates.Gains ramp across the block. A polarity flip is a full sign change and a mute is a step to zero; both click otherwise. Two flops a sample fixes it and also smooths the level knob.
Mute and solo follow console behaviour: any solo silences what isn't soloed, an explicit mute still wins, and a solo on an empty slot is ignored rather than killing the whole blend.
One deliberate trade worth flagging: muted slots are still processed. The models are stateful, so skipping inference freezes their state and glitches on unmute — which defeats the point when mute/solo exist mainly for fast A/B-ing. The cost is that three loaded models is ~3× the CPU regardless of mute state. Happy to make this configurable, or to skip inference after a slot has been muted for a while, if you'd rather have the CPU back.
Parameters and serialization
Twelve new parameters, all appended after
kSlimso the first six keep the indices the GUI grid relies on:Blend1-3,Invert1-3,Mute1-3,Solo1-3.SerializeStatewrites three model paths.Unserialization.cppgains a_GetConfigFrom_0_7_16reader, and_UnserializePathsAndExpectedKeystakes a path count so every older reader is untouched._UpdateConfigFrom_0_7_14was a stub and now fills the new defaults, so every pre-existing version inherits them for free through the chain that's already there — an older session loads into slot 1 with the rest empty and every blend control neutral, i.e. sounding exactly as it did.One drive-by fix
NAMFileBrowserControlnow reasserts its Get/Clear button visibility when it's un-hidden.IContainerBase::Hide(false)un-hides every descendant, which resurrects the "Get" button on a browser that's showing a loaded file. This is latent onmain— the existing browsers are never inside a hidden container — but it's visible the moment a browser lives on an overlay page.Testing
Done, on macOS (Apple silicon):
-e), 0 failures. That coversScan Parametersacross all 26 parameters,Variable Block Size, the process/silence/flush suites and bus arrangements. The block-size test matters here specifically, becauseResamplingNAM::processthrows if it's handed more frames than it was reset for.getState,setStateinto a fresh instance,getStateagain — byte-for-byte identical (256 bytes).main(0.7.15) as a VST3, captured a real state chunk from it (152 bytes, 14 parameters), and loaded that into this build. Accepted without error, and the migrated state then round-trips identically. So the_UpdateConfigFrom_*chain does what it's supposed to on a genuine older chunk rather than a hand-made one..namcaptures into the three slots in the standalone, stepped them with the arrows, cleared them, and confirmed slot 1 stays in sync between the main page and the blend page.EParamsorder, the constructor'sInit*names and the new serialization list programmatically — all 25 line up. That list is read positionally, so a mismatch would corrupt state silently rather than fail loudly.Still outstanding:
PR Checklist
format.bash? — yes (Apple clang-format 21); it only re-wrapped lines in the new code, nothing else in the repo drifted.validatorrather than the GUI test host: 47/47, and 537/537 with-e.main, loads and re-serializes stably.BlendIcon.svg,BLEND_ICON_FNinconfig.h, and both blocks ofmain.rc.