From 17810ad057876dcf54c2c36f762f79ff0c91538d Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 09:48:34 -0500 Subject: [PATCH 01/20] Only change loudness on zero crossing for both `audiomixer.Mixer` and `synthio.Synthesizer` --- shared-module/audiomixer/Mixer.c | 97 ++++++++++++++++++++------- shared-module/audiomixer/MixerVoice.h | 2 + shared-module/synthio/__init__.c | 28 ++++++-- shared-module/synthio/__init__.h | 1 + 4 files changed, 99 insertions(+), 29 deletions(-) diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index f3111b8dc6f..6ce291d26cb 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -173,6 +173,18 @@ static inline uint32_t copy16msb(uint32_t val) { #endif } +static inline void assignmul(uint32_t word, uint32_t *last_word, int32_t *voice_lomul, int32_t *voice_himul, int32_t lomul, int32_t himul) { + if (MP_LIKELY(*voice_lomul == lomul) && MP_LIKELY(*voice_himul == himul)) { + return; + } + if (word == 0 || (*last_word != 0 && ((*last_word) & 0x80008000) != (word & 0x80008000))) { + *voice_lomul = lomul; + *voice_himul = himul; + } else { + *last_word = word; + } +} + #define ALMOST_ONE (MICROPY_FLOAT_CONST(32767.) / 32768) static void mix_down_one_voice(audiomixer_mixer_obj_t *self, @@ -245,35 +257,49 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, hi_level = (right_panning_scaled * hi_level) >> 15; } + int32_t voice_lo_level = voice->lo_level; + int32_t voice_hi_level = voice->hi_level; + uint32_t last_word = 0; + // First active voice gets copied over verbatim. if (!voices_active) { if (MP_LIKELY(self->base.bits_per_sample == 16)) { if (MP_LIKELY(self->base.samples_signed)) { if (MP_LIKELY(self->base.channel_count == sample->channel_count)) { for (uint32_t i = 0; i < n; i++) { - uint32_t v = src[i]; - word_buffer[i] = mult16signed(v, lo_level, hi_level); + uint32_t word = src[i]; + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = mult16signed(word, voice_lo_level, voice_hi_level); } } else { for (uint32_t i = 0; i < n; i += 2) { - uint32_t v = src[i >> 1]; - word_buffer[i] = mult16signed(copy16lsb(v), lo_level, hi_level); - word_buffer[i + 1] = mult16signed(copy16msb(v), lo_level, hi_level); + uint32_t word = src[i >> 1]; + uint32_t word_lsb = copy16lsb(word); + assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = mult16signed(word_lsb, voice_lo_level, voice_hi_level); + word = copy16msb(word); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i + 1] = mult16signed(word, voice_lo_level, voice_hi_level); } } } else { if (MP_LIKELY(self->base.channel_count == sample->channel_count)) { for (uint32_t i = 0; i < n; i++) { - uint32_t v = src[i]; - v = tosigned16(v); - word_buffer[i] = mult16signed(v, lo_level, hi_level); + uint32_t word = src[i]; + word = tosigned16(word); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = mult16signed(word, voice_lo_level, voice_hi_level); } } else { for (uint32_t i = 0; i + 1 < n; i += 2) { - uint32_t v = src[i >> 1]; - v = tosigned16(v); - word_buffer[i] = mult16signed(copy16lsb(v), lo_level, hi_level); - word_buffer[i + 1] = mult16signed(copy16msb(v), lo_level, hi_level); + uint32_t word = src[i >> 1]; + word = tosigned16(word); + uint32_t word_lsb = copy16lsb(word); + assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = mult16signed(word_lsb, voice_lo_level, voice_hi_level); + word = copy16msb(word); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i + 1] = mult16signed(word, voice_lo_level, voice_hi_level); } } } @@ -286,7 +312,8 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(!self->base.samples_signed)) { word = tosigned16(word); } - word = mult16signed(word, lo_level, hi_level); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word = mult16signed(word, voice_lo_level, voice_hi_level); hword_buffer[i] = pack8(word); } } else { @@ -295,8 +322,12 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(!self->base.samples_signed)) { word = tosigned16(word); } - hword_buffer[i] = pack8(mult16signed(copy16lsb(word), lo_level, hi_level)); - hword_buffer[i + 1] = pack8(mult16signed(copy16msb(word), lo_level, hi_level)); + uint32_t word_lsb = copy16lsb(word); + assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + hword_buffer[i] = pack8(mult16signed(word_lsb, voice_lo_level, voice_hi_level)); + word = copy16msb(word); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + hword_buffer[i + 1] = pack8(mult16signed(word, voice_lo_level, voice_hi_level)); } } } @@ -306,13 +337,18 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(self->base.channel_count == sample->channel_count)) { for (uint32_t i = 0; i < n; i++) { uint32_t word = src[i]; - word_buffer[i] = add16signed(mult16signed(word, lo_level, hi_level), word_buffer[i]); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i]); } } else { for (uint32_t i = 0; i + 1 < n; i += 2) { uint32_t word = src[i >> 1]; - word_buffer[i] = add16signed(mult16signed(copy16lsb(word), lo_level, hi_level), word_buffer[i]); - word_buffer[i + 1] = add16signed(mult16signed(copy16msb(word), lo_level, hi_level), word_buffer[i + 1]); + uint32_t word_lsb = copy16lsb(word); + assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = add16signed(mult16signed(word_lsb, voice_lo_level, voice_hi_level), word_buffer[i]); + word = copy16msb(word); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i + 1] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i + 1]); } } } else { @@ -320,14 +356,19 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, for (uint32_t i = 0; i < n; i++) { uint32_t word = src[i]; word = tosigned16(word); - word_buffer[i] = add16signed(mult16signed(word, lo_level, hi_level), word_buffer[i]); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i]); } } else { for (uint32_t i = 0; i + 1 < n; i += 2) { uint32_t word = src[i >> 1]; word = tosigned16(word); - word_buffer[i] = add16signed(mult16signed(copy16lsb(word), lo_level, hi_level), word_buffer[i]); - word_buffer[i + 1] = add16signed(mult16signed(copy16msb(word), lo_level, hi_level), word_buffer[i + 1]); + uint32_t word_lsb = copy16lsb(word); + assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i] = add16signed(mult16signed(word_lsb, voice_lo_level, voice_hi_level), word_buffer[i]); + word = copy16msb(word); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word_buffer[i + 1] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i + 1]); } } } @@ -340,7 +381,8 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(!self->base.samples_signed)) { word = tosigned16(word); } - word = mult16signed(word, lo_level, hi_level); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + word = mult16signed(word, voice_lo_level, voice_hi_level); word = add16signed(word, unpack8(hword_buffer[i])); hword_buffer[i] = pack8(word); } @@ -350,8 +392,12 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(!self->base.samples_signed)) { word = tosigned16(word); } - hword_buffer[i] = pack8(add16signed(mult16signed(copy16lsb(word), lo_level, hi_level), unpack8(hword_buffer[i]))); - hword_buffer[i + 1] = pack8(add16signed(mult16signed(copy16msb(word), lo_level, hi_level), unpack8(hword_buffer[i + 1]))); + uint32_t word_lsb = copy16lsb(word); + assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + hword_buffer[i] = pack8(add16signed(mult16signed(word_lsb, voice_lo_level, voice_hi_level), unpack8(hword_buffer[i]))); + word = copy16msb(word); + assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); + hword_buffer[i + 1] = pack8(add16signed(mult16signed(word, voice_lo_level, voice_hi_level), unpack8(hword_buffer[i + 1]))); } } } @@ -365,6 +411,9 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, voice->remaining_buffer += n >> 1; voice->buffer_length -= n >> 1; } + + voice->lo_level = voice_lo_level; + voice->hi_level = voice_hi_level; } if (length && !voices_active) { diff --git a/shared-module/audiomixer/MixerVoice.h b/shared-module/audiomixer/MixerVoice.h index 75e7d47faa3..d683e9cb091 100644 --- a/shared-module/audiomixer/MixerVoice.h +++ b/shared-module/audiomixer/MixerVoice.h @@ -28,4 +28,6 @@ typedef struct { uint16_t level; int16_t panning; #endif + int32_t lo_level; + int32_t hi_level; } audiomixer_mixervoice_obj_t; diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index db0c449bfab..a3a9103d578 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -289,15 +289,33 @@ static mp_obj_t synthio_synth_get_note_filter(mp_obj_t note_obj) { return mp_const_none; } -static void sum_with_loudness(int32_t *out_buffer32, int32_t *tmp_buffer32, int16_t loudness[2], size_t dur, int synth_chan) { +static inline void assign_loudness(int32_t word, int32_t *last_word, int16_t note_loudness[2], int16_t loudness[2]) { + if (MP_LIKELY(note_loudness[0] == loudness[0]) && MP_LIKELY(note_loudness[1] == loudness[1])) { + return; + } + if (word == 0 || (*last_word != 0 && ((*last_word > 0) == (word < 0) || (*last_word < 0) == (word > 0)))) { + note_loudness[0] = loudness[0]; + note_loudness[1] = loudness[1]; + } else { + *last_word = word; + } +} + +static void sum_with_loudness(int32_t *out_buffer32, int32_t *tmp_buffer32, int16_t note_loudness[2], int16_t loudness[2], size_t dur, int synth_chan) { + int32_t word, last_word = 0; if (synth_chan == 1) { for (size_t i = 0; i < dur; i++) { - *out_buffer32++ += synthio_sat16((*tmp_buffer32++ *loudness[0]), 16); + word = *tmp_buffer32++; + assign_loudness(word, &last_word, note_loudness, loudness); + *out_buffer32++ += synthio_sat16((word * note_loudness[0]), 16); } } else { for (size_t i = 0; i < dur; i++) { - *out_buffer32++ += synthio_sat16((*tmp_buffer32 * loudness[0]), 16); - *out_buffer32++ += synthio_sat16((*tmp_buffer32++ *loudness[1]), 16); + word = *tmp_buffer32; + assign_loudness(word, &last_word, note_loudness, loudness); + *out_buffer32++ += synthio_sat16((word * note_loudness[0]), 16); + *out_buffer32++ += synthio_sat16((word * note_loudness[1]), 16); + tmp_buffer32++; } } } @@ -351,7 +369,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t } // adjust loudness by envelope - sum_with_loudness(out_buffer32, tmp_buffer32, loudness, dur, synth->base.channel_count); + sum_with_loudness(out_buffer32, tmp_buffer32, synth->loudness[chan], loudness, dur, synth->base.channel_count); } int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index]; diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 5162b49fd55..4154553fb70 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -55,6 +55,7 @@ typedef struct synthio_synth { uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; uint32_t ring_accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; synthio_envelope_state_t envelope_state[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; + int16_t loudness[CIRCUITPY_SYNTHIO_MAX_CHANNELS][2]; } synthio_synth_t; typedef struct { From 7e65486d5ea14fb3b7ad28df1d7850b15c6a939f Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 10:17:47 -0500 Subject: [PATCH 02/20] Force volume updates within `SYNTHIO_MAX_DUR` --- shared-module/audiomixer/Mixer.c | 4 ++-- shared-module/synthio/__init__.c | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index 6ce291d26cb..1b7f49d534c 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -412,8 +412,8 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, voice->buffer_length -= n >> 1; } - voice->lo_level = voice_lo_level; - voice->hi_level = voice_hi_level; + voice->lo_level = lo_level; + voice->hi_level = hi_level; } if (length && !voices_active) { diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index a3a9103d578..7e60d8820d2 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -318,6 +318,8 @@ static void sum_with_loudness(int32_t *out_buffer32, int32_t *tmp_buffer32, int1 tmp_buffer32++; } } + note_loudness[0] = loudness[0]; + note_loudness[1] = loudness[1]; } void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) { From 8378f65439f586871ec8782feb00915301fac34f Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 15:06:51 -0500 Subject: [PATCH 03/20] Remove inline from zero cross loudness functions --- shared-module/audiomixer/Mixer.c | 2 +- shared-module/synthio/__init__.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index 1b7f49d534c..72e7ece6574 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -173,7 +173,7 @@ static inline uint32_t copy16msb(uint32_t val) { #endif } -static inline void assignmul(uint32_t word, uint32_t *last_word, int32_t *voice_lomul, int32_t *voice_himul, int32_t lomul, int32_t himul) { +static void assignmul(uint32_t word, uint32_t *last_word, int32_t *voice_lomul, int32_t *voice_himul, int32_t lomul, int32_t himul) { if (MP_LIKELY(*voice_lomul == lomul) && MP_LIKELY(*voice_himul == himul)) { return; } diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 7e60d8820d2..01e90e847b4 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -289,7 +289,7 @@ static mp_obj_t synthio_synth_get_note_filter(mp_obj_t note_obj) { return mp_const_none; } -static inline void assign_loudness(int32_t word, int32_t *last_word, int16_t note_loudness[2], int16_t loudness[2]) { +static void assign_loudness(int32_t word, int32_t *last_word, int16_t note_loudness[2], int16_t loudness[2]) { if (MP_LIKELY(note_loudness[0] == loudness[0]) && MP_LIKELY(note_loudness[1] == loudness[1])) { return; } From 04b790ac6465e79299c00443144130034c8d8692 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 15:13:55 -0500 Subject: [PATCH 04/20] Increase clarity of active and pending loudness --- shared-module/audiomixer/Mixer.c | 96 +++++++++++++-------------- shared-module/audiomixer/MixerVoice.h | 4 +- shared-module/synthio/__init__.c | 26 ++++---- shared-module/synthio/__init__.h | 2 +- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index 72e7ece6574..ed152bf7142 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -173,13 +173,13 @@ static inline uint32_t copy16msb(uint32_t val) { #endif } -static void assignmul(uint32_t word, uint32_t *last_word, int32_t *voice_lomul, int32_t *voice_himul, int32_t lomul, int32_t himul) { - if (MP_LIKELY(*voice_lomul == lomul) && MP_LIKELY(*voice_himul == himul)) { +static void assignmul(uint32_t word, uint32_t *last_word, int32_t *active_lomul, int32_t *active_himul, int32_t pending_lomul, int32_t pending_himul) { + if (MP_LIKELY(*active_lomul == pending_lomul) && MP_LIKELY(*active_himul == pending_himul)) { return; } if (word == 0 || (*last_word != 0 && ((*last_word) & 0x80008000) != (word & 0x80008000))) { - *voice_lomul = lomul; - *voice_himul = himul; + *active_lomul = pending_lomul; + *active_himul = pending_himul; } else { *last_word = word; } @@ -250,15 +250,15 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, } } - int32_t lo_level = level; - int32_t hi_level = level; + int32_t pending_lo_level = level; + int32_t pending_hi_level = level; if (MP_LIKELY(self->base.channel_count == 2)) { - lo_level = (left_panning_scaled * lo_level) >> 15; - hi_level = (right_panning_scaled * hi_level) >> 15; + pending_lo_level = (left_panning_scaled * pending_lo_level) >> 15; + pending_hi_level = (right_panning_scaled * pending_hi_level) >> 15; } - int32_t voice_lo_level = voice->lo_level; - int32_t voice_hi_level = voice->hi_level; + int32_t active_lo_level = voice->active_lo_level; + int32_t active_hi_level = voice->active_hi_level; uint32_t last_word = 0; // First active voice gets copied over verbatim. @@ -268,18 +268,18 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(self->base.channel_count == sample->channel_count)) { for (uint32_t i = 0; i < n; i++) { uint32_t word = src[i]; - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = mult16signed(word, voice_lo_level, voice_hi_level); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = mult16signed(word, active_lo_level, active_hi_level); } } else { for (uint32_t i = 0; i < n; i += 2) { uint32_t word = src[i >> 1]; uint32_t word_lsb = copy16lsb(word); - assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = mult16signed(word_lsb, voice_lo_level, voice_hi_level); + assignmul(word_lsb, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = mult16signed(word_lsb, active_lo_level, active_hi_level); word = copy16msb(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i + 1] = mult16signed(word, voice_lo_level, voice_hi_level); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i + 1] = mult16signed(word, active_lo_level, active_hi_level); } } } else { @@ -287,19 +287,19 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, for (uint32_t i = 0; i < n; i++) { uint32_t word = src[i]; word = tosigned16(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = mult16signed(word, voice_lo_level, voice_hi_level); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = mult16signed(word, active_lo_level, active_hi_level); } } else { for (uint32_t i = 0; i + 1 < n; i += 2) { uint32_t word = src[i >> 1]; word = tosigned16(word); uint32_t word_lsb = copy16lsb(word); - assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = mult16signed(word_lsb, voice_lo_level, voice_hi_level); + assignmul(word_lsb, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = mult16signed(word_lsb, active_lo_level, active_hi_level); word = copy16msb(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i + 1] = mult16signed(word, voice_lo_level, voice_hi_level); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i + 1] = mult16signed(word, active_lo_level, active_hi_level); } } } @@ -312,8 +312,8 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(!self->base.samples_signed)) { word = tosigned16(word); } - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word = mult16signed(word, voice_lo_level, voice_hi_level); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word = mult16signed(word, active_lo_level, active_hi_level); hword_buffer[i] = pack8(word); } } else { @@ -323,11 +323,11 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, word = tosigned16(word); } uint32_t word_lsb = copy16lsb(word); - assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - hword_buffer[i] = pack8(mult16signed(word_lsb, voice_lo_level, voice_hi_level)); + assignmul(word_lsb, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + hword_buffer[i] = pack8(mult16signed(word_lsb, active_lo_level, active_hi_level)); word = copy16msb(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - hword_buffer[i + 1] = pack8(mult16signed(word, voice_lo_level, voice_hi_level)); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + hword_buffer[i + 1] = pack8(mult16signed(word, active_lo_level, active_hi_level)); } } } @@ -337,18 +337,18 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(self->base.channel_count == sample->channel_count)) { for (uint32_t i = 0; i < n; i++) { uint32_t word = src[i]; - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i]); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = add16signed(mult16signed(word, active_lo_level, active_hi_level), word_buffer[i]); } } else { for (uint32_t i = 0; i + 1 < n; i += 2) { uint32_t word = src[i >> 1]; uint32_t word_lsb = copy16lsb(word); - assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = add16signed(mult16signed(word_lsb, voice_lo_level, voice_hi_level), word_buffer[i]); + assignmul(word_lsb, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = add16signed(mult16signed(word_lsb, active_lo_level, active_hi_level), word_buffer[i]); word = copy16msb(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i + 1] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i + 1]); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i + 1] = add16signed(mult16signed(word, active_lo_level, active_hi_level), word_buffer[i + 1]); } } } else { @@ -356,19 +356,19 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, for (uint32_t i = 0; i < n; i++) { uint32_t word = src[i]; word = tosigned16(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i]); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = add16signed(mult16signed(word, active_lo_level, active_hi_level), word_buffer[i]); } } else { for (uint32_t i = 0; i + 1 < n; i += 2) { uint32_t word = src[i >> 1]; word = tosigned16(word); uint32_t word_lsb = copy16lsb(word); - assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i] = add16signed(mult16signed(word_lsb, voice_lo_level, voice_hi_level), word_buffer[i]); + assignmul(word_lsb, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i] = add16signed(mult16signed(word_lsb, active_lo_level, active_hi_level), word_buffer[i]); word = copy16msb(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word_buffer[i + 1] = add16signed(mult16signed(word, voice_lo_level, voice_hi_level), word_buffer[i + 1]); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word_buffer[i + 1] = add16signed(mult16signed(word, active_lo_level, active_hi_level), word_buffer[i + 1]); } } } @@ -381,8 +381,8 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, if (MP_LIKELY(!self->base.samples_signed)) { word = tosigned16(word); } - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - word = mult16signed(word, voice_lo_level, voice_hi_level); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + word = mult16signed(word, active_lo_level, active_hi_level); word = add16signed(word, unpack8(hword_buffer[i])); hword_buffer[i] = pack8(word); } @@ -393,11 +393,11 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, word = tosigned16(word); } uint32_t word_lsb = copy16lsb(word); - assignmul(word_lsb, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - hword_buffer[i] = pack8(add16signed(mult16signed(word_lsb, voice_lo_level, voice_hi_level), unpack8(hword_buffer[i]))); + assignmul(word_lsb, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + hword_buffer[i] = pack8(add16signed(mult16signed(word_lsb, active_lo_level, active_hi_level), unpack8(hword_buffer[i]))); word = copy16msb(word); - assignmul(word, &last_word, &voice_lo_level, &voice_hi_level, lo_level, hi_level); - hword_buffer[i + 1] = pack8(add16signed(mult16signed(word, voice_lo_level, voice_hi_level), unpack8(hword_buffer[i + 1]))); + assignmul(word, &last_word, &active_lo_level, &active_hi_level, pending_lo_level, pending_hi_level); + hword_buffer[i + 1] = pack8(add16signed(mult16signed(word, active_lo_level, active_hi_level), unpack8(hword_buffer[i + 1]))); } } } @@ -412,8 +412,8 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, voice->buffer_length -= n >> 1; } - voice->lo_level = lo_level; - voice->hi_level = hi_level; + voice->active_lo_level = pending_lo_level; + voice->active_hi_level = pending_hi_level; } if (length && !voices_active) { diff --git a/shared-module/audiomixer/MixerVoice.h b/shared-module/audiomixer/MixerVoice.h index d683e9cb091..db5cac1ca27 100644 --- a/shared-module/audiomixer/MixerVoice.h +++ b/shared-module/audiomixer/MixerVoice.h @@ -28,6 +28,6 @@ typedef struct { uint16_t level; int16_t panning; #endif - int32_t lo_level; - int32_t hi_level; + int32_t active_lo_level; + int32_t active_hi_level; } audiomixer_mixervoice_obj_t; diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 01e90e847b4..6cf4f31a011 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -289,37 +289,37 @@ static mp_obj_t synthio_synth_get_note_filter(mp_obj_t note_obj) { return mp_const_none; } -static void assign_loudness(int32_t word, int32_t *last_word, int16_t note_loudness[2], int16_t loudness[2]) { - if (MP_LIKELY(note_loudness[0] == loudness[0]) && MP_LIKELY(note_loudness[1] == loudness[1])) { +static void assign_loudness(int32_t word, int32_t *last_word, int16_t active_loudness[2], int16_t pending_loudness[2]) { + if (MP_LIKELY(active_loudness[0] == pending_loudness[0]) && MP_LIKELY(active_loudness[1] == pending_loudness[1])) { return; } if (word == 0 || (*last_word != 0 && ((*last_word > 0) == (word < 0) || (*last_word < 0) == (word > 0)))) { - note_loudness[0] = loudness[0]; - note_loudness[1] = loudness[1]; + active_loudness[0] = pending_loudness[0]; + active_loudness[1] = pending_loudness[1]; } else { *last_word = word; } } -static void sum_with_loudness(int32_t *out_buffer32, int32_t *tmp_buffer32, int16_t note_loudness[2], int16_t loudness[2], size_t dur, int synth_chan) { +static void sum_with_loudness(int32_t *out_buffer32, int32_t *tmp_buffer32, int16_t active_loudness[2], int16_t pending_loudness[2], size_t dur, int synth_chan) { int32_t word, last_word = 0; if (synth_chan == 1) { for (size_t i = 0; i < dur; i++) { word = *tmp_buffer32++; - assign_loudness(word, &last_word, note_loudness, loudness); - *out_buffer32++ += synthio_sat16((word * note_loudness[0]), 16); + assign_loudness(word, &last_word, active_loudness, pending_loudness); + *out_buffer32++ += synthio_sat16((word * active_loudness[0]), 16); } } else { for (size_t i = 0; i < dur; i++) { word = *tmp_buffer32; - assign_loudness(word, &last_word, note_loudness, loudness); - *out_buffer32++ += synthio_sat16((word * note_loudness[0]), 16); - *out_buffer32++ += synthio_sat16((word * note_loudness[1]), 16); + assign_loudness(word, &last_word, active_loudness, pending_loudness); + *out_buffer32++ += synthio_sat16((word * active_loudness[0]), 16); + *out_buffer32++ += synthio_sat16((word * active_loudness[1]), 16); tmp_buffer32++; } } - note_loudness[0] = loudness[0]; - note_loudness[1] = loudness[1]; + active_loudness[0] = pending_loudness[0]; + active_loudness[1] = pending_loudness[1]; } void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) { @@ -371,7 +371,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t } // adjust loudness by envelope - sum_with_loudness(out_buffer32, tmp_buffer32, synth->loudness[chan], loudness, dur, synth->base.channel_count); + sum_with_loudness(out_buffer32, tmp_buffer32, synth->active_loudness[chan], loudness, dur, synth->base.channel_count); } int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index]; diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 4154553fb70..0335831f80f 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -55,7 +55,7 @@ typedef struct synthio_synth { uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; uint32_t ring_accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; synthio_envelope_state_t envelope_state[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; - int16_t loudness[CIRCUITPY_SYNTHIO_MAX_CHANNELS][2]; + int16_t active_loudness[CIRCUITPY_SYNTHIO_MAX_CHANNELS][2]; } synthio_synth_t; typedef struct { From 8ca9f0925e0c86c4ccda37433263ce8cf9e51b7f Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 15:31:32 -0500 Subject: [PATCH 05/20] Add comments to zero crossing detection --- shared-module/audiomixer/Mixer.c | 18 ++++++++++++++++++ shared-module/synthio/__init__.c | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index ed152bf7142..88c842fcebc 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -173,14 +173,29 @@ static inline uint32_t copy16msb(uint32_t val) { #endif } +// Rather than immediately changing the loudness of audio playback, we keep a separate buffer of +// the "active" loudness and wait until we meet the conditions of a "zero crossing". A zero crossing +// occurs when either the current value is 0 or the value changes from negative to positive or +// vice-versa. This is detected by keeping a copy of the previous frame of audio data and checking +// to see if the sign of the value has changed. By only changing loudness during zero crossings, we +// avoid audible pops/clicks which can be unpleasant. static void assignmul(uint32_t word, uint32_t *last_word, int32_t *active_lomul, int32_t *active_himul, int32_t pending_lomul, int32_t pending_himul) { + // If the active loudness already matches the pending loudness, exit early. if (MP_LIKELY(*active_lomul == pending_lomul) && MP_LIKELY(*active_himul == pending_himul)) { return; } + + // Check for a zero crossing: current value is 0 or has changed sign from the previous value. + // We check for the sign change by bitmasking only the top bits of each 16-bit signed value + // packed into the 32-bit word (two's complement). If either bit doesn't match the previous + // value, a sign change has occurred on either the left or right channel. if (word == 0 || (*last_word != 0 && ((*last_word) & 0x80008000) != (word & 0x80008000))) { + // Copy over our pending loudness. Will cause future calls to `assignmul` to exit early. *active_lomul = pending_lomul; *active_himul = pending_himul; } else { + // Update our copy of the previous word for future comparisons (an initial value of 0 is + // ignored). *last_word = word; } } @@ -412,6 +427,9 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self, voice->buffer_length -= n >> 1; } + // Force the active level to match the pending level in the case that the conditions of a + // zero crossing weren't met within the last `SYNTHIO_MAX_DUR` frames. Will ensure minimal + // delay in level or panning changes at the potential expensive of an audible pop. voice->active_lo_level = pending_lo_level; voice->active_hi_level = pending_hi_level; } diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 6cf4f31a011..b30b5f35b5f 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -289,14 +289,27 @@ static mp_obj_t synthio_synth_get_note_filter(mp_obj_t note_obj) { return mp_const_none; } +// Rather than immediately changing the loudness of audio playback, we keep a separate buffer of +// the "active" loudness and wait until we meet the conditions of a "zero crossing". A zero crossing +// occurs when either the current value is 0 or the value changes from negative to positive or +// vice-versa. This is detected by keeping a copy of the previous frame of audio data and checking +// to see if the sign of the value has changed. By only changing loudness during zero crossings, we +// avoid audible pops/clicks which can be unpleasant. static void assign_loudness(int32_t word, int32_t *last_word, int16_t active_loudness[2], int16_t pending_loudness[2]) { + // If the active loudness already matches the pending loudness, exit early. if (MP_LIKELY(active_loudness[0] == pending_loudness[0]) && MP_LIKELY(active_loudness[1] == pending_loudness[1])) { return; } + + // Check for a zero crossing: current value is 0 or has changed sign from the previous value. if (word == 0 || (*last_word != 0 && ((*last_word > 0) == (word < 0) || (*last_word < 0) == (word > 0)))) { + // Copy over our pending loudness. Will cause future calls to `assign_loudness` to exit + // early. active_loudness[0] = pending_loudness[0]; active_loudness[1] = pending_loudness[1]; } else { + // Update our copy of the previous word for future comparisons (an initial value of 0 is + // ignored). *last_word = word; } } @@ -318,6 +331,10 @@ static void sum_with_loudness(int32_t *out_buffer32, int32_t *tmp_buffer32, int1 tmp_buffer32++; } } + + // Force the active loudness to match the pending loudness just in case the conditions of a + // zero crossing weren't met within the last `SYNTHIO_MAX_DUR` frames. Will ensure minimal + // delay in amplitude or panning changes at the potential expensive of an audible pop. active_loudness[0] = pending_loudness[0]; active_loudness[1] = pending_loudness[1]; } From fccfdbb8a6ddb2400f5991eb7a762b1f1b54ef8d Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 15:32:23 -0500 Subject: [PATCH 06/20] Check left and right channels independently for zero value --- shared-module/audiomixer/Mixer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index 88c842fcebc..1177b6e0015 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -189,7 +189,7 @@ static void assignmul(uint32_t word, uint32_t *last_word, int32_t *active_lomul, // We check for the sign change by bitmasking only the top bits of each 16-bit signed value // packed into the 32-bit word (two's complement). If either bit doesn't match the previous // value, a sign change has occurred on either the left or right channel. - if (word == 0 || (*last_word != 0 && ((*last_word) & 0x80008000) != (word & 0x80008000))) { + if ((word & 0xffff0000) == 0 || (word & 0x0000ffff) == 0 || (*last_word != 0 && ((*last_word) & 0x80008000) != (word & 0x80008000))) { // Copy over our pending loudness. Will cause future calls to `assignmul` to exit early. *active_lomul = pending_lomul; *active_himul = pending_himul; From aaa4441c9c46391817c5273be03d5a2249efdce7 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 20:44:35 -0500 Subject: [PATCH 07/20] Add filter to `audiodelays.Echo` --- shared-bindings/audiodelays/Echo.c | 31 ++++++++++- shared-bindings/audiodelays/Echo.h | 5 +- shared-module/audiodelays/Echo.c | 89 +++++++++++++++++++++++++++--- shared-module/audiodelays/Echo.h | 6 ++ shared-module/synthio/Biquad.c | 19 ++++++- shared-module/synthio/Biquad.h | 1 + 6 files changed, 140 insertions(+), 11 deletions(-) diff --git a/shared-bindings/audiodelays/Echo.c b/shared-bindings/audiodelays/Echo.c index dbb496c61b9..f1c79de0e13 100644 --- a/shared-bindings/audiodelays/Echo.c +++ b/shared-bindings/audiodelays/Echo.c @@ -25,6 +25,7 @@ //| max_delay_ms: int = 500, //| delay_ms: synthio.BlockInput = 250.0, //| decay: synthio.BlockInput = 0.7, +//| filter: Optional[synthio.Biquad | Tuple[synthio.Biquad]] = None, //| mix: synthio.BlockInput = 0.25, //| buffer_size: int = 512, //| sample_rate: int = 8000, @@ -45,6 +46,7 @@ //| :param int max_delay_ms: The maximum time the echo can be in milliseconds //| :param synthio.BlockInput delay_ms: The current time of the echo delay in milliseconds. Must be less the max_delay_ms //| :param synthio.BlockInput decay: The rate the echo fades. 0.0 = instant; 1.0 = never. +//| :param Optional[synthio.Biquad|Tuple[synthio.Biquad]] filter: A normalized biquad filter object or tuple of normalized biquad filter objects. A copy of the samples are processed sequentially by each filter before writing into the echo buffer. The original samples are not affected by this argument. //| :param synthio.BlockInput mix: The mix as a ratio of the sample (0.0) to the effect (1.0). //| :param int buffer_size: The total size in bytes of each of the two playback buffers to use //| :param int sample_rate: The sample rate to be used @@ -76,11 +78,12 @@ //| ... //| static mp_obj_t audiodelays_echo_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_max_delay_ms, ARG_delay_ms, ARG_decay, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, ARG_freq_shift, }; + enum { ARG_max_delay_ms, ARG_delay_ms, ARG_decay, ARG_filter, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, ARG_freq_shift, }; static const mp_arg_t allowed_args[] = { { MP_QSTR_max_delay_ms, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 500 } }, { MP_QSTR_delay_ms, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_decay, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, { MP_QSTR_mix, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 512} }, { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} }, @@ -103,7 +106,7 @@ static mp_obj_t audiodelays_echo_make_new(const mp_obj_type_t *type, size_t n_ar } audiodelays_echo_obj_t *self = mp_obj_malloc(audiodelays_echo_obj_t, &audiodelays_echo_type); - common_hal_audiodelays_echo_construct(self, max_delay_ms, args[ARG_delay_ms].u_obj, args[ARG_decay].u_obj, args[ARG_mix].u_obj, args[ARG_buffer_size].u_int, bits_per_sample, args[ARG_samples_signed].u_bool, channel_count, sample_rate, args[ARG_freq_shift].u_bool); + common_hal_audiodelays_echo_construct(self, max_delay_ms, args[ARG_delay_ms].u_obj, args[ARG_decay].u_obj, args[ARG_filter].u_obj, args[ARG_mix].u_obj, args[ARG_buffer_size].u_int, bits_per_sample, args[ARG_samples_signed].u_bool, channel_count, sample_rate, args[ARG_freq_shift].u_bool); return MP_OBJ_FROM_PTR(self); } @@ -177,6 +180,29 @@ MP_PROPERTY_GETSET(audiodelays_echo_decay_obj, (mp_obj_t)&audiodelays_echo_get_decay_obj, (mp_obj_t)&audiodelays_echo_set_decay_obj); + +//| filter: synthio.Biquad | Tuple[synthio.Biquad] | None +//| """A normalized biquad filter object or tuple of normalized biquad filter objects. A copy of the samples are processed sequentially by each filter before writing into the echo buffer. The original samples are not affected by this property.""" +//| +static mp_obj_t audiodelays_echo_obj_get_filter(mp_obj_t self_in) { + audiodelays_echo_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return common_hal_audiodelays_echo_get_filter(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_echo_get_filter_obj, audiodelays_echo_obj_get_filter); + +static mp_obj_t audiodelays_echo_obj_set_filter(mp_obj_t self_in, mp_obj_t filter_in) { + audiodelays_echo_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiodelays_echo_set_filter(self, filter_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiodelays_echo_set_filter_obj, audiodelays_echo_obj_set_filter); + +MP_PROPERTY_GETSET(audiodelays_echo_filter_obj, + (mp_obj_t)&audiodelays_echo_get_filter_obj, + (mp_obj_t)&audiodelays_echo_set_filter_obj); + + //| mix: synthio.BlockInput //| """The rate the echo mix between 0 and 1 where 0 is only sample, 0.5 is an equal mix of the sample and the effect and 1 is all effect.""" static mp_obj_t audiodelays_echo_obj_get_mix(mp_obj_t self_in) { @@ -285,6 +311,7 @@ static const mp_rom_map_elem_t audiodelays_echo_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiodelays_echo_playing_obj) }, { MP_ROM_QSTR(MP_QSTR_delay_ms), MP_ROM_PTR(&audiodelays_echo_delay_ms_obj) }, { MP_ROM_QSTR(MP_QSTR_decay), MP_ROM_PTR(&audiodelays_echo_decay_obj) }, + { MP_ROM_QSTR(MP_QSTR_filter), MP_ROM_PTR(&audiodelays_echo_filter_obj) }, { MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiodelays_echo_mix_obj) }, { MP_ROM_QSTR(MP_QSTR_freq_shift), MP_ROM_PTR(&audiodelays_echo_freq_shift_obj) }, AUDIOSAMPLE_FIELDS, diff --git a/shared-bindings/audiodelays/Echo.h b/shared-bindings/audiodelays/Echo.h index 83d454ed05c..67b7436b896 100644 --- a/shared-bindings/audiodelays/Echo.h +++ b/shared-bindings/audiodelays/Echo.h @@ -11,7 +11,7 @@ extern const mp_obj_type_t audiodelays_echo_type; void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_t max_delay_ms, - mp_obj_t delay_ms, mp_obj_t decay, mp_obj_t mix, + mp_obj_t delay_ms, mp_obj_t decay, mp_obj_t filter, mp_obj_t mix, uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, uint8_t channel_count, uint32_t sample_rate, bool freq_shift); @@ -26,6 +26,9 @@ void common_hal_audiodelays_echo_set_freq_shift(audiodelays_echo_obj_t *self, bo mp_obj_t common_hal_audiodelays_echo_get_decay(audiodelays_echo_obj_t *self); void common_hal_audiodelays_echo_set_decay(audiodelays_echo_obj_t *self, mp_obj_t decay); +mp_obj_t common_hal_audiodelays_echo_get_filter(audiodelays_echo_obj_t *self); +void common_hal_audiodelays_echo_set_filter(audiodelays_echo_obj_t *self, mp_obj_t arg); + mp_obj_t common_hal_audiodelays_echo_get_mix(audiodelays_echo_obj_t *self); void common_hal_audiodelays_echo_set_mix(audiodelays_echo_obj_t *self, mp_obj_t arg); diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c index b2501b7d01e..243fd365f4a 100644 --- a/shared-module/audiodelays/Echo.c +++ b/shared-module/audiodelays/Echo.c @@ -11,7 +11,7 @@ #include void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_t max_delay_ms, - mp_obj_t delay_ms, mp_obj_t decay, mp_obj_t mix, + mp_obj_t delay_ms, mp_obj_t decay, mp_obj_t filter, mp_obj_t mix, uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, uint8_t channel_count, uint32_t sample_rate, bool freq_shift) { @@ -59,6 +59,8 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_ // The below section sets up the echo effect's starting values. For a different effect this section will change + common_hal_audiodelays_echo_set_filter(self, filter); + // If we did not receive a BlockInput we need to create a default float value if (decay == MP_OBJ_NULL) { decay = mp_obj_new_float(MICROPY_FLOAT_CONST(0.7)); @@ -109,6 +111,8 @@ void common_hal_audiodelays_echo_deinit(audiodelays_echo_obj_t *self) { self->echo_buffer = NULL; self->buffer[0] = NULL; self->buffer[1] = NULL; + self->filter = mp_const_none; + self->filter_states = NULL; } mp_obj_t common_hal_audiodelays_echo_get_delay_ms(audiodelays_echo_obj_t *self) { @@ -166,6 +170,55 @@ void common_hal_audiodelays_echo_set_decay(audiodelays_echo_obj_t *self, mp_obj_ synthio_block_assign_slot(decay, &self->decay, MP_QSTR_decay); } +mp_obj_t common_hal_audiodelays_echo_get_filter(audiodelays_echo_obj_t *self) { + return self->filter; +} + +void common_hal_audiodelays_echo_set_filter(audiodelays_echo_obj_t *self, mp_obj_t filter_in) { + size_t n_items; + mp_obj_t *items; + mp_obj_t *filter_objs; + + if (filter_in == mp_const_none) { + n_items = 0; + filter_objs = NULL; + } 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 { + n_items = 1; + if (!mp_obj_is_type(filter_in, &synthio_biquad_type_obj)) { + mp_raise_TypeError_varg( + MP_ERROR_TEXT("%q must be of type %q or %q, not %q"), + MP_QSTR_filter, MP_QSTR_Biquad, MP_QSTR_iterable, mp_obj_get_type(filter_in)->name); + } + filter_objs = &self->filter; + } + + // everything has been checked, so we can do the following without fear + + self->filter = filter_in; + self->filter_objs = filter_objs; + self->filter_states = m_renew(biquad_filter_state, + self->filter_states, + self->filter_objs_len * self->base.channel_count, + n_items * self->base.channel_count); + self->filter_objs_len = n_items; +} + mp_obj_t common_hal_audiodelays_echo_get_mix(audiodelays_echo_obj_t *self) { return self->mix.obj; } @@ -197,6 +250,12 @@ void audiodelays_echo_reset_buffer(audiodelays_echo_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); memset(self->echo_buffer, 0, self->max_echo_buffer_len); + + if (self->filter_states) { + for (uint8_t i = 0; i < self->filter_objs_len * self->base.channel_count; i++) { + synthio_biquad_filter_reset(&self->filter_states[i]); + } + } } bool common_hal_audiodelays_echo_get_playing(audiodelays_echo_obj_t *self) { @@ -227,6 +286,17 @@ void common_hal_audiodelays_echo_stop(audiodelays_echo_obj_t *self) { return; } +static int32_t process_biquad_filters(audiodelays_echo_obj_t *self, uint8_t channel, int32_t word) { + // Process biquad filters + for (uint8_t j = 0; j < self->filter_objs_len; j++) { + mp_obj_t filter_obj = self->filter_objs[j]; + for (uint8_t k = 0; k < self->base.channel_count; k++) { + word = synthio_biquad_filter_sample(filter_obj, &self->filter_states[j * self->base.channel_count + channel], word); + } + } + return word; +} + audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *self, bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { @@ -289,6 +359,11 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * recalculate_delay(self, f_delay_ms); } + // Tick biquad filters + for (uint8_t j = 0; j < self->filter_objs_len; j++) { + common_hal_synthio_biquad_tick(self->filter_objs[j]); + } + uint32_t echo_buf_len = self->echo_buffer_len / sizeof(uint16_t); uint32_t max_echo_buf_len = (self->max_echo_buffer_len >> (self->base.channel_count - 1)) / sizeof(uint16_t); @@ -324,12 +399,12 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) { word = (int16_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = word; + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); } } else { echo = echo_buffer[echo_buffer_pos + echo_buffer_offset]; word = (int16_t)(echo * decay); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = word; + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); } word = (int16_t)(echo * MIN(mix, MICROPY_FLOAT_CONST(1.0))); @@ -409,11 +484,11 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) { word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word); word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)word; + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); } } else { word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)word; + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); } } else { if (self->freq_shift) { @@ -421,12 +496,12 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word); // Do not have mix_down for 8 bit so just hard cap samples into 1 byte word = MIN(MAX(word, -128), 127); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int8_t)word; + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int8_t)process_biquad_filters(self, !!echo_buffer_offset, word); } } else { // Do not have mix_down for 8 bit so just hard cap samples into 1 byte word = MIN(MAX(word, -128), 127); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int8_t)word; + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int8_t)process_biquad_filters(self, !!echo_buffer_offset, word); } } diff --git a/shared-module/audiodelays/Echo.h b/shared-module/audiodelays/Echo.h index cc37f7030be..94c9ee4d2de 100644 --- a/shared-module/audiodelays/Echo.h +++ b/shared-module/audiodelays/Echo.h @@ -10,6 +10,7 @@ #include "shared-module/audiocore/__init__.h" #include "shared-module/synthio/__init__.h" #include "shared-module/synthio/block.h" +#include "shared-module/synthio/Biquad.h" extern const mp_obj_type_t audiodelays_echo_type; @@ -20,8 +21,13 @@ typedef struct { mp_float_t current_delay_ms; mp_float_t sample_ms; synthio_block_slot_t decay; + mp_obj_t filter; synthio_block_slot_t mix; + mp_obj_t *filter_objs; + size_t filter_objs_len; + biquad_filter_state *filter_states; + int8_t *buffer[2]; uint8_t last_buf_idx; uint32_t buffer_len; // max buffer in bytes diff --git a/shared-module/synthio/Biquad.c b/shared-module/synthio/Biquad.c index 08aab81a7bf..edefd071403 100644 --- a/shared-module/synthio/Biquad.c +++ b/shared-module/synthio/Biquad.c @@ -197,6 +197,10 @@ void synthio_biquad_filter_reset(biquad_filter_state *st) { memset(&st->x, 0, 4 * sizeof(int16_t)); } +static inline int32_t biquad_filter_sample(int32_t input, int32_t a1, int32_t a2, int32_t b0, int32_t b1, int32_t b2, int32_t x0, int32_t x1, int32_t y0, int32_t y1) { + return synthio_sat16((b0 * input + b1 * x0 + b2 * x1 - a1 * y0 - a2 * y1 + (1 << (BIQUAD_SHIFT - 1))), BIQUAD_SHIFT); +} + void synthio_biquad_filter_samples(mp_obj_t self_in, biquad_filter_state *st, int32_t *buffer, size_t n_samples) { synthio_biquad_t *self = MP_OBJ_TO_PTR(self_in); @@ -213,7 +217,7 @@ void synthio_biquad_filter_samples(mp_obj_t self_in, biquad_filter_state *st, in for (size_t n = n_samples; n; --n, ++buffer) { int32_t input = *buffer; - int32_t output = synthio_sat16((b0 * input + b1 * x0 + b2 * x1 - a1 * y0 - a2 * y1 + (1 << (BIQUAD_SHIFT - 1))), BIQUAD_SHIFT); + int32_t output = biquad_filter_sample(input, a1, a2, b0, b1, b2, x0, x1, y0, y1); x1 = x0; x0 = input; @@ -226,3 +230,16 @@ void synthio_biquad_filter_samples(mp_obj_t self_in, biquad_filter_state *st, in st->y[0] = y0; st->y[1] = y1; } + +int32_t synthio_biquad_filter_sample(mp_obj_t self_in, biquad_filter_state *st, int32_t input) { + synthio_biquad_t *self = MP_OBJ_TO_PTR(self_in); + + int32_t output = biquad_filter_sample(input, self->a1, self->a2, self->b0, self->b1, self->b2, st->x[0], st->x[1], st->y[0], st->y[1]); + + st->x[1] = st->x[0]; + st->x[0] = input; + st->y[1] = st->y[0]; + st->y[0] = output; + + return output; +} diff --git a/shared-module/synthio/Biquad.h b/shared-module/synthio/Biquad.h index 3b9920ed92f..cc9b4f53b0e 100644 --- a/shared-module/synthio/Biquad.h +++ b/shared-module/synthio/Biquad.h @@ -27,3 +27,4 @@ typedef struct { void common_hal_synthio_biquad_tick(mp_obj_t self_in); void synthio_biquad_filter_reset(biquad_filter_state *st); void synthio_biquad_filter_samples(mp_obj_t self_in, biquad_filter_state *st, int32_t *buffer, size_t n_samples); +int32_t synthio_biquad_filter_sample(mp_obj_t self_in, biquad_filter_state *st, int32_t input); From f10f561cd79848fa456785efef954e375f34ca2b Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 23:17:48 -0500 Subject: [PATCH 08/20] Share filter object functionality --- shared-module/audiodelays/Echo.c | 77 ++++----------------------- shared-module/audiofilters/Filter.c | 57 +++----------------- shared-module/audiofilters/__init__.c | 68 +++++++++++++++++++++++ shared-module/audiofilters/__init__.h | 8 +++ 4 files changed, 92 insertions(+), 118 deletions(-) diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c index 243fd365f4a..cdf34541972 100644 --- a/shared-module/audiodelays/Echo.c +++ b/shared-module/audiodelays/Echo.c @@ -5,6 +5,7 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/audiodelays/Echo.h" #include "shared-bindings/audiocore/__init__.h" +#include "shared-module/audiofilters/__init__.h" #include #include "py/runtime.h" @@ -175,48 +176,7 @@ mp_obj_t common_hal_audiodelays_echo_get_filter(audiodelays_echo_obj_t *self) { } void common_hal_audiodelays_echo_set_filter(audiodelays_echo_obj_t *self, mp_obj_t filter_in) { - size_t n_items; - mp_obj_t *items; - mp_obj_t *filter_objs; - - if (filter_in == mp_const_none) { - n_items = 0; - filter_objs = NULL; - } 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 { - n_items = 1; - if (!mp_obj_is_type(filter_in, &synthio_biquad_type_obj)) { - mp_raise_TypeError_varg( - MP_ERROR_TEXT("%q must be of type %q or %q, not %q"), - MP_QSTR_filter, MP_QSTR_Biquad, MP_QSTR_iterable, mp_obj_get_type(filter_in)->name); - } - filter_objs = &self->filter; - } - - // everything has been checked, so we can do the following without fear - - self->filter = filter_in; - self->filter_objs = filter_objs; - self->filter_states = m_renew(biquad_filter_state, - self->filter_states, - self->filter_objs_len * self->base.channel_count, - n_items * self->base.channel_count); - self->filter_objs_len = n_items; + audiofilters_assign_filters(filter_in, &self->filter, &self->filter_objs, &self->filter_objs_len, &self->filter_states, self->base.channel_count); } mp_obj_t common_hal_audiodelays_echo_get_mix(audiodelays_echo_obj_t *self) { @@ -251,11 +211,7 @@ void audiodelays_echo_reset_buffer(audiodelays_echo_obj_t *self, memset(self->buffer[1], 0, self->buffer_len); memset(self->echo_buffer, 0, self->max_echo_buffer_len); - if (self->filter_states) { - for (uint8_t i = 0; i < self->filter_objs_len * self->base.channel_count; i++) { - synthio_biquad_filter_reset(&self->filter_states[i]); - } - } + audiofilters_reset_filters(self->filter_states, self->filter_objs_len, self->base.channel_count); } bool common_hal_audiodelays_echo_get_playing(audiodelays_echo_obj_t *self) { @@ -286,17 +242,6 @@ void common_hal_audiodelays_echo_stop(audiodelays_echo_obj_t *self) { return; } -static int32_t process_biquad_filters(audiodelays_echo_obj_t *self, uint8_t channel, int32_t word) { - // Process biquad filters - for (uint8_t j = 0; j < self->filter_objs_len; j++) { - mp_obj_t filter_obj = self->filter_objs[j]; - for (uint8_t k = 0; k < self->base.channel_count; k++) { - word = synthio_biquad_filter_sample(filter_obj, &self->filter_states[j * self->base.channel_count + channel], word); - } - } - return word; -} - audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t *self, bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { @@ -360,9 +305,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * } // Tick biquad filters - for (uint8_t j = 0; j < self->filter_objs_len; j++) { - common_hal_synthio_biquad_tick(self->filter_objs[j]); - } + audiofilters_tick_filters(self->filter_objs, self->filter_objs_len); uint32_t echo_buf_len = self->echo_buffer_len / sizeof(uint16_t); uint32_t max_echo_buf_len = (self->max_echo_buffer_len >> (self->base.channel_count - 1)) / sizeof(uint16_t); @@ -399,12 +342,12 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) { word = (int16_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); } } else { echo = echo_buffer[echo_buffer_pos + echo_buffer_offset]; word = (int16_t)(echo * decay); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); } word = (int16_t)(echo * MIN(mix, MICROPY_FLOAT_CONST(1.0))); @@ -484,11 +427,11 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) { word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word); word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); } } else { word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)process_biquad_filters(self, !!echo_buffer_offset, word); + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); } } else { if (self->freq_shift) { @@ -496,12 +439,12 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word); // Do not have mix_down for 8 bit so just hard cap samples into 1 byte word = MIN(MAX(word, -128), 127); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int8_t)process_biquad_filters(self, !!echo_buffer_offset, word); + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int8_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); } } else { // Do not have mix_down for 8 bit so just hard cap samples into 1 byte word = MIN(MAX(word, -128), 127); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int8_t)process_biquad_filters(self, !!echo_buffer_offset, word); + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int8_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); } } diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index 57a763629fa..5ccd7c8a936 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -5,8 +5,9 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/audiofilters/Filter.h" #include "shared-bindings/audiocore/__init__.h" - +#include "shared-module/audiofilters/__init__.h" #include "shared-module/synthio/Biquad.h" + #include #include "py/runtime.h" @@ -67,48 +68,7 @@ void common_hal_audiofilters_filter_deinit(audiofilters_filter_obj_t *self) { } void common_hal_audiofilters_filter_set_filter(audiofilters_filter_obj_t *self, mp_obj_t filter_in) { - size_t n_items; - mp_obj_t *items; - mp_obj_t *filter_objs; - - if (filter_in == mp_const_none) { - n_items = 0; - filter_objs = NULL; - } 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 { - n_items = 1; - if (!mp_obj_is_type(filter_in, &synthio_biquad_type_obj)) { - mp_raise_TypeError_varg( - MP_ERROR_TEXT("%q must be of type %q or %q, not %q"), - MP_QSTR_filter, MP_QSTR_Biquad, MP_QSTR_iterable, mp_obj_get_type(filter_in)->name); - } - filter_objs = &self->filter; - } - - // everything has been checked, so we can do the following without fear - - self->filter = filter_in; - self->filter_objs = filter_objs; - self->filter_states = m_renew(biquad_filter_state, - self->filter_states, - self->filter_objs_len * self->base.channel_count, - n_items * self->base.channel_count); - self->filter_objs_len = n_items; + audiofilters_assign_filters(filter_in, &self->filter, &self->filter_objs, &self->filter_objs_len, &self->filter_states, self->base.channel_count); } mp_obj_t common_hal_audiofilters_filter_get_filter(audiofilters_filter_obj_t *self) { @@ -131,11 +91,7 @@ void audiofilters_filter_reset_buffer(audiofilters_filter_obj_t *self, memset(self->buffer[1], 0, self->buffer_len); memset(self->filter_buffer, 0, SYNTHIO_MAX_DUR * self->base.channel_count * sizeof(int32_t)); - if (self->filter_states) { - for (uint8_t i = 0; i < self->filter_objs_len * self->base.channel_count; i++) { - synthio_biquad_filter_reset(&self->filter_states[i]); - } - } + audiofilters_reset_filters(self->filter_states, self->filter_objs_len, self->base.channel_count); } bool common_hal_audiofilters_filter_get_playing(audiofilters_filter_obj_t *self) { @@ -213,9 +169,8 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o (void)synthio_block_slot_get(&self->mix); // Tick biquad filters - for (uint8_t j = 0; j < self->filter_objs_len; j++) { - common_hal_synthio_biquad_tick(self->filter_objs[j]); - } + audiofilters_tick_filters(self->filter_objs, self->filter_objs_len); + if (self->base.samples_signed) { memset(word_buffer, 0, length * (self->base.bits_per_sample / 8)); } else { diff --git a/shared-module/audiofilters/__init__.c b/shared-module/audiofilters/__init__.c index 83929b4c4fb..6435ec36e36 100644 --- a/shared-module/audiofilters/__init__.c +++ b/shared-module/audiofilters/__init__.c @@ -3,3 +3,71 @@ // SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple // // SPDX-License-Identifier: MIT + +#include "shared-module/audiofilters/__init__.h" + +void audiofilters_assign_filters(mp_obj_t filter_in, mp_obj_t *filter_out, mp_obj_t **filter_objs, size_t *filter_objs_len, biquad_filter_state **filter_states, uint8_t channel_count) { + size_t n_items; + mp_obj_t *items; + + if (filter_in == mp_const_none) { + n_items = 0; + *filter_objs = NULL; + } 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 { + n_items = 1; + if (!mp_obj_is_type(filter_in, &synthio_biquad_type_obj)) { + mp_raise_TypeError_varg( + MP_ERROR_TEXT("%q must be of type %q or %q, not %q"), + MP_QSTR_filter, MP_QSTR_Biquad, MP_QSTR_iterable, mp_obj_get_type(filter_in)->name); + } + *filter_objs = filter_out; + } + + // everything has been checked, so we can do the following without fear + + *filter_out = filter_in; + *filter_states = m_renew(biquad_filter_state, + *filter_states, + *filter_objs_len * channel_count, + n_items * channel_count); + *filter_objs_len = n_items; +} + +void audiofilters_reset_filters(biquad_filter_state *filter_states, size_t filter_objs_len, uint8_t channel_count) { + if (filter_states) { + for (uint8_t i = 0; i < filter_objs_len * channel_count; i++) { + synthio_biquad_filter_reset(&filter_states[i]); + } + } +} + +void audiofilters_tick_filters(mp_obj_t *filter_objs, size_t filter_objs_len) { + for (uint8_t j = 0; j < filter_objs_len; j++) { + common_hal_synthio_biquad_tick(filter_objs[j]); + } +} + +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) { + // Process biquad filters + for (uint8_t j = 0; j < filter_objs_len; j++) { + mp_obj_t filter_obj = filter_objs[j]; + word = synthio_biquad_filter_sample(filter_obj, &filter_states[j * channel_count + channel], word); + } + return word; +} diff --git a/shared-module/audiofilters/__init__.h b/shared-module/audiofilters/__init__.h index 29d2f672655..067d04ef209 100644 --- a/shared-module/audiofilters/__init__.h +++ b/shared-module/audiofilters/__init__.h @@ -5,3 +5,11 @@ // SPDX-License-Identifier: MIT #pragma once + +#include "py/obj.h" +#include "shared-module/synthio/Biquad.h" + +void audiofilters_assign_filters(mp_obj_t filter_in, mp_obj_t *filter_out, mp_obj_t **filter_objs, size_t *filter_objs_len, biquad_filter_state **filter_states, uint8_t channel_count); +void audiofilters_reset_filters(biquad_filter_state *filter_states, size_t filter_objs_len, uint8_t channel_count); +void audiofilters_tick_filters(mp_obj_t *filter_objs, size_t filter_objs_len); +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); From 4eab9125c39c44cfb8b81a28a30f5aee3965fc19 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 13 Aug 2026 23:18:01 -0500 Subject: [PATCH 09/20] Add pre and post filters to Freeverb --- shared-bindings/audiofreeverb/Freeverb.c | 57 +++++++++++++++++++++++- shared-bindings/audiofreeverb/Freeverb.h | 8 +++- shared-module/audiofreeverb/Freeverb.c | 41 ++++++++++++++++- shared-module/audiofreeverb/Freeverb.h | 11 +++++ 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/shared-bindings/audiofreeverb/Freeverb.c b/shared-bindings/audiofreeverb/Freeverb.c index 12eb7ef70ab..83c9eb7c01f 100644 --- a/shared-bindings/audiofreeverb/Freeverb.c +++ b/shared-bindings/audiofreeverb/Freeverb.c @@ -24,6 +24,8 @@ //| self, //| roomsize: synthio.BlockInput = 0.5, //| damp: synthio.BlockInput = 0.5, +//| pre_filter: Optional[synthio.Biquad | Tuple[synthio.Biquad]] = None, +//| post_filter: Optional[synthio.Biquad | Tuple[synthio.Biquad]] = None, //| mix: synthio.BlockInput = 0.5, //| buffer_size: int = 512, //| sample_rate: int = 8000, @@ -40,6 +42,8 @@ //| //| :param synthio.BlockInput roomsize: The size of the room. 0.0 = smallest; 1.0 = largest. //| :param synthio.BlockInput damp: How much the walls absorb. 0.0 = least; 1.0 = most. +//| :param Optional[synthio.Biquad|Tuple[synthio.Biquad]] pre_filter: A normalized biquad filter object or tuple of normalized biquad filter objects. The sample is processed sequentially by each "Pre-EQ" filter before being fed to the reverb effect. +//| :param Optional[synthio.Biquad|Tuple[synthio.Biquad]] post_filter: A normalized biquad filter object or tuple of normalized biquad filter objects. The output of the reverb effect is processed sequentially by each "Post-EQ" filter before being mixed with the original sample. //| :param synthio.BlockInput mix: The mix as a ratio of the sample (0.0) to the effect (1.0). //| :param int buffer_size: The total size in bytes of each of the two playback buffers to use //| :param int sample_rate: The sample rate to be used @@ -70,10 +74,12 @@ //| ... //| static mp_obj_t audiofreeverb_freeverb_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_roomsize, ARG_damp, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, }; + enum { ARG_roomsize, ARG_damp, ARG_pre_filter, ARG_post_filter, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, }; static const mp_arg_t allowed_args[] = { { MP_QSTR_roomsize, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_damp, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_pre_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, + { MP_QSTR_post_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, { MP_QSTR_mix, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 512} }, { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} }, @@ -96,7 +102,7 @@ static mp_obj_t audiofreeverb_freeverb_make_new(const mp_obj_type_t *type, size_ } audiofreeverb_freeverb_obj_t *self = mp_obj_malloc(audiofreeverb_freeverb_obj_t, &audiofreeverb_freeverb_type); - common_hal_audiofreeverb_freeverb_construct(self, args[ARG_roomsize].u_obj, args[ARG_damp].u_obj, args[ARG_mix].u_obj, args[ARG_buffer_size].u_int, bits_per_sample, args[ARG_samples_signed].u_bool, channel_count, sample_rate); + common_hal_audiofreeverb_freeverb_construct(self, args[ARG_roomsize].u_obj, args[ARG_damp].u_obj, args[ARG_pre_filter].u_obj, args[ARG_post_filter].u_obj, args[ARG_mix].u_obj, args[ARG_buffer_size].u_int, bits_per_sample, args[ARG_samples_signed].u_bool, channel_count, sample_rate); return MP_OBJ_FROM_PTR(self); } @@ -165,6 +171,51 @@ MP_PROPERTY_GETSET(audiofreeverb_freeverb_damp_obj, (mp_obj_t)&audiofreeverb_freeverb_get_damp_obj, (mp_obj_t)&audiofreeverb_freeverb_set_damp_obj); + +//| pre_filter: synthio.Biquad | Tuple[synthio.Biquad] | None +//| """A normalized biquad filter object or tuple of normalized biquad filter objects. The sample is processed sequentially by each "Pre-EQ" filter before being fed to the reverb effect.""" +//| +static mp_obj_t audiofreeverb_freeverb_obj_get_pre_filter(mp_obj_t self_in) { + audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return common_hal_audiofreeverb_freeverb_get_pre_filter(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_get_pre_filter_obj, audiofreeverb_freeverb_obj_get_pre_filter); + +static mp_obj_t audiofreeverb_freeverb_obj_set_pre_filter(mp_obj_t self_in, mp_obj_t filter_in) { + audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofreeverb_freeverb_set_pre_filter(self, filter_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofreeverb_freeverb_set_pre_filter_obj, audiofreeverb_freeverb_obj_set_pre_filter); + +MP_PROPERTY_GETSET(audiofreeverb_freeverb_pre_filter_obj, + (mp_obj_t)&audiofreeverb_freeverb_get_pre_filter_obj, + (mp_obj_t)&audiofreeverb_freeverb_set_pre_filter_obj); + + +//| post_filter: synthio.Biquad | Tuple[synthio.Biquad] | None +//| """A normalized biquad filter object or tuple of normalized biquad filter objects. The output of the reverb effect is processed sequentially by each "Post-EQ" filter before being mixed with the original sample.""" +//| +static mp_obj_t audiofreeverb_freeverb_obj_get_post_filter(mp_obj_t self_in) { + audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return common_hal_audiofreeverb_freeverb_get_post_filter(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofreeverb_freeverb_get_post_filter_obj, audiofreeverb_freeverb_obj_get_post_filter); + +static mp_obj_t audiofreeverb_freeverb_obj_set_post_filter(mp_obj_t self_in, mp_obj_t filter_in) { + audiofreeverb_freeverb_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofreeverb_freeverb_set_post_filter(self, filter_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofreeverb_freeverb_set_post_filter_obj, audiofreeverb_freeverb_obj_set_post_filter); + +MP_PROPERTY_GETSET(audiofreeverb_freeverb_post_filter_obj, + (mp_obj_t)&audiofreeverb_freeverb_get_post_filter_obj, + (mp_obj_t)&audiofreeverb_freeverb_set_post_filter_obj); + + //| mix: synthio.BlockInput //| """The rate the reverb mix between 0 and 1 where 0 is only sample and 1 is all effect.""" static mp_obj_t audiofreeverb_freeverb_obj_get_mix(mp_obj_t self_in) { @@ -251,6 +302,8 @@ static const mp_rom_map_elem_t audiofreeverb_freeverb_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofreeverb_freeverb_playing_obj) }, { MP_ROM_QSTR(MP_QSTR_roomsize), MP_ROM_PTR(&audiofreeverb_freeverb_roomsize_obj) }, { MP_ROM_QSTR(MP_QSTR_damp), MP_ROM_PTR(&audiofreeverb_freeverb_damp_obj) }, + { MP_ROM_QSTR(MP_QSTR_pre_filter), MP_ROM_PTR(&audiofreeverb_freeverb_pre_filter_obj) }, + { MP_ROM_QSTR(MP_QSTR_post_filter), MP_ROM_PTR(&audiofreeverb_freeverb_post_filter_obj) }, { MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiofreeverb_freeverb_mix_obj) }, AUDIOSAMPLE_FIELDS, }; diff --git a/shared-bindings/audiofreeverb/Freeverb.h b/shared-bindings/audiofreeverb/Freeverb.h index bde6cfffda7..7eed9775006 100644 --- a/shared-bindings/audiofreeverb/Freeverb.h +++ b/shared-bindings/audiofreeverb/Freeverb.h @@ -11,7 +11,7 @@ extern const mp_obj_type_t audiofreeverb_freeverb_type; void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *self, - mp_obj_t roomsize, mp_obj_t damp, mp_obj_t mix, + mp_obj_t roomsize, mp_obj_t damp, mp_obj_t pre_filter, mp_obj_t post_filter, mp_obj_t mix, uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, uint8_t channel_count, uint32_t sample_rate); @@ -24,6 +24,12 @@ void common_hal_audiofreeverb_freeverb_set_roomsize(audiofreeverb_freeverb_obj_t mp_obj_t common_hal_audiofreeverb_freeverb_get_damp(audiofreeverb_freeverb_obj_t *self); void common_hal_audiofreeverb_freeverb_set_damp(audiofreeverb_freeverb_obj_t *self, mp_obj_t damp); +mp_obj_t common_hal_audiofreeverb_freeverb_get_pre_filter(audiofreeverb_freeverb_obj_t *self); +void common_hal_audiofreeverb_freeverb_set_pre_filter(audiofreeverb_freeverb_obj_t *self, mp_obj_t arg); + +mp_obj_t common_hal_audiofreeverb_freeverb_get_post_filter(audiofreeverb_freeverb_obj_t *self); +void common_hal_audiofreeverb_freeverb_set_post_filter(audiofreeverb_freeverb_obj_t *self, mp_obj_t arg); + mp_obj_t common_hal_audiofreeverb_freeverb_get_mix(audiofreeverb_freeverb_obj_t *self); void common_hal_audiofreeverb_freeverb_set_mix(audiofreeverb_freeverb_obj_t *self, mp_obj_t mix); diff --git a/shared-module/audiofreeverb/Freeverb.c b/shared-module/audiofreeverb/Freeverb.c index d0f1aeaa546..4bea25e9807 100644 --- a/shared-module/audiofreeverb/Freeverb.c +++ b/shared-module/audiofreeverb/Freeverb.c @@ -9,13 +9,14 @@ // #include "shared-bindings/audiofreeverb/Freeverb.h" #include "shared-bindings/audiocore/__init__.h" +#include "shared-module/audiofilters/__init__.h" #include "shared-module/synthio/__init__.h" #include #include "py/runtime.h" #include -void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *self, mp_obj_t roomsize, mp_obj_t damp, mp_obj_t mix, +void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *self, mp_obj_t roomsize, mp_obj_t damp, mp_obj_t pre_filter, mp_obj_t post_filter, mp_obj_t mix, uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, uint8_t channel_count, uint32_t sample_rate) { @@ -59,6 +60,9 @@ void common_hal_audiofreeverb_freeverb_construct(audiofreeverb_freeverb_obj_t *s self->more_data = false; // Is there still more data to read from the sample or did we finish // The below section sets up the reverb effect's starting values. For a different effect this section will change + common_hal_audiofreeverb_freeverb_set_pre_filter(self, pre_filter); + common_hal_audiofreeverb_freeverb_set_post_filter(self, post_filter); + if (roomsize == MP_OBJ_NULL) { roomsize = mp_obj_new_float(MICROPY_FLOAT_CONST(0.5)); } @@ -128,6 +132,10 @@ void common_hal_audiofreeverb_freeverb_deinit(audiofreeverb_freeverb_obj_t *self audiosample_mark_deinit(&self->base); self->buffer[0] = NULL; self->buffer[1] = NULL; + self->pre_filter = mp_const_none; + self->pre_filter_states = NULL; + self->post_filter = mp_const_none; + self->post_filter_states = NULL; } mp_obj_t common_hal_audiofreeverb_freeverb_get_roomsize(audiofreeverb_freeverb_obj_t *self) { @@ -167,6 +175,22 @@ void audiofreeverb_freeverb_get_damp_fixedpoint(mp_float_t n, int16_t *damp1, in *damp2 = (int16_t)(32768 - *damp1); // inverse of x1 damp2 = 1.0 - damp1 } +mp_obj_t common_hal_audiofreeverb_freeverb_get_pre_filter(audiofreeverb_freeverb_obj_t *self) { + return self->pre_filter; +} + +void common_hal_audiofreeverb_freeverb_set_pre_filter(audiofreeverb_freeverb_obj_t *self, mp_obj_t filter_in) { + audiofilters_assign_filters(filter_in, &self->pre_filter, &self->pre_filter_objs, &self->pre_filter_objs_len, &self->pre_filter_states, self->base.channel_count); +} + +mp_obj_t common_hal_audiofreeverb_freeverb_get_post_filter(audiofreeverb_freeverb_obj_t *self) { + return self->post_filter; +} + +void common_hal_audiofreeverb_freeverb_set_post_filter(audiofreeverb_freeverb_obj_t *self, mp_obj_t filter_in) { + audiofilters_assign_filters(filter_in, &self->post_filter, &self->post_filter_objs, &self->post_filter_objs_len, &self->post_filter_states, self->base.channel_count); +} + mp_obj_t common_hal_audiofreeverb_freeverb_get_mix(audiofreeverb_freeverb_obj_t *self) { return self->mix.obj; } @@ -187,6 +211,9 @@ void audiofreeverb_freeverb_reset_buffer(audiofreeverb_freeverb_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); + + audiofilters_reset_filters(self->pre_filter_states, self->pre_filter_objs_len, self->base.channel_count); + audiofilters_reset_filters(self->post_filter_states, self->post_filter_objs_len, self->base.channel_count); } bool common_hal_audiofreeverb_freeverb_get_playing(audiofreeverb_freeverb_obj_t *self) { @@ -274,6 +301,10 @@ audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_free mp_float_t roomsize = synthio_block_slot_get_limited(&self->roomsize, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); int16_t feedback = audiofreeverb_freeverb_get_roomsize_fixedpoint(roomsize); + // Tick biquad filters + audiofilters_tick_filters(self->pre_filter_objs, self->pre_filter_objs_len); + audiofilters_tick_filters(self->post_filter_objs, self->post_filter_objs_len); + int16_t *sample_src = (int16_t *)self->sample_remaining_buffer; for (uint32_t i = 0; i < n; i++) { @@ -286,7 +317,10 @@ audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_free int16_t input, bufout, output; uint32_t channel_comb_offset = 0, channel_allpass_offset = 0; - input = synthio_sat16(sample_word * 8738, 17); // Initial input scaled down so we can add reverb + // Apply filters as Pre-EQ + input = (int16_t)audiofilters_process_filters(self->pre_filter_objs, self->pre_filter_objs_len, self->pre_filter_states, self->base.channel_count, n % self->base.channel_count, sample_word); + + input = synthio_sat16((int32_t)input * 8738, 17); // Initial input scaled down so we can add reverb sum = 0; // Calculate each of the 8 comb buffers @@ -312,6 +346,9 @@ audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_free } } + // Apply filters as Post-EQ + output = (int16_t)audiofilters_process_filters(self->post_filter_objs, self->post_filter_objs_len, self->post_filter_states, self->base.channel_count, n % self->base.channel_count, (int32_t)output); + word = output * 30; // Add some volume back don't have to saturate as next step will word = synthio_sat16(sample_word * mix_sample, 15) + synthio_sat16(word * mix_effect, 15); diff --git a/shared-module/audiofreeverb/Freeverb.h b/shared-module/audiofreeverb/Freeverb.h index 44747f0fc95..79a11e3490e 100644 --- a/shared-module/audiofreeverb/Freeverb.h +++ b/shared-module/audiofreeverb/Freeverb.h @@ -10,6 +10,7 @@ #include "shared-module/audiocore/__init__.h" #include "shared-module/synthio/__init__.h" #include "shared-module/synthio/block.h" +#include "shared-module/synthio/Biquad.h" extern const mp_obj_type_t audiofreeverb_freeverb_type; @@ -17,8 +18,18 @@ typedef struct { audiosample_base_t base; synthio_block_slot_t roomsize; synthio_block_slot_t damp; + mp_obj_t pre_filter; + mp_obj_t post_filter; synthio_block_slot_t mix; + mp_obj_t *pre_filter_objs; + size_t pre_filter_objs_len; + biquad_filter_state *pre_filter_states; + + mp_obj_t *post_filter_objs; + size_t post_filter_objs_len; + biquad_filter_state *post_filter_states; + int8_t *buffer[2]; uint8_t last_buf_idx; uint32_t buffer_len; // max buffer in bytes From 31158c0837417e15bf6567452cef2f7558e44554 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 14 Aug 2026 09:40:51 -0500 Subject: [PATCH 10/20] Removing trailing whitespace --- shared-module/audiofilters/Filter.c | 2 +- shared-module/audiofreeverb/Freeverb.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index 5ccd7c8a936..b98aa0aa887 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -170,7 +170,7 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o // Tick biquad filters audiofilters_tick_filters(self->filter_objs, self->filter_objs_len); - + if (self->base.samples_signed) { memset(word_buffer, 0, length * (self->base.bits_per_sample / 8)); } else { diff --git a/shared-module/audiofreeverb/Freeverb.c b/shared-module/audiofreeverb/Freeverb.c index 4bea25e9807..3d41eb0ab3f 100644 --- a/shared-module/audiofreeverb/Freeverb.c +++ b/shared-module/audiofreeverb/Freeverb.c @@ -319,7 +319,7 @@ audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_free // Apply filters as Pre-EQ input = (int16_t)audiofilters_process_filters(self->pre_filter_objs, self->pre_filter_objs_len, self->pre_filter_states, self->base.channel_count, n % self->base.channel_count, sample_word); - + input = synthio_sat16((int32_t)input * 8738, 17); // Initial input scaled down so we can add reverb sum = 0; From 3ce41e2b89bc494b1542f444d08c9f5c5d97f224 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 14 Aug 2026 18:05:27 -0500 Subject: [PATCH 11/20] Add optimization flags to bradanlanestudio_explorer_rp2040 --- .../boards/bradanlanestudio_explorer_rp2040/mpconfigboard.mk | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/mpconfigboard.mk b/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/mpconfigboard.mk index 14f27c07b99..edb4f3bb3d8 100644 --- a/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/mpconfigboard.mk +++ b/ports/raspberrypi/boards/bradanlanestudio_explorer_rp2040/mpconfigboard.mk @@ -15,6 +15,9 @@ EXTERNAL_FLASH_DEVICES = "GD25Q64C" CIRCUITPY__EVE = 1 +# The default is -O3. +OPTIMIZATION_FLAGS = -Os + FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_asyncio FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_Bitmap_Font FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_Display_Shapes From d8f175b8e9fe2501c1d412029570183d680eef7c Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 14 Aug 2026 18:06:17 -0500 Subject: [PATCH 12/20] Disable msgpack on meowbit_v121 --- ports/stm/boards/meowbit_v121/mpconfigboard.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/stm/boards/meowbit_v121/mpconfigboard.mk b/ports/stm/boards/meowbit_v121/mpconfigboard.mk index 419f073b5ea..271c4cd775c 100644 --- a/ports/stm/boards/meowbit_v121/mpconfigboard.mk +++ b/ports/stm/boards/meowbit_v121/mpconfigboard.mk @@ -31,6 +31,7 @@ CIRCUITPY_EPAPERDISPLAY = 0 CIRCUITPY_FRAMEBUFFERIO = 0 CIRCUITPY_I2CDISPLAYBUS = 0 CIRCUITPY_KEYPAD_DEMUX = 0 +CIRCUITPY_MSGPACK = 0 CIRCUITPY_PIXELMAP = 0 CIRCUITPY_SHARPDISPLAY = 0 CIRCUITPY_TILEPALETTEMAPPER = 0 From 8085a9633cc6c2aa1fb0bbd8193c3abd62cdf9d2 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Mon, 17 Aug 2026 16:45:32 -0500 Subject: [PATCH 13/20] Require that filter object is `None`, `synthio.Biquad` or `tuple` of `synthio.Biquad` (disallow `list`) --- shared-module/audiofilters/__init__.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/shared-module/audiofilters/__init__.c b/shared-module/audiofilters/__init__.c index 6435ec36e36..a663170de88 100644 --- a/shared-module/audiofilters/__init__.c +++ b/shared-module/audiofilters/__init__.c @@ -13,10 +13,7 @@ void audiofilters_assign_filters(mp_obj_t filter_in, mp_obj_t *filter_out, mp_ob if (filter_in == mp_const_none) { n_items = 0; *filter_objs = NULL; - } 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); + } else if (mp_obj_is_type(filter_in, &mp_type_tuple)) { 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)) { From 30e76fdc120557e422bf1d014c8bbf39607af6d5 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Mon, 17 Aug 2026 14:17:28 -0700 Subject: [PATCH 14/20] Add Clue test build for Zephyr --- .../autogen_board_info.toml | 129 ++++++++++++++++++ .../clue_nrf52840_zephyr/circuitpython.toml | 4 + .../boards/adafruit_clue_nrf52840_uf2.conf | 6 + .../boards/adafruit_clue_nrf52840_uf2.overlay | 49 +++++++ ports/zephyr-cp/boards/board_aliases.cmake | 1 + ports/zephyr-cp/common-hal/_bleio/Adapter.c | 12 ++ ports/zephyr-cp/cptools/zephyr2cp.py | 23 ++++ ports/zephyr-cp/debug.conf | 1 + ports/zephyr-cp/supervisor/usb.c | 39 +++++- ports/zephyr-cp/zephyr-config/west.yml | 2 +- 10 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/autogen_board_info.toml create mode 100644 ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml create mode 100644 ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.conf create mode 100644 ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.overlay diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/autogen_board_info.toml b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/autogen_board_info.toml new file mode 100644 index 00000000000..0afea836c10 --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/autogen_board_info.toml @@ -0,0 +1,129 @@ +# This file is autogenerated when a board is built. Do not edit. Do commit it to git. Other scripts use its info. +name = "Adafruit Industries LLC CLUE nRF52840 Express" + +[modules] +__future__ = true +_bleio = true # Zephyr board has _bleio +_eve = false +_pew = false +_pixelmap = false +_stage = false +adafruit_bus_device = true +adafruit_pixelbuf = false +aesio = true +alarm = false +analogbufio = false +analogio = false +atexit = false +audiobusio = false +audiocore = false +audiodelays = false +audiofilewriter = false +audiofilters = false +audiofreeverb = false +audioi2sin = false +audioio = false +audiomixer = false +audiomp3 = false +audiopwmio = false +audiospeed = false +aurora_epaper = false +bitbangio = false +bitmapfilter = true # Zephyr board has busio +bitmaptools = true # Zephyr board has busio +bitops = false +board = false +busdisplay = true # Zephyr board has busio +busio = true # Zephyr board has busio +camera = false +canio = false +codeop = false +countio = false +digitalio = true +displayio = true # Zephyr board has displayio +dotclockframebuffer = false +dualbank = false +epaperdisplay = true # Zephyr board has busio +floppyio = false +fontio = true # Zephyr board has busio +fourwire = true # Zephyr board has busio +framebufferio = true # Zephyr board has busio +frequencyio = false +getpass = true +gifio = true # Zephyr board has busio +gnss = false +hashlib = true +hostnetwork = false +i2cdisplaybus = true # Zephyr board has busio +i2cioexpander = false +i2ctarget = false +imagecapture = false +ipaddress = false +is31fl3741 = false +jpegio = true # Zephyr board has busio +keypad = false +keypad_demux = false +locale = false +lvfontio = true # Zephyr board has busio +math = true +max3421e = false +mcp4822 = false +mdns = false +memorymap = false +memorymonitor = false +microcontroller = true +mipidsi = false +msgpack = true +neopixel_write = false +nvm = true # Zephyr board has nvm +onewireio = false +os = true +paralleldisplaybus = false +ps2io = false +pulseio = false +pwmio = false +qrio = false +qspibus = false +rainbowio = true +random = true +rclcpy = false +rgbmatrix = false +rotaryio = true # Zephyr board has rotaryio +rtc = false +sdcardio = true # Zephyr board has busio +sdioio = false +sharpdisplay = true # Zephyr board has busio +socketpool = false +spitarget = false +ssl = false +storage = true # Zephyr board has flash +struct = true +supervisor = true +synthio = false +terminalio = true # Zephyr board has busio +tilepalettemapper = true # Zephyr board has busio +time = true +touchio = false +traceback = true +uheap = false +usb = false +usb_audio = false +usb_cdc = true +usb_hid = true +usb_host = false +usb_midi = false +usb_video = false +ustack = false +vectorio = true # Zephyr board has busio +warnings = true +watchdog = false +wifi = false +zephyr_display = true # Zephyr board has zephyr_display +zephyr_kernel = false +zlib = true +# extmod modules shared with MicroPython +asyncio = true +binascii = true +json = true +re = true +select = true diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml new file mode 100644 index 00000000000..2d7b8c3db48 --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/circuitpython.toml @@ -0,0 +1,4 @@ +CIRCUITPY_BUILD_EXTENSIONS = ["hex", "uf2"] +USB_VID=0x239A +USB_PID=0x8072 +NAME="CLUE nRF52840 Express" diff --git a/ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.conf b/ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.conf new file mode 100644 index 00000000000..cfa31e2fdcf --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.conf @@ -0,0 +1,6 @@ +CONFIG_USE_DT_CODE_PARTITION=y + +CONFIG_BOARD_SERIAL_BACKEND_CDC_ACM=n + +# Enable the ST7789V TFT via the Zephyr display subsystem +CONFIG_DISPLAY=y diff --git a/ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.overlay b/ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.overlay new file mode 100644 index 00000000000..405b65ec8eb --- /dev/null +++ b/ports/zephyr-cp/boards/adafruit_clue_nrf52840_uf2.overlay @@ -0,0 +1,49 @@ +/ { + chosen { + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-mon-uart = &uart0; + zephyr,bt-c2h-uart = &uart0; + }; +}; + +&zephyr_udc0 { + /delete-node/ board_cdc_acm_uart; +}; + + +&gd25q16 { + /delete-node/ partitions; +}; + +/delete-node/ &storage_partition; +/delete-node/ &code_partition; + +&flash0 { + partitions { + code_partition: partition@26000 { + compatible = "zephyr,mapped-partition"; + label = "Application"; + reg = <0x00026000 0x000c4000>; + }; + + storage_partition: partition@ea000 { + compatible = "zephyr,mapped-partition"; + label = "storage"; + reg = <0x000ea000 0x00008000>; + }; + + nvm_partition: partition@f2000 { + compatible = "zephyr,mapped-partition"; + label = "nvm"; + reg = <0x000f2000 0x00002000>; + }; + }; +}; + +&uart0 { + status = "okay"; +}; + +#include "../app.overlay" diff --git a/ports/zephyr-cp/boards/board_aliases.cmake b/ports/zephyr-cp/boards/board_aliases.cmake index 548070279c2..f9eaca45d8c 100644 --- a/ports/zephyr-cp/boards/board_aliases.cmake +++ b/ports/zephyr-cp/boards/board_aliases.cmake @@ -28,6 +28,7 @@ endmacro() cp_board_alias(pca10056 nrf52840dk/nrf52840) cp_board_alias(adafruit_feather_nrf52840_zephyr adafruit_feather_nrf52840/nrf52840/uf2) +cp_board_alias(adafruit_clue_nrf52840_zephyr adafruit_clue_nrf52840/nrf52840/uf2) cp_board_alias(adafruit_feather_nrf52840_sense_zephyr adafruit_feather_nrf52840/nrf52840/sense/uf2) cp_board_alias(adafruit_feather_rp2040_zephyr adafruit_feather_rp2040/rp2040) cp_board_alias(renesas_ek_ra6m5 ek_ra6m5) diff --git a/ports/zephyr-cp/common-hal/_bleio/Adapter.c b/ports/zephyr-cp/common-hal/_bleio/Adapter.c index ac2b1948eb7..44abea55416 100644 --- a/ports/zephyr-cp/common-hal/_bleio/Adapter.c +++ b/ports/zephyr-cp/common-hal/_bleio/Adapter.c @@ -305,6 +305,7 @@ bool common_hal_bleio_adapter_get_enabled(bleio_adapter_obj_t *self) { } mp_int_t common_hal_bleio_adapter_get_tx_power(bleio_adapter_obj_t *self) { + #if defined(CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL) struct bt_hci_cp_vs_read_tx_power_level *cp; struct bt_hci_rp_vs_read_tx_power_level *rp; struct net_buf *buf, *rsp = NULL; @@ -326,9 +327,15 @@ mp_int_t common_hal_bleio_adapter_get_tx_power(bleio_adapter_obj_t *self) { int8_t power = rp->tx_power_level; net_buf_unref(rsp); return power; + #else + // Controller doesn't support dynamic TX power control; return default. + (void)self; + return 0; + #endif } void common_hal_bleio_adapter_set_tx_power(bleio_adapter_obj_t *self, mp_int_t tx_power) { + #if defined(CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL) struct bt_hci_cp_vs_write_tx_power_level *cp; struct net_buf *buf, *rsp = NULL; @@ -347,6 +354,11 @@ void common_hal_bleio_adapter_set_tx_power(bleio_adapter_obj_t *self, mp_int_t t } net_buf_unref(rsp); + #else + // Controller doesn't support dynamic TX power control; silently ignore. + (void)self; + (void)tx_power; + #endif } bleio_address_obj_t *common_hal_bleio_adapter_get_address(bleio_adapter_obj_t *self) { diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index a9b5ecf6e1d..a39b837e978 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -99,6 +99,29 @@ "D12", "D13", ], + "adafruit-clue": [ + ["P0", "D0", "A2", "RX"], + ["P1", "D1", "A3", "TX"], + ["P2", "D2", "A4"], + ["P3", "D3", "A5"], + ["P4", "D4", "A6"], + ["P5", "D5", "BUTTON_A"], + ["P6", "D6"], + ["P7", "D7"], + ["P8", "D8"], + ["P9", "D9"], + ["P10", "D10", "A7"], + ["P11", "D11", "BUTTON_B"], + ["P12", "D12", "A0"], + ["P13", "D13", "SCK"], + ["P14", "D14", "MISO"], + ["P15", "D15", "MOSI"], + ["P16", "D16", "A1"], + ["P17", "D17", "L", "LED"], + ["P18", "D18", "NEOPIXEL"], + ["P19", "D19", "SCL"], + ["P20", "D20", "SDA"], + ], "nordic,expansion-board-header": [ "P1_04", "P1_05", diff --git a/ports/zephyr-cp/debug.conf b/ports/zephyr-cp/debug.conf index 625ef45198f..ab6ae95d416 100644 --- a/ports/zephyr-cp/debug.conf +++ b/ports/zephyr-cp/debug.conf @@ -28,6 +28,7 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 CONFIG_MAIN_STACK_SIZE=32768 CONFIG_ISR_STACK_SIZE=4096 CONFIG_UDC_DWC2_STACK_SIZE=4096 +CONFIG_UDC_NRF_THREAD_STACK_SIZE=4096 CONFIG_USBD_THREAD_STACK_SIZE=4096 CONFIG_USBD_MSC_STACK_SIZE=4096 CONFIG_IDLE_STACK_SIZE=1024 diff --git a/ports/zephyr-cp/supervisor/usb.c b/ports/zephyr-cp/supervisor/usb.c index bb7f15f056a..6d3bf3286dd 100644 --- a/ports/zephyr-cp/supervisor/usb.c +++ b/ports/zephyr-cp/supervisor/usb.c @@ -20,6 +20,9 @@ #include #include #include +#if DT_HAS_COMPAT_STATUS_OKAY(nordic_nrf_usbd) +#include +#endif #include "shared-module/storage/__init__.h" #include "supervisor/filesystem.h" @@ -396,19 +399,49 @@ void usb_init(void) { printk("usbd_init\n"); err = usbd_init(&main_usbd); if (err) { - LOG_ERR("Failed to initialize device support"); + LOG_ERR("Failed to initialize device support (%d)", err); return; } printk("USB initialized\n"); if (!usbd_can_detect_vbus(&main_usbd)) { + /* No VBUS detection — just enable unconditionally. */ err = usbd_enable(&main_usbd); if (err) { - LOG_ERR("Failed to enable device support"); + LOG_ERR("Failed to enable device support (%d)", err); return; } - printk("usbd enabled\n"); + } else { + #if DT_HAS_COMPAT_STATUS_OKAY(nordic_nrf_usbd) + /* + * On nRF52 USBD, the UDC driver enables VBUS detection interrupts + * inside usbd_init(). If USB is already plugged in, the VBUS_READY + * event arrives before usbd_init() sets status.initialized = true, + * causing a __ASSERT in usbd_thread (with CONFIG_ASSERT=y) that kills + * the event-processing thread. Our _msg_cb then never receives + * VBUS_READY, and USB never enumerates. + * + * Work around the race by checking the current VBUS state directly. + * udc_nrf_enable() does not require VBUS to be present, so it's safe + * to call usbd_enable() if VBUS is already up. + */ + nrfx_power_usb_state_t vbus = nrfx_power_usbstatus_get(); + if (vbus != NRFX_POWER_USB_STATE_DISCONNECTED) { + err = usbd_enable(&main_usbd); + if (err) { + LOG_ERR("Failed to enable device support (%d)", err); + return; + } + } + #else + /* + * On nRF54 USBHS, VBUS events arrive via NRFS IPC which has a natural + * startup delay, so the assert race doesn't occur. Additionally, + * udc_enable() blocks until VBUS is ready, so we must NOT call + * usbd_enable() here — let _msg_cb handle it on VBUS_READY. + */ + #endif } } diff --git a/ports/zephyr-cp/zephyr-config/west.yml b/ports/zephyr-cp/zephyr-config/west.yml index cff4be3993c..32394fe224c 100644 --- a/ports/zephyr-cp/zephyr-config/west.yml +++ b/ports/zephyr-cp/zephyr-config/west.yml @@ -8,6 +8,6 @@ manifest: path: modules/bsim_hw_models/nrf_hw_models - name: zephyr url: https://github.com/adafruit/zephyr - revision: 3c402d80bd86628065f2a7d2af4a55a42556b08e + revision: 62e7a3764b652fff733cee43f23f82217403a51d clone-depth: 100 import: true From d8847b075783a912a1be1c0c116f667e7bb81965 Mon Sep 17 00:00:00 2001 From: Alec Delaney <89490472+tekktrik@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:13:46 -0400 Subject: [PATCH 15/20] Update issue-labeled-ping to v3 --- .github/workflows/notify-on-issue-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/notify-on-issue-label.yml b/.github/workflows/notify-on-issue-label.yml index 2b229156811..2465851e35b 100644 --- a/.github/workflows/notify-on-issue-label.yml +++ b/.github/workflows/notify-on-issue-label.yml @@ -10,7 +10,7 @@ jobs: permissions: issues: write steps: - - uses: tekktrik/issue-labeled-ping@v2 + - uses: tekktrik/issue-labeled-ping@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} user: v923z From 8921d34efc9f6de433528c92920cbe7057207f49 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Tue, 18 Aug 2026 15:34:22 -0400 Subject: [PATCH 16/20] shared-bindings/traceback/__init__.c: check args more carefully; enhance doc --- locale/circuitpython.pot | 4 +++ shared-bindings/traceback/__init__.c | 50 ++++++++++++++++----------- tests/circuitpython/traceback_test.py | 16 +++++++++ 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 543a6ecdfa3..130bb695683 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -4189,6 +4189,10 @@ msgstr "" msgid "timestamp out of range for platform time_t" msgstr "" +#: shared-bindings/traceback/__init__.c +msgid "Supply both or neither of %q and %q" +msgstr "" + #: shared-bindings/traceback/__init__.c msgid "file write is not available" msgstr "" diff --git a/shared-bindings/traceback/__init__.c b/shared-bindings/traceback/__init__.c index ca1e6fe976a..651ec2c0142 100644 --- a/shared-bindings/traceback/__init__.c +++ b/shared-bindings/traceback/__init__.c @@ -36,10 +36,18 @@ static void traceback_exception_common(bool is_print_exception, mp_print_t *prin mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); mp_obj_t value = args[ARG_value].u_obj; + mp_obj_t tb_obj = args[ARG_tb].u_obj; + + // Both or neither of value and tb must be supplied. + if ((value != MP_OBJ_NULL && tb_obj == MP_OBJ_NULL) || + (value == MP_OBJ_NULL && tb_obj != MP_OBJ_NULL)) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Supply both or neither of %q and %q"), MP_QSTR_value, MP_QSTR_tb); + } + if (value == MP_OBJ_NULL) { value = args[ARG_exc].u_obj; } - mp_obj_t tb_obj = args[ARG_tb].u_obj; + mp_obj_t limit_obj = args[ARG_limit].u_obj; #if MICROPY_CPYTHON_EXCEPTION_CHAIN bool chain = args[ARG_chain].u_bool; @@ -105,21 +113,21 @@ static void traceback_exception_common(bool is_print_exception, mp_print_t *prin //| def format_exception( //| exc: BaseException | Type[BaseException], //| /, -//| value: Optional[BaseException] = None, +//| value: BaseException, //| tb: Optional[TracebackType] = None, //| limit: Optional[int] = None, //| chain: Optional[bool] = True, //| ) -> List[str]: //| """Format a stack trace and the exception information. //| -//| If the exception value is passed in ``exc``, then this exception value and its -//| associated traceback are used. This is compatible with CPython 3.10 and newer. +//| If the exception value is passed in ``exc``, and ``value`` and ``tb`` are not supplied, +//| then this exception value and its associated traceback are used. +//| This is compatible with CPython 3.10 and newer. //| -//| If the exception value is passed in ``value``, then any value passed in for -//| ``exc`` is ignored. ``value`` is used as the exception value and the -//| traceback in the ``tb`` argument is used. In this case, if ``tb`` is None, -//| no traceback will be shown. This is compatible with CPython 3.5 and -//| newer. +//| If the exception value is passed in ``value``, then both ``value`` and ``tb`` must be supplied. +//| Any value passed in for ``exc`` is *ignored*. ``value`` is used as the exception value and the +//| traceback in the ``tb`` argument is used. In this case, if ``tb`` is `None`, +//| no traceback will be shown. This is compatible with CPython 3.5 and newer. //| //| The arguments have the same meaning as the corresponding arguments //| to print_exception(). The return value is a list of strings, each @@ -128,8 +136,8 @@ static void traceback_exception_common(bool is_print_exception, mp_print_t *prin //| printed as does print_exception(). //| //| :param exc: The exception. Must be an instance of `BaseException`. Unused if value is specified. -//| :param value: If specified, is used in place of ``exc``. -//| :param TracebackType tb: When value is alsp specified, ``tb`` is used in place of the exception's own traceback. If `None`, the traceback will not be printed. +//| :param value: If specified, is used in place of ``exc``, and ``tb`` must also be specified. +//| :param tb: When ``value`` is also specified, ``tb`` is used in place of the exception's own traceback. If `None`, the traceback will not be printed. //| :param int limit: Print up to limit stack trace entries (starting from the caller’s frame) if limit is positive. //| Otherwise, print the last ``abs(limit)`` entries. If limit is omitted or None, all entries are printed. //| :param bool chain: If `True` then chained exceptions will be printed. @@ -150,7 +158,7 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(traceback_format_exception_obj, 0, traceback_f //| def print_exception( //| exc: BaseException | Type[BaseException], //| /, -//| value: Optional[BaseException] = None, +//| value: BaseException, //| tb: Optional[TracebackType] = None, //| limit: Optional[int] = None, //| file: Optional[io.FileIO] = None, @@ -158,18 +166,18 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(traceback_format_exception_obj, 0, traceback_f //| ) -> None: //| """Prints exception information and stack trace entries. //| -//| If the exception value is passed in ``exc``, then this exception value and its -//| associated traceback are used. This is compatible with CPython 3.10 and newer. +//| If the exception value is passed in ``exc``, and ``value`` and ``tb`` are not supplied, +//| then this exception value and its associated traceback are used. +//| This is compatible with CPython 3.10 and newer. //| -//| If the exception value is passed in ``value``, then any value passed in for -//| ``exc`` is ignored. ``value`` is used as the exception value and the -//| traceback in the ``tb`` argument is used. In this case, if ``tb`` is None, -//| no traceback will be shown. This is compatible with CPython 3.5 and -//| newer. +//| If the exception value is passed in ``value``, then both ``value`` and ``tb`` must be supplied. +//| Any value passed in for ``exc`` is *ignored*. ``value`` is used as the exception value and the +//| traceback in the ``tb`` argument is used. In this case, if ``tb`` is `None`, +//| no traceback will be shown. This is compatible with CPython 3.5 and newer. //| //| :param exc: The exception. Must be an instance of `BaseException`. Unused if value is specified. -//| :param value: If specified, is used in place of ``exc``. -//| :param tb: When value is alsp specified, ``tb`` is used in place of the exception's own traceback. If `None`, the traceback will not be printed. +//| :param value: If specified, is used in place of ``exc``, and ``tb`` must also be specified. +//| :param tb: When ``value`` is also specified, ``tb`` is used in place of the exception's own traceback. If `None`, the traceback will not be printed. //| :param int limit: Print up to limit stack trace entries (starting from the caller’s frame) if limit is positive. //| Otherwise, print the last ``abs(limit)`` entries. If limit is omitted or None, all entries are printed. //| :param io.FileIO file: If file is omitted or `None`, the output goes to `sys.stderr`; otherwise it should be an open diff --git a/tests/circuitpython/traceback_test.py b/tests/circuitpython/traceback_test.py index 17c54c857a9..71888437a22 100644 --- a/tests/circuitpython/traceback_test.py +++ b/tests/circuitpython/traceback_test.py @@ -23,6 +23,22 @@ def fun(): print("\nLimit=-1 Trace:") print("".join(traceback.format_exception(None, exc, exc.__traceback__, limit=-1)), end="") +# value and tb must both be supplied or neither +print() +try: + fun() +except Exception as exc: + try: + traceback.print_exception(None, value=exc) + print("Should have raised ValueError for missing tb arg") + except ValueError: + print("ValueError for missing tb arg, as expected") + try: + traceback.print_exception(None, tb=None) + print("Should have raised ValueError for missing value arg") + except ValueError: + print("ValueError for missing value arg, as expected") + class NonNativeException(Exception): pass From 513b9b699dfe00e33bdfbf8fb27f3c02750f40cf Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Tue, 18 Aug 2026 12:46:55 -0700 Subject: [PATCH 17/20] Revert TX power changes They don't work on builds with separate controllers like the nRF5340. --- ports/zephyr-cp/common-hal/_bleio/Adapter.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ports/zephyr-cp/common-hal/_bleio/Adapter.c b/ports/zephyr-cp/common-hal/_bleio/Adapter.c index 44abea55416..ac2b1948eb7 100644 --- a/ports/zephyr-cp/common-hal/_bleio/Adapter.c +++ b/ports/zephyr-cp/common-hal/_bleio/Adapter.c @@ -305,7 +305,6 @@ bool common_hal_bleio_adapter_get_enabled(bleio_adapter_obj_t *self) { } mp_int_t common_hal_bleio_adapter_get_tx_power(bleio_adapter_obj_t *self) { - #if defined(CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL) struct bt_hci_cp_vs_read_tx_power_level *cp; struct bt_hci_rp_vs_read_tx_power_level *rp; struct net_buf *buf, *rsp = NULL; @@ -327,15 +326,9 @@ mp_int_t common_hal_bleio_adapter_get_tx_power(bleio_adapter_obj_t *self) { int8_t power = rp->tx_power_level; net_buf_unref(rsp); return power; - #else - // Controller doesn't support dynamic TX power control; return default. - (void)self; - return 0; - #endif } void common_hal_bleio_adapter_set_tx_power(bleio_adapter_obj_t *self, mp_int_t tx_power) { - #if defined(CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL) struct bt_hci_cp_vs_write_tx_power_level *cp; struct net_buf *buf, *rsp = NULL; @@ -354,11 +347,6 @@ void common_hal_bleio_adapter_set_tx_power(bleio_adapter_obj_t *self, mp_int_t t } net_buf_unref(rsp); - #else - // Controller doesn't support dynamic TX power control; silently ignore. - (void)self; - (void)tx_power; - #endif } bleio_address_obj_t *common_hal_bleio_adapter_get_address(bleio_adapter_obj_t *self) { From 06cbba6b6fdc31ee3eb8a3d66a216a9daec845e9 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Tue, 18 Aug 2026 14:50:56 -0500 Subject: [PATCH 18/20] Create filter chain struct --- shared-module/audiodelays/Echo.c | 24 ++++++------- shared-module/audiodelays/Echo.h | 7 ++-- shared-module/audiofilters/Filter.c | 21 +++++------ shared-module/audiofilters/Filter.h | 7 ++-- shared-module/audiofilters/__init__.c | 48 +++++++++++++++----------- shared-module/audiofilters/__init__.h | 16 ++++++--- shared-module/audiofreeverb/Freeverb.c | 27 +++++++-------- shared-module/audiofreeverb/Freeverb.h | 13 ++----- 8 files changed, 78 insertions(+), 85 deletions(-) diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c index cdf34541972..a57ed237c48 100644 --- a/shared-module/audiodelays/Echo.c +++ b/shared-module/audiodelays/Echo.c @@ -5,7 +5,6 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/audiodelays/Echo.h" #include "shared-bindings/audiocore/__init__.h" -#include "shared-module/audiofilters/__init__.h" #include #include "py/runtime.h" @@ -109,11 +108,10 @@ void common_hal_audiodelays_echo_construct(audiodelays_echo_obj_t *self, uint32_ void common_hal_audiodelays_echo_deinit(audiodelays_echo_obj_t *self) { audiosample_mark_deinit(&self->base); + audiofilters_deinit_filter_chain(&self->filter); self->echo_buffer = NULL; self->buffer[0] = NULL; self->buffer[1] = NULL; - self->filter = mp_const_none; - self->filter_states = NULL; } mp_obj_t common_hal_audiodelays_echo_get_delay_ms(audiodelays_echo_obj_t *self) { @@ -172,11 +170,11 @@ void common_hal_audiodelays_echo_set_decay(audiodelays_echo_obj_t *self, mp_obj_ } mp_obj_t common_hal_audiodelays_echo_get_filter(audiodelays_echo_obj_t *self) { - return self->filter; + return self->filter.obj; } void common_hal_audiodelays_echo_set_filter(audiodelays_echo_obj_t *self, mp_obj_t filter_in) { - audiofilters_assign_filters(filter_in, &self->filter, &self->filter_objs, &self->filter_objs_len, &self->filter_states, self->base.channel_count); + audiofilters_assign_filter_chain(&self->filter, filter_in, self->base.channel_count); } mp_obj_t common_hal_audiodelays_echo_get_mix(audiodelays_echo_obj_t *self) { @@ -211,7 +209,7 @@ void audiodelays_echo_reset_buffer(audiodelays_echo_obj_t *self, memset(self->buffer[1], 0, self->buffer_len); memset(self->echo_buffer, 0, self->max_echo_buffer_len); - audiofilters_reset_filters(self->filter_states, self->filter_objs_len, self->base.channel_count); + audiofilters_reset_filter_chain(&self->filter, self->base.channel_count); } bool common_hal_audiodelays_echo_get_playing(audiodelays_echo_obj_t *self) { @@ -305,7 +303,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * } // Tick biquad filters - audiofilters_tick_filters(self->filter_objs, self->filter_objs_len); + audiofilters_tick_filter_chain(&self->filter); uint32_t echo_buf_len = self->echo_buffer_len / sizeof(uint16_t); uint32_t max_echo_buf_len = (self->max_echo_buffer_len >> (self->base.channel_count - 1)) / sizeof(uint16_t); @@ -342,12 +340,12 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) { word = (int16_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)audiofilters_process_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); } } else { echo = echo_buffer[echo_buffer_pos + echo_buffer_offset]; word = (int16_t)(echo * decay); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)audiofilters_process_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); } word = (int16_t)(echo * MIN(mix, MICROPY_FLOAT_CONST(1.0))); @@ -427,11 +425,11 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * for (uint32_t j = echo_buffer_pos >> 8; j < next_buffer_pos >> 8; j++) { word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word); word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int16_t)audiofilters_process_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); } } else { word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int16_t)audiofilters_process_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); } } else { if (self->freq_shift) { @@ -439,12 +437,12 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * word = (int32_t)(echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay + sample_word); // Do not have mix_down for 8 bit so just hard cap samples into 1 byte word = MIN(MAX(word, -128), 127); - echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int8_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); + echo_buffer[(j % echo_buf_len) + echo_buffer_offset] = (int8_t)audiofilters_process_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); } } else { // Do not have mix_down for 8 bit so just hard cap samples into 1 byte word = MIN(MAX(word, -128), 127); - echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int8_t)audiofilters_process_filters(self->filter_objs, self->filter_objs_len, self->filter_states, self->base.channel_count, !!echo_buffer_offset, word); + echo_buffer[echo_buffer_pos++ + echo_buffer_offset] = (int8_t)audiofilters_process_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); } } diff --git a/shared-module/audiodelays/Echo.h b/shared-module/audiodelays/Echo.h index 94c9ee4d2de..be01563b85a 100644 --- a/shared-module/audiodelays/Echo.h +++ b/shared-module/audiodelays/Echo.h @@ -8,6 +8,7 @@ #include "py/obj.h" #include "shared-module/audiocore/__init__.h" +#include "shared-module/audiofilters/__init__.h" #include "shared-module/synthio/__init__.h" #include "shared-module/synthio/block.h" #include "shared-module/synthio/Biquad.h" @@ -21,13 +22,9 @@ typedef struct { mp_float_t current_delay_ms; mp_float_t sample_ms; synthio_block_slot_t decay; - mp_obj_t filter; + audiofilters_filter_chain_t filter; synthio_block_slot_t mix; - mp_obj_t *filter_objs; - size_t filter_objs_len; - biquad_filter_state *filter_states; - int8_t *buffer[2]; uint8_t last_buf_idx; uint32_t buffer_len; // max buffer in bytes diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index b98aa0aa887..a5351edecc4 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -5,7 +5,6 @@ // SPDX-License-Identifier: MIT #include "shared-bindings/audiofilters/Filter.h" #include "shared-bindings/audiocore/__init__.h" -#include "shared-module/audiofilters/__init__.h" #include "shared-module/synthio/Biquad.h" #include @@ -60,19 +59,17 @@ void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self, void common_hal_audiofilters_filter_deinit(audiofilters_filter_obj_t *self) { audiosample_mark_deinit(&self->base); + audiofilters_deinit_filter_chain(&self->filter); self->buffer[0] = NULL; self->buffer[1] = NULL; - self->filter = mp_const_none; - self->filter_buffer = NULL; - self->filter_states = NULL; } void common_hal_audiofilters_filter_set_filter(audiofilters_filter_obj_t *self, mp_obj_t filter_in) { - audiofilters_assign_filters(filter_in, &self->filter, &self->filter_objs, &self->filter_objs_len, &self->filter_states, self->base.channel_count); + audiofilters_assign_filter_chain(&self->filter, filter_in, self->base.channel_count); } mp_obj_t common_hal_audiofilters_filter_get_filter(audiofilters_filter_obj_t *self) { - return self->filter; + return self->filter.obj; } mp_obj_t common_hal_audiofilters_filter_get_mix(audiofilters_filter_obj_t *self) { @@ -91,7 +88,7 @@ void audiofilters_filter_reset_buffer(audiofilters_filter_obj_t *self, memset(self->buffer[1], 0, self->buffer_len); memset(self->filter_buffer, 0, SYNTHIO_MAX_DUR * self->base.channel_count * sizeof(int32_t)); - audiofilters_reset_filters(self->filter_states, self->filter_objs_len, self->base.channel_count); + audiofilters_reset_filter_chain(&self->filter, self->base.channel_count); } bool common_hal_audiofilters_filter_get_playing(audiofilters_filter_obj_t *self) { @@ -169,7 +166,7 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o (void)synthio_block_slot_get(&self->mix); // Tick biquad filters - audiofilters_tick_filters(self->filter_objs, self->filter_objs_len); + audiofilters_tick_filter_chain(&self->filter); if (self->base.samples_signed) { memset(word_buffer, 0, length * (self->base.bits_per_sample / 8)); @@ -198,7 +195,7 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o shared_bindings_synthio_lfo_tick(self->base.sample_rate, n / self->base.channel_count); mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); - if (mix <= MICROPY_FLOAT_CONST(0.01) || !self->filter_states) { // if mix is zero pure sample only or no biquad filter objects are provided + if (mix <= MICROPY_FLOAT_CONST(0.01) || !self->filter.states) { // if mix is zero pure sample only or no biquad filter objects are provided for (uint32_t i = 0; i < n; i++) { if (MP_LIKELY(self->base.bits_per_sample == 16)) { word_buffer[i] = sample_src[i]; @@ -228,11 +225,11 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o } // Process biquad filters - for (uint8_t j = 0; j < self->filter_objs_len; j++) { - mp_obj_t filter_obj = self->filter_objs[j]; + for (uint8_t j = 0; j < self->filter.objs_len; j++) { + mp_obj_t filter_obj = self->filter.objs[j]; common_hal_synthio_biquad_tick(filter_obj); for (uint8_t k = 0; k < self->base.channel_count; k++) { - synthio_biquad_filter_samples(filter_obj, &self->filter_states[j * self->base.channel_count + k], self->filter_buffer + k * SYNTHIO_MAX_DUR, n_samples); + synthio_biquad_filter_samples(filter_obj, &self->filter.states[j * self->base.channel_count + k], self->filter_buffer + k * SYNTHIO_MAX_DUR, n_samples); } } diff --git a/shared-module/audiofilters/Filter.h b/shared-module/audiofilters/Filter.h index e24827e44a8..6cb352d4273 100644 --- a/shared-module/audiofilters/Filter.h +++ b/shared-module/audiofilters/Filter.h @@ -9,6 +9,7 @@ #include "shared-bindings/synthio/Biquad.h" #include "shared-module/audiocore/__init__.h" +#include "shared-module/audiofilters/__init__.h" #include "shared-module/synthio/__init__.h" #include "shared-module/synthio/block.h" #include "shared-module/synthio/Biquad.h" @@ -17,13 +18,9 @@ extern const mp_obj_type_t audiofilters_filter_type; typedef struct { audiosample_base_t base; - mp_obj_t filter; + audiofilters_filter_chain_t filter; synthio_block_slot_t mix; - mp_obj_t *filter_objs; - size_t filter_objs_len; - biquad_filter_state *filter_states; - int8_t *buffer[2]; uint8_t last_buf_idx; uint32_t buffer_len; // max buffer in bytes diff --git a/shared-module/audiofilters/__init__.c b/shared-module/audiofilters/__init__.c index a663170de88..4b2ec0ed063 100644 --- a/shared-module/audiofilters/__init__.c +++ b/shared-module/audiofilters/__init__.c @@ -6,13 +6,13 @@ #include "shared-module/audiofilters/__init__.h" -void audiofilters_assign_filters(mp_obj_t filter_in, mp_obj_t *filter_out, mp_obj_t **filter_objs, size_t *filter_objs_len, biquad_filter_state **filter_states, uint8_t channel_count) { +void audiofilters_assign_filter_chain(audiofilters_filter_chain_t *self, mp_obj_t filter_in, uint8_t channel_count) { size_t n_items; mp_obj_t *items; if (filter_in == mp_const_none) { n_items = 0; - *filter_objs = NULL; + items = NULL; } else if (mp_obj_is_type(filter_in, &mp_type_tuple)) { mp_obj_tuple_get(filter_in, &n_items, &items); for (size_t i = 0; i < n_items; i++) { @@ -25,46 +25,52 @@ void audiofilters_assign_filters(mp_obj_t filter_in, mp_obj_t *filter_out, mp_ob mp_obj_get_type(items[i])->name); } } - *filter_objs = items; } else { n_items = 1; if (!mp_obj_is_type(filter_in, &synthio_biquad_type_obj)) { mp_raise_TypeError_varg( MP_ERROR_TEXT("%q must be of type %q or %q, not %q"), - MP_QSTR_filter, MP_QSTR_Biquad, MP_QSTR_iterable, mp_obj_get_type(filter_in)->name); + MP_QSTR_filter, MP_QSTR_Biquad, MP_QSTR_tuple, mp_obj_get_type(filter_in)->name); } - *filter_objs = filter_out; + items = &self->obj; } // everything has been checked, so we can do the following without fear - *filter_out = filter_in; - *filter_states = m_renew(biquad_filter_state, - *filter_states, - *filter_objs_len * channel_count, + self->obj = filter_in; + self->states = m_renew(biquad_filter_state, + self->states, + self->objs_len * channel_count, n_items * channel_count); - *filter_objs_len = n_items; + self->objs = items; + self->objs_len = n_items; } -void audiofilters_reset_filters(biquad_filter_state *filter_states, size_t filter_objs_len, uint8_t channel_count) { - if (filter_states) { - for (uint8_t i = 0; i < filter_objs_len * channel_count; i++) { - synthio_biquad_filter_reset(&filter_states[i]); +void audiofilters_reset_filter_chain(audiofilters_filter_chain_t *self, uint8_t channel_count) { + if (self->states) { + for (uint8_t i = 0; i < self->objs_len * channel_count; i++) { + synthio_biquad_filter_reset(&self->states[i]); } } } -void audiofilters_tick_filters(mp_obj_t *filter_objs, size_t filter_objs_len) { - for (uint8_t j = 0; j < filter_objs_len; j++) { - common_hal_synthio_biquad_tick(filter_objs[j]); +void audiofilters_tick_filter_chain(audiofilters_filter_chain_t *self) { + for (uint8_t j = 0; j < self->objs_len; j++) { + common_hal_synthio_biquad_tick(self->objs[j]); } } -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) { +int32_t audiofilters_process_filter_chain(audiofilters_filter_chain_t *self, uint8_t channel_count, uint8_t channel, int32_t word) { // Process biquad filters - for (uint8_t j = 0; j < filter_objs_len; j++) { - mp_obj_t filter_obj = filter_objs[j]; - word = synthio_biquad_filter_sample(filter_obj, &filter_states[j * channel_count + channel], word); + for (uint8_t j = 0; j < self->objs_len; j++) { + word = synthio_biquad_filter_sample(self->objs[j], &self->states[j * channel_count + channel], word); } return word; } + +void audiofilters_deinit_filter_chain(audiofilters_filter_chain_t *self) { + self->obj = mp_const_none; + self->objs = NULL; + self->objs_len = 0; + self->states = NULL; +} diff --git a/shared-module/audiofilters/__init__.h b/shared-module/audiofilters/__init__.h index 067d04ef209..2a5a933d96f 100644 --- a/shared-module/audiofilters/__init__.h +++ b/shared-module/audiofilters/__init__.h @@ -9,7 +9,15 @@ #include "py/obj.h" #include "shared-module/synthio/Biquad.h" -void audiofilters_assign_filters(mp_obj_t filter_in, mp_obj_t *filter_out, mp_obj_t **filter_objs, size_t *filter_objs_len, biquad_filter_state **filter_states, uint8_t channel_count); -void audiofilters_reset_filters(biquad_filter_state *filter_states, size_t filter_objs_len, uint8_t channel_count); -void audiofilters_tick_filters(mp_obj_t *filter_objs, size_t filter_objs_len); -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); +typedef struct audiofilters_filter_chain { + mp_obj_t obj; + mp_obj_t *objs; + size_t objs_len; + biquad_filter_state *states; +} audiofilters_filter_chain_t; + +void audiofilters_assign_filter_chain(audiofilters_filter_chain_t *filter_chain, mp_obj_t filter_in, uint8_t channel_count); +void audiofilters_reset_filter_chain(audiofilters_filter_chain_t *filter_chain, uint8_t channel_count); +void audiofilters_tick_filter_chain(audiofilters_filter_chain_t *filter_chain); +int32_t audiofilters_process_filter_chain(audiofilters_filter_chain_t *filter_chain, uint8_t channel_count, uint8_t channel, int32_t word); +void audiofilters_deinit_filter_chain(audiofilters_filter_chain_t *self); diff --git a/shared-module/audiofreeverb/Freeverb.c b/shared-module/audiofreeverb/Freeverb.c index 3d41eb0ab3f..85aefb6fc61 100644 --- a/shared-module/audiofreeverb/Freeverb.c +++ b/shared-module/audiofreeverb/Freeverb.c @@ -9,7 +9,6 @@ // #include "shared-bindings/audiofreeverb/Freeverb.h" #include "shared-bindings/audiocore/__init__.h" -#include "shared-module/audiofilters/__init__.h" #include "shared-module/synthio/__init__.h" #include @@ -130,12 +129,10 @@ bool common_hal_audiofreeverb_freeverb_deinited(audiofreeverb_freeverb_obj_t *se void common_hal_audiofreeverb_freeverb_deinit(audiofreeverb_freeverb_obj_t *self) { audiosample_mark_deinit(&self->base); + audiofilters_deinit_filter_chain(&self->pre_filter); + audiofilters_deinit_filter_chain(&self->post_filter); self->buffer[0] = NULL; self->buffer[1] = NULL; - self->pre_filter = mp_const_none; - self->pre_filter_states = NULL; - self->post_filter = mp_const_none; - self->post_filter_states = NULL; } mp_obj_t common_hal_audiofreeverb_freeverb_get_roomsize(audiofreeverb_freeverb_obj_t *self) { @@ -176,19 +173,19 @@ void audiofreeverb_freeverb_get_damp_fixedpoint(mp_float_t n, int16_t *damp1, in } mp_obj_t common_hal_audiofreeverb_freeverb_get_pre_filter(audiofreeverb_freeverb_obj_t *self) { - return self->pre_filter; + return self->pre_filter.obj; } void common_hal_audiofreeverb_freeverb_set_pre_filter(audiofreeverb_freeverb_obj_t *self, mp_obj_t filter_in) { - audiofilters_assign_filters(filter_in, &self->pre_filter, &self->pre_filter_objs, &self->pre_filter_objs_len, &self->pre_filter_states, self->base.channel_count); + audiofilters_assign_filter_chain(&self->pre_filter, filter_in, self->base.channel_count); } mp_obj_t common_hal_audiofreeverb_freeverb_get_post_filter(audiofreeverb_freeverb_obj_t *self) { - return self->post_filter; + return self->post_filter.obj; } void common_hal_audiofreeverb_freeverb_set_post_filter(audiofreeverb_freeverb_obj_t *self, mp_obj_t filter_in) { - audiofilters_assign_filters(filter_in, &self->post_filter, &self->post_filter_objs, &self->post_filter_objs_len, &self->post_filter_states, self->base.channel_count); + audiofilters_assign_filter_chain(&self->post_filter, filter_in, self->base.channel_count); } mp_obj_t common_hal_audiofreeverb_freeverb_get_mix(audiofreeverb_freeverb_obj_t *self) { @@ -212,8 +209,8 @@ void audiofreeverb_freeverb_reset_buffer(audiofreeverb_freeverb_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); - audiofilters_reset_filters(self->pre_filter_states, self->pre_filter_objs_len, self->base.channel_count); - audiofilters_reset_filters(self->post_filter_states, self->post_filter_objs_len, self->base.channel_count); + audiofilters_reset_filter_chain(&self->pre_filter, self->base.channel_count); + audiofilters_reset_filter_chain(&self->post_filter, self->base.channel_count); } bool common_hal_audiofreeverb_freeverb_get_playing(audiofreeverb_freeverb_obj_t *self) { @@ -302,8 +299,8 @@ audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_free int16_t feedback = audiofreeverb_freeverb_get_roomsize_fixedpoint(roomsize); // Tick biquad filters - audiofilters_tick_filters(self->pre_filter_objs, self->pre_filter_objs_len); - audiofilters_tick_filters(self->post_filter_objs, self->post_filter_objs_len); + audiofilters_tick_filter_chain(&self->pre_filter); + audiofilters_tick_filter_chain(&self->post_filter); int16_t *sample_src = (int16_t *)self->sample_remaining_buffer; @@ -318,7 +315,7 @@ audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_free uint32_t channel_comb_offset = 0, channel_allpass_offset = 0; // Apply filters as Pre-EQ - input = (int16_t)audiofilters_process_filters(self->pre_filter_objs, self->pre_filter_objs_len, self->pre_filter_states, self->base.channel_count, n % self->base.channel_count, sample_word); + input = (int16_t)audiofilters_process_filter_chain(&self->pre_filter, self->base.channel_count, n % self->base.channel_count, sample_word); input = synthio_sat16((int32_t)input * 8738, 17); // Initial input scaled down so we can add reverb sum = 0; @@ -347,7 +344,7 @@ audioio_get_buffer_result_t audiofreeverb_freeverb_get_buffer(audiofreeverb_free } // Apply filters as Post-EQ - output = (int16_t)audiofilters_process_filters(self->post_filter_objs, self->post_filter_objs_len, self->post_filter_states, self->base.channel_count, n % self->base.channel_count, (int32_t)output); + output = (int16_t)audiofilters_process_filter_chain(&self->post_filter, self->base.channel_count, n % self->base.channel_count, (int32_t)output); word = output * 30; // Add some volume back don't have to saturate as next step will diff --git a/shared-module/audiofreeverb/Freeverb.h b/shared-module/audiofreeverb/Freeverb.h index 79a11e3490e..1a60a64d39e 100644 --- a/shared-module/audiofreeverb/Freeverb.h +++ b/shared-module/audiofreeverb/Freeverb.h @@ -8,6 +8,7 @@ #include "py/obj.h" #include "shared-module/audiocore/__init__.h" +#include "shared-module/audiofilters/__init__.h" #include "shared-module/synthio/__init__.h" #include "shared-module/synthio/block.h" #include "shared-module/synthio/Biquad.h" @@ -18,18 +19,10 @@ typedef struct { audiosample_base_t base; synthio_block_slot_t roomsize; synthio_block_slot_t damp; - mp_obj_t pre_filter; - mp_obj_t post_filter; + audiofilters_filter_chain_t pre_filter; + audiofilters_filter_chain_t post_filter; synthio_block_slot_t mix; - mp_obj_t *pre_filter_objs; - size_t pre_filter_objs_len; - biquad_filter_state *pre_filter_states; - - mp_obj_t *post_filter_objs; - size_t post_filter_objs_len; - biquad_filter_state *post_filter_states; - int8_t *buffer[2]; uint8_t last_buf_idx; uint32_t buffer_len; // max buffer in bytes From ec5d640a1280c4b281497568180b091c68ade0e4 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Tue, 18 Aug 2026 15:35:09 -0500 Subject: [PATCH 19/20] Reset `MixerVoice` buffers rather than sample --- shared-bindings/audiomixer/MixerVoice.h | 1 + shared-module/audiomixer/Mixer.c | 2 +- shared-module/audiomixer/MixerVoice.c | 16 +++++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/shared-bindings/audiomixer/MixerVoice.h b/shared-bindings/audiomixer/MixerVoice.h index d60820f7f57..89c87218e7c 100644 --- a/shared-bindings/audiomixer/MixerVoice.h +++ b/shared-bindings/audiomixer/MixerVoice.h @@ -14,6 +14,7 @@ extern const mp_obj_type_t audiomixer_mixervoice_type; void common_hal_audiomixer_mixervoice_construct(audiomixer_mixervoice_obj_t *self); void common_hal_audiomixer_mixervoice_set_parent(audiomixer_mixervoice_obj_t *self, audiomixer_mixer_obj_t *parent); void common_hal_audiomixer_mixervoice_play(audiomixer_mixervoice_obj_t *self, mp_obj_t sample, bool loop); +void common_hal_audiomixer_mixervoice_reset_buffer(audiomixer_mixervoice_obj_t *self); void common_hal_audiomixer_mixervoice_stop(audiomixer_mixervoice_obj_t *self); void common_hal_audiomixer_mixervoice_end(audiomixer_mixervoice_obj_t *self); mp_obj_t common_hal_audiomixer_mixervoice_get_level(audiomixer_mixervoice_obj_t *self); diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index 1177b6e0015..a6c4397e8bf 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -66,7 +66,7 @@ void audiomixer_mixer_reset_buffer(audiomixer_mixer_obj_t *self, bool single_channel_output, uint8_t channel) { for (uint8_t i = 0; i < self->voice_count; i++) { - common_hal_audiomixer_mixervoice_stop(self->voice[i]); + common_hal_audiomixer_mixervoice_reset_buffer(self->voice[i]); } } diff --git a/shared-module/audiomixer/MixerVoice.c b/shared-module/audiomixer/MixerVoice.c index 3a598516e7c..af015fc46f1 100644 --- a/shared-module/audiomixer/MixerVoice.c +++ b/shared-module/audiomixer/MixerVoice.c @@ -70,11 +70,17 @@ void common_hal_audiomixer_mixervoice_play(audiomixer_mixervoice_obj_t *self, mp self->sample = sample; self->loop = loop; - audiosample_reset_buffer(sample, false, 0); - audioio_get_buffer_result_t result = audiosample_get_buffer(sample, false, 0, (uint8_t **)&self->remaining_buffer, &self->buffer_length); - // Track length in terms of words. - self->buffer_length /= sizeof(uint32_t); - self->more_data = result == GET_BUFFER_MORE_DATA; + common_hal_audiomixer_mixervoice_reset_buffer(self); +} + +void common_hal_audiomixer_mixervoice_reset_buffer(audiomixer_mixervoice_obj_t *self) { + if (self->sample != NULL) { + audiosample_reset_buffer(self->sample, false, 0); + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, (uint8_t **)&self->remaining_buffer, &self->buffer_length); + // Track length in terms of words. + self->buffer_length /= sizeof(uint32_t); + self->more_data = result == GET_BUFFER_MORE_DATA; + } } bool common_hal_audiomixer_mixervoice_get_playing(audiomixer_mixervoice_obj_t *self) { From d2a38f58df88ea62c16e69a729ab3377f793cf9a Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 18 Aug 2026 19:12:54 -0700 Subject: [PATCH 20/20] socketpool: let ports raise gaierror with the real error code common_hal_socketpool_socketpool_raise_gaierror_noname() was the only way for a port to raise gaierror, so every getaddrinfo failure surfaced as EAI_NONAME no matter what actually went wrong. A lookup that failed because the resolver was unreachable looked identical to one for a name that does not exist. Add common_hal_socketpool_socketpool_raise_gaierror(int err), which puts the real code in args[0] where callers can act on it, and express the existing _noname() helper in terms of it. Existing callers are unchanged. No new translatable strings. EAI_NONAME keeps "Name or service not known"; anything else uses the existing generic "%q failure: %d" with getaddrinfo. zephyr-cp is the first user. Both of its resolver call sites had the real error in hand and discarded it: getaddrinfo() in SocketPool.c, and the host lookup behind socket.connect() in Socket.c. --- ports/zephyr-cp/common-hal/socketpool/Socket.c | 8 +++++++- .../common-hal/socketpool/SocketPool.c | 6 +++++- shared-bindings/socketpool/SocketPool.c | 18 +++++++++++++++--- shared-bindings/socketpool/SocketPool.h | 1 + 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/ports/zephyr-cp/common-hal/socketpool/Socket.c b/ports/zephyr-cp/common-hal/socketpool/Socket.c index dd2c94b0147..bb626857a98 100644 --- a/ports/zephyr-cp/common-hal/socketpool/Socket.c +++ b/ports/zephyr-cp/common-hal/socketpool/Socket.c @@ -27,6 +27,7 @@ #include #include +#include #include #define SOCKETPOOL_IP_STR_LEN 48 @@ -82,7 +83,12 @@ static void socketpool_resolve_host_or_throw(int family, int type, const char *h int error = zsock_getaddrinfo(hostname, service_buf, &hints, &result_i); if (error != 0 || result_i == NULL) { - common_hal_socketpool_socketpool_raise_gaierror_noname(); + // Report what the resolver said. A null result with no error is the + // one case that really is "not found". + if (error == 0) { + error = DNS_EAI_NONAME; + } + common_hal_socketpool_socketpool_raise_gaierror(error); } memcpy(addr, result_i->ai_addr, sizeof(struct sockaddr_storage)); diff --git a/ports/zephyr-cp/common-hal/socketpool/SocketPool.c b/ports/zephyr-cp/common-hal/socketpool/SocketPool.c index c9a3f008321..2df1af69187 100644 --- a/ports/zephyr-cp/common-hal/socketpool/SocketPool.c +++ b/ports/zephyr-cp/common-hal/socketpool/SocketPool.c @@ -20,6 +20,7 @@ #include #include +#include void common_hal_socketpool_socketpool_construct(socketpool_socketpool_obj_t *self, mp_obj_t radio) { bool is_wifi = false; @@ -104,7 +105,10 @@ mp_obj_t common_hal_socketpool_getaddrinfo_raise(socketpool_socketpool_obj_t *se struct zsock_addrinfo *res = NULL; int err = socketpool_getaddrinfo_common(host, port, &hints, &res); if (err != 0 || res == NULL) { - common_hal_socketpool_socketpool_raise_gaierror_noname(); + if (err == 0) { + err = DNS_EAI_NONAME; + } + common_hal_socketpool_socketpool_raise_gaierror(err); } nlr_buf_t nlr; diff --git a/shared-bindings/socketpool/SocketPool.c b/shared-bindings/socketpool/SocketPool.c index 1fc9e2c3335..f50a7a5fcfe 100644 --- a/shared-bindings/socketpool/SocketPool.c +++ b/shared-bindings/socketpool/SocketPool.c @@ -190,15 +190,27 @@ MP_DEFINE_CONST_OBJ_TYPE( ); MP_WEAK MP_NORETURN -void common_hal_socketpool_socketpool_raise_gaierror_noname(void) { +void common_hal_socketpool_socketpool_raise_gaierror(int err) { vstr_t vstr; mp_print_t print; vstr_init_print(&vstr, 64, &print); - mp_printf(&print, "%S", MP_ERROR_TEXT("Name or service not known")); + // Only EAI_NONAME means the name was not found. Other codes cover + // unrelated failures, such as an unsupported family, so use the generic + // message for those rather than one that would be misleading. + if (err == SOCKETPOOL_EAI_NONAME) { + mp_printf(&print, "%S", MP_ERROR_TEXT("Name or service not known")); + } else { + mp_cprintf(&print, MP_ERROR_TEXT("%q failure: %d"), MP_QSTR_getaddrinfo, err); + } mp_obj_t exc_args[] = { - MP_OBJ_NEW_SMALL_INT(SOCKETPOOL_EAI_NONAME), + MP_OBJ_NEW_SMALL_INT(err), mp_obj_new_str_from_vstr(&vstr), }; nlr_raise(mp_obj_new_exception_args(&mp_type_gaierror, MP_ARRAY_SIZE(exc_args), exc_args)); } + +MP_WEAK MP_NORETURN +void common_hal_socketpool_socketpool_raise_gaierror_noname(void) { + common_hal_socketpool_socketpool_raise_gaierror(SOCKETPOOL_EAI_NONAME); +} diff --git a/shared-bindings/socketpool/SocketPool.h b/shared-bindings/socketpool/SocketPool.h index 60d58372b91..1d0a3bbccff 100644 --- a/shared-bindings/socketpool/SocketPool.h +++ b/shared-bindings/socketpool/SocketPool.h @@ -26,5 +26,6 @@ bool socketpool_socket(socketpool_socketpool_obj_t *self, int proto, socketpool_socket_obj_t *sock); MP_NORETURN void common_hal_socketpool_socketpool_raise_gaierror_noname(void); +MP_NORETURN void common_hal_socketpool_socketpool_raise_gaierror(int err); mp_obj_t common_hal_socketpool_getaddrinfo_raise(socketpool_socketpool_obj_t *self, const char *host, int port, int family, int type, int proto, int flags);