From 5246053563e553cca8a33bbd9c311ac891c99c49 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Tue, 22 Oct 2024 09:17:58 -0500 Subject: [PATCH 1/6] Initial structure for biquad filter effect. --- py/circuitpy_defns.mk | 5 + py/circuitpy_mpconfig.mk | 2 + shared-bindings/audiofilters/Filter.c | 266 ++++++++++++++++++++++ shared-bindings/audiofilters/Filter.h | 33 +++ shared-bindings/audiofilters/__init__.c | 33 +++ shared-bindings/audiofilters/__init__.h | 7 + shared-module/audiofilters/Filter.c | 281 ++++++++++++++++++++++++ shared-module/audiofilters/Filter.h | 51 +++++ shared-module/audiofilters/__init__.c | 5 + shared-module/audiofilters/__init__.h | 7 + 10 files changed, 690 insertions(+) create mode 100644 shared-bindings/audiofilters/Filter.c create mode 100644 shared-bindings/audiofilters/Filter.h create mode 100644 shared-bindings/audiofilters/__init__.c create mode 100644 shared-bindings/audiofilters/__init__.h create mode 100644 shared-module/audiofilters/Filter.c create mode 100644 shared-module/audiofilters/Filter.h create mode 100644 shared-module/audiofilters/__init__.c create mode 100644 shared-module/audiofilters/__init__.h diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index c2c9ffa6736e8..9d6c73064e0bf 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -134,6 +134,9 @@ endif ifeq ($(CIRCUITPY_AUDIODELAYS),1) SRC_PATTERNS += audiodelays/% endif +ifeq ($(CIRCUITPY_AUDIOFILTERS),1) +SRC_PATTERNS += audiofilters/% +endif ifeq ($(CIRCUITPY_AUDIOMIXER),1) SRC_PATTERNS += audiomixer/% endif @@ -622,6 +625,8 @@ SRC_SHARED_MODULE_ALL = \ audiocore/__init__.c \ audiodelays/Echo.c \ audiodelays/__init__.c \ + audiofilters/Distortion.c \ + audiofilters/__init__.c \ audioio/__init__.c \ audiomixer/Mixer.c \ audiomixer/MixerVoice.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index a8e44a12c9418..e425552911929 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -144,6 +144,8 @@ CFLAGS += -DCIRCUITPY_AUDIOMP3=$(CIRCUITPY_AUDIOMP3) CIRCUITPY_AUDIOEFFECTS ?= 0 CIRCUITPY_AUDIODELAYS ?= $(CIRCUITPY_AUDIOEFFECTS) CFLAGS += -DCIRCUITPY_AUDIODELAYS=$(CIRCUITPY_AUDIODELAYS) +CIRCUITPY_AUDIOFILTERS ?= $(CIRCUITPY_AUDIOEFFECTS) +CFLAGS += -DCIRCUITPY_AUDIOFILTERS=$(CIRCUITPY_AUDIOFILTERS) CIRCUITPY_AURORA_EPAPER ?= 0 CFLAGS += -DCIRCUITPY_AURORA_EPAPER=$(CIRCUITPY_AURORA_EPAPER) diff --git a/shared-bindings/audiofilters/Filter.c b/shared-bindings/audiofilters/Filter.c new file mode 100644 index 0000000000000..f78c909d21bc6 --- /dev/null +++ b/shared-bindings/audiofilters/Filter.c @@ -0,0 +1,266 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared-bindings/audiofilters/Filter.h" +#include "shared-module/audiofilters/Filter.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" + +#define MIX_DEFAULT 1.0f + +//| class Filter: +//| """A Filter effect""" +//| +//| def __init__( +//| self, +//| biquad: synthio.Biquad = None, +//| mix: synthio.BlockInput = 1.0, +//| buffer_size: int = 512, +//| sample_rate: int = 8000, +//| bits_per_sample: int = 16, +//| samples_signed: bool = True, +//| channel_count: int = 1, +//| ) -> None: +//| """Create a Filter effect where the original sample is processed through a biquad filter +//| created by a synthio.Synthesizer object. This can be used to generate a low-pass, +//| high-pass, or band-pass filter. +//| +//| The mix parameter allows you to change how much of the unchanged sample passes through to +//| the output to how much of the effect audio you hear as the output. +//| +//| :param synthio.Biquad biquad: The normalized biquad filter object used to process the 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 filter 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) +//| filter = audiofilters.Filter(biquad=synth.low_pass_filter(frequency=2000, q_factor=1.25), buffer_size=1024, channel_count=1, sample_rate=44100, mix=1.0) +//| filter.play(synth) +//| audio.play(filter) +//| +//| note = synthio.Note(261) +//| while True: +//| synth.press(note) +//| time.sleep(0.25) +//| synth.release(note) +//| time.sleep(5)""" +//| ... +static mp_obj_t audiofilters_filter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_biquad, 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_biquad, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} }, + { 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} }, + { 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_filter_obj_t *self = mp_obj_malloc(audiofilters_filter_obj_t, &audiofilters_filter_type); + common_hal_audiofilters_filter_construct(self, args[ARG_biquad].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); +} + +//| def deinit(self) -> None: +//| """Deinitialises the Filter.""" +//| ... +static mp_obj_t audiofilters_filter_deinit(mp_obj_t self_in) { + audiofilters_filter_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilters_filter_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_deinit_obj, audiofilters_filter_deinit); + +static void check_for_deinit(audiofilters_filter_obj_t *self) { + if (common_hal_audiofilters_filter_deinited(self)) { + raise_deinited_error(); + } +} + +//| def __enter__(self) -> Filter: +//| """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.""" +//| ... +static mp_obj_t audiofilters_filter_obj___exit__(size_t n_args, const mp_obj_t *args) { + (void)n_args; + common_hal_audiofilters_filter_deinit(args[0]); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiofilters_filter___exit___obj, 4, 4, audiofilters_filter_obj___exit__); + + +//| biquad: synthio.Biquad +//| """The normalized biquad filter object used to process the signal.""" +static mp_obj_t audiofilters_filter_obj_get_biquad(mp_obj_t self_in) { + return common_hal_audiofilters_filter_get_biquad(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_get_biquad_obj, audiofilters_filter_obj_get_biquad); + +static mp_obj_t audiofilters_filter_obj_set_biquad(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_biquad }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_biquad, MP_ARG_OBJ | MP_ARG_REQUIRED, {} }, + }; + audiofilters_filter_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + 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); + + common_hal_audiofilters_filter_set_biquad(self, args[ARG_biquad].u_obj); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_filter_set_biquad_obj, 1, audiofilters_filter_obj_set_biquad); + +MP_PROPERTY_GETSET(audiofilters_filter_biquad_obj, + (mp_obj_t)&audiofilters_filter_get_biquad_obj, + (mp_obj_t)&audiofilters_filter_set_biquad_obj); + + +//| mix: synthio.BlockInput +//| """The rate the filtered signal mix between 0 and 1 where 0 is only sample and 1 is all effect.""" +static mp_obj_t audiofilters_filter_obj_get_mix(mp_obj_t self_in) { + return common_hal_audiofilters_filter_get_mix(self_in); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_get_mix_obj, audiofilters_filter_obj_get_mix); + +static mp_obj_t audiofilters_filter_obj_set_mix(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_mix }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_mix, MP_ARG_OBJ | MP_ARG_REQUIRED, {} }, + }; + audiofilters_filter_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + 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); + + common_hal_audiofilters_filter_set_mix(self, args[ARG_mix].u_obj); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_filter_set_mix_obj, 1, audiofilters_filter_obj_set_mix); + +MP_PROPERTY_GETSET(audiofilters_filter_mix_obj, + (mp_obj_t)&audiofilters_filter_get_mix_obj, + (mp_obj_t)&audiofilters_filter_set_mix_obj); + + +//| playing: bool +//| """True when the effect is playing a sample. (read-only)""" +static mp_obj_t audiofilters_filter_obj_get_playing(mp_obj_t self_in) { + audiofilters_filter_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiofilters_filter_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_get_playing_obj, audiofilters_filter_obj_get_playing); + +MP_PROPERTY_GETTER(audiofilters_filter_playing_obj, + (mp_obj_t)&audiofilters_filter_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_filter_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_filter_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_filter_play(self, sample, args[ARG_loop].u_bool); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_filter_play_obj, 1, audiofilters_filter_obj_play); + +//| def stop(self) -> None: +//| """Stops playback of the sample.""" +//| ... +//| +static mp_obj_t audiofilters_filter_obj_stop(mp_obj_t self_in) { + audiofilters_filter_obj_t *self = MP_OBJ_TO_PTR(self_in); + + common_hal_audiofilters_filter_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_stop_obj, audiofilters_filter_obj_stop); + +static const mp_rom_map_elem_t audiofilters_filter_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiofilters_filter_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&audiofilters_filter___exit___obj) }, + { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiofilters_filter_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiofilters_filter_stop_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofilters_filter_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_biquad), MP_ROM_PTR(&audiofilters_filter_biquad_obj) }, + { MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiofilters_filter_mix_obj) }, +}; +static MP_DEFINE_CONST_DICT(audiofilters_filter_locals_dict, audiofilters_filter_locals_dict_table); + +static const audiosample_p_t audiofilters_filter_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + .sample_rate = (audiosample_sample_rate_fun)common_hal_audiofilters_filter_get_sample_rate, + .bits_per_sample = (audiosample_bits_per_sample_fun)common_hal_audiofilters_filter_get_bits_per_sample, + .channel_count = (audiosample_channel_count_fun)common_hal_audiofilters_filter_get_channel_count, + .reset_buffer = (audiosample_reset_buffer_fun)audiofilters_filter_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)audiofilters_filter_get_buffer, + .get_buffer_structure = (audiosample_get_buffer_structure_fun)audiofilters_filter_get_buffer_structure, +}; + +MP_DEFINE_CONST_OBJ_TYPE( + audiofilters_filter_type, + MP_QSTR_Filter, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audiofilters_filter_make_new, + locals_dict, &audiofilters_filter_locals_dict, + protocol, &audiofilters_filter_proto + ); diff --git a/shared-bindings/audiofilters/Filter.h b/shared-bindings/audiofilters/Filter.h new file mode 100644 index 0000000000000..6a58b1ba1a968 --- /dev/null +++ b/shared-bindings/audiofilters/Filter.h @@ -0,0 +1,33 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiofilters/Filter.h" + +extern const mp_obj_type_t audiofilters_filter_type; + +void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self, + mp_obj_t biquad, mp_obj_t mix, + uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, + uint8_t channel_count, uint32_t sample_rate); + +void common_hal_audiofilters_filter_deinit(audiofilters_filter_obj_t *self); +bool common_hal_audiofilters_filter_deinited(audiofilters_filter_obj_t *self); + +uint32_t common_hal_audiofilters_filter_get_sample_rate(audiofilters_filter_obj_t *self); +uint8_t common_hal_audiofilters_filter_get_channel_count(audiofilters_filter_obj_t *self); +uint8_t common_hal_audiofilters_filter_get_bits_per_sample(audiofilters_filter_obj_t *self); + +mp_obj_t common_hal_audiofilters_filter_get_biquad(audiofilters_filter_obj_t *self); +void common_hal_audiofilters_filter_set_biquad(audiofilters_filter_obj_t *self, mp_obj_t arg); + +mp_obj_t common_hal_audiofilters_filter_get_mix(audiofilters_filter_obj_t *self); +void common_hal_audiofilters_filter_set_mix(audiofilters_filter_obj_t *self, mp_obj_t arg); + +bool common_hal_audiofilters_filter_get_playing(audiofilters_filter_obj_t *self); +void common_hal_audiofilters_filter_play(audiofilters_filter_obj_t *self, mp_obj_t sample, bool loop); +void common_hal_audiofilters_filter_stop(audiofilters_filter_obj_t *self); diff --git a/shared-bindings/audiofilters/__init__.c b/shared-bindings/audiofilters/__init__.c new file mode 100644 index 0000000000000..c4124515b7d36 --- /dev/null +++ b/shared-bindings/audiofilters/__init__.c @@ -0,0 +1,33 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/audiofilters/__init__.h" +#include "shared-bindings/audiofilters/Filter.h" + +//| """Support for audio filter effects +//| +//| The `audiofilters` module contains classes to provide access to audio filter effects. +//| +//| """ + +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) }, +}; + +static MP_DEFINE_CONST_DICT(audiofilters_module_globals, audiofilters_module_globals_table); + +const mp_obj_module_t audiofilters_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&audiofilters_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_audiofilters, audiofilters_module); diff --git a/shared-bindings/audiofilters/__init__.h b/shared-bindings/audiofilters/__init__.h new file mode 100644 index 0000000000000..29d2f6726559b --- /dev/null +++ b/shared-bindings/audiofilters/__init__.h @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c new file mode 100644 index 0000000000000..da3d106379b9c --- /dev/null +++ b/shared-module/audiofilters/Filter.c @@ -0,0 +1,281 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT +#include "shared-bindings/audiofilters/Filter.h" + +#include +#include "py/runtime.h" + +void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self, + mp_obj_t biquad, mp_obj_t mix, + 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->bits_per_sample = bits_per_sample; // Most common is 16, but 8 is also supported in many places + self->samples_signed = samples_signed; // Are the samples we provide signed (common is true) + self->channel_count = channel_count; // Channels can be 1 for mono or 2 for stereo + self->sample_rate = sample_rate; // Sample rate for the effect, this generally needs to match all audio objects + + // 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(self->buffer_len); + if (self->buffer[0] == NULL) { + common_hal_audiofilters_filter_deinit(self); + m_malloc_fail(self->buffer_len); + } + memset(self->buffer[0], 0, self->buffer_len); + + self->buffer[1] = m_malloc(self->buffer_len); + if (self->buffer[1] == NULL) { + common_hal_audiofilters_filter_deinit(self); + m_malloc_fail(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 + + // 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. + + if (biquad != MP_OBJ_NULL) { + biquad = mp_const_none; + } + synthio_biquad_filter_assign(biquad, &self->biquad); + + // If we did not receive a BlockInput we need to create a default float value + if (mix == MP_OBJ_NULL) { + mix = mp_obj_new_float(1.0); + } + synthio_block_assign_slot(mix, &self->mix, MP_QSTR_mix); +} + +bool common_hal_audiofilters_filter_deinited(audiofilters_filter_obj_t *self) { + if (self->buffer[0] == NULL) { + return true; + } + return false; +} + +void common_hal_audiofilters_filter_deinit(audiofilters_filter_obj_t *self) { + if (common_hal_audiofilters_filter_deinited(self)) { + return; + } + self->buffer[0] = NULL; + self->buffer[1] = NULL; +} + +mp_obj_t common_hal_audiofilters_filter_get_biquad(audiofilters_filter_obj_t *self) { + return self->biquad.obj; +} + +void common_hal_audiofilters_filter_set_biquad(audiofilters_filter_obj_t *self, mp_obj_t arg) { + synthio_biquad_filter_assign(arg, &self->biquad); +} + +mp_obj_t common_hal_audiofilters_filter_get_mix(audiofilters_filter_obj_t *self) { + return self->mix.obj; +} + +void common_hal_audiofilters_filter_set_mix(audiofilters_filter_obj_t *self, mp_obj_t arg) { + synthio_block_assign_slot(arg, &self->mix, MP_QSTR_mix); +} + +uint32_t common_hal_audiofilters_filter_get_sample_rate(audiofilters_filter_obj_t *self) { + return self->sample_rate; +} + +uint8_t common_hal_audiofilters_filter_get_channel_count(audiofilters_filter_obj_t *self) { + return self->channel_count; +} + +uint8_t common_hal_audiofilters_filter_get_bits_per_sample(audiofilters_filter_obj_t *self) { + return self->bits_per_sample; +} + +void audiofilters_filter_reset_buffer(audiofilters_filter_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_filter_get_playing(audiofilters_filter_obj_t *self) { + return self->sample != NULL; +} + +void common_hal_audiofilters_filter_play(audiofilters_filter_obj_t *self, mp_obj_t sample, bool loop) { + // When a sample is to be played we must ensure the samples values matches what we expect + // Then we reset the sample and get the first buffer to play + // The get_buffer function will actually process that data + + if (audiosample_sample_rate(sample) != self->sample_rate) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_sample_rate); + } + if (audiosample_channel_count(sample) != self->channel_count) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_channel_count); + } + if (audiosample_bits_per_sample(sample) != self->bits_per_sample) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_bits_per_sample); + } + bool single_buffer; + bool samples_signed; + uint32_t max_buffer_length; + uint8_t spacing; + audiosample_get_buffer_structure(sample, false, &single_buffer, &samples_signed, &max_buffer_length, &spacing); + if (samples_signed != self->samples_signed) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_signedness); + } + + 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->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_filter_stop(audiofilters_filter_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_filter_get_buffer(audiofilters_filter_obj_t *self, bool single_channel_output, uint8_t channel, + uint8_t **buffer, uint32_t *buffer_length) { + + if (!single_channel_output) { + channel = 0; + } + + // get the effect values we need from the BlockInput. These may change at run time so you need to do bounds checking if required + mp_float_t mix = MIN(1.0, MAX(synthio_block_slot_get(&self->mix), 0.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->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->bits_per_sample / 8); + self->more_data = result == GET_BUFFER_MORE_DATA; + } + } + + // If we have a sample, filter it + if (self->sample != NULL) { + // 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(self->sample_buffer_length, length); + + 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 + + if (mix <= 0.01) { // if mix is zero pure sample only + for (uint32_t i = 0; i < n; i++) { + if (MP_LIKELY(self->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->bits_per_sample == 16)) { + sample_word = sample_src[i]; + } else { + if (self->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: Filter through synthio_biquad_filter_samples + + if (MP_LIKELY(self->bits_per_sample == 16)) { + word_buffer[i] = (sample_word * (1.0 - mix)) + (word * mix); + if (!self->samples_signed) { + word_buffer[i] ^= 0x8000; + } + } else { + int8_t mixed = (sample_word * (1.0 - mix)) + (word * mix); + if (self->samples_signed) { + hword_buffer[i] = mixed; + } else { + hword_buffer[i] = (uint8_t)mixed ^ 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->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; + + // Filter 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; +} + +void audiofilters_filter_get_buffer_structure(audiofilters_filter_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, uint32_t *max_buffer_length, uint8_t *spacing) { + + // Return information about the effect's buffer (not the sample's) + // These are used by calling audio objects to determine how to handle the effect's buffer + *single_buffer = false; + *samples_signed = self->samples_signed; + *max_buffer_length = self->buffer_len; + if (single_channel_output) { + *spacing = self->channel_count; + } else { + *spacing = 1; + } +} diff --git a/shared-module/audiofilters/Filter.h b/shared-module/audiofilters/Filter.h new file mode 100644 index 0000000000000..dc29743a55634 --- /dev/null +++ b/shared-module/audiofilters/Filter.h @@ -0,0 +1,51 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "py/obj.h" + +#include "shared-module/audiocore/__init__.h" +#include "shared-module/synthio/block.h" +#include "shared-module/synthio/Biquad.h" + +extern const mp_obj_type_t audiofilters_filter_type; + +typedef struct { + mp_obj_base_t base; + biquad_filter_state biquad; + synthio_block_slot_t mix; + + uint8_t bits_per_sample; + bool samples_signed; + uint8_t channel_count; + uint32_t sample_rate; + + 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_filter_obj_t; + +void audiofilters_filter_reset_buffer(audiofilters_filter_obj_t *self, + bool single_channel_output, + uint8_t channel); + +audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length); // length in bytes + +void audiofilters_filter_get_buffer_structure(audiofilters_filter_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, + uint32_t *max_buffer_length, uint8_t *spacing); diff --git a/shared-module/audiofilters/__init__.c b/shared-module/audiofilters/__init__.c new file mode 100644 index 0000000000000..83929b4c4fb82 --- /dev/null +++ b/shared-module/audiofilters/__init__.c @@ -0,0 +1,5 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT diff --git a/shared-module/audiofilters/__init__.h b/shared-module/audiofilters/__init__.h new file mode 100644 index 0000000000000..29d2f6726559b --- /dev/null +++ b/shared-module/audiofilters/__init__.h @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once From 1bf400d2dd3a315ae6f40f20d9cf4dd82f205734 Mon Sep 17 00:00:00 2001 From: dcooperdalrymple Date: Tue, 22 Oct 2024 12:08:33 -0500 Subject: [PATCH 2/6] Rename `biquad` property to `filter`. --- py/circuitpy_defns.mk | 2 +- shared-bindings/audiofilters/Filter.c | 38 +++++++++++++-------------- shared-bindings/audiofilters/Filter.h | 6 ++--- shared-module/audiofilters/Filter.c | 20 ++++++++------ shared-module/audiofilters/Filter.h | 4 ++- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 9d6c73064e0bf..61516561822c7 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -625,7 +625,7 @@ SRC_SHARED_MODULE_ALL = \ audiocore/__init__.c \ audiodelays/Echo.c \ audiodelays/__init__.c \ - audiofilters/Distortion.c \ + audiofilters/Filter.c \ audiofilters/__init__.c \ audioio/__init__.c \ audiomixer/Mixer.c \ diff --git a/shared-bindings/audiofilters/Filter.c b/shared-bindings/audiofilters/Filter.c index f78c909d21bc6..dd3b74f7854e2 100644 --- a/shared-bindings/audiofilters/Filter.c +++ b/shared-bindings/audiofilters/Filter.c @@ -23,7 +23,7 @@ //| //| def __init__( //| self, -//| biquad: synthio.Biquad = None, +//| filter: synthio.Biquad = None, //| mix: synthio.BlockInput = 1.0, //| buffer_size: int = 512, //| sample_rate: int = 8000, @@ -38,7 +38,7 @@ //| The mix parameter allows you to change how much of the unchanged sample passes through to //| the output to how much of the effect audio you hear as the output. //| -//| :param synthio.Biquad biquad: The normalized biquad filter object used to process the signal. +//| :param synthio.Biquad filter: The normalized biquad filter object used to process the 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 @@ -56,7 +56,7 @@ //| //| audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22) //| synth = synthio.Synthesizer(channel_count=1, sample_rate=44100) -//| filter = audiofilters.Filter(biquad=synth.low_pass_filter(frequency=2000, q_factor=1.25), buffer_size=1024, channel_count=1, sample_rate=44100, mix=1.0) +//| filter = audiofilters.Filter(filter=synth.low_pass_filter(frequency=2000, Q=1.25), buffer_size=1024, channel_count=1, sample_rate=44100, mix=1.0) //| filter.play(synth) //| audio.play(filter) //| @@ -68,9 +68,9 @@ //| time.sleep(5)""" //| ... static mp_obj_t audiofilters_filter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_biquad, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, }; + enum { ARG_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_biquad, 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_OBJ_NULL} }, { 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} }, @@ -90,7 +90,7 @@ static mp_obj_t audiofilters_filter_make_new(const mp_obj_type_t *type, size_t n } audiofilters_filter_obj_t *self = mp_obj_malloc(audiofilters_filter_obj_t, &audiofilters_filter_type); - common_hal_audiofilters_filter_construct(self, args[ARG_biquad].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_audiofilters_filter_construct(self, 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); return MP_OBJ_FROM_PTR(self); } @@ -128,31 +128,31 @@ static mp_obj_t audiofilters_filter_obj___exit__(size_t n_args, const mp_obj_t * static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiofilters_filter___exit___obj, 4, 4, audiofilters_filter_obj___exit__); -//| biquad: synthio.Biquad +//| filter: synthio.Biquad //| """The normalized biquad filter object used to process the signal.""" -static mp_obj_t audiofilters_filter_obj_get_biquad(mp_obj_t self_in) { - return common_hal_audiofilters_filter_get_biquad(self_in); +static mp_obj_t audiofilters_filter_obj_get_filter(mp_obj_t self_in) { + return common_hal_audiofilters_filter_get_filter(self_in); } -MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_get_biquad_obj, audiofilters_filter_obj_get_biquad); +MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_get_filter_obj, audiofilters_filter_obj_get_filter); -static mp_obj_t audiofilters_filter_obj_set_biquad(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - enum { ARG_biquad }; +static mp_obj_t audiofilters_filter_obj_set_filter(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_filter }; static const mp_arg_t allowed_args[] = { - { MP_QSTR_biquad, MP_ARG_OBJ | MP_ARG_REQUIRED, {} }, + { MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_REQUIRED, {} }, }; audiofilters_filter_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); 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); - common_hal_audiofilters_filter_set_biquad(self, args[ARG_biquad].u_obj); + common_hal_audiofilters_filter_set_filter(self, args[ARG_filter].u_obj); return mp_const_none; } -MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_filter_set_biquad_obj, 1, audiofilters_filter_obj_set_biquad); +MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_filter_set_filter_obj, 1, audiofilters_filter_obj_set_filter); -MP_PROPERTY_GETSET(audiofilters_filter_biquad_obj, - (mp_obj_t)&audiofilters_filter_get_biquad_obj, - (mp_obj_t)&audiofilters_filter_set_biquad_obj); +MP_PROPERTY_GETSET(audiofilters_filter_filter_obj, + (mp_obj_t)&audiofilters_filter_get_filter_obj, + (mp_obj_t)&audiofilters_filter_set_filter_obj); //| mix: synthio.BlockInput @@ -241,7 +241,7 @@ static const mp_rom_map_elem_t audiofilters_filter_locals_dict_table[] = { // Properties { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofilters_filter_playing_obj) }, - { MP_ROM_QSTR(MP_QSTR_biquad), MP_ROM_PTR(&audiofilters_filter_biquad_obj) }, + { MP_ROM_QSTR(MP_QSTR_filter), MP_ROM_PTR(&audiofilters_filter_filter_obj) }, { MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiofilters_filter_mix_obj) }, }; static MP_DEFINE_CONST_DICT(audiofilters_filter_locals_dict, audiofilters_filter_locals_dict_table); diff --git a/shared-bindings/audiofilters/Filter.h b/shared-bindings/audiofilters/Filter.h index 6a58b1ba1a968..739b625ee6c51 100644 --- a/shared-bindings/audiofilters/Filter.h +++ b/shared-bindings/audiofilters/Filter.h @@ -11,7 +11,7 @@ extern const mp_obj_type_t audiofilters_filter_type; void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self, - mp_obj_t biquad, mp_obj_t mix, + 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); @@ -22,8 +22,8 @@ uint32_t common_hal_audiofilters_filter_get_sample_rate(audiofilters_filter_obj_ uint8_t common_hal_audiofilters_filter_get_channel_count(audiofilters_filter_obj_t *self); uint8_t common_hal_audiofilters_filter_get_bits_per_sample(audiofilters_filter_obj_t *self); -mp_obj_t common_hal_audiofilters_filter_get_biquad(audiofilters_filter_obj_t *self); -void common_hal_audiofilters_filter_set_biquad(audiofilters_filter_obj_t *self, mp_obj_t arg); +mp_obj_t common_hal_audiofilters_filter_get_filter(audiofilters_filter_obj_t *self); +void common_hal_audiofilters_filter_set_filter(audiofilters_filter_obj_t *self, mp_obj_t arg); mp_obj_t common_hal_audiofilters_filter_get_mix(audiofilters_filter_obj_t *self); void common_hal_audiofilters_filter_set_mix(audiofilters_filter_obj_t *self, mp_obj_t arg); diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index da3d106379b9c..06788351c24f1 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -9,7 +9,7 @@ #include "py/runtime.h" void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self, - mp_obj_t biquad, mp_obj_t mix, + 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) { @@ -52,10 +52,11 @@ void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self, // The below section sets up the effect's starting values. - if (biquad != MP_OBJ_NULL) { - biquad = mp_const_none; + if (filter == MP_OBJ_NULL) { + filter = mp_const_none; } - synthio_biquad_filter_assign(biquad, &self->biquad); + synthio_biquad_filter_assign(&self->filter_state, filter); + self->filter_obj = filter; // If we did not receive a BlockInput we need to create a default float value if (mix == MP_OBJ_NULL) { @@ -79,12 +80,13 @@ void common_hal_audiofilters_filter_deinit(audiofilters_filter_obj_t *self) { self->buffer[1] = NULL; } -mp_obj_t common_hal_audiofilters_filter_get_biquad(audiofilters_filter_obj_t *self) { - return self->biquad.obj; +mp_obj_t common_hal_audiofilters_filter_get_filter(audiofilters_filter_obj_t *self) { + return self->filter_obj; } -void common_hal_audiofilters_filter_set_biquad(audiofilters_filter_obj_t *self, mp_obj_t arg) { - synthio_biquad_filter_assign(arg, &self->biquad); +void common_hal_audiofilters_filter_set_filter(audiofilters_filter_obj_t *self, mp_obj_t arg) { + synthio_biquad_filter_assign(&self->filter_state, arg); + self->filter_obj = arg; } mp_obj_t common_hal_audiofilters_filter_get_mix(audiofilters_filter_obj_t *self) { @@ -113,6 +115,8 @@ void audiofilters_filter_reset_buffer(audiofilters_filter_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); + + synthio_biquad_filter_reset(&self->filter_state); } bool common_hal_audiofilters_filter_get_playing(audiofilters_filter_obj_t *self) { diff --git a/shared-module/audiofilters/Filter.h b/shared-module/audiofilters/Filter.h index dc29743a55634..777f5a077de04 100644 --- a/shared-module/audiofilters/Filter.h +++ b/shared-module/audiofilters/Filter.h @@ -15,9 +15,11 @@ extern const mp_obj_type_t audiofilters_filter_type; typedef struct { mp_obj_base_t base; - biquad_filter_state biquad; + mp_obj_t filter_obj; synthio_block_slot_t mix; + biquad_filter_state filter_state; + uint8_t bits_per_sample; bool samples_signed; uint8_t channel_count; From db540c67ece8ac7dac816f4badce55fa4087826d Mon Sep 17 00:00:00 2001 From: dcooperdalrymple Date: Tue, 22 Oct 2024 12:09:07 -0500 Subject: [PATCH 3/6] Avoid processing sample if `filter` is `None`. --- shared-module/audiofilters/Filter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index 06788351c24f1..a537261af6ed7 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -212,7 +212,7 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o 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 - if (mix <= 0.01) { // if mix is zero pure sample only + if (mix <= 0.01 || self->filter_obj == mp_const_none) { // if mix is zero pure sample only or no biquad filter object is provided for (uint32_t i = 0; i < n; i++) { if (MP_LIKELY(self->bits_per_sample == 16)) { word_buffer[i] = sample_src[i]; From e7c02bd56194a86d4c8a3d743555ddb9517b0b0d Mon Sep 17 00:00:00 2001 From: dcooperdalrymple Date: Tue, 22 Oct 2024 12:09:28 -0500 Subject: [PATCH 4/6] Biquad filter processing. --- shared-module/audiofilters/Filter.c | 101 ++++++++++++++++++++++------ shared-module/audiofilters/Filter.h | 2 + 2 files changed, 83 insertions(+), 20 deletions(-) diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index a537261af6ed7..af353563a6c6d 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -43,6 +43,22 @@ void common_hal_audiofilters_filter_construct(audiofilters_filter_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[0] = m_malloc(SYNTHIO_MAX_DUR * sizeof(int32_t)); + if (self->filter_buffer[0] == NULL) { + common_hal_audiofilters_filter_deinit(self); + m_malloc_fail(SYNTHIO_MAX_DUR * sizeof(int32_t)); + } + memset(self->filter_buffer[0], 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); + + // This buffer will be used to mix original sample with processed signal + self->filter_buffer[1] = m_malloc(SYNTHIO_MAX_DUR * sizeof(int32_t)); + if (self->filter_buffer[1] == NULL) { + common_hal_audiofilters_filter_deinit(self); + m_malloc_fail(SYNTHIO_MAX_DUR * sizeof(int32_t)); + } + memset(self->filter_buffer[1], 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 @@ -78,6 +94,8 @@ void common_hal_audiofilters_filter_deinit(audiofilters_filter_obj_t *self) { } self->buffer[0] = NULL; self->buffer[1] = NULL; + self->filter_buffer[0] = NULL; + self->filter_buffer[1] = NULL; } mp_obj_t common_hal_audiofilters_filter_get_filter(audiofilters_filter_obj_t *self) { @@ -115,6 +133,8 @@ void audiofilters_filter_reset_buffer(audiofilters_filter_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); + memset(self->filter_buffer[0], 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); + memset(self->filter_buffer[1], 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); synthio_biquad_filter_reset(&self->filter_state); } @@ -166,6 +186,32 @@ void common_hal_audiofilters_filter_stop(audiofilters_filter_obj_t *self) { return; } +#define RANGE_LOW_16 (-28000) +#define RANGE_HIGH_16 (28000) +#define RANGE_SHIFT_16 (16) +#define RANGE_SCALE_16 (0xfffffff / (32768 * 2 - RANGE_HIGH_16)) // 2 for echo+sample + +// dynamic range compression via a downward compressor with hard knee +// +// When the output value is within the range +-28000 (about 85% of full scale), +// it is unchanged. Otherwise, it undergoes a gain reduction so that the +// largest possible values, (+32768,-32767) * 2 (2 for echo and sample), +// still fit within the output range +// +// This produces a much louder overall volume with multiple voices, without +// much additional processing. +// +// https://en.wikipedia.org/wiki/Dynamic_range_compression +static +int16_t mix_down_sample(int32_t sample) { + if (sample < RANGE_LOW_16) { + sample = (((sample - RANGE_LOW_16) * RANGE_SCALE_16) >> RANGE_SHIFT_16) + RANGE_LOW_16; + } else if (sample > RANGE_HIGH_16) { + sample = (((sample - RANGE_HIGH_16) * RANGE_SCALE_16) >> RANGE_SHIFT_16) + RANGE_HIGH_16; + } + return sample; +} + audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_obj_t *self, bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { @@ -221,34 +267,49 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o } } } else { - for (uint32_t i = 0; i < n; i++) { - int32_t sample_word = 0; - if (MP_LIKELY(self->bits_per_sample == 16)) { - sample_word = sample_src[i]; - } else { - if (self->samples_signed) { - sample_word = sample_hsrc[i]; + uint32_t i = 0; + while (i < n) { + uint32_t n_samples = MIN(SYNTHIO_MAX_DUR, n - i); + + // Fill filter buffer with samples + for (uint32_t j = 0; j < n_samples; j++) { + if (MP_LIKELY(self->bits_per_sample == 16)) { + self->filter_buffer[0][j] = sample_src[i + j]; } 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); + if (self->samples_signed) { + self->filter_buffer[0][j] = sample_hsrc[i + j]; + } else { + // Be careful here changing from an 8 bit unsigned to signed into a 32-bit signed + self->filter_buffer[0][j] = (int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80); + } } } - // TODO: Filter through synthio_biquad_filter_samples + // Copy original signal for mixing back in later + memcpy(self->filter_buffer[1], self->filter_buffer[0], n_samples); - if (MP_LIKELY(self->bits_per_sample == 16)) { - word_buffer[i] = (sample_word * (1.0 - mix)) + (word * mix); - if (!self->samples_signed) { - word_buffer[i] ^= 0x8000; - } - } else { - int8_t mixed = (sample_word * (1.0 - mix)) + (word * mix); - if (self->samples_signed) { - hword_buffer[i] = mixed; + // Process biquad filter + synthio_biquad_filter_samples(&self->filter_state, self->filter_buffer[0], n_samples); + + // Mix processed signal with original sample and transfer to output buffer + for (uint32_t j = 0; j < n_samples; j++) { + int32_t word = (self->filter_buffer[1][j] * (1.0 - mix)) + (self->filter_buffer[0][j] * mix); + if (MP_LIKELY(self->bits_per_sample == 16)) { + word_buffer[i + j] = mix_down_sample(word); + if (!self->samples_signed) { + word_buffer[i + j] ^= 0x8000; + } } else { - hword_buffer[i] = (uint8_t)mixed ^ 0x80; + int8_t mixed = word; + if (self->samples_signed) { + hword_buffer[i + j] = mixed; + } else { + hword_buffer[i + j] = (uint8_t)mixed ^ 0x80; + } } } + + i += n_samples; } } diff --git a/shared-module/audiofilters/Filter.h b/shared-module/audiofilters/Filter.h index 777f5a077de04..dd774e3dbf78c 100644 --- a/shared-module/audiofilters/Filter.h +++ b/shared-module/audiofilters/Filter.h @@ -32,6 +32,8 @@ typedef struct { uint8_t *sample_remaining_buffer; uint32_t sample_buffer_length; + int32_t *filter_buffer[2]; + bool loop; bool more_data; From c7e87cfae3930a92ea279d8fff750da8377123aa Mon Sep 17 00:00:00 2001 From: dcooperdalrymple Date: Tue, 22 Oct 2024 21:33:28 -0500 Subject: [PATCH 5/6] Remove unnecessary double buffer on `filter_buffer`. --- shared-module/audiofilters/Filter.c | 39 +++++++++-------------------- shared-module/audiofilters/Filter.h | 2 +- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/shared-module/audiofilters/Filter.c b/shared-module/audiofilters/Filter.c index af353563a6c6d..86edb6cb04183 100644 --- a/shared-module/audiofilters/Filter.c +++ b/shared-module/audiofilters/Filter.c @@ -44,20 +44,12 @@ void common_hal_audiofilters_filter_construct(audiofilters_filter_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[0] = m_malloc(SYNTHIO_MAX_DUR * sizeof(int32_t)); - if (self->filter_buffer[0] == NULL) { + self->filter_buffer = m_malloc(SYNTHIO_MAX_DUR * sizeof(int32_t)); + if (self->filter_buffer == NULL) { common_hal_audiofilters_filter_deinit(self); m_malloc_fail(SYNTHIO_MAX_DUR * sizeof(int32_t)); } - memset(self->filter_buffer[0], 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); - - // This buffer will be used to mix original sample with processed signal - self->filter_buffer[1] = m_malloc(SYNTHIO_MAX_DUR * sizeof(int32_t)); - if (self->filter_buffer[1] == NULL) { - common_hal_audiofilters_filter_deinit(self); - m_malloc_fail(SYNTHIO_MAX_DUR * sizeof(int32_t)); - } - memset(self->filter_buffer[1], 0, 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 @@ -94,8 +86,7 @@ void common_hal_audiofilters_filter_deinit(audiofilters_filter_obj_t *self) { } self->buffer[0] = NULL; self->buffer[1] = NULL; - self->filter_buffer[0] = NULL; - self->filter_buffer[1] = NULL; + self->filter_buffer = NULL; } mp_obj_t common_hal_audiofilters_filter_get_filter(audiofilters_filter_obj_t *self) { @@ -133,8 +124,7 @@ void audiofilters_filter_reset_buffer(audiofilters_filter_obj_t *self, memset(self->buffer[0], 0, self->buffer_len); memset(self->buffer[1], 0, self->buffer_len); - memset(self->filter_buffer[0], 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); - memset(self->filter_buffer[1], 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); + memset(self->filter_buffer, 0, SYNTHIO_MAX_DUR * sizeof(int32_t)); synthio_biquad_filter_reset(&self->filter_state); } @@ -274,37 +264,32 @@ audioio_get_buffer_result_t audiofilters_filter_get_buffer(audiofilters_filter_o // Fill filter buffer with samples for (uint32_t j = 0; j < n_samples; j++) { if (MP_LIKELY(self->bits_per_sample == 16)) { - self->filter_buffer[0][j] = sample_src[i + j]; + self->filter_buffer[j] = sample_src[i + j]; } else { if (self->samples_signed) { - self->filter_buffer[0][j] = sample_hsrc[i + j]; + self->filter_buffer[j] = sample_hsrc[i + j]; } else { // Be careful here changing from an 8 bit unsigned to signed into a 32-bit signed - self->filter_buffer[0][j] = (int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80); + self->filter_buffer[j] = (int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80); } } } - // Copy original signal for mixing back in later - memcpy(self->filter_buffer[1], self->filter_buffer[0], n_samples); - // Process biquad filter - synthio_biquad_filter_samples(&self->filter_state, self->filter_buffer[0], n_samples); + synthio_biquad_filter_samples(&self->filter_state, self->filter_buffer, n_samples); // Mix processed signal with original sample and transfer to output buffer for (uint32_t j = 0; j < n_samples; j++) { - int32_t word = (self->filter_buffer[1][j] * (1.0 - mix)) + (self->filter_buffer[0][j] * mix); if (MP_LIKELY(self->bits_per_sample == 16)) { - word_buffer[i + j] = mix_down_sample(word); + word_buffer[i + j] = mix_down_sample((sample_src[i + j] * (1.0 - mix)) + (self->filter_buffer[j] * mix)); if (!self->samples_signed) { word_buffer[i + j] ^= 0x8000; } } else { - int8_t mixed = word; if (self->samples_signed) { - hword_buffer[i + j] = mixed; + hword_buffer[i + j] = (int8_t)((sample_hsrc[i + j] * (1.0 - mix)) + (self->filter_buffer[j] * mix)); } else { - hword_buffer[i + j] = (uint8_t)mixed ^ 0x80; + hword_buffer[i + j] = (uint8_t)(((int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80) * (1.0 - mix)) + (self->filter_buffer[j] * mix)) ^ 0x80; } } } diff --git a/shared-module/audiofilters/Filter.h b/shared-module/audiofilters/Filter.h index dd774e3dbf78c..b5d743a6c1918 100644 --- a/shared-module/audiofilters/Filter.h +++ b/shared-module/audiofilters/Filter.h @@ -32,7 +32,7 @@ typedef struct { uint8_t *sample_remaining_buffer; uint32_t sample_buffer_length; - int32_t *filter_buffer[2]; + int32_t *filter_buffer; bool loop; bool more_data; From 8503318f4ab1a8bf57f3bfb04956c47c87750550 Mon Sep 17 00:00:00 2001 From: dcooperdalrymple Date: Tue, 22 Oct 2024 21:41:37 -0500 Subject: [PATCH 6/6] Add `Optional` to `synthio.Biquad` in documentation to allow for `None` value. --- shared-bindings/audiofilters/Filter.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-bindings/audiofilters/Filter.c b/shared-bindings/audiofilters/Filter.c index dd3b74f7854e2..457897909e0d3 100644 --- a/shared-bindings/audiofilters/Filter.c +++ b/shared-bindings/audiofilters/Filter.c @@ -23,7 +23,7 @@ //| //| def __init__( //| self, -//| filter: synthio.Biquad = None, +//| filter: Optional[synthio.Biquad] = None, //| mix: synthio.BlockInput = 1.0, //| buffer_size: int = 512, //| sample_rate: int = 8000, @@ -38,7 +38,7 @@ //| The mix parameter allows you to change how much of the unchanged sample passes through to //| the output to how much of the effect audio you hear as the output. //| -//| :param synthio.Biquad filter: The normalized biquad filter object used to process the signal. +//| :param Optional[synthio.Biquad] filter: The normalized biquad filter object used to process the 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 @@ -128,7 +128,7 @@ static mp_obj_t audiofilters_filter_obj___exit__(size_t n_args, const mp_obj_t * static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiofilters_filter___exit___obj, 4, 4, audiofilters_filter_obj___exit__); -//| filter: synthio.Biquad +//| filter: Optional[synthio.Biquad] //| """The normalized biquad filter object used to process the signal.""" static mp_obj_t audiofilters_filter_obj_get_filter(mp_obj_t self_in) { return common_hal_audiofilters_filter_get_filter(self_in);