Skip to content

feat: v0.3.0 — circuit-grade bass engine (ADAA drive, RMS detector, modern gate, antialiased clip) - #69

Merged
yves-vogl merged 10 commits into
mainfrom
feat/v0.3.0-sota-dsp
Jul 27, 2026
Merged

feat: v0.3.0 — circuit-grade bass engine (ADAA drive, RMS detector, modern gate, antialiased clip)#69
yves-vogl merged 10 commits into
mainfrom
feat/v0.3.0-sota-dsp

Conversation

@yves-vogl

Copy link
Copy Markdown
Collaborator

Implements the binding v0.3.0 brief (.scaffold/research/2026-07-25-sota/brief-crypta.md) including its Revision notes.

The headline: every stock-JUCE sound-making element in the Mid and High bands is replaced with circuit-derived, antialiased DSP; the low band gets a detector that stops it tremoloing on bass fundamentals; the gate gets hysteresis, hold and a straight-line release; and the safety clip stops aliasing. Every existing session and preset still sounds exactly as it did — the v0.2.0 code paths ship on as Classic and are what all legacy state is migrated onto.

Features

Circuit drive engine (driveEngine = Circuit, src/dsp/CircuitDrive.{h,cpp}) — the Mid and High bands now share ONE oversampling region instead of the two independent ones v0.2.0 ran (a duplication MidBand.h's own comment already conceded). The remainder is upsampled once, split by an LR4 crossover running at the oversampled rate, processed, summed and downsampled once. The saved region pays for the extra per-voicing filtering. The factor adapts to the host rate: 4x below 50 kHz, 2x below 100 kHz, 1x above.

ADAA-1 (src/dsp/ADAAShaper.h) — Parker, Zavalishin & Le Bivic, Reducing the aliasing of nonlinear waveshaping using continuous-time convolution, DAFx-16 (via research-triode-adaa.md §2.4). Closed forms for tanh (F1 = ln cosh) and hard clip; a 2048-point cubic-interpolated table with Simpson-integrated F1 for curves whose antiderivative is not elementary.

Per-voicing circuit topologies (research-diode-clipper-dk.md §1.1, §2.1, §2.3, §4):

  • Gnaw — pre-emphasis shelf plus its exact algebraic inverse behind the clipper, so drive 0 collapses to unity structurally rather than approximately.
  • Wool — the asymmetric diode clipper's DC curve (iD(v) = Is·(exp(v/2nVT) − 1) − Is·(exp(−v/nVT) − 1), Is = 2.52 nA, n = 1.75, VT = 25.85 mV), Newton-solved per table point at prepare time, plus a dynamic-bias side chain.
  • Razor — the feedback clipper's unity-clean-plus-clipped-difference structure with Yeh's tanh-fit (eq. 19), the 720 Hz pedal corner moved to 330 Hz for the bass register.
  • Gnaw and Razor share the drive-tracked Cc pole, fc = 1/(2π·R2(D)·Cc).

Smooth RMS low-band detector (src/dsp/LevelDetector.h) — Giannoulis, Massberg & Reiss, JAES 60(6) (via research-gate-expander.md §2.1–2.3): mean-square detection over a window longer than one bass cycle, soft knee, and smoothing after the gain computer so attack/release do not become level-dependent. Plus program-dependent release and auto-makeup.

Modern gate (src/dsp/GateEngine.{h,cpp}) — research-gate-expander.md §2.1/§2.4: hysteresis, retriggering hold, detector-only sidechain highpass, dB-linear release, per-sample control path, channels linked.

ADAA safety clip (src/dsp/OutputClipper.h), lock-free metering (src/dsp/MeterTaps.h, closes #13), 12 new parameters (39 → 51), state schema v2, three new factory presets.

Measurement evidence

All figures from this branch's test run (151 cases, 60,883 assertions, green under ctest).

Measurement Result
Alias-to-signal, Circuit vs Classic (1244/2489/4978/9956 Hz, Gnaw, drive 100, 48 kHz) −81.9 / −64.0 / −57.1 / −51.9 dB vs Classic's −48.0 / −36.2 / −27.9 / −22.7 dB — 25–30 dB better against a 10 dB requirement
Alias floor, bass register (311/622/1244 Hz) ≤ −80 dB
2x at 96 kHz vs 4x at 48 kHz no measurable penalty
ADAA-1 unit improvement over plain tanh (5 kHz, gain 8, no oversampling) ≥ 12 dB
Low-comp GR ripple, 80 Hz 6 dB over threshold, 6 ms release ≤ 0.5 dB peak-to-peak (Classic peak detector: > 1 dB)
Soft-knee static curve vs Giannoulis equations within 0.25 dB at six points
Auto-makeup at T = −18, R = 2 +4.5 dB (sign anchored in test)
Gate release linearity in dB slope within 10 % of range/release, R² > 0.99
Gate chatter, ±1.5 dB dither at threshold with 4 dB hysteresis zero transitions after settling
Gate sidechain HPF 50 Hz at T+10 dB stays closed; 2 kHz at T+3 dB opens
Safety clip transparency below ceiling deviation spread 0.13 dB across 40 Hz–20 kHz; −73 dB null vs input
Safety clip ceiling accuracy peak ≤ ceiling × 1.012 at 0/−3/−6/−12 dBFS
Legacy session render vs committed v0.2.0 goldens sample-exact (memcmp, macOS)
Allocations in processBlock, all features on, both engines 0 across 200 blocks
Reported latency, both engines × 4 sample rates identical; Classic dirac peak exact

Known deviations from the brief

Each is documented in full at the relevant assertion. Summarised for review:

  1. The flat −80 dB alias floor is not reachable for Gnaw. It is a 40x hard clip; its harmonics fall off as 1/n with no bandwidth limit, so the floor is set by which harmonic order folds back, and that rises with the fundamental. I measured 8x oversampling too — it buys 7–10 dB, still misses −80, and doubles the cost of the stage. This is a property of the 1/n series, not of the implementation. Delivered instead: 25–30 dB better than Classic, −80 dB through the bass register, −49 dB everywhere.
  2. The drive-tracked pole opens to 61 kHz at drive 0, not 24 kHz. A one-pole at 24 kHz is already −1.9 dB at 18 kHz and cannot satisfy the brief's own T3 transparency requirement. 61 kHz is the figure research-diode-clipper-dk.md §2.3 gives for the real circuit with the pot open, and measures −0.36 dB at 18 kHz. R2(D) uses a square-law (audio-taper) pot rather than the datasheet's linear law, which is what keeps the pole above 12 kHz at half drive as T3 requires.
  3. Engine parity at drive 0 holds to ±0.5 dB up to 3 kHz, not 14 kHz. Above that Circuit is up to 2.5 dB brighter, in one direction and for one reason: its tone lowpass runs at the oversampled rate and so escapes the bilinear frequency warping the base-rate Classic filter has. Restoring parity would mean either reintroducing that warping deliberately or splitting the shared oversampling region back apart — and the second is the thing this release exists to fix.
  4. Wool's sag has the opposite sign to T5's prediction. The probe blooms rather than dips: the bias makes the clipping asymmetric, that asymmetry generates real DC, and the 10 Hz blocker restores it over its own ~16 ms constant, which dominates the slope change. History-dependence is confirmed and measures 11 dB on Wool against 1 dB on the memoryless voicings. Flagged for the ear-tuning gate.
  5. T3's 330 Hz corner and the drive-0 pole transparency are asserted on the filter primitives, not through the plugin. Neither is observable end-to-end: splitHighHz bottoms out at 300 Hz by frozen range, so the high band barely contains 330 Hz.
  6. The clip-ON golden null is stated at −25 dB (measured −26.5), not −40. The brief qualifies that figure as applying at typical levels; the fixture is deliberately driven 12 dB past the ceiling, where v0.2.0's tanh produces close to a square wave. The contract that matters is asserted far more tightly (row 12 above).
  7. T15 is measured in the time domain. A spectral version was written first and rejected: sweeping drive across a 1 kHz tone legitimately rewrites its harmonic structure, and the modulation sidebands land on non-harmonic bins, so the metric reports ~−28 dBc whether the parameter is stepped or perfectly smooth.
  8. Golden renders are 0.25 s rather than 4 s — a repo-weight trade that costs no regression power, since a migration landing on the wrong engine diverges in the first milliseconds.
  9. Latency is reported as the maximum across both engines, with the Circuit path padded up to it, rather than re-reported when driveEngine changes. Hosts handle mid-transport latency changes poorly and an automated engine switch should not shift the plugin's timing.

Three defects found and fixed by the measurements

  • Stale-state dump on engine switch. Only one engine runs at a time, so the idle one's oversampling history and delay lines held whatever was passing through when it was last selected, and released it as a burst on the next switch — 1.96 peak and a 0.44 sample-to-sample step against a 1.13 steady state. The incoming engine is now flushed and the crossfade lengthened to cover its refill. Now 1.39 peak, 0.13 step.
  • Equal-power crossfade overshoot. That law is for uncorrelated sources; two renderings of the same programme are strongly correlated, so cos/sin summed up to +3 dB mid-fade. Constant-gain is correct here.
  • The delta-form clipper could exceed its own ceiling. It is algebraically ADAA1(clip) + (x[n]−x[n−1])/2, and that first-difference term pushed samples 15 % over the ceiling on fast material, breaking an existing gain-staging assertion. A final hard bound at the ceiling is applied — it never engages below the ceiling, so transparency is untouched.

Migration

Two independent entry points, both covered:

  • SessionsgetStateInformation() stamps stateVersion; setStateInformation() injects the Classic value for any of the three engine IDs a state without that attribute fails to mention.
  • Presets — presets never pass through setStateInformation(), and applyParsedPreset() resets to defaults before applying values, so a legacy preset would otherwise adopt the new engines. PresetManager gains a generic, version-gated legacy back-fill, empty by default so the rest of the suite is unaffected. The preset JSON schema is unchanged — read-side default-fill only, format tag and parseAndValidate() contract untouched.

Both refuse to override an explicitly present value. presets/factory/default.json pins Circuit/Smooth RMS/Modern and declares pluginVersion 0.3.0 — it is the mechanism by which a fresh instance reaches the new engines. The eight presets voiced against the v0.2.0 DSP pin Classic.

R1 sign-off gate: the Circuit-by-default flip for fresh instances is gated on your A/B. If it fails, flip the driveEngine APVTS default and the three pins in presets/factory/default.json back to Classic — two small edits, no schema impact.

Third-party code and licensing

None added. Every algorithm is implemented from the cited papers' equations — facts and mathematics, not code. No snippet was adapted from any reference implementation. hiir (WTFPL) remains deferred to the suite-level oversampler project as the brief specifies. The release therefore introduces no new licence obligations and remains AGPLv3-compatible.

Review checklist (brief R4)

  • DryWetMixer primed via setWetMixProportion before reset() — Classic plumbing untouched; the Circuit engine uses a direct crossfade inside its oversampled region and needs no DryWetMixer at all.
  • Oversampling latency reported through setLatencySamples, integer per stage.
  • Low-band delay compensation verified by flat-sum at 44.1/48/96 kHz.
  • No ComboBoxAttachment item-population assumption (generic editor).

CI

The macOS release workflow is known-blocked on the org secret APPLE_CERT_P12 having visibility=private; that is unrelated to this branch. The regular CI matrix (build + tests, macOS and Windows) is unaffected.

Do not merge without green CI.

yves-vogl added 10 commits July 27, 2026 03:16
Adds the golden-render regression harness that the v0.3.0 state/preset
migration will be validated against (brief section 6, T10).

The four fixtures (Gnaw/Wool/Razor with the gate and low-band compressor
engaged and the safety clip off, plus one clip-on case) were generated from
the v0.2.0 code at this branch's point of origin, so the committed state XML
carries exactly 39 PARAM elements and no stateVersion attribute - which is
what makes it genuine legacy state rather than a v0.3.0 round-trip.

Renders are 0.25 s of deterministic stereo program material at 48 kHz. The
brief sketches 4 s; the shorter window is a deliberate repo-weight trade that
costs no regression power, since any engine landing on the wrong code path
diverges within the first milliseconds, and the burst envelope already sweeps
gate open/hold/close and a full compressor attack/release cycle inside it.

The generator is hidden behind a [.generate-goldens] tag so CI can never
regenerate what it is supposed to be checking against.
Introduces the parameter surface for the circuit-grade bass engine (39 -> 51
parameters) together with both halves of the migration that keeps existing
work sounding identical.

The three engine selectors (driveEngine, lowCompDetector, gateMode) default to
the new engines, because that is what a fresh instance should boot into. Saved
work never sees those defaults, via two independent paths:

- Sessions: getStateInformation() now stamps a stateVersion attribute on the
  APVTS root element, and setStateInformation() injects the Classic value for
  any of the three IDs a state without that attribute fails to mention. It
  runs after the existing v0.1 crossover migration, so a v0.1 session gets
  both in schema order.
- Presets: presets never pass through setStateInformation() at all. Because
  applyParsedPreset() resets to defaults before applying a preset's values, a
  legacy preset - which cannot name the new IDs - would otherwise adopt the
  new engines. PresetManager gains a generic, version-gated legacy back-fill
  for this, configured per plugin and empty by default, so the rest of the
  suite is unaffected. The preset JSON schema is untouched: this is a
  read-side default-fill, not a format change.

Both paths refuse to override a value that is explicitly present, matching the
existing crossover migration's defensive shape.

Nine of the twelve parameters carry non-neutral defaults; that is safe because
each is unread unless its engine selector is on the new value, which legacy
state never selects. The three that are live regardless - highBias, auto
makeup and clip ceiling - all default to exact no-ops.

Tests: parameter count and per-parameter default/range coverage, four new
state-migration cases (legacy injection, v0.1 double migration, no-override
guard, versioned round-trip), and the golden-render comparison itself, which
now renders the committed v0.2.0 fixtures through the migrated processor and
asserts sample-exact equality on macOS.
Introduces the circuit-derived replacement for the Mid and High bands, behind
the driveEngine selector. The Classic engine is untouched and remains the
bit-identical fallback, which the golden-render test continues to verify.

src/dsp/ADAAShaper.h - first-order antiderivative antialiasing (Parker,
Zavalishin & Le Bivic, DAFx-16). Closed forms for tanh (F1 = ln cosh,
evaluated via log1p so it neither overflows nor cancels) and hard clip, plus a
tabulated variant with cubic interpolation for curves whose antiderivative is
not elementary. The table integrates the sampled curve with Simpson's rule so
f and F1 stay mutually consistent - an F1 that is not really the antiderivative
of the f being applied shows up as a DC step on overload, not as a small error.
Out-of-range inputs continue F1 linearly at the edge slope, which is exact for
every saturating curve tabulated here.

src/dsp/CircuitDrive.{h,cpp} - one shared oversampling region for both bands,
replacing v0.2.0's two independent 4x instances (a duplication MidBand.h's own
comment already conceded). The remainder is upsampled once, split by an LR4
crossover running at the oversampled rate, processed, summed and downsampled
once. The factor adapts to the host rate: 4x below 50 kHz, 2x below 100 kHz,
1x above, since ADAA-1 contributes 20-30 dB of alias suppression on top of the
oversampling headroom.

Per voicing: Gnaw gets a pre-emphasis shelf and its EXACT algebraic inverse
behind the clipper, so drive 0 collapses to unity structurally rather than
approximately; Wool gets the asymmetric diode clipper's DC curve, Newton-solved
per table point at prepare time, plus a dynamic bias side chain that leaves the
clipper offset for ~20 ms after a loud passage (the sag a memoryless shaper
cannot produce); Razor gets the feedback clipper's unity-clean-plus-clipped-
difference structure with the 720 Hz pedal corner moved to 330 Hz for the bass
register. Gnaw and Razor share the drive-tracked Cc pole.

Two deliberate deviations from the brief, both to satisfy the brief's own
assertions:

- The tracked lowpass opens to 61 kHz at drive 0, not 24 kHz. A one-pole at
  24 kHz is already -1.9 dB at 18 kHz and cannot meet T3's requirement that
  drive 0 measure within +/-0.5 dB of the same filter bypassed. 61 kHz is both
  the figure the research gives for the real circuit with the pot open and the
  one that meets the contract (-0.36 dB at 18 kHz). R2(D) uses a square-law
  (audio-taper) pot rather than the datasheet's linear law, which is what keeps
  the pole above 12 kHz at half drive as T3 requires.
- Latency is reported as the maximum across BOTH engines, with the Circuit path
  padded up to it, instead of being re-reported when driveEngine changes. Hosts
  handle mid-transport latency changes poorly, and an automated engine switch
  should not shift the plugin's timing. Reported latency now depends on sample
  rate alone.

Switching engines runs both for 64 samples and fades between them; the two
produce genuinely different signals, so branch-swapping alone would step the
output.
Adds the aliasing and drive-engine measurement suites (brief T1-T6, T18, T19)
together with an FFT analysis harness in TestHelpers.

Three real defects the measurements caught, all fixed here:

- Switching drive engines dumped stale audio. Only one engine runs at a time,
  so the idle one's oversampling history, crossover state and blend delay
  lines still held whatever was passing through when it was last selected -
  released as a burst on the next switch. Measured: 1.96 peak and a 0.44
  sample-to-sample step against a 1.13 steady state. The incoming engine is now
  flushed at the switch, and the crossfade runs 256 samples rather than 64 so
  it covers the engine's own latency while it refills. Now 1.39 peak, 0.13
  step - below what the programme itself produces.
- The crossfade used an equal-power law. That law is for uncorrelated sources;
  two renderings of the same programme are strongly correlated, so cos/sin
  summed up to +3 dB mid-fade. Constant-gain is correct here.
- Wool's dynamic bias injected its own signal. The decaying bias envelope was
  audible as a thump after every loud note, and no DC blocker can remove a
  20 ms decay. The shaper's response to the offset alone is now subtracted, so
  only what the bias is for - the change in the curve's local slope - survives.
  Bias depth was also reduced, since the asymmetry it creates is real DC that
  the blocker then has to restore.

Four assertions deviate from the brief's numbers. Each is a case where the
brief's figure could not be met or could not be measured as written, and the
reasoning is recorded at the assertion:

- T1's flat -80 dB alias floor is not reachable for Gnaw, a 40x hard clip
  whose harmonics fall off as 1/n. Raising the engine to 8x was measured too
  and still misses it. Delivered instead: 25-30 dB better than Classic against
  a 10 dB requirement, -80 dB or better through the bass register, and a -49 dB
  floor everywhere.
- T3's 330 Hz corner and the drive-0 pole transparency are asserted on the
  filter primitives. Neither is observable through the plugin: splitHighHz
  bottoms out at 300 Hz, so the high band barely contains 330 Hz.
- T6's +/-0.5 dB engine parity holds to 3 kHz. Above that Circuit is up to
  2.5 dB brighter, in one direction and for one reason - its tone lowpass runs
  at the oversampled rate and so escapes the bilinear warping the base-rate
  Classic filter has. Restoring parity would mean reintroducing that warping
  deliberately or splitting the shared oversampling region back apart.
- T5's sag comes out with the opposite sign to the brief's prediction: the
  probe blooms rather than dips, because the DC the bias creates dominates the
  slope change. History-dependence is confirmed and is 11 dB on Wool against
  1 dB on the memoryless voicings. Flagged for the ear-tuning gate.
…ter taps

Four stages, each behind its own engine switch so the v0.2.0 behaviour stays
reachable and stays the default for migrated sessions.

LevelDetector (lowCompDetector = Smooth RMS). A log-domain RMS detector with a
soft knee, smoothing applied after the gain computer rather than before it, so
attack and release do not become level-dependent. This fixes the low band's
most audible v0.2.0 weakness: a peak detector with the sourced 6 ms release
follows the half-cycles of a bass fundamental, so the gain reduction ripples
and the low end tremolos. Measured ripple on an 80 Hz tone 6 dB over threshold
drops from over 1 dB to under 0.5 dB. Adds program-dependent release (a
dual-envelope race, so transients recover fast and sustained notes are not
pumped) and auto-makeup at -0.5*T*(1 - 1/R) - written with the sign spelled
out and anchored in a test, because the transposed form is a plausible slip
that would quietly attenuate every preset.

GateEngine (gateMode = Modern). Hysteresis, retriggering hold, a detector-only
sidechain highpass and a dB-linear release, with the control path running per
sample because a block-rate gate chatters and cannot express a 2 ms attack at
all. Channels are linked, so a stereo image cannot wander with one side
opening before the other. gateRatio stays Classic-only - Modern is a gate with
a range floor, not a ratio expander.

OutputClipper. Replaces the raw base-rate std::tanh with an ADAA ceiling clip
in delta form, which is transparent below the ceiling instead of lowpassing
the whole mix whenever the clip is armed (the naive antialiased form
degenerates to a two-tap average, -8.3 dB at 18 kHz). Measured deviation
spread across 40 Hz - 20 kHz is 0.13 dB.

One correction to the brief's design here: the delta form is algebraically
ADAA(clip) plus a first-difference term, and that term can push a sample back
OVER the ceiling - measured at 1.15 against a ceiling of 1.0, which would make
this a tone shaper rather than a safety clip, and broke an existing gain
-staging assertion. A final hard bound at the ceiling is applied. It costs
nothing below the ceiling (the residual is ~0 and it never engages) and only
trims the overshoot above it. At extreme overdrive the bound does cost the
antialiasing advantage; that is the right priority ordering for a SAFETY clip,
and heavy clipping belongs in the drive stages, which are oversampled for it.

MeterTaps. A plain struct of atomics - no FIFO, no queue, no allocation -
carrying I/O peak, per-band level, and gate and low-comp gain reduction. Both
drive engines report their own band levels, since the Circuit engine's bands
are summed inside its oversampled region and cannot be measured from outside.

The clip-ON golden fixture's null contract is restated at -25 dB (measured
-26.5) rather than the brief's -40. The brief qualifies that figure as
applying at typical levels; the fixture is deliberately driven 12 dB past the
ceiling, where v0.2.0's tanh is producing close to a square wave and rounding
those corners is the entire point of the change. The contract that matters -
transparency when not clipping - is asserted directly and far more tightly.
…uit showcases

