Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 ""
Expand Down
8 changes: 8 additions & 0 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions ports/espressif/mpconfigport.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ CIRCUITPY_SOCKETPOOL = 1
CIRCUITPY_WIFI = 1

CIRCUITPY_PICODVI = 1
CIRCUITPY_AUDIOWRITER = 0

CFLAGS += \
-DCYW43_PIN_WL_DYNAMIC=0 \
Expand Down
1 change: 1 addition & 0 deletions ports/raspberrypi/mpconfigport.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions py/circuitpy_defns.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down
3 changes: 3 additions & 0 deletions py/circuitpy_mpconfig.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
181 changes: 181 additions & 0 deletions shared-bindings/audiowriter/AudioWriter.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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 <stdint.h>

#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, ``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."""
//|
//| 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 synthio to SD::
//|
//| 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)
//| """
//| ...
//|
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
);
23 changes: 23 additions & 0 deletions shared-bindings/audiowriter/AudioWriter.h
Original file line number Diff line number Diff line change
@@ -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/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);
29 changes: 29 additions & 0 deletions shared-bindings/audiowriter/__init__.c
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

#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);
7 changes: 7 additions & 0 deletions shared-bindings/audiowriter/__init__.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// 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
Loading
Loading