Skip to content

synthio.Biquad: take CircuitPython main's widened biquad - #353

Merged
bdbarnett merged 2 commits into
mainfrom
sync/cp-biquad-widen
Oct 30, 2026
Merged

bdbarnett merged 2 commits into
mainfrom
sync/cp-biquad-widen

Conversation

@bdbarnett

@bdbarnett bdbarnett commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #310. A biquad on a low shelf or a low-frequency filter no longer settles at a DC offset in silence.

synthio.Biquad, and so audiofilters.Filter, Note.filter, Echo.filter, freeverb's filter and the graph's LPG node, now run CircuitPython main's arithmetic at ae47a3f15d (adafruit/circuitpython#11679, which also carried #11678):

  • 24546b7d1d: fast_sincos reflects the angle into the first quadrant first.
  • 0a7df85004: each filter's coefficients get a shift of up to 30 bits, and the state keeps 12 fractional bits in a 64-bit sum that rounds to nearest.

Both are taken in upstream's words. The Python filter chain behind Echo.filter and freeverb on CPython also had PEAKING_EQ's b2 sign the wrong way round, and that's fixed too.

What it does

#310's repro is a 200 Hz LOW_SHELF at A=1.0, quiet noise then silence, through audiofilters.Filter at 36 kHz. Here is the offset over the last 2000 silent samples:

build before after
unix MicroPython +371 0
CPython +371 0

Upstream's four biquad tests (synthio_biquad_dc_offset, _low_frequency, _high_frequency, _peaking) now print upstream's expected output on both builds. They failed before.

A harness built from upstream's own Biquad.c at ae47a3f15d agrees with this port on 20,000 random filters across all seven modes and rates from 8 to 48 kHz. That is 40,960,000 samples with no difference. The previous kernel fails the harness.

Gates

  • verify_dsp with MicroPython and CPython: 62 comparisons, 0 failures.
  • verify_graph with MicroPython and CPython: all four probes agree to 0 counts. A planted lpg fault fails it.
  • verify_effects, verify_streaming and verify_mixdown_knee pass unchanged.
  • The unit tests that reach biquads, filters, echo, freeverb and the graph: 343 tests, OK.

What moved, and why

No CircuitPython release has #11679 yet, so the 11.0.0-alpha.2 oracle now disagrees wherever a synthio.Biquad is heard.

  • verify_dsp oracle skips. echo_filter_probe and freeverb_filter_probe matched the oracle byte for byte before this change, and they now skip the CircuitPython comparison. The note on biquad_component_probe is updated to match. MicroPython and CPython are still compared.

  • biquad_component golden. It was re-captured because the filter output changed: d9950b16… became ba714db8….

  • synthtools_acceptance golden. Its stdout was re-captured. lead, bend and bass run a resonant low-pass and moved, and the other five lines are unchanged. circuitpython_stdout still holds alpha.2's answer, and its note now says why the two differ.

  • test_cpython_audiograph_nodes Overdrive comparison. The reference now gets tone as float32, the way the node holds it. With 30-bit coefficients, a tone that isn't float32 moves the output by one count.

  • test_cpython_audiobiquad fixed-point test. It now pins the new numbers. A DC burst into a low-pass settles at exactly 0 at 400 Hz and 1000 Hz. At 100 Hz and 40 Hz it settles 1 and 4 counts away, where Q15 used to hold 2631 and 16143.

  • Float-precision CI bounds. The single-precision MicroPython cell already allowed biquad_component_probe and echo_filter_probe to differ from CPython on 4 lines each, because synthio.Biquad derives W0 in mp_float_t (synthio.Biquad derives W0 in mp_float_t while the CPython extension uses the shared double helper, so a single-precision target runs a different filter #101). The 30-bit coefficients now carry more of that float rounding than Q15 did, and CI measured 35 and 6 lines. The bounds are set to those numbers, so the gate still fails if either one grows.

When the oracle pin moves to a release that has #11679, drop the three skips and re-capture both goldens from the oracle. upstream-sync.md has the details.

Held for the next resync

These upstream changes landed between alpha.2 and ae47a3f15d and are not taken here:

  • the synthio.MidiTrack commits 679e9700ab, ff05e5b620, aad79fe5eb, 6dfa7e5f20 and a54b951de6
  • the new audiotap module (e9b2fd49f5, f889146e81)

Downstream digests that run a synthio.Biquad, or the graph's OVERDRIVE with a knob that isn't float32, will move when they pick this up.

A low shelf or low-frequency filter no longer settles at a DC offset in
silence. synthio.Biquad, audiofilters.Filter, Note.filter and the LPG node
now run upstream's arithmetic at ae47a3f15d (adafruit/circuitpython#11679):
the reflected fast_sincos (24546b7d1d), per-filter coefficient shifts and
12-bit-fraction state (0a7df85004). The Python filter chain also gets
PEAKING_EQ's b2 sign right.

No CircuitPython release carries this yet, so the oracle is skipped for
three verify_dsp probes and biquad_component and synthtools_acceptance are
re-captured from this port; upstream-sync.md says what to undo when the pin
moves.
@bdbarnett
bdbarnett merged commit 2773312 into main Oct 30, 2026
31 checks passed
@bdbarnett
bdbarnett deleted the sync/cp-biquad-widen branch October 30, 2026 22:15
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.

Biquad: CircuitPython-path filters drift to a DC offset in silence

1 participant