LPG, and an FM-pair shaper insert #43

Closed
opened 2026-08-30 15:17:30 +00:00 by bcox · 1 comment
Owner

Scoped 2026-09-05. The wave-wrapper half moved to #64 (more shaper shapes).
What is left here is the LPG, plus the one shaper reach that has no other
route.

An LPG is a lowpass and a VCA driven off one control, behind a vactrol -- fast
attack, slow and level-dependent release. The musical content is that
amplitude and brightness fall together, so quieter is always darker. That is
what natural instruments do and what a filter plus an independently enveloped
VCA does not.

1. dsp/low_pass_gate.h

Resonant two-pole lowpass plus a gain, both off one control, behind a vactrol.

  • Resonance to va_filter's measured Q=64 floor, with the same injection
    idiom at the top of travel: a ring that is fed, not one that is generated.
    No negative k, so no limiter and no oversampled stage. dsp/va_filter.h:25-62
    already priced true self-oscillation and declined it.
  • SvfState width, not TiltState. dsp/layer_strip.h:57-70 makes the
    argument: a resonant bandpass swings 81x its input at Q=64.
  • Round::kTowardZero, which is what makes the ring reach exact zero and so
    lets the voice free.

2. Three placements, one core, differing only by policy

Strip. Control is the vactrol and nothing else, which keeps
dsp/layer_strip.h:266-270's no-modulation rule intact. NoteOn gains the
note number alongside the sample rate, for key follow on the base cutoff.
Activity is a scalar "is the vactrol still open", OR'd into Voice::IsActive
-- which today folds over layers only and never consults the strip, so a short
exciter would retire the voice while the resonator is still ringing. Controls:
base cutoff, key follow, resonance, response, mode (VCF/VCA/both), depth.
Every engine inherits it, and this is the FM pair's LPG.

VA. A new VaFilterTopology value. Cutoff, resonance, matrix routing and
the injection are already in the filter row, and the VCA sits downstream of
the row, so the activity story needs nothing.

Unvoiced. A real stage with matrix-modulatable cutoff, against the
existing backstop_/flush_ tail logic at dsp/unvoiced_layer.h:367-379 --
the one file in the tree that already holds a voice for a ringing filter and
bounds it. A short noise burst into high resonance is the techno kick, and it
lands in the engine that already solved the hard part.

Not the FM pair, not modal, not partial. Modal already darkens as it decays
because decay_seconds is per mode; an LPG there imposes a fake decay over a
true one. Partial is additive and expresses it exactly already. The FM pair's
case was the amplitude-brightness coupling, which the strip delivers.

3. An FM-pair shaper insert

dsp/fm_pair_layer.h has no nonlinearity at all today. Layers do not chain --
Voice::Render sums slots with no slot-to-slot routing -- so an FM tone
cannot be routed through the VA layer's shaper, and the strip cannot host one
at 1x. A saturated FM tone is unreachable unless the shaper is in the engine.

  • The full ShaperShape set, not a subset. dsp/shaper.h is the single owner
    of the curve list; a per-engine allowlist is the second edit site that file
    exists to prevent, and it would go stale the moment #64 lands.
  • Drive plus soft and hard clip is the point. The fold comes along and is not
    the reason.
  • ratio_ becomes max(Carson, MinimumRatio(shape)), still clamped to
    kOversample. It must read MinimumRatio rather than hardcode a number, so
    #64's shapes are inherited automatically.
  • Two things to pin rather than assume: a shaper can push a patch off the 1x
    path onto the decimator path, where flush_ and decimator_flush_ differ
    (dsp/fm_pair_layer.h:2185-2223); and reported latency must not move, since
    TapsForRatio(r) = 64r + 1 puts group delay at 32 output samples at every
    ratio and kLatencySamples reads kOversample, not ratio_.

Open

Naming. The strip stage and the engine stage behave differently, and calling
both "LPG" is a patch-author trap. Provisionally Damper for the strip and
LPG for the engine version.

