feat: the phaser, a layer effect with no storage and its own dry path #111
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
bcox/tymbal!111
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "phaser"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a phaser as the eleventh layer kind: the next effect from #105. It does not close that issue.
What it is
A stereo swept allpass chain as a layer engine,
dsp/phaser_layer.h. 2 to 12 stages (even counts), a sweep in octaves from the engine's own LFO, signed feedback, a stereo phase offset between the sides, and its own matrix. It holds no storage and runs at voice or channel scope.Two things differ from the delay layer, both on purpose:
mixruns from the input alone (0) through the deepest notch (1/2) to the chain alone (1). A notch is the input canceling against the chain, so that sum has to be sample-exact, and the mixer cannot do it: a channel-scope layer reads its tap one sample late, and on a return the dry signal is another channel's fader.PhaserStage. It usesOnePoleFilter's multiply and state but forms2 * lowpass - inputexactly and rounds once, toward zero. See "The limit cycle" below.Also in the branch: the resolver, format (
PHSR, minor 2.8), patch-diff walker, editor page, and one factory patch,phaser-return(bank 61 to 62).The limit cycle
The first version built each stage on
OnePoleFilter::Lowpass. A widened decay test found that with feedback on and the stage frequency at the one-pole's upper limit (12 kHz at 48 kHz), the chain never reached zero: a residual of at most 599 raw units that repeated exactly, which would keep a note-scope voice from ever freeing.The cause, confirmed by injection: the lowpass was rounded to nearest before being doubled.
PhaserStageremoves that rounding, and all 1,380 points of//dsp:phaser_layer_decay_testnow reach exact zero at 48 and 96 kHz.dsp/svf.his not modified.What it does not touch
Built while F8b and F9.1a were in progress, so the branch edits none of:
dsp/va_*.h,dsp/layer_strip.h,dsp/mod_ops.h,dsp/mod_matrix.h,dsp/voice_allocator.h,dsp/svf.h,dsp/phase.h,dsp/lfo.h,dsp/layer.h,resolve/va*.h,patches/poly800_strings.h, or anything underfabric/,rtl/,hps/.sizeof(VoiceType)is unchanged (the pin inplugin/instrument_test.ccis untouched and passes), andplugin/render_golden_test.ccandfabric/master_pin_test.ccare unmoved. The layer itself is 560 bytes.Verification
After the rebase onto current
main:bazel test //...: 313 of 313 pass.bazel test --config=opt //...: 313 of 313 pass.Before the rebase (not re-run after it):
--config=asan //...and--config=ubsan //...: 307 of 307 each.--config=tsanon the five scoped targets: 5 of 5.An independent review re-ran 20 of the authors' injections (all reproduced) and added 14 of its own, of which 10 failed no test. Each of those now fails a named test; the ledger's "Review fixes" section has the table. bcox has listened to it in the workstation.
Cost
--config=opt,//plugin:instrument_bench, three runs on the laptop, ns per voice per sample: chorus 54.6–59.4, phaser at 4 stages 93.7–100.8, at 12 stages 134.8–141.7. No fabric price is stated; this machine has no Quartus.Known, and left
//plugin:preset_race_testflakes under load onmaintoo. Its coverage floor atpreset_race_test.cc:636failed 28 of 300 oversubscribed opt runs onmainand 14 of 300 here; it passes 20 of 20 alone.//patches:auditionwrites a silent wav for every return, this one included, since it feeds no input.Spec, plan and ledger:
docs/superpowers/{specs,plans,ledgers}/2026-10-04-phaser-layer*.md.🤖 Generated with Claude Code