From f876d04e2f3a37e1e7ceca475f282ce9c398eed5 Mon Sep 17 00:00:00 2001 From: Peter Bay Date: Fri, 18 Sep 2026 20:53:06 +0200 Subject: [PATCH] audio: apply mix and decay in fixed point, not floating point The mix, decay and level an effect works with are read once per block from their BlockInput and do not change for the rest of it, but four of the audio callbacks were applying them to every sample in floating point. On a part with no floating point unit each of those is an out-of-line call into a soft-float routine, inside a callback that has to keep up with the sample rate. Each parameter is now converted to Q15 where it is already being read, and the per-sample arithmetic is an integer multiply and a shift. audiodelays/Flanger, audiofilters/Phaser and audiofreeverb/Freeverb already work this way; this brings audiomixer, audiodelays/Echo, audiodelays/MultiTapDelay, audiodelays/Chorus and audiofilters/Filter in line with them. The scale is 1<<15, which is exactly unity. audiomixer had been dividing by 32767 instead, and its level reaches that code as a float in 0 to 1 scaled by 1<<15, so unity became a gain slightly above one: a full-scale sample came back clipped, and the portable branch disagreed with the ARM one it exists to mirror. The effects that already use fixed point scale by 32767, which errs the other way and by less, and they are left alone. --- shared-module/audiodelays/Chorus.c | 3 ++- shared-module/audiodelays/Echo.c | 20 ++++++++++-------- shared-module/audiodelays/MultiTapDelay.c | 10 ++++++--- shared-module/audiofilters/Filter.c | 9 +++++--- shared-module/audiomixer/Mixer.c | 25 +++++++++++------------ 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/shared-module/audiodelays/Chorus.c b/shared-module/audiodelays/Chorus.c index 7033df0e272..ccd33975948 100644 --- a/shared-module/audiodelays/Chorus.c +++ b/shared-module/audiodelays/Chorus.c @@ -248,6 +248,7 @@ audioio_get_buffer_result_t audiodelays_chorus_get_buffer(audiodelays_chorus_obj int32_t voices = (int32_t)MAX(synthio_block_slot_get(&self->voices), 1.0); int32_t mix_down_scale = SYNTHIO_MIX_DOWN_SCALE(voices); mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + int32_t mix_scaled = (int32_t)(mix * MICROPY_FLOAT_CONST(32768.0)); mp_float_t f_delay_ms = synthio_block_slot_get(&self->delay_ms); if (MICROPY_FLOAT_C_FUN(fabs)(self->current_delay_ms - f_delay_ms) >= self->sample_ms) { @@ -311,7 +312,7 @@ audioio_get_buffer_result_t audiodelays_chorus_get_buffer(audiodelays_chorus_obj } // Add original sample + effect - word = sample_word + (int32_t)(word * mix); + word = sample_word + ((word * mix_scaled) >> 15); word = synthio_mix_down_sample(word, 2); if (MP_LIKELY(self->base.bits_per_sample == 16)) { diff --git a/shared-module/audiodelays/Echo.c b/shared-module/audiodelays/Echo.c index 675c7cb5ebc..1e5d2ec0944 100644 --- a/shared-module/audiodelays/Echo.c +++ b/shared-module/audiodelays/Echo.c @@ -298,6 +298,11 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)) * MICROPY_FLOAT_CONST(2.0); mp_float_t decay = synthio_block_slot_get_limited(&self->decay, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + int32_t decay_scaled = (int32_t)(decay * MICROPY_FLOAT_CONST(32768.0)); + int32_t echo_scaled = (int32_t)(MIN(mix, MICROPY_FLOAT_CONST(1.0)) * MICROPY_FLOAT_CONST(32768.0)); + int32_t sample_scaled = (int32_t)(MIN(MICROPY_FLOAT_CONST(2.0) - mix, + MICROPY_FLOAT_CONST(1.0)) * MICROPY_FLOAT_CONST(32768.0)); + mp_float_t f_delay_ms = synthio_block_slot_get(&self->delay_ms); if (MICROPY_FLOAT_C_FUN(fabs)(self->current_delay_ms - f_delay_ms) >= self->sample_ms) { recalculate_delay(self, f_delay_ms); @@ -340,16 +345,16 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * next_buffer_pos = echo_buffer_pos + self->echo_buffer_rate; 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); + word = (int16_t)((echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay_scaled) >> 15); 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); + word = (int16_t)((echo * decay_scaled) >> 15); 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))); + word = (int16_t)((echo * echo_scaled) >> 15); if (MP_LIKELY(self->base.bits_per_sample == 16)) { word_buffer[i] = word; @@ -418,13 +423,13 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * next_buffer_pos = echo_buffer_pos + self->echo_buffer_rate; } else { echo = echo_buffer[echo_buffer_pos + echo_buffer_offset]; - word = (int32_t)(echo * decay + sample_word); + word = ((echo * decay_scaled) >> 15) + sample_word; } if (MP_LIKELY(self->base.bits_per_sample == 16)) { if (self->freq_shift) { 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 = ((echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay_scaled) >> 15) + 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_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); } @@ -435,7 +440,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * } else { if (self->freq_shift) { 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 = ((echo_buffer[(j % echo_buf_len) + echo_buffer_offset] * decay_scaled) >> 15) + 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_filter_chain(&self->filter, self->base.channel_count, !!echo_buffer_offset, word); @@ -447,8 +452,7 @@ audioio_get_buffer_result_t audiodelays_echo_get_buffer(audiodelays_echo_obj_t * } } - word = (int32_t)((sample_word * MIN(MICROPY_FLOAT_CONST(2.0) - mix, MICROPY_FLOAT_CONST(1.0))) - + (echo * MIN(mix, MICROPY_FLOAT_CONST(1.0)))); + word = ((sample_word * sample_scaled) >> 15) + ((echo * echo_scaled) >> 15); word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); if (MP_LIKELY(self->base.bits_per_sample == 16)) { diff --git a/shared-module/audiodelays/MultiTapDelay.c b/shared-module/audiodelays/MultiTapDelay.c index d4cda65b0ed..eb912c82778 100644 --- a/shared-module/audiodelays/MultiTapDelay.c +++ b/shared-module/audiodelays/MultiTapDelay.c @@ -374,6 +374,11 @@ audioio_get_buffer_result_t audiodelays_multi_tap_delay_get_buffer(audiodelays_m mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)) * MICROPY_FLOAT_CONST(2.0); mp_float_t decay = synthio_block_slot_get_limited(&self->decay, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + int32_t decay_scaled = (int32_t)(decay * MICROPY_FLOAT_CONST(32768.0)); + int32_t tap_scaled = (int32_t)(MIN(mix, MICROPY_FLOAT_CONST(1.0)) * MICROPY_FLOAT_CONST(32768.0)); + int32_t sample_scaled = (int32_t)(MIN(MICROPY_FLOAT_CONST(2.0) - mix, + MICROPY_FLOAT_CONST(1.0)) * MICROPY_FLOAT_CONST(32768.0)); + int16_t *sample_src = NULL; int8_t *sample_hsrc = NULL; if (self->sample != NULL) { @@ -424,7 +429,7 @@ audioio_get_buffer_result_t audiodelays_multi_tap_delay_get_buffer(audiodelays_m } // Apply decay and add sample - delay_word = (int32_t)(delay_word * decay) + sample_word; + delay_word = ((delay_word * decay_scaled) >> 15) + sample_word; if (MP_LIKELY(self->base.bits_per_sample == 16)) { delay_word = synthio_mix_down_sample(delay_word, SYNTHIO_MIX_DOWN_SCALE(2)); @@ -436,8 +441,7 @@ audioio_get_buffer_result_t audiodelays_multi_tap_delay_get_buffer(audiodelays_m } // Mix sample with tap output - word = (int32_t)((sample_word * MIN(MICROPY_FLOAT_CONST(2.0) - mix, MICROPY_FLOAT_CONST(1.0))) - + (word * MIN(mix, MICROPY_FLOAT_CONST(1.0)))); + word = ((sample_word * sample_scaled) >> 15) + ((word * tap_scaled) >> 15); word = synthio_mix_down_sample(word, SYNTHIO_MIX_DOWN_SCALE(2)); if (MP_LIKELY(self->base.bits_per_sample == 16)) { diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index 37deaa06b58..dc7f7fe9809 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -195,6 +195,9 @@ 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)); + int32_t wet_scaled = (int32_t)(mix * MICROPY_FLOAT_CONST(32768.0)); + int32_t dry_scaled = (int32_t)((MICROPY_FLOAT_CONST(1.0) - mix) * MICROPY_FLOAT_CONST(32768.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 for (uint32_t i = 0; i < n; i++) { if (MP_LIKELY(self->base.bits_per_sample == 16)) { @@ -238,15 +241,15 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o bool buf_offset = (j % self->base.channel_count) == 1; uint32_t k = j / self->base.channel_count; if (MP_LIKELY(self->base.bits_per_sample == 16)) { - word_buffer[i + j] = synthio_mix_down_sample((int32_t)((sample_src[i + j] * (MICROPY_FLOAT_CONST(1.0) - mix)) + (self->filter_buffer[k + SYNTHIO_MAX_DUR * buf_offset] * mix)), SYNTHIO_MIX_DOWN_SCALE(2)); + word_buffer[i + j] = synthio_mix_down_sample(((sample_src[i + j] * dry_scaled) >> 15) + ((self->filter_buffer[k + SYNTHIO_MAX_DUR * buf_offset] * wet_scaled) >> 15), SYNTHIO_MIX_DOWN_SCALE(2)); if (!self->base.samples_signed) { word_buffer[i + j] ^= 0x8000; } } else { if (self->base.samples_signed) { - hword_buffer[i + j] = (int8_t)((sample_hsrc[i + j] * (MICROPY_FLOAT_CONST(1.0) - mix)) + (self->filter_buffer[k + SYNTHIO_MAX_DUR * buf_offset] * mix)); + hword_buffer[i + j] = (int8_t)(((sample_hsrc[i + j] * dry_scaled) >> 15) + ((self->filter_buffer[k + SYNTHIO_MAX_DUR * buf_offset] * wet_scaled) >> 15)); } else { - hword_buffer[i + j] = (uint8_t)(((int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80) * (MICROPY_FLOAT_CONST(1.0) - mix)) + (self->filter_buffer[k + SYNTHIO_MAX_DUR * buf_offset] * mix)) ^ 0x80; + hword_buffer[i + j] = (uint8_t)((((int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80) * dry_scaled) >> 15) + ((self->filter_buffer[k + SYNTHIO_MAX_DUR * buf_offset] * wet_scaled) >> 15)) ^ 0x80; } } } diff --git a/shared-module/audiomixer/Mixer.c b/shared-module/audiomixer/Mixer.c index 464117fae8d..a171baf92d4 100644 --- a/shared-module/audiomixer/Mixer.c +++ b/shared-module/audiomixer/Mixer.c @@ -106,20 +106,19 @@ static inline uint32_t mult16signed(uint32_t val, int32_t lomul, int32_t himul) __asm__ volatile ("pkhbt %0, %1, %2, lsl #16" : "=r" (val) : "r" (lo), "r" (hi)); // pack return val; #else - uint32_t result = 0; - for (int8_t i = 0; i < 2; i++) { - float mod_mul = (float)(i ? himul : lomul) / (float)((1 << 15) - 1); - int16_t ai = (val >> (sizeof(uint16_t) * 8 * i)); - int32_t intermediate = (int32_t)(ai * mod_mul); - if (intermediate > SHRT_MAX) { - intermediate = SHRT_MAX; - } else if (intermediate < SHRT_MIN) { - intermediate = SHRT_MIN; - } - intermediate &= 0x0000FFFF; - result |= (((uint32_t)intermediate)) << (sizeof(int16_t) * 8 * i); + int32_t lo = ((int32_t)(int16_t)val * lomul) >> 15; + int32_t hi = ((int32_t)(int16_t)(val >> 16) * himul) >> 15; + if (lo > SHRT_MAX) { + lo = SHRT_MAX; + } else if (lo < SHRT_MIN) { + lo = SHRT_MIN; } - return result; + if (hi > SHRT_MAX) { + hi = SHRT_MAX; + } else if (hi < SHRT_MIN) { + hi = SHRT_MIN; + } + return ((uint32_t)hi << 16) | ((uint32_t)lo & 0xFFFF); #endif }