Completes the second half of the migration story. Every factory preset now
names all three engine selectors explicitly rather than relying on defaults or
on the version gate:

- default.json pins Circuit / Smooth RMS / Modern and moves to pluginVersion
  0.3.0. This is the actual mechanism by which a fresh instance reaches the new
  engines: the processor constructor calls applyStartupDefault(), which loads
  this file. Until this commit the release's headline feature was off by
  default, because the file still declared 0.2.0 and the legacy back-fill was
  correctly overriding it.
- The eight presets voiced against the v0.2.0 DSP pin Classic, so none of them
  changes character.
- Circuit Foundation, Circuit Grind and Circuit Knife are new, and exercise the
  parts of the engine the older presets cannot reach: dynamic bias, the soft
  knee, auto-release and auto-makeup, and the Modern gate's hysteresis, hold
  and sidechain highpass.

Also bumps the project version to 0.3.0.

Tests cover the whole matrix: the default's Circuit pins, the eight Classic
pins, the new presets loading and rendering finite, legacy user presets
(tuned, a Default shadow, and one with no pluginVersion at all) landing on
Classic, and - the case that keeps the gate honest - a v0.3.0 preset that
names one engine and omits the others keeping the NEW defaults for those,
rather than being dragged back to legacy values.
Adds the brief's T14, T15 and T16, and the allocation guard they need.

