-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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: Add a form of biquad filter that uses BlockInputs #9756
Conversation
BlockBiquad takes kind, f0 (center frequency) & Q (sharpness) block type arguments and calculates the actual filter coefficients every frame. This allows the filter characteristics f0 and Q to be changed dynamically from LFOs & arithmetic blocks. A new manual test demonstrates this on a host computer, playing a simple tone that is dynamically filtered.
161e109
to
1fef6b4
Compare
Looking great so far, but I have a few quick notes:
Since you're keeping backwards compatibility with the original |
I considered caching f0 & q values but decided against it until we had evidence these calculations were a problem. Naming suggestions are always appreciated, thanks. I could make the type/kind assignable and see what happens. I mentioned in the docs being unsure about the situation with the x[] and y[] coefficients when changing the F & Q numbers, but I'm even more dubious that those numbers would make any sense when changing the filter type, since that's like the hugest step change possible .. I'd leave it for a future addition if it DOES turn out to work or even sorta-work. |
i see you mention resetting x[] and y[] but .. we don't have access to them when setting the property, because they're associated with a note. You can have the same filter on multiple notes, and each note has its own x[] and y[]. |
@todbot ping in case of interest |
I took your suggestion to rename FilterType to FilterMode. I also added the notch filter from https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html. shelf & peaking don't seem to be useful until it's possible to have multiple filters (and/or their application is beyond my knowledge); they also pose a problem in that they have an additional "A" parameter related to overall gain. Presumably all BlockBiquads would have an "A" or "gain" property that would be ignored if it is not pertinent. And I still don't get what an all-pass filter is for. |
Shall I mark the original Biquad APIs deprecated? (only one of the 3 Synthesizer functions show, but all would be deprecated) diff --git a/shared-bindings/synthio/Biquad.c b/shared-bindings/synthio/Biquad.c
index 79a9fa4593..2323551a91 100644
--- a/shared-bindings/synthio/Biquad.c
+++ b/shared-bindings/synthio/Biquad.c
@@ -38,7 +38,11 @@ static const mp_arg_t biquad_properties[] = {
//| class Biquad:
//| def __init__(self, b0: float, b1: float, b2: float, a1: float, a2: float) -> None:
-//| """Construct a normalized biquad filter object.
+//| """Construct a normalized biquad filter object. **DEPRECATED**
+//|
+//| The Biquad class is deprecated and will be removed in a future release
+//| of CircuitPython (but not before CircuitPython 10); use `BlockBiquad`
+//| instead.
//|
//| This implements the "direct form 1" biquad filter, where each coefficient
//| has been pre-divided by a0.
diff --git a/shared-bindings/synthio/Synthesizer.c b/shared-bindings/synthio/Synthesizer.c
index 87ea44de69..da81d4f18c 100644
--- a/shared-bindings/synthio/Synthesizer.c
+++ b/shared-bindings/synthio/Synthesizer.c
@@ -288,7 +288,9 @@ MP_PROPERTY_GETTER(synthio_synthesizer_blocks_obj,
//|
//| def low_pass_filter(cls, frequency: float, q_factor: float = 0.7071067811865475) -> Biquad:
-//| """Construct a low-pass filter with the given parameters.
+//| """Construct a low-pass filter with the given parameters. **DEPRECATED**
+//|
+//| Instead, construct a `BlockBiquad` directly.
//|
//| ``frequency``, called f0 in the cookbook, is the corner frequency in Hz
//| of the filter. |
I'm planning to add support for multiple filters within
Definitely not a question for me to answer. Do any other moderators (ie: @tannewt or @dhalbert) have thoughts on this? The result of this would also affect this draft PR: #9754. (no point in added a feature while it is already being deprecated) Yet again, do you think there are performance benefits to using the original |
When you're working on allowing multiple filters, please see whether you can structure it so that synthio can share the code. e.g., if a structure more complicated than an list is needed / has to be traversed when filtering some samples, do it as a function both uses can call. Or, at least, make your best guess as to how that would need to work. |
@jepler Here's the PR for multiple filters within |
@dcooperdalrymple @todbot can either of you do a review of this PR on the technical side? I don't think you can mark it "approved" but your comments would be helpful. |
It would not hurt to do something smarter when frequency value is inappropriate. @kevinjwalters noted that with the current biquad implementation, using a frequency value above f_s/2 gives "white noise like" outputs; I think it'd probably be better to have silence (or maybe no filter for notch & lpf?) in this case. |
Tests so far are promising. On an RP2040, I've got 12 notes running at once, each with a import audiobusio
import board
import synthio
audio = audiobusio.I2SOut(bit_clock=board.GP0, word_select=board.GP1, data=board.GP2)
synth = synthio.Synthesizer(sample_rate=48000, channel_count=2)
audio.play(synth)
for i in range(12):
synth.press(synthio.Note(
frequency=synthio.midi_to_hz(48 + i * 3),
amplitude=0.1,
filter=synthio.BlockBiquad(
mode=synthio.FilterMode.LOW_PASS,
frequency=synthio.LFO(
scale=4000,
offset=4500,
rate=0.1 + i * 0.05,
),
Q=synthio.LFO(
scale=0.4,
offset=1.1,
rate=0.025 + i * 0.0125,
),
),
panning=i / 6 - 1.0
)) Notes:
Everything else looks and sounds clean. |
The docstrings incorrectly gave the name of the argument; the code only accepts the argument name "Q": ``` { MP_QSTR_Q, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL } }, ```
Synthesizer.lpf takes `Q` as the argument name, use the same convention here.
Thanks @relic-se particularly for noting the problem with I added some code that should avoid recalculations when neither W0 (derived from f0 and sample rate) nor Q have changed. Nothing smart is done if only one or the other factor changes. |
Your implementation is great. I think this update is mostly beneficial for static |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not have time to test on hardware but the code looks good. I had 3 minor comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes are good. Looks good to me. Thanks!
... for frequency & q_factor. Not tested on HW but it looks good in a manual host computer test. Here, a sine wave is played 4 times:
first, unfiltered. Then with 3 frequency-swept filters: low-, high- and band-pass. The band pass filter has a much higher Q-factor than the other filters.
This needs to be separately wired into the new audio filtering code