Skip to content

audiodelays, audiofilters, audiofreeverb: buffer lengths and silence … - #11367

Open
peterbay wants to merge 1 commit into
adafruit:mainfrom
peterbay:audio-effects-buffer-fixes
Open

audiodelays, audiofilters, audiofreeverb: buffer lengths and silence …#11367
peterbay wants to merge 1 commit into
adafruit:mainfrom
peterbay:audio-effects-buffer-fixes

Conversation

@peterbay

Copy link
Copy Markdown

Code written by Claude Code, guided and corrected by @peterbay.

The problem

Eleven defects in the audio effect modules, most of them in how a delay line's length is worked out or how a buffer is filled with silence. Three of them fault the board outright.

The changes

  • Echo and MultiTapDelay applied their floor after their ceiling. The two limits were arms of one else if, so raising a short delay to the audio buffer size could put it back above the maximum, and the memset that follows took max - len as an unsigned length. Changed to apply the floor first and the ceiling always after it.

  • Chorus never clamped its delay to the buffer it allocated. The buffer is sized once from max_delay_ms, and nothing stopped a longer delay_ms being used against it; Echo and MultiTapDelay both limit this. Added the same clamp.

  • PitchShift accepted a window too small to hold a sample. The window is given in bytes but holds one int16_t per channel, so window=1 allocated a single byte that the playback loop then wrote a 16-bit sample into. Added mp_arg_validate_int_min.

  • The silence fill wrote the minimum sample value, not the midpoint. memset(word_buffer, 32768, ...) repeats a single byte, and 32768 truncates to 0, so the unsigned 16-bit path filled with full negative deflection where silence was intended. Replaced with a loop writing 0x8000, in PitchShift, GranularPitchShift and Distortion. The 8-bit branch beside it is correct, because 128 does fit in a byte.

  • Distortion's hard clip had an upper bound of 32768. That becomes -32768 on the cast to int16_t below it, so a positive overdrive came out as a full negative spike. Changed to 32767.

  • Freeverb cleared half of each delay line. The buffers hold uint16_t and are allocated as size * sizeof(uint16_t), but only size bytes were cleared, so the reverb started from whatever was on the heap.

  • Freeverb was not a stereo reverb. The channel offsets were declared inside the per-sample loop, so they were zero again at the top of every sample and the switch at the bottom of it had no effect; both channels went through the left bank of comb and allpass filters.

  • The biquad chain counted a size_t length with a uint8_t. In the reset, tick and process helpers in audiofilters/__init__.c and the per-block loop in Filter.c. Past 256 Biquad objects — 128 in the reset helper, which counts objs_len * channel_count — the counter wrapped and the loop did not terminate.

Testing

Seeed XIAO nRF52840 Sense with an Adafruit Audio BFF, running two builds of this branch that differ only by these changes. Effects were rendered into a WAV in RAM with audiofilewriter.AudioFileWriter over an io.BytesIO, so the output could be compared numerically rather than by ear.

before after
Echo(max_delay_ms=10, delay_ms=5, freq_shift=False) hard fault constructs
MultiTapDelay(max_delay_ms=10, delay_ms=5) hard fault constructs
Chorus(max_delay_ms=10, delay_ms=50), playing hard fault plays
PitchShift(window=1) accepted ValueError
Distortion, square wave driven into the clip min -32768, max 0, mean -14223 min -32767, max 32767, mean 113
Freeverb, stereo input with the channels at 3:1 energy L 381624, R 479880 L 8604, R 2124
GranularPitchShift, fill after the sample ends 0x0000 0x8000

The three faults were recorded through microcontroller.nvm, set immediately before the call under test and immediately after it, so the result does not depend on catching the USB serial before the board goes down.

Two of these are not covered by that table. Clearing the whole of Freeverb's delay lines makes no observable difference on this board, because the heap read back as zero either way; it is correct by inspection, since the allocation is size * sizeof(uint16_t) and the memset was size. The biquad counter needs more than 256 Biquad objects to show, which does not fit in this part's RAM.

audiodelays, audiofilters and audiofreeverb are off by default on this port, so the builds set CIRCUITPY_AUDIODELAYS, CIRCUITPY_AUDIOFILTERS and CIRCUITPY_AUDIOFREEVERB explicitly.

…fills

Echo and MultiTapDelay applied their length floor after their ceiling, so a
short delay could be raised above the maximum and the memset that follows
took "max - len" as an unsigned length. Chorus never clamped its delay to the
buffer it allocated. PitchShift accepted a window too small to hold one
sample.

The unsigned 16-bit silence fill used memset with 32768, which repeats a
single byte and so wrote zeros -- full negative deflection rather than the
midpoint -- in PitchShift, GranularPitchShift and Distortion. Distortion's
hard clip had an upper bound of 32768, which becomes -32768 on the cast to
int16_t below it.

Freeverb cleared only half of each delay line, counting uint16_t entries as
bytes, and declared its channel offsets inside the per-sample loop, so both
channels went through the left bank of filters. The biquad chain counted a
size_t length with a uint8_t in four places.
@dhalbert

Copy link
Copy Markdown
Collaborator

@todbot, @relic-se: interested in your review -- thanks. I've been having side conversations with @peterbay, who had an LLM do a lot of looking for potential bugs. This is the first batch of what was found.

@todbot

todbot commented Sep 13, 2026

Copy link
Copy Markdown

Looks reasonable at first glance but I’ve not tested. This does address an issue I believe I’ve experienced with Echo and Chorus.

@peterbay

Copy link
Copy Markdown
Author

Testing and diagnostic script.
audio_effects_buffer_fixes.py

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.

3 participants