From b531a07e0f1c6540937b3a8609e639af7fefa512 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 9 Jul 2026 10:23:49 -0500 Subject: [PATCH 1/5] audiowriter module --- locale/circuitpython.pot | 9 + main.c | 8 + ports/espressif/mpconfigport.mk | 1 + ports/raspberrypi/mpconfigport.mk | 1 + py/circuitpy_defns.mk | 5 + py/circuitpy_mpconfig.mk | 3 + shared-bindings/audiowriter/AudioWriter.c | 168 +++++++++ shared-bindings/audiowriter/AudioWriter.h | 23 ++ shared-bindings/audiowriter/__init__.c | 35 ++ shared-bindings/audiowriter/__init__.h | 7 + shared-module/audiowriter/AudioWriter.c | 403 ++++++++++++++++++++++ shared-module/audiowriter/AudioWriter.h | 67 ++++ shared-module/audiowriter/__init__.c | 5 + supervisor/shared/tick.c | 8 + 14 files changed, 743 insertions(+) create mode 100644 shared-bindings/audiowriter/AudioWriter.c create mode 100644 shared-bindings/audiowriter/AudioWriter.h create mode 100644 shared-bindings/audiowriter/__init__.c create mode 100644 shared-bindings/audiowriter/__init__.h create mode 100644 shared-module/audiowriter/AudioWriter.c create mode 100644 shared-module/audiowriter/AudioWriter.h create mode 100644 shared-module/audiowriter/__init__.c diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index dcf10e4276f..b3ce45404c9 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -587,6 +587,7 @@ msgid "Already have all-matches listener" msgstr "" #: ports/espressif/common-hal/_bleio/__init__.c +#: shared-module/audiowriter/AudioWriter.c msgid "Already in progress" msgstr "" @@ -1738,6 +1739,10 @@ msgstr "" msgid "Only 8 or 16 bit mono with %dx oversampling supported." msgstr "" +#: shared-module/audiowriter/AudioWriter.c +msgid "Only 8/16-bit mono/stereo is supported" +msgstr "" + #: ports/espressif/common-hal/wifi/__init__.c #: ports/raspberrypi/common-hal/wifi/__init__.c msgid "Only IPv4 addresses supported" @@ -2789,6 +2794,10 @@ msgstr "" msgid "buffer too small for requested bytes" msgstr "" +#: shared-module/audiowriter/AudioWriter.c +msgid "buffer_size too small for source" +msgstr "" + #: py/emitbc.c msgid "bytecode overflow" msgstr "" diff --git a/main.c b/main.c index ff5238d0c53..cfd6d140cea 100644 --- a/main.c +++ b/main.c @@ -84,6 +84,10 @@ #include "shared-module/keypad/__init__.h" #endif +#if CIRCUITPY_AUDIOWRITER +#include "shared-module/audiowriter/AudioWriter.h" +#endif + #if CIRCUITPY_MEMORYMONITOR #include "shared-module/memorymonitor/__init__.h" #endif @@ -396,6 +400,10 @@ static void cleanup_after_vm(mp_obj_t exception) { keypad_reset(); #endif + #if CIRCUITPY_AUDIOWRITER + audiowriter_reset(); + #endif + // Close user-initiated sockets. #if CIRCUITPY_SOCKETPOOL socketpool_user_reset(); diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 0b027333b97..57cef1917ad 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -336,6 +336,7 @@ CIRCUITPY_MIPIDSI = 1 else ifeq ($(IDF_TARGET),esp32s2) # Modules CIRCUITPY_AUDIOIO ?= 1 +CIRCUITPY_AUDIOWRITER ?= 1 # No I2S peripheral PDM-to-PCM hardware support CIRCUITPY_AUDIOBUSIO_PDMIN = 0 diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index b4d1ed99696..1b2d276145f 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -13,6 +13,7 @@ CIRCUITPY_FULL_BUILD ?= 1 CIRCUITPY_AUDIOMP3 ?= 1 CIRCUITPY_AUDIOSPEED ?= 1 CIRCUITPY_AUDIOEFFECTS ?= 1 +CIRCUITPY_AUDIOWRITER ?= 1 CIRCUITPY_BITOPS ?= 1 CIRCUITPY_HASHLIB ?= 1 CIRCUITPY_HASHLIB_MBEDTLS ?= 1 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 4c91ed102c2..453328861e7 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -152,6 +152,9 @@ endif ifeq ($(CIRCUITPY_AUDIOSPEED),1) SRC_PATTERNS += audiospeed/% endif +ifeq ($(CIRCUITPY_AUDIOWRITER),1) +SRC_PATTERNS += audiowriter/% +endif ifeq ($(CIRCUITPY_AURORA_EPAPER),1) SRC_PATTERNS += aurora_epaper/% endif @@ -718,6 +721,8 @@ SRC_SHARED_MODULE_ALL = \ audiofilters/__init__.c \ audiofreeverb/__init__.c \ audiofreeverb/Freeverb.c \ + audiowriter/AudioWriter.c \ + audiowriter/__init__.c \ audioio/__init__.c \ audiomixer/Mixer.c \ audiomixer/MixerVoice.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index ed0f5e5f1f2..d9b2d35dd05 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -174,6 +174,9 @@ CFLAGS += -DCIRCUITPY_AUDIOFILTERS=$(CIRCUITPY_AUDIOFILTERS) CIRCUITPY_AUDIOFREEVERB ?= $(CIRCUITPY_AUDIOEFFECTS) CFLAGS += -DCIRCUITPY_AUDIOFREEVERB=$(CIRCUITPY_AUDIOFREEVERB) +CIRCUITPY_AUDIOWRITER ?= 0 +CFLAGS += -DCIRCUITPY_AUDIOWRITER=$(CIRCUITPY_AUDIOWRITER) + CIRCUITPY_AURORA_EPAPER ?= 0 CFLAGS += -DCIRCUITPY_AURORA_EPAPER=$(CIRCUITPY_AURORA_EPAPER) diff --git a/shared-bindings/audiowriter/AudioWriter.c b/shared-bindings/audiowriter/AudioWriter.c new file mode 100644 index 00000000000..0751d3a3c21 --- /dev/null +++ b/shared-bindings/audiowriter/AudioWriter.c @@ -0,0 +1,168 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared/runtime/context_manager_helpers.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/audiowriter/AudioWriter.h" +#include "shared-bindings/util.h" + +// ~1 s of 16 kHz mono 16-bit PCM. Sized to absorb a worst-case SD-write stall. +#define AUDIOWRITER_DEFAULT_BUFFER_SIZE (32 * 1024) + +//| class AudioWriter: +//| """Streams an audio source to a ``.wav`` file in the background. +//| +//| ``AudioWriter`` is the inverse of `audiocore.WaveFile`: rather than being +//| an audio *source* played by an `audioio.AudioOut`, it is a *sink* that +//| drives an audio source (a microphone, or an ``audiofilters``/ +//| ``audiodelays``/``audiofreeverb``/``audiospeed`` effect chain) and writes +//| the resulting PCM to a file as a WAV. +//| +//| Recording runs on a background pump paced to the source's real-time rate, +//| so it does not block and does not require a Python read loop (which is +//| what makes hand-rolled recorders choppy).""" +//| +//| def __init__(self, file: typing.BinaryIO, *, buffer_size: int = 32768) -> None: +//| """Create an ``AudioWriter`` that writes to ``file``. +//| +//| :param typing.BinaryIO file: An already-open writable binary stream +//| (a file opened in ``"wb"`` mode, or an `io.BytesIO`). The stream must +//| support seeking so the WAV header sizes can be patched when recording +//| stops. ``AudioWriter`` does not close it; the caller owns it. +//| :param int buffer_size: Size in bytes of the internal RAM ring that +//| decouples file-write latency from the source. Larger values tolerate +//| longer write stalls (e.g. a slow SD card) at the cost of RAM. +//| +//| The audio format (sample rate, channel count, bit depth) is taken from +//| the source at `play()` time, so there are no format arguments here. +//| +//| Recording a microphone through an effect chain to SD:: +//| +//| import audiowriter, board +//| # ``amp`` is the top of an effect chain pulling from a mic +//| with open("/sd/recording.wav", "wb") as f: +//| writer = audiowriter.AudioWriter(f) +//| writer.play(amp) +//| time.sleep(5) +//| writer.stop() +//| """ +//| ... +//| +static mp_obj_t audiowriter_audiowriter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_file, ARG_buffer_size }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_file, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = AUDIOWRITER_DEFAULT_BUFFER_SIZE} }, + }; + 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); + + // A buffer smaller than one source buffer is useless; require a sane floor. + mp_int_t buffer_size = mp_arg_validate_int_min(args[ARG_buffer_size].u_int, 512, MP_QSTR_buffer_size); + + audiowriter_audiowriter_obj_t *self = mp_obj_malloc(audiowriter_audiowriter_obj_t, &audiowriter_audiowriter_type); + common_hal_audiowriter_audiowriter_construct(self, args[ARG_file].u_obj, (uint32_t)buffer_size); + + return MP_OBJ_FROM_PTR(self); +} + +static void check_for_deinit(audiowriter_audiowriter_obj_t *self) { + if (common_hal_audiowriter_audiowriter_deinited(self)) { + raise_deinited_error(); + } +} + +//| def deinit(self) -> None: +//| """Stops recording (patching the WAV header) and releases resources.""" +//| ... +//| +static mp_obj_t audiowriter_audiowriter_deinit(mp_obj_t self_in) { + audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiowriter_audiowriter_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiowriter_audiowriter_deinit_obj, audiowriter_audiowriter_deinit); + +//| def __enter__(self) -> AudioWriter: +//| """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. + +//| def play(self, sample: circuitpython_typing.AudioSample) -> None: +//| """Begin recording ``sample`` to the file. Does not block. +//| +//| Writes a WAV header (in the format of ``sample``) and starts the +//| background pump. ``sample`` must be an 8-bit or 16-bit mono or stereo +//| audio source. Use `playing` to tell when a finite source has finished, +//| or call `stop()` to end recording of a continuous source (e.g. a mic).""" +//| ... +//| +static mp_obj_t audiowriter_audiowriter_obj_play(mp_obj_t self_in, mp_obj_t sample_in) { + audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_audiowriter_audiowriter_play(self, sample_in); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(audiowriter_audiowriter_play_obj, audiowriter_audiowriter_obj_play); + +//| def stop(self) -> None: +//| """Stop recording, flush the RAM ring to the file, and patch the WAV +//| header sizes. The file is left open for the caller to close.""" +//| ... +//| +static mp_obj_t audiowriter_audiowriter_obj_stop(mp_obj_t self_in) { + audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_audiowriter_audiowriter_stop(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiowriter_audiowriter_stop_obj, audiowriter_audiowriter_obj_stop); + +//| playing: bool +//| """True while recording is in progress. Becomes False on its own when a +//| finite source finishes, or after `stop()`. (read-only)""" +//| +static mp_obj_t audiowriter_audiowriter_obj_get_playing(mp_obj_t self_in) { + audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiowriter_audiowriter_get_playing(self)); +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiowriter_audiowriter_get_playing_obj, audiowriter_audiowriter_obj_get_playing); + +MP_PROPERTY_GETTER(audiowriter_audiowriter_playing_obj, + (mp_obj_t)&audiowriter_audiowriter_get_playing_obj); + +static const mp_rom_map_elem_t audiowriter_audiowriter_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiowriter_audiowriter_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(&audiowriter_audiowriter_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiowriter_audiowriter_stop_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiowriter_audiowriter_playing_obj) }, +}; +static MP_DEFINE_CONST_DICT(audiowriter_audiowriter_locals_dict, audiowriter_audiowriter_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + audiowriter_audiowriter_type, + MP_QSTR_AudioWriter, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audiowriter_audiowriter_make_new, + locals_dict, &audiowriter_audiowriter_locals_dict + ); diff --git a/shared-bindings/audiowriter/AudioWriter.h b/shared-bindings/audiowriter/AudioWriter.h new file mode 100644 index 00000000000..f39e0fcfc88 --- /dev/null +++ b/shared-bindings/audiowriter/AudioWriter.h @@ -0,0 +1,23 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +#include "shared-module/audiowriter/AudioWriter.h" + +extern const mp_obj_type_t audiowriter_audiowriter_type; + +void common_hal_audiowriter_audiowriter_construct(audiowriter_audiowriter_obj_t *self, + mp_obj_t file, uint32_t buffer_size); + +void common_hal_audiowriter_audiowriter_deinit(audiowriter_audiowriter_obj_t *self); +bool common_hal_audiowriter_audiowriter_deinited(audiowriter_audiowriter_obj_t *self); + +void common_hal_audiowriter_audiowriter_play(audiowriter_audiowriter_obj_t *self, mp_obj_t sample); +void common_hal_audiowriter_audiowriter_stop(audiowriter_audiowriter_obj_t *self); +bool common_hal_audiowriter_audiowriter_get_playing(audiowriter_audiowriter_obj_t *self); diff --git a/shared-bindings/audiowriter/__init__.c b/shared-bindings/audiowriter/__init__.c new file mode 100644 index 00000000000..2cf88e76076 --- /dev/null +++ b/shared-bindings/audiowriter/__init__.c @@ -0,0 +1,35 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/audiowriter/__init__.h" +#include "shared-bindings/audiowriter/AudioWriter.h" + +//| """Support for streaming audio to a WAV file +//| +//| The `audiowriter` module contains `AudioWriter`, a *sink* that records an +//| audio source (a microphone or an effect chain) to a ``.wav`` file in the +//| background -- the inverse of `audiocore.WaveFile`. +//| +//| """ + +static const mp_rom_map_elem_t audiowriter_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiowriter) }, + { MP_ROM_QSTR(MP_QSTR_AudioWriter), MP_ROM_PTR(&audiowriter_audiowriter_type) }, +}; + +static MP_DEFINE_CONST_DICT(audiowriter_module_globals, audiowriter_module_globals_table); + +const mp_obj_module_t audiowriter_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&audiowriter_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_audiowriter, audiowriter_module); diff --git a/shared-bindings/audiowriter/__init__.h b/shared-bindings/audiowriter/__init__.h new file mode 100644 index 00000000000..3ddd6344a68 --- /dev/null +++ b/shared-bindings/audiowriter/__init__.h @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once diff --git a/shared-module/audiowriter/AudioWriter.c b/shared-module/audiowriter/AudioWriter.c new file mode 100644 index 00000000000..01111699669 --- /dev/null +++ b/shared-module/audiowriter/AudioWriter.c @@ -0,0 +1,403 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "shared-bindings/audiowriter/AudioWriter.h" +#include "shared-bindings/audiocore/__init__.h" +#include "shared-module/audiocore/__init__.h" + +#include + +#include "py/mperrno.h" +#include "py/runtime.h" +#include "py/stream.h" + +#include "supervisor/background_callback.h" +#include "supervisor/shared/tick.h" + +// --------------------------------------------------------------------------- +// Little-endian header helpers +// --------------------------------------------------------------------------- + +static void put_u16le(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); +} + +static void put_u32le(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +// --------------------------------------------------------------------------- +// Active-writer registry, walked once per supervisor tick. +// +// The list head lives in MP_STATE_VM (see MP_REGISTER_ROOT_POINTER below) so +// that (a) active writers and their reg_next chain are GC-rooted while +// recording, and (b) the list is wiped automatically when the VM heap is +// recreated on soft reset. +// --------------------------------------------------------------------------- + +#define REGISTRY_HEAD ((audiowriter_audiowriter_obj_t *)MP_STATE_VM(audiowriter_linked_list)) + +// Add self to the registry. Called from Python (play()) context, so it must +// guard against a background tick walking the list mid-mutation. +static void audiowriter_register(audiowriter_audiowriter_obj_t *self) { + background_callback_prevent(); + // Avoid double-linking if already present. + bool present = false; + for (audiowriter_audiowriter_obj_t *w = REGISTRY_HEAD; w != NULL; w = w->reg_next) { + if (w == self) { + present = true; + break; + } + } + if (!present) { + self->reg_next = REGISTRY_HEAD; + MP_STATE_VM(audiowriter_linked_list) = self; + } + background_callback_allow(); +} + +// Remove self from the registry. Callers must ensure no background tick is +// walking the list concurrently: either they are the background tick itself +// (single-threaded, so safe), or they wrap the call in prevent/allow. +static void audiowriter_unregister(audiowriter_audiowriter_obj_t *self) { + audiowriter_audiowriter_obj_t **pp = (audiowriter_audiowriter_obj_t **)&MP_STATE_VM(audiowriter_linked_list); + while (*pp != NULL) { + if (*pp == self) { + *pp = self->reg_next; + self->reg_next = NULL; + return; + } + pp = &(*pp)->reg_next; + } +} + +// --------------------------------------------------------------------------- +// RAM ring +// --------------------------------------------------------------------------- + +// Copy len bytes from src into the ring. The caller guarantees there is room. +// 8-bit signed PCM is flipped to unsigned to match the WAV convention. +static void audiowriter_ring_write(audiowriter_audiowriter_obj_t *self, const uint8_t *src, uint32_t len) { + bool flip = (self->bits_per_sample == 8 && self->samples_signed); + uint32_t i = 0; + while (i < len) { + uint32_t span = self->ring_size - self->ring_head; + if (span > (len - i)) { + span = len - i; + } + if (flip) { + for (uint32_t j = 0; j < span; j++) { + self->ring[self->ring_head + j] = src[i + j] ^ 0x80; + } + } else { + memcpy(self->ring + self->ring_head, src + i, span); + } + self->ring_head += span; + if (self->ring_head == self->ring_size) { + self->ring_head = 0; + } + i += span; + } + self->ring_count += len; +} + +// Drain the ring to the file. Returns false on a write error. Non-raising: +// safe to call from background-task context. +static bool audiowriter_flush(audiowriter_audiowriter_obj_t *self) { + while (self->ring_count > 0) { + uint32_t span = self->ring_size - self->ring_tail; + if (span > self->ring_count) { + span = self->ring_count; + } + int err = 0; + mp_uint_t wrote = mp_stream_write_exactly(self->file, self->ring + self->ring_tail, span, &err); + if (err != 0 || wrote != span) { + return false; + } + self->ring_tail += span; + if (self->ring_tail == self->ring_size) { + self->ring_tail = 0; + } + self->ring_count -= span; + self->data_bytes += span; + } + return true; +} + +// --------------------------------------------------------------------------- +// Header patching + finalize +// --------------------------------------------------------------------------- + +static void audiowriter_patch_header(audiowriter_audiowriter_obj_t *self) { + uint8_t sz[4]; + int err = 0; + + // RIFF chunk size lives at header_offset + 4. + put_u32le(sz, 36 + self->data_bytes); + if (mp_stream_seek(self->file, self->header_offset + 4, MP_SEEK_SET, &err) == (mp_off_t)-1) { + return; + } + mp_stream_write_exactly(self->file, sz, 4, &err); + + // data chunk size lives at header_offset + 40. + put_u32le(sz, self->data_bytes); + if (mp_stream_seek(self->file, self->header_offset + 40, MP_SEEK_SET, &err) == (mp_off_t)-1) { + return; + } + mp_stream_write_exactly(self->file, sz, 4, &err); + + // Leave the cursor at the end of the PCM so the caller can keep appending + // or simply close the file. + mp_stream_seek(self->file, self->header_offset + 44 + self->data_bytes, MP_SEEK_SET, &err); +} + +// Stop pumping, drain, patch the header, and release the source. Idempotent: +// only the first call (while playing) does work. Non-raising. +static void audiowriter_finalize(audiowriter_audiowriter_obj_t *self) { + if (!self->playing) { + return; + } + // Stop the pump first so a background tick can't re-enter us. + self->playing = false; + + audiowriter_flush(self); + audiowriter_patch_header(self); + + supervisor_disable_tick(); + audiowriter_unregister(self); + self->sample = MP_OBJ_NULL; +} + +// --------------------------------------------------------------------------- +// The pump: one real-time-paced step per supervisor tick +// --------------------------------------------------------------------------- + +static void audiowriter_pump(audiowriter_audiowriter_obj_t *self) { + if (!self->playing) { + return; + } + + // The pull granularity through the chain is one source buffer, so we pace + // in whole-buffer units. + int64_t frames_per_pull = (int64_t)(self->source_max_buffer / self->bytes_per_frame); + if (frames_per_pull < 1) { + frames_per_pull = 1; + } + + // Accrue a real-time sample budget from elapsed ticks. + uint64_t now = supervisor_ticks_ms64(); + uint64_t elapsed = now - self->last_tick_ms; + self->last_tick_ms = now; + // Ignore long gaps (GC pause, SD stall) so we don't try to catch up an + // unbounded backlog all at once. + if (elapsed > 100) { + elapsed = 100; + } + self->budget_frames += (int64_t)((elapsed * self->sample_rate) / 1000); + // Never bank more than a single buffer of catch-up. For a LIVE source the + // captured audio sits in the source's own bounded ring; once we fall more + // than that behind, the surplus is already gone, so a large banked budget + // cannot recover it -- it would only burst-drain the source on consecutive + // ticks and then silence-pad, stretching one stall into a long glitch. + // Capping at one buffer yields at most one brief discontinuity per stall. + if (self->budget_frames > frames_per_pull) { + self->budget_frames = frames_per_pull; + } + + // Pull one buffer only once a full buffer of real time has elapsed: strict + // real-time pacing so we never pull ahead of a live source (which would + // only hand back silence). Also require room for a full source buffer. + if (!self->source_done && self->budget_frames >= frames_per_pull && + (self->ring_size - self->ring_count) >= self->source_max_buffer) { + uint8_t *buf = NULL; + uint32_t len = 0; + audioio_get_buffer_result_t res = + audiosample_get_buffer(self->sample, false, 0, &buf, &len); + if (res == GET_BUFFER_ERROR) { + self->source_done = true; + } else { + if (len > 0 && buf != NULL) { + // Defend against a source that hands back more than it advertised. + if (len > self->source_max_buffer) { + len = self->source_max_buffer; + } + audiowriter_ring_write(self, buf, len); + self->budget_frames -= (int64_t)(len / self->bytes_per_frame); + } + if (res == GET_BUFFER_DONE) { + self->source_done = true; + } + } + } + + if (!audiowriter_flush(self)) { + // File write failed; give up gracefully rather than spin. + self->source_done = true; + } + + if (self->source_done && self->ring_count == 0) { + audiowriter_finalize(self); + } +} + +void audiowriter_background(void) { + audiowriter_audiowriter_obj_t *self = REGISTRY_HEAD; + while (self != NULL) { + // Capture next before pumping: pump() may finalize self, which unlinks + // it from the registry (but leaves our saved next pointer valid). + audiowriter_audiowriter_obj_t *next = self->reg_next; + audiowriter_pump(self); + self = next; + } +} + +// Called during soft reset (VM teardown). Any writer still active is abandoned: +// we don't try to touch its file (it may already be gone), we just balance the +// tick-enable count and drop it from the list. +void audiowriter_reset(void) { + audiowriter_audiowriter_obj_t *self = REGISTRY_HEAD; + while (self != NULL) { + audiowriter_audiowriter_obj_t *next = self->reg_next; + if (self->playing) { + self->playing = false; + supervisor_disable_tick(); + } + self->reg_next = NULL; + self = next; + } + MP_STATE_VM(audiowriter_linked_list) = NULL; +} + +// --------------------------------------------------------------------------- +// common-hal surface +// --------------------------------------------------------------------------- + +void common_hal_audiowriter_audiowriter_construct(audiowriter_audiowriter_obj_t *self, + mp_obj_t file, uint32_t buffer_size) { + // The file must be a writable, seekable binary stream (a file or BytesIO). + mp_get_stream_raise(file, MP_STREAM_OP_WRITE | MP_STREAM_OP_IOCTL); + + self->file = file; + self->sample = MP_OBJ_NULL; + self->ring_size = buffer_size; + self->ring = m_malloc(buffer_size); + self->ring_head = 0; + self->ring_tail = 0; + self->ring_count = 0; + self->playing = false; + self->source_done = false; + self->reg_next = NULL; +} + +bool common_hal_audiowriter_audiowriter_deinited(audiowriter_audiowriter_obj_t *self) { + return self->ring == NULL; +} + +void common_hal_audiowriter_audiowriter_deinit(audiowriter_audiowriter_obj_t *self) { + if (self->playing) { + common_hal_audiowriter_audiowriter_stop(self); + } + self->ring = NULL; + self->file = MP_OBJ_NULL; + self->sample = MP_OBJ_NULL; +} + +void common_hal_audiowriter_audiowriter_play(audiowriter_audiowriter_obj_t *self, mp_obj_t sample_obj) { + if (self->playing) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Already in progress")); + } + + audiosample_base_t *sample = audiosample_check(sample_obj); + uint32_t rate = audiosample_get_sample_rate(sample); + uint8_t channels = audiosample_get_channel_count(sample); + uint8_t bits = audiosample_get_bits_per_sample(sample); + bool single_buffer, samples_signed; + uint32_t max_buffer_length; + uint8_t spacing; + audiosample_get_buffer_structure(sample, false, &single_buffer, &samples_signed, + &max_buffer_length, &spacing); + + if ((bits != 8 && bits != 16) || channels < 1 || channels > 2) { + mp_raise_ValueError(MP_ERROR_TEXT("Only 8/16-bit mono/stereo is supported")); + } + if (max_buffer_length == 0 || self->ring_size < max_buffer_length) { + mp_raise_ValueError(MP_ERROR_TEXT("buffer_size too small for source")); + } + + self->sample_rate = rate; + self->channel_count = channels; + self->bits_per_sample = bits; + self->samples_signed = samples_signed; + self->bytes_per_frame = (uint8_t)(channels * (bits / 8)); + self->source_max_buffer = max_buffer_length; + + // Remember where the header starts so stop() can patch its size fields, + // then write a placeholder header with zeroed sizes. + int err = 0; + mp_off_t off = mp_stream_seek(self->file, 0, MP_SEEK_CUR, &err); + if (off == (mp_off_t)-1) { + mp_raise_OSError(err ? err : MP_EIO); + } + self->header_offset = (uint32_t)off; + + uint32_t block_align = self->bytes_per_frame; + uint32_t byte_rate = rate * block_align; + uint8_t hdr[44]; + memcpy(hdr + 0, "RIFF", 4); + put_u32le(hdr + 4, 36); // RIFF size (patched at stop) + memcpy(hdr + 8, "WAVE", 4); + memcpy(hdr + 12, "fmt ", 4); + put_u32le(hdr + 16, 16); // fmt chunk size + put_u16le(hdr + 20, 1); // PCM + put_u16le(hdr + 22, channels); + put_u32le(hdr + 24, rate); + put_u32le(hdr + 28, byte_rate); + put_u16le(hdr + 32, (uint16_t)block_align); + put_u16le(hdr + 34, bits); + memcpy(hdr + 36, "data", 4); + put_u32le(hdr + 40, 0); // data size (patched at stop) + + err = 0; + mp_uint_t wrote = mp_stream_write_exactly(self->file, hdr, sizeof(hdr), &err); + if (err != 0 || wrote != sizeof(hdr)) { + mp_raise_OSError(err ? err : MP_EIO); + } + + audiosample_reset_buffer(sample_obj, false, 0); + + self->sample = sample_obj; + self->ring_head = 0; + self->ring_tail = 0; + self->ring_count = 0; + self->budget_frames = 0; + self->data_bytes = 0; + self->source_done = false; + self->last_tick_ms = supervisor_ticks_ms64(); + self->playing = true; + + audiowriter_register(self); + supervisor_enable_tick(); +} + +void common_hal_audiowriter_audiowriter_stop(audiowriter_audiowriter_obj_t *self) { + if (!self->playing) { + return; + } + // Keep the background pump out while we finalize from Python context. + background_callback_prevent(); + audiowriter_finalize(self); + background_callback_allow(); +} + +bool common_hal_audiowriter_audiowriter_get_playing(audiowriter_audiowriter_obj_t *self) { + return self->playing; +} + +MP_REGISTER_ROOT_POINTER(mp_obj_t audiowriter_linked_list); diff --git a/shared-module/audiowriter/AudioWriter.h b/shared-module/audiowriter/AudioWriter.h new file mode 100644 index 00000000000..4b1874167f5 --- /dev/null +++ b/shared-module/audiowriter/AudioWriter.h @@ -0,0 +1,67 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +#include "py/obj.h" + +// A streaming WAV *sink*: it consumes an audiosample source (a mic or an effect +// chain) and writes the resulting PCM to a file. Unlike WaveFile it is NOT an +// audiosample (it has no audiosample_base_t) -- it is the thing that drives a +// source, playing the role an AudioOut would. +typedef struct _audiowriter_audiowriter_obj_t { + mp_obj_base_t base; + + // Output stream (anything with write + MP_STREAM_SEEK ioctl: a file or a + // BytesIO). Held so it stays referenced while recording. + mp_obj_t file; + // The source being recorded. Only valid (and referenced) while playing. + mp_obj_t sample; + + // Format, captured from the source at play() time. AudioWriter is the + // format authority for the WAV header. + uint32_t sample_rate; + uint8_t channel_count; + uint8_t bits_per_sample; + bool samples_signed; + uint8_t bytes_per_frame; // channel_count * bits_per_sample / 8 + uint32_t source_max_buffer; // largest buffer the source can hand back, bytes + + // RAM ring that decouples SD-write latency from the source. Written by the + // pump, drained to the file by the pump. Only touched from background-task + // context (never an ISR), so no locking is needed. + uint8_t *ring; + uint32_t ring_size; // capacity in bytes + uint32_t ring_head; // write cursor + uint32_t ring_tail; // read cursor + uint32_t ring_count; // bytes currently buffered + + // Real-time pacing: budget accrues sample_rate frames per second of elapsed + // supervisor ticks; a buffer is pulled only when budget is positive. + int64_t budget_frames; + uint64_t last_tick_ms; + + // Absolute file offset of the RIFF header start, so stop() can seek back and + // patch the two size fields once the final length is known. + uint32_t header_offset; + uint32_t data_bytes; // total PCM bytes handed to the file + + bool playing; + bool source_done; // source returned DONE/ERROR; drain then finalize + + // Intrusive linked list of active writers, walked once per supervisor tick. + struct _audiowriter_audiowriter_obj_t *reg_next; +} audiowriter_audiowriter_obj_t; + +// Called once per supervisor tick (from supervisor_background_tick), in +// background-task context. Pumps every active writer. +void audiowriter_background(void); + +// Called during soft reset to abandon any writer left recording. +void audiowriter_reset(void); diff --git a/shared-module/audiowriter/__init__.c b/shared-module/audiowriter/__init__.c new file mode 100644 index 00000000000..6a919f9fcbf --- /dev/null +++ b/shared-module/audiowriter/__init__.c @@ -0,0 +1,5 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// +// SPDX-License-Identifier: MIT diff --git a/supervisor/shared/tick.c b/supervisor/shared/tick.c index 346ef9a93c4..a224de14720 100644 --- a/supervisor/shared/tick.c +++ b/supervisor/shared/tick.c @@ -27,6 +27,10 @@ #include "shared-module/keypad/__init__.h" #endif +#if CIRCUITPY_AUDIOWRITER +#include "shared-module/audiowriter/AudioWriter.h" +#endif + #include "shared-bindings/microcontroller/__init__.h" #if CIRCUITPY_WATCHDOG @@ -59,6 +63,10 @@ static void supervisor_background_tick(void *unused) { filesystem_background(); + #if CIRCUITPY_AUDIOWRITER + audiowriter_background(); + #endif + port_background_tick(); assert_heap_ok(); From dc6db28b7e5238a043b8f3911e401ed5aabb4382 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 9 Jul 2026 12:47:27 -0500 Subject: [PATCH 2/5] cleanup docstrings, example, and comments --- shared-bindings/audiowriter/AudioWriter.c | 35 ++++++++++++++++------- shared-bindings/audiowriter/AudioWriter.h | 2 +- shared-bindings/audiowriter/__init__.c | 10 ++----- shared-bindings/audiowriter/__init__.h | 2 +- shared-module/audiowriter/AudioWriter.c | 2 +- shared-module/audiowriter/AudioWriter.h | 10 +++---- shared-module/audiowriter/__init__.c | 2 +- 7 files changed, 35 insertions(+), 28 deletions(-) diff --git a/shared-bindings/audiowriter/AudioWriter.c b/shared-bindings/audiowriter/AudioWriter.c index 0751d3a3c21..d6ad33f1a54 100644 --- a/shared-bindings/audiowriter/AudioWriter.c +++ b/shared-bindings/audiowriter/AudioWriter.c @@ -1,6 +1,6 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries // // SPDX-License-Identifier: MIT @@ -20,13 +20,12 @@ //| //| ``AudioWriter`` is the inverse of `audiocore.WaveFile`: rather than being //| an audio *source* played by an `audioio.AudioOut`, it is a *sink* that -//| drives an audio source (a microphone, or an ``audiofilters``/ +//| drives an audio source (a microphone, ``synthio``, or an ``audiofilters``/ //| ``audiodelays``/``audiofreeverb``/``audiospeed`` effect chain) and writes //| the resulting PCM to a file as a WAV. //| //| Recording runs on a background pump paced to the source's real-time rate, -//| so it does not block and does not require a Python read loop (which is -//| what makes hand-rolled recorders choppy).""" +//| so it does not block and does not require a Python read loop.""" //| //| def __init__(self, file: typing.BinaryIO, *, buffer_size: int = 32768) -> None: //| """Create an ``AudioWriter`` that writes to ``file``. @@ -42,15 +41,29 @@ //| The audio format (sample rate, channel count, bit depth) is taken from //| the source at `play()` time, so there are no format arguments here. //| -//| Recording a microphone through an effect chain to SD:: +//| Recording synthio to SD:: //| -//| import audiowriter, board -//| # ``amp`` is the top of an effect chain pulling from a mic -//| with open("/sd/recording.wav", "wb") as f: -//| writer = audiowriter.AudioWriter(f) -//| writer.play(amp) -//| time.sleep(5) +//| import time +//| import synthio +//| from audiowriter import AudioWriter +//| import storage +//| +//| SAMPLE_RATE = 16000 +//| OUTPUT_PATH = "/sd/demo_file.wav" +//| +//| C_major_scale = [60, 62, 64, 65, 67, 69, 71, 72, 71, 69, 67, 65, 64, 62, 60] +//| synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) +//| +//| with open(OUTPUT_PATH, "wb") as f: +//| writer = AudioWriter(f) +//| writer.play(synth) +//| for note in C_major_scale: +//| synth.press(note) +//| time.sleep(0.1) +//| synth.release(note) +//| time.sleep(0.10) //| writer.stop() +//| print("Done ->", OUTPUT_PATH) //| """ //| ... //| diff --git a/shared-bindings/audiowriter/AudioWriter.h b/shared-bindings/audiowriter/AudioWriter.h index f39e0fcfc88..e0de6b2ad9f 100644 --- a/shared-bindings/audiowriter/AudioWriter.h +++ b/shared-bindings/audiowriter/AudioWriter.h @@ -1,6 +1,6 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries // // SPDX-License-Identifier: MIT diff --git a/shared-bindings/audiowriter/__init__.c b/shared-bindings/audiowriter/__init__.c index 2cf88e76076..21c0d15cc7b 100644 --- a/shared-bindings/audiowriter/__init__.c +++ b/shared-bindings/audiowriter/__init__.c @@ -1,6 +1,6 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries // // SPDX-License-Identifier: MIT @@ -12,13 +12,7 @@ #include "shared-bindings/audiowriter/__init__.h" #include "shared-bindings/audiowriter/AudioWriter.h" -//| """Support for streaming audio to a WAV file -//| -//| The `audiowriter` module contains `AudioWriter`, a *sink* that records an -//| audio source (a microphone or an effect chain) to a ``.wav`` file in the -//| background -- the inverse of `audiocore.WaveFile`. -//| -//| """ +//| """Support for streaming audio to a WAV file""" static const mp_rom_map_elem_t audiowriter_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiowriter) }, diff --git a/shared-bindings/audiowriter/__init__.h b/shared-bindings/audiowriter/__init__.h index 3ddd6344a68..779b49ffd8d 100644 --- a/shared-bindings/audiowriter/__init__.h +++ b/shared-bindings/audiowriter/__init__.h @@ -1,6 +1,6 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries // // SPDX-License-Identifier: MIT diff --git a/shared-module/audiowriter/AudioWriter.c b/shared-module/audiowriter/AudioWriter.c index 01111699669..0ddbe07c96c 100644 --- a/shared-module/audiowriter/AudioWriter.c +++ b/shared-module/audiowriter/AudioWriter.c @@ -1,6 +1,6 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries // // SPDX-License-Identifier: MIT diff --git a/shared-module/audiowriter/AudioWriter.h b/shared-module/audiowriter/AudioWriter.h index 4b1874167f5..1aab3f401ad 100644 --- a/shared-module/audiowriter/AudioWriter.h +++ b/shared-module/audiowriter/AudioWriter.h @@ -1,6 +1,6 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries // // SPDX-License-Identifier: MIT @@ -11,10 +11,10 @@ #include "py/obj.h" -// A streaming WAV *sink*: it consumes an audiosample source (a mic or an effect -// chain) and writes the resulting PCM to a file. Unlike WaveFile it is NOT an -// audiosample (it has no audiosample_base_t) -- it is the thing that drives a -// source, playing the role an AudioOut would. +// A streaming WAV *sink*: it consumes an audiosample source (a mic, synthio, +// or an effect chain) and writes the resulting PCM to a file. Unlike WaveFile +// it is NOT an audiosample, it is the thing that drives a source, playing the +// role an AudioOut would. typedef struct _audiowriter_audiowriter_obj_t { mp_obj_base_t base; diff --git a/shared-module/audiowriter/__init__.c b/shared-module/audiowriter/__init__.c index 6a919f9fcbf..584c821b996 100644 --- a/shared-module/audiowriter/__init__.c +++ b/shared-module/audiowriter/__init__.c @@ -1,5 +1,5 @@ // This file is part of the CircuitPython project: https://circuitpython.org // -// SPDX-FileCopyrightText: Copyright (c) 2026 Adafruit Industries +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries // // SPDX-License-Identifier: MIT From 78dc0157822773f13a42c093549903b862152dbd Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 9 Jul 2026 14:59:53 -0500 Subject: [PATCH 3/5] disable for pimoroni_pico_dv_base_w --- .../raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk b/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk index 9218192d083..45bea72a65d 100644 --- a/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk +++ b/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk @@ -19,6 +19,7 @@ CIRCUITPY_SOCKETPOOL = 1 CIRCUITPY_WIFI = 1 CIRCUITPY_PICODVI = 1 +CIRCUITPY_AUDIOWRITER = 0 CFLAGS += \ -DCYW43_PIN_WL_DYNAMIC=0 \ From 9fd8e3fe314970b2e19ca9e49058acd4fedec4d0 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 10 Jul 2026 14:16:16 -0500 Subject: [PATCH 4/5] move to audiofilewriter.AudioFileWriter --- locale/circuitpython.pot | 6 +- main.c | 8 +- ports/espressif/mpconfigport.mk | 2 +- .../pimoroni_pico_dv_base_w/mpconfigboard.mk | 2 +- ports/raspberrypi/mpconfigport.mk | 2 +- py/circuitpy_defns.mk | 8 +- py/circuitpy_mpconfig.mk | 4 +- .../AudioFileWriter.c} | 86 +++++++++---------- .../audiofilewriter/AudioFileWriter.h | 23 +++++ shared-bindings/audiofilewriter/__init__.c | 29 +++++++ .../__init__.h | 0 shared-bindings/audiowriter/AudioWriter.h | 23 ----- shared-bindings/audiowriter/__init__.c | 29 ------- .../AudioFileWriter.c} | 72 ++++++++-------- .../AudioFileWriter.h} | 12 +-- .../__init__.c | 0 supervisor/shared/tick.c | 8 +- 17 files changed, 157 insertions(+), 157 deletions(-) rename shared-bindings/{audiowriter/AudioWriter.c => audiofilewriter/AudioFileWriter.c} (59%) create mode 100644 shared-bindings/audiofilewriter/AudioFileWriter.h create mode 100644 shared-bindings/audiofilewriter/__init__.c rename shared-bindings/{audiowriter => audiofilewriter}/__init__.h (100%) delete mode 100644 shared-bindings/audiowriter/AudioWriter.h delete mode 100644 shared-bindings/audiowriter/__init__.c rename shared-module/{audiowriter/AudioWriter.c => audiofilewriter/AudioFileWriter.c} (83%) rename shared-module/{audiowriter/AudioWriter.h => audiofilewriter/AudioFileWriter.h} (88%) rename shared-module/{audiowriter => audiofilewriter}/__init__.c (100%) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index b3ce45404c9..e480912e721 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -587,7 +587,7 @@ msgid "Already have all-matches listener" msgstr "" #: ports/espressif/common-hal/_bleio/__init__.c -#: shared-module/audiowriter/AudioWriter.c +#: shared-module/audiofilewriter/AudioFileWriter.c msgid "Already in progress" msgstr "" @@ -1739,7 +1739,7 @@ msgstr "" msgid "Only 8 or 16 bit mono with %dx oversampling supported." msgstr "" -#: shared-module/audiowriter/AudioWriter.c +#: shared-module/audiofilewriter/AudioFileWriter.c msgid "Only 8/16-bit mono/stereo is supported" msgstr "" @@ -2794,7 +2794,7 @@ msgstr "" msgid "buffer too small for requested bytes" msgstr "" -#: shared-module/audiowriter/AudioWriter.c +#: shared-module/audiofilewriter/AudioFileWriter.c msgid "buffer_size too small for source" msgstr "" diff --git a/main.c b/main.c index cfd6d140cea..135195f2d51 100644 --- a/main.c +++ b/main.c @@ -84,8 +84,8 @@ #include "shared-module/keypad/__init__.h" #endif -#if CIRCUITPY_AUDIOWRITER -#include "shared-module/audiowriter/AudioWriter.h" +#if CIRCUITPY_AUDIOFILEWRITER +#include "shared-module/audiofilewriter/AudioFileWriter.h" #endif #if CIRCUITPY_MEMORYMONITOR @@ -400,8 +400,8 @@ static void cleanup_after_vm(mp_obj_t exception) { keypad_reset(); #endif - #if CIRCUITPY_AUDIOWRITER - audiowriter_reset(); + #if CIRCUITPY_AUDIOFILEWRITER + audiofilewriter_reset(); #endif // Close user-initiated sockets. diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 57cef1917ad..bbe815af468 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -336,7 +336,7 @@ CIRCUITPY_MIPIDSI = 1 else ifeq ($(IDF_TARGET),esp32s2) # Modules CIRCUITPY_AUDIOIO ?= 1 -CIRCUITPY_AUDIOWRITER ?= 1 +CIRCUITPY_AUDIOFILEWRITER ?= 1 # No I2S peripheral PDM-to-PCM hardware support CIRCUITPY_AUDIOBUSIO_PDMIN = 0 diff --git a/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk b/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk index 45bea72a65d..d380732afc1 100644 --- a/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk +++ b/ports/raspberrypi/boards/pimoroni_pico_dv_base_w/mpconfigboard.mk @@ -19,7 +19,7 @@ CIRCUITPY_SOCKETPOOL = 1 CIRCUITPY_WIFI = 1 CIRCUITPY_PICODVI = 1 -CIRCUITPY_AUDIOWRITER = 0 +CIRCUITPY_AUDIOFILEWRITER = 0 CFLAGS += \ -DCYW43_PIN_WL_DYNAMIC=0 \ diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 1b2d276145f..f4c6e809ba0 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -13,7 +13,7 @@ CIRCUITPY_FULL_BUILD ?= 1 CIRCUITPY_AUDIOMP3 ?= 1 CIRCUITPY_AUDIOSPEED ?= 1 CIRCUITPY_AUDIOEFFECTS ?= 1 -CIRCUITPY_AUDIOWRITER ?= 1 +CIRCUITPY_AUDIOFILEWRITER ?= 1 CIRCUITPY_BITOPS ?= 1 CIRCUITPY_HASHLIB ?= 1 CIRCUITPY_HASHLIB_MBEDTLS ?= 1 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 453328861e7..63a874b094e 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -152,8 +152,8 @@ endif ifeq ($(CIRCUITPY_AUDIOSPEED),1) SRC_PATTERNS += audiospeed/% endif -ifeq ($(CIRCUITPY_AUDIOWRITER),1) -SRC_PATTERNS += audiowriter/% +ifeq ($(CIRCUITPY_AUDIOFILEWRITER),1) +SRC_PATTERNS += audiofilewriter/% endif ifeq ($(CIRCUITPY_AURORA_EPAPER),1) SRC_PATTERNS += aurora_epaper/% @@ -721,8 +721,8 @@ SRC_SHARED_MODULE_ALL = \ audiofilters/__init__.c \ audiofreeverb/__init__.c \ audiofreeverb/Freeverb.c \ - audiowriter/AudioWriter.c \ - audiowriter/__init__.c \ + audiofilewriter/AudioFileWriter.c \ + audiofilewriter/__init__.c \ audioio/__init__.c \ audiomixer/Mixer.c \ audiomixer/MixerVoice.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index d9b2d35dd05..62943cff3ad 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -174,8 +174,8 @@ CFLAGS += -DCIRCUITPY_AUDIOFILTERS=$(CIRCUITPY_AUDIOFILTERS) CIRCUITPY_AUDIOFREEVERB ?= $(CIRCUITPY_AUDIOEFFECTS) CFLAGS += -DCIRCUITPY_AUDIOFREEVERB=$(CIRCUITPY_AUDIOFREEVERB) -CIRCUITPY_AUDIOWRITER ?= 0 -CFLAGS += -DCIRCUITPY_AUDIOWRITER=$(CIRCUITPY_AUDIOWRITER) +CIRCUITPY_AUDIOFILEWRITER ?= 0 +CFLAGS += -DCIRCUITPY_AUDIOFILEWRITER=$(CIRCUITPY_AUDIOFILEWRITER) CIRCUITPY_AURORA_EPAPER ?= 0 CFLAGS += -DCIRCUITPY_AURORA_EPAPER=$(CIRCUITPY_AURORA_EPAPER) diff --git a/shared-bindings/audiowriter/AudioWriter.c b/shared-bindings/audiofilewriter/AudioFileWriter.c similarity index 59% rename from shared-bindings/audiowriter/AudioWriter.c rename to shared-bindings/audiofilewriter/AudioFileWriter.c index d6ad33f1a54..51b63d834aa 100644 --- a/shared-bindings/audiowriter/AudioWriter.c +++ b/shared-bindings/audiofilewriter/AudioFileWriter.c @@ -9,16 +9,16 @@ #include "shared/runtime/context_manager_helpers.h" #include "py/objproperty.h" #include "py/runtime.h" -#include "shared-bindings/audiowriter/AudioWriter.h" +#include "shared-bindings/audiofilewriter/AudioFileWriter.h" #include "shared-bindings/util.h" // ~1 s of 16 kHz mono 16-bit PCM. Sized to absorb a worst-case SD-write stall. -#define AUDIOWRITER_DEFAULT_BUFFER_SIZE (32 * 1024) +#define AUDIOFILEWRITER_DEFAULT_BUFFER_SIZE (32 * 1024) -//| class AudioWriter: +//| class AudioFileWriter: //| """Streams an audio source to a ``.wav`` file in the background. //| -//| ``AudioWriter`` is the inverse of `audiocore.WaveFile`: rather than being +//| ``AudioFileWriter`` is the inverse of `audiocore.WaveFile`: rather than being //| an audio *source* played by an `audioio.AudioOut`, it is a *sink* that //| drives an audio source (a microphone, ``synthio``, or an ``audiofilters``/ //| ``audiodelays``/``audiofreeverb``/``audiospeed`` effect chain) and writes @@ -28,12 +28,12 @@ //| so it does not block and does not require a Python read loop.""" //| //| def __init__(self, file: typing.BinaryIO, *, buffer_size: int = 32768) -> None: -//| """Create an ``AudioWriter`` that writes to ``file``. +//| """Create an ``AudioFileWriter`` that writes to ``file``. //| //| :param typing.BinaryIO file: An already-open writable binary stream //| (a file opened in ``"wb"`` mode, or an `io.BytesIO`). The stream must //| support seeking so the WAV header sizes can be patched when recording -//| stops. ``AudioWriter`` does not close it; the caller owns it. +//| stops. ``AudioFileWriter`` does not close it; the caller owns it. //| :param int buffer_size: Size in bytes of the internal RAM ring that //| decouples file-write latency from the source. Larger values tolerate //| longer write stalls (e.g. a slow SD card) at the cost of RAM. @@ -45,7 +45,7 @@ //| //| import time //| import synthio -//| from audiowriter import AudioWriter +//| from audiofilewriter import AudioFileWriter //| import storage //| //| SAMPLE_RATE = 16000 @@ -55,7 +55,7 @@ //| synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) //| //| with open(OUTPUT_PATH, "wb") as f: -//| writer = AudioWriter(f) +//| writer = AudioFileWriter(f) //| writer.play(synth) //| for note in C_major_scale: //| synth.press(note) @@ -67,11 +67,11 @@ //| """ //| ... //| -static mp_obj_t audiowriter_audiowriter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { +static mp_obj_t audiofilewriter_audiofilewriter_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { enum { ARG_file, ARG_buffer_size }; static const mp_arg_t allowed_args[] = { { MP_QSTR_file, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_OBJ_NULL} }, - { MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = AUDIOWRITER_DEFAULT_BUFFER_SIZE} }, + { MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = AUDIOFILEWRITER_DEFAULT_BUFFER_SIZE} }, }; 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); @@ -79,14 +79,14 @@ static mp_obj_t audiowriter_audiowriter_make_new(const mp_obj_type_t *type, size // A buffer smaller than one source buffer is useless; require a sane floor. mp_int_t buffer_size = mp_arg_validate_int_min(args[ARG_buffer_size].u_int, 512, MP_QSTR_buffer_size); - audiowriter_audiowriter_obj_t *self = mp_obj_malloc(audiowriter_audiowriter_obj_t, &audiowriter_audiowriter_type); - common_hal_audiowriter_audiowriter_construct(self, args[ARG_file].u_obj, (uint32_t)buffer_size); + audiofilewriter_audiofilewriter_obj_t *self = mp_obj_malloc(audiofilewriter_audiofilewriter_obj_t, &audiofilewriter_audiofilewriter_type); + common_hal_audiofilewriter_audiofilewriter_construct(self, args[ARG_file].u_obj, (uint32_t)buffer_size); return MP_OBJ_FROM_PTR(self); } -static void check_for_deinit(audiowriter_audiowriter_obj_t *self) { - if (common_hal_audiowriter_audiowriter_deinited(self)) { +static void check_for_deinit(audiofilewriter_audiofilewriter_obj_t *self) { + if (common_hal_audiofilewriter_audiofilewriter_deinited(self)) { raise_deinited_error(); } } @@ -95,14 +95,14 @@ static void check_for_deinit(audiowriter_audiowriter_obj_t *self) { //| """Stops recording (patching the WAV header) and releases resources.""" //| ... //| -static mp_obj_t audiowriter_audiowriter_deinit(mp_obj_t self_in) { - audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); - common_hal_audiowriter_audiowriter_deinit(self); +static mp_obj_t audiofilewriter_audiofilewriter_deinit(mp_obj_t self_in) { + audiofilewriter_audiofilewriter_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiofilewriter_audiofilewriter_deinit(self); return mp_const_none; } -static MP_DEFINE_CONST_FUN_OBJ_1(audiowriter_audiowriter_deinit_obj, audiowriter_audiowriter_deinit); +static MP_DEFINE_CONST_FUN_OBJ_1(audiofilewriter_audiofilewriter_deinit_obj, audiofilewriter_audiofilewriter_deinit); -//| def __enter__(self) -> AudioWriter: +//| def __enter__(self) -> AudioFileWriter: //| """No-op used by Context Managers.""" //| ... //| @@ -124,58 +124,58 @@ static MP_DEFINE_CONST_FUN_OBJ_1(audiowriter_audiowriter_deinit_obj, audiowriter //| or call `stop()` to end recording of a continuous source (e.g. a mic).""" //| ... //| -static mp_obj_t audiowriter_audiowriter_obj_play(mp_obj_t self_in, mp_obj_t sample_in) { - audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); +static mp_obj_t audiofilewriter_audiofilewriter_obj_play(mp_obj_t self_in, mp_obj_t sample_in) { + audiofilewriter_audiofilewriter_obj_t *self = MP_OBJ_TO_PTR(self_in); check_for_deinit(self); - common_hal_audiowriter_audiowriter_play(self, sample_in); + common_hal_audiofilewriter_audiofilewriter_play(self, sample_in); return mp_const_none; } -static MP_DEFINE_CONST_FUN_OBJ_2(audiowriter_audiowriter_play_obj, audiowriter_audiowriter_obj_play); +static MP_DEFINE_CONST_FUN_OBJ_2(audiofilewriter_audiofilewriter_play_obj, audiofilewriter_audiofilewriter_obj_play); //| def stop(self) -> None: //| """Stop recording, flush the RAM ring to the file, and patch the WAV //| header sizes. The file is left open for the caller to close.""" //| ... //| -static mp_obj_t audiowriter_audiowriter_obj_stop(mp_obj_t self_in) { - audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); +static mp_obj_t audiofilewriter_audiofilewriter_obj_stop(mp_obj_t self_in) { + audiofilewriter_audiofilewriter_obj_t *self = MP_OBJ_TO_PTR(self_in); check_for_deinit(self); - common_hal_audiowriter_audiowriter_stop(self); + common_hal_audiofilewriter_audiofilewriter_stop(self); return mp_const_none; } -static MP_DEFINE_CONST_FUN_OBJ_1(audiowriter_audiowriter_stop_obj, audiowriter_audiowriter_obj_stop); +static MP_DEFINE_CONST_FUN_OBJ_1(audiofilewriter_audiofilewriter_stop_obj, audiofilewriter_audiofilewriter_obj_stop); //| playing: bool //| """True while recording is in progress. Becomes False on its own when a //| finite source finishes, or after `stop()`. (read-only)""" //| -static mp_obj_t audiowriter_audiowriter_obj_get_playing(mp_obj_t self_in) { - audiowriter_audiowriter_obj_t *self = MP_OBJ_TO_PTR(self_in); +static mp_obj_t audiofilewriter_audiofilewriter_obj_get_playing(mp_obj_t self_in) { + audiofilewriter_audiofilewriter_obj_t *self = MP_OBJ_TO_PTR(self_in); check_for_deinit(self); - return mp_obj_new_bool(common_hal_audiowriter_audiowriter_get_playing(self)); + return mp_obj_new_bool(common_hal_audiofilewriter_audiofilewriter_get_playing(self)); } -static MP_DEFINE_CONST_FUN_OBJ_1(audiowriter_audiowriter_get_playing_obj, audiowriter_audiowriter_obj_get_playing); +static MP_DEFINE_CONST_FUN_OBJ_1(audiofilewriter_audiofilewriter_get_playing_obj, audiofilewriter_audiofilewriter_obj_get_playing); -MP_PROPERTY_GETTER(audiowriter_audiowriter_playing_obj, - (mp_obj_t)&audiowriter_audiowriter_get_playing_obj); +MP_PROPERTY_GETTER(audiofilewriter_audiofilewriter_playing_obj, + (mp_obj_t)&audiofilewriter_audiofilewriter_get_playing_obj); -static const mp_rom_map_elem_t audiowriter_audiowriter_locals_dict_table[] = { +static const mp_rom_map_elem_t audiofilewriter_audiofilewriter_locals_dict_table[] = { // Methods - { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiowriter_audiowriter_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiofilewriter_audiofilewriter_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(&audiowriter_audiowriter_play_obj) }, - { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiowriter_audiowriter_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiofilewriter_audiofilewriter_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiofilewriter_audiofilewriter_stop_obj) }, // Properties - { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiowriter_audiowriter_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiofilewriter_audiofilewriter_playing_obj) }, }; -static MP_DEFINE_CONST_DICT(audiowriter_audiowriter_locals_dict, audiowriter_audiowriter_locals_dict_table); +static MP_DEFINE_CONST_DICT(audiofilewriter_audiofilewriter_locals_dict, audiofilewriter_audiofilewriter_locals_dict_table); MP_DEFINE_CONST_OBJ_TYPE( - audiowriter_audiowriter_type, - MP_QSTR_AudioWriter, + audiofilewriter_audiofilewriter_type, + MP_QSTR_AudioFileWriter, MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, - make_new, audiowriter_audiowriter_make_new, - locals_dict, &audiowriter_audiowriter_locals_dict + make_new, audiofilewriter_audiofilewriter_make_new, + locals_dict, &audiofilewriter_audiofilewriter_locals_dict ); diff --git a/shared-bindings/audiofilewriter/AudioFileWriter.h b/shared-bindings/audiofilewriter/AudioFileWriter.h new file mode 100644 index 00000000000..22d456979b9 --- /dev/null +++ b/shared-bindings/audiofilewriter/AudioFileWriter.h @@ -0,0 +1,23 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +#include "shared-module/audiofilewriter/AudioFileWriter.h" + +extern const mp_obj_type_t audiofilewriter_audiofilewriter_type; + +void common_hal_audiofilewriter_audiofilewriter_construct(audiofilewriter_audiofilewriter_obj_t *self, + mp_obj_t file, uint32_t buffer_size); + +void common_hal_audiofilewriter_audiofilewriter_deinit(audiofilewriter_audiofilewriter_obj_t *self); +bool common_hal_audiofilewriter_audiofilewriter_deinited(audiofilewriter_audiofilewriter_obj_t *self); + +void common_hal_audiofilewriter_audiofilewriter_play(audiofilewriter_audiofilewriter_obj_t *self, mp_obj_t sample); +void common_hal_audiofilewriter_audiofilewriter_stop(audiofilewriter_audiofilewriter_obj_t *self); +bool common_hal_audiofilewriter_audiofilewriter_get_playing(audiofilewriter_audiofilewriter_obj_t *self); diff --git a/shared-bindings/audiofilewriter/__init__.c b/shared-bindings/audiofilewriter/__init__.c new file mode 100644 index 00000000000..e67a74b9451 --- /dev/null +++ b/shared-bindings/audiofilewriter/__init__.c @@ -0,0 +1,29 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/audiofilewriter/__init__.h" +#include "shared-bindings/audiofilewriter/AudioFileWriter.h" + +//| """Support for streaming audio to a WAV file""" + +static const mp_rom_map_elem_t audiofilewriter_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiofilewriter) }, + { MP_ROM_QSTR(MP_QSTR_AudioFileWriter), MP_ROM_PTR(&audiofilewriter_audiofilewriter_type) }, +}; + +static MP_DEFINE_CONST_DICT(audiofilewriter_module_globals, audiofilewriter_module_globals_table); + +const mp_obj_module_t audiofilewriter_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&audiofilewriter_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_audiofilewriter, audiofilewriter_module); diff --git a/shared-bindings/audiowriter/__init__.h b/shared-bindings/audiofilewriter/__init__.h similarity index 100% rename from shared-bindings/audiowriter/__init__.h rename to shared-bindings/audiofilewriter/__init__.h diff --git a/shared-bindings/audiowriter/AudioWriter.h b/shared-bindings/audiowriter/AudioWriter.h deleted file mode 100644 index e0de6b2ad9f..00000000000 --- a/shared-bindings/audiowriter/AudioWriter.h +++ /dev/null @@ -1,23 +0,0 @@ -// This file is part of the CircuitPython project: https://circuitpython.org -// -// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries -// -// SPDX-License-Identifier: MIT - -#pragma once - -#include "py/obj.h" - -#include "shared-module/audiowriter/AudioWriter.h" - -extern const mp_obj_type_t audiowriter_audiowriter_type; - -void common_hal_audiowriter_audiowriter_construct(audiowriter_audiowriter_obj_t *self, - mp_obj_t file, uint32_t buffer_size); - -void common_hal_audiowriter_audiowriter_deinit(audiowriter_audiowriter_obj_t *self); -bool common_hal_audiowriter_audiowriter_deinited(audiowriter_audiowriter_obj_t *self); - -void common_hal_audiowriter_audiowriter_play(audiowriter_audiowriter_obj_t *self, mp_obj_t sample); -void common_hal_audiowriter_audiowriter_stop(audiowriter_audiowriter_obj_t *self); -bool common_hal_audiowriter_audiowriter_get_playing(audiowriter_audiowriter_obj_t *self); diff --git a/shared-bindings/audiowriter/__init__.c b/shared-bindings/audiowriter/__init__.c deleted file mode 100644 index 21c0d15cc7b..00000000000 --- a/shared-bindings/audiowriter/__init__.c +++ /dev/null @@ -1,29 +0,0 @@ -// This file is part of the CircuitPython project: https://circuitpython.org -// -// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries -// -// SPDX-License-Identifier: MIT - -#include - -#include "py/obj.h" -#include "py/runtime.h" - -#include "shared-bindings/audiowriter/__init__.h" -#include "shared-bindings/audiowriter/AudioWriter.h" - -//| """Support for streaming audio to a WAV file""" - -static const mp_rom_map_elem_t audiowriter_module_globals_table[] = { - { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiowriter) }, - { MP_ROM_QSTR(MP_QSTR_AudioWriter), MP_ROM_PTR(&audiowriter_audiowriter_type) }, -}; - -static MP_DEFINE_CONST_DICT(audiowriter_module_globals, audiowriter_module_globals_table); - -const mp_obj_module_t audiowriter_module = { - .base = { &mp_type_module }, - .globals = (mp_obj_dict_t *)&audiowriter_module_globals, -}; - -MP_REGISTER_MODULE(MP_QSTR_audiowriter, audiowriter_module); diff --git a/shared-module/audiowriter/AudioWriter.c b/shared-module/audiofilewriter/AudioFileWriter.c similarity index 83% rename from shared-module/audiowriter/AudioWriter.c rename to shared-module/audiofilewriter/AudioFileWriter.c index 0ddbe07c96c..099345888a0 100644 --- a/shared-module/audiowriter/AudioWriter.c +++ b/shared-module/audiofilewriter/AudioFileWriter.c @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: MIT -#include "shared-bindings/audiowriter/AudioWriter.h" +#include "shared-bindings/audiofilewriter/AudioFileWriter.h" #include "shared-bindings/audiocore/__init__.h" #include "shared-module/audiocore/__init__.h" @@ -42,15 +42,15 @@ static void put_u32le(uint8_t *p, uint32_t v) { // recreated on soft reset. // --------------------------------------------------------------------------- -#define REGISTRY_HEAD ((audiowriter_audiowriter_obj_t *)MP_STATE_VM(audiowriter_linked_list)) +#define REGISTRY_HEAD ((audiofilewriter_audiofilewriter_obj_t *)MP_STATE_VM(audiofilewriter_linked_list)) // Add self to the registry. Called from Python (play()) context, so it must // guard against a background tick walking the list mid-mutation. -static void audiowriter_register(audiowriter_audiowriter_obj_t *self) { +static void audiofilewriter_register(audiofilewriter_audiofilewriter_obj_t *self) { background_callback_prevent(); // Avoid double-linking if already present. bool present = false; - for (audiowriter_audiowriter_obj_t *w = REGISTRY_HEAD; w != NULL; w = w->reg_next) { + for (audiofilewriter_audiofilewriter_obj_t *w = REGISTRY_HEAD; w != NULL; w = w->reg_next) { if (w == self) { present = true; break; @@ -58,7 +58,7 @@ static void audiowriter_register(audiowriter_audiowriter_obj_t *self) { } if (!present) { self->reg_next = REGISTRY_HEAD; - MP_STATE_VM(audiowriter_linked_list) = self; + MP_STATE_VM(audiofilewriter_linked_list) = self; } background_callback_allow(); } @@ -66,8 +66,8 @@ static void audiowriter_register(audiowriter_audiowriter_obj_t *self) { // Remove self from the registry. Callers must ensure no background tick is // walking the list concurrently: either they are the background tick itself // (single-threaded, so safe), or they wrap the call in prevent/allow. -static void audiowriter_unregister(audiowriter_audiowriter_obj_t *self) { - audiowriter_audiowriter_obj_t **pp = (audiowriter_audiowriter_obj_t **)&MP_STATE_VM(audiowriter_linked_list); +static void audiofilewriter_unregister(audiofilewriter_audiofilewriter_obj_t *self) { + audiofilewriter_audiofilewriter_obj_t **pp = (audiofilewriter_audiofilewriter_obj_t **)&MP_STATE_VM(audiofilewriter_linked_list); while (*pp != NULL) { if (*pp == self) { *pp = self->reg_next; @@ -84,7 +84,7 @@ static void audiowriter_unregister(audiowriter_audiowriter_obj_t *self) { // Copy len bytes from src into the ring. The caller guarantees there is room. // 8-bit signed PCM is flipped to unsigned to match the WAV convention. -static void audiowriter_ring_write(audiowriter_audiowriter_obj_t *self, const uint8_t *src, uint32_t len) { +static void audiofilewriter_ring_write(audiofilewriter_audiofilewriter_obj_t *self, const uint8_t *src, uint32_t len) { bool flip = (self->bits_per_sample == 8 && self->samples_signed); uint32_t i = 0; while (i < len) { @@ -110,7 +110,7 @@ static void audiowriter_ring_write(audiowriter_audiowriter_obj_t *self, const ui // Drain the ring to the file. Returns false on a write error. Non-raising: // safe to call from background-task context. -static bool audiowriter_flush(audiowriter_audiowriter_obj_t *self) { +static bool audiofilewriter_flush(audiofilewriter_audiofilewriter_obj_t *self) { while (self->ring_count > 0) { uint32_t span = self->ring_size - self->ring_tail; if (span > self->ring_count) { @@ -135,7 +135,7 @@ static bool audiowriter_flush(audiowriter_audiowriter_obj_t *self) { // Header patching + finalize // --------------------------------------------------------------------------- -static void audiowriter_patch_header(audiowriter_audiowriter_obj_t *self) { +static void audiofilewriter_patch_header(audiofilewriter_audiofilewriter_obj_t *self) { uint8_t sz[4]; int err = 0; @@ -160,18 +160,18 @@ static void audiowriter_patch_header(audiowriter_audiowriter_obj_t *self) { // Stop pumping, drain, patch the header, and release the source. Idempotent: // only the first call (while playing) does work. Non-raising. -static void audiowriter_finalize(audiowriter_audiowriter_obj_t *self) { +static void audiofilewriter_finalize(audiofilewriter_audiofilewriter_obj_t *self) { if (!self->playing) { return; } // Stop the pump first so a background tick can't re-enter us. self->playing = false; - audiowriter_flush(self); - audiowriter_patch_header(self); + audiofilewriter_flush(self); + audiofilewriter_patch_header(self); supervisor_disable_tick(); - audiowriter_unregister(self); + audiofilewriter_unregister(self); self->sample = MP_OBJ_NULL; } @@ -179,7 +179,7 @@ static void audiowriter_finalize(audiowriter_audiowriter_obj_t *self) { // The pump: one real-time-paced step per supervisor tick // --------------------------------------------------------------------------- -static void audiowriter_pump(audiowriter_audiowriter_obj_t *self) { +static void audiofilewriter_pump(audiofilewriter_audiofilewriter_obj_t *self) { if (!self->playing) { return; } @@ -228,7 +228,7 @@ static void audiowriter_pump(audiowriter_audiowriter_obj_t *self) { if (len > self->source_max_buffer) { len = self->source_max_buffer; } - audiowriter_ring_write(self, buf, len); + audiofilewriter_ring_write(self, buf, len); self->budget_frames -= (int64_t)(len / self->bytes_per_frame); } if (res == GET_BUFFER_DONE) { @@ -237,23 +237,23 @@ static void audiowriter_pump(audiowriter_audiowriter_obj_t *self) { } } - if (!audiowriter_flush(self)) { + if (!audiofilewriter_flush(self)) { // File write failed; give up gracefully rather than spin. self->source_done = true; } if (self->source_done && self->ring_count == 0) { - audiowriter_finalize(self); + audiofilewriter_finalize(self); } } -void audiowriter_background(void) { - audiowriter_audiowriter_obj_t *self = REGISTRY_HEAD; +void audiofilewriter_background(void) { + audiofilewriter_audiofilewriter_obj_t *self = REGISTRY_HEAD; while (self != NULL) { // Capture next before pumping: pump() may finalize self, which unlinks // it from the registry (but leaves our saved next pointer valid). - audiowriter_audiowriter_obj_t *next = self->reg_next; - audiowriter_pump(self); + audiofilewriter_audiofilewriter_obj_t *next = self->reg_next; + audiofilewriter_pump(self); self = next; } } @@ -261,10 +261,10 @@ void audiowriter_background(void) { // Called during soft reset (VM teardown). Any writer still active is abandoned: // we don't try to touch its file (it may already be gone), we just balance the // tick-enable count and drop it from the list. -void audiowriter_reset(void) { - audiowriter_audiowriter_obj_t *self = REGISTRY_HEAD; +void audiofilewriter_reset(void) { + audiofilewriter_audiofilewriter_obj_t *self = REGISTRY_HEAD; while (self != NULL) { - audiowriter_audiowriter_obj_t *next = self->reg_next; + audiofilewriter_audiofilewriter_obj_t *next = self->reg_next; if (self->playing) { self->playing = false; supervisor_disable_tick(); @@ -272,14 +272,14 @@ void audiowriter_reset(void) { self->reg_next = NULL; self = next; } - MP_STATE_VM(audiowriter_linked_list) = NULL; + MP_STATE_VM(audiofilewriter_linked_list) = NULL; } // --------------------------------------------------------------------------- // common-hal surface // --------------------------------------------------------------------------- -void common_hal_audiowriter_audiowriter_construct(audiowriter_audiowriter_obj_t *self, +void common_hal_audiofilewriter_audiofilewriter_construct(audiofilewriter_audiofilewriter_obj_t *self, mp_obj_t file, uint32_t buffer_size) { // The file must be a writable, seekable binary stream (a file or BytesIO). mp_get_stream_raise(file, MP_STREAM_OP_WRITE | MP_STREAM_OP_IOCTL); @@ -296,20 +296,20 @@ void common_hal_audiowriter_audiowriter_construct(audiowriter_audiowriter_obj_t self->reg_next = NULL; } -bool common_hal_audiowriter_audiowriter_deinited(audiowriter_audiowriter_obj_t *self) { +bool common_hal_audiofilewriter_audiofilewriter_deinited(audiofilewriter_audiofilewriter_obj_t *self) { return self->ring == NULL; } -void common_hal_audiowriter_audiowriter_deinit(audiowriter_audiowriter_obj_t *self) { +void common_hal_audiofilewriter_audiofilewriter_deinit(audiofilewriter_audiofilewriter_obj_t *self) { if (self->playing) { - common_hal_audiowriter_audiowriter_stop(self); + common_hal_audiofilewriter_audiofilewriter_stop(self); } self->ring = NULL; self->file = MP_OBJ_NULL; self->sample = MP_OBJ_NULL; } -void common_hal_audiowriter_audiowriter_play(audiowriter_audiowriter_obj_t *self, mp_obj_t sample_obj) { +void common_hal_audiofilewriter_audiofilewriter_play(audiofilewriter_audiofilewriter_obj_t *self, mp_obj_t sample_obj) { if (self->playing) { mp_raise_RuntimeError(MP_ERROR_TEXT("Already in progress")); } @@ -382,22 +382,22 @@ void common_hal_audiowriter_audiowriter_play(audiowriter_audiowriter_obj_t *self self->last_tick_ms = supervisor_ticks_ms64(); self->playing = true; - audiowriter_register(self); + audiofilewriter_register(self); supervisor_enable_tick(); } -void common_hal_audiowriter_audiowriter_stop(audiowriter_audiowriter_obj_t *self) { +void common_hal_audiofilewriter_audiofilewriter_stop(audiofilewriter_audiofilewriter_obj_t *self) { if (!self->playing) { return; } // Keep the background pump out while we finalize from Python context. background_callback_prevent(); - audiowriter_finalize(self); + audiofilewriter_finalize(self); background_callback_allow(); } -bool common_hal_audiowriter_audiowriter_get_playing(audiowriter_audiowriter_obj_t *self) { +bool common_hal_audiofilewriter_audiofilewriter_get_playing(audiofilewriter_audiofilewriter_obj_t *self) { return self->playing; } -MP_REGISTER_ROOT_POINTER(mp_obj_t audiowriter_linked_list); +MP_REGISTER_ROOT_POINTER(mp_obj_t audiofilewriter_linked_list); diff --git a/shared-module/audiowriter/AudioWriter.h b/shared-module/audiofilewriter/AudioFileWriter.h similarity index 88% rename from shared-module/audiowriter/AudioWriter.h rename to shared-module/audiofilewriter/AudioFileWriter.h index 1aab3f401ad..0e99e90e915 100644 --- a/shared-module/audiowriter/AudioWriter.h +++ b/shared-module/audiofilewriter/AudioFileWriter.h @@ -15,7 +15,7 @@ // or an effect chain) and writes the resulting PCM to a file. Unlike WaveFile // it is NOT an audiosample, it is the thing that drives a source, playing the // role an AudioOut would. -typedef struct _audiowriter_audiowriter_obj_t { +typedef struct _audiofilewriter_audiofilewriter_obj_t { mp_obj_base_t base; // Output stream (anything with write + MP_STREAM_SEEK ioctl: a file or a @@ -24,7 +24,7 @@ typedef struct _audiowriter_audiowriter_obj_t { // The source being recorded. Only valid (and referenced) while playing. mp_obj_t sample; - // Format, captured from the source at play() time. AudioWriter is the + // Format, captured from the source at play() time. AudioFileWriter is the // format authority for the WAV header. uint32_t sample_rate; uint8_t channel_count; @@ -56,12 +56,12 @@ typedef struct _audiowriter_audiowriter_obj_t { bool source_done; // source returned DONE/ERROR; drain then finalize // Intrusive linked list of active writers, walked once per supervisor tick. - struct _audiowriter_audiowriter_obj_t *reg_next; -} audiowriter_audiowriter_obj_t; + struct _audiofilewriter_audiofilewriter_obj_t *reg_next; +} audiofilewriter_audiofilewriter_obj_t; // Called once per supervisor tick (from supervisor_background_tick), in // background-task context. Pumps every active writer. -void audiowriter_background(void); +void audiofilewriter_background(void); // Called during soft reset to abandon any writer left recording. -void audiowriter_reset(void); +void audiofilewriter_reset(void); diff --git a/shared-module/audiowriter/__init__.c b/shared-module/audiofilewriter/__init__.c similarity index 100% rename from shared-module/audiowriter/__init__.c rename to shared-module/audiofilewriter/__init__.c diff --git a/supervisor/shared/tick.c b/supervisor/shared/tick.c index a224de14720..81815c0813b 100644 --- a/supervisor/shared/tick.c +++ b/supervisor/shared/tick.c @@ -27,8 +27,8 @@ #include "shared-module/keypad/__init__.h" #endif -#if CIRCUITPY_AUDIOWRITER -#include "shared-module/audiowriter/AudioWriter.h" +#if CIRCUITPY_AUDIOFILEWRITER +#include "shared-module/audiofilewriter/AudioFileWriter.h" #endif #include "shared-bindings/microcontroller/__init__.h" @@ -63,8 +63,8 @@ static void supervisor_background_tick(void *unused) { filesystem_background(); - #if CIRCUITPY_AUDIOWRITER - audiowriter_background(); + #if CIRCUITPY_AUDIOFILEWRITER + audiofilewriter_background(); #endif port_background_tick(); From 2e40b01e78ca90beb07abf218f33452320ed6b86 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 10 Jul 2026 14:20:12 -0500 Subject: [PATCH 5/5] merge main, fix translations --- locale/circuitpython.pot | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 9c81ed5dbd7..32f6e4ef631 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -498,9 +498,8 @@ msgstr "" msgid "wrong length of index array" msgstr "" -#: ports/espressif/common-hal/_bleio/__init__.c -#: shared-module/audiofilewriter/AudioFileWriter.c -msgid "Already in progress" +#: extmod/ulab/code/numpy/transform.c +msgid "dimensions do not match" msgstr "" #: extmod/ulab/code/numpy/vector.c @@ -1247,6 +1246,7 @@ msgid "Not connected" msgstr "" #: ports/espressif/common-hal/_bleio/__init__.c +#: shared-module/audiofilewriter/AudioFileWriter.c msgid "Already in progress" msgstr "" @@ -1783,10 +1783,6 @@ msgstr "" msgid "WatchDogTimer cannot be deinitialized once mode is set to RESET" msgstr "" -#: shared-module/audiofilewriter/AudioFileWriter.c -msgid "Only 8/16-bit mono/stereo is supported" -msgstr "" - #: ports/nordic/common-hal/watchdog/WatchDogTimer.c msgid "timeout duration exceeded the maximum supported value" msgstr "" @@ -2654,10 +2650,6 @@ msgstr "" msgid "'%s' expects an FPU register" msgstr "" -#: shared-module/audiofilewriter/AudioFileWriter.c -msgid "buffer_size too small for source" -msgstr "" - #: py/emitinlinethumb.c #, c-format msgid "'%s' expects {r0, r1, ...}" @@ -4261,6 +4253,14 @@ msgstr "" msgid "%q in %q must be of type %q or %q, not %q" msgstr "" +#: shared-module/audiofilewriter/AudioFileWriter.c +msgid "Only 8/16-bit mono/stereo is supported" +msgstr "" + +#: shared-module/audiofilewriter/AudioFileWriter.c +msgid "buffer_size too small for source" +msgstr "" + #: shared-module/audiomp3/MP3Decoder.c msgid "Couldn't allocate decoder" msgstr ""