Skip to content

Add Biquad Filters to audiodelays.Echo and audiofreeverb.Freeverb - #11196

Open
relic-se wants to merge 4 commits into
adafruit:mainfrom
relic-se:audiodelays-echo-biquads
Open

Add Biquad Filters to audiodelays.Echo and audiofreeverb.Freeverb#11196
relic-se wants to merge 4 commits into
adafruit:mainfrom
relic-se:audiodelays-echo-biquads

Conversation

@relic-se

Copy link
Copy Markdown

This update expands the capabilities of audiodelays.Echo and audiofreeverb.Freeverb by adding synthio.Biquad filter support internally to their audio processing chain.

Echo

The audiodelays.Echo.filter property filters samples before they are stored in the echo buffer to be recalled later. This can produce a "tape-like" effect when repeated echos are processed through the filter on each iteration.

Freeverb

The audiofreeverb.Freeverb.pre_filter and audiofreeverb.Freeverb.post_filter properties allow the incoming sample to be filtered going into and out of the reverb algorithm. These new "Pre-EQ" and "Post-EQ" allow deeper control and shaping of the reverb effect while maintaining the integrity of the original sample.

Demonstration

# boot.py
import usb_audio
usb_audio.enable(sample_rate=8000, channel_count=1)
# code.py
from audiodelays import Echo
from audiofilters import Filter
from audiofreeverb import Freeverb
import synthio
import time
import ulab.numpy as np
from usb_audio import usb_microphone

FILTER = False

synth = synthio.Synthesizer(
    envelope=synthio.Envelope(
        attack_time=0.05,
        release_time=0.05,
        attack_level=0.7,
        sustain_level=0.7,
    ),
    
    sample_rate=8000,
    channel_count=1,
)

effect_echo = Echo(
    max_delay_ms=200,
    delay_ms=200,
    decay=0.7,
    filter=synthio.Biquad(synthio.FilterMode.LOW_PASS, synthio.LFO(offset=1000, scale=800, rate=0.3), 1.5) if FILTER else None,
    mix=0.5,
    freq_shift=True,
    
    sample_rate=synth.sample_rate,
    channel_count=synth.channel_count,
)

effect_reverb = Freeverb(
    pre_filter=synthio.Biquad(synthio.FilterMode.HIGH_PASS, 200) if FILTER else None,
    post_filter=synthio.Biquad(synthio.FilterMode.LOW_PASS, 1000) if FILTER else None,
    mix=0.5,
    
    sample_rate=synth.sample_rate,
    channel_count=synth.channel_count,
)

# Select effect type
effect = effect_echo

# Audio Chain
effect.play(synth)
usb_microphone.play(effect, loop=True)

# Play through scale
root_note = 60  # middle C
major_scale = [0, 2, 4, 5, 7, 9, 11, 12]
try:
    for note in major_scale:
        synth.press(root_note + note)
        time.sleep(0.1)
        synth.release(root_note + note)
        time.sleep(2)
    time.sleep(2)
except KeyboardInterrupt:
    pass
usb_microphone.stop()

Results

This above program was recorded using Audacity over USB Audio under 4 conditions: dry echo, filtered echo, dry reverb, and filtered reverb. filter-tests.zip

All tests were run by a Waveshare RP2040 Zero. In some tests, audible popping can be heard. This is likely due to buffer underflows caused by the slower processor and the computational requirements of these effects or the experimental nature of usb_audio in its current state.

Notes

  • Originally, I was just going to implement either pre or post filtering on audiofreeverb.Freeverb. After using the effect under each circumstance, I determined that it would be useful to have both options. "Pre-EQ" does a good job of "taming" the reverb by reducing the effect of sudden pops which can cause the reverb to act abruptly. "Post-EQ" is more artistic in the way that it shapes the reverb output.
  • I decided to share much of the filter object handling within a centralized location (audio-module/audiofilters/__init__.c). This has definitely helped simplify implementation and reduce flash usage, but there is likely room for improvement with performance. I'd appreciate direction on how best to share these methods.

@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.

A couple suggestions. Overall I'm excited for more synth improvements!

Comment on lines +16 to +32
} else if (MP_OBJ_TYPE_HAS_SLOT(mp_obj_get_type(filter_in), iter)) {
// convert object to tuple if it wasn't before
filter_in = MP_OBJ_TYPE_GET_SLOT(&mp_type_tuple, make_new)(
&mp_type_tuple, 1, 0, &filter_in);
mp_obj_tuple_get(filter_in, &n_items, &items);
for (size_t i = 0; i < n_items; i++) {
if (!mp_obj_is_type(items[i], &synthio_biquad_type_obj)) {
mp_raise_TypeError_varg(
MP_ERROR_TEXT("%q in %q must be of type %q, not %q"),
MP_QSTR_object,
MP_QSTR_filter,
MP_QSTR_Biquad,
mp_obj_get_type(items[i])->name);
}
}
*filter_objs = items;
} else {

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.

I'd just be stricter here. The docs say it's a tuple or single Biquad. Don't support any sequence to save yourself the allocation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Do you mean to get rid of:

// convert object to tuple if it wasn't before
filter_in = MP_OBJ_TYPE_GET_SLOT(&mp_type_tuple, make_new)(
    &mp_type_tuple, 1, 0, &filter_in);
mp_obj_tuple_get(filter_in, &n_items, &items);

And replace it with a strict tuple type check? I could see some users providing a list without thinking about it which is why this code is here.

}
}

int32_t audiofilters_process_filters(mp_obj_t *filter_objs, size_t filter_objs_len, biquad_filter_state *filter_states, uint8_t channel_count, uint8_t channel, int32_t word) {

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.

I wonder if it's worth having a "filter chain" struct that holds the objs, len and states. That may make it easier to manage and clearer what can be shared.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I totally thinking of this earlier. That'll definitely clear up those function calls.

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.

2 participants