From e729849fac17fb78bf902e3a6c1112d6c9fc7eff Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 1 May 2025 08:15:43 -0500 Subject: [PATCH 1/5] Initial setup of phaser effect. --- .../unix/variants/coverage/mpconfigvariant.mk | 2 + py/circuitpy_defns.mk | 1 + shared-bindings/audiofilters/Phaser.c | 287 ++++++++++++++++++ shared-bindings/audiofilters/Phaser.h | 34 +++ shared-bindings/audiofilters/__init__.c | 2 + shared-module/audiofilters/Phaser.c | 276 +++++++++++++++++ shared-module/audiofilters/Phaser.h | 46 +++ 7 files changed, 648 insertions(+) create mode 100644 shared-bindings/audiofilters/Phaser.c create mode 100644 shared-bindings/audiofilters/Phaser.h create mode 100644 shared-module/audiofilters/Phaser.c create mode 100644 shared-module/audiofilters/Phaser.h diff --git a/ports/unix/variants/coverage/mpconfigvariant.mk b/ports/unix/variants/coverage/mpconfigvariant.mk index e1924479bbf53..579e42cc05cf9 100644 --- a/ports/unix/variants/coverage/mpconfigvariant.mk +++ b/ports/unix/variants/coverage/mpconfigvariant.mk @@ -40,6 +40,7 @@ SRC_BITMAP := \ shared-bindings/audiodelays/__init__.c \ shared-bindings/audiofilters/Distortion.c \ shared-bindings/audiofilters/Filter.c \ + shared-bindings/audiofilters/Phaser.c \ shared-bindings/audiofilters/__init__.c \ shared-bindings/audiofreeverb/Freeverb.c \ shared-bindings/audiofreeverb/__init__.c \ @@ -87,6 +88,7 @@ SRC_BITMAP := \ shared-module/audiodelays/__init__.c \ shared-module/audiofilters/Distortion.c \ shared-module/audiofilters/Filter.c \ + shared-module/audiofilters/Phaser.c \ shared-module/audiofilters/__init__.c \ shared-module/audiofreeverb/Freeverb.c \ shared-module/audiofreeverb/__init__.c \ diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index fa90481e648ea..f73d52a6c4b12 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -674,6 +674,7 @@ SRC_SHARED_MODULE_ALL = \ audiodelays/__init__.c \ audiofilters/Distortion.c \ audiofilters/Filter.c \ + audiofilters/Phaser.c \ audiofilters/__init__.c \ audiofreeverb/__init__.c \ audiofreeverb/Freeverb.c \ diff --git a/shared-bindings/audiofilters/Phaser.c b/shared-bindings/audiofilters/Phaser.c new file mode 100644 index 0000000000000..64fd01d7ab10c --- /dev/null +++ b/shared-bindings/audiofilters/Phaser.c @@ -0,0 +1,287 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared-bindings/audiofilters/Phaser.h" +#include "shared-bindings/audiocore/__init__.h" +#include "shared-module/audiofilters/Phaser.h" + +#include "shared/runtime/context_manager_helpers.h" +#include "py/binary.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/util.h" +#include "shared-module/synthio/block.h" + +//| class Phaser: +//| """A Phaser effect""" +//| +//| def __init__( +//| self, +//| frequency: synthio.BlockInput = 1000.0, +//| feedback: synthio.BlockInput = 0.7, +//| mix: synthio.BlockInput = 1.0, +//| stages: int = 6, +//| buffer_size: int = 512, +//| sample_rate: int = 8000, +//| bits_per_sample: int = 16, +//| samples_signed: bool = True, +//| channel_count: int = 1, +//| ) -> None: +//| """Create a Phaser effect where the original sample is processed through a variable +//| number of all-pass filter stages. This slightly delays the signal so that it is out +//| of phase with the original signal. When the amount of phase is modulated and mixed +//| back into the original signal with the mix parameter, it creates a distinctive +//| phasing sound. +//| +//| :param synthio.BlockInput frequency: The target frequency which is affected by the effect in hz. +//| :param int stages: The number of all-pass filters which will be applied to the signal. +//| :param synthio.BlockInput feedback: The amount that the previous output of the filters is mixed back into their input along with the unprocessed signal. +//| :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 +//| :param int channel_count: The number of channels the source samples contain. 1 = mono; 2 = stereo. +//| :param int bits_per_sample: The bits per sample of the effect +//| :param bool samples_signed: Effect is signed (True) or unsigned (False) +//| +//| Playing adding a phaser to a synth:: +//| +//| import time +//| import board +//| import audiobusio +//| import synthio +//| import audiofilters +//| +//| audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22) +//| synth = synthio.Synthesizer(channel_count=1, sample_rate=44100) +//| effect = audiofilters.Phaser(buffer_size=1024, channel_count=1, sample_rate=44100, mix=1.0) +//| effect.frequency = synthio.LFO(offset=1000.0, scale=600.0, rate=0.5) +//| effect.play(synth) +//| audio.play(effect) +//| +//| synth.press(48)""" +//| ... +//| +static mp_obj_t audiofilters_phaser_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_frequency, ARG_stages, ARG_feedback, 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_frequency, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1000) } }, + { MP_QSTR_feedback, 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_ROM_INT(1)} }, + { MP_QSTR_stages, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 6 } }, + { 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} }, + { MP_QSTR_bits_per_sample, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} }, + { MP_QSTR_samples_signed, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = true} }, + { MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1 } }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count); + mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate); + mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int; + if (bits_per_sample != 8 && bits_per_sample != 16) { + mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 8 or 16")); + } + + audiofilters_phaser_obj_t *self = mp_obj_malloc(audiofilters_phaser_obj_t, &audiofilters_phaser_type); + common_hal_audiofilters_phaser_construct(self, args[ARG_frequency].u_obj, args[ARG_feedback].u_obj, args[ARG_mix].u_obj, args[ARG_stages].u_int, 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); +} + +//| def deinit(self) -> None: +//| """Deinitialises the Phaser.""" +//| ... +//| +static mp_obj_t audiofilters_phaser_deinit(mp_obj_t self_in) { + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_phaser_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_deinit_obj, audiofilters_phaser_deinit); + +static void check_for_deinit(audiofilters_phaser_obj_t *self) { + audiosample_check_for_deinit(&self->base); +} + +//| def __enter__(self) -> Phaser: +//| """No-op used by Context Managers.""" +//| ... +//| +// Provided by context manager helper. + +//| def __exit__(self) -> None: +//| """Automatically deinitializes when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +//| +// Provided by context manager helper. + + +//| frequency: synthio.BlockInput +//| """The target frequency in hertz at which the phaser is delaying the signal.""" +static mp_obj_t audiofilters_phaser_obj_get_frequency(mp_obj_t self_in) { + return common_hal_audiofilters_phaser_get_frequency(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_frequency_obj, audiofilters_phaser_obj_get_frequency); + +static mp_obj_t audiofilters_phaser_obj_set_frequency(mp_obj_t self_in, mp_obj_t frequency_in) { + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_phaser_set_frequency(self, frequency_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_frequency_obj, audiofilters_phaser_obj_set_frequency); + +MP_PROPERTY_GETSET(audiofilters_phaser_frequency_obj, + (mp_obj_t)&audiofilters_phaser_get_frequency_obj, + (mp_obj_t)&audiofilters_phaser_set_frequency_obj); + + +//| feedback: synthio.BlockInput +//| """The amount of which the incoming signal is fed back into the phasing filters from 0 to 1 where 0 is no feedback and 1 is full feedback.""" +static mp_obj_t audiofilters_phaser_obj_get_feedback(mp_obj_t self_in) { + return common_hal_audiofilters_phaser_get_feedback(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_feedback_obj, audiofilters_phaser_obj_get_feedback); + +static mp_obj_t audiofilters_phaser_obj_set_feedback(mp_obj_t self_in, mp_obj_t feedback_in) { + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_phaser_set_feedback(self, feedback_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_feedback_obj, audiofilters_phaser_obj_set_feedback); + +MP_PROPERTY_GETSET(audiofilters_phaser_feedback_obj, + (mp_obj_t)&audiofilters_phaser_get_feedback_obj, + (mp_obj_t)&audiofilters_phaser_set_feedback_obj); + + +//| mix: synthio.BlockInput +//| """The amount that the effect signal is mixed into the output between 0 and 1 where 0 is only the original sample and 1 is all effect.""" +static mp_obj_t audiofilters_phaser_obj_get_mix(mp_obj_t self_in) { + return common_hal_audiofilters_phaser_get_mix(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_mix_obj, audiofilters_phaser_obj_get_mix); + +static mp_obj_t audiofilters_phaser_obj_set_mix(mp_obj_t self_in, mp_obj_t mix_in) { + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_phaser_set_mix(self, mix_in); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_mix_obj, audiofilters_phaser_obj_set_mix); + +MP_PROPERTY_GETSET(audiofilters_phaser_mix_obj, + (mp_obj_t)&audiofilters_phaser_get_mix_obj, + (mp_obj_t)&audiofilters_phaser_set_mix_obj); + + +//| stages: int +//| """The number of allpass filters to pass the signal through. More stages requires more processing but produces a more pronounced effect. Requires a minimum value of 1.""" +static mp_obj_t audiofilters_phaser_obj_get_stages(mp_obj_t self_in) { + return MP_OBJ_NEW_SMALL_INT(common_hal_audiofilters_phaser_get_stages(self_in)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_stages_obj, audiofilters_phaser_obj_get_stages); + +static mp_obj_t audiofilters_phaser_obj_set_stages(mp_obj_t self_in, mp_obj_t stages_in) { + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_phaser_set_stages(self, mp_obj_get_int(stages_in)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_stages_obj, audiofilters_phaser_obj_set_stages); + +MP_PROPERTY_GETSET(audiofilters_phaser_stages_obj, + (mp_obj_t)&audiofilters_phaser_get_stages_obj, + (mp_obj_t)&audiofilters_phaser_set_stages_obj); + + +//| playing: bool +//| """True when the effect is playing a sample. (read-only)""" +//| +static mp_obj_t audiofilters_phaser_obj_get_playing(mp_obj_t self_in) { + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiofilters_phaser_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_playing_obj, audiofilters_phaser_obj_get_playing); + +MP_PROPERTY_GETTER(audiofilters_phaser_playing_obj, + (mp_obj_t)&audiofilters_phaser_get_playing_obj); + +//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None: +//| """Plays the sample once when loop=False and continuously when loop=True. +//| Does not block. Use `playing` to block. +//| +//| The sample must match the encoding settings given in the constructor.""" +//| ... +//| +static mp_obj_t audiofilters_phaser_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_sample, ARG_loop }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED, {} }, + { MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + }; + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + + mp_obj_t sample = args[ARG_sample].u_obj; + common_hal_audiofilters_phaser_play(self, sample, args[ARG_loop].u_bool); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_phaser_play_obj, 1, audiofilters_phaser_obj_play); + +//| def stop(self) -> None: +//| """Stops playback of the sample.""" +//| ... +//| +//| +static mp_obj_t audiofilters_phaser_obj_stop(mp_obj_t self_in) { + audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in); + + common_hal_audiofilters_phaser_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_stop_obj, audiofilters_phaser_obj_stop); + +static const mp_rom_map_elem_t audiofilters_phaser_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiofilters_phaser_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) }, + { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiofilters_phaser_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiofilters_phaser_stop_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofilters_phaser_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&audiofilters_phaser_frequency_obj) }, + { MP_ROM_QSTR(MP_QSTR_feedback), MP_ROM_PTR(&audiofilters_phaser_feedback_obj) }, + { MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiofilters_phaser_mix_obj) }, + { MP_ROM_QSTR(MP_QSTR_stages), MP_ROM_PTR(&audiofilters_phaser_stages_obj) }, + AUDIOSAMPLE_FIELDS, +}; +static MP_DEFINE_CONST_DICT(audiofilters_phaser_locals_dict, audiofilters_phaser_locals_dict_table); + +static const audiosample_p_t audiofilters_phaser_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + .reset_buffer = (audiosample_reset_buffer_fun)audiofilters_phaser_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)audiofilters_phaser_get_buffer, +}; + +MP_DEFINE_CONST_OBJ_TYPE( + audiofilters_phaser_type, + MP_QSTR_Phaser, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audiofilters_phaser_make_new, + locals_dict, &audiofilters_phaser_locals_dict, + protocol, &audiofilters_phaser_proto + ); diff --git a/shared-bindings/audiofilters/Phaser.h b/shared-bindings/audiofilters/Phaser.h new file mode 100644 index 0000000000000..b020ba6183cf2 --- /dev/null +++ b/shared-bindings/audiofilters/Phaser.h @@ -0,0 +1,34 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiofilters/Phaser.h" + +extern const mp_obj_type_t audiofilters_phaser_type; + +void common_hal_audiofilters_phaser_construct(audiofilters_phaser_obj_t *self, + mp_obj_t frequency, mp_obj_t feedback, mp_obj_t mix, uint8_t stages, + uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, + uint8_t channel_count, uint32_t sample_rate); + +void common_hal_audiofilters_phaser_deinit(audiofilters_phaser_obj_t *self); + +mp_obj_t common_hal_audiofilters_phaser_get_frequency(audiofilters_phaser_obj_t *self); +void common_hal_audiofilters_phaser_set_frequency(audiofilters_phaser_obj_t *self, mp_obj_t arg); + +mp_obj_t common_hal_audiofilters_phaser_get_feedback(audiofilters_phaser_obj_t *self); +void common_hal_audiofilters_phaser_set_feedback(audiofilters_phaser_obj_t *self, mp_obj_t arg); + +mp_obj_t common_hal_audiofilters_phaser_get_mix(audiofilters_phaser_obj_t *self); +void common_hal_audiofilters_phaser_set_mix(audiofilters_phaser_obj_t *self, mp_obj_t arg); + +uint8_t common_hal_audiofilters_phaser_get_stages(audiofilters_phaser_obj_t *self); +void common_hal_audiofilters_phaser_set_stages(audiofilters_phaser_obj_t *self, mp_obj_t arg); + +bool common_hal_audiofilters_phaser_get_playing(audiofilters_phaser_obj_t *self); +void common_hal_audiofilters_phaser_play(audiofilters_phaser_obj_t *self, mp_obj_t sample, bool loop); +void common_hal_audiofilters_phaser_stop(audiofilters_phaser_obj_t *self); diff --git a/shared-bindings/audiofilters/__init__.c b/shared-bindings/audiofilters/__init__.c index 7a17ec655e628..ae43af9bfef83 100644 --- a/shared-bindings/audiofilters/__init__.c +++ b/shared-bindings/audiofilters/__init__.c @@ -12,6 +12,7 @@ #include "shared-bindings/audiofilters/__init__.h" #include "shared-bindings/audiofilters/Distortion.h" #include "shared-bindings/audiofilters/Filter.h" +#include "shared-bindings/audiofilters/Phaser.h" //| """Support for audio filter effects //| @@ -23,6 +24,7 @@ static const mp_rom_map_elem_t audiofilters_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiofilters) }, { MP_ROM_QSTR(MP_QSTR_Filter), MP_ROM_PTR(&audiofilters_filter_type) }, { MP_ROM_QSTR(MP_QSTR_Distortion), MP_ROM_PTR(&audiofilters_distortion_type) }, + { MP_ROM_QSTR(MP_QSTR_Phaser), MP_ROM_PTR(&audiofilters_phaser_type) }, // Enum-like Classes. { MP_ROM_QSTR(MP_QSTR_DistortionMode), MP_ROM_PTR(&audiofilters_distortion_mode_type) }, diff --git a/shared-module/audiofilters/Phaser.c b/shared-module/audiofilters/Phaser.c new file mode 100644 index 0000000000000..8e669dc90a015 --- /dev/null +++ b/shared-module/audiofilters/Phaser.c @@ -0,0 +1,276 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT +#include "shared-bindings/audiofilters/Phaser.h" +#include "shared-bindings/audiocore/__init__.h" + +#include +#include "py/runtime.h" + +void common_hal_audiofilters_phaser_construct(audiofilters_phaser_obj_t *self, + mp_obj_t frequency, mp_obj_t feedback, mp_obj_t mix, uint8_t stages, + uint32_t buffer_size, uint8_t bits_per_sample, + bool samples_signed, uint8_t channel_count, uint32_t sample_rate) { + + // Basic settings every effect and audio sample has + // These are the effects values, not the source sample(s) + self->base.bits_per_sample = bits_per_sample; // Most common is 16, but 8 is also supported in many places + self->base.samples_signed = samples_signed; // Are the samples we provide signed (common is true) + self->base.channel_count = channel_count; // Channels can be 1 for mono or 2 for stereo + self->base.sample_rate = sample_rate; // Sample rate for the effect, this generally needs to match all audio objects + self->base.single_buffer = false; + self->base.max_buffer_length = buffer_size; + + // To smooth things out as CircuitPython is doing other tasks most audio objects have a buffer + // A double buffer is set up here so the audio output can use DMA on buffer 1 while we + // write to and create buffer 2. + // This buffer is what is passed to the audio component that plays the effect. + // Samples are set sequentially. For stereo audio they are passed L/R/L/R/... + self->buffer_len = buffer_size; // in bytes + + self->buffer[0] = m_malloc_without_collect(self->buffer_len); + memset(self->buffer[0], 0, self->buffer_len); + + self->buffer[1] = m_malloc_without_collect(self->buffer_len); + memset(self->buffer[1], 0, self->buffer_len); + + self->last_buf_idx = 1; // Which buffer to use first, toggle between 0 and 1 + + // This buffer will be used to process samples through the biquad filter + self->filter_buffer = m_malloc_without_collect(SYNTHIO_MAX_DUR * sizeof(int32_t)); + memset(self->filter_buffer, 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); + + // Initialize other values most effects will need. + self->sample = NULL; // The current playing sample + self->sample_remaining_buffer = NULL; // Pointer to the start of the sample buffer we have not played + self->sample_buffer_length = 0; // How many samples do we have left to play (these may be 16 bit!) + self->loop = false; // When the sample is done do we loop to the start again or stop (e.g. in a wav file) + self->more_data = false; // Is there still more data to read from the sample or did we finish + + // The below section sets up the effect's starting values. + + self->nyquist = (mp_float_t) self->base.sample_rate / 2; + + if (feedback == mp_const_none) { + feedback = mp_obj_new_float(MICROPY_FLOAT_CONST(0.7)); + } + + synthio_block_assign_slot(frequency, &self->frequency, MP_QSTR_frequency); + synthio_block_assign_slot(feedback, &self->feedback, MP_QSTR_feedback); + synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix); + + common_hal_audiofilters_phaser_set_stages(self, stages); +} + +void common_hal_audiofilters_phaser_deinit(audiofilters_phaser_obj_t *self) { + audiosample_mark_deinit(&self->base); + self->buffer[0] = NULL; + self->buffer[1] = NULL; +} + +mp_obj_t common_hal_audiofilters_phaser_get_frequency(audiofilters_phaser_obj_t *self) { + return self->frequency.obj; +} + +void common_hal_audiofilters_phaser_set_frequency(audiofilters_phaser_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->frequency, MP_QSTR_frequency); +} + +mp_obj_t common_hal_audiofilters_phaser_get_feedback(audiofilters_phaser_obj_t *self) { + return self->feedback.obj; +} + +void common_hal_audiofilters_phaser_set_feedback(audiofilters_phaser_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->feedback, MP_QSTR_feedback); +} + +mp_obj_t common_hal_audiofilters_phaser_get_mix(audiofilters_phaser_obj_t *self) { + return self->mix.obj; +} + +void common_hal_audiofilters_phaser_set_mix(audiofilters_phaser_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->mix, MP_QSTR_mix); +} + +uint8_t common_hal_audiofilters_phaser_get_stages(audiofilters_phaser_obj_t *self) { + return self->stages; +} + +void common_hal_audiofilters_phaser_set_stages(audiofilters_phaser_obj_t *self, uint8_t arg) { + if (!arg) { + arg = 1; + } + // TODO: reallocate filters + self->stages = arg; +} + +void audiofilters_phaser_reset_buffer(audiofilters_phaser_obj_t *self, + bool single_channel_output, + uint8_t channel) { + + memset(self->buffer[0], 0, self->buffer_len); + memset(self->buffer[1], 0, self->buffer_len); +} + +bool common_hal_audiofilters_phaser_get_playing(audiofilters_phaser_obj_t *self) { + return self->sample != NULL; +} + +void common_hal_audiofilters_phaser_play(audiofilters_phaser_obj_t *self, mp_obj_t sample, bool loop) { + audiosample_must_match(&self->base, sample); + + self->sample = sample; + self->loop = loop; + + audiosample_reset_buffer(self->sample, false, 0); + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); + + // Track remaining sample length in terms of bytes per sample + self->sample_buffer_length /= (self->base.bits_per_sample / 8); + // Store if we have more data in the sample to retrieve + self->more_data = result == GET_BUFFER_MORE_DATA; + + return; +} + +void common_hal_audiofilters_phaser_stop(audiofilters_phaser_obj_t *self) { + // When the sample is set to stop playing do any cleanup here + self->sample = NULL; + return; +} + +audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_obj_t *self, bool single_channel_output, uint8_t channel, + uint8_t **buffer, uint32_t *buffer_length) { + (void)channel; + + if (!single_channel_output) { + channel = 0; + } + + // Switch our buffers to the other buffer + self->last_buf_idx = !self->last_buf_idx; + + // If we are using 16 bit samples we need a 16 bit pointer, 8 bit needs an 8 bit pointer + int16_t *word_buffer = (int16_t *)self->buffer[self->last_buf_idx]; + int8_t *hword_buffer = self->buffer[self->last_buf_idx]; + uint32_t length = self->buffer_len / (self->base.bits_per_sample / 8); + + // Loop over the entire length of our buffer to fill it, this may require several calls to get data from the sample + while (length != 0) { + // Check if there is no more sample to play, we will either load more data, reset the sample if loop is on or clear the sample + if (self->sample_buffer_length == 0) { + if (!self->more_data) { // The sample has indicated it has no more data to play + if (self->loop && self->sample) { // If we are supposed to loop reset the sample to the start + audiosample_reset_buffer(self->sample, false, 0); + } else { // If we were not supposed to loop the sample, stop playing it + self->sample = NULL; + } + } + if (self->sample) { + // Load another sample buffer to play + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); + // Track length in terms of words. + self->sample_buffer_length /= (self->base.bits_per_sample / 8); + self->more_data = result == GET_BUFFER_MORE_DATA; + } + } + + if (self->sample == NULL) { + // tick all block inputs + shared_bindings_synthio_lfo_tick(self->base.sample_rate, length / self->base.channel_count); + (void)synthio_block_slot_get(&self->frequency); + (void)synthio_block_slot_get(&self->feedback); + (void)synthio_block_slot_get(&self->mix); + + if (self->base.samples_signed) { + memset(word_buffer, 0, length * (self->base.bits_per_sample / 8)); + } else { + // For unsigned samples set to the middle which is "quiet" + if (MP_LIKELY(self->base.bits_per_sample == 16)) { + uint16_t *uword_buffer = (uint16_t *)word_buffer; + while (length--) { + *uword_buffer++ = 32768; + } + } else { + memset(hword_buffer, 128, length * (self->base.bits_per_sample / 8)); + } + } + + length = 0; + } else { + // we have a sample to play and filter + // Determine how many bytes we can process to our buffer, the less of the sample we have left and our buffer remaining + uint32_t n = MIN(MIN(self->sample_buffer_length, length), SYNTHIO_MAX_DUR * self->base.channel_count); + + int16_t *sample_src = (int16_t *)self->sample_remaining_buffer; // for 16-bit samples + int8_t *sample_hsrc = (int8_t *)self->sample_remaining_buffer; // for 8-bit samples + + // get the effect values we need from the BlockInput. These may change at run time so you need to do bounds checking if required + shared_bindings_synthio_lfo_tick(self->base.sample_rate, n / self->base.channel_count); + mp_float_t frequency = synthio_block_slot_get_limited(&self->frequency, MICROPY_FLOAT_CONST(0.0), self->nyquist); + mp_float_t feedback = synthio_block_slot_get_limited(&self->feedback, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + 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)) { // if mix is zero pure sample only + for (uint32_t i = 0; i < n; i++) { + if (MP_LIKELY(self->base.bits_per_sample == 16)) { + word_buffer[i] = sample_src[i]; + } else { + hword_buffer[i] = sample_hsrc[i]; + } + } + } else { + for (uint32_t i = 0; i < n; i++) { + int32_t sample_word = 0; + if (MP_LIKELY(self->base.bits_per_sample == 16)) { + sample_word = sample_src[i]; + } else { + if (self->base.samples_signed) { + sample_word = sample_hsrc[i]; + } else { + // Be careful here changing from an 8 bit unsigned to signed into a 32-bit signed + sample_word = (int8_t)(((uint8_t)sample_hsrc[i]) ^ 0x80); + } + } + + // TODO: Process sample + int32_t word = 0; + + // Add original sample + effect + word = sample_word + (int32_t)(word * mix); + word = synthio_mix_down_sample(word, 2); + + if (MP_LIKELY(self->base.bits_per_sample == 16)) { + word_buffer[i] = word; + if (!self->base.samples_signed) { + word_buffer[i] ^= 0x8000; + } + } else { + int8_t out = word; + if (self->base.samples_signed) { + hword_buffer[i] = out; + } else { + hword_buffer[i] = (uint8_t)out ^ 0x80; + } + } + } + } + + // Update the remaining length and the buffer positions based on how much we wrote into our buffer + length -= n; + word_buffer += n; + hword_buffer += n; + self->sample_remaining_buffer += (n * (self->base.bits_per_sample / 8)); + self->sample_buffer_length -= n; + } + } + + // Finally pass our buffer and length to the calling audio function + *buffer = (uint8_t *)self->buffer[self->last_buf_idx]; + *buffer_length = self->buffer_len; + + // Phaser always returns more data but some effects may return GET_BUFFER_DONE or GET_BUFFER_ERROR (see audiocore/__init__.h) + return GET_BUFFER_MORE_DATA; +} diff --git a/shared-module/audiofilters/Phaser.h b/shared-module/audiofilters/Phaser.h new file mode 100644 index 0000000000000..c2d9aeeb21de7 --- /dev/null +++ b/shared-module/audiofilters/Phaser.h @@ -0,0 +1,46 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "py/obj.h" + +#include "shared-module/audiocore/__init__.h" +#include "shared-module/synthio/__init__.h" +#include "shared-module/synthio/block.h" + +extern const mp_obj_type_t audiofilters_phaser_type; + +typedef struct { + audiosample_base_t base; + synthio_block_slot_t frequency; + synthio_block_slot_t feedback; + synthio_block_slot_t mix; + uint8_t stages; + + mp_float_t nyquist; + + int8_t *buffer[2]; + uint8_t last_buf_idx; + uint32_t buffer_len; // max buffer in bytes + + uint8_t *sample_remaining_buffer; + uint32_t sample_buffer_length; + + bool loop; + bool more_data; + + mp_obj_t sample; +} audiofilters_phaser_obj_t; + +void audiofilters_phaser_reset_buffer(audiofilters_phaser_obj_t *self, + bool single_channel_output, + uint8_t channel); + +audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length); // length in bytes From ba81c6b1c4c16ee190bd16e34b1217f452e68923 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 1 May 2025 10:42:03 -0500 Subject: [PATCH 2/5] Implement allpass filters. --- shared-bindings/audiofilters/Phaser.c | 6 ++-- shared-bindings/audiofilters/Phaser.h | 2 +- shared-module/audiofilters/Phaser.c | 45 +++++++++++++++++++++------ shared-module/audiofilters/Phaser.h | 3 ++ 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/shared-bindings/audiofilters/Phaser.c b/shared-bindings/audiofilters/Phaser.c index 64fd01d7ab10c..817614fa21f35 100644 --- a/shared-bindings/audiofilters/Phaser.c +++ b/shared-bindings/audiofilters/Phaser.c @@ -53,12 +53,12 @@ //| import time //| import board //| import audiobusio -//| import synthio //| import audiofilters +//| import synthio //| //| audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22) //| synth = synthio.Synthesizer(channel_count=1, sample_rate=44100) -//| effect = audiofilters.Phaser(buffer_size=1024, channel_count=1, sample_rate=44100, mix=1.0) +//| effect = audiofilters.Phaser(channel_count=1, sample_rate=44100) //| effect.frequency = synthio.LFO(offset=1000.0, scale=600.0, rate=0.5) //| effect.play(synth) //| audio.play(effect) @@ -67,7 +67,7 @@ //| ... //| static mp_obj_t audiofilters_phaser_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_frequency, ARG_stages, ARG_feedback, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, }; + enum { ARG_frequency, ARG_feedback, ARG_mix, ARG_stages, 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_frequency, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1000) } }, { MP_QSTR_feedback, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, diff --git a/shared-bindings/audiofilters/Phaser.h b/shared-bindings/audiofilters/Phaser.h index b020ba6183cf2..dbab22f571025 100644 --- a/shared-bindings/audiofilters/Phaser.h +++ b/shared-bindings/audiofilters/Phaser.h @@ -27,7 +27,7 @@ mp_obj_t common_hal_audiofilters_phaser_get_mix(audiofilters_phaser_obj_t *self) void common_hal_audiofilters_phaser_set_mix(audiofilters_phaser_obj_t *self, mp_obj_t arg); uint8_t common_hal_audiofilters_phaser_get_stages(audiofilters_phaser_obj_t *self); -void common_hal_audiofilters_phaser_set_stages(audiofilters_phaser_obj_t *self, mp_obj_t arg); +void common_hal_audiofilters_phaser_set_stages(audiofilters_phaser_obj_t *self, uint8_t arg); bool common_hal_audiofilters_phaser_get_playing(audiofilters_phaser_obj_t *self); void common_hal_audiofilters_phaser_play(audiofilters_phaser_obj_t *self, mp_obj_t sample, bool loop); diff --git a/shared-module/audiofilters/Phaser.c b/shared-module/audiofilters/Phaser.c index 8e669dc90a015..f31aea4caaa3d 100644 --- a/shared-module/audiofilters/Phaser.c +++ b/shared-module/audiofilters/Phaser.c @@ -38,10 +38,6 @@ void common_hal_audiofilters_phaser_construct(audiofilters_phaser_obj_t *self, self->last_buf_idx = 1; // Which buffer to use first, toggle between 0 and 1 - // This buffer will be used to process samples through the biquad filter - self->filter_buffer = m_malloc_without_collect(SYNTHIO_MAX_DUR * sizeof(int32_t)); - memset(self->filter_buffer, 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); - // Initialize other values most effects will need. self->sample = NULL; // The current playing sample self->sample_remaining_buffer = NULL; // Pointer to the start of the sample buffer we have not played @@ -51,7 +47,11 @@ void common_hal_audiofilters_phaser_construct(audiofilters_phaser_obj_t *self, // The below section sets up the effect's starting values. - self->nyquist = (mp_float_t) self->base.sample_rate / 2; + // Create buffer to hold the last processed word + self->word_buffer = m_malloc_without_collect(self->base.channel_count * sizeof(int32_t)); + memset(self->word_buffer, 0, self->base.channel_count * sizeof(int32_t)); + + self->nyquist = (mp_float_t)self->base.sample_rate / 2; if (feedback == mp_const_none) { feedback = mp_obj_new_float(MICROPY_FLOAT_CONST(0.7)); @@ -68,6 +68,8 @@ void common_hal_audiofilters_phaser_deinit(audiofilters_phaser_obj_t *self) { audiosample_mark_deinit(&self->base); self->buffer[0] = NULL; self->buffer[1] = NULL; + self->word_buffer = NULL; + self->allpass_buffer = NULL; } mp_obj_t common_hal_audiofilters_phaser_get_frequency(audiofilters_phaser_obj_t *self) { @@ -102,8 +104,15 @@ void common_hal_audiofilters_phaser_set_stages(audiofilters_phaser_obj_t *self, if (!arg) { arg = 1; } - // TODO: reallocate filters + + self->allpass_buffer = (int32_t *)m_realloc(self->allpass_buffer, + #if MICROPY_MALLOC_USES_ALLOCATED_SIZE + self->base.channel_count * self->stages * sizeof(int32_t), // Old size + #endif + self->base.channel_count * arg * sizeof(int32_t)); self->stages = arg; + + memset(self->allpass_buffer, 0, self->base.channel_count * self->stages * sizeof(int32_t)); } void audiofilters_phaser_reset_buffer(audiofilters_phaser_obj_t *self, @@ -112,6 +121,8 @@ void audiofilters_phaser_reset_buffer(audiofilters_phaser_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); + memset(self->word_buffer, 0, self->base.channel_count * sizeof(int32_t)); + memset(self->allpass_buffer, 0, self->base.channel_count * self->stages * sizeof(int32_t)); } bool common_hal_audiofilters_phaser_get_playing(audiofilters_phaser_obj_t *self) { @@ -222,7 +233,15 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o } } } else { + + // Update all-pass filter coefficient + mp_float_t allpasscoef = frequency / self->nyquist; + allpasscoef = (MICROPY_FLOAT_CONST(1.0) - allpasscoef) / (MICROPY_FLOAT_CONST(1.0) + allpasscoef); + for (uint32_t i = 0; i < n; i++) { + bool right_channel = (single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1); + uint32_t allpass_buffer_offset = self->stages * right_channel; + int32_t sample_word = 0; if (MP_LIKELY(self->base.bits_per_sample == 16)) { sample_word = sample_src[i]; @@ -235,13 +254,21 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o } } - // TODO: Process sample - int32_t word = 0; + int32_t word = sample_word + self->word_buffer[right_channel] * feedback; + int32_t allpass_word = 0; + + // Update all-pass filters + for (uint32_t j = 0; j < self->stages; j++) { + allpass_word = word * -allpasscoef + self->allpass_buffer[j + allpass_buffer_offset]; + self->allpass_buffer[j + allpass_buffer_offset] = allpass_word * allpasscoef + word; + word = allpass_word; + } + self->word_buffer[(bool)allpass_buffer_offset] = word; // Add original sample + effect word = sample_word + (int32_t)(word * mix); word = synthio_mix_down_sample(word, 2); - + if (MP_LIKELY(self->base.bits_per_sample == 16)) { word_buffer[i] = word; if (!self->base.samples_signed) { diff --git a/shared-module/audiofilters/Phaser.h b/shared-module/audiofilters/Phaser.h index c2d9aeeb21de7..707a2b0f0841d 100644 --- a/shared-module/audiofilters/Phaser.h +++ b/shared-module/audiofilters/Phaser.h @@ -32,6 +32,9 @@ typedef struct { bool loop; bool more_data; + int32_t *allpass_buffer; + int32_t *word_buffer; + mp_obj_t sample; } audiofilters_phaser_obj_t; From e855e3e1bae5af371826efcaf975111e9f31c1e1 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 1 May 2025 10:54:10 -0500 Subject: [PATCH 3/5] Limit feedback range to 0.1 - 0.9 --- shared-bindings/audiofilters/Phaser.c | 2 +- shared-module/audiofilters/Phaser.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-bindings/audiofilters/Phaser.c b/shared-bindings/audiofilters/Phaser.c index 817614fa21f35..e7ddd986176b3 100644 --- a/shared-bindings/audiofilters/Phaser.c +++ b/shared-bindings/audiofilters/Phaser.c @@ -145,7 +145,7 @@ MP_PROPERTY_GETSET(audiofilters_phaser_frequency_obj, //| feedback: synthio.BlockInput -//| """The amount of which the incoming signal is fed back into the phasing filters from 0 to 1 where 0 is no feedback and 1 is full feedback.""" +//| """The amount of which the incoming signal is fed back into the phasing filters from 0.1 to 0.9.""" static mp_obj_t audiofilters_phaser_obj_get_feedback(mp_obj_t self_in) { return common_hal_audiofilters_phaser_get_feedback(self_in); } diff --git a/shared-module/audiofilters/Phaser.c b/shared-module/audiofilters/Phaser.c index f31aea4caaa3d..3a426ff2c302b 100644 --- a/shared-module/audiofilters/Phaser.c +++ b/shared-module/audiofilters/Phaser.c @@ -221,7 +221,7 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o // get the effect values we need from the BlockInput. These may change at run time so you need to do bounds checking if required shared_bindings_synthio_lfo_tick(self->base.sample_rate, n / self->base.channel_count); mp_float_t frequency = synthio_block_slot_get_limited(&self->frequency, MICROPY_FLOAT_CONST(0.0), self->nyquist); - mp_float_t feedback = synthio_block_slot_get_limited(&self->feedback, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + mp_float_t feedback = synthio_block_slot_get_limited(&self->feedback, MICROPY_FLOAT_CONST(0.1), MICROPY_FLOAT_CONST(0.9)); 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)) { // if mix is zero pure sample only From 421521516c221629ff6c15fd066a1be3fc6a95f3 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Thu, 1 May 2025 15:27:23 -0500 Subject: [PATCH 4/5] Convert floating point operations to fixed point and use `synthio_sat16`. --- shared-module/audiofilters/Phaser.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/shared-module/audiofilters/Phaser.c b/shared-module/audiofilters/Phaser.c index 3a426ff2c302b..73a2a8d54a59c 100644 --- a/shared-module/audiofilters/Phaser.c +++ b/shared-module/audiofilters/Phaser.c @@ -221,10 +221,10 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o // get the effect values we need from the BlockInput. These may change at run time so you need to do bounds checking if required shared_bindings_synthio_lfo_tick(self->base.sample_rate, n / self->base.channel_count); mp_float_t frequency = synthio_block_slot_get_limited(&self->frequency, MICROPY_FLOAT_CONST(0.0), self->nyquist); - mp_float_t feedback = synthio_block_slot_get_limited(&self->feedback, MICROPY_FLOAT_CONST(0.1), MICROPY_FLOAT_CONST(0.9)); - mp_float_t mix = synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)); + int16_t feedback = (int16_t)(synthio_block_slot_get_limited(&self->feedback, MICROPY_FLOAT_CONST(0.1), MICROPY_FLOAT_CONST(0.9)) * 32767); + int16_t mix = (int16_t)(synthio_block_slot_get_limited(&self->mix, MICROPY_FLOAT_CONST(0.0), MICROPY_FLOAT_CONST(1.0)) * 32767); - if (mix <= MICROPY_FLOAT_CONST(0.01)) { // if mix is zero pure sample only + if (mix <= 328) { // if mix is zero (0.01 in fixed point), pure sample only for (uint32_t i = 0; i < n; i++) { if (MP_LIKELY(self->base.bits_per_sample == 16)) { word_buffer[i] = sample_src[i]; @@ -233,10 +233,9 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o } } } else { - // Update all-pass filter coefficient - mp_float_t allpasscoef = frequency / self->nyquist; - allpasscoef = (MICROPY_FLOAT_CONST(1.0) - allpasscoef) / (MICROPY_FLOAT_CONST(1.0) + allpasscoef); + frequency /= self->nyquist; // scale relative to frequency range + int16_t allpasscoef = (int16_t)((MICROPY_FLOAT_CONST(1.0) - frequency) / (MICROPY_FLOAT_CONST(1.0) + frequency) * 32767); for (uint32_t i = 0; i < n; i++) { bool right_channel = (single_channel_output && channel == 1) || (!single_channel_output && (i % self->base.channel_count) == 1); @@ -254,19 +253,19 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o } } - int32_t word = sample_word + self->word_buffer[right_channel] * feedback; + int32_t word = synthio_sat16(sample_word + synthio_sat16(self->word_buffer[right_channel] * feedback, 15), 0); int32_t allpass_word = 0; // Update all-pass filters for (uint32_t j = 0; j < self->stages; j++) { - allpass_word = word * -allpasscoef + self->allpass_buffer[j + allpass_buffer_offset]; - self->allpass_buffer[j + allpass_buffer_offset] = allpass_word * allpasscoef + word; + allpass_word = synthio_sat16(synthio_sat16(word * -allpasscoef, 15) + self->allpass_buffer[j + allpass_buffer_offset], 0); + self->allpass_buffer[j + allpass_buffer_offset] = synthio_sat16(synthio_sat16(allpass_word * allpasscoef, 15) + word, 0); word = allpass_word; } self->word_buffer[(bool)allpass_buffer_offset] = word; // Add original sample + effect - word = sample_word + (int32_t)(word * mix); + word = sample_word + (int32_t)(synthio_sat16(word * mix, 15)); word = synthio_mix_down_sample(word, 2); if (MP_LIKELY(self->base.bits_per_sample == 16)) { From 02382401c6060154d8283c8871fa6d7a5887cc6f Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 2 May 2025 09:08:19 -0500 Subject: [PATCH 5/5] Reduce data size of internal buffers. --- shared-module/audiofilters/Phaser.c | 20 ++++++++++---------- shared-module/audiofilters/Phaser.h | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/shared-module/audiofilters/Phaser.c b/shared-module/audiofilters/Phaser.c index 73a2a8d54a59c..81d9c0bea3083 100644 --- a/shared-module/audiofilters/Phaser.c +++ b/shared-module/audiofilters/Phaser.c @@ -48,8 +48,8 @@ void common_hal_audiofilters_phaser_construct(audiofilters_phaser_obj_t *self, // The below section sets up the effect's starting values. // Create buffer to hold the last processed word - self->word_buffer = m_malloc_without_collect(self->base.channel_count * sizeof(int32_t)); - memset(self->word_buffer, 0, self->base.channel_count * sizeof(int32_t)); + self->word_buffer = m_malloc_without_collect(self->base.channel_count * sizeof(int16_t)); + memset(self->word_buffer, 0, self->base.channel_count * sizeof(int16_t)); self->nyquist = (mp_float_t)self->base.sample_rate / 2; @@ -105,14 +105,14 @@ void common_hal_audiofilters_phaser_set_stages(audiofilters_phaser_obj_t *self, arg = 1; } - self->allpass_buffer = (int32_t *)m_realloc(self->allpass_buffer, + self->allpass_buffer = (int16_t *)m_realloc(self->allpass_buffer, #if MICROPY_MALLOC_USES_ALLOCATED_SIZE - self->base.channel_count * self->stages * sizeof(int32_t), // Old size + self->base.channel_count * self->stages * sizeof(int16_t), // Old size #endif - self->base.channel_count * arg * sizeof(int32_t)); + self->base.channel_count * arg * sizeof(int16_t)); self->stages = arg; - memset(self->allpass_buffer, 0, self->base.channel_count * self->stages * sizeof(int32_t)); + memset(self->allpass_buffer, 0, self->base.channel_count * self->stages * sizeof(int16_t)); } void audiofilters_phaser_reset_buffer(audiofilters_phaser_obj_t *self, @@ -121,8 +121,8 @@ void audiofilters_phaser_reset_buffer(audiofilters_phaser_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); - memset(self->word_buffer, 0, self->base.channel_count * sizeof(int32_t)); - memset(self->allpass_buffer, 0, self->base.channel_count * self->stages * sizeof(int32_t)); + memset(self->word_buffer, 0, self->base.channel_count * sizeof(int16_t)); + memset(self->allpass_buffer, 0, self->base.channel_count * self->stages * sizeof(int16_t)); } bool common_hal_audiofilters_phaser_get_playing(audiofilters_phaser_obj_t *self) { @@ -253,7 +253,7 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o } } - int32_t word = synthio_sat16(sample_word + synthio_sat16(self->word_buffer[right_channel] * feedback, 15), 0); + int32_t word = synthio_sat16(sample_word + synthio_sat16((int32_t)self->word_buffer[right_channel] * feedback, 15), 0); int32_t allpass_word = 0; // Update all-pass filters @@ -262,7 +262,7 @@ audioio_get_buffer_result_t audiofilters_phaser_get_buffer(audiofilters_phaser_o self->allpass_buffer[j + allpass_buffer_offset] = synthio_sat16(synthio_sat16(allpass_word * allpasscoef, 15) + word, 0); word = allpass_word; } - self->word_buffer[(bool)allpass_buffer_offset] = word; + self->word_buffer[(bool)allpass_buffer_offset] = (int16_t)word; // Add original sample + effect word = sample_word + (int32_t)(synthio_sat16(word * mix, 15)); diff --git a/shared-module/audiofilters/Phaser.h b/shared-module/audiofilters/Phaser.h index 707a2b0f0841d..f627b147014a0 100644 --- a/shared-module/audiofilters/Phaser.h +++ b/shared-module/audiofilters/Phaser.h @@ -32,8 +32,8 @@ typedef struct { bool loop; bool more_data; - int32_t *allpass_buffer; - int32_t *word_buffer; + int16_t *allpass_buffer; + int16_t *word_buffer; mp_obj_t sample; } audiofilters_phaser_obj_t;