Skip to content

Commit 0208aa9

Browse files
committed
add new audiotools.SpeedChanger module for WAV/MP3 speed changing
1 parent f7f0fd6 commit 0208aa9

File tree

10 files changed

+418
-0
lines changed

10 files changed

+418
-0
lines changed

ports/raspberrypi/mpconfigport.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CIRCUITPY_FLOPPYIO ?= 1
1111
CIRCUITPY_FRAMEBUFFERIO ?= $(CIRCUITPY_DISPLAYIO)
1212
CIRCUITPY_FULL_BUILD ?= 1
1313
CIRCUITPY_AUDIOMP3 ?= 1
14+
CIRCUITPY_AUDIOTOOLS ?= 1
1415
CIRCUITPY_BITOPS ?= 1
1516
CIRCUITPY_HASHLIB ?= 1
1617
CIRCUITPY_HASHLIB_MBEDTLS ?= 1

py/circuitpy_defns.mk

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ endif
146146
ifeq ($(CIRCUITPY_AUDIOMP3),1)
147147
SRC_PATTERNS += audiomp3/%
148148
endif
149+
ifeq ($(CIRCUITPY_AUDIOTOOLS),1)
150+
SRC_PATTERNS += audiotools/%
151+
endif
149152
ifeq ($(CIRCUITPY_AURORA_EPAPER),1)
150153
SRC_PATTERNS += aurora_epaper/%
151154
endif
@@ -688,6 +691,8 @@ SRC_SHARED_MODULE_ALL = \
688691
audiocore/RawSample.c \
689692
audiocore/WaveFile.c \
690693
audiocore/__init__.c \
694+
audiotools/SpeedChanger.c \
695+
audiotools/__init__.c \
691696
audiodelays/Echo.c \
692697
audiodelays/Chorus.c \
693698
audiodelays/PitchShift.c \
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
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/audiotools/SpeedChanger.h"
13+
#include "shared-bindings/audiocore/__init__.h"
14+
#include "shared-bindings/util.h"
15+
#include "shared-module/audiotools/SpeedChanger.h"
16+
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+
28+
//| class SpeedChanger:
29+
//| """Wraps an audio sample to play it back at a different speed.
30+
//|
31+
//| Uses nearest-neighbor resampling with a fixed-point phase accumulator
32+
//| for CPU-efficient variable-speed playback."""
33+
//|
34+
//| def __init__(self, source: audiosample, rate: float = 1.0) -> None:
35+
//| """Create a SpeedChanger that wraps ``source``.
36+
//|
37+
//| :param audiosample source: The audio source to resample.
38+
//| :param float rate: Playback speed multiplier. 1.0 = normal, 2.0 = double speed,
39+
//| 0.5 = half speed. Must be positive.
40+
//|
41+
//| Playing a wave file at 1.5x speed::
42+
//|
43+
//| import board
44+
//| import audiocore
45+
//| import audiotools
46+
//| import audioio
47+
//|
48+
//| wav = audiocore.WaveFile("drum.wav")
49+
//| fast = audiotools.SpeedChanger(wav, rate=1.5)
50+
//| audio = audioio.AudioOut(board.A0)
51+
//| audio.play(fast)
52+
//|
53+
//| # Change speed during playback:
54+
//| fast.rate = 2.0 # double speed
55+
//| fast.rate = 0.5 # half speed
56+
//| """
57+
//| ...
58+
//|
59+
static mp_obj_t audiotools_speedchanger_make_new(const mp_obj_type_t *type,
60+
size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
61+
enum { ARG_source, ARG_rate };
62+
static const mp_arg_t allowed_args[] = {
63+
{ MP_QSTR_source, MP_ARG_REQUIRED | MP_ARG_OBJ },
64+
{ MP_QSTR_rate, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} },
65+
};
66+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
67+
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
68+
69+
// Validate source implements audiosample protocol
70+
mp_obj_t source = args[ARG_source].u_obj;
71+
audiosample_check(source);
72+
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+
78+
audiotools_speedchanger_obj_t *self = mp_obj_malloc(audiotools_speedchanger_obj_t, &audiotools_speedchanger_type);
79+
common_hal_audiotools_speedchanger_construct(self, source, rate_fp);
80+
return MP_OBJ_FROM_PTR(self);
81+
}
82+
83+
//| def deinit(self) -> None:
84+
//| """Deinitialises the SpeedChanger and releases all memory resources for reuse."""
85+
//| ...
86+
//|
87+
static mp_obj_t audiotools_speedchanger_deinit(mp_obj_t self_in) {
88+
audiotools_speedchanger_obj_t *self = MP_OBJ_TO_PTR(self_in);
89+
common_hal_audiotools_speedchanger_deinit(self);
90+
return mp_const_none;
91+
}
92+
static MP_DEFINE_CONST_FUN_OBJ_1(audiotools_speedchanger_deinit_obj, audiotools_speedchanger_deinit);
93+
94+
//| rate: float
95+
//| """Playback speed multiplier. Can be changed during playback."""
96+
//|
97+
static mp_obj_t audiotools_speedchanger_obj_get_rate(mp_obj_t self_in) {
98+
audiotools_speedchanger_obj_t *self = MP_OBJ_TO_PTR(self_in);
99+
audiosample_check_for_deinit(&self->base);
100+
return fp_to_rate(common_hal_audiotools_speedchanger_get_rate(self));
101+
}
102+
MP_DEFINE_CONST_FUN_OBJ_1(audiotools_speedchanger_get_rate_obj, audiotools_speedchanger_obj_get_rate);
103+
104+
static mp_obj_t audiotools_speedchanger_obj_set_rate(mp_obj_t self_in, mp_obj_t rate_obj) {
105+
audiotools_speedchanger_obj_t *self = MP_OBJ_TO_PTR(self_in);
106+
audiosample_check_for_deinit(&self->base);
107+
common_hal_audiotools_speedchanger_set_rate(self, rate_to_fp(rate_obj));
108+
return mp_const_none;
109+
}
110+
MP_DEFINE_CONST_FUN_OBJ_2(audiotools_speedchanger_set_rate_obj, audiotools_speedchanger_obj_set_rate);
111+
112+
MP_PROPERTY_GETSET(audiotools_speedchanger_rate_obj,
113+
(mp_obj_t)&audiotools_speedchanger_get_rate_obj,
114+
(mp_obj_t)&audiotools_speedchanger_set_rate_obj);
115+
116+
static const mp_rom_map_elem_t audiotools_speedchanger_locals_dict_table[] = {
117+
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiotools_speedchanger_deinit_obj) },
118+
{ MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
119+
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
120+
{ MP_ROM_QSTR(MP_QSTR_rate), MP_ROM_PTR(&audiotools_speedchanger_rate_obj) },
121+
AUDIOSAMPLE_FIELDS,
122+
};
123+
static MP_DEFINE_CONST_DICT(audiotools_speedchanger_locals_dict, audiotools_speedchanger_locals_dict_table);
124+
125+
static const audiosample_p_t audiotools_speedchanger_proto = {
126+
MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample)
127+
.reset_buffer = (audiosample_reset_buffer_fun)audiotools_speedchanger_reset_buffer,
128+
.get_buffer = (audiosample_get_buffer_fun)audiotools_speedchanger_get_buffer,
129+
};
130+
131+
MP_DEFINE_CONST_OBJ_TYPE(
132+
audiotools_speedchanger_type,
133+
MP_QSTR_SpeedChanger,
134+
MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
135+
make_new, audiotools_speedchanger_make_new,
136+
locals_dict, &audiotools_speedchanger_locals_dict,
137+
protocol, &audiotools_speedchanger_proto
138+
);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
8+
9+
#include "shared-module/audiotools/SpeedChanger.h"
10+
11+
extern const mp_obj_type_t audiotools_speedchanger_type;
12+
13+
void common_hal_audiotools_speedchanger_construct(audiotools_speedchanger_obj_t *self,
14+
mp_obj_t source, uint32_t rate_fp);
15+
void common_hal_audiotools_speedchanger_deinit(audiotools_speedchanger_obj_t *self);
16+
void common_hal_audiotools_speedchanger_set_rate(audiotools_speedchanger_obj_t *self, uint32_t rate_fp);
17+
uint32_t common_hal_audiotools_speedchanger_get_rate(audiotools_speedchanger_obj_t *self);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include <stdint.h>
8+
9+
#include "py/obj.h"
10+
#include "py/runtime.h"
11+
12+
#include "shared-bindings/audiotools/SpeedChanger.h"
13+
14+
//| """Audio processing tools"""
15+
16+
static const mp_rom_map_elem_t audiotools_module_globals_table[] = {
17+
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiotools) },
18+
{ MP_ROM_QSTR(MP_QSTR_SpeedChanger), MP_ROM_PTR(&audiotools_speedchanger_type) },
19+
};
20+
21+
static MP_DEFINE_CONST_DICT(audiotools_module_globals, audiotools_module_globals_table);
22+
23+
const mp_obj_module_t audiotools_module = {
24+
.base = { &mp_type_module },
25+
.globals = (mp_obj_dict_t *)&audiotools_module_globals,
26+
};
27+
28+
MP_REGISTER_MODULE(MP_QSTR_audiotools, audiotools_module);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Tod Kurt
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include "shared-bindings/audiotools/SpeedChanger.h"
8+
9+
#include <string.h>
10+
#include "py/runtime.h"
11+
#include "py/gc.h"
12+
13+
#include "shared-module/audiocore/WaveFile.h"
14+
#include "shared-bindings/audiocore/__init__.h"
15+
16+
#define OUTPUT_BUFFER_FRAMES 128
17+
18+
void common_hal_audiotools_speedchanger_construct(audiotools_speedchanger_obj_t *self,
19+
mp_obj_t source, uint32_t rate_fp) {
20+
audiosample_base_t *src_base = audiosample_check(source);
21+
22+
self->source = source;
23+
self->rate_fp = rate_fp;
24+
self->phase = 0;
25+
self->src_buffer = NULL;
26+
self->src_buffer_length = 0;
27+
self->src_sample_count = 0;
28+
self->source_done = false;
29+
self->source_exhausted = false;
30+
31+
// Copy format from source
32+
self->base.sample_rate = src_base->sample_rate;
33+
self->base.channel_count = src_base->channel_count;
34+
self->base.bits_per_sample = src_base->bits_per_sample;
35+
self->base.samples_signed = src_base->samples_signed;
36+
self->base.single_buffer = true;
37+
38+
uint8_t bytes_per_frame = (src_base->bits_per_sample / 8) * src_base->channel_count;
39+
self->output_buffer_length = OUTPUT_BUFFER_FRAMES * bytes_per_frame;
40+
self->base.max_buffer_length = self->output_buffer_length;
41+
42+
self->output_buffer = m_malloc_without_collect(self->output_buffer_length);
43+
if (self->output_buffer == NULL) {
44+
m_malloc_fail(self->output_buffer_length);
45+
}
46+
}
47+
48+
void common_hal_audiotools_speedchanger_deinit(audiotools_speedchanger_obj_t *self) {
49+
self->output_buffer = NULL;
50+
self->source = MP_OBJ_NULL;
51+
audiosample_mark_deinit(&self->base);
52+
}
53+
54+
void common_hal_audiotools_speedchanger_set_rate(audiotools_speedchanger_obj_t *self, uint32_t rate_fp) {
55+
self->rate_fp = rate_fp;
56+
}
57+
58+
uint32_t common_hal_audiotools_speedchanger_get_rate(audiotools_speedchanger_obj_t *self) {
59+
return self->rate_fp;
60+
}
61+
62+
// Fetch the next buffer from the source. Returns false if no data available.
63+
static bool fetch_source_buffer(audiotools_speedchanger_obj_t *self) {
64+
if (self->source_exhausted) {
65+
return false;
66+
}
67+
uint8_t *buf = NULL;
68+
uint32_t len = 0;
69+
audioio_get_buffer_result_t result = audiosample_get_buffer(self->source, false, 0, &buf, &len);
70+
if (result == GET_BUFFER_ERROR) {
71+
self->source_exhausted = true;
72+
return false;
73+
}
74+
if (len == 0) {
75+
self->source_exhausted = true;
76+
return false;
77+
}
78+
self->src_buffer = buf;
79+
self->src_buffer_length = len;
80+
uint8_t bytes_per_frame = (self->base.bits_per_sample / 8) * self->base.channel_count;
81+
self->src_sample_count = len / bytes_per_frame;
82+
self->source_done = (result == GET_BUFFER_DONE);
83+
// Reset phase to index within this new buffer
84+
self->phase = 0;
85+
return true;
86+
}
87+
88+
void audiotools_speedchanger_reset_buffer(audiotools_speedchanger_obj_t *self,
89+
bool single_channel_output, uint8_t channel) {
90+
if (single_channel_output && channel == 1) {
91+
return;
92+
}
93+
audiosample_reset_buffer(self->source, false, 0);
94+
self->phase = 0;
95+
self->src_buffer = NULL;
96+
self->src_buffer_length = 0;
97+
self->src_sample_count = 0;
98+
self->source_done = false;
99+
self->source_exhausted = false;
100+
}
101+
102+
audioio_get_buffer_result_t audiotools_speedchanger_get_buffer(audiotools_speedchanger_obj_t *self,
103+
bool single_channel_output, uint8_t channel,
104+
uint8_t **buffer, uint32_t *buffer_length) {
105+
106+
// Ensure we have a source buffer
107+
if (self->src_buffer == NULL) {
108+
if (!fetch_source_buffer(self)) {
109+
*buffer = NULL;
110+
*buffer_length = 0;
111+
return GET_BUFFER_DONE;
112+
}
113+
}
114+
115+
uint8_t bytes_per_sample = self->base.bits_per_sample / 8;
116+
uint8_t channels = self->base.channel_count;
117+
uint8_t bytes_per_frame = bytes_per_sample * channels;
118+
uint32_t out_frames = 0;
119+
uint32_t max_out_frames = self->output_buffer_length / bytes_per_frame;
120+
121+
if (bytes_per_sample == 1) {
122+
// 8-bit samples
123+
uint8_t *out = self->output_buffer;
124+
while (out_frames < max_out_frames) {
125+
uint32_t src_index = self->phase >> SPEED_SHIFT;
126+
// Advance to next source buffer if needed
127+
if (src_index >= self->src_sample_count) {
128+
if (self->source_done) {
129+
self->source_exhausted = true;
130+
break;
131+
}
132+
if (!fetch_source_buffer(self)) {
133+
break;
134+
}
135+
src_index = 0; // phase was reset by fetch
136+
}
137+
uint8_t *src = self->src_buffer + src_index * bytes_per_frame;
138+
for (uint8_t c = 0; c < channels; c++) {
139+
*out++ = src[c];
140+
}
141+
out_frames++;
142+
self->phase += self->rate_fp;
143+
}
144+
} else {
145+
// 16-bit samples
146+
int16_t *out = (int16_t *)self->output_buffer;
147+
while (out_frames < max_out_frames) {
148+
uint32_t src_index = self->phase >> SPEED_SHIFT;
149+
if (src_index >= self->src_sample_count) {
150+
if (self->source_done) {
151+
self->source_exhausted = true;
152+
break;
153+
}
154+
if (!fetch_source_buffer(self)) {
155+
break;
156+
}
157+
src_index = 0;
158+
}
159+
int16_t *src = (int16_t *)(self->src_buffer + src_index * bytes_per_frame);
160+
for (uint8_t c = 0; c < channels; c++) {
161+
*out++ = src[c];
162+
}
163+
out_frames++;
164+
self->phase += self->rate_fp;
165+
}
166+
}
167+
168+
*buffer = self->output_buffer;
169+
*buffer_length = out_frames * bytes_per_frame;
170+
171+
if (out_frames == 0) {
172+
return GET_BUFFER_DONE;
173+
}
174+
return self->source_exhausted ? GET_BUFFER_DONE : GET_BUFFER_MORE_DATA;
175+
}

0 commit comments

Comments
 (0)