AllocationGuard.h replaces the global operator new/delete so allocations can be
counted from anywhere - including inside JUCE and the standard library, which
is where a real regression would hide. Real-time safety is the one property of
this plugin that cannot be heard until it is too late: a processBlock() that
calls malloc sounds perfect right up until the allocator takes a lock. Measured
at zero allocations across 200 blocks with every v0.3.0 feature enabled, on
both engines, and zero on the oversized-block chunking path.

T15 is measured in the time domain rather than the frequency domain. A spectral
version was written first and rejected: sweeping drive across a 1 kHz tone
legitimately rewrites its harmonic structure, and the resulting modulation
sidebands land on non-harmonic bins, so a non-harmonic-energy metric reports
about -28 dBc whether the parameter is stepped or perfectly smooth. Zipper
noise is a discontinuity, so it is now measured as one: the largest
sample-to-sample step during a fast sweep, against the largest the same signal
produces with the parameter held still.

The Circuit engine also gains per-sample ramping of its automatable scalars
(drive, blend, bias, level, and the tracked and tone lowpass coefficients),
replacing per-block constants. Each channel walks the same ramp and the stored
value advances once per block, so the channels stay in step.

T14's dirac assertion is split by engine. Classic's impulse peaks exactly at
the reported latency at every rate, as it always has. The Circuit engine peaks
up to 25 samples (0.5 ms) later, and that is not a reporting error: the
reported figure is the oversampling delay both engines share, while the peak of
a reconstructed impulse also carries the group delay of every IIR filter in the
chain - and the Circuit high band adds two the Classic one does not have. No
single number can describe a frequency-dependent group delay, so the properties
actually asserted are that reported latency is identical across both engines
and all four sample rates, and that the three-way sum stays flat within 1 dB at
each rate.
The editor gains a plain labelled row - input peak, output peak, low-comp gain
reduction, gate gain reduction - polled from the lock-free MeterTaps at 30 Hz
and sitting between the preset bar and the generic parameter editor. The
generic editor already surfaces the twelve new parameters for free, so this is
the only UI code the release needs. It exists so the metering BACKEND is
usable and verifiable now; the photoreal M3 GUI consumes the same struct later.

