Repository navigation
audiograph: a clipping two-pole filter and an overdrive - #322
Merged
Merged
Conversation
Two node kinds, added after SAMPLE_HOLD so the existing kinds keep their numbers, on CPython, MicroPython and CircuitPython alike. No node file outside audiograph changed; circuitpython.mk now also builds the distortion, biquad and synth-DSP kernels the overdrive calls. TWOPOLE is the three filters of vrack's dsp/twopole.py behind one node with a type: the Sallen-Key low-pass and high-pass (diode-clipped peak feedback) and the CMOS state-variable filter in low-pass, band-pass and high-pass. The cutoff CV is read at the block's first and last samples and the coefficient walks between them, as those cores take it. The CMOS filter matches CMOSFilterCore at 0 counts with the CV still, stepping, running through a block, and not connected (184,320 samples). The Sallen-Key pair matches within 1 count, on 90 of 122,880 samples: their tanh is a rational function in float that gives the same bits on every target, where vrack's is libm's in double. OVERDRIVE is vrack's OverdriveCore: audiofilters.Distortion's soft-clipped overdrive, then a high shelf in synthio.Biquad's own Q15 arithmetic, then audiofilters.Filter's mix-down knee. It matches the chain of audiofilters nodes at 0 counts, with the knobs moving every block. tests/parity/graph_nodes_probe.py renders both, and verify_graph.py compares it across interpreters: CPython, MicroPython (double and single float) and CircuitPython unix agree on all 491,520 samples of that probe. Two new planted faults, a cutoff CV a block late and an overdrive tone a block late, each make it fail; CI runs them first.
VCA_CURVE = VCA_EXPONENTIAL makes the gain 60 dB over the CV, 10^(3 * (cv / UNIT - 1)), unity at UNIT, times VCA_GAIN. Linear stays the default.
The overdrive node computes its gains with double-precision pow and log10, and calls into audiodsp_distortion.c and audiodsp_biquad.c, which use pow, round and ldexp. CircuitPython's RP2040 build has none of these to link, so test_circuitpython_libm failed once those files joined circuitpython.mk. The node now lives in audiodsp_graph_overdrive.c. CircuitPython builds without it (AUDIODSP_GRAPH_HAS_OVERDRIVE=0) and without the two files. There, audiodsp_graph_kind_built() says no, add() refuses the kind, and the module has no OVERDRIVE constants. Every other build is unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two new
audiographnode kinds, added afterSAMPLE_HOLDso the existing kinds keep their numbers. They work on CPython, MicroPython and CircuitPython, and no node file outside audiograph changes.TWOPOLEis three filters behind one node with a type: the Sallen-Key low-pass and high-pass (diode-clipped peak feedback) and the CMOS state-variable filter in low-pass, band-pass and high-pass. These keep their sound in a clipper inside the loop, so a biquad can't stand in. Parameters areTWOPOLE_TYPE,TWOPOLE_CUTOFF_HZ,TWOPOLE_DEPTH,TWOPOLE_RESONANCEandTWOPOLE_DRIVE, with a second input for the cutoff CV. The cutoff isCUTOFF_HZ * 2^(cv / UNIT * depth * 10), held between 20 Hz and 0.45 of the sample rate. The CV is read at the block's first and last samples and the coefficient walks between them.OVERDRIVEisaudiofilters.Distortion's soft-clipped overdrive followed by a high shelf at 1.5 kHz (synthio.Biquad's arithmetic), withOVERDRIVE_DRIVE,OVERDRIVE_TONEandOVERDRIVE_LEVEL.Both are held to the Python cores they stand in for (vrack's
dsp/twopole.pyandOverdriveCore):CMOSFilterCore, CV still, stepping, running through a block, and openSallenKeyLPFCore,SallenKeyHPFCore, same four CV casesaudiofiltersnodesOverdriveCorebuilds, knobs moving every blockThe Sallen-Key pair isn't at 0 because its
tanhis a rational function in float that returns the same bits on every target, where the Python core's is libm's in double. The cutoff arithmetic does reproduce the Python core's float32 steps, because at 1e-7 the CMOS filter clips hard enough to turn a seventh-digit difference into 16 counts.tests/parity/graph_nodes_probe.pynow renders both, andverify_graph.pycompares it across interpreters. CPython, MicroPython (double and single float) and CircuitPython unix agree on all 491,520 samples of that probe at 0 counts. Two new planted faults (a cutoff CV a block late, an overdrive tone a block late) each make it fail, and CI runs them first alongside the existing ones.circuitpython.mknow also buildsaudiodsp_distortion.c,audiodsp_biquad.candaudiodsp_synth_dsp.c, which the overdrive calls.One cost to know: while the cutoff CV moves within a block, the coefficient walks in double, one add and one divide a sample, so it matches the Python core exactly. On a board without a double FPU that is a few percent of one core per filter.
The VCA also takes
VCA_CURVE:VCA_EXPONENTIALis 60 dB over the CV,10^(3·(cv/UNIT − 1)), the response a voice with an exponential VCA has. Linear stays the default.