Skip to content

Commit 64a1be7

Browse files
authored
Merge pull request #11057 from relic-se/resampler
Add new `audiospeed.Resampler` module
2 parents 9ef36f2 + ab17798 commit 64a1be7

14 files changed

Lines changed: 474 additions & 183 deletions

File tree

py/circuitpy_defns.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,7 @@ SRC_SHARED_MODULE_ALL = \
704704
audiocore/RawSample.c \
705705
audiocore/WaveFile.c \
706706
audiocore/__init__.c \
707+
audiospeed/Resampler.c \
707708
audiospeed/SpeedChanger.c \
708709
audiospeed/__init__.c \
709710
audiodelays/Echo.c \

py/circuitpy_mpconfig.mk

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ CFLAGS += -DCIRCUITPY_AUDIOCORE_DEBUG=$(CIRCUITPY_AUDIOCORE_DEBUG)
159159
CIRCUITPY_AUDIOMP3 ?= $(call enable-if-all,$(CIRCUITPY_FULL_BUILD) $(CIRCUITPY_AUDIOCORE))
160160
CFLAGS += -DCIRCUITPY_AUDIOMP3=$(CIRCUITPY_AUDIOMP3)
161161

162+
CIRCUITPY_AUDIOSPEED ?= 0
163+
CFLAGS += -DCIRCUITPY_AUDIOSPEED=$(CIRCUITPY_AUDIOSPEED)
164+
162165
CIRCUITPY_AUDIOEFFECTS ?= 0
163166
CIRCUITPY_AUDIODELAYS ?= $(CIRCUITPY_AUDIOEFFECTS)
164167
CFLAGS += -DCIRCUITPY_AUDIODELAYS=$(CIRCUITPY_AUDIODELAYS)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Cooper Dalrymple
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include <stdint.h>
8+
9+
#include "shared/runtime/context_manager_helpers.h"
10+
#include "py/objproperty.h"
11+
#include "py/runtime.h"
12+
#include "shared-bindings/audiospeed/Resampler.h"
13+
#include "shared-bindings/audiocore/__init__.h"
14+
#include "shared-bindings/util.h"
15+
#include "shared-module/audiospeed/Resampler.h"
16+
17+
//| class Resampler:
18+
//| """Wraps an audio sample to match it to the destination sample rate."""
19+
//|
20+
//| def __init__(self) -> None:
21+
//| """Create a Resampler that wraps ``source``.
22+
//|
23+
//| :param audiosample source: The audio source to resample.
24+
//|
25+
//| Playing a wave file through a mixer with half the sample rate::
26+
//|
27+
//| import board
28+
//| import audiocore
29+
//| import audiomixer
30+
//| import audiospeed
31+
//| import audioio
32+
//|
33+
//| wav = audiocore.WaveFile("sample.wav")
34+
//| resampler = audiospeed.Resampler(wav)
35+
//| mixer = audiomixer.Mixer(
36+
//| channel_count=wav.channel_count,
37+
//| bits_per_sample=wav.bits_per_sample,
38+
//| sample_rate=wav.sample_rate // 2,
39+
//| )
40+
//| audio = audioio.AudioOut(board.A0)
41+
//| audio.play(mixer)
42+
//| mixer.play(resampler)
43+
//| """
44+
//| ...
45+
//|
46+
static mp_obj_t audiospeed_resampler_make_new(const mp_obj_type_t *type,
47+
size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
48+
enum { ARG_source };
49+
static const mp_arg_t allowed_args[] = {
50+
{ MP_QSTR_source, MP_ARG_REQUIRED | MP_ARG_OBJ },
51+
};
52+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
53+
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
54+
55+
// Validate source implements audiosample protocol
56+
mp_obj_t source = args[ARG_source].u_obj;
57+
audiosample_check(source);
58+
59+
audiospeed_resampler_obj_t *self = mp_obj_malloc(audiospeed_resampler_obj_t, &audiospeed_resampler_type);
60+
common_hal_audiospeed_resampler_construct(self, source);
61+
return MP_OBJ_FROM_PTR(self);
62+
}
63+
64+
//| def deinit(self) -> None:
65+
//| """Deinitialises the Resampler and releases all memory resources for reuse."""
66+
//| ...
67+
//|
68+
static mp_obj_t audiospeed_resampler_deinit(mp_obj_t self_in) {
69+
audiospeed_resampler_obj_t *self = MP_OBJ_TO_PTR(self_in);
70+
common_hal_audiospeed_resampler_deinit(self);
71+
return mp_const_none;
72+
}
73+
static MP_DEFINE_CONST_FUN_OBJ_1(audiospeed_resampler_deinit_obj, audiospeed_resampler_deinit);
74+
75+
//| rate: float
76+
//| """Playback speed multiplier."""
77+
//|
78+
static mp_obj_t audiospeed_resampler_obj_get_rate(mp_obj_t self_in) {
79+
audiospeed_resampler_obj_t *self = MP_OBJ_TO_PTR(self_in);
80+
audiosample_check_for_deinit(&self->base.base);
81+
return common_hal_audiospeed_resampler_get_rate(self);
82+
}
83+
MP_DEFINE_CONST_FUN_OBJ_1(audiospeed_resampler_get_rate_obj, audiospeed_resampler_obj_get_rate);
84+
85+
MP_PROPERTY_GETTER(audiospeed_resampler_rate_obj,
86+
(mp_obj_t)&audiospeed_resampler_get_rate_obj);
87+
88+
static const mp_rom_map_elem_t audiospeed_resampler_locals_dict_table[] = {
89+
// Methods
90+
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiospeed_resampler_deinit_obj) },
91+
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
92+
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
93+
94+
// Properties
95+
{ MP_ROM_QSTR(MP_QSTR_rate), MP_ROM_PTR(&audiospeed_resampler_rate_obj) },
96+
AUDIOSAMPLE_FIELDS,
97+
};
98+
static MP_DEFINE_CONST_DICT(audiospeed_resampler_locals_dict, audiospeed_resampler_locals_dict_table);
99+
100+
static const audiosample_p_t audiospeed_resampler_proto = {
101+
MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample)
102+
.reset_buffer = (audiosample_reset_buffer_fun)audiospeed_resampler_reset_buffer,
103+
.get_buffer = (audiosample_get_buffer_fun)audiospeed_resampler_get_buffer,
104+
};
105+
106+
MP_DEFINE_CONST_OBJ_TYPE(
107+
audiospeed_resampler_type,
108+
MP_QSTR_Resampler,
109+
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
110+
make_new, audiospeed_resampler_make_new,
111+
locals_dict, &audiospeed_resampler_locals_dict,
112+
protocol, &audiospeed_resampler_proto
113+
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Cooper Dalrymple
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
8+
9+
#include "shared-module/audiospeed/Resampler.h"
10+
11+
extern const mp_obj_type_t audiospeed_resampler_type;
12+
13+
void common_hal_audiospeed_resampler_construct(audiospeed_resampler_obj_t *self, mp_obj_t source);
14+
void common_hal_audiospeed_resampler_deinit(audiospeed_resampler_obj_t *self);
15+
16+
mp_obj_t common_hal_audiospeed_resampler_get_rate(audiospeed_resampler_obj_t *self);
17+
18+
void audiospeed_resampler_set_sample_rate(audiospeed_resampler_obj_t *self, uint32_t sample_rate);

shared-bindings/audiospeed/SpeedChanger.c

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,9 @@
1212
#include "shared-bindings/audiospeed/SpeedChanger.h"
1313
#include "shared-bindings/audiocore/__init__.h"
1414
#include "shared-bindings/util.h"
15+
#include "shared-module/audiospeed/__init__.h"
1516
#include "shared-module/audiospeed/SpeedChanger.h"
1617

17-
// Convert a Python float to 16.16 fixed-point rate
18-
static uint32_t rate_to_fp(mp_obj_t rate_obj) {
19-
mp_float_t rate = mp_arg_validate_obj_float_range(rate_obj, 0.001, 1000.0, MP_QSTR_rate);
20-
return (uint32_t)(rate * (1 << 16));
21-
}
22-
23-
// Convert 16.16 fixed-point rate to Python float
24-
static mp_obj_t fp_to_rate(uint32_t rate_fp) {
25-
return mp_obj_new_float((mp_float_t)rate_fp / (1 << 16));
26-
}
27-
2818
//| class SpeedChanger:
2919
//| """Wraps an audio sample to play it back at a different speed.
3020
//|
@@ -70,13 +60,8 @@ static mp_obj_t audiospeed_speedchanger_make_new(const mp_obj_type_t *type,
7060
mp_obj_t source = args[ARG_source].u_obj;
7161
audiosample_check(source);
7262

73-
uint32_t rate_fp = 1 << 16; // default 1.0
74-
if (args[ARG_rate].u_obj != mp_const_none) {
75-
rate_fp = rate_to_fp(args[ARG_rate].u_obj);
76-
}
77-
7863
audiospeed_speedchanger_obj_t *self = mp_obj_malloc(audiospeed_speedchanger_obj_t, &audiospeed_speedchanger_type);
79-
common_hal_audiospeed_speedchanger_construct(self, source, rate_fp);
64+
common_hal_audiospeed_speedchanger_construct(self, source, args[ARG_rate].u_obj);
8065
return MP_OBJ_FROM_PTR(self);
8166
}
8267

@@ -97,14 +82,14 @@ static MP_DEFINE_CONST_FUN_OBJ_1(audiospeed_speedchanger_deinit_obj, audiospeed_
9782
static mp_obj_t audiospeed_speedchanger_obj_get_rate(mp_obj_t self_in) {
9883
audiospeed_speedchanger_obj_t *self = MP_OBJ_TO_PTR(self_in);
9984
audiosample_check_for_deinit(&self->base);
100-
return fp_to_rate(common_hal_audiospeed_speedchanger_get_rate(self));
85+
return common_hal_audiospeed_speedchanger_get_rate(self);
10186
}
10287
MP_DEFINE_CONST_FUN_OBJ_1(audiospeed_speedchanger_get_rate_obj, audiospeed_speedchanger_obj_get_rate);
10388

10489
static mp_obj_t audiospeed_speedchanger_obj_set_rate(mp_obj_t self_in, mp_obj_t rate_obj) {
10590
audiospeed_speedchanger_obj_t *self = MP_OBJ_TO_PTR(self_in);
10691
audiosample_check_for_deinit(&self->base);
107-
common_hal_audiospeed_speedchanger_set_rate(self, rate_to_fp(rate_obj));
92+
common_hal_audiospeed_speedchanger_set_rate(self, rate_obj);
10893
return mp_const_none;
10994
}
11095
MP_DEFINE_CONST_FUN_OBJ_2(audiospeed_speedchanger_set_rate_obj, audiospeed_speedchanger_obj_set_rate);

shared-bindings/audiospeed/SpeedChanger.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
extern const mp_obj_type_t audiospeed_speedchanger_type;
1212

1313
void common_hal_audiospeed_speedchanger_construct(audiospeed_speedchanger_obj_t *self,
14-
mp_obj_t source, uint32_t rate_fp);
14+
mp_obj_t source, mp_obj_t rate_obj);
1515
void common_hal_audiospeed_speedchanger_deinit(audiospeed_speedchanger_obj_t *self);
16-
void common_hal_audiospeed_speedchanger_set_rate(audiospeed_speedchanger_obj_t *self, uint32_t rate_fp);
17-
uint32_t common_hal_audiospeed_speedchanger_get_rate(audiospeed_speedchanger_obj_t *self);
16+
17+
void common_hal_audiospeed_speedchanger_set_rate(audiospeed_speedchanger_obj_t *self, mp_obj_t rate_obj);
18+
mp_obj_t common_hal_audiospeed_speedchanger_get_rate(audiospeed_speedchanger_obj_t *self);

shared-bindings/audiospeed/__init__.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
#include "py/obj.h"
1010
#include "py/runtime.h"
1111

12+
#include "shared-bindings/audiospeed/Resampler.h"
1213
#include "shared-bindings/audiospeed/SpeedChanger.h"
1314

1415
//| """Audio processing tools"""
1516

1617
static const mp_rom_map_elem_t audiospeed_module_globals_table[] = {
1718
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiospeed) },
19+
{ MP_ROM_QSTR(MP_QSTR_Resampler), MP_ROM_PTR(&audiospeed_resampler_type) },
1820
{ MP_ROM_QSTR(MP_QSTR_SpeedChanger), MP_ROM_PTR(&audiospeed_speedchanger_type) },
1921
};
2022

