Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synthio: switch to per-note biquad filtering #8048

Merged
merged 11 commits into from Jun 7, 2023

Conversation

jepler
Copy link
Member

@jepler jepler commented May 29, 2023

Not tested on hardware yet, but gives plausible results for the new manual host computer test "synthio/note/biquad.py"

My main reference has been https://www.w3.org/TR/audio-eq-cookbook/

each note can have just 0 or 1 biquad filters, there's not a filter topology.

the filter property can in theory be dynamically updated but I didn't test it; I think there's not actually a sound mathematical theory of how changing the a/b coefficients dynamically affects things, since the internal y state of the filter doesn't make sense when coefficients change. (w3 audio api notes that changing coefficients at runtime can "create unstable biquad filters")

It's also not currently possible to hook up the biquad filter inputs from blocks/LFOs. This is because of laziness, but also because computing the filter coefficients involves a sin() and cos() call, which are costly. If this design is otherwise adequate, but automation of the biquad filters from LFOs is needed, we can find out whether sin/cos are fast enough (or whether a fixed point sin/cos would be adequate & fast enough).

@gamblor21 @todbot

@jepler
Copy link
Member Author

jepler commented May 29, 2023

and I verified the filter coefficients against https://arachnoid.com/BiQuadDesigner/

this removes a marked DC offset and may cure the 'pops' problem.
Apply envelope & panning after biquad filtering.

This may fix the weird popping problem. It also reduces the number
of operations that are done "in stereo", so it could help performance.

It also fixes a previously unnoticed problem where a ring-modulated
waveform had 2x the amplitude of an un-modulated waveform.

The test differences look large but it's because some values got changed
in the LSB after the mathematical divisions were moved around.
@jepler jepler marked this pull request as ready for review June 5, 2023 18:14
@jepler
Copy link
Member Author

jepler commented Jun 5, 2023

This "survived" testing by @gamblor21 who used it to make some great percussion sounds by frequency-filtering a noise sample... so I dropped it into review status. There are still some potentially useful biquad filters to add, which should be entered as a feature request issue:

  • low & high shelf
  • notch
  • peak

@jepler jepler requested a review from tannewt June 5, 2023 18:19
@todbot
Copy link

todbot commented Jun 5, 2023

Hi! Sorry I've not been able to get to this until now. I am having a hard time using this. It seems to be geared to creating static filters that do not change. But filters in synths are usually modulated, a common sound being the low-pass filter sweep bwwwowwww sound. E.g. I would like to do something like the below, but I cannot access the function that goes from frequency & q_factor to biquad coeffs a1,a2,b0,b1,b2:

top_f = 1600
q = 0.5
myfilter = synth.low_pass_filter(frequency=top_f, q_factor=q)
note = synthio.Note(frequency= 440, filter=myfilter)
synth.press(note)
while True:
  myfilter.frequency  = (myfilter.frequency - 10) % f
  time.sleep(0.01)

@todbot
Copy link

todbot commented Jun 5, 2023

Two other things:

  • it appears one cannot set the filter coeffs after instantiation. e.g. this does not work:
def adjust_filter(afilter,w0,Q):  # f,q
    s,c = math.sin(w0), math.cos(w0)
    alpha = s / (2 * Q)
    a0 = 1 + alpha
    afilter.a1 = -2 * c / a0
    afilter.a2 = 1 - alpha / a0
    afilter.b0 = (1 - c) / 2 / a0
    afilter.b1 = 1 - c / a0 
    afilter.b2 = (1 - c) / 2 / a0
  • Doing lpf = synth.low_pass_filter(frequency=1600, q_factor=0.5) fails with "extra keyword arguments given" but lpf = synth.low_pass_fitler(1600,0.5) works

@todbot
Copy link

todbot commented Jun 6, 2023

Okay this works as expected*:

# synthio_filter_test.py --
# 5 Jun 2023 - @todbot / Tod Kurt
import time, random, board, analogio, audiopwmio, synthio
knob1 = analogio.AnalogIn(board.GP26)
knob2 = analogio.AnalogIn(board.GP27)
audio = audiopwmio.PWMAudioOut(board.GP10)
synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)

note1 = synthio.Note(frequency=110)
synth.press(note1)

while True:
    lpf_f = 10 + knob1.value / 32
    lpf_q = knob2.value / 20000
    note1.filter = synth.low_pass_filter(lpf_f, lpf_q)
    print(lpf_f, lpf_q)
    time.sleep(0.1)
  • with the exception there are some very nasty instabilities at high resonance values (Q>2.35) and, oddly, with lower Q = ~1.3 and F = ~130Hz.

But otherwise from my hour or so of testing, I say "ship!" This is a really cool feature that's immediately useful.

Copy link
Member

@gamblor21 gamblor21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed tested manually. Code looks good to me. Really useful update! Thanks.

@tannewt
Copy link
Member

tannewt commented Jun 7, 2023

@jepler Any comments wrt todbot's feedback before I do a final review?

@todbot
Copy link

todbot commented Jun 7, 2023

I don't think my comments should hold this up. The code is behaving as intended, as this is a common issue with filters at high resonance/feedback values. I'm sure we can devise band-aids as needed later.

@jepler
Copy link
Member Author

jepler commented Jun 7, 2023

Let's slip it in @tannewt !

@jepler jepler merged commit c408193 into adafruit:main Jun 7, 2023
300 checks passed
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.

None yet

4 participants