Skip to content

Commit 054c0c1

Browse files
authored
Merge pull request #10304 from relic-se/phaser
Phaser Audio Effect
2 parents a6cd2f3 + 0238240 commit 054c0c1

File tree

7 files changed

+677
-0
lines changed

7 files changed

+677
-0
lines changed

ports/unix/variants/coverage/mpconfigvariant.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ SRC_BITMAP := \
4040
shared-bindings/audiodelays/__init__.c \
4141
shared-bindings/audiofilters/Distortion.c \
4242
shared-bindings/audiofilters/Filter.c \
43+
shared-bindings/audiofilters/Phaser.c \
4344
shared-bindings/audiofilters/__init__.c \
4445
shared-bindings/audiofreeverb/Freeverb.c \
4546
shared-bindings/audiofreeverb/__init__.c \
@@ -87,6 +88,7 @@ SRC_BITMAP := \
8788
shared-module/audiodelays/__init__.c \
8889
shared-module/audiofilters/Distortion.c \
8990
shared-module/audiofilters/Filter.c \
91+
shared-module/audiofilters/Phaser.c \
9092
shared-module/audiofilters/__init__.c \
9193
shared-module/audiofreeverb/Freeverb.c \
9294
shared-module/audiofreeverb/__init__.c \

py/circuitpy_defns.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ SRC_SHARED_MODULE_ALL = \
674674
audiodelays/__init__.c \
675675
audiofilters/Distortion.c \
676676
audiofilters/Filter.c \
677+
audiofilters/Phaser.c \
677678
audiofilters/__init__.c \
678679
audiofreeverb/__init__.c \
679680
audiofreeverb/Freeverb.c \

shared-bindings/audiofilters/Phaser.c

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2025 Cooper Dalrymple
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include <stdint.h>
8+
9+
#include "shared-bindings/audiofilters/Phaser.h"
10+
#include "shared-bindings/audiocore/__init__.h"
11+
#include "shared-module/audiofilters/Phaser.h"
12+
13+
#include "shared/runtime/context_manager_helpers.h"
14+
#include "py/binary.h"
15+
#include "py/objproperty.h"
16+
#include "py/runtime.h"
17+
#include "shared-bindings/util.h"
18+
#include "shared-module/synthio/block.h"
19+
20+
//| class Phaser:
21+
//| """A Phaser effect"""
22+
//|
23+
//| def __init__(
24+
//| self,
25+
//| frequency: synthio.BlockInput = 1000.0,
26+
//| feedback: synthio.BlockInput = 0.7,
27+
//| mix: synthio.BlockInput = 1.0,
28+
//| stages: int = 6,
29+
//| buffer_size: int = 512,
30+
//| sample_rate: int = 8000,
31+
//| bits_per_sample: int = 16,
32+
//| samples_signed: bool = True,
33+
//| channel_count: int = 1,
34+
//| ) -> None:
35+
//| """Create a Phaser effect where the original sample is processed through a variable
36+
//| number of all-pass filter stages. This slightly delays the signal so that it is out
37+
//| of phase with the original signal. When the amount of phase is modulated and mixed
38+
//| back into the original signal with the mix parameter, it creates a distinctive
39+
//| phasing sound.
40+
//|
41+
//| :param synthio.BlockInput frequency: The target frequency which is affected by the effect in hz.
42+
//| :param int stages: The number of all-pass filters which will be applied to the signal.
43+
//| :param synthio.BlockInput feedback: The amount that the previous output of the filters is mixed back into their input along with the unprocessed signal.
44+
//| :param synthio.BlockInput mix: The mix as a ratio of the sample (0.0) to the effect (1.0).
45+
//| :param int buffer_size: The total size in bytes of each of the two playback buffers to use
46+
//| :param int sample_rate: The sample rate to be used
47+
//| :param int channel_count: The number of channels the source samples contain. 1 = mono; 2 = stereo.
48+
//| :param int bits_per_sample: The bits per sample of the effect
49+
//| :param bool samples_signed: Effect is signed (True) or unsigned (False)
50+
//|
51+
//| Playing adding a phaser to a synth::
52+
//|
53+
//| import time
54+
//| import board
55+
//| import audiobusio
56+
//| import audiofilters
57+
//| import synthio
58+
//|
59+
//| audio = audiobusio.I2SOut(bit_clock=board.GP20, word_select=board.GP21, data=board.GP22)
60+
//| synth = synthio.Synthesizer(channel_count=1, sample_rate=44100)
61+
//| effect = audiofilters.Phaser(channel_count=1, sample_rate=44100)
62+
//| effect.frequency = synthio.LFO(offset=1000.0, scale=600.0, rate=0.5)
63+
//| effect.play(synth)
64+
//| audio.play(effect)
65+
//|
66+
//| synth.press(48)"""
67+
//| ...
68+
//|
69+
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) {
70+
enum { ARG_frequency, ARG_feedback, ARG_mix, ARG_stages, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, };
71+
static const mp_arg_t allowed_args[] = {
72+
{ MP_QSTR_frequency, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1000) } },
73+
{ MP_QSTR_feedback, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } },
74+
{ MP_QSTR_mix, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1)} },
75+
{ MP_QSTR_stages, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 6 } },
76+
{ MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 512} },
77+
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} },
78+
{ MP_QSTR_bits_per_sample, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} },
79+
{ MP_QSTR_samples_signed, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = true} },
80+
{ MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1 } },
81+
};
82+
83+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
84+
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
85+
86+
mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count);
87+
mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate);
88+
mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int;
89+
if (bits_per_sample != 8 && bits_per_sample != 16) {
90+
mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 8 or 16"));
91+
}
92+
93+
audiofilters_phaser_obj_t *self = mp_obj_malloc(audiofilters_phaser_obj_t, &audiofilters_phaser_type);
94+
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);
95+
96+
return MP_OBJ_FROM_PTR(self);
97+
}
98+
99+
//| def deinit(self) -> None:
100+
//| """Deinitialises the Phaser."""
101+
//| ...
102+
//|
103+
static mp_obj_t audiofilters_phaser_deinit(mp_obj_t self_in) {
104+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in);
105+
common_hal_audiofilters_phaser_deinit(self);
106+
return mp_const_none;
107+
}
108+
static MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_deinit_obj, audiofilters_phaser_deinit);
109+
110+
static void check_for_deinit(audiofilters_phaser_obj_t *self) {
111+
audiosample_check_for_deinit(&self->base);
112+
}
113+
114+
//| def __enter__(self) -> Phaser:
115+
//| """No-op used by Context Managers."""
116+
//| ...
117+
//|
118+
// Provided by context manager helper.
119+
120+
//| def __exit__(self) -> None:
121+
//| """Automatically deinitializes when exiting a context. See
122+
//| :ref:`lifetime-and-contextmanagers` for more info."""
123+
//| ...
124+
//|
125+
// Provided by context manager helper.
126+
127+
128+
//| frequency: synthio.BlockInput
129+
//| """The target frequency in hertz at which the phaser is delaying the signal."""
130+
static mp_obj_t audiofilters_phaser_obj_get_frequency(mp_obj_t self_in) {
131+
return common_hal_audiofilters_phaser_get_frequency(self_in);
132+
}
133+
MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_frequency_obj, audiofilters_phaser_obj_get_frequency);
134+
135+
static mp_obj_t audiofilters_phaser_obj_set_frequency(mp_obj_t self_in, mp_obj_t frequency_in) {
136+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in);
137+
common_hal_audiofilters_phaser_set_frequency(self, frequency_in);
138+
return mp_const_none;
139+
}
140+
MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_frequency_obj, audiofilters_phaser_obj_set_frequency);
141+
142+
MP_PROPERTY_GETSET(audiofilters_phaser_frequency_obj,
143+
(mp_obj_t)&audiofilters_phaser_get_frequency_obj,
144+
(mp_obj_t)&audiofilters_phaser_set_frequency_obj);
145+
146+
147+
//| feedback: synthio.BlockInput
148+
//| """The amount of which the incoming signal is fed back into the phasing filters from 0.1 to 0.9."""
149+
static mp_obj_t audiofilters_phaser_obj_get_feedback(mp_obj_t self_in) {
150+
return common_hal_audiofilters_phaser_get_feedback(self_in);
151+
}
152+
MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_feedback_obj, audiofilters_phaser_obj_get_feedback);
153+
154+
static mp_obj_t audiofilters_phaser_obj_set_feedback(mp_obj_t self_in, mp_obj_t feedback_in) {
155+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in);
156+
common_hal_audiofilters_phaser_set_feedback(self, feedback_in);
157+
return mp_const_none;
158+
}
159+
MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_feedback_obj, audiofilters_phaser_obj_set_feedback);
160+
161+
MP_PROPERTY_GETSET(audiofilters_phaser_feedback_obj,
162+
(mp_obj_t)&audiofilters_phaser_get_feedback_obj,
163+
(mp_obj_t)&audiofilters_phaser_set_feedback_obj);
164+
165+
166+
//| mix: synthio.BlockInput
167+
//| """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."""
168+
static mp_obj_t audiofilters_phaser_obj_get_mix(mp_obj_t self_in) {
169+
return common_hal_audiofilters_phaser_get_mix(self_in);
170+
}
171+
MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_mix_obj, audiofilters_phaser_obj_get_mix);
172+
173+
static mp_obj_t audiofilters_phaser_obj_set_mix(mp_obj_t self_in, mp_obj_t mix_in) {
174+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in);
175+
common_hal_audiofilters_phaser_set_mix(self, mix_in);
176+
return mp_const_none;
177+
}
178+
MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_mix_obj, audiofilters_phaser_obj_set_mix);
179+
180+
MP_PROPERTY_GETSET(audiofilters_phaser_mix_obj,
181+
(mp_obj_t)&audiofilters_phaser_get_mix_obj,
182+
(mp_obj_t)&audiofilters_phaser_set_mix_obj);
183+
184+
185+
//| stages: int
186+
//| """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."""
187+
static mp_obj_t audiofilters_phaser_obj_get_stages(mp_obj_t self_in) {
188+
return MP_OBJ_NEW_SMALL_INT(common_hal_audiofilters_phaser_get_stages(self_in));
189+
}
190+
MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_stages_obj, audiofilters_phaser_obj_get_stages);
191+
192+
static mp_obj_t audiofilters_phaser_obj_set_stages(mp_obj_t self_in, mp_obj_t stages_in) {
193+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in);
194+
common_hal_audiofilters_phaser_set_stages(self, mp_obj_get_int(stages_in));
195+
return mp_const_none;
196+
}
197+
MP_DEFINE_CONST_FUN_OBJ_2(audiofilters_phaser_set_stages_obj, audiofilters_phaser_obj_set_stages);
198+
199+
MP_PROPERTY_GETSET(audiofilters_phaser_stages_obj,
200+
(mp_obj_t)&audiofilters_phaser_get_stages_obj,
201+
(mp_obj_t)&audiofilters_phaser_set_stages_obj);
202+
203+
204+
//| playing: bool
205+
//| """True when the effect is playing a sample. (read-only)"""
206+
//|
207+
static mp_obj_t audiofilters_phaser_obj_get_playing(mp_obj_t self_in) {
208+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in);
209+
check_for_deinit(self);
210+
return mp_obj_new_bool(common_hal_audiofilters_phaser_get_playing(self));
211+
}
212+
MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_get_playing_obj, audiofilters_phaser_obj_get_playing);
213+
214+
MP_PROPERTY_GETTER(audiofilters_phaser_playing_obj,
215+
(mp_obj_t)&audiofilters_phaser_get_playing_obj);
216+
217+
//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None:
218+
//| """Plays the sample once when loop=False and continuously when loop=True.
219+
//| Does not block. Use `playing` to block.
220+
//|
221+
//| The sample must match the encoding settings given in the constructor."""
222+
//| ...
223+
//|
224+
static mp_obj_t audiofilters_phaser_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
225+
enum { ARG_sample, ARG_loop };
226+
static const mp_arg_t allowed_args[] = {
227+
{ MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED, {} },
228+
{ MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
229+
};
230+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
231+
check_for_deinit(self);
232+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
233+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
234+
235+
236+
mp_obj_t sample = args[ARG_sample].u_obj;
237+
common_hal_audiofilters_phaser_play(self, sample, args[ARG_loop].u_bool);
238+
239+
return mp_const_none;
240+
}
241+
MP_DEFINE_CONST_FUN_OBJ_KW(audiofilters_phaser_play_obj, 1, audiofilters_phaser_obj_play);
242+
243+
//| def stop(self) -> None:
244+
//| """Stops playback of the sample."""
245+
//| ...
246+
//|
247+
//|
248+
static mp_obj_t audiofilters_phaser_obj_stop(mp_obj_t self_in) {
249+
audiofilters_phaser_obj_t *self = MP_OBJ_TO_PTR(self_in);
250+
251+
common_hal_audiofilters_phaser_stop(self);
252+
return mp_const_none;
253+
}
254+
MP_DEFINE_CONST_FUN_OBJ_1(audiofilters_phaser_stop_obj, audiofilters_phaser_obj_stop);
255+
256+
static const mp_rom_map_elem_t audiofilters_phaser_locals_dict_table[] = {
257+
// Methods
258+
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiofilters_phaser_deinit_obj) },
259+
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
260+
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
261+
{ MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiofilters_phaser_play_obj) },
262+
{ MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiofilters_phaser_stop_obj) },
263+
264+
// Properties
265+
{ MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofilters_phaser_playing_obj) },
266+
{ MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&audiofilters_phaser_frequency_obj) },
267+
{ MP_ROM_QSTR(MP_QSTR_feedback), MP_ROM_PTR(&audiofilters_phaser_feedback_obj) },
268+
{ MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiofilters_phaser_mix_obj) },
269+
{ MP_ROM_QSTR(MP_QSTR_stages), MP_ROM_PTR(&audiofilters_phaser_stages_obj) },
270+
AUDIOSAMPLE_FIELDS,
271+
};
272+
static MP_DEFINE_CONST_DICT(audiofilters_phaser_locals_dict, audiofilters_phaser_locals_dict_table);
273+
274+
static const audiosample_p_t audiofilters_phaser_proto = {
275+
MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample)
276+
.reset_buffer = (audiosample_reset_buffer_fun)audiofilters_phaser_reset_buffer,
277+
.get_buffer = (audiosample_get_buffer_fun)audiofilters_phaser_get_buffer,
278+
};
279+
280+
MP_DEFINE_CONST_OBJ_TYPE(
281+
audiofilters_phaser_type,
282+
MP_QSTR_Phaser,
283+
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
284+
make_new, audiofilters_phaser_make_new,
285+
locals_dict, &audiofilters_phaser_locals_dict,
286+
protocol, &audiofilters_phaser_proto
287+
);

shared-bindings/audiofilters/Phaser.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2025 Cooper Dalrymple
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
8+
9+
#include "shared-module/audiofilters/Phaser.h"
10+
11+
extern const mp_obj_type_t audiofilters_phaser_type;
12+
13+
void common_hal_audiofilters_phaser_construct(audiofilters_phaser_obj_t *self,
14+
mp_obj_t frequency, mp_obj_t feedback, mp_obj_t mix, uint8_t stages,
15+
uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed,
16+
uint8_t channel_count, uint32_t sample_rate);
17+
18+
void common_hal_audiofilters_phaser_deinit(audiofilters_phaser_obj_t *self);
19+
20+
mp_obj_t common_hal_audiofilters_phaser_get_frequency(audiofilters_phaser_obj_t *self);
21+
void common_hal_audiofilters_phaser_set_frequency(audiofilters_phaser_obj_t *self, mp_obj_t arg);
22+
23+
mp_obj_t common_hal_audiofilters_phaser_get_feedback(audiofilters_phaser_obj_t *self);
24+
void common_hal_audiofilters_phaser_set_feedback(audiofilters_phaser_obj_t *self, mp_obj_t arg);
25+
26+
mp_obj_t common_hal_audiofilters_phaser_get_mix(audiofilters_phaser_obj_t *self);
27+
void common_hal_audiofilters_phaser_set_mix(audiofilters_phaser_obj_t *self, mp_obj_t arg);
28+
29+
uint8_t common_hal_audiofilters_phaser_get_stages(audiofilters_phaser_obj_t *self);
30+
void common_hal_audiofilters_phaser_set_stages(audiofilters_phaser_obj_t *self, uint8_t arg);
31+
32+
bool common_hal_audiofilters_phaser_get_playing(audiofilters_phaser_obj_t *self);
33+
void common_hal_audiofilters_phaser_play(audiofilters_phaser_obj_t *self, mp_obj_t sample, bool loop);
34+
void common_hal_audiofilters_phaser_stop(audiofilters_phaser_obj_t *self);

shared-bindings/audiofilters/__init__.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "shared-bindings/audiofilters/__init__.h"
1313
#include "shared-bindings/audiofilters/Distortion.h"
1414
#include "shared-bindings/audiofilters/Filter.h"
15+
#include "shared-bindings/audiofilters/Phaser.h"
1516

1617
//| """Support for audio filter effects
1718
//|
@@ -23,6 +24,7 @@ static const mp_rom_map_elem_t audiofilters_module_globals_table[] = {
2324
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiofilters) },
2425
{ MP_ROM_QSTR(MP_QSTR_Filter), MP_ROM_PTR(&audiofilters_filter_type) },
2526
{ MP_ROM_QSTR(MP_QSTR_Distortion), MP_ROM_PTR(&audiofilters_distortion_type) },
27+
{ MP_ROM_QSTR(MP_QSTR_Phaser), MP_ROM_PTR(&audiofilters_phaser_type) },
2628

2729
// Enum-like Classes.
2830
{ MP_ROM_QSTR(MP_QSTR_DistortionMode), MP_ROM_PTR(&audiofilters_distortion_mode_type) },

0 commit comments

Comments
 (0)