Skip to content

Commit bd13492

Browse files
authored
Merge pull request #10044 from gamblor21/audioeffect-chorus
Chorus audio effect
2 parents 36ec480 + 3e7a36c commit bd13492

File tree

7 files changed

+713
-0
lines changed

7 files changed

+713
-0
lines changed

ports/unix/variants/coverage/mpconfigvariant.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ SRC_BITMAP := \
3434
shared-bindings/audiocore/RawSample.c \
3535
shared-bindings/audiocore/WaveFile.c \
3636
shared-bindings/audiodelays/Echo.c \
37+
shared-bindings/audiodelays/Chorus.c \
3738
shared-bindings/audiodelays/PitchShift.c \
3839
shared-bindings/audiodelays/__init__.c \
3940
shared-bindings/audiofilters/Distortion.c \
@@ -77,6 +78,7 @@ SRC_BITMAP := \
7778
shared-module/audiocore/RawSample.c \
7879
shared-module/audiocore/WaveFile.c \
7980
shared-module/audiodelays/Echo.c \
81+
shared-module/audiodelays/Chorus.c \
8082
shared-module/audiodelays/PitchShift.c \
8183
shared-module/audiodelays/__init__.c \
8284
shared-module/audiofilters/Distortion.c \

py/circuitpy_defns.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ SRC_SHARED_MODULE_ALL = \
635635
audiocore/WaveFile.c \
636636
audiocore/__init__.c \
637637
audiodelays/Echo.c \
638+
audiodelays/Chorus.c \
638639
audiodelays/PitchShift.c \
639640
audiodelays/__init__.c \
640641
audiofilters/Distortion.c \

