Skip to content

audiograph: a phaser, a glide, LFO shapes and a clocked sample-and-hold - #303

Merged
bdbarnett merged 1 commit into
mainfrom
feat/graph-nodes
Oct 21, 2026
Merged

bdbarnett merged 1 commit into
mainfrom
feat/graph-nodes

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

Four node kinds for audiograph, so the rest of the polysynths can run as one C graph: Poly6's phase mode, glide and portamento, the LFO shapes their LFO modules use, and sample-and-hold. They are on all three bindings, and no node file outside audiograph changed.

node what it is held to
PHASER audiobiquad.AllPass on one channel, through the same kernel: stages set at build (arg), with frequency, feedback and mix AllPass, 0 counts, fed the same settings each 256-frame block
LAG a glide on a pitch CV, the bend on top: a constant-time line (off, on, legato only) or a one-pole vrack's dsp/glide.py and dsp/utility.py GlideCore, 0 counts and 1 count
LFO_SHAPE a sine, a falling saw, a square and a random level beside the triangle, all on one phase vrack's LFOCore's shapes on that phase: sine 1 count, saw and square 0; the triangle is byte-identical to main
SAMPLE_HOLD latches at every rising clock edge, with an optional lag; with no clock, refreshes at a rate through audioshaper's kernel SampleAndHoldCore at 0 counts, 1 with a lag; audioshaper.SampleHold at 0

Across interpreters, tests/parity/graph_nodes_probe.py renders all four and verify_graph.py now compares it beside the runner's probe. CPython, MicroPython unix and CircuitPython unix agree on all 307,200 samples, and the runner's probe still agrees on its 41,221. Each of the four new planted faults (a phaser frequency a block late, a legato flag flipped, a shape switch a block late, a clock edge a sample late) makes the comparison fail, and CI runs them first.

Faults planted in the C, one per node, each failed tests/test_cpython_audiograph_nodes.py and were then reverted. They were: the phaser's mix never configured, the glide's ramp one step short, the saw rising, and the hold latching on falling edges.

Also worth knowing:

  • The LFO's phase adds a float step each sample, as the triangle always has, and runs up to 0.06% off its rate. That is 0.0065 of a cycle over ten seconds at 1 Hz, where LFOCore stays within 5e-5. Fixing it would move the triangle, so it stays, and docs/audiograph.md says so.
  • audiodsp_graph.c now includes audiodsp_fp_contract.h, as every other float file in src/shared does. On x86-64 nothing moves (both probes are byte-identical to main). On a target that fuses multiply-adds, the graph now renders what the desktop does.

Four node kinds, added after OUT so the existing kinds keep their numbers,
on CPython, MicroPython and CircuitPython alike. No node file outside
audiograph changed.

PHASER calls the all-pass kernel audiobiquad.AllPass uses, on one channel,
with its frequency, feedback and mix as parameters and the stage count as
the build argument. It matches AllPass at 0 counts fed the same settings
each 256-frame block.

LAG glides a pitch CV to each new note with the bend riding on top: a
constant-time line with off, on and legato-only modes, or a one-pole.
It matches vrack's two GlideCore classes at 0 counts and 1 count.

LFO_SHAPE adds a sine, a falling saw, a square and a random level drawn at
each cycle's start beside the triangle, all from the triangle's phase. The
triangle renders the same bytes as before. The shapes match vrack's
LFOCore on that phase (sine within 1 count, saw and square exact); the
phase itself runs up to 0.06% off its rate, as it always has.

SAMPLE_HOLD latches its signal at every rising clock edge with an optional
one-pole lag (vrack's SampleAndHoldCore, 0 counts, 1 with a lag), or, with
the clock open, refreshes at a rate through audioshaper's sample-and-hold
kernel (0 counts against audioshaper.SampleHold).

audiodsp_graph.c now includes audiodsp_fp_contract.h, as every other float
file in src/shared does, so no target fuses its multiply-adds.

tests/parity/graph_nodes_probe.py renders all four, and verify_graph.py
compares it across interpreters beside the runner's probe: CPython,
MicroPython and CircuitPython unix agree on all 307200 samples. Four new
planted faults, one per node, each make it fail; CI runs them first.
tests/test_cpython_audiograph_nodes.py holds each node to what it stands in
for.
@bdbarnett
bdbarnett merged commit 87090e3 into main Oct 21, 2026
31 checks passed
@bdbarnett
bdbarnett deleted the feat/graph-nodes branch October 21, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant