From e9b2fd49f5c81cfaab1ee80f42b5fa325c252103 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 20 Oct 2026 10:23:41 -0500 Subject: [PATCH 1/2] audiotap module initial commit --- .../unix/variants/coverage/mpconfigvariant.mk | 5 + py/circuitpy_defns.mk | 5 + py/circuitpy_mpconfig.mk | 2 + shared-bindings/audiotap/Tap.c | 380 ++++++++++++++++++ shared-bindings/audiotap/Tap.h | 29 ++ shared-bindings/audiotap/__init__.c | 33 ++ shared-module/audiotap/Tap.c | 317 +++++++++++++++ shared-module/audiotap/Tap.h | 57 +++ shared-module/audiotap/__init__.c | 7 + tests/circuitpython/audiotap_tap.py | 85 ++++ tests/circuitpython/audiotap_tap.py.exp | 19 + 11 files changed, 939 insertions(+) create mode 100644 shared-bindings/audiotap/Tap.c create mode 100644 shared-bindings/audiotap/Tap.h create mode 100644 shared-bindings/audiotap/__init__.c create mode 100644 shared-module/audiotap/Tap.c create mode 100644 shared-module/audiotap/Tap.h create mode 100644 shared-module/audiotap/__init__.c create mode 100644 tests/circuitpython/audiotap_tap.py create mode 100644 tests/circuitpython/audiotap_tap.py.exp diff --git a/ports/unix/variants/coverage/mpconfigvariant.mk b/ports/unix/variants/coverage/mpconfigvariant.mk index beab74832cb..b9a170b012a 100644 --- a/ports/unix/variants/coverage/mpconfigvariant.mk +++ b/ports/unix/variants/coverage/mpconfigvariant.mk @@ -43,6 +43,8 @@ SRC_BITMAP := \ shared-bindings/audiofilters/Filter.c \ shared-bindings/audiofilters/Phaser.c \ shared-bindings/audiofilters/__init__.c \ + shared-bindings/audiotap/Tap.c \ + shared-bindings/audiotap/__init__.c \ shared-bindings/audiofreeverb/Freeverb.c \ shared-bindings/audiofreeverb/__init__.c \ shared-bindings/audiomixer/__init__.c \ @@ -93,6 +95,8 @@ SRC_BITMAP := \ shared-module/audiofilters/Filter.c \ shared-module/audiofilters/Phaser.c \ shared-module/audiofilters/__init__.c \ + shared-module/audiotap/Tap.c \ + shared-module/audiotap/__init__.c \ shared-module/audiofreeverb/Freeverb.c \ shared-module/audiofreeverb/__init__.c \ shared-module/audiomixer/__init__.c \ @@ -159,6 +163,7 @@ CFLAGS += \ -DCIRCUITPY_AUDIODELAYS=1 \ -DCIRCUITPY_AUDIOFILTERS=1 \ -DCIRCUITPY_AUDIOMIXER=1 \ + -DCIRCUITPY_AUDIOTAP=1 \ -DCIRCUITPY_AUDIOMP3=1 \ -DCIRCUITPY_AUDIOCORE_DEBUG=1 \ -DCIRCUITPY_BITMAPTOOLS=1 \ diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index ec84d3ec381..ccfcaac96d2 100755 --- 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_AUDIOTAP),1) +SRC_PATTERNS += audiotap/% +endif ifeq ($(CIRCUITPY_AUDIOFILEWRITER),1) SRC_PATTERNS += audiofilewriter/% endif @@ -758,6 +761,8 @@ SRC_SHARED_MODULE_ALL = \ audiospeed/Resampler.c \ audiospeed/SpeedChanger.c \ audiospeed/__init__.c \ + audiotap/Tap.c \ + audiotap/__init__.c \ audiodelays/Echo.c \ audiodelays/Chorus.c \ audiodelays/Flanger.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 0fc82d03704..7c8a57fecc7 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -173,6 +173,8 @@ CIRCUITPY_AUDIOFILTERS ?= $(CIRCUITPY_AUDIOEFFECTS) CFLAGS += -DCIRCUITPY_AUDIOFILTERS=$(CIRCUITPY_AUDIOFILTERS) CIRCUITPY_AUDIOFREEVERB ?= $(CIRCUITPY_AUDIOEFFECTS) CFLAGS += -DCIRCUITPY_AUDIOFREEVERB=$(CIRCUITPY_AUDIOFREEVERB) +CIRCUITPY_AUDIOTAP ?= $(CIRCUITPY_AUDIOEFFECTS) +CFLAGS += -DCIRCUITPY_AUDIOTAP=$(CIRCUITPY_AUDIOTAP) CIRCUITPY_AUDIOFILEWRITER ?= 0 CFLAGS += -DCIRCUITPY_AUDIOFILEWRITER=$(CIRCUITPY_AUDIOFILEWRITER) diff --git a/shared-bindings/audiotap/Tap.c b/shared-bindings/audiotap/Tap.c new file mode 100644 index 00000000000..e8e151edc62 --- /dev/null +++ b/shared-bindings/audiotap/Tap.c @@ -0,0 +1,380 @@ +// 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 "shared-bindings/audiotap/Tap.h" +#include "shared-bindings/audiocore/__init__.h" + +#include "shared/runtime/context_manager_helpers.h" +#include "py/binary.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/util.h" + +//| class Tap: +//| """Passes audio through unchanged and keeps a copy that the program can read""" +//| +//| def __init__( +//| self, +//| capacity: int = 4096, +//| buffer_size: int = 512, +//| sample_rate: int = 8000, +//| bits_per_sample: int = 16, +//| samples_signed: bool = True, +//| channel_count: int = 1, +//| ) -> None: +//| """Create a Tap. It plays a sample like the effects in `audiofilters` +//| and `audiodelays` do, so it can go anywhere in an audio chain. Whatever +//| plays the tap gets the sample's audio unchanged. +//| +//| As the audio passes through, the tap keeps it in a buffer of +//| ``capacity`` samples, which `readinto` copies out of without waiting, +//| and measures its levels for `levels`. The program can look at the +//| audio this way, at the point in the chain where the tap is, while it +//| plays. +//| +//| The tap only sees audio that is played through it: something, such +//| as an `audiomixer.Mixer` voice playing to an audio output, must play +//| it or a chain that it is part of. To look at audio without hearing +//| it, play it at a ``level`` of 0. Until its sample is played, and +//| after it ends, the tap plays silence and keeps nothing. +//| +//| When the program reads too slowly, the buffer fills up, the tap keeps +//| the audio it has and drops the newer audio until there is room +//| again, and `overflow` turns True. The audio that passes through is +//| never affected. +//| +//| :param int capacity: The number of samples kept for `readinto`, rounded +//| down to whole frames. A stereo frame is two samples. 0 keeps none, +//| for a tap that only measures `levels`. +//| :param int buffer_size: The total size in bytes of each of the two playback buffers to use +//| :param int sample_rate: The sample rate to be used +//| :param int bits_per_sample: The bits per sample of the audio: 8 or 16 +//| :param bool samples_signed: Effect is signed (True) or unsigned (False) +//| :param int channel_count: The number of channels the source samples contain. 1 = mono; 2 = stereo. +//| +//| Showing the level of a USB microphone while it is heard through the +//| DVI output, and keeping the audio for a spectrum display. The +//| ``adafruit_usb_host_descriptors`` library finds the microphone and +//| its format, and `usb.core` starts its stream; see +//| `usb_host_audio.USBIn`. DVI audio plays at 32, 44.1 or 48 kHz, so an +//| `audiospeed.Resampler` converts other sample rates:: +//| +//| import array +//| import time +//| +//| import adafruit_usb_host_descriptors +//| import audiomixer +//| import audiospeed +//| import audiotap +//| import displayio +//| import supervisor +//| import usb_host_audio +//| import vectorio +//| +//| device = adafruit_usb_host_descriptors.find_microphone() +//| if device is None: +//| raise RuntimeError("Plug a USB microphone or webcam into the USB host port") +//| formats = adafruit_usb_host_descriptors.find_microphone_formats(device) +//| mic_format = adafruit_usb_host_descriptors.choose_microphone_format(formats, sample_rate=48000) +//| sample_rate = mic_format.nearest_rate(48000) +//| +//| device.set_configuration() +//| # SET_INTERFACE to the format's alternate setting starts the stream, then +//| # SET_CUR of the endpoint's sampling frequency picks the rate. +//| device.ctrl_transfer(0x01, 11, mic_format.alternate, mic_format.interface, None) +//| if mic_format.rate_control: +//| device.ctrl_transfer( +//| 0x22, 0x01, 0x0100, mic_format.endpoint, sample_rate.to_bytes(3, "little") +//| ) +//| +//| mic = usb_host_audio.USBIn( +//| device, +//| mic_format.endpoint, +//| sample_rate=sample_rate, +//| channel_count=mic_format.channel_count, +//| bit_depth=8 * mic_format.bytes_per_sample, +//| samples_signed=mic_format.samples_signed, +//| max_packet_size=mic_format.max_packet_size, +//| ) +//| # Keep a quarter second, more than arrives between reads of the loop below. +//| tap = audiotap.Tap( +//| capacity=sample_rate * mic.channel_count // 4, +//| sample_rate=sample_rate, +//| channel_count=mic.channel_count, +//| ) +//| mixer = audiomixer.Mixer(voice_count=1, sample_rate=48000, channel_count=mic.channel_count) +//| supervisor.runtime.audio.play(mixer) +//| mixer.voice[0].play(audiospeed.Resampler(tap.play(mic))) +//| +//| # Bars for the peak (top) and RMS (bottom) levels. Showing them hides the +//| # console, whose scrolling stalls the microphone's stream. print() still +//| # goes to the serial console. +//| display = supervisor.runtime.display +//| palette = displayio.Palette(1) +//| palette[0] = 0x00FF00 +//| peak_bar = vectorio.Rectangle(pixel_shader=palette, width=1, height=40, x=0, y=60) +//| rms_bar = vectorio.Rectangle(pixel_shader=palette, width=1, height=40, x=0, y=140) +//| meter = displayio.Group() +//| meter.append(peak_bar) +//| meter.append(rms_bar) +//| display.root_group = meter +//| +//| samples = array.array("h", [0] * 1024) +//| while True: +//| # Read the audio that is waiting. A spectrum display would use these samples. +//| while tap.available >= len(samples): +//| tap.readinto(samples) +//| peak, rms = tap.levels() +//| peak_bar.width = max(1, int(peak * display.width)) +//| rms_bar.width = max(1, int(rms * display.width)) +//| print(f"peak {peak:.3f} rms {rms:.3f}") +//| time.sleep(0.1)""" +//| ... +//| +static mp_obj_t audiotap_tap_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_capacity, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_capacity, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 4096} }, + { MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 512} }, + { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} }, + { MP_QSTR_bits_per_sample, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} }, + { MP_QSTR_samples_signed, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = true} }, + { MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1 } }, + }; + + 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); + + mp_int_t capacity = mp_arg_validate_int_range(args[ARG_capacity].u_int, 0, 1 << 24, MP_QSTR_capacity); + mp_int_t buffer_size = mp_arg_validate_int_range(args[ARG_buffer_size].u_int, 4, 1 << 16, MP_QSTR_buffer_size); + mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count); + mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate); + mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int; + if (bits_per_sample != 8 && bits_per_sample != 16) { + mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 8 or 16")); + } + + audiotap_tap_obj_t *self = mp_obj_malloc_with_finaliser(audiotap_tap_obj_t, &audiotap_tap_type); + common_hal_audiotap_tap_construct(self, capacity, buffer_size, bits_per_sample, + args[ARG_samples_signed].u_bool, channel_count, sample_rate); + + return MP_OBJ_FROM_PTR(self); +} + +//| def deinit(self) -> None: +//| """Deinitialises the Tap.""" +//| ... +//| +static mp_obj_t audiotap_tap_deinit(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiotap_tap_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_deinit_obj, audiotap_tap_deinit); + +static void check_for_deinit(audiotap_tap_obj_t *self) { + audiosample_check_for_deinit(&self->base); +} + +//| def __enter__(self) -> Tap: +//| """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. + +//| playing: bool +//| """True when the tap is playing a sample. (read-only)""" +//| +static mp_obj_t audiotap_tap_obj_get_playing(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiotap_tap_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_playing_obj, audiotap_tap_obj_get_playing); + +MP_PROPERTY_GETTER(audiotap_tap_playing_obj, + (mp_obj_t)&audiotap_tap_get_playing_obj); + +//| capacity: int +//| """The number of samples that the tap keeps for `readinto`. (read-only)""" +//| +static mp_obj_t audiotap_tap_obj_get_capacity(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_int_from_uint(common_hal_audiotap_tap_get_capacity(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_capacity_obj, audiotap_tap_obj_get_capacity); + +MP_PROPERTY_GETTER(audiotap_tap_capacity_obj, + (mp_obj_t)&audiotap_tap_get_capacity_obj); + +//| available: int +//| """The number of samples waiting to be read with `readinto`. (read-only)""" +//| +static mp_obj_t audiotap_tap_obj_get_available(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_int_from_uint(common_hal_audiotap_tap_get_available(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_available_obj, audiotap_tap_obj_get_available); + +MP_PROPERTY_GETTER(audiotap_tap_available_obj, + (mp_obj_t)&audiotap_tap_get_available_obj); + +//| overflow: bool +//| """True if audio was dropped since the last time this was checked, +//| because `readinto` did not keep up. Reading this clears it. (read-only)""" +//| +static mp_obj_t audiotap_tap_obj_get_overflow(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiotap_tap_get_overflow(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_overflow_obj, audiotap_tap_obj_get_overflow); + +MP_PROPERTY_GETTER(audiotap_tap_overflow_obj, + (mp_obj_t)&audiotap_tap_get_overflow_obj); + +//| def readinto(self, buffer: WriteableBuffer) -> int: +//| """Copies the oldest audio waiting into ``buffer`` and returns the number +//| of samples copied, which is 0 when none is waiting. Does not wait for +//| audio. Copies whole frames only: a stereo frame is two samples, left +//| then right. +//| +//| The samples are copied as they are played, so ``buffer`` is +//| usually an ``array.array`` of typecode ``'h'`` for 16 bit signed +//| samples. A ``bytearray`` works for any format and holds half as +//| many 16 bit samples as its length. To read into part of a buffer, +//| pass a slice of a ``memoryview`` of it.""" +//| ... +//| +static mp_obj_t audiotap_tap_obj_readinto(mp_obj_t self_in, mp_obj_t buffer_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(buffer_in, &bufinfo, MP_BUFFER_WRITE); + uint32_t length = bufinfo.len / (self->base.bits_per_sample / 8); + return mp_obj_new_int_from_uint(common_hal_audiotap_tap_readinto(self, bufinfo.buf, length)); +} +MP_DEFINE_CONST_FUN_OBJ_2(audiotap_tap_readinto_obj, audiotap_tap_obj_readinto); + +//| def clear(self) -> None: +//| """Discards the audio waiting, so that `readinto` returns only audio +//| that passes through after this. Use it to skip ahead to the live +//| audio, for example after an `overflow`.""" +//| ... +//| +static mp_obj_t audiotap_tap_obj_clear(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_audiotap_tap_clear(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_clear_obj, audiotap_tap_obj_clear); + +//| def levels(self) -> Tuple[float, float]: +//| """Returns the peak and RMS level of the audio that passed through +//| since the last call, from 0.0 to 1.0 of full scale, and starts a new +//| measurement. Both are 0.0 when no audio passed through. The channels +//| of stereo audio are measured together. This works with a ``capacity`` +//| of 0 too.""" +//| ... +//| +static mp_obj_t audiotap_tap_obj_levels(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + mp_float_t peak, rms; + common_hal_audiotap_tap_get_levels(self, &peak, &rms); + mp_obj_t levels[2] = { mp_obj_new_float(peak), mp_obj_new_float(rms) }; + return mp_obj_new_tuple(2, levels); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_levels_obj, audiotap_tap_obj_levels); + +//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> Tap: +//| """Plays the sample once when loop=False and continuously when loop=True. +//| Does not block. Use `playing` to block. +//| +//| The sample must match the encoding settings given in the constructor. +//| +//| :return: The tap itself. Can be used for chaining, ie: +//| ``audio.play(tap.play(sample))``. +//| :rtype: Tap""" +//| ... +//| +static mp_obj_t audiotap_tap_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_sample, ARG_loop }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED, {} }, + { MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + }; + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + common_hal_audiotap_tap_play(self, args[ARG_sample].u_obj, args[ARG_loop].u_bool); + + return MP_OBJ_FROM_PTR(self); +} +MP_DEFINE_CONST_FUN_OBJ_KW(audiotap_tap_play_obj, 1, audiotap_tap_obj_play); + +//| def stop(self) -> None: +//| """Stops playback of the sample. The audio already kept can still be read.""" +//| ... +//| +//| +static mp_obj_t audiotap_tap_obj_stop(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_audiotap_tap_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_stop_obj, audiotap_tap_obj_stop); + +static const mp_rom_map_elem_t audiotap_tap_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiotap_tap_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audiotap_tap_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(&audiotap_tap_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiotap_tap_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&audiotap_tap_readinto_obj) }, + { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&audiotap_tap_clear_obj) }, + { MP_ROM_QSTR(MP_QSTR_levels), MP_ROM_PTR(&audiotap_tap_levels_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiotap_tap_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_capacity), MP_ROM_PTR(&audiotap_tap_capacity_obj) }, + { MP_ROM_QSTR(MP_QSTR_available), MP_ROM_PTR(&audiotap_tap_available_obj) }, + { MP_ROM_QSTR(MP_QSTR_overflow), MP_ROM_PTR(&audiotap_tap_overflow_obj) }, + AUDIOSAMPLE_FIELDS, +}; +static MP_DEFINE_CONST_DICT(audiotap_tap_locals_dict, audiotap_tap_locals_dict_table); + +static const audiosample_p_t audiotap_tap_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + .reset_buffer = (audiosample_reset_buffer_fun)audiotap_tap_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)audiotap_tap_get_buffer, +}; + +MP_DEFINE_CONST_OBJ_TYPE( + audiotap_tap_type, + MP_QSTR_Tap, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audiotap_tap_make_new, + locals_dict, &audiotap_tap_locals_dict, + protocol, &audiotap_tap_proto + ); diff --git a/shared-bindings/audiotap/Tap.h b/shared-bindings/audiotap/Tap.h new file mode 100644 index 00000000000..87796e779f3 --- /dev/null +++ b/shared-bindings/audiotap/Tap.h @@ -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 + +#pragma once + +#include "shared-module/audiotap/Tap.h" + +extern const mp_obj_type_t audiotap_tap_type; + +void common_hal_audiotap_tap_construct(audiotap_tap_obj_t *self, uint32_t capacity, + uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, + uint8_t channel_count, uint32_t sample_rate); + +void common_hal_audiotap_tap_deinit(audiotap_tap_obj_t *self); + +bool common_hal_audiotap_tap_get_playing(audiotap_tap_obj_t *self); +void common_hal_audiotap_tap_play(audiotap_tap_obj_t *self, mp_obj_t sample, bool loop); +void common_hal_audiotap_tap_stop(audiotap_tap_obj_t *self); + +uint32_t common_hal_audiotap_tap_get_capacity(audiotap_tap_obj_t *self); +uint32_t common_hal_audiotap_tap_get_available(audiotap_tap_obj_t *self); +// Copies at most length samples, in whole frames. Returns the number copied. +uint32_t common_hal_audiotap_tap_readinto(audiotap_tap_obj_t *self, uint8_t *buffer, uint32_t length); +void common_hal_audiotap_tap_clear(audiotap_tap_obj_t *self); +bool common_hal_audiotap_tap_get_overflow(audiotap_tap_obj_t *self); +void common_hal_audiotap_tap_get_levels(audiotap_tap_obj_t *self, mp_float_t *peak, mp_float_t *rms); diff --git a/shared-bindings/audiotap/__init__.c b/shared-bindings/audiotap/__init__.c new file mode 100644 index 00000000000..06278eaf2d7 --- /dev/null +++ b/shared-bindings/audiotap/__init__.c @@ -0,0 +1,33 @@ +// 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/audiotap/Tap.h" + +//| """Look at audio as it plays +//| +//| The `audiotap` module contains `Tap`, which goes into an audio chain like +//| the effects in `audiofilters` and `audiodelays`. Audio passes through it +//| unchanged, and the program can read a copy of it and its levels, for meters, +//| waveforms, spectrum analysis or recording, while it plays.""" + +static const mp_rom_map_elem_t audiotap_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiotap) }, + { MP_ROM_QSTR(MP_QSTR_Tap), MP_ROM_PTR(&audiotap_tap_type) }, +}; + +static MP_DEFINE_CONST_DICT(audiotap_module_globals, audiotap_module_globals_table); + +const mp_obj_module_t audiotap_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&audiotap_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_audiotap, audiotap_module); diff --git a/shared-module/audiotap/Tap.c b/shared-module/audiotap/Tap.c new file mode 100644 index 00000000000..6f710629175 --- /dev/null +++ b/shared-module/audiotap/Tap.c @@ -0,0 +1,317 @@ +// 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 "shared-bindings/audiotap/Tap.h" +#include "shared-bindings/audiocore/__init__.h" + +#include +#include +#include + +#include "py/runtime.h" + +void common_hal_audiotap_tap_construct(audiotap_tap_obj_t *self, uint32_t capacity, + uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, + uint8_t channel_count, uint32_t sample_rate) { + + self->base.bits_per_sample = bits_per_sample; + self->base.samples_signed = samples_signed; + self->base.channel_count = channel_count; + self->base.sample_rate = sample_rate; + self->base.single_buffer = false; + + // Whole frames only, so that a stereo pair is never split between buffers + // or between the ring's end and its start. + const uint32_t frame_bytes = (bits_per_sample / 8) * channel_count; + self->buffer_len = MAX(frame_bytes, buffer_size - buffer_size % frame_bytes); + self->base.max_buffer_length = self->buffer_len; + + self->buffer[0] = m_malloc_without_collect(self->buffer_len); + memset(self->buffer[0], 0, self->buffer_len); + self->buffer[1] = m_malloc_without_collect(self->buffer_len); + memset(self->buffer[1], 0, self->buffer_len); + self->last_buf_idx = 1; + + self->sample = NULL; + self->sample_remaining_buffer = NULL; + self->sample_buffer_length = 0; + self->loop = false; + self->more_data = false; + + self->capacity = capacity - capacity % channel_count; + self->ring = NULL; + if (self->capacity > 0) { + self->ring = m_malloc_without_collect(self->capacity * (bits_per_sample / 8)); + } + self->write_pos = 0; + self->read_pos = 0; + self->overflow = false; + + self->peak = 0; + self->sum_squares = 0; + self->level_count = 0; + self->levels_reset = false; +} + +void common_hal_audiotap_tap_deinit(audiotap_tap_obj_t *self) { + audiosample_mark_deinit(&self->base); + self->sample = NULL; + self->buffer[0] = NULL; + self->buffer[1] = NULL; + self->ring = NULL; + self->capacity = 0; +} + +void audiotap_tap_reset_buffer(audiotap_tap_obj_t *self, + bool single_channel_output, + uint8_t channel) { + (void)single_channel_output; + (void)channel; + if (self->buffer[0] == NULL) { + return; + } + // The ring belongs to the reader; what it holds stays readable. + memset(self->buffer[0], 0, self->buffer_len); + memset(self->buffer[1], 0, self->buffer_len); +} + +bool common_hal_audiotap_tap_get_playing(audiotap_tap_obj_t *self) { + return self->sample != NULL; +} + +void common_hal_audiotap_tap_play(audiotap_tap_obj_t *self, mp_obj_t sample, bool loop) { + audiosample_must_match(&self->base, sample, false); + + self->sample = sample; + self->loop = loop; + + audiosample_reset_buffer(self->sample, false, 0); + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, + (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); + // Track the remaining length in samples. + self->sample_buffer_length /= (self->base.bits_per_sample / 8); + self->more_data = result == GET_BUFFER_MORE_DATA; +} + +void common_hal_audiotap_tap_stop(audiotap_tap_obj_t *self) { + self->sample = NULL; +} + +uint32_t common_hal_audiotap_tap_get_capacity(audiotap_tap_obj_t *self) { + return self->capacity; +} + +// Positions run from 0 to 2 * capacity. +static uint32_t ring_used(uint32_t capacity, uint32_t write, uint32_t read) { + return write >= read ? write - read : write + 2 * capacity - read; +} + +static uint32_t ring_index(uint32_t capacity, uint32_t pos) { + return pos >= capacity ? pos - capacity : pos; +} + +static uint32_t ring_advance(uint32_t capacity, uint32_t pos, uint32_t count) { + pos += count; + return pos >= 2 * capacity ? pos - 2 * capacity : pos; +} + +// The sample at i, as a signed 16 bit value. +static int32_t sample_at(audiotap_tap_obj_t *self, const uint8_t *data, uint32_t i) { + if (MP_LIKELY(self->base.bits_per_sample == 16)) { + uint16_t raw = ((const uint16_t *)data)[i]; + return self->base.samples_signed ? (int16_t)raw : (int16_t)(raw ^ 0x8000); + } + uint8_t raw = data[i]; + return (self->base.samples_signed ? (int8_t)raw : (int8_t)(raw ^ 0x80)) * 256; +} + +// Called by whatever plays the tap, for each run of samples from the source. +// It may be an interrupt, so it never allocates and changes only write_pos and +// the levels. +static void tap_capture(audiotap_tap_obj_t *self, const uint8_t *data, uint32_t count) { + if (count == 0) { + return; + } + + if (self->levels_reset) { + self->peak = 0; + self->sum_squares = 0; + self->level_count = 0; + self->levels_reset = false; + } + uint32_t peak = self->peak; + uint64_t sum = 0; + for (uint32_t i = 0; i < count; i++) { + int32_t value = sample_at(self, data, i); + uint32_t magnitude = value < 0 ? -value : value; + if (magnitude > peak) { + peak = magnitude; + } + sum += (uint64_t)(value * value); + } + self->peak = peak; + self->sum_squares += (float)sum / (32768.0f * 32768.0f); + self->level_count += count; + + if (self->ring == NULL) { + return; + } + const uint32_t capacity = self->capacity; + const uint32_t sample_bytes = self->base.bits_per_sample / 8; + uint32_t write = self->write_pos; + uint32_t free = capacity - ring_used(capacity, write, self->read_pos); + if (count > free) { + // The reader fell behind. Moving read_pos to keep the newest audio + // would race with it, so drop what doesn't fit. + count = free; + self->overflow = true; + } + while (count > 0) { + uint32_t index = ring_index(capacity, write); + uint32_t run = MIN(count, capacity - index); + memcpy(self->ring + index * sample_bytes, data, run * sample_bytes); + data += run * sample_bytes; + count -= run; + write = ring_advance(capacity, write, run); + } + // The samples must be in the ring before the reader can see them. + __sync_synchronize(); + self->write_pos = write; +} + +uint32_t common_hal_audiotap_tap_get_available(audiotap_tap_obj_t *self) { + if (self->ring == NULL) { + return 0; + } + return ring_used(self->capacity, self->write_pos, self->read_pos); +} + +uint32_t common_hal_audiotap_tap_readinto(audiotap_tap_obj_t *self, uint8_t *buffer, uint32_t length) { + if (self->ring == NULL) { + return 0; + } + const uint32_t capacity = self->capacity; + const uint32_t sample_bytes = self->base.bits_per_sample / 8; + uint32_t read = self->read_pos; + uint32_t write = self->write_pos; + __sync_synchronize(); + uint32_t count = MIN(length - length % self->base.channel_count, + ring_used(capacity, write, read)); + uint32_t done = 0; + while (done < count) { + uint32_t index = ring_index(capacity, read); + uint32_t run = MIN(count - done, capacity - index); + memcpy(buffer + done * sample_bytes, self->ring + index * sample_bytes, run * sample_bytes); + done += run; + read = ring_advance(capacity, read, run); + } + // Copy out before the writer may reuse the space. + __sync_synchronize(); + self->read_pos = read; + return count; +} + +void common_hal_audiotap_tap_clear(audiotap_tap_obj_t *self) { + self->read_pos = self->write_pos; +} + +bool common_hal_audiotap_tap_get_overflow(audiotap_tap_obj_t *self) { + bool overflow = self->overflow; + self->overflow = false; + return overflow; +} + +void common_hal_audiotap_tap_get_levels(audiotap_tap_obj_t *self, mp_float_t *peak, mp_float_t *rms) { + if (self->levels_reset) { + // No audio passed since the last call. + *peak = 0; + *rms = 0; + return; + } + uint32_t count = self->level_count; + float sum_squares = self->sum_squares; + *peak = (mp_float_t)self->peak / MICROPY_FLOAT_CONST(32768.0); + self->levels_reset = true; + *rms = count > 0 ? MICROPY_FLOAT_C_FUN(sqrt)((mp_float_t)sum_squares / count) : 0; +} + +static void fill_silence(audiotap_tap_obj_t *self, uint8_t *out, uint32_t samples) { + if (self->base.samples_signed) { + memset(out, 0, samples * (self->base.bits_per_sample / 8)); + } else if (self->base.bits_per_sample == 16) { + uint16_t *words = (uint16_t *)out; + while (samples--) { + *words++ = 0x8000; + } + } else { + memset(out, 0x80, samples); + } +} + +audioio_get_buffer_result_t audiotap_tap_get_buffer(audiotap_tap_obj_t *self, bool single_channel_output, uint8_t channel, + uint8_t **buffer, uint32_t *buffer_length) { + (void)single_channel_output; + (void)channel; + + if (self->buffer[0] == NULL) { + // Deinitialized while something still plays it. + *buffer = NULL; + *buffer_length = 0; + return GET_BUFFER_DONE; + } + + self->last_buf_idx = !self->last_buf_idx; + uint8_t *out = (uint8_t *)self->buffer[self->last_buf_idx]; + const uint32_t sample_bytes = self->base.bits_per_sample / 8; + uint32_t length = self->buffer_len / sample_bytes; + + while (length != 0) { + if (self->sample_buffer_length == 0) { + if (!self->more_data) { + if (self->loop && self->sample) { + audiosample_reset_buffer(self->sample, false, 0); + } else { + self->sample = NULL; + } + } + if (self->sample) { + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, + (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); + if (result == GET_BUFFER_ERROR) { + self->sample = NULL; + self->sample_buffer_length = 0; + self->more_data = false; + } else { + self->sample_buffer_length /= sample_bytes; + self->more_data = result == GET_BUFFER_MORE_DATA; + if (self->sample_buffer_length == 0 && self->more_data) { + // The source has nothing yet. Play silence rather + // than ask again in a loop. + fill_silence(self, out, length); + break; + } + } + } + } + + if (self->sample == NULL) { + fill_silence(self, out, length); + length = 0; + } else { + uint32_t n = MIN(self->sample_buffer_length, length); + memcpy(out, self->sample_remaining_buffer, n * sample_bytes); + tap_capture(self, out, n); + out += n * sample_bytes; + length -= n; + self->sample_remaining_buffer += n * sample_bytes; + self->sample_buffer_length -= n; + } + } + + *buffer = (uint8_t *)self->buffer[self->last_buf_idx]; + *buffer_length = self->buffer_len; + // Like the effects, a tap plays silence after its sample ends. + return GET_BUFFER_MORE_DATA; +} diff --git a/shared-module/audiotap/Tap.h b/shared-module/audiotap/Tap.h new file mode 100644 index 00000000000..56661e610fc --- /dev/null +++ b/shared-module/audiotap/Tap.h @@ -0,0 +1,57 @@ +// 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/audiocore/__init__.h" + +extern const mp_obj_type_t audiotap_tap_type; + +typedef struct { + audiosample_base_t base; + + // Double buffer handed to whatever plays the tap, as in the effects. + int8_t *buffer[2]; + uint8_t last_buf_idx; + uint32_t buffer_len; // in bytes + + uint8_t *sample_remaining_buffer; + uint32_t sample_buffer_length; // in samples + + bool loop; + bool more_data; + + mp_obj_t sample; + + // A copy of the audio that passed through, for readinto(). Whatever plays + // the tap writes it, from a background task or an interrupt, and the VM + // reads it, so each side moves only its own position. Both positions run + // from 0 to 2 * capacity, which tells a full ring from an empty one. + uint8_t *ring; + uint32_t capacity; // in samples, a whole number of frames + volatile uint32_t write_pos; + volatile uint32_t read_pos; + // Audio was dropped because the ring was full. Cleared when read. + volatile bool overflow; + + // Levels of the audio since levels() was last called. levels() asks for + // the reset, and the writer does it, so that only the writer changes them. + volatile uint32_t peak; // largest magnitude, 0 to 32768 + volatile float sum_squares; // of samples scaled to -1.0 to 1.0 + volatile uint32_t level_count; // samples summed + volatile bool levels_reset; +} audiotap_tap_obj_t; + +void audiotap_tap_reset_buffer(audiotap_tap_obj_t *self, + bool single_channel_output, + uint8_t channel); + +audioio_get_buffer_result_t audiotap_tap_get_buffer(audiotap_tap_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length); // length in bytes diff --git a/shared-module/audiotap/__init__.c b/shared-module/audiotap/__init__.c new file mode 100644 index 00000000000..4ab4bccdc69 --- /dev/null +++ b/shared-module/audiotap/__init__.c @@ -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 + +// Nothing to do here. The module's only state is in its Tap objects. diff --git a/tests/circuitpython/audiotap_tap.py b/tests/circuitpython/audiotap_tap.py new file mode 100644 index 00000000000..aec3770448d --- /dev/null +++ b/tests/circuitpython/audiotap_tap.py @@ -0,0 +1,85 @@ +import array +from audiocore import get_buffer, RawSample +from audiofilters import Filter +from audiotap import Tap + +ramp = array.array("h", range(-300, 300, 3)) # 200 samples +source = RawSample(ramp, sample_rate=8000) + + +def pull(sample, times=1): + out = [] + for _ in range(times): + result, buffer = get_buffer(sample) + out.extend(buffer) + return out + + +# Audio passes through unchanged and a copy can be read. +tap = Tap(capacity=1000, buffer_size=128, sample_rate=8000) +print("idle", tap.playing, tap.available, tap.levels()) +print("idle silence", set(pull(tap)), tap.available) +tap.play(source, loop=True) +print("playing", tap.playing) +played = pull(tap, 4) +print("played", len(played), played[:3], played == list(ramp) + list(ramp)[:56]) +print("available", tap.available) +copy = array.array("h", [0] * 300) +print("read", tap.readinto(copy), list(copy[:256]) == played, tap.available) +print("read empty", tap.readinto(copy)) + +# Levels of the audio since the last call. +peak, rms = tap.levels() +print("levels", round(peak, 4), round(rms, 4)) +print("levels again", tap.levels()) + +# Overflow keeps the oldest audio and drops the newer. +small = Tap(capacity=100, buffer_size=128, sample_rate=8000) +small.play(source, loop=True) +played = pull(small, 2) +print("full", small.available, small.overflow, small.overflow) +copy = array.array("h", [0] * 100) +print("kept oldest", small.readinto(copy), list(copy) == played[:100]) +pull(small) +small.clear() +print("cleared", small.available) + +# A bytearray holds half as many 16 bit samples, and memoryview slices work. +pull(small) +raw = bytearray(10) +print("bytearray", small.readinto(raw), small.available) +part = array.array("h", [7] * 8) +print("slice", small.readinto(memoryview(part)[2:5]), list(part)) + +# Levels only. +meter = Tap(capacity=0, buffer_size=64, sample_rate=8000) +meter.play(RawSample(array.array("h", [16384, -32768] * 16), sample_rate=8000), loop=True) +pull(meter) +print("meter", meter.capacity, meter.readinto(copy), meter.levels()) + +# Stereo reads whole frames. +stereo = Tap(capacity=64, buffer_size=32, sample_rate=8000, channel_count=2) +stereo.play(RawSample(array.array("h", range(16)), sample_rate=8000, channel_count=2), loop=True) +pull(stereo) +odd = array.array("h", [0] * 5) +print("stereo", stereo.readinto(odd), list(odd)) + +# 8 bit unsigned levels: 0x80 is silence. +eight = Tap(capacity=16, buffer_size=16, sample_rate=8000, bits_per_sample=8, samples_signed=False) +eight.play(RawSample(array.array("B", [0x80, 0xC0] * 8), sample_rate=8000), loop=True) +pull(eight) +print("8 bit", eight.levels()) + +# In a chain, and the end of a sample that does not loop. +once = Tap(capacity=1000, buffer_size=128, sample_rate=8000) +chain = Filter(buffer_size=128, sample_rate=8000) +chain.play(once) +once.play(source) +pull(chain, 6) +print("ended", once.playing, once.available) + +once.deinit() +try: + once.readinto(copy) +except ValueError as error: + print("deinit", error) diff --git a/tests/circuitpython/audiotap_tap.py.exp b/tests/circuitpython/audiotap_tap.py.exp new file mode 100644 index 00000000000..e5ea328ed28 --- /dev/null +++ b/tests/circuitpython/audiotap_tap.py.exp @@ -0,0 +1,19 @@ +idle False 0 (0.0, 0.0) +idle silence {0} 0 +playing True +played 256 [-300, -297, -294] True +available 256 +read 256 True 0 +read empty 0 +levels 0.0092 0.0057 +levels again (0.0, 0.0) +full 100 True False +kept oldest 100 True +cleared 0 +bytearray 5 59 +slice 3 [7, 7, 291, 294, 297, 7, 7, 7] +meter 0 0 (1.0, 0.7905694150420949) +stereo 4 [0, 1, 2, 3, 0] +8 bit (0.5, 0.3535533905932738) +ended False 200 +deinit Object has been deinitialized and can no longer be used. Create a new object. From f889146e812cd91188b5263e2027522b12d287d9 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 21 Oct 2026 13:13:00 -0500 Subject: [PATCH 2/2] split rms and peak, take format from upstream audio object, change capacity to time based --- shared-bindings/audiotap/Tap.c | 128 +++++++---------- shared-bindings/audiotap/Tap.h | 8 +- shared-module/audiotap/Tap.c | 245 ++++++++++++++++++--------------- shared-module/audiotap/Tap.h | 18 ++- 4 files changed, 198 insertions(+), 201 deletions(-) diff --git a/shared-bindings/audiotap/Tap.c b/shared-bindings/audiotap/Tap.c index e8e151edc62..beb797638a1 100644 --- a/shared-bindings/audiotap/Tap.c +++ b/shared-bindings/audiotap/Tap.c @@ -18,44 +18,35 @@ //| class Tap: //| """Passes audio through unchanged and keeps a copy that the program can read""" //| -//| def __init__( -//| self, -//| capacity: int = 4096, -//| buffer_size: int = 512, -//| sample_rate: int = 8000, -//| bits_per_sample: int = 16, -//| samples_signed: bool = True, -//| channel_count: int = 1, -//| ) -> None: +//| def __init__(self, capacity_ms: int = 100) -> None: //| """Create a Tap. It plays a sample like the effects in `audiofilters` //| and `audiodelays` do, so it can go anywhere in an audio chain. Whatever //| plays the tap gets the sample's audio unchanged. //| -//| As the audio passes through, the tap keeps it in a buffer of -//| ``capacity`` samples, which `readinto` copies out of without waiting, -//| and measures its levels for `levels`. The program can look at the -//| audio this way, at the point in the chain where the tap is, while it -//| plays. +//| As the audio passes through, the tap keeps up to ``capacity_ms`` of +//| it in a buffer, which `readinto` copies out of without waiting, and +//| measures its `peak` and `rms` levels. The program can look at +//| the audio this way, at the point in the chain where the tap is, +//| while it plays. +//| +//| The tap takes its sample rate, channel count, bits per sample and +//| signedness from the first sample it plays, and allocates the buffer +//| then. So `play` a sample with the tap before anything plays the tap. //| //| The tap only sees audio that is played through it: something, such //| as an `audiomixer.Mixer` voice playing to an audio output, must play //| it or a chain that it is part of. To look at audio without hearing -//| it, play it at a ``level`` of 0. Until its sample is played, and -//| after it ends, the tap plays silence and keeps nothing. +//| it, play it at a ``level`` of 0. When its sample is stopped or ends, +//| the tap plays silence and keeps nothing. //| //| When the program reads too slowly, the buffer fills up, the tap keeps //| the audio it has and drops the newer audio until there is room //| again, and `overflow` turns True. The audio that passes through is //| never affected. //| -//| :param int capacity: The number of samples kept for `readinto`, rounded -//| down to whole frames. A stereo frame is two samples. 0 keeps none, -//| for a tap that only measures `levels`. -//| :param int buffer_size: The total size in bytes of each of the two playback buffers to use -//| :param int sample_rate: The sample rate to be used -//| :param int bits_per_sample: The bits per sample of the audio: 8 or 16 -//| :param bool samples_signed: Effect is signed (True) or unsigned (False) -//| :param int channel_count: The number of channels the source samples contain. 1 = mono; 2 = stereo. +//| :param int capacity_ms: How many milliseconds of audio are kept for +//| `readinto`. 0 keeps none, for a tap that only measures `peak` and +//| `rms`. //| //| Showing the level of a USB microphone while it is heard through the //| DVI output, and keeping the audio for a spectrum display. The @@ -102,11 +93,7 @@ //| max_packet_size=mic_format.max_packet_size, //| ) //| # Keep a quarter second, more than arrives between reads of the loop below. -//| tap = audiotap.Tap( -//| capacity=sample_rate * mic.channel_count // 4, -//| sample_rate=sample_rate, -//| channel_count=mic.channel_count, -//| ) +//| tap = audiotap.Tap(capacity_ms=250) //| mixer = audiomixer.Mixer(voice_count=1, sample_rate=48000, channel_count=mic.channel_count) //| supervisor.runtime.audio.play(mixer) //| mixer.voice[0].play(audiospeed.Resampler(tap.play(mic))) @@ -129,7 +116,8 @@ //| # Read the audio that is waiting. A spectrum display would use these samples. //| while tap.available >= len(samples): //| tap.readinto(samples) -//| peak, rms = tap.levels() +//| peak = tap.peak +//| rms = tap.rms //| peak_bar.width = max(1, int(peak * display.width)) //| rms_bar.width = max(1, int(rms * display.width)) //| print(f"peak {peak:.3f} rms {rms:.3f}") @@ -137,31 +125,18 @@ //| ... //| static mp_obj_t audiotap_tap_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_capacity, ARG_buffer_size, ARG_sample_rate, ARG_bits_per_sample, ARG_samples_signed, ARG_channel_count, }; + enum { ARG_capacity_ms, }; static const mp_arg_t allowed_args[] = { - { MP_QSTR_capacity, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 4096} }, - { MP_QSTR_buffer_size, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 512} }, - { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} }, - { MP_QSTR_bits_per_sample, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} }, - { MP_QSTR_samples_signed, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = true} }, - { MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1 } }, + { MP_QSTR_capacity_ms, MP_ARG_INT, {.u_int = 100} }, }; 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); - mp_int_t capacity = mp_arg_validate_int_range(args[ARG_capacity].u_int, 0, 1 << 24, MP_QSTR_capacity); - mp_int_t buffer_size = mp_arg_validate_int_range(args[ARG_buffer_size].u_int, 4, 1 << 16, MP_QSTR_buffer_size); - mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count); - mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate); - mp_int_t bits_per_sample = args[ARG_bits_per_sample].u_int; - if (bits_per_sample != 8 && bits_per_sample != 16) { - mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 8 or 16")); - } + mp_int_t capacity_ms = mp_arg_validate_int_range(args[ARG_capacity_ms].u_int, 0, 60000, MP_QSTR_capacity_ms); audiotap_tap_obj_t *self = mp_obj_malloc_with_finaliser(audiotap_tap_obj_t, &audiotap_tap_type); - common_hal_audiotap_tap_construct(self, capacity, buffer_size, bits_per_sample, - args[ARG_samples_signed].u_bool, channel_count, sample_rate); + common_hal_audiotap_tap_construct(self, capacity_ms); return MP_OBJ_FROM_PTR(self); } @@ -207,19 +182,6 @@ MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_playing_obj, audiotap_tap_obj_get_pla MP_PROPERTY_GETTER(audiotap_tap_playing_obj, (mp_obj_t)&audiotap_tap_get_playing_obj); -//| capacity: int -//| """The number of samples that the tap keeps for `readinto`. (read-only)""" -//| -static mp_obj_t audiotap_tap_obj_get_capacity(mp_obj_t self_in) { - audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); - check_for_deinit(self); - return mp_obj_new_int_from_uint(common_hal_audiotap_tap_get_capacity(self)); -} -MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_capacity_obj, audiotap_tap_obj_get_capacity); - -MP_PROPERTY_GETTER(audiotap_tap_capacity_obj, - (mp_obj_t)&audiotap_tap_get_capacity_obj); - //| available: int //| """The number of samples waiting to be read with `readinto`. (read-only)""" //| @@ -284,29 +246,43 @@ static mp_obj_t audiotap_tap_obj_clear(mp_obj_t self_in) { } MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_clear_obj, audiotap_tap_obj_clear); -//| def levels(self) -> Tuple[float, float]: -//| """Returns the peak and RMS level of the audio that passed through -//| since the last call, from 0.0 to 1.0 of full scale, and starts a new -//| measurement. Both are 0.0 when no audio passed through. The channels -//| of stereo audio are measured together. This works with a ``capacity`` -//| of 0 too.""" -//| ... +//| peak: float +//| """The largest magnitude of the audio that passed through since this was +//| last read, from 0.0 to 1.0 of full scale. Reading this starts a new +//| measurement. 0.0 when no audio passed through. The channels of stereo +//| audio are measured together. (read-only)""" +//| +static mp_obj_t audiotap_tap_obj_get_peak(mp_obj_t self_in) { + audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_float(common_hal_audiotap_tap_get_peak(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_peak_obj, audiotap_tap_obj_get_peak); + +MP_PROPERTY_GETTER(audiotap_tap_peak_obj, + (mp_obj_t)&audiotap_tap_get_peak_obj); + +//| rms: float +//| """The RMS level of the audio that passed through since this was last +//| read, from 0.0 to 1.0 of full scale. Reading this starts a new +//| measurement, separate from `peak`'s. 0.0 when no audio passed through. +//| The channels of stereo audio are measured together. (read-only)""" //| -static mp_obj_t audiotap_tap_obj_levels(mp_obj_t self_in) { +static mp_obj_t audiotap_tap_obj_get_rms(mp_obj_t self_in) { audiotap_tap_obj_t *self = MP_OBJ_TO_PTR(self_in); check_for_deinit(self); - mp_float_t peak, rms; - common_hal_audiotap_tap_get_levels(self, &peak, &rms); - mp_obj_t levels[2] = { mp_obj_new_float(peak), mp_obj_new_float(rms) }; - return mp_obj_new_tuple(2, levels); + return mp_obj_new_float(common_hal_audiotap_tap_get_rms(self)); } -MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_levels_obj, audiotap_tap_obj_levels); +MP_DEFINE_CONST_FUN_OBJ_1(audiotap_tap_get_rms_obj, audiotap_tap_obj_get_rms); + +MP_PROPERTY_GETTER(audiotap_tap_rms_obj, + (mp_obj_t)&audiotap_tap_get_rms_obj); //| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> Tap: //| """Plays the sample once when loop=False and continuously when loop=True. //| Does not block. Use `playing` to block. //| -//| The sample must match the encoding settings given in the constructor. +//| The first sample sets the tap's format. Later samples must match it. //| //| :return: The tap itself. Can be used for chaining, ie: //| ``audio.play(tap.play(sample))``. @@ -353,13 +329,13 @@ static const mp_rom_map_elem_t audiotap_tap_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiotap_tap_stop_obj) }, { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&audiotap_tap_readinto_obj) }, { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&audiotap_tap_clear_obj) }, - { MP_ROM_QSTR(MP_QSTR_levels), MP_ROM_PTR(&audiotap_tap_levels_obj) }, // Properties { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiotap_tap_playing_obj) }, - { MP_ROM_QSTR(MP_QSTR_capacity), MP_ROM_PTR(&audiotap_tap_capacity_obj) }, { MP_ROM_QSTR(MP_QSTR_available), MP_ROM_PTR(&audiotap_tap_available_obj) }, { MP_ROM_QSTR(MP_QSTR_overflow), MP_ROM_PTR(&audiotap_tap_overflow_obj) }, + { MP_ROM_QSTR(MP_QSTR_peak), MP_ROM_PTR(&audiotap_tap_peak_obj) }, + { MP_ROM_QSTR(MP_QSTR_rms), MP_ROM_PTR(&audiotap_tap_rms_obj) }, AUDIOSAMPLE_FIELDS, }; static MP_DEFINE_CONST_DICT(audiotap_tap_locals_dict, audiotap_tap_locals_dict_table); diff --git a/shared-bindings/audiotap/Tap.h b/shared-bindings/audiotap/Tap.h index 87796e779f3..892d4fd2fdd 100644 --- a/shared-bindings/audiotap/Tap.h +++ b/shared-bindings/audiotap/Tap.h @@ -10,9 +10,7 @@ extern const mp_obj_type_t audiotap_tap_type; -void common_hal_audiotap_tap_construct(audiotap_tap_obj_t *self, uint32_t capacity, - uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, - uint8_t channel_count, uint32_t sample_rate); +void common_hal_audiotap_tap_construct(audiotap_tap_obj_t *self, uint32_t capacity_ms); void common_hal_audiotap_tap_deinit(audiotap_tap_obj_t *self); @@ -20,10 +18,10 @@ bool common_hal_audiotap_tap_get_playing(audiotap_tap_obj_t *self); void common_hal_audiotap_tap_play(audiotap_tap_obj_t *self, mp_obj_t sample, bool loop); void common_hal_audiotap_tap_stop(audiotap_tap_obj_t *self); -uint32_t common_hal_audiotap_tap_get_capacity(audiotap_tap_obj_t *self); uint32_t common_hal_audiotap_tap_get_available(audiotap_tap_obj_t *self); // Copies at most length samples, in whole frames. Returns the number copied. uint32_t common_hal_audiotap_tap_readinto(audiotap_tap_obj_t *self, uint8_t *buffer, uint32_t length); void common_hal_audiotap_tap_clear(audiotap_tap_obj_t *self); bool common_hal_audiotap_tap_get_overflow(audiotap_tap_obj_t *self); -void common_hal_audiotap_tap_get_levels(audiotap_tap_obj_t *self, mp_float_t *peak, mp_float_t *rms); +mp_float_t common_hal_audiotap_tap_get_peak(audiotap_tap_obj_t *self); +mp_float_t common_hal_audiotap_tap_get_rms(audiotap_tap_obj_t *self); diff --git a/shared-module/audiotap/Tap.c b/shared-module/audiotap/Tap.c index 6f710629175..ff1f34c14fe 100644 --- a/shared-module/audiotap/Tap.c +++ b/shared-module/audiotap/Tap.c @@ -12,27 +12,19 @@ #include "py/runtime.h" -void common_hal_audiotap_tap_construct(audiotap_tap_obj_t *self, uint32_t capacity, - uint32_t buffer_size, uint8_t bits_per_sample, bool samples_signed, - uint8_t channel_count, uint32_t sample_rate) { - - self->base.bits_per_sample = bits_per_sample; - self->base.samples_signed = samples_signed; - self->base.channel_count = channel_count; - self->base.sample_rate = sample_rate; +void common_hal_audiotap_tap_construct(audiotap_tap_obj_t *self, uint32_t capacity_ms) { + // The format comes from the first sample played. Until then the tap is + // 16 bit mono at 0 Hz, which nothing plays: a channel count of 0 would + // mark it deinitialized. + self->base.bits_per_sample = 16; + self->base.samples_signed = true; + self->base.channel_count = 1; + self->base.sample_rate = 0; self->base.single_buffer = false; + self->base.max_buffer_length = 0; - // Whole frames only, so that a stereo pair is never split between buffers - // or between the ring's end and its start. - const uint32_t frame_bytes = (bits_per_sample / 8) * channel_count; - self->buffer_len = MAX(frame_bytes, buffer_size - buffer_size % frame_bytes); - self->base.max_buffer_length = self->buffer_len; - - self->buffer[0] = m_malloc_without_collect(self->buffer_len); - memset(self->buffer[0], 0, self->buffer_len); - self->buffer[1] = m_malloc_without_collect(self->buffer_len); - memset(self->buffer[1], 0, self->buffer_len); - self->last_buf_idx = 1; + self->silence = NULL; + self->silence_len = 0; self->sample = NULL; self->sample_remaining_buffer = NULL; @@ -40,26 +32,24 @@ void common_hal_audiotap_tap_construct(audiotap_tap_obj_t *self, uint32_t capaci self->loop = false; self->more_data = false; - self->capacity = capacity - capacity % channel_count; + self->capacity_ms = capacity_ms; self->ring = NULL; - if (self->capacity > 0) { - self->ring = m_malloc_without_collect(self->capacity * (bits_per_sample / 8)); - } + self->capacity = 0; self->write_pos = 0; self->read_pos = 0; self->overflow = false; self->peak = 0; + self->peak_reset = false; self->sum_squares = 0; self->level_count = 0; - self->levels_reset = false; + self->rms_reset = false; } void common_hal_audiotap_tap_deinit(audiotap_tap_obj_t *self) { audiosample_mark_deinit(&self->base); self->sample = NULL; - self->buffer[0] = NULL; - self->buffer[1] = NULL; + self->silence = NULL; self->ring = NULL; self->capacity = 0; } @@ -67,42 +57,84 @@ void common_hal_audiotap_tap_deinit(audiotap_tap_obj_t *self) { void audiotap_tap_reset_buffer(audiotap_tap_obj_t *self, bool single_channel_output, uint8_t channel) { + (void)self; (void)single_channel_output; (void)channel; - if (self->buffer[0] == NULL) { - return; - } - // The ring belongs to the reader; what it holds stays readable. - memset(self->buffer[0], 0, self->buffer_len); - memset(self->buffer[1], 0, self->buffer_len); + // Nothing to reset: the tap passes on the source's buffers, and the ring + // belongs to the reader, so what it holds stays readable. } bool common_hal_audiotap_tap_get_playing(audiotap_tap_obj_t *self) { return self->sample != NULL; } +// Takes the format of the first sample played and allocates for it. +static void take_format(audiotap_tap_obj_t *self, mp_obj_t sample) { + const audiosample_base_t *source = audiosample_check(sample); + audiosample_check_for_deinit(source); + if (source->bits_per_sample != 8 && source->bits_per_sample != 16) { + mp_raise_ValueError(MP_ERROR_TEXT("bits_per_sample must be 8 or 16")); + } + const uint32_t sample_bytes = source->bits_per_sample / 8; + const uint32_t frame_bytes = sample_bytes * source->channel_count; + + // Silence for when the source has no audio, no longer than the source's + // buffers and in whole frames. + uint32_t silence_len = MIN(MAX(source->max_buffer_length, frame_bytes), 512); + silence_len -= silence_len % frame_bytes; + int8_t *silence = m_malloc_without_collect(silence_len); + if (source->samples_signed) { + memset(silence, 0, silence_len); + } else if (sample_bytes == 2) { + uint16_t *words = (uint16_t *)silence; + for (uint32_t i = 0; i < silence_len / 2; i++) { + words[i] = 0x8000; + } + } else { + memset(silence, 0x80, silence_len); + } + + uint32_t capacity = (uint32_t)((uint64_t)self->capacity_ms * source->sample_rate / 1000) * source->channel_count; + uint8_t *ring = NULL; + if (capacity > 0) { + ring = m_malloc_without_collect(capacity * sample_bytes); + } + + // Nothing plays the tap before it has a format, so the reader is the + // only one using these yet. + self->silence = silence; + self->silence_len = silence_len; + self->ring = ring; + self->capacity = capacity; + self->write_pos = 0; + self->read_pos = 0; + + self->base.bits_per_sample = source->bits_per_sample; + self->base.samples_signed = source->samples_signed; + self->base.channel_count = source->channel_count; + self->base.sample_rate = source->sample_rate; + self->base.max_buffer_length = MAX(source->max_buffer_length, silence_len); +} + void common_hal_audiotap_tap_play(audiotap_tap_obj_t *self, mp_obj_t sample, bool loop) { - audiosample_must_match(&self->base, sample, false); + if (self->silence == NULL) { + take_format(self, sample); + } else { + audiosample_must_match(&self->base, sample, false); + } - self->sample = sample; + audiosample_reset_buffer(sample, false, 0); self->loop = loop; - - audiosample_reset_buffer(self->sample, false, 0); - audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, - (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); - // Track the remaining length in samples. - self->sample_buffer_length /= (self->base.bits_per_sample / 8); - self->more_data = result == GET_BUFFER_MORE_DATA; + self->sample_buffer_length = 0; + // Ask the source for its first buffer. + self->more_data = true; + self->sample = sample; } void common_hal_audiotap_tap_stop(audiotap_tap_obj_t *self) { self->sample = NULL; } -uint32_t common_hal_audiotap_tap_get_capacity(audiotap_tap_obj_t *self) { - return self->capacity; -} - // Positions run from 0 to 2 * capacity. static uint32_t ring_used(uint32_t capacity, uint32_t write, uint32_t read) { return write >= read ? write - read : write + 2 * capacity - read; @@ -135,11 +167,14 @@ static void tap_capture(audiotap_tap_obj_t *self, const uint8_t *data, uint32_t return; } - if (self->levels_reset) { + if (self->peak_reset) { self->peak = 0; + self->peak_reset = false; + } + if (self->rms_reset) { self->sum_squares = 0; self->level_count = 0; - self->levels_reset = false; + self->rms_reset = false; } uint32_t peak = self->peak; uint64_t sum = 0; @@ -223,31 +258,25 @@ bool common_hal_audiotap_tap_get_overflow(audiotap_tap_obj_t *self) { return overflow; } -void common_hal_audiotap_tap_get_levels(audiotap_tap_obj_t *self, mp_float_t *peak, mp_float_t *rms) { - if (self->levels_reset) { - // No audio passed since the last call. - *peak = 0; - *rms = 0; - return; +mp_float_t common_hal_audiotap_tap_get_peak(audiotap_tap_obj_t *self) { + if (self->peak_reset) { + // No audio passed since the last read. + return 0; } - uint32_t count = self->level_count; - float sum_squares = self->sum_squares; - *peak = (mp_float_t)self->peak / MICROPY_FLOAT_CONST(32768.0); - self->levels_reset = true; - *rms = count > 0 ? MICROPY_FLOAT_C_FUN(sqrt)((mp_float_t)sum_squares / count) : 0; + mp_float_t peak = (mp_float_t)self->peak / MICROPY_FLOAT_CONST(32768.0); + self->peak_reset = true; + return peak; } -static void fill_silence(audiotap_tap_obj_t *self, uint8_t *out, uint32_t samples) { - if (self->base.samples_signed) { - memset(out, 0, samples * (self->base.bits_per_sample / 8)); - } else if (self->base.bits_per_sample == 16) { - uint16_t *words = (uint16_t *)out; - while (samples--) { - *words++ = 0x8000; - } - } else { - memset(out, 0x80, samples); +mp_float_t common_hal_audiotap_tap_get_rms(audiotap_tap_obj_t *self) { + if (self->rms_reset) { + // No audio passed since the last read. + return 0; } + uint32_t count = self->level_count; + float sum_squares = self->sum_squares; + self->rms_reset = true; + return count > 0 ? MICROPY_FLOAT_C_FUN(sqrt)((mp_float_t)sum_squares / count) : 0; } audioio_get_buffer_result_t audiotap_tap_get_buffer(audiotap_tap_obj_t *self, bool single_channel_output, uint8_t channel, @@ -255,63 +284,53 @@ audioio_get_buffer_result_t audiotap_tap_get_buffer(audiotap_tap_obj_t *self, bo (void)single_channel_output; (void)channel; - if (self->buffer[0] == NULL) { + if (self->silence == NULL) { // Deinitialized while something still plays it. *buffer = NULL; *buffer_length = 0; return GET_BUFFER_DONE; } - self->last_buf_idx = !self->last_buf_idx; - uint8_t *out = (uint8_t *)self->buffer[self->last_buf_idx]; const uint32_t sample_bytes = self->base.bits_per_sample / 8; - uint32_t length = self->buffer_len / sample_bytes; - - while (length != 0) { - if (self->sample_buffer_length == 0) { - if (!self->more_data) { - if (self->loop && self->sample) { - audiosample_reset_buffer(self->sample, false, 0); - } else { - self->sample = NULL; - } + if (self->sample_buffer_length == 0 && self->sample != NULL) { + if (!self->more_data) { + if (self->loop) { + audiosample_reset_buffer(self->sample, false, 0); + } else { + self->sample = NULL; } - if (self->sample) { - audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, - (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); - if (result == GET_BUFFER_ERROR) { - self->sample = NULL; - self->sample_buffer_length = 0; - self->more_data = false; - } else { - self->sample_buffer_length /= sample_bytes; - self->more_data = result == GET_BUFFER_MORE_DATA; - if (self->sample_buffer_length == 0 && self->more_data) { - // The source has nothing yet. Play silence rather - // than ask again in a loop. - fill_silence(self, out, length); - break; - } - } + } + if (self->sample != NULL) { + audioio_get_buffer_result_t result = audiosample_get_buffer(self->sample, false, 0, + (uint8_t **)&self->sample_remaining_buffer, &self->sample_buffer_length); + if (result == GET_BUFFER_ERROR) { + self->sample = NULL; + self->sample_buffer_length = 0; + self->more_data = false; + } else { + self->sample_buffer_length /= sample_bytes; + self->more_data = result == GET_BUFFER_MORE_DATA; } } + } - if (self->sample == NULL) { - fill_silence(self, out, length); - length = 0; - } else { - uint32_t n = MIN(self->sample_buffer_length, length); - memcpy(out, self->sample_remaining_buffer, n * sample_bytes); - tap_capture(self, out, n); - out += n * sample_bytes; - length -= n; - self->sample_remaining_buffer += n * sample_bytes; - self->sample_buffer_length -= n; - } + if (self->sample == NULL || self->sample_buffer_length == 0) { + // Like the effects, a tap plays silence when it has no audio, and + // rather than ask a source that has nothing yet again in a loop. + *buffer = (uint8_t *)self->silence; + *buffer_length = self->silence_len; + return GET_BUFFER_MORE_DATA; } - *buffer = (uint8_t *)self->buffer[self->last_buf_idx]; - *buffer_length = self->buffer_len; - // Like the effects, a tap plays silence after its sample ends. + // Pass on the source's own buffer, in whole frames no longer than what + // the player expects, which a later sample's buffers may be. + const uint32_t channel_count = self->base.channel_count; + const uint32_t max_samples = self->base.max_buffer_length / sample_bytes / channel_count * channel_count; + uint32_t n = MIN(self->sample_buffer_length, max_samples); + tap_capture(self, self->sample_remaining_buffer, n); + *buffer = self->sample_remaining_buffer; + *buffer_length = n * sample_bytes; + self->sample_remaining_buffer += n * sample_bytes; + self->sample_buffer_length -= n; return GET_BUFFER_MORE_DATA; } diff --git a/shared-module/audiotap/Tap.h b/shared-module/audiotap/Tap.h index 56661e610fc..6bc667ad294 100644 --- a/shared-module/audiotap/Tap.h +++ b/shared-module/audiotap/Tap.h @@ -14,11 +14,13 @@ extern const mp_obj_type_t audiotap_tap_type; typedef struct { audiosample_base_t base; - // Double buffer handed to whatever plays the tap, as in the effects. - int8_t *buffer[2]; - uint8_t last_buf_idx; - uint32_t buffer_len; // in bytes + // Played while there is no audio to pass through. Allocated, like the + // ring, when the first sample is played, which also sets the format. + int8_t *silence; + uint32_t silence_len; // in bytes + // The part of the source's buffer not passed through yet. It stays the + // source's until the source is asked for its next buffer. uint8_t *sample_remaining_buffer; uint32_t sample_buffer_length; // in samples @@ -31,6 +33,7 @@ typedef struct { // the tap writes it, from a background task or an interrupt, and the VM // reads it, so each side moves only its own position. Both positions run // from 0 to 2 * capacity, which tells a full ring from an empty one. + uint32_t capacity_ms; uint8_t *ring; uint32_t capacity; // in samples, a whole number of frames volatile uint32_t write_pos; @@ -38,12 +41,13 @@ typedef struct { // Audio was dropped because the ring was full. Cleared when read. volatile bool overflow; - // Levels of the audio since levels() was last called. levels() asks for - // the reset, and the writer does it, so that only the writer changes them. + // Levels of the audio since each was last read. Reading one asks for its + // reset, and the writer does it, so that only the writer changes them. volatile uint32_t peak; // largest magnitude, 0 to 32768 + volatile bool peak_reset; volatile float sum_squares; // of samples scaled to -1.0 to 1.0 volatile uint32_t level_count; // samples summed - volatile bool levels_reset; + volatile bool rms_reset; } audiotap_tap_obj_t; void audiotap_tap_reset_buffer(audiotap_tap_obj_t *self,