shared-module/audiocore/__init__.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
#include "shared-module/audiocore/RawSample.h"
1414
#include "shared-module/audiocore/WaveFile.h"
1515

16+
#if CIRCUITPY_AUDIOSPEED
17+
#include "shared-bindings/audiospeed/Resampler.h"
18+
#endif
19+
1620
#include "shared-bindings/audiomixer/Mixer.h"
1721
#include "shared-module/audiomixer/Mixer.h"
1822

@@ -198,7 +202,11 @@ void audiosample_convert_s16s_u8s(uint8_t *buffer_out, const int16_t *buffer_in,
198202

199203
void audiosample_must_match(audiosample_base_t *self, mp_obj_t other_in, bool allow_mono_to_stereo) {
200204
const audiosample_base_t *other = audiosample_check(other_in);
205+
#if !CIRCUITPY_AUDIOSPEED
201206
if (other->sample_rate != self->sample_rate) {
207+
#else
208+
if (other->sample_rate != self->sample_rate && !mp_obj_is_type(other_in, &audiospeed_resampler_type)) {
209+
#endif
202210
mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_sample_rate);
203211
}
204212
if ((!allow_mono_to_stereo || (allow_mono_to_stereo && self->channel_count != 2)) && other->channel_count != self->channel_count) {
@@ -210,4 +218,11 @@ void audiosample_must_match(audiosample_base_t *self, mp_obj_t other_in, bool al
210218
if (other->samples_signed != self->samples_signed) {
211219
mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_signedness);
212220
}
221+
222+
#if CIRCUITPY_AUDIOSPEED
223+
if (mp_obj_is_type(other_in, &audiospeed_resampler_type)) {
224+
audiospeed_resampler_obj_t *other_resampler = MP_OBJ_TO_PTR(other_in);
225+
audiospeed_resampler_set_sample_rate(other_resampler, self->sample_rate);
226+
}
227+
#endif
213228
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Cooper Dalrymple
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include "shared-bindings/audiospeed/Resampler.h"
8+
9+
static void calculate_rate(audiospeed_base_t *self, uint32_t sample_rate) {
10+
if (self->source != NULL && sample_rate) {
11+
self->speed.rate_fp = (uint32_t)((mp_float_t)self->base.sample_rate / sample_rate * (1 << SPEED_SHIFT));
12+
} else {
13+
audiospeed_set_rate(&self->speed, mp_const_none);
14+
}
15+
}
16+
17+
void common_hal_audiospeed_resampler_construct(audiospeed_resampler_obj_t *self, mp_obj_t source) {
18+
audiospeed_construct(&self->base, source, mp_const_none); // default rate 1.0
19+
self->sample_rate = 0;
20+
}
21+
22+
void common_hal_audiospeed_resampler_deinit(audiospeed_resampler_obj_t *self) {
23+
audiospeed_deinit(&self->base);
24+
}
25+
26+
mp_obj_t common_hal_audiospeed_resampler_get_rate(audiospeed_resampler_obj_t *self) {
27+
return audiospeed_get_rate(&self->base.speed);
28+
}
29+
30+
void audiospeed_resampler_set_sample_rate(audiospeed_resampler_obj_t *self, uint32_t sample_rate) {
31+
self->sample_rate = sample_rate;
32+
calculate_rate(&self->base, self->sample_rate);
33+
}
34+
35+
void audiospeed_resampler_reset_buffer(audiospeed_resampler_obj_t *self,
36+
bool single_channel_output, uint8_t channel) {
37+
audiospeed_reset_buffer(&self->base, single_channel_output, channel);
38+
}
39+
40+
audioio_get_buffer_result_t audiospeed_resampler_get_buffer(audiospeed_resampler_obj_t *self,
41+
bool single_channel_output, uint8_t channel,
42+
uint8_t **buffer, uint32_t *buffer_length) {
43+
return audiospeed_get_buffer(&self->base, single_channel_output, channel, buffer, buffer_length);
44+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Cooper Dalrymple
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
8+
9+
#include "py/obj.h"
10+
#include "shared-module/audiocore/__init__.h"
11+
#include "shared-module/audiospeed/__init__.h"
12+
13+
typedef struct {
14+
audiospeed_base_t base;
15+
uint32_t sample_rate;
16+
} audiospeed_resampler_obj_t;
17+
18+
void audiospeed_resampler_reset_buffer(audiospeed_resampler_obj_t *self,
19+
bool single_channel_output, uint8_t channel);
20+
audioio_get_buffer_result_t audiospeed_resampler_get_buffer(audiospeed_resampler_obj_t *self,
21+
bool single_channel_output, uint8_t channel,
22+
uint8_t **buffer, uint32_t *buffer_length);

0 commit comments

Comments
 (0)