kAmplitudeOnly on a kLowPassGate slot escapes the oversample ratio's cutoff ceiling #86

Closed
opened 2026-09-09 22:28:29 +00:00 by bcox · 1 comment
Owner

ResolveGate sets cutoff = sweep_cutoff ? swept_cutoff : kSvfFrequencyLimit — under LowPassGateMode::kAmplitudeOnly it pins the corner wide open, not unchanged. On a kLowPassGate slot nothing narrows that afterwards, so the SVF runs its corner at kSvfFrequencyLimit regardless of the layer's derived oversample ratio.

That is fs_internal/4: 12 kHz at 1x and 48 kHz at 4x, where the decimator removes it. Same patch, same notes, a different render depending on a ratio the user cannot see — which is the invariant dsp/va_layer.h's header calls the stage's governing one ("the derived ratio must be inaudible except for aliasing").

It is not a correctness bug. The two-pole is linear, the value is in-format, and no factory patch is affected — the bank has no kLowPassGate slot in a VA filter row at all (lpg-kick is on the unvoiced engine with its own gate). It is a ratio-invariance dent, and it is reachable from a stored patch byte and from the editor.

The cheap fix exists and was deliberately not taken during ladder-gate: std::min(resolved.cutoff, cutoff_limit). That is bit-exact at 1x, since VaCutoffLimitForRatio(1) == kSvfFrequencyLimit makes the min an identity there, so it changes no shipped 1x render. It keeps "wide open" meaning the format ceiling and holds it at the same hertz at every ratio, which is exactly what VaCutoffLimitForRatio exists for.

Note the distinction that made this its own decision: narrowing to cutoff — the row's own knob, which is what the ladder gate does — would redefine what kAmplitudeOnly means on a shipped topology, since gate_mode's field comment promises "a plain VCA off the vactrol". Narrowing to cutoff_limit redefines nothing. The code at the kLowPassGate branch now states the dent and names this fix without taking it.

`ResolveGate` sets `cutoff = sweep_cutoff ? swept_cutoff : kSvfFrequencyLimit` — under `LowPassGateMode::kAmplitudeOnly` it pins the corner **wide open**, not unchanged. On a `kLowPassGate` slot nothing narrows that afterwards, so the SVF runs its corner at `kSvfFrequencyLimit` regardless of the layer's derived oversample ratio. That is fs_internal/4: 12 kHz at 1x and 48 kHz at 4x, where the decimator removes it. Same patch, same notes, a different render depending on a ratio the user cannot see — which is the invariant `dsp/va_layer.h`'s header calls the stage's governing one ("the derived ratio must be inaudible except for aliasing"). It is not a correctness bug. The two-pole is linear, the value is in-format, and no factory patch is affected — the bank has no `kLowPassGate` slot in a VA filter row at all (`lpg-kick` is on the unvoiced engine with its own gate). It is a ratio-invariance dent, and it is reachable from a stored patch byte and from the editor. **The cheap fix exists and was deliberately not taken during `ladder-gate`:** `std::min(resolved.cutoff, cutoff_limit)`. That is bit-exact at 1x, since `VaCutoffLimitForRatio(1) == kSvfFrequencyLimit` makes the `min` an identity there, so it changes no shipped 1x render. It keeps "wide open" meaning the format ceiling and holds it at the same hertz at every ratio, which is exactly what `VaCutoffLimitForRatio` exists for. Note the distinction that made this its own decision: narrowing to `cutoff` — the row's own knob, which is what the **ladder** gate does — would redefine what `kAmplitudeOnly` means on a shipped topology, since `gate_mode`'s field comment promises "a plain VCA off the vactrol". Narrowing to `cutoff_limit` redefines nothing. The code at the `kLowPassGate` branch now states the dent and names this fix without taking it.
Author
Owner

Fixed on ladder-gate-followups, commit 271645b. Taken as the issue specified it, with the clamp moved one level down.

ResolveGate now takes a limit (defaulting to the format ceiling) and applies it to the corner it resolves, rather than the call site clamping resolved.cutoff afterwards. That placement matters: ResolveGate builds its coefficients from the mode-selected cutoff, so a min applied to the returned field alone would have satisfied every obvious assertion and changed no sound at all. There is an injection for exactly that, and it fails.

The min covers both arms rather than only the pinned one, so the ceiling is a statement about the function's output instead of a property of one branch. The swept arm is already underneath it -- a sweep only scales base downward -- so it is an identity there, and the test pins that a swept corner is not lifted to a ceiling by being handed one.

Deliberately the ceiling and not cutoff, exactly as the issue argued: narrowing to the row's own knob would make a VCA's filter follow it and redefine what gate_mode promises. Bit-exact at 1x, and the default keeps dsp/layer_strip.h's damper and every existing test on the same bits.

Three injections, each caught by the assertion that names it: restoring the bare limit inside ResolveGate; applying the min to the reported field but not the coefficients; and dropping the argument at the VA call site, where the two probes come back bit-identical because the limit reached nothing.

Fixed on `ladder-gate-followups`, commit 271645b. Taken as the issue specified it, with the clamp moved one level down. `ResolveGate` now takes a `limit` (defaulting to the format ceiling) and applies it to the corner it resolves, rather than the call site clamping `resolved.cutoff` afterwards. That placement matters: `ResolveGate` builds its coefficients from the mode-selected cutoff, so a `min` applied to the returned field alone would have satisfied every obvious assertion and changed no sound at all. There is an injection for exactly that, and it fails. The `min` covers both arms rather than only the pinned one, so the ceiling is a statement about the function's output instead of a property of one branch. The swept arm is already underneath it -- a sweep only scales `base` downward -- so it is an identity there, and the test pins that a swept corner is not *lifted* to a ceiling by being handed one. Deliberately the ceiling and not `cutoff`, exactly as the issue argued: narrowing to the row's own knob would make a VCA's filter follow it and redefine what `gate_mode` promises. Bit-exact at 1x, and the default keeps `dsp/layer_strip.h`'s damper and every existing test on the same bits. Three injections, each caught by the assertion that names it: restoring the bare limit inside `ResolveGate`; applying the min to the reported field but not the coefficients; and dropping the argument at the VA call site, where the two probes come back bit-identical because the limit reached nothing.
bcox closed this issue 2026-09-10 18:52:16 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
bcox/tymbal#86
No description provided.