Merge pull request #9744 from dcooperdalrymple/audiofilters

Add Audio effects: Filters
This commit is contained in:
Dan Halbert 2024-10-23 14:54:24 -04:00 committed by GitHub
commit 38779cd668
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 744 additions and 0 deletions

View file

@ -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/Filter.c \
audiofilters/__init__.c \
audioio/__init__.c \
audiomixer/Mixer.c \
audiomixer/MixerVoice.c \

View file

@ -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)

View file

@ -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 <stdint.h>
#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,
//| filter: Optional[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 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
//| :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(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)
//|
//| 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_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_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} },
{ 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_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);
}
//| 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__);
//| 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);
}
MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_filter_get_filter_obj, audiofilters_filter_obj_get_filter);
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_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_filter(self, args[ARG_filter].u_obj);
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_filter_set_filter_obj, 1, audiofilters_filter_obj_set_filter);
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
//| """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_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);
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
);

View file

@ -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 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);
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_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);
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);

View file

@ -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 <stdint.h>
#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);

View file

@ -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

View file

@ -0,0 +1,331 @@
// 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 <stdint.h>
#include "py/runtime.h"
void common_hal_audiofilters_filter_construct(audiofilters_filter_obj_t *self,
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) {
// 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
// This buffer will be used to process samples through the biquad filter
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, 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.
if (filter == MP_OBJ_NULL) {
filter = mp_const_none;
}
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) {
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;
self->filter_buffer = NULL;
}
mp_obj_t common_hal_audiofilters_filter_get_filter(audiofilters_filter_obj_t *self) {
return self->filter_obj;
}
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) {
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);
memset(self->filter_buffer, 0, SYNTHIO_MAX_DUR * sizeof(int32_t));
synthio_biquad_filter_reset(&self->filter_state);
}
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;
}
#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) {
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 || 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];
} else {
hword_buffer[i] = sample_hsrc[i];
}
}
} else {
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[j] = sample_src[i + j];
} else {
if (self->samples_signed) {
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[j] = (int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80);
}
}
}
// Process biquad filter
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++) {
if (MP_LIKELY(self->bits_per_sample == 16)) {
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 {
if (self->samples_signed) {
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)(((int8_t)(((uint8_t)sample_hsrc[i + j]) ^ 0x80) * (1.0 - mix)) + (self->filter_buffer[j] * mix)) ^ 0x80;
}
}
}
i += n_samples;
}
}
// 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;
}
}

View file

@ -0,0 +1,55 @@
// 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;
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;
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;
int32_t *filter_buffer;
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);

View file

@ -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

View file

@ -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