Scoped 2026-09-05. The wave-wrapper half moved to #64 (more shaper shapes). What is left here is the LPG, plus the one shaper reach that has no other route. An LPG is a lowpass and a VCA driven off one control, behind a vactrol -- fast attack, slow and level-dependent release. The musical content is that amplitude and brightness fall together, so quieter is always darker. That is what natural instruments do and what a filter plus an independently enveloped VCA does not. ## 1. `dsp/low_pass_gate.h` Resonant two-pole lowpass plus a gain, both off one control, behind a vactrol. - Resonance to `va_filter`'s measured Q=64 floor, with the same injection idiom at the top of travel: a ring that is *fed*, not one that is generated. No negative k, so no limiter and no oversampled stage. `dsp/va_filter.h:25-62` already priced true self-oscillation and declined it. - `SvfState` width, not `TiltState`. `dsp/layer_strip.h:57-70` makes the argument: a resonant bandpass swings 81x its input at Q=64. - `Round::kTowardZero`, which is what makes the ring reach exact zero and so lets the voice free. ## 2. Three placements, one core, differing only by policy **Strip.** Control is the vactrol and nothing else, which keeps `dsp/layer_strip.h:266-270`'s no-modulation rule intact. `NoteOn` gains the note number alongside the sample rate, for key follow on the base cutoff. Activity is a scalar "is the vactrol still open", OR'd into `Voice::IsActive` -- which today folds over layers only and never consults the strip, so a short exciter would retire the voice while the resonator is still ringing. Controls: base cutoff, key follow, resonance, response, mode (VCF/VCA/both), depth. Every engine inherits it, and this is the FM pair's LPG. **VA.** A new `VaFilterTopology` value. Cutoff, resonance, matrix routing and the injection are already in the filter row, and the VCA sits downstream of the row, so the activity story needs nothing. **Unvoiced.** A real stage with matrix-modulatable cutoff, against the existing `backstop_`/`flush_` tail logic at `dsp/unvoiced_layer.h:367-379` -- the one file in the tree that already holds a voice for a ringing filter *and* bounds it. A short noise burst into high resonance is the techno kick, and it lands in the engine that already solved the hard part. Not the FM pair, not modal, not partial. Modal already darkens as it decays because `decay_seconds` is per mode; an LPG there imposes a fake decay over a true one. Partial is additive and expresses it exactly already. The FM pair's case was the amplitude-brightness coupling, which the strip delivers. ## 3. An FM-pair shaper insert `dsp/fm_pair_layer.h` has no nonlinearity at all today. Layers do not chain -- `Voice::Render` sums slots with no slot-to-slot routing -- so an FM tone cannot be routed through the VA layer's shaper, and the strip cannot host one at 1x. A saturated FM tone is unreachable unless the shaper is in the engine. - The full `ShaperShape` set, not a subset. `dsp/shaper.h` is the single owner of the curve list; a per-engine allowlist is the second edit site that file exists to prevent, and it would go stale the moment #64 lands. - Drive plus soft and hard clip is the point. The fold comes along and is not the reason. - `ratio_` becomes `max(Carson, MinimumRatio(shape))`, still clamped to `kOversample`. It must read `MinimumRatio` rather than hardcode a number, so #64's shapes are inherited automatically. - Two things to pin rather than assume: a shaper can push a patch off the 1x path onto the decimator path, where `flush_` and `decimator_flush_` differ (`dsp/fm_pair_layer.h:2185-2223`); and reported latency must not move, since `TapsForRatio(r) = 64r + 1` puts group delay at 32 output samples at every ratio and `kLatencySamples` reads `kOversample`, not `ratio_`. ## Open Naming. The strip stage and the engine stage behave differently, and calling both "LPG" is a patch-author trap. Provisionally **Damper** for the strip and **LPG** for the engine version.
bcox changed title from LPG, wave wrappers to LPG, and an FM-pair shaper insert 2026-09-05 16:28:46 +00:00
Author
Owner

Landed on main as of 0910519.

What shipped

  • dsp/low_pass_gate.h -- the shared core: resonant two-pole lowpass
    plus gain off one control, behind a Vactrol. kGateMinDamping at the
    measured Q=64 floor, SvfState width, Round::kTowardZero so the ring
    reaches exact zero and frees the voice.
  • Three placements from that one core, split by which modulation
    sources each reaches: the Damper in dsp/layer_strip.h,
    VaFilterTopology::kLowPassGate in the VA filter row, and a real LPG
    stage in dsp/unvoiced_layer.h against the existing
    backstop_/flush_ tail logic.
  • The FM-pair shaper insert, full ShaperShape set, with
    ratio_ = max(Carson, MinimumRatio(shape)) clamped to kOversample.
  • Patch format minor 32 with the SMTX chunk, editor controls for the
    Damper, and patches/lpg_kick.h as a factory patch.

