Skip to content

synthio: Widen the Biquad coefficients and feedback state - #11679

Open
bdbarnett wants to merge 2 commits into
adafruit:mainfrom
bdbarnett:biquad-coefficient-and-state
Open

bdbarnett wants to merge 2 commits into
adafruit:mainfrom
bdbarnett:biquad-coefficient-and-state

Conversation

@bdbarnett

Copy link
Copy Markdown

Steps 1 and 2 of #11269. This sits on top of #11678, so only the last commit is new. The two steps go together because the wider coefficients do nothing audible without the wider state.

Coefficients were Q15 and the feedback state was rounded to whole samples, so a quiet 50 Hz tone through a 100 Hz low-pass came out silent. Now the coefficients get as many fractional bits as the largest one leaves room for (up to 30), and the state keeps 12 bits below the sample. The sum of products is 64-bit. On a Cortex-M0+ that means five __aeabi_lmul calls a sample, and raspberry_pi_pico grows by 864 bytes. The output now rounds to nearest, which removes the DC offset GlasierXplor reported.

Tested with new low-frequency and DC-offset tests on the unix coverage build, and on a Pico Plus 2 W and a Feather RP2040. With both PRs, a 50 Hz low-pass is within 11 LSB worst and 7.4 RMS of a float64 reference, against 7300 and 3627 before.

Written by an agent, reviewed by a human

The old fit covered [0, pi/2] only. A filter above sample_rate / 4 used it
outside that range, where the error grows to 0.17 by Nyquist, so a low-pass
at 22 kHz / 48 kHz was 7 dB down at its own cutoff and a 22 kHz high-pass
rejected a 12 kHz tone by only 28 dB.

Reflect the angle into [0, pi/2] and then into [0, pi/4] by swapping sine and
cosine, and evaluate a four-term minimax polynomial for each. The error is
at the last bits of a float over the whole 0..Nyquist range, and a frequency
above Nyquist is merely wrong rather than absurd.
The five coefficients were Q15 for every filter, and the feedback state was
rounded to whole samples. Low down that loses everything. A low-pass at
100 Hz has b0 = 4.2e-5, which is 1.4 in Q15 and is kept as 1. And near 0 Hz a
biquad turns each unit of error in its feedback into thousands of units of
output (about 5900 at DC for a 100 Hz low-pass, 23000 for 50 Hz), so rounding
the feedback hands that gain half a count to amplify.

Give all five coefficients as many fractional bits as the largest of them
leaves room for in an int32_t, up to 30, and keep the count in the filter.
Keep the feedback state with 12 bits below the sample. A 30-bit coefficient
times a 28-bit state does not fit in 32 bits, so the sum of products is an
int64_t. The state is still an int32_t.

Round to nearest by adding half a step before the shift. The old code added
half a step and then rounded towards zero as well, so every negative result
came out one count high, and a shelf with a gain of 1.0 at 200 Hz settled
about 370 counts off zero in silence.
@tannewt
tannewt requested a review from FoamyGuy October 28, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant