Skip to content

audio Flanger FX implementation - #11175

Open
FoamyGuy wants to merge 5 commits into
adafruit:mainfrom
FoamyGuy:audio_flanger
Open

audio Flanger FX implementation#11175
FoamyGuy wants to merge 5 commits into
adafruit:mainfrom
FoamyGuy:audio_flanger

Conversation

@FoamyGuy

@FoamyGuy FoamyGuy commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Adds a new effect class Flanger to audiodelays implementing flanging effect

Testing was performed on Metro RP2350

tannewt
tannewt previously approved these changes Aug 4, 2026

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks great to me! Please always consider a new module for new effects. That way boards won't need the existing ones disabled for the new one to fit.

@relic-se relic-se left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looking good so far. I'd like to see a getter to obtain the current state of the internal triangle lfo (from 0.0 to 1.0). Would also be cool to be able to change the waveform, but then we're getting into synthio.BlockInput territory in which some of this functionality could already be achieved with audiodelays.Echo.

Comment thread shared-module/audiodelays/Flanger.c Outdated
Comment thread shared-module/audiodelays/Flanger.c Outdated
Comment thread shared-bindings/audiodelays/Flanger.c Outdated
@FoamyGuy

FoamyGuy commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@relic-se I added lfo_value property. Though it's worth noting outside callers will be get a "stair-stepped" view of the value due to it being updated internally for each byte within the sample in the inner loop around here: https://github.com/FoamyGuy/circuitpython/blob/bbe4ec62ca99524e7ead5029016d9224315e0128/shared-module/audiodelays/Flanger.c#L311-L329. I think outside callers would only ever get the value from the end of a full sample.

@relic-se

relic-se commented Aug 5, 2026

Copy link
Copy Markdown

@relic-se I added lfo_value property. Though it's worth noting outside callers will be get a "stair-stepped" view of the value due to it being updated internally for each byte within the sample in the inner loop around here: https://github.com/FoamyGuy/circuitpython/blob/bbe4ec62ca99524e7ead5029016d9224315e0128/shared-module/audiodelays/Flanger.c#L311-L329. I think outside callers would only ever get the value from the end of a full sample.

That works for me. Tbh, this was a request of personal interest. For my purpose, I'll be using an LED to indicate the current state of the flanger. This update should be completely sufficient. synthio.LFO.value has a similar use-case.

@relic-se

relic-se commented Aug 5, 2026

Copy link
Copy Markdown

To add to my prior comment on the implementation of a "Flange" effect with the existing audiodelays.Echo, @todbot has a good demonstration in his synthio tutorial on this procedure: https://todbot.github.io/CircuitPython_Synthio_Tutorial/README-6-Audio-Effects.html#make-a-flange-effect-with-echo.

I still see the benefit of having the full flanger functionality right out of the box with this new class.

@FoamyGuy

FoamyGuy commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Using Echo and LFO from python code can create a similar effect but it comes with a similar "stairstepped" behavior as the getter property discussed above I think. The synthio LFO doesn't get updated as frequently as the internal one inside of Flanger that ticks for every byte of the sample. Using smaller buffer_size with Echo can mitigate it some by making the samples smaller allowing for more frequent synthio LFO ticks. I had to drop buffer_size down way below the default 512 before I could hear the wobble effect on a basic synthio scale test.

@todbot

todbot commented Aug 5, 2026

Copy link
Copy Markdown

I'd need to listen to this to check, but isn't this redundant?
As @relic-se mentions, a flanger effect is just a modulated delay-time audio copy, achievable with an Echo & LFO.

Edit: ahh yes, I see your comment @FoamyGuy. You're addressing the fact that LFO updates happen per buffer but your Flanger can modulate per sample. Okay that's valid. But that makes me start to wonder if we should develop a "fast modulator" vs "slow modulator" distinction. Some other audio engines do this for this very reason.

@FoamyGuy

FoamyGuy commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

flanger_fx.wav.zip

Here is a recording that contains:

  1. clean synthio scale with no effect
  2. same scale with this core implemented Flanger
  3. same scale with Echo/LFO from python code with buffer_size=64 on the Echo.

It does sound a bit different to my ear, but maybe not enough difference to justify the new implementation?

I'm not sure what are the other consequences of using smaller buffer_size is either though. I've typically been keeping around 512-1024 for most of my experiments, but to hear the wobble from Echo/LFO I had to drop it down.

@relic-se

relic-se commented Aug 5, 2026

Copy link
Copy Markdown

synthio.BlockInput updates every 256 frames (defined by SYNTHIO_MAX_DUR). At 44.1kHz, that's once every 5.8 ms. That may be enough to cause the perceived "stair-stepping".

@FoamyGuy Out of curiosity, are you using freq_shift=True? That might perform better for this task. The standard echo clears any remaining buffer when delay_ms changes whereas freq_shift has a fixed delay length and changes the rate at which it is played back (see the following snippet of code). There's a chance that the "stair-stepping" might be caused by buffer clears since the standard echo doesn't handle rapidly changing delay length well.

// Clear the now unused part of the buffer or some weird artifacts appear
for (uint32_t i = 0; i < self->base.channel_count; i++) {
memset(self->echo_buffer + (i * max_echo_buffer_len) + self->echo_buffer_len, 0, max_echo_buffer_len - self->echo_buffer_len);
}

@relic-se

relic-se commented Aug 5, 2026

Copy link
Copy Markdown

Edit: ahh yes, I see your comment @FoamyGuy. You're addressing the fact that LFO updates happen per buffer but your Flanger can modulate per sample. Okay that's valid. But that makes me start to wonder if we should develop a "fast modulator" vs "slow modulator" distinction. Some other audio engines do this for this very reason.

@todbot I really like the idea of a "fast mode" for BlockInput sources. Here's a few issues that are related to this:

@relic-se

relic-se commented Aug 6, 2026

Copy link
Copy Markdown

I just finished up a round of subjective testing and here are my results:

  • I do believe the stair stepping is mostly caused by freq_shift=False, but even so, the slower LFO still isn't ideal. I actually ran into a runtime error within the core when playing around with this. Not sure what exactly is the cause, but I'll file an Issue if I can pin it down.
  • It is possible to achieve a close approximation using audiodelays.Echo, but audiodelays.Flanger does sound better and is easier to implement. The following code demonstrates this:
effect = Echo(
    freq_shift=True,
    max_delay_ms=10,  # max_delay_ms
    delay_ms=synthio.Math(
        synthio.MathOperation.CONSTRAINED_LERP,
        1,  # min_delay_ms
        10,  # max_delay_ms
        lfo := synthio.LFO(
            rate=0.5,  # rate
            scale=0.0,  # a combination of scale and offset is used to determine depth
            offset=0.0,
        )
    ),
    mix=0.5,
    # audiosample properties...
)
def set_depth(value: float) -> None:
    lfo.scale = lfo.offset = min(max(value, 0), 1) / 2
  • I'm not exactly sure how the invert property works mathematically, but I like it sonically and it doesn't appear to be possible to mimic within audiodelays.Echo.
  • The feedback system works differently than I expected. I'd think that feedback=1 would cause some self-oscillation, but it doesn't. I'm totally good with this.

TL;DR: I like the new effect and I think it should be added to the core. 😸

The demonstration programs can be found here, but they require specific hardware to operate:

@tannewt

tannewt commented Aug 6, 2026

Copy link
Copy Markdown
Member

@todbot and @relic-se are you ok with this?

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.

4 participants