shared-bindings/audiodelays/Chorus.c

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include <stdint.h>
8+
9+
#include "shared-bindings/audiodelays/Chorus.h"
10+
#include "shared-module/audiodelays/Chorus.h"
11+
#include "shared-bindings/audiocore/__init__.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 Chorus:
21+
//| """An Chorus effect"""
22+
//|
23+
//| def __init__(
24+
//| self,
25+
//| max_delay_ms: int = 50,
26+
//| delay_ms: synthio.BlockInput = 50.0,
27+
//| voices: synthio.BlockInput = 1.0,
28+
//| buffer_size: int = 512,
29+
//| sample_rate: int = 8000,
30+
//| bits_per_sample: int = 16,
31+
//| samples_signed: bool = True,
32+
//| channel_count: int = 1,
33+
//| ) -> None:
34+
//| """Create a Chorus effect by playing the current sample along with one or more samples
35+
//| (the voices) from the delay buffer. The voices played are evenly spaced across the delay
36+
//| buffer. So for 2 voices you would hear the current sample and the one delay milliseconds back.
37+
//| The delay timing of the chorus can be changed at runtime with the delay_ms parameter but the delay
38+
//| can never exceed the max_delay_ms parameter. The maximum delay you can set is limited by available
39+
//| memory.
40+
//|
41+
//| :param int max_delay_ms: The maximum time the chorus can be in milliseconds
42+
//| :param synthio.BlockInput delay_ms: The current time of the chorus delay in milliseconds. Must be less the max_delay_ms.
43+
//| :param synthio.BlockInput voices: The number of voices playing split evenly over the delay buffer.
44+
//| :param synthio.BlockInput mix: How much of the wet audio to include along with the original signal.
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 an chorus to a synth::
52+
//|
53+
//| import time
54+
//| import board
55+
//| import audiobusio
56+
//| import synthio
57+
//| import audiodelays
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+
//| chorus = audiodelays.Chorus(max_delay_ms=50, delay_ms=5, buffer_size=1024, channel_count=1, sample_rate=44100)
62+
//| chorus.play(synth)
63+
//| audio.play(chorus)
64+
//|
65+
//| note = synthio.Note(261)
66+
//| while True:
67+
//| synth.press(note)
68+
//| time.sleep(0.25)
69+
//| synth.release(note)
70+
//| time.sleep(5)"""
71+
//| ...
72+
//|
73+
static mp_obj_t audiodelays_chorus_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
74+
enum { ARG_max_delay_ms, ARG_delay_ms, ARG_voices, ARG_mix, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, };
75+
static const mp_arg_t allowed_args[] = {
76+
{ MP_QSTR_max_delay_ms, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 50 } },
77+
{ MP_QSTR_delay_ms, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} },
78+
{ MP_QSTR_voices, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} },
79+
{ MP_QSTR_mix, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL} },
80+
{ MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 512} },
81+
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} },
82+
{ MP_QSTR_bits_per_sample, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} },
83+
{ MP_QSTR_samples_signed, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = true} },
84+
{ MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1 } },
85+
};
86+
87+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
88+
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
89+
90+
mp_int_t max_delay_ms = mp_arg_validate_int_range(args[ARG_max_delay_ms].u_int, 1, 4000, MP_QSTR_max_delay_ms);
91+
92+
mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count);
93+
mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate);
94+
mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int;
95+
if (bits_per_sample != 8 && bits_per_sample != 16) {
96+
mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 8 or 16"));
97+
}
98+
99+
audiodelays_chorus_obj_t *self = mp_obj_malloc(audiodelays_chorus_obj_t, &audiodelays_chorus_type);
100+
common_hal_audiodelays_chorus_construct(self, max_delay_ms, args[ARG_delay_ms].u_obj, args[ARG_voices].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);
101+
102+
return MP_OBJ_FROM_PTR(self);
103+
}
104+
105+
//| def deinit(self) -> None:
106+
//| """Deinitialises the Chorus."""
107+
//| ...
108+
//|
109+
static mp_obj_t audiodelays_chorus_deinit(mp_obj_t self_in) {
110+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(self_in);
111+
common_hal_audiodelays_chorus_deinit(self);
112+
return mp_const_none;
113+
}
114+
static MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_chorus_deinit_obj, audiodelays_chorus_deinit);
115+
116+
static void check_for_deinit(audiodelays_chorus_obj_t *self) {
117+
audiosample_check_for_deinit(&self->base);
118+
}
119+
120+
//| def __enter__(self) -> Chorus:
121+
//| """No-op used by Context Managers."""
122+
//| ...
123+
//|
124+
// Provided by context manager helper.
125+
126+
//| def __exit__(self) -> None:
127+
//| """Automatically deinitializes when exiting a context. See
128+
//| :ref:`lifetime-and-contextmanagers` for more info."""
129+
//| ...
130+
//|
131+
static mp_obj_t audiodelays_chorus_obj___exit__(size_t n_args, const mp_obj_t *args) {
132+
(void)n_args;
133+
common_hal_audiodelays_chorus_deinit(args[0]);
134+
return mp_const_none;
135+
}
136+
static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiodelays_chorus___exit___obj, 4, 4, audiodelays_chorus_obj___exit__);
137+
138+
139+
//| delay_ms: synthio.BlockInput
140+
//| """The current time of the chorus delay in milliseconds. Must be less the max_delay_ms."""
141+
//|
142+
static mp_obj_t audiodelays_chorus_obj_get_delay_ms(mp_obj_t self_in) {
143+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(self_in);
144+
145+
return common_hal_audiodelays_chorus_get_delay_ms(self);
146+
}
147+
MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_chorus_get_delay_ms_obj, audiodelays_chorus_obj_get_delay_ms);
148+
149+
static mp_obj_t audiodelays_chorus_obj_set_delay_ms(mp_obj_t self_in, mp_obj_t delay_ms_in) {
150+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(self_in);
151+
common_hal_audiodelays_chorus_set_delay_ms(self, delay_ms_in);
152+
return mp_const_none;
153+
}
154+
MP_DEFINE_CONST_FUN_OBJ_2(audiodelays_chorus_set_delay_ms_obj, audiodelays_chorus_obj_set_delay_ms);
155+
156+
MP_PROPERTY_GETSET(audiodelays_chorus_delay_ms_obj,
157+
(mp_obj_t)&audiodelays_chorus_get_delay_ms_obj,
158+
(mp_obj_t)&audiodelays_chorus_set_delay_ms_obj);
159+
160+
//| voices: synthio.BlockInput
161+
//| """The number of voices playing split evenly over the delay buffer."""
162+
static mp_obj_t audiodelays_chorus_obj_get_voices(mp_obj_t self_in) {
163+
return common_hal_audiodelays_chorus_get_voices(self_in);
164+
}
165+
MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_chorus_get_voices_obj, audiodelays_chorus_obj_get_voices);
166+
167+
static mp_obj_t audiodelays_chorus_obj_set_voices(mp_obj_t self_in, mp_obj_t voices_in) {
168+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(self_in);
169+
common_hal_audiodelays_chorus_set_voices(self, voices_in);
170+
return mp_const_none;
171+
}
172+
MP_DEFINE_CONST_FUN_OBJ_2(audiodelays_chorus_set_voices_obj, audiodelays_chorus_obj_set_voices);
173+
174+
MP_PROPERTY_GETSET(audiodelays_chorus_voices_obj,
175+
(mp_obj_t)&audiodelays_chorus_get_voices_obj,
176+
(mp_obj_t)&audiodelays_chorus_set_voices_obj);
177+
178+
//| mix: synthio.BlockInput
179+
//| """The rate the echo mix between 0 and 1 where 0 is only sample and 1 is all effect."""
180+
static mp_obj_t audiodelays_chorus_obj_get_mix(mp_obj_t self_in) {
181+
return common_hal_audiodelays_chorus_get_mix(self_in);
182+
}
183+
MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_chorus_get_mix_obj, audiodelays_chorus_obj_get_mix);
184+
185+
static mp_obj_t audiodelays_chorus_obj_set_mix(mp_obj_t self_in, mp_obj_t mix_in) {
186+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(self_in);
187+
common_hal_audiodelays_chorus_set_mix(self, mix_in);
188+
return mp_const_none;
189+
}
190+
MP_DEFINE_CONST_FUN_OBJ_2(audiodelays_chorus_set_mix_obj, audiodelays_chorus_obj_set_mix);
191+
192+
MP_PROPERTY_GETSET(audiodelays_chorus_mix_obj,
193+
(mp_obj_t)&audiodelays_chorus_get_mix_obj,
194+
(mp_obj_t)&audiodelays_chorus_set_mix_obj);
195+
196+
//| playing: bool
197+
//| """True when the effect is playing a sample. (read-only)"""
198+
//|
199+
static mp_obj_t audiodelays_chorus_obj_get_playing(mp_obj_t self_in) {
200+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(self_in);
201+
check_for_deinit(self);
202+
return mp_obj_new_bool(common_hal_audiodelays_chorus_get_playing(self));
203+
}
204+
MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_chorus_get_playing_obj, audiodelays_chorus_obj_get_playing);
205+
206+
MP_PROPERTY_GETTER(audiodelays_chorus_playing_obj,
207+
(mp_obj_t)&audiodelays_chorus_get_playing_obj);
208+
209+
//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None:
210+
//| """Plays the sample once when loop=False and continuously when loop=True.
211+
//| Does not block. Use `playing` to block.
212+
//|
213+
//| The sample must match the encoding settings given in the constructor."""
214+
//| ...
215+
//|
216+
static mp_obj_t audiodelays_chorus_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
217+
enum { ARG_sample, ARG_loop };
218+
static const mp_arg_t allowed_args[] = {
219+
{ MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED, {} },
220+
{ MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
221+
};
222+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
223+
check_for_deinit(self);
224+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
225+
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
226+
227+
228+
mp_obj_t sample = args[ARG_sample].u_obj;
229+
common_hal_audiodelays_chorus_play(self, sample, args[ARG_loop].u_bool);
230+
231+
return mp_const_none;
232+
}
233+
MP_DEFINE_CONST_FUN_OBJ_KW(audiodelays_chorus_play_obj, 1, audiodelays_chorus_obj_play);
234+
235+
//| def stop(self) -> None:
236+
//| """Stops playback of the sample."""
237+
//| ...
238+
//|
239+
//|
240+
static mp_obj_t audiodelays_chorus_obj_stop(mp_obj_t self_in) {
241+
audiodelays_chorus_obj_t *self = MP_OBJ_TO_PTR(self_in);
242+
243+
common_hal_audiodelays_chorus_stop(self);
244+
return mp_const_none;
245+
}
246+
MP_DEFINE_CONST_FUN_OBJ_1(audiodelays_chorus_stop_obj, audiodelays_chorus_obj_stop);
247+
248+
static const mp_rom_map_elem_t audiodelays_chorus_locals_dict_table[] = {
249+
// Methods
250+
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiodelays_chorus_deinit_obj) },
251+
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
252+
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&audiodelays_chorus___exit___obj) },
253+
{ MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiodelays_chorus_play_obj) },
254+
{ MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiodelays_chorus_stop_obj) },
255+
256+
// Properties
257+
{ MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiodelays_chorus_playing_obj) },
258+
{ MP_ROM_QSTR(MP_QSTR_delay_ms), MP_ROM_PTR(&audiodelays_chorus_delay_ms_obj) },
259+
{ MP_ROM_QSTR(MP_QSTR_voices), MP_ROM_PTR(&audiodelays_chorus_voices_obj) },
260+
{ MP_ROM_QSTR(MP_QSTR_mix), MP_ROM_PTR(&audiodelays_chorus_mix_obj) },
261+
AUDIOSAMPLE_FIELDS,
262+
};
263+
static MP_DEFINE_CONST_DICT(audiodelays_chorus_locals_dict, audiodelays_chorus_locals_dict_table);
264+
265+
static const audiosample_p_t audiodelays_chorus_proto = {
266+
MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample)
267+
.reset_buffer = (audiosample_reset_buffer_fun)audiodelays_chorus_reset_buffer,
268+
.get_buffer = (audiosample_get_buffer_fun)audiodelays_chorus_get_buffer,
269+
};
270+
271+
MP_DEFINE_CONST_OBJ_TYPE(
272+
audiodelays_chorus_type,
273+
MP_QSTR_Chorus,
274+
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
275+
make_new, audiodelays_chorus_make_new,
276+
locals_dict, &audiodelays_chorus_locals_dict,
277+
protocol, &audiodelays_chorus_proto
278+
);

shared-bindings/audiodelays/Chorus.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2025 Mark Komus
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
8+
9+
#include "shared-module/audiodelays/Chorus.h"
10+
11+
extern const mp_obj_type_t audiodelays_chorus_type;
12+
13+
void common_hal_audiodelays_chorus_construct(audiodelays_chorus_obj_t *self, uint32_t max_delay_ms,
14+
mp_obj_t delay_ms, mp_obj_t voices, mp_obj_t mix,
15+
uint32_t buffer_size, uint8_t bits_per_sample,
16+
bool samples_signed, uint8_t channel_count, uint32_t sample_rate);
17+
18+
void common_hal_audiodelays_chorus_deinit(audiodelays_chorus_obj_t *self);
19+
bool common_hal_audiodelays_chorus_deinited(audiodelays_chorus_obj_t *self);
20+
21+
uint32_t common_hal_audiodelays_chorus_get_sample_rate(audiodelays_chorus_obj_t *self);
22+
uint8_t common_hal_audiodelays_chorus_get_channel_count(audiodelays_chorus_obj_t *self);
23+
uint8_t common_hal_audiodelays_chorus_get_bits_per_sample(audiodelays_chorus_obj_t *self);
24+
25+
mp_obj_t common_hal_audiodelays_chorus_get_delay_ms(audiodelays_chorus_obj_t *self);
26+
void common_hal_audiodelays_chorus_set_delay_ms(audiodelays_chorus_obj_t *self, mp_obj_t delay_ms);
27+
28+
mp_obj_t common_hal_audiodelays_chorus_get_voices(audiodelays_chorus_obj_t *self);
29+
void common_hal_audiodelays_chorus_set_voices(audiodelays_chorus_obj_t *self, mp_obj_t voices);
30+
31+
mp_obj_t common_hal_audiodelays_chorus_get_mix(audiodelays_chorus_obj_t *self);
32+
void common_hal_audiodelays_chorus_set_mix(audiodelays_chorus_obj_t *self, mp_obj_t arg);
33+
34+
bool common_hal_audiodelays_chorus_get_playing(audiodelays_chorus_obj_t *self);
35+
void common_hal_audiodelays_chorus_play(audiodelays_chorus_obj_t *self, mp_obj_t sample, bool loop);
36+
void common_hal_audiodelays_chorus_stop(audiodelays_chorus_obj_t *self);

shared-bindings/audiodelays/__init__.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include "shared-bindings/audiodelays/__init__.h"
1313
#include "shared-bindings/audiodelays/Echo.h"
14+
#include "shared-bindings/audiodelays/Chorus.h"
1415
#include "shared-bindings/audiodelays/PitchShift.h"
1516

1617
//| """Support for audio delay effects
@@ -22,6 +23,7 @@
2223
static const mp_rom_map_elem_t audiodelays_module_globals_table[] = {
2324
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiodelays) },
2425
{ MP_ROM_QSTR(MP_QSTR_Echo), MP_ROM_PTR(&audiodelays_echo_type) },
26+
{ MP_ROM_QSTR(MP_QSTR_Chorus), MP_ROM_PTR(&audiodelays_chorus_type) },
2527
{ MP_ROM_QSTR(MP_QSTR_PitchShift), MP_ROM_PTR(&audiodelays_pitch_shift_type) },
2628
};
2729

0 commit comments

Comments
 (0)