Both of the "pin rather than assume" items in section 3 are tested rather
than assumed. TheShaperDoesNotMoveTheAlignment asserts the unshaped case
stands at ratio 1 and the shaped case has crossed onto the decimator path
before it measures realized delay, so it fails loudly if it ever stops
exercising that boundary; ReportsDecimatorLatency holds
kLatencySamples at 32 across every ratio.

One deviation from this issue's text

Section 2 said the strip's control would be "the vactrol and nothing
else", keeping dsp/layer_strip.h:266-270's no-modulation rule intact.
That was overruled during design: velocity and pressure should not be
hardwired, they should be default modulation routings. So the strip gained
a ModMatrix with four destinations (kDamperTarget, kDamperCutoff,
kDamperResonance, kDamperDepth) and two default routings, and that
comment was rewritten rather than preserved. A superset of what was asked
for here, but this issue's body no longer describes what shipped.

Verification

bazel test //... 93/93 at fastbuild and --config=opt, goldens unmoved;
--config=asan 93/93, --config=ubsan 93/93, --config=tsan on
//plugin:preset_race_test 1/1.

Follow-up

#73 -- the unvoiced engine's mod matrix has no editor UI, so this issue's
three new LPG destinations are unreachable from the panel. Pre-existing
rather than introduced here: every unvoiced destination has always been
unreachable, and the routes still round-trip correctly through a
.dsppatch.

Landed on `main` as of `0910519`. ## What shipped - **`dsp/low_pass_gate.h`** -- the shared core: resonant two-pole lowpass plus gain off one control, behind a `Vactrol`. `kGateMinDamping` at the measured Q=64 floor, `SvfState` width, `Round::kTowardZero` so the ring reaches exact zero and frees the voice. - **Three placements from that one core**, split by which modulation sources each reaches: the **Damper** in `dsp/layer_strip.h`, `VaFilterTopology::kLowPassGate` in the VA filter row, and a real LPG stage in `dsp/unvoiced_layer.h` against the existing `backstop_`/`flush_` tail logic. - **The FM-pair shaper insert**, full `ShaperShape` set, with `ratio_ = max(Carson, MinimumRatio(shape))` clamped to `kOversample`. - Patch format **minor 32** with the `SMTX` chunk, editor controls for the Damper, and `patches/lpg_kick.h` as a factory patch. Both of the "pin rather than assume" items in section 3 are tested rather than assumed. `TheShaperDoesNotMoveTheAlignment` asserts the unshaped case stands at ratio 1 and the shaped case has crossed onto the decimator path *before* it measures realized delay, so it fails loudly if it ever stops exercising that boundary; `ReportsDecimatorLatency` holds `kLatencySamples` at 32 across every ratio. ## One deviation from this issue's text Section 2 said the strip's control would be "the vactrol and nothing else", keeping `dsp/layer_strip.h:266-270`'s no-modulation rule intact. That was overruled during design: velocity and pressure should not be hardwired, they should be default modulation routings. So the strip gained a `ModMatrix` with four destinations (`kDamperTarget`, `kDamperCutoff`, `kDamperResonance`, `kDamperDepth`) and two default routings, and that comment was rewritten rather than preserved. A superset of what was asked for here, but this issue's body no longer describes what shipped. ## Verification `bazel test //...` 93/93 at fastbuild and `--config=opt`, goldens unmoved; `--config=asan` 93/93, `--config=ubsan` 93/93, `--config=tsan` on `//plugin:preset_race_test` 1/1. ## Follow-up #73 -- the unvoiced engine's mod matrix has no editor UI, so this issue's three new LPG destinations are unreachable from the panel. Pre-existing rather than introduced here: every unvoiced destination has always been unreachable, and the routes still round-trip correctly through a `.dsppatch`.
bcox closed this issue 2026-09-07 04:45:05 +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#43
No description provided.