Docs: CHANGELOG gets the full v0.3.0 entry including a Known deviations section
recording every place the implementation departs from the brief and why;
docs/manual.md gains an Engines section explaining that a fresh instance boots
into the new engines while saved work does not, the twelve new parameters with
their engine-gating called out, and a rewritten migration section; and
docs/architecture.md gains sections on the two drive engines, the antialiasing
core, why latency is reported as the maximum across both engines, why the
safety clip applies ADAA to the residual rather than the signal, the metering
design, and the two independent migration entry points.
MSVC does not implement std::aligned_alloc, so the Windows CI job failed to
compile RobustnessTests with C2039/C3861/C3535 on the aligned operator new.
Route aligned allocation through _aligned_malloc/_aligned_free on MSVC and
keep std::aligned_alloc/std::free elsewhere; the aligned operator delete
overloads have to take the same branch, because freeing an _aligned_malloc
block with std::free is undefined behaviour.
The non-macOS branch of the migration golden test asserted a -120 dB
absolute RMS null on the assumption that cross-toolchain drift stays at
the last ulp. It does not: the gate and the low-band compressor both
decide off a detector level, so a 1-ulp difference near a threshold moves
a gate transition or a ballistics trajectory by a sample. The Windows
runner measures -75 to -81 dB absolute, i.e. about -73 dB relative to the
~0 dBFS goldens.

Assert that ratio instead, at 60 dB below program, and guard it with a
REQUIRE that the golden is not silent. A migration landing on the wrong
engine changes the render grossly, so the test discriminates exactly as
before. The null is now computed unconditionally and only the assertion
is platform-dependent, so the macOS job compiles the Windows path too.

macOS keeps its sample-exact memcmp.
@yves-vogl
yves-vogl merged commit 9430c12 into main Jul 27, 2026
2 checks passed
@yves-vogl
yves-vogl deleted the feat/v0.3.0-sota-dsp branch July 27, 2026 03:38
@yves-vogl yves-vogl mentioned this pull request Jul 27, 2026
5 tasks
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.

Metering backend (I/O peaks + GR) via atomics

1 participant