Skip to content

Blend up to 3 NAM models in one instance - #662

Open
abelptvts wants to merge 1 commit into
sdatkinson:mainfrom
abelptvts:multi-model-blend
Open

Blend up to 3 NAM models in one instance#662
abelptvts wants to merge 1 commit into
sdatkinson:mainfrom
abelptvts:multi-model-blend

Conversation

@abelptvts

@abelptvts abelptvts commented Aug 2, 2026

Copy link
Copy Markdown

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.jpg or Lines.png.

Main page

Each row has a browser, a level, and polarity / mute / solo:

Blend page

Blend page with toggles engaged

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:

pick "Mesa Crunch 421.nam" on slot 1   ->  1: 421   2: (empty)   3: (empty)
press > on row 2                       ->  1: 421   2: 545       3: (empty)
press > on row 3                       ->  1: 421   2: 545       3: M160

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 ProcessBlock changed.

Four things that needed care rather than being obvious:

Slot 1 is the levelling reference. _SetInputGain and _SetOutputGain keep their exact current bodies and simply read the first loaded slot. A new _SetSlotTrims then 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 exactly 1.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. _UpdateLatency now reports the largest latency and delays the others up to match. The delay lines are sized once in OnReset so 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 kSlim so the first six keep the indices the GUI grid relies on: Blend1-3, Invert1-3, Mute1-3, Solo1-3.

SerializeState writes three model paths. Unserialization.cpp gains a _GetConfigFrom_0_7_16 reader, and _UnserializePathsAndExpectedKeys takes a path count so every older reader is untouched. _UpdateConfigFrom_0_7_14 was 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

NAMFileBrowserControl now 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 on main — 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):

  • Standalone (Debug), AU and VST3 (Release) all build clean, no new warnings.
  • VST3 validator: 47/47 tests passed, and 537/537 in extensive mode (-e), 0 failures. That covers Scan Parameters across all 26 parameters, Variable Block Size, the process/silence/flush suites and bus arrangements. The block-size test matters here specifically, because ResamplingNAM::process throws if it's handed more frames than it was reset for.
  • State round-trip: set all 26 parameters to distinct values, getState, setState into a fresh instance, getState again — byte-for-byte identical (256 bytes).
  • Older-version state loads: built 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.
  • Loaded three .nam captures 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.
  • Unit-tested the new delay line's ring-buffer arithmetic standalone (delays either side of the block size, buffers wrapping repeatedly, clamping, clearing on delay change).
  • Cross-checked the EParams order, the constructor's Init* 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:

  • No null test against a pre-change build. One model in slot 1, render a DI through both builds, confirm the difference is digital silence. That's what the reference-slot design is meant to buy and I'd rather have the measurement than the argument. It needs a DAW I don't have set up here.
  • Not tested on Windows.
  • IR loading and the EQ section weren't specifically re-tested, though neither was touched.

PR Checklist

  • Did you format your code using format.bash? — yes (Apple clang-format 21); it only re-wrapped lines in the new code, nothing else in the repo drifted.
  • Does the VST3 plugin pass all of the unit tests in the VST3PluginTestHost? — via the SDK's command-line validator rather than the GUI test host: 47/47, and 537/537 with -e.
    • Windows — not tested.
    • macOS
  • Does your PR add, remove, or rename any plugin parameters? If yes... — yes, 12 added
    • Have you ensured that the plug-in unserializes correctly? — round-trips byte-identically through a fresh instance with all 26 parameters at distinct values.
    • Have you ensured that older versions of the plug-in load correctly? — a real 0.7.15 state chunk, captured from a build of main, loads and re-serializes stably.
  • Does your PR add or remove any graphical assets? If yes, are they defined in config.h and added in the two required locations in main.rc? — yes: BlendIcon.svg, BLEND_ICON_FN in config.h, and both blocks of main.rc.

@abelptvts
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
abelptvts marked this pull request as draft August 2, 2026 17:08
@abelptvts
abelptvts marked this pull request as ready for review August 3, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Blend several NAM models in one instance (e.g. multiple mic captures of one amp)

1 participant