diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index fa088fdda5e1f..4cfc7ff1f443f 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -2271,7 +2271,12 @@ msgstr "" msgid "UID:" msgstr "" +#: shared-bindings/usb_audio/USBSpeaker.c +msgid "USB audio not enabled for output in boot.py" +msgstr "" + #: shared-bindings/usb_audio/USBMicrophone.c +#: shared-bindings/usb_audio/USBSpeaker.c msgid "USB audio not enabled in boot.py" msgstr "" @@ -3117,6 +3122,10 @@ msgstr "" msgid "destination buffer must be an array of type 'H' for bit_depth = 16" msgstr "" +#: shared-bindings/usb_audio/USBSpeaker.c +msgid "destination must be an array of type 'h'" +msgstr "" + #: py/objdict.c msgid "dict update sequence has wrong length" msgstr "" diff --git a/ports/nordic/mpconfigport.mk b/ports/nordic/mpconfigport.mk index 48536ec2acd7b..c243d54a7072a 100644 --- a/ports/nordic/mpconfigport.mk +++ b/ports/nordic/mpconfigport.mk @@ -9,6 +9,15 @@ CIRCUITPY_BUILD_EXTENSIONS ?= uf2 # Number of USB endpoint pairs. USB_NUM_ENDPOINT_PAIRS = 8 +# The nRF52 USBD implements isochronous transfers only on the fixed, dedicated +# endpoint number 8 (EP_ISO_NUM in TinyUSB's dcd_nrf5x.c); dcd_edpt_open() rejects +# an ISO endpoint on any other number, so a usb_audio mic/speaker placed on a +# sequentially-allocated endpoint would enumerate but never transfer data. Force +# the usb_audio isochronous endpoint onto endpoint 8. (Only read by usb_audio +# code, so it is harmless when CIRCUITPY_USB_AUDIO is off; defined unconditionally +# to stay independent of where that flag gets set in the include order.) +CFLAGS += -DUSB_AUDIO_ISO_EP_NUM=8 + # All nRF ports have longints. LONGINT_IMPL = MPZ @@ -48,6 +57,7 @@ CIRCUITPY_SERIAL_BLE ?= 1 CIRCUITPY_COMPUTED_GOTO_SAVE_SPACE ?= 1 +CIRCUITPY_USB_AUDIO ?= 1 # nRF52840-specific diff --git a/shared-bindings/usb_audio/Direction.c b/shared-bindings/usb_audio/Direction.c new file mode 100644 index 0000000000000..1dbe17c11aa4b --- /dev/null +++ b/shared-bindings/usb_audio/Direction.c @@ -0,0 +1,51 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/enum.h" +#include "py/runtime.h" + +#include "shared-bindings/usb_audio/Direction.h" + +MAKE_ENUM_VALUE(usb_audio_direction_type, direction, INPUT, USB_AUDIO_DIRECTION_INPUT); +MAKE_ENUM_VALUE(usb_audio_direction_type, direction, OUTPUT, USB_AUDIO_DIRECTION_OUTPUT); +MAKE_ENUM_VALUE(usb_audio_direction_type, direction, INPUT_OUTPUT, USB_AUDIO_DIRECTION_INPUT_OUTPUT); + +//| class Direction: +//| """The direction of a USB audio stream, relative to the host computer.""" +//| +//| def __init__(self) -> None: +//| """Enum-like class to define the USB audio stream direction.""" +//| ... +//| +//| INPUT: Direction +//| """Audio flows board -> host: the board appears as a USB microphone +//| (audio *source*). This is the default.""" +//| +//| OUTPUT: Direction +//| """Audio flows host -> board: the board appears as a USB speaker +//| (audio *sink*).""" +//| +//| INPUT_OUTPUT: Direction +//| """Audio flows in both directions: the board appears as a USB headset +//| (microphone + speaker).""" +//| +//| +MAKE_ENUM_MAP(usb_audio_direction) { + MAKE_ENUM_MAP_ENTRY(direction, INPUT), + MAKE_ENUM_MAP_ENTRY(direction, OUTPUT), + MAKE_ENUM_MAP_ENTRY(direction, INPUT_OUTPUT), +}; + +static MP_DEFINE_CONST_DICT(usb_audio_direction_locals_dict, usb_audio_direction_locals_table); + +MAKE_PRINTER(usb_audio, usb_audio_direction); + +MAKE_ENUM_TYPE(usb_audio, Direction, usb_audio_direction); + +usb_audio_direction_t validate_direction(mp_obj_t obj, qstr arg_name) { + return cp_enum_value(&usb_audio_direction_type, obj, arg_name); +} diff --git a/shared-bindings/usb_audio/Direction.h b/shared-bindings/usb_audio/Direction.h new file mode 100644 index 0000000000000..53ad3be16c40b --- /dev/null +++ b/shared-bindings/usb_audio/Direction.h @@ -0,0 +1,20 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/enum.h" +#include "py/obj.h" + +typedef enum _usb_audio_direction_t { + USB_AUDIO_DIRECTION_INPUT, + USB_AUDIO_DIRECTION_OUTPUT, + USB_AUDIO_DIRECTION_INPUT_OUTPUT, +} usb_audio_direction_t; + +extern const mp_obj_type_t usb_audio_direction_type; + +usb_audio_direction_t validate_direction(mp_obj_t obj, qstr arg_name); diff --git a/shared-bindings/usb_audio/USBSpeaker.c b/shared-bindings/usb_audio/USBSpeaker.c new file mode 100644 index 0000000000000..7df8cf5798da6 --- /dev/null +++ b/shared-bindings/usb_audio/USBSpeaker.c @@ -0,0 +1,193 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared/runtime/context_manager_helpers.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/usb_audio/USBSpeaker.h" +#include "shared-bindings/audiocore/__init__.h" +#include "shared-bindings/util.h" +#include "shared-module/usb_audio/__init__.h" + +//| class USBSpeaker: +//| """Plays audio streamed from the host computer as a USB Audio Class speaker. +//| +//| A ``USBSpeaker`` is a *source* of audio samples, exactly like +//| `audiocore.RawSample` or `audiocore.WaveFile`. The host PC streams audio to +//| the board over USB, and the ``USBSpeaker`` hands that audio to a consumer +//| such as `audiobusio.I2SOut`, `audiopwmio.PWMAudioOut` or `audioio.AudioOut` +//| (optionally through the effect modules), so the board appears as a speaker. +//| +//| ``usb_audio.enable(direction=usb_audio.Direction.OUTPUT)`` must have been +//| called in ``boot.py`` before this object can be constructed. +//| +//| .. code-block:: py +//| +//| # boot.py +//| import usb_audio +//| usb_audio.enable(sample_rate=16000, channel_count=1, bits_per_sample=16, +//| direction=usb_audio.Direction.OUTPUT) +//| +//| .. code-block:: py +//| +//| # code.py +//| import board +//| import usb_audio +//| import audiobusio +//| +//| spk = usb_audio.USBSpeaker() +//| out = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) +//| out.play(spk, loop=True) +//| +//| """ +//| +//| def __init__(self) -> None: +//| """Create a USBSpeaker using the audio format configured in ``boot.py``.""" +//| ... +//| +static mp_obj_t usb_audio_usbspeaker_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + static const mp_arg_t allowed_args[] = {}; + 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); + + // The audio format and USB interface are claimed by usb_audio.enable() in + // boot.py. Without it there is no speaker for the host to play to. + if (!usb_audio_enabled()) { + mp_raise_RuntimeError(MP_ERROR_TEXT("USB audio not enabled in boot.py")); + } + // A USBSpeaker only makes sense when the OUT endpoint is enumerated. + if (usb_audio_direction != USB_AUDIO_DIRECTION_OUTPUT && + usb_audio_direction != USB_AUDIO_DIRECTION_INPUT_OUTPUT) { + mp_raise_RuntimeError(MP_ERROR_TEXT("USB audio not enabled for output in boot.py")); + } + + usb_audio_usbspeaker_obj_t *self = mp_obj_malloc_with_finaliser(usb_audio_usbspeaker_obj_t, &usb_audio_USBSpeaker_type); + common_hal_usb_audio_usbspeaker_construct(self); + + return MP_OBJ_FROM_PTR(self); +} + +//| def deinit(self) -> None: +//| """Deinitialises the USBSpeaker and releases any resources for reuse.""" +//| ... +//| +static mp_obj_t usb_audio_usbspeaker_deinit(mp_obj_t self_in) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_usb_audio_usbspeaker_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbspeaker_deinit_obj, usb_audio_usbspeaker_deinit); + +static void check_for_deinit(usb_audio_usbspeaker_obj_t *self) { + audiosample_check_for_deinit(&self->base); +} + +//| def read(self, destination: circuitpython_typing.WriteableBuffer, destination_length: int) -> int: +//| """Copies up to ``destination_length`` of the most recent samples streamed +//| from the host into ``destination``, for analysis such as an audio-reactive +//| effect or VU meter. This does not block: it returns whatever the host has +//| delivered so far, which may be fewer than ``destination_length`` samples +//| (or zero when the host is not streaming). +//| +//| ``destination`` must be an ``array.array`` of 16-bit signed samples +//| (typecode ``"h"``), matching the negotiated speaker format. +//| +//| Reading consumes the samples, so a ``USBSpeaker`` being read this way +//| should not also be passed to an output backend's ``play()`` at the same +//| time. +//| +//| :return: The number of samples copied into ``destination``.""" +//| ... +//| +static mp_obj_t usb_audio_usbspeaker_obj_read(mp_obj_t self_in, mp_obj_t destination, mp_obj_t destination_length) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + uint32_t length = mp_arg_validate_type_int(destination_length, MP_QSTR_length); + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(destination, &bufinfo, MP_BUFFER_WRITE); + // The negotiated speaker format is 16-bit signed PCM, so require a matching + // 'h' array. This keeps the copy a straight memcpy with no conversion. + if (bufinfo.typecode != 'h') { + mp_raise_TypeError(MP_ERROR_TEXT("destination must be an array of type 'h'")); + } + size_t capacity = bufinfo.len / sizeof(int16_t); + if (capacity < length) { + length = capacity; + } + + uint32_t length_read = common_hal_usb_audio_usbspeaker_read(self, bufinfo.buf, length); + return MP_OBJ_NEW_SMALL_INT(length_read); +} +static MP_DEFINE_CONST_FUN_OBJ_3(usb_audio_usbspeaker_read_obj, usb_audio_usbspeaker_obj_read); + +//| def __enter__(self) -> USBSpeaker: +//| """No-op used by Context Managers.""" +//| ... +//| +// Provided by context manager helper. + +//| def __exit__(self) -> None: +//| """Automatically deinitializes the hardware when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +//| +// Provided by context manager helper. + +//| connected: bool +//| """True while the host is streaming audio to this speaker. (read-only)""" +//| +//| +static mp_obj_t usb_audio_usbspeaker_obj_get_connected(mp_obj_t self_in) { + usb_audio_usbspeaker_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_usb_audio_usbspeaker_get_connected(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbspeaker_get_connected_obj, usb_audio_usbspeaker_obj_get_connected); + +MP_PROPERTY_GETTER(usb_audio_usbspeaker_connected_obj, + (mp_obj_t)&usb_audio_usbspeaker_get_connected_obj); + +//| sample_rate: int +//| """The sample rate negotiated with the host in ``boot.py``. (read-only)""" +//| +//| bits_per_sample: int +//| """The bit depth negotiated with the host in ``boot.py``. (read-only)""" +//| +//| channel_count: int +//| """The number of channels negotiated with the host in ``boot.py``. (read-only)""" +//| +//| +static const mp_rom_map_elem_t usb_audio_usbspeaker_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&usb_audio_usbspeaker_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&usb_audio_usbspeaker_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&usb_audio_usbspeaker_read_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) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_connected), MP_ROM_PTR(&usb_audio_usbspeaker_connected_obj) }, + AUDIOSAMPLE_FIELDS, +}; +static MP_DEFINE_CONST_DICT(usb_audio_usbspeaker_locals_dict, usb_audio_usbspeaker_locals_dict_table); + +static const audiosample_p_t usb_audio_usbspeaker_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + .reset_buffer = (audiosample_reset_buffer_fun)usb_audio_usbspeaker_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)usb_audio_usbspeaker_get_buffer, +}; + +MP_DEFINE_CONST_OBJ_TYPE( + usb_audio_USBSpeaker_type, + MP_QSTR_USBSpeaker, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, usb_audio_usbspeaker_make_new, + locals_dict, &usb_audio_usbspeaker_locals_dict, + protocol, &usb_audio_usbspeaker_proto + ); diff --git a/shared-bindings/usb_audio/USBSpeaker.h b/shared-bindings/usb_audio/USBSpeaker.h new file mode 100644 index 0000000000000..30d2b0862a35d --- /dev/null +++ b/shared-bindings/usb_audio/USBSpeaker.h @@ -0,0 +1,17 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/usb_audio/USBSpeaker.h" + +extern const mp_obj_type_t usb_audio_USBSpeaker_type; + +void common_hal_usb_audio_usbspeaker_construct(usb_audio_usbspeaker_obj_t *self); +void common_hal_usb_audio_usbspeaker_deinit(usb_audio_usbspeaker_obj_t *self); +bool common_hal_usb_audio_usbspeaker_deinited(usb_audio_usbspeaker_obj_t *self); +bool common_hal_usb_audio_usbspeaker_get_connected(usb_audio_usbspeaker_obj_t *self); +uint32_t common_hal_usb_audio_usbspeaker_read(usb_audio_usbspeaker_obj_t *self, void *buffer, uint32_t length); diff --git a/shared-bindings/usb_audio/__init__.c b/shared-bindings/usb_audio/__init__.c index d6b5e15f02e37..027dc1fa92c41 100644 --- a/shared-bindings/usb_audio/__init__.c +++ b/shared-bindings/usb_audio/__init__.c @@ -8,7 +8,9 @@ #include "py/runtime.h" #include "shared-bindings/usb_audio/__init__.h" +#include "shared-bindings/usb_audio/Direction.h" #include "shared-bindings/usb_audio/USBMicrophone.h" +#include "shared-bindings/usb_audio/USBSpeaker.h" #include "shared-module/usb_audio/__init__.h" #include "shared-module/usb_audio/usb_audio_descriptors.h" @@ -36,36 +38,61 @@ //| .. code-block:: py //| //| # code.py +//| import time //| import usb_audio //| import synthio //| +//| # A USBMicrophone is a consumer of an audio sample, just like audioio.AudioOut: +//| # the samples it pulls are streamed to the host PC instead of to a pin. //| mic = usb_audio.USBMicrophone() //| synth = synthio.Synthesizer(sample_rate=16000, channel_count=1) //| mic.play(synth, loop=True) -//| synth.press(60) //| -//| """ +//| c_major_scale = [60, 62, 64, 65, 67, 69, 71, 72] +//| try: +//| while True: +//| for note in c_major_scale: +//| synth.press(note) +//| time.sleep(0.1) +//| synth.release(note) +//| time.sleep(0.05) +//| except KeyboardInterrupt: +//| pass +//| mic.stop() +//| +//| The ``sample_rate`` and ``channel_count`` of the sample played must match the +//| values passed to `enable`, and the sample must be 16-bit signed; otherwise +//| ``play`` raises a ``ValueError``. +//| +//| This interface is experimental and may change without notice even in stable +//| versions of CircuitPython.""" //| //| //| def enable( -//| sample_rate: int = 16000, channel_count: int = 1, bits_per_sample: int = 16 +//| sample_rate: int = 16000, +//| channel_count: int = 1, +//| bits_per_sample: int = 16, +//| direction: Direction = Direction.INPUT, //| ) -> None: -//| """Enable the USB audio microphone interface with the given PCM format. +//| """Enable the USB audio interface with the given PCM format. //| //| This function may only be used from ``boot.py``. //| //| :param int sample_rate: Samples per second of the streamed audio. //| :param int channel_count: Number of channels. Only mono (1) is supported initially. -//| :param int bits_per_sample: Bits per signed PCM sample. Only 16 is supported initially.""" +//| :param int bits_per_sample: Bits per signed PCM sample. Only 16 is supported initially. +//| :param Direction direction: Stream direction relative to the host. ``Direction.INPUT`` +//| (the default) presents a microphone; ``Direction.OUTPUT`` presents a speaker.""" //| //| static mp_obj_t usb_audio_enable(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - enum { ARG_sample_rate, ARG_channel_count, ARG_bits_per_sample }; + enum { ARG_sample_rate, ARG_channel_count, ARG_bits_per_sample, ARG_direction }; static const mp_arg_t allowed_args[] = { { MP_QSTR_sample_rate, MP_ARG_INT, { .u_int = 16000 } }, { MP_QSTR_channel_count, MP_ARG_INT, { .u_int = 1 } }, { MP_QSTR_bits_per_sample, MP_ARG_INT, { .u_int = 16 } }, + { MP_QSTR_direction, MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); @@ -73,8 +100,11 @@ static mp_obj_t usb_audio_enable(size_t n_args, const mp_obj_t *pos_args, mp_map mp_int_t sample_rate = mp_arg_validate_int_range(args[ARG_sample_rate].u_int, 1, USB_AUDIO_MAX_SAMPLE_RATE, MP_QSTR_sample_rate); mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, USB_AUDIO_N_CHANNELS, MP_QSTR_channel_count); mp_int_t bits_per_sample = mp_arg_validate_int(args[ARG_bits_per_sample].u_int, USB_AUDIO_BITS_PER_SAMPLE, MP_QSTR_bits_per_sample); + usb_audio_direction_t direction = args[ARG_direction].u_obj == MP_OBJ_NULL + ? USB_AUDIO_DIRECTION_INPUT + : validate_direction(args[ARG_direction].u_obj, MP_QSTR_direction); - if (!shared_module_usb_audio_enable(sample_rate, channel_count, bits_per_sample)) { + if (!shared_module_usb_audio_enable(sample_rate, channel_count, bits_per_sample, direction)) { mp_raise_RuntimeError(MP_ERROR_TEXT("Cannot change USB devices now")); } @@ -84,7 +114,9 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(usb_audio_enable_obj, 0, usb_audio_enable); static const mp_rom_map_elem_t usb_audio_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_usb_audio) }, + { MP_ROM_QSTR(MP_QSTR_Direction), MP_ROM_PTR(&usb_audio_direction_type) }, { MP_ROM_QSTR(MP_QSTR_USBMicrophone), MP_ROM_PTR(&usb_audio_USBMicrophone_type) }, + { MP_ROM_QSTR(MP_QSTR_USBSpeaker), MP_ROM_PTR(&usb_audio_USBSpeaker_type) }, { MP_ROM_QSTR(MP_QSTR_enable), MP_ROM_PTR(&usb_audio_enable_obj) }, }; diff --git a/shared-module/usb_audio/USBSpeaker.c b/shared-module/usb_audio/USBSpeaker.c new file mode 100644 index 0000000000000..12921430bc380 --- /dev/null +++ b/shared-module/usb_audio/USBSpeaker.c @@ -0,0 +1,227 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/misc.h" + +#include "shared-bindings/usb_audio/USBSpeaker.h" +#include "shared-bindings/audiocore/__init__.h" +#include "shared-bindings/microcontroller/__init__.h" +#include "shared-module/usb_audio/__init__.h" + +#include "tusb.h" + +// The ring is sized independently of the TinyUSB headers (see USBSpeaker.h); +// check it still matches the OUT endpoint's software FIFO so the push side can be +// reasoned about against the USB plumbing. +MP_STATIC_ASSERT(USB_AUDIO_SPEAKER_RING_SIZE == CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ); + +// Only one speaker can be fed by the single USB OUT endpoint at a time. This +// points at the most recently constructed USBSpeaker, or NULL when none exists, +// mirroring active_microphone in USBMicrophone.c. The USB background task pushes +// received bytes into it via usb_audio_usbspeaker_background_drain(). +static usb_audio_usbspeaker_obj_t *active_speaker = NULL; + +void common_hal_usb_audio_usbspeaker_construct(usb_audio_usbspeaker_obj_t *self) { + // The pipeline treats the speaker as an ordinary audiosample source, so + // populate base from the format negotiated by usb_audio.enable(). The UAC2 + // format we present is 16-bit signed LE PCM, which is exactly what the + // CircuitPython audio pipeline carries, so no conversion is needed. + self->base.sample_rate = usb_audio_sample_rate; + self->base.bits_per_sample = usb_audio_bits_per_sample; + self->base.channel_count = usb_audio_channel_count; + self->base.samples_signed = true; + self->base.single_buffer = false; + self->base.max_buffer_length = USB_AUDIO_SPEAKER_OUTPUT_BUFFER_SIZE; + + self->ring_head = 0; + self->ring_tail = 0; + self->ring_count = 0; + self->output_index = 0; + + // The most recently created speaker receives the host's OUT stream. + active_speaker = self; +} + +void common_hal_usb_audio_usbspeaker_deinit(usb_audio_usbspeaker_obj_t *self) { + // Stop directing USB OUT data at this object. The producer (drain) and this + // deinit both run in non-interrupt context, so the pointer swap needs no + // interrupt guard. + if (active_speaker == self) { + active_speaker = NULL; + } + audiosample_mark_deinit(&self->base); +} + +bool common_hal_usb_audio_usbspeaker_deinited(usb_audio_usbspeaker_obj_t *self) { + return audiosample_deinited(&self->base); +} + +bool common_hal_usb_audio_usbspeaker_get_connected(usb_audio_usbspeaker_obj_t *self) { + (void)self; + // True while the host has opened the OUT streaming interface, i.e. it is + // actively sending audio. Speaker-specific so it stays correct when a mic + // shares the same headset function. + return usb_audio_speaker_streaming(); +} + +// --------------------------------------------------------------------+ +// Receive ring (push side, producer = USB background task) +// --------------------------------------------------------------------+ +// +// The ring decouples the two independently clocked stages from +// usb_audio_output_plan.md: USB push (paced by the host's SOF clock, in the +// background task) and the audiosample pull (paced by the output backend's +// sample clock, in its refill interrupt). It is a single-producer/ +// single-consumer ring across an interrupt boundary: +// +// * Producer: usb_audio_usbspeaker_background_drain(), task context. +// * Consumer: usb_audio_usbspeaker_get_buffer(), output DMA/refill ISR. +// +// The consumer is an interrupt, so it can never be preempted by the producer and +// needs no guard of its own. The producer can be preempted by the consumer, and +// its drop-oldest overrun handling touches both cursors, so it does its whole +// read-modify-write with interrupts disabled. + +void usb_audio_usbspeaker_streaming_reset(void) { + usb_audio_usbspeaker_obj_t *self = active_speaker; + if (self == NULL) { + return; + } + common_hal_mcu_disable_interrupts(); + self->ring_head = 0; + self->ring_tail = 0; + self->ring_count = 0; + common_hal_mcu_enable_interrupts(); +} + +void usb_audio_usbspeaker_background_drain(const uint8_t *in, size_t n) { + usb_audio_usbspeaker_obj_t *self = active_speaker; + if (self == NULL || n == 0) { + return; + } + if (n >= USB_AUDIO_SPEAKER_RING_SIZE) { + // A single chunk larger than the whole ring can only contribute its + // newest tail. (Cannot happen with USB packets << ring size, but keep + // the copies provably in-bounds.) + in += n - USB_AUDIO_SPEAKER_RING_SIZE; + n = USB_AUDIO_SPEAKER_RING_SIZE; + } + + common_hal_mcu_disable_interrupts(); + + size_t free_space = USB_AUDIO_SPEAKER_RING_SIZE - self->ring_count; + if (n > free_space) { + // Overrun: advance the read cursor past the oldest bytes we're about to + // overwrite, keeping latency bounded and following the newest host audio. + size_t drop = n - free_space; + self->ring_tail = (self->ring_tail + drop) % USB_AUDIO_SPEAKER_RING_SIZE; + self->ring_count -= drop; + } + + // Copy in one or two segments, wrapping at the end of the ring. + size_t first = MIN(n, USB_AUDIO_SPEAKER_RING_SIZE - self->ring_head); + memcpy(&self->ring[self->ring_head], in, first); + if (n > first) { + memcpy(&self->ring[0], in + first, n - first); + } + self->ring_head = (self->ring_head + n) % USB_AUDIO_SPEAKER_RING_SIZE; + self->ring_count += n; + + common_hal_mcu_enable_interrupts(); +} + +uint32_t common_hal_usb_audio_usbspeaker_read(usb_audio_usbspeaker_obj_t *self, + void *buffer, uint32_t length) { + // Hand the most recent host audio to Python so it can be analysed (e.g. an + // audio-reactive effect or VU meter). This drains the ring just like the + // output backend's get_buffer() does, so it is an alternative *consumer*: + // a USBSpeaker being read this way must not also be play()ed to an output + // backend at the same time, or the two consumers would race on the single + // SPSC ring. + // + // Unlike get_buffer() (which runs in the output refill ISR and so needs no + // guard) this runs in VM/task context and can be preempted by the USB + // producer, so it brackets its read-modify-write with interrupts disabled, + // mirroring usb_audio_usbspeaker_background_drain(). + size_t bytes_per_frame = (self->base.bits_per_sample / 8) * self->base.channel_count; + size_t want = (size_t)length * bytes_per_frame; + + common_hal_mcu_disable_interrupts(); + size_t to_copy = MIN(self->ring_count, want); + // Never split a frame across calls (the ring always holds whole UAC2 frames, + // but stay defensive so the returned count is always a whole number). + to_copy -= to_copy % bytes_per_frame; + + size_t first = MIN(to_copy, USB_AUDIO_SPEAKER_RING_SIZE - self->ring_tail); + memcpy(buffer, &self->ring[self->ring_tail], first); + if (to_copy > first) { + memcpy((uint8_t *)buffer + first, &self->ring[0], to_copy - first); + } + self->ring_tail = (self->ring_tail + to_copy) % USB_AUDIO_SPEAKER_RING_SIZE; + self->ring_count -= to_copy; + common_hal_mcu_enable_interrupts(); + + return to_copy / bytes_per_frame; +} + +// --------------------------------------------------------------------+ +// audiosample protocol (pull side, consumer = output backend) +// --------------------------------------------------------------------+ + +void usb_audio_usbspeaker_reset_buffer(usb_audio_usbspeaker_obj_t *self, + bool single_channel_output, uint8_t channel) { + (void)single_channel_output; + (void)channel; + // Begin playback from live audio rather than whatever was buffered before: + // drop the ring and restart the double-buffer. Guarded because some ports may + // call reset_buffer outside the initial setup. + common_hal_mcu_disable_interrupts(); + self->ring_head = 0; + self->ring_tail = 0; + self->ring_count = 0; + common_hal_mcu_enable_interrupts(); + self->output_index = 0; +} + +audioio_get_buffer_result_t usb_audio_usbspeaker_get_buffer(usb_audio_usbspeaker_obj_t *self, + bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { + + uint32_t half = self->base.max_buffer_length / 2; + uint8_t *out = self->output_buffer + half * self->output_index; + self->output_index = 1 - self->output_index; + + // Consumer side of the SPSC ring (runs in the output backend's refill ISR). + // It is never preempted by the producer, so no interrupt guard is required. + size_t to_copy = MIN(self->ring_count, (size_t)half); + + size_t first = MIN(to_copy, USB_AUDIO_SPEAKER_RING_SIZE - self->ring_tail); + memcpy(out, &self->ring[self->ring_tail], first); + if (to_copy > first) { + memcpy(out + first, &self->ring[0], to_copy - first); + } + self->ring_tail = (self->ring_tail + to_copy) % USB_AUDIO_SPEAKER_RING_SIZE; + self->ring_count -= to_copy; + + if (to_copy < half) { + // Underrun: pad the remainder with silence. Samples are signed, so + // silence is 0. This is the consume-side of the pacing failure mode + // tracked in the usb-audio-artifact-pacing memory: we never spin. + memset(out + to_copy, 0, half - to_copy); + } + + // Mono only for v1, so the single-channel offset is always 0; computed the + // same way as audiocore.RawSample so stereo (interleaved ring) can extend it. + if (single_channel_output) { + out += (channel % self->base.channel_count) * (self->base.bits_per_sample / 8); + } + + *buffer = out; + *buffer_length = half; + // A live USB stream is infinite; never report DONE or the backend would stop. + return GET_BUFFER_MORE_DATA; +} diff --git a/shared-module/usb_audio/USBSpeaker.h b/shared-module/usb_audio/USBSpeaker.h new file mode 100644 index 0000000000000..8c06cff519d1f --- /dev/null +++ b/shared-module/usb_audio/USBSpeaker.h @@ -0,0 +1,73 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +#include "py/obj.h" + +#include "shared-module/audiocore/__init__.h" +#include "shared-module/usb_audio/usb_audio_descriptors.h" + +// One half-buffer of the audiosample double-buffer holds this many frames. The +// output backend pulls a half each get_buffer() call, so this sets the pull +// granularity and the silence-pad chunk on underrun. It is deliberately +// independent of the USB OUT software FIFO (the ring below) so the push and pull +// stages can be tuned separately, and small enough that two halves sit +// comfortably inside the ring. +#define USB_AUDIO_SPEAKER_FRAMES_PER_BUFFER (128) + +// Bytes per audio frame in the negotiated UAC2 format (mono 16-bit for v1). +#define USB_AUDIO_SPEAKER_BYTES_PER_FRAME (USB_AUDIO_N_BYTES_PER_SAMPLE * USB_AUDIO_N_CHANNELS) + +// Full owned double-buffer: two halves of FRAMES_PER_BUFFER frames each. Matches +// audiocore.RawSample's convention where base.max_buffer_length is the whole +// buffer and get_buffer() returns half of it. +#define USB_AUDIO_SPEAKER_OUTPUT_BUFFER_SIZE (2 * USB_AUDIO_SPEAKER_FRAMES_PER_BUFFER * USB_AUDIO_SPEAKER_BYTES_PER_FRAME) + +// Host -> board receive ring size. Mirrors CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ +// (16 * the full-speed OUT wMaxPacketSize) but is spelled out from the format +// constants so this struct definition stays free of the TinyUSB headers. A +// MP_STATIC_ASSERT in USBSpeaker.c checks the two stay equal. +#define USB_AUDIO_SPEAKER_OUT_PACKET_SIZE ((USB_AUDIO_MAX_SAMPLE_RATE / 1000 + 1) * USB_AUDIO_SPEAKER_BYTES_PER_FRAME) +#define USB_AUDIO_SPEAKER_RING_SIZE (16 * USB_AUDIO_SPEAKER_OUT_PACKET_SIZE) + +typedef struct usb_audio_usbspeaker_obj { + // First member so the object can be used directly as an audiosample source. + audiosample_base_t base; + + // Host -> board receive ring (the "push" stage). Filled by the USB + // background task via usb_audio_usbspeaker_background_drain() and drained by + // get_buffer(). Single producer (task), single consumer (output DMA ISR); + // see the concurrency notes in USBSpeaker.c. + uint8_t ring[USB_AUDIO_SPEAKER_RING_SIZE]; + size_t ring_head; // next write offset + size_t ring_tail; // next read offset + size_t ring_count; // valid bytes currently in the ring + + // Owned double-buffer returned to the output backend by get_buffer(). + uint8_t output_buffer[USB_AUDIO_SPEAKER_OUTPUT_BUFFER_SIZE]; + uint8_t output_index; // 0 or 1: which half get_buffer() fills next +} usb_audio_usbspeaker_obj_t; + +// audiosample protocol implementation. Not exposed to Python because get_buffer() +// runs in the output backend's refill interrupt. +void usb_audio_usbspeaker_reset_buffer(usb_audio_usbspeaker_obj_t *self, + bool single_channel_output, uint8_t channel); +audioio_get_buffer_result_t usb_audio_usbspeaker_get_buffer(usb_audio_usbspeaker_obj_t *self, + bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length); + +// Push n bytes received on the USB OUT endpoint into the active speaker's ring. +// Called from the USB background task (see usb_audio_task()). No-op when no +// speaker is active. Overrun policy: drop the oldest buffered bytes. +void usb_audio_usbspeaker_background_drain(const uint8_t *in, size_t n); + +// Drop anything buffered in the active speaker's ring so a (re)opened stream +// starts from live audio. Called when the host opens/closes the OUT streaming +// interface. +void usb_audio_usbspeaker_streaming_reset(void); diff --git a/shared-module/usb_audio/__init__.c b/shared-module/usb_audio/__init__.c index 1ce2c6691fdfe..41d8b14ba2c35 100644 --- a/shared-module/usb_audio/__init__.c +++ b/shared-module/usb_audio/__init__.c @@ -6,24 +6,38 @@ #include "shared-module/usb_audio/__init__.h" #include "shared-module/usb_audio/USBMicrophone.h" +#include "shared-module/usb_audio/USBSpeaker.h" #include "shared-module/usb_audio/usb_audio_descriptors.h" #include +#include "py/misc.h" #include "tusb.h" static bool usb_audio_is_enabled = false; -static bool usb_audio_is_streaming = false; + +// The host opens each AudioStreaming interface independently (alt 0 = idle, alt 1 +// = streaming), so the two directions of a headset are tracked separately. For +// the single-direction microphone/speaker only the matching flag is ever set. +static bool usb_audio_mic_streaming = false; +static bool usb_audio_spk_streaming = false; + +// AudioStreaming interface numbers assigned when the descriptor is built, used to +// route the host's set-interface requests to the right direction. 0xff until a +// descriptor that includes that direction has been emitted. +static uint8_t usb_audio_mic_as_itf = 0xff; +static uint8_t usb_audio_spk_as_itf = 0xff; uint32_t usb_audio_sample_rate; uint8_t usb_audio_channel_count; uint8_t usb_audio_bits_per_sample; +usb_audio_direction_t usb_audio_direction; // Audio control state surfaced to the host. One extra entry for the master channel 0. static int8_t usb_audio_mute[USB_AUDIO_N_CHANNELS + 1]; static int16_t usb_audio_volume[USB_AUDIO_N_CHANNELS + 1]; -bool shared_module_usb_audio_enable(mp_int_t sample_rate, mp_int_t channel_count, mp_int_t bits_per_sample) { +bool shared_module_usb_audio_enable(mp_int_t sample_rate, mp_int_t channel_count, mp_int_t bits_per_sample, usb_audio_direction_t direction) { if (tud_connected()) { return false; } @@ -31,6 +45,7 @@ bool shared_module_usb_audio_enable(mp_int_t sample_rate, mp_int_t channel_count usb_audio_sample_rate = sample_rate; usb_audio_channel_count = channel_count; usb_audio_bits_per_sample = bits_per_sample; + usb_audio_direction = direction; usb_audio_is_enabled = true; return true; @@ -49,14 +64,211 @@ bool usb_audio_enabled(void) { } bool usb_audio_streaming(void) { - return usb_audio_is_streaming; + return usb_audio_mic_streaming || usb_audio_spk_streaming; +} + +// True while the host has the speaker (OUT) stream open, i.e. it is actively +// sending audio. Used by USBSpeaker.connected so it reflects the speaker +// direction specifically even when a mic shares the same headset function. +bool usb_audio_speaker_streaming(void) { + return usb_audio_spk_streaming; +} + +// Hand-rolled UAC2 mono speaker (host -> board) descriptor WITHOUT an async +// feedback endpoint. This mirrors TinyUSB's TUD_AUDIO_SPEAKER_MONO_FB_DESCRIPTOR +// (lib/tinyusb/src/device/usbd.h) but drops the trailing feedback endpoint, so +// the streaming alt-setting declares a single OUT endpoint (_nEPs = 0x01). The +// entity IDs match the mic descriptor (see usb_audio_descriptors.h); only the +// terminal roles reverse: the input terminal is the USB-streaming side and the +// output terminal is the desktop speaker, and the AS interface links the input +// terminal (0x01). Async feedback for true clock matching is a later step. +#define USB_AUDIO_SPEAKER_DESCRIPTOR(_itfnum, _stridx, _nBytesPerSample, _nBitsUsedPerSample, _epout, _epsize) \ + /* Standard Interface Association Descriptor (IAD) */ \ + TUD_AUDIO_DESC_IAD(/*_firstitf*/ _itfnum, /*_nitfs*/ 0x02, /*_stridx*/ 0x00), \ + /* Standard AC Interface Descriptor(4.7.1) */ \ + TUD_AUDIO_DESC_STD_AC(/*_itfnum*/ _itfnum, /*_nEPs*/ 0x00, /*_stridx*/ _stridx), \ + /* Class-Specific AC Interface Header Descriptor(4.7.2) */ \ + TUD_AUDIO_DESC_CS_AC(/*_bcdADC*/ 0x0200, /*_category*/ AUDIO_FUNC_DESKTOP_SPEAKER, /*_totallen*/ TUD_AUDIO_DESC_CLK_SRC_LEN + TUD_AUDIO_DESC_INPUT_TERM_LEN + TUD_AUDIO_DESC_OUTPUT_TERM_LEN + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL_LEN, /*_ctrl*/ AUDIO_CS_AS_INTERFACE_CTRL_LATENCY_POS), \ + /* Clock Source Descriptor(4.7.2.1) */ \ + TUD_AUDIO_DESC_CLK_SRC(/*_clkid*/ USB_AUDIO_ENTITY_CLOCK_SOURCE, /*_attr*/ AUDIO_CLOCK_SOURCE_ATT_INT_FIX_CLK, /*_ctrl*/ (AUDIO_CTRL_R << AUDIO_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), /*_assocTerm*/ USB_AUDIO_ENTITY_INPUT_TERMINAL, /*_stridx*/ 0x00), \ + /* Input Terminal Descriptor(4.7.2.4) -- USB streaming in from the host */ \ + TUD_AUDIO_DESC_INPUT_TERM(/*_termid*/ USB_AUDIO_ENTITY_INPUT_TERMINAL, /*_termtype*/ AUDIO_TERM_TYPE_USB_STREAMING, /*_assocTerm*/ 0x00, /*_clkid*/ USB_AUDIO_ENTITY_CLOCK_SOURCE, /*_nchannelslogical*/ USB_AUDIO_N_CHANNELS, /*_channelcfg*/ AUDIO_CHANNEL_CONFIG_NON_PREDEFINED, /*_idxchannelnames*/ 0x00, /*_ctrl*/ 0 * (AUDIO_CTRL_R << AUDIO_IN_TERM_CTRL_CONNECTOR_POS), /*_stridx*/ 0x00), \ + /* Output Terminal Descriptor(4.7.2.5) -- desktop speaker */ \ + TUD_AUDIO_DESC_OUTPUT_TERM(/*_termid*/ USB_AUDIO_ENTITY_OUTPUT_TERMINAL, /*_termtype*/ AUDIO_TERM_TYPE_OUT_DESKTOP_SPEAKER, /*_assocTerm*/ USB_AUDIO_ENTITY_INPUT_TERMINAL, /*_srcid*/ USB_AUDIO_ENTITY_FEATURE_UNIT, /*_clkid*/ USB_AUDIO_ENTITY_CLOCK_SOURCE, /*_ctrl*/ 0x0000, /*_stridx*/ 0x00), \ + /* Feature Unit Descriptor(4.7.2.8) */ \ + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL(/*_unitid*/ USB_AUDIO_ENTITY_FEATURE_UNIT, /*_srcid*/ USB_AUDIO_ENTITY_INPUT_TERMINAL, /*_ctrlch0master*/ AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_MUTE_POS | AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_VOLUME_POS, /*_ctrlch1*/ AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_MUTE_POS | AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_VOLUME_POS, /*_stridx*/ 0x00), \ + /* Standard AS Interface Descriptor(4.9.1) -- alt 0, zero bandwidth */ \ + TUD_AUDIO_DESC_STD_AS_INT(/*_itfnum*/ (uint8_t)((_itfnum) + 1), /*_altset*/ 0x00, /*_nEPs*/ 0x00, /*_stridx*/ 0x00), \ + /* Standard AS Interface Descriptor(4.9.1) -- alt 1, one OUT endpoint */ \ + TUD_AUDIO_DESC_STD_AS_INT(/*_itfnum*/ (uint8_t)((_itfnum) + 1), /*_altset*/ 0x01, /*_nEPs*/ 0x01, /*_stridx*/ 0x00), \ + /* Class-Specific AS Interface Descriptor(4.9.2) -- linked to the input terminal */ \ + TUD_AUDIO_DESC_CS_AS_INT(/*_termid*/ USB_AUDIO_ENTITY_INPUT_TERMINAL, /*_ctrl*/ AUDIO_CTRL_NONE, /*_formattype*/ AUDIO_FORMAT_TYPE_I, /*_formats*/ AUDIO_DATA_FORMAT_TYPE_I_PCM, /*_nchannelsphysical*/ USB_AUDIO_N_CHANNELS, /*_channelcfg*/ AUDIO_CHANNEL_CONFIG_NON_PREDEFINED, /*_stridx*/ 0x00), \ + /* Type I Format Type Descriptor(2.3.1.6 - Audio Formats) */ \ + TUD_AUDIO_DESC_TYPE_I_FORMAT(_nBytesPerSample, _nBitsUsedPerSample), \ + /* Standard AS Isochronous Audio Data Endpoint Descriptor(4.10.1.1) */ \ + TUD_AUDIO_DESC_STD_AS_ISO_EP(/*_ep*/ _epout, /*_attr*/ (uint8_t)((uint8_t)TUSB_XFER_ISOCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_ASYNCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_DATA), /*_maxEPsize*/ _epsize, /*_interval*/ 0x01), \ + /* Class-Specific AS Isochronous Audio Data Endpoint Descriptor(4.10.1.2) */ \ + TUD_AUDIO_DESC_CS_AS_ISO_EP(/*_attr*/ AUDIO_CS_AS_ISO_DATA_EP_ATT_NON_MAX_PACKETS_OK, /*_ctrl*/ AUDIO_CTRL_NONE, /*_lockdelayunit*/ AUDIO_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_UNDEFINED, /*_lockdelay*/ 0x0000) + +// Hand-rolled UAC2 mono headset (Direction.INPUT_OUTPUT): one audio function +// presenting both a speaker (host -> board OUT) and a microphone (board -> host +// IN) at once. This combines USB_AUDIO_SPEAKER_DESCRIPTOR's speaker chain with +// TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR's mic chain under a single IAD. The two chains +// must use distinct entity IDs (USB_AUDIO_HS_ENTITY_*; see usb_audio_descriptors.h) +// because they live in the same AudioControl interface, and they share one clock +// source. The function spans three interfaces: AudioControl (_itfnum), the +// speaker AudioStreaming interface (_itfnum + 1, OUT endpoint), and the mic +// AudioStreaming interface (_itfnum + 2, IN endpoint). Neither stream has an +// async feedback endpoint, matching the single-direction descriptors. +#define USB_AUDIO_HEADSET_DESCRIPTOR(_itfnum, _stridx, _nBytesPerSample, _nBitsUsedPerSample, _epout, _epin, _epsize) \ + /* Standard Interface Association Descriptor (IAD) -- 3 interfaces */ \ + TUD_AUDIO_DESC_IAD(/*_firstitf*/ _itfnum, /*_nitfs*/ 0x03, /*_stridx*/ 0x00), \ + /* Standard AC Interface Descriptor(4.7.1) */ \ + TUD_AUDIO_DESC_STD_AC(/*_itfnum*/ _itfnum, /*_nEPs*/ 0x00, /*_stridx*/ _stridx), \ + /* Class-Specific AC Interface Header Descriptor(4.7.2) -- clock + both chains */ \ + TUD_AUDIO_DESC_CS_AC(/*_bcdADC*/ 0x0200, /*_category*/ AUDIO_FUNC_HEADSET, /*_totallen*/ TUD_AUDIO_DESC_CLK_SRC_LEN + 2 * (TUD_AUDIO_DESC_INPUT_TERM_LEN + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL_LEN + TUD_AUDIO_DESC_OUTPUT_TERM_LEN), /*_ctrl*/ AUDIO_CS_AS_INTERFACE_CTRL_LATENCY_POS), \ + /* Clock Source Descriptor(4.7.2.1) -- shared by both chains */ \ + TUD_AUDIO_DESC_CLK_SRC(/*_clkid*/ USB_AUDIO_HS_ENTITY_CLOCK_SOURCE, /*_attr*/ AUDIO_CLOCK_SOURCE_ATT_INT_FIX_CLK, /*_ctrl*/ (AUDIO_CTRL_R << AUDIO_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), /*_assocTerm*/ 0x00, /*_stridx*/ 0x00), \ + /* --- Speaker chain (host -> board) --- */ \ + /* Input Terminal Descriptor(4.7.2.4) -- USB streaming in from the host */ \ + TUD_AUDIO_DESC_INPUT_TERM(/*_termid*/ USB_AUDIO_HS_ENTITY_SPK_INPUT_TERMINAL, /*_termtype*/ AUDIO_TERM_TYPE_USB_STREAMING, /*_assocTerm*/ 0x00, /*_clkid*/ USB_AUDIO_HS_ENTITY_CLOCK_SOURCE, /*_nchannelslogical*/ USB_AUDIO_N_CHANNELS, /*_channelcfg*/ AUDIO_CHANNEL_CONFIG_NON_PREDEFINED, /*_idxchannelnames*/ 0x00, /*_ctrl*/ 0 * (AUDIO_CTRL_R << AUDIO_IN_TERM_CTRL_CONNECTOR_POS), /*_stridx*/ 0x00), \ + /* Feature Unit Descriptor(4.7.2.8) */ \ + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL(/*_unitid*/ USB_AUDIO_HS_ENTITY_SPK_FEATURE_UNIT, /*_srcid*/ USB_AUDIO_HS_ENTITY_SPK_INPUT_TERMINAL, /*_ctrlch0master*/ AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_MUTE_POS | AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_VOLUME_POS, /*_ctrlch1*/ AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_MUTE_POS | AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_VOLUME_POS, /*_stridx*/ 0x00), \ + /* Output Terminal Descriptor(4.7.2.5) -- desktop speaker */ \ + TUD_AUDIO_DESC_OUTPUT_TERM(/*_termid*/ USB_AUDIO_HS_ENTITY_SPK_OUTPUT_TERMINAL, /*_termtype*/ AUDIO_TERM_TYPE_OUT_DESKTOP_SPEAKER, /*_assocTerm*/ 0x00, /*_srcid*/ USB_AUDIO_HS_ENTITY_SPK_FEATURE_UNIT, /*_clkid*/ USB_AUDIO_HS_ENTITY_CLOCK_SOURCE, /*_ctrl*/ 0x0000, /*_stridx*/ 0x00), \ + /* --- Mic chain (board -> host) --- */ \ + /* Input Terminal Descriptor(4.7.2.4) -- generic microphone */ \ + TUD_AUDIO_DESC_INPUT_TERM(/*_termid*/ USB_AUDIO_HS_ENTITY_MIC_INPUT_TERMINAL, /*_termtype*/ AUDIO_TERM_TYPE_IN_GENERIC_MIC, /*_assocTerm*/ 0x00, /*_clkid*/ USB_AUDIO_HS_ENTITY_CLOCK_SOURCE, /*_nchannelslogical*/ USB_AUDIO_N_CHANNELS, /*_channelcfg*/ AUDIO_CHANNEL_CONFIG_NON_PREDEFINED, /*_idxchannelnames*/ 0x00, /*_ctrl*/ AUDIO_CTRL_R << AUDIO_IN_TERM_CTRL_CONNECTOR_POS, /*_stridx*/ 0x00), \ + /* Feature Unit Descriptor(4.7.2.8) */ \ + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL(/*_unitid*/ USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT, /*_srcid*/ USB_AUDIO_HS_ENTITY_MIC_INPUT_TERMINAL, /*_ctrlch0master*/ AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_MUTE_POS | AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_VOLUME_POS, /*_ctrlch1*/ AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_MUTE_POS | AUDIO_CTRL_RW << AUDIO_FEATURE_UNIT_CTRL_VOLUME_POS, /*_stridx*/ 0x00), \ + /* Output Terminal Descriptor(4.7.2.5) -- USB streaming out to the host */ \ + TUD_AUDIO_DESC_OUTPUT_TERM(/*_termid*/ USB_AUDIO_HS_ENTITY_MIC_OUTPUT_TERMINAL, /*_termtype*/ AUDIO_TERM_TYPE_USB_STREAMING, /*_assocTerm*/ 0x00, /*_srcid*/ USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT, /*_clkid*/ USB_AUDIO_HS_ENTITY_CLOCK_SOURCE, /*_ctrl*/ 0x0000, /*_stridx*/ 0x00), \ + /* --- Speaker AudioStreaming interface (_itfnum + 1) --- */ \ + /* Standard AS Interface Descriptor(4.9.1) -- alt 0, zero bandwidth */ \ + TUD_AUDIO_DESC_STD_AS_INT(/*_itfnum*/ (uint8_t)((_itfnum) + 1), /*_altset*/ 0x00, /*_nEPs*/ 0x00, /*_stridx*/ 0x00), \ + /* Standard AS Interface Descriptor(4.9.1) -- alt 1, one OUT endpoint */ \ + TUD_AUDIO_DESC_STD_AS_INT(/*_itfnum*/ (uint8_t)((_itfnum) + 1), /*_altset*/ 0x01, /*_nEPs*/ 0x01, /*_stridx*/ 0x00), \ + /* Class-Specific AS Interface Descriptor(4.9.2) -- linked to the speaker input terminal */ \ + TUD_AUDIO_DESC_CS_AS_INT(/*_termid*/ USB_AUDIO_HS_ENTITY_SPK_INPUT_TERMINAL, /*_ctrl*/ AUDIO_CTRL_NONE, /*_formattype*/ AUDIO_FORMAT_TYPE_I, /*_formats*/ AUDIO_DATA_FORMAT_TYPE_I_PCM, /*_nchannelsphysical*/ USB_AUDIO_N_CHANNELS, /*_channelcfg*/ AUDIO_CHANNEL_CONFIG_NON_PREDEFINED, /*_stridx*/ 0x00), \ + /* Type I Format Type Descriptor(2.3.1.6 - Audio Formats) */ \ + TUD_AUDIO_DESC_TYPE_I_FORMAT(_nBytesPerSample, _nBitsUsedPerSample), \ + /* Standard AS Isochronous Audio Data Endpoint Descriptor(4.10.1.1) */ \ + TUD_AUDIO_DESC_STD_AS_ISO_EP(/*_ep*/ _epout, /*_attr*/ (uint8_t)((uint8_t)TUSB_XFER_ISOCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_ASYNCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_DATA), /*_maxEPsize*/ _epsize, /*_interval*/ 0x01), \ + /* Class-Specific AS Isochronous Audio Data Endpoint Descriptor(4.10.1.2) */ \ + TUD_AUDIO_DESC_CS_AS_ISO_EP(/*_attr*/ AUDIO_CS_AS_ISO_DATA_EP_ATT_NON_MAX_PACKETS_OK, /*_ctrl*/ AUDIO_CTRL_NONE, /*_lockdelayunit*/ AUDIO_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_UNDEFINED, /*_lockdelay*/ 0x0000), \ + /* --- Mic AudioStreaming interface (_itfnum + 2) --- */ \ + /* Standard AS Interface Descriptor(4.9.1) -- alt 0, zero bandwidth */ \ + TUD_AUDIO_DESC_STD_AS_INT(/*_itfnum*/ (uint8_t)((_itfnum) + 2), /*_altset*/ 0x00, /*_nEPs*/ 0x00, /*_stridx*/ 0x00), \ + /* Standard AS Interface Descriptor(4.9.1) -- alt 1, one IN endpoint */ \ + TUD_AUDIO_DESC_STD_AS_INT(/*_itfnum*/ (uint8_t)((_itfnum) + 2), /*_altset*/ 0x01, /*_nEPs*/ 0x01, /*_stridx*/ 0x00), \ + /* Class-Specific AS Interface Descriptor(4.9.2) -- linked to the mic output terminal */ \ + TUD_AUDIO_DESC_CS_AS_INT(/*_termid*/ USB_AUDIO_HS_ENTITY_MIC_OUTPUT_TERMINAL, /*_ctrl*/ AUDIO_CTRL_NONE, /*_formattype*/ AUDIO_FORMAT_TYPE_I, /*_formats*/ AUDIO_DATA_FORMAT_TYPE_I_PCM, /*_nchannelsphysical*/ USB_AUDIO_N_CHANNELS, /*_channelcfg*/ AUDIO_CHANNEL_CONFIG_NON_PREDEFINED, /*_stridx*/ 0x00), \ + /* Type I Format Type Descriptor(2.3.1.6 - Audio Formats) */ \ + TUD_AUDIO_DESC_TYPE_I_FORMAT(_nBytesPerSample, _nBitsUsedPerSample), \ + /* Standard AS Isochronous Audio Data Endpoint Descriptor(4.10.1.1) */ \ + TUD_AUDIO_DESC_STD_AS_ISO_EP(/*_ep*/ _epin, /*_attr*/ (uint8_t)((uint8_t)TUSB_XFER_ISOCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_ASYNCHRONOUS | (uint8_t)TUSB_ISO_EP_ATT_DATA), /*_maxEPsize*/ _epsize, /*_interval*/ 0x01), \ + /* Class-Specific AS Isochronous Audio Data Endpoint Descriptor(4.10.1.2) */ \ + TUD_AUDIO_DESC_CS_AS_ISO_EP(/*_attr*/ AUDIO_CS_AS_ISO_DATA_EP_ATT_NON_MAX_PACKETS_OK, /*_ctrl*/ AUDIO_CTRL_NONE, /*_lockdelayunit*/ AUDIO_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_UNDEFINED, /*_lockdelay*/ 0x0000) + +static bool usb_audio_direction_is_input_output(void) { + return usb_audio_direction == USB_AUDIO_DIRECTION_INPUT_OUTPUT; +} + +static bool usb_audio_direction_is_output(void) { + return usb_audio_direction == USB_AUDIO_DIRECTION_OUTPUT; } size_t usb_audio_descriptor_length(void) { + if (usb_audio_direction_is_input_output()) { + return USB_AUDIO_HEADSET_DESC_LEN; + } + if (usb_audio_direction_is_output()) { + return USB_AUDIO_SPEAKER_DESC_LEN; + } return TUD_AUDIO_MIC_ONE_CH_DESC_LEN; } size_t usb_audio_add_descriptor(uint8_t *descriptor_buf, descriptor_counts_t *descriptor_counts, uint8_t *current_interface_string) { + // Pick the isochronous endpoint number. By default it follows the same + // sequential allocation as every other interface. On ports that pin ISO to a + // fixed, dedicated endpoint (USB_AUDIO_ISO_EP_NUM != 0; see the header for the + // nRF52 case), we use that number instead and leave the sequential counters + // untouched: the dedicated ISO endpoint is a separate hardware resource, so + // it must not consume one of the regular endpoint numbers the other + // interfaces draw from. + const bool forced_iso_ep = (USB_AUDIO_ISO_EP_NUM != 0); + const uint8_t iso_ep_num = forced_iso_ep ? USB_AUDIO_ISO_EP_NUM : descriptor_counts->current_endpoint; + + if (usb_audio_direction_is_input_output()) { + // Combined headset: a speaker AudioStreaming interface (OUT) and a mic + // AudioStreaming interface (IN) under one AudioControl interface. This + // needs two isochronous endpoints, so it cannot be served by ports that + // pin ISO to a single dedicated endpoint number (forced_iso_ep, e.g. + // nRF52, which has only one ISO-capable endpoint). On those ports the + // sequential numbers below will not match the hardware's required ISO + // endpoint and the stream will not open; INPUT_OUTPUT is effectively + // unsupported there. The sequential-allocation ports (e.g. RP2) take a + // distinct number for each direction. + const uint8_t ep_out = descriptor_counts->current_endpoint; + const uint8_t ep_in = descriptor_counts->current_endpoint + 1; + + usb_add_interface_string(*current_interface_string, "CircuitPython Headset"); + const uint8_t usb_audio_descriptor[] = { + USB_AUDIO_HEADSET_DESCRIPTOR( + /*_itfnum*/ descriptor_counts->current_interface, + /*_stridx*/ *current_interface_string, + /*_nBytesPerSample*/ USB_AUDIO_N_BYTES_PER_SAMPLE, + /*_nBitsUsedPerSample*/ USB_AUDIO_BITS_PER_SAMPLE, + /*_epout*/ ep_out, + /*_epin*/ ep_in | 0x80, + /*_epsize*/ CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX) + }; + + // Speaker AS is the first interface after AudioControl, mic AS the second. + usb_audio_spk_as_itf = descriptor_counts->current_interface + 1; + usb_audio_mic_as_itf = descriptor_counts->current_interface + 2; + + (*current_interface_string)++; + // One IAD wrapping an AudioControl + two AudioStreaming interfaces, plus + // one OUT and one IN endpoint. + descriptor_counts->current_interface += 3; + descriptor_counts->num_out_endpoints++; + descriptor_counts->num_in_endpoints++; + descriptor_counts->current_endpoint += 2; + + memcpy(descriptor_buf, usb_audio_descriptor, sizeof(usb_audio_descriptor)); + + return sizeof(usb_audio_descriptor); + } + + if (usb_audio_direction_is_output()) { + usb_add_interface_string(*current_interface_string, "CircuitPython Speaker"); + const uint8_t usb_audio_descriptor[] = { + USB_AUDIO_SPEAKER_DESCRIPTOR( + /*_itfnum*/ descriptor_counts->current_interface, + /*_stridx*/ *current_interface_string, + /*_nBytesPerSample*/ USB_AUDIO_N_BYTES_PER_SAMPLE, + /*_nBitsUsedPerSample*/ USB_AUDIO_BITS_PER_SAMPLE, + /*_epout*/ iso_ep_num, + /*_epsize*/ CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX) + }; + + // The AudioStreaming interface follows the AudioControl interface. + usb_audio_spk_as_itf = descriptor_counts->current_interface + 1; + + (*current_interface_string)++; + // One IAD wrapping an AudioControl + an AudioStreaming interface, plus one OUT endpoint. + descriptor_counts->current_interface += 2; + if (!forced_iso_ep) { + descriptor_counts->num_out_endpoints++; + descriptor_counts->current_endpoint++; + } + + memcpy(descriptor_buf, usb_audio_descriptor, sizeof(usb_audio_descriptor)); + + return sizeof(usb_audio_descriptor); + } + usb_add_interface_string(*current_interface_string, "CircuitPython Microphone"); const uint8_t usb_audio_descriptor[] = { TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR( @@ -64,26 +276,60 @@ size_t usb_audio_add_descriptor(uint8_t *descriptor_buf, descriptor_counts_t *de /*_stridx*/ *current_interface_string, /*_nBytesPerSample*/ USB_AUDIO_N_BYTES_PER_SAMPLE, /*_nBitsUsedPerSample*/ USB_AUDIO_BITS_PER_SAMPLE, - /*_epin*/ descriptor_counts->current_endpoint | 0x80, + /*_epin*/ iso_ep_num | 0x80, /*_epsize*/ CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX) }; + // The AudioStreaming interface follows the AudioControl interface. + usb_audio_mic_as_itf = descriptor_counts->current_interface + 1; + (*current_interface_string)++; // One IAD wrapping an AudioControl + an AudioStreaming interface, plus one IN endpoint. descriptor_counts->current_interface += 2; - descriptor_counts->num_in_endpoints++; - descriptor_counts->current_endpoint++; + if (!forced_iso_ep) { + descriptor_counts->num_in_endpoints++; + descriptor_counts->current_endpoint++; + } memcpy(descriptor_buf, usb_audio_descriptor, sizeof(usb_audio_descriptor)); return sizeof(usb_audio_descriptor); } -void usb_audio_task(void) { - if (!usb_audio_is_streaming) { - return; +// --------------------------------------------------------------------+ +// Speaker (host -> board) receive task +// --------------------------------------------------------------------+ + +// Drain everything the host has delivered to the OUT endpoint since the last +// pass into the active USBSpeaker's ring. TinyUSB's weak rx-done handler has +// already moved the isochronous data into ep_out_ff; we copy it out here, in +// task (non-ISR) context, matching the project's "defer ISR work" rule. The ring +// itself, and the overrun/underrun handling, live in USBSpeaker.c so the data +// sits in the audiosample source the output backend pulls from. +static void usb_audio_speaker_task(void) { + // One USB packet of scratch; we loop until ep_out_ff is empty. + static uint8_t chunk[CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX]; + + while (tud_audio_available() > 0) { + uint16_t got = tud_audio_read(chunk, sizeof(chunk)); + if (got == 0) { + break; + } + // tud_audio_read() never returns more than the buffer it was given, but + // clamp so the compiler can prove the drain copies stay within chunk[] + // once this loop is inlined into it. + if (got > sizeof(chunk)) { + got = sizeof(chunk); + } + usb_audio_usbspeaker_background_drain(chunk, got); } +} + +// --------------------------------------------------------------------+ +// Microphone (board -> host) transmit task +// --------------------------------------------------------------------+ +static void usb_audio_microphone_task(void) { // Pace production by the IN FIFO level. Each pass we top the software FIFO // back up to its half-full setpoint, generating only the samples the host has // actually drained since the last pass. This limits our production rate to the @@ -131,22 +377,54 @@ void usb_audio_task(void) { } } +void usb_audio_task(void) { + // Each direction is gated on the host having opened its AudioStreaming + // interface. For a headset both run in the same pass: drain the host's + // speaker audio and refill the mic stream independently. The single-direction + // modes simply never have the other flag set. + if (usb_audio_spk_streaming) { + usb_audio_speaker_task(); + } + if (usb_audio_mic_streaming) { + usb_audio_microphone_task(); + } +} + // --------------------------------------------------------------------+ // TinyUSB audio class callbacks (weak symbols overridden here) // --------------------------------------------------------------------+ -// Host opened/closed the AudioStreaming alternate setting. +// Host opened/closed the AudioStreaming alternate setting. The host selects each +// direction's interface independently (a headset has two), so route by interface +// number to the matching streaming flag rather than assuming a single stream. bool tud_audio_set_itf_cb(uint8_t rhport, tusb_control_request_t const *p_request) { (void)rhport; - uint8_t const alt = (uint8_t)tu_u16_low(p_request->wValue); - usb_audio_is_streaming = (alt != 0); + uint8_t const itf = (uint8_t)tu_u16_low(p_request->wIndex); + bool const streaming = (tu_u16_low(p_request->wValue) != 0); + + if (itf == usb_audio_spk_as_itf) { + usb_audio_spk_streaming = streaming; + // Start each speaker streaming session from live audio: drop anything + // left in the OUT FIFO/ring from a previous session. + tud_audio_clear_ep_out_ff(); + usb_audio_usbspeaker_streaming_reset(); + } else if (itf == usb_audio_mic_as_itf) { + usb_audio_mic_streaming = streaming; + } return true; } bool tud_audio_set_itf_close_ep_cb(uint8_t rhport, tusb_control_request_t const *p_request) { (void)rhport; - (void)p_request; - usb_audio_is_streaming = false; + uint8_t const itf = (uint8_t)tu_u16_low(p_request->wIndex); + + if (itf == usb_audio_spk_as_itf) { + usb_audio_spk_streaming = false; + tud_audio_clear_ep_out_ff(); + usb_audio_usbspeaker_streaming_reset(); + } else if (itf == usb_audio_mic_as_itf) { + usb_audio_mic_streaming = false; + } return true; } @@ -161,7 +439,11 @@ bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p // Only current-value requests are supported. TU_VERIFY(p_request->bRequest == AUDIO_CS_REQ_CUR); - if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT) { + // A headset exposes a feature unit per direction; the speaker's id matches the + // single-direction USB_AUDIO_ENTITY_FEATURE_UNIT, the mic adds a second one. + // Mute/volume state is shared across them (mono, cosmetic for now). + if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT || + entityID == USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT) { if (channelNum > USB_AUDIO_N_CHANNELS) { return false; } @@ -191,8 +473,12 @@ bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p uint8_t const ctrlSel = (uint8_t)tu_u16_high(p_request->wValue); uint8_t const entityID = (uint8_t)tu_u16_high(p_request->wIndex); - // Input terminal (microphone). - if (entityID == USB_AUDIO_ENTITY_INPUT_TERMINAL) { + // Input terminal connector control. The single-mic descriptor uses + // USB_AUDIO_ENTITY_INPUT_TERMINAL; the headset's microphone input terminal is + // a distinct id. (The USB-streaming input terminals don't advertise a readable + // connector control, so the host won't query them here.) + if (entityID == USB_AUDIO_ENTITY_INPUT_TERMINAL || + entityID == USB_AUDIO_HS_ENTITY_MIC_INPUT_TERMINAL) { switch (ctrlSel) { case AUDIO_TE_CTRL_CONNECTOR: { audio_desc_channel_cluster_t ret; @@ -206,8 +492,9 @@ bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p } } - // Feature unit (mute/volume). - if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT) { + // Feature unit (mute/volume) for either the speaker or mic chain. + if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT || + entityID == USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT) { if (channelNum > USB_AUDIO_N_CHANNELS) { return false; } diff --git a/shared-module/usb_audio/__init__.h b/shared-module/usb_audio/__init__.h index c2ef25d0590a9..a275996f7a68f 100644 --- a/shared-module/usb_audio/__init__.h +++ b/shared-module/usb_audio/__init__.h @@ -13,24 +13,35 @@ #include "py/obj.h" #include "supervisor/usb.h" -// Enable/disable the USB Audio Class (UAC2) microphone interface. These may -// only be called before USB is connected (i.e. from boot.py); they return -// false otherwise. -bool shared_module_usb_audio_enable(mp_int_t sample_rate, mp_int_t channel_count, mp_int_t bits_per_sample); +#include "shared-bindings/usb_audio/Direction.h" + +// Enable/disable the USB Audio Class (UAC2) interface. These may only be +// called before USB is connected (i.e. from boot.py); they return false +// otherwise. +bool shared_module_usb_audio_enable(mp_int_t sample_rate, mp_int_t channel_count, mp_int_t bits_per_sample, usb_audio_direction_t direction); bool shared_module_usb_audio_disable(void); // True once enable() has been called successfully. bool usb_audio_enabled(void); -// True while the host has opened the AudioStreaming alternate setting, i.e. it is -// actively listening. This is the real "stream the audio now" signal. +// True while the host has opened either AudioStreaming alternate setting, i.e. it +// is actively listening or sending. This is the real "stream the audio now" +// signal. bool usb_audio_streaming(void); +// True while the host has the speaker (host -> board OUT) stream open. Distinct +// from usb_audio_streaming() so USBSpeaker can report its own direction even when +// it shares a headset function with a microphone. +bool usb_audio_speaker_streaming(void); + // Negotiated audio format, valid when usb_audio_enabled() is true. extern uint32_t usb_audio_sample_rate; extern uint8_t usb_audio_channel_count; extern uint8_t usb_audio_bits_per_sample; +// Stream direction requested in enable(), valid when usb_audio_enabled() is true. +extern usb_audio_direction_t usb_audio_direction; + // Descriptor injection hooks, called from supervisor/shared/usb/usb_desc.c. size_t usb_audio_descriptor_length(void); size_t usb_audio_add_descriptor(uint8_t *descriptor_buf, descriptor_counts_t *descriptor_counts, uint8_t *current_interface_string); diff --git a/shared-module/usb_audio/usb_audio_descriptors.h b/shared-module/usb_audio/usb_audio_descriptors.h index e2124214d418a..20103b1907c1e 100644 --- a/shared-module/usb_audio/usb_audio_descriptors.h +++ b/shared-module/usb_audio/usb_audio_descriptors.h @@ -6,10 +6,24 @@ #pragma once +#include + // Fixed audio format for the UAC2 microphone profile. These must have no other // dependencies because this header is included from the TinyUSB tusb_config.h // (to size the IN endpoint) as well as from the descriptor/binding code. +// Actual length, in bytes, of the audio function descriptor emitted for the +// current direction (mic, speaker, or headset). Declared here -- in the +// dependency-free header tusb_config.h already includes -- because TinyUSB's +// audio class driver reads CFG_TUD_AUDIO_FUNC_1_DESC_LEN at enumeration time and +// returns it to the device core as the number of configuration-descriptor bytes +// the function owns. That value MUST equal the descriptor we actually emitted: +// the three directions differ in length, so a compile-time maximum would over- +// report for the shorter ones and make the core swallow the interfaces that +// follow audio (CDC/MSC), breaking their enumeration. The full definition lives +// in __init__.c (also declared in __init__.h for the descriptor builder). +size_t usb_audio_descriptor_length(void); + // The isochronous IN endpoint's wMaxPacketSize in the USB descriptor is computed // for this rate, so it is the highest rate usb_audio.enable() will accept. #define USB_AUDIO_MAX_SAMPLE_RATE (48000) @@ -19,8 +33,95 @@ #define USB_AUDIO_N_CHANNELS (1) #define USB_AUDIO_BITS_PER_SAMPLE (USB_AUDIO_N_BYTES_PER_SAMPLE * 8) -// Fixed UAC2 entity IDs baked into TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR. +// Endpoint number for the single isochronous audio data endpoint. Most device +// controllers accept an ISO endpoint on any number, so the descriptor builder +// just takes the next sequential one (signalled by 0 here). The nRF52 USBD, +// however, implements isochronous transfers only on a fixed, dedicated endpoint +// number (8): its dcd_edpt_open() rejects any other number for an ISO endpoint, +// so the stream silently never opens and the host sees a mic/speaker that +// transfers no data. Such ports override this (e.g. -DUSB_AUDIO_ISO_EP_NUM=8 in +// the port's mpconfigport.mk) to force the audio endpoint onto that number. +#ifndef USB_AUDIO_ISO_EP_NUM +#define USB_AUDIO_ISO_EP_NUM (0) +#endif + +// Fixed UAC2 entity IDs baked into TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR and the +// hand-rolled speaker descriptor (USB_AUDIO_SPEAKER_DESCRIPTOR in __init__.c). +// The speaker reuses the same IDs as the mic; only the terminal roles reverse +// (input terminal = USB streaming, output terminal = desktop speaker). #define USB_AUDIO_ENTITY_INPUT_TERMINAL (0x01) #define USB_AUDIO_ENTITY_FEATURE_UNIT (0x02) #define USB_AUDIO_ENTITY_OUTPUT_TERMINAL (0x03) #define USB_AUDIO_ENTITY_CLOCK_SOURCE (0x04) + +// Combined headset (Direction.INPUT_OUTPUT) topology. A single audio function +// carries both a speaker chain (host -> board) and a mic chain (board -> host), +// so every unit/terminal needs an ID unique across the whole function -- unlike +// the single-direction descriptors above, which can reuse the same small set. +// The clock source is shared by both chains. (USB_AUDIO_HEADSET_DESCRIPTOR in +// __init__.c bakes these in.) +#define USB_AUDIO_HS_ENTITY_CLOCK_SOURCE (0x04) +#define USB_AUDIO_HS_ENTITY_SPK_INPUT_TERMINAL (0x01) // USB streaming in from host +#define USB_AUDIO_HS_ENTITY_SPK_FEATURE_UNIT (0x02) +#define USB_AUDIO_HS_ENTITY_SPK_OUTPUT_TERMINAL (0x03) // desktop speaker +#define USB_AUDIO_HS_ENTITY_MIC_INPUT_TERMINAL (0x05) // generic microphone +#define USB_AUDIO_HS_ENTITY_MIC_FEATURE_UNIT (0x06) +#define USB_AUDIO_HS_ENTITY_MIC_OUTPUT_TERMINAL (0x07) // USB streaming out to host + +// Length of the no-feedback mono speaker descriptor. It uses the same set of +// sub-descriptors as TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR (one isochronous data +// endpoint, no feedback endpoint), so this is identical to +// TUD_AUDIO_MIC_ONE_CH_DESC_LEN -- but spell it out independently so the two +// can diverge later (e.g. stereo) without silently mis-sizing the descriptor. +// These TUD_AUDIO_DESC_*_LEN macros come from TinyUSB's usbd.h; this expression +// is only expanded where that header is already included (never at the point +// tusb_config.h includes us), so the header stays dependency-free. +#define USB_AUDIO_SPEAKER_DESC_LEN (TUD_AUDIO_DESC_IAD_LEN \ + + TUD_AUDIO_DESC_STD_AC_LEN \ + + TUD_AUDIO_DESC_CS_AC_LEN \ + + TUD_AUDIO_DESC_CLK_SRC_LEN \ + + TUD_AUDIO_DESC_INPUT_TERM_LEN \ + + TUD_AUDIO_DESC_OUTPUT_TERM_LEN \ + + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL_LEN \ + + TUD_AUDIO_DESC_STD_AS_INT_LEN \ + + TUD_AUDIO_DESC_STD_AS_INT_LEN \ + + TUD_AUDIO_DESC_CS_AS_INT_LEN \ + + TUD_AUDIO_DESC_TYPE_I_FORMAT_LEN \ + + TUD_AUDIO_DESC_STD_AS_ISO_EP_LEN \ + + TUD_AUDIO_DESC_CS_AS_ISO_EP_LEN) + +// Length of the combined headset descriptor (Direction.INPUT_OUTPUT): one IAD +// wrapping a single AudioControl interface plus two AudioStreaming interfaces +// (speaker OUT + mic IN). The AC interface declares one shared clock, plus an +// input terminal / feature unit / output terminal for each of the two chains; +// each AS interface contributes a zero-bandwidth alt 0 and a streaming alt 1 +// with one isochronous data endpoint (no feedback endpoint, matching the +// single-direction descriptors). See USB_AUDIO_HEADSET_DESCRIPTOR in __init__.c. +// Expanded only where TinyUSB's usbd.h is already included (never at the point +// tusb_config.h includes us), so this header stays dependency-free. +#define USB_AUDIO_HEADSET_DESC_LEN (TUD_AUDIO_DESC_IAD_LEN \ + + TUD_AUDIO_DESC_STD_AC_LEN \ + + TUD_AUDIO_DESC_CS_AC_LEN \ + + TUD_AUDIO_DESC_CLK_SRC_LEN \ + /* speaker chain: USB-streaming input terminal -> feature unit -> speaker */ \ + + TUD_AUDIO_DESC_INPUT_TERM_LEN \ + + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL_LEN \ + + TUD_AUDIO_DESC_OUTPUT_TERM_LEN \ + /* mic chain: microphone input terminal -> feature unit -> USB-streaming out */ \ + + TUD_AUDIO_DESC_INPUT_TERM_LEN \ + + TUD_AUDIO_DESC_FEATURE_UNIT_ONE_CHANNEL_LEN \ + + TUD_AUDIO_DESC_OUTPUT_TERM_LEN \ + /* speaker AudioStreaming interface (alt 0 + alt 1 with OUT endpoint) */ \ + + TUD_AUDIO_DESC_STD_AS_INT_LEN \ + + TUD_AUDIO_DESC_STD_AS_INT_LEN \ + + TUD_AUDIO_DESC_CS_AS_INT_LEN \ + + TUD_AUDIO_DESC_TYPE_I_FORMAT_LEN \ + + TUD_AUDIO_DESC_STD_AS_ISO_EP_LEN \ + + TUD_AUDIO_DESC_CS_AS_ISO_EP_LEN \ + /* mic AudioStreaming interface (alt 0 + alt 1 with IN endpoint) */ \ + + TUD_AUDIO_DESC_STD_AS_INT_LEN \ + + TUD_AUDIO_DESC_STD_AS_INT_LEN \ + + TUD_AUDIO_DESC_CS_AS_INT_LEN \ + + TUD_AUDIO_DESC_TYPE_I_FORMAT_LEN \ + + TUD_AUDIO_DESC_STD_AS_ISO_EP_LEN \ + + TUD_AUDIO_DESC_CS_AS_ISO_EP_LEN) diff --git a/supervisor/shared/usb/tusb_config.h b/supervisor/shared/usb/tusb_config.h index 37a8a670b6b04..a10cd71222677 100644 --- a/supervisor/shared/usb/tusb_config.h +++ b/supervisor/shared/usb/tusb_config.h @@ -122,9 +122,21 @@ extern "C" { #if CIRCUITPY_USB_AUDIO #include "shared-module/usb_audio/usb_audio_descriptors.h" -// Single audio function: 1 AudioStreaming interface, 1 isochronous IN endpoint. -#define CFG_TUD_AUDIO_FUNC_1_DESC_LEN TUD_AUDIO_MIC_ONE_CH_DESC_LEN -#define CFG_TUD_AUDIO_FUNC_1_N_AS_INT 1 +// Single audio function. The emitted descriptor is chosen at boot from the +// stored direction: a mic (1 AS interface, IN endpoint), a speaker (1 AS +// interface, OUT endpoint), or a combined headset (2 AS interfaces, one IN and +// one OUT endpoint). The class driver returns this length to the device core as +// the span of config descriptor the function owns, so it must equal the +// descriptor we actually emitted -- the three directions differ in length, so a +// compile-time maximum would over-report for the shorter ones and make the core +// skip the interfaces that follow audio. usb_audio_descriptor_length() returns +// the live length for the stored direction; it is only read at enumeration time +// (audiod_open), by which point usb_audio.enable() has fixed the direction. +#define CFG_TUD_AUDIO_FUNC_1_DESC_LEN usb_audio_descriptor_length() +// The headset presents two AudioStreaming interfaces; the single-direction +// descriptors use only the first. The class driver sizes its per-interface alt +// tracking from this, so it must cover the largest case. +#define CFG_TUD_AUDIO_FUNC_1_N_AS_INT 2 // EP0 buffer for class-specific control requests (sample-freq range, volume range, ...). #define CFG_TUD_AUDIO_FUNC_1_CTRL_BUF_SZ 64 @@ -135,6 +147,18 @@ extern "C" { #define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX TUD_AUDIO_EP_SIZE(USB_AUDIO_MAX_SAMPLE_RATE, USB_AUDIO_N_BYTES_PER_SAMPLE, USB_AUDIO_N_CHANNELS) // Deep software FIFO so the 1 ms refill keeps clear of the underrun floor. #define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ (16 * CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX) + +// OUT endpoint (host -> board speaker). Compiled into the class driver +// unconditionally; whether it actually enumerates is decided by the emitted +// descriptor (still mic-only until the speaker descriptor lands). Mirrors the +// IN sizing above. +#define CFG_TUD_AUDIO_ENABLE_EP_OUT 1 +#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_RX USB_AUDIO_N_BYTES_PER_SAMPLE +#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX USB_AUDIO_N_CHANNELS +// wMaxPacketSize, sized for the highest supported sample rate. +#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX TUD_AUDIO_EP_SIZE(USB_AUDIO_MAX_SAMPLE_RATE, USB_AUDIO_N_BYTES_PER_SAMPLE, USB_AUDIO_N_CHANNELS) +// Deep software FIFO so the 1 ms drain keeps clear of the overrun ceiling. +#define CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ (16 * CFG_TUD_AUDIO_FUNC_1_EP_OUT_SZ_MAX) #endif /*------------------------------------------------------------------*/ diff --git a/supervisor/supervisor.mk b/supervisor/supervisor.mk index 23ae866242ca9..63c0b903b1cf1 100644 --- a/supervisor/supervisor.mk +++ b/supervisor/supervisor.mk @@ -185,9 +185,12 @@ ifeq ($(CIRCUITPY_TINYUSB),1) ifeq ($(CIRCUITPY_USB_AUDIO), 1) SRC_SUPERVISOR += \ shared-bindings/usb_audio/__init__.c \ + shared-bindings/usb_audio/Direction.c \ shared-module/usb_audio/__init__.c \ shared-bindings/usb_audio/USBMicrophone.c \ shared-module/usb_audio/USBMicrophone.c \ + shared-bindings/usb_audio/USBSpeaker.c \ + shared-module/usb_audio/USBSpeaker.c \ lib/tinyusb/src/class/audio/audio_device.c \ # The CFG_TUD_AUDIO_* class driver settings are defined in