Initial setup of phaser effect.
This commit is contained in:
parent
1e4d766789
commit
e729849fac
7 changed files with 648 additions and 0 deletions
|
|
@ -40,6 +40,7 @@ SRC_BITMAP := \
|
||||||
shared-bindings/audiodelays/__init__.c \
|
shared-bindings/audiodelays/__init__.c \
|
||||||
shared-bindings/audiofilters/Distortion.c \
|
shared-bindings/audiofilters/Distortion.c \
|
||||||
shared-bindings/audiofilters/Filter.c \
|
shared-bindings/audiofilters/Filter.c \
|
||||||
|
shared-bindings/audiofilters/Phaser.c \
|
||||||
shared-bindings/audiofilters/__init__.c \
|
shared-bindings/audiofilters/__init__.c \
|
||||||
shared-bindings/audiofreeverb/Freeverb.c \
|
shared-bindings/audiofreeverb/Freeverb.c \
|
||||||
shared-bindings/audiofreeverb/__init__.c \
|
shared-bindings/audiofreeverb/__init__.c \
|
||||||
|
|
@ -87,6 +88,7 @@ SRC_BITMAP := \
|
||||||
shared-module/audiodelays/__init__.c \
|
shared-module/audiodelays/__init__.c \
|
||||||
shared-module/audiofilters/Distortion.c \
|
shared-module/audiofilters/Distortion.c \
|
||||||
shared-module/audiofilters/Filter.c \
|
shared-module/audiofilters/Filter.c \
|
||||||
|
shared-module/audiofilters/Phaser.c \
|
||||||
shared-module/audiofilters/__init__.c \
|
shared-module/audiofilters/__init__.c \
|
||||||
shared-module/audiofreeverb/Freeverb.c \
|
shared-module/audiofreeverb/Freeverb.c \
|
||||||
shared-module/audiofreeverb/__init__.c \
|
shared-module/audiofreeverb/__init__.c \
|
||||||
|
|
|
||||||
|
|
@ -674,6 +674,7 @@ SRC_SHARED_MODULE_ALL = \
|
||||||
audiodelays/__init__.c \
|
audiodelays/__init__.c \
|
||||||
audiofilters/Distortion.c \
|
audiofilters/Distortion.c \
|
||||||
audiofilters/Filter.c \
|
audiofilters/Filter.c \
|
||||||
|
audiofilters/Phaser.c \
|
||||||
audiofilters/__init__.c \
|
audiofilters/__init__.c \
|
||||||
audiofreeverb/__init__.c \
|
audiofreeverb/__init__.c \
|
||||||
audiofreeverb/Freeverb.c \
|
audiofreeverb/Freeverb.c \
|
||||||
|
|
|
||||||
287
shared-bindings/audiofilters/Phaser.c
Normal file
287
shared-bindings/audiofilters/Phaser.c
Normal file
|
|
@ -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 <stdint.h>
|
||||||
|
|
||||||
|
#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
|
||||||
|
);
|
||||||
34
shared-bindings/audiofilters/Phaser.h
Normal file
34
shared-bindings/audiofilters/Phaser.h
Normal file
|
|
@ -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);
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
#include "shared-bindings/audiofilters/__init__.h"
|
#include "shared-bindings/audiofilters/__init__.h"
|
||||||
#include "shared-bindings/audiofilters/Distortion.h"
|
#include "shared-bindings/audiofilters/Distortion.h"
|
||||||
#include "shared-bindings/audiofilters/Filter.h"
|
#include "shared-bindings/audiofilters/Filter.h"
|
||||||
|
#include "shared-bindings/audiofilters/Phaser.h"
|
||||||
|
|
||||||
//| """Support for audio filter effects
|
//| """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___name__), MP_ROM_QSTR(MP_QSTR_audiofilters) },
|
||||||
{ MP_ROM_QSTR(MP_QSTR_Filter), MP_ROM_PTR(&audiofilters_filter_type) },
|
{ 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_Distortion), MP_ROM_PTR(&audiofilters_distortion_type) },
|
||||||
|
{ MP_ROM_QSTR(MP_QSTR_Phaser), MP_ROM_PTR(&audiofilters_phaser_type) },
|
||||||
|
|
||||||
// Enum-like Classes.
|
// Enum-like Classes.
|
||||||
{ MP_ROM_QSTR(MP_QSTR_DistortionMode), MP_ROM_PTR(&audiofilters_distortion_mode_type) },
|
{ MP_ROM_QSTR(MP_QSTR_DistortionMode), MP_ROM_PTR(&audiofilters_distortion_mode_type) },
|
||||||
|
|
|
||||||
276
shared-module/audiofilters/Phaser.c
Normal file
276
shared-module/audiofilters/Phaser.c
Normal file
|
|
@ -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 <stdint.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
46
shared-module/audiofilters/Phaser.h
Normal file
46
shared-module/audiofilters/Phaser.h
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue