diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index eefb0f3dc22e8..fa088fdda5e1f 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -775,9 +775,9 @@ msgstr "" msgid "Can't set CCCD on local Characteristic" msgstr "" -#: shared-bindings/storage/__init__.c shared-bindings/usb_cdc/__init__.c -#: shared-bindings/usb_hid/__init__.c shared-bindings/usb_midi/__init__.c -#: shared-bindings/usb_video/__init__.c +#: shared-bindings/storage/__init__.c shared-bindings/usb_audio/__init__.c +#: shared-bindings/usb_cdc/__init__.c shared-bindings/usb_hid/__init__.c +#: shared-bindings/usb_midi/__init__.c shared-bindings/usb_video/__init__.c msgid "Cannot change USB devices now" msgstr "" @@ -1686,6 +1686,7 @@ msgstr "" #: shared-bindings/audiobusio/I2SOut.c shared-bindings/audioio/AudioOut.c #: shared-bindings/audiopwmio/PWMAudioOut.c shared-bindings/mcp4822/MCP4822.c +#: shared-bindings/usb_audio/USBMicrophone.c msgid "Not playing" msgstr "" @@ -2148,7 +2149,7 @@ msgstr "" msgid "The length of rgb_pins must be 6, 12, 18, 24, or 30" msgstr "" -#: shared-module/audiocore/__init__.c +#: shared-module/audiocore/__init__.c shared-module/usb_audio/USBMicrophone.c msgid "The sample's %q does not match" msgstr "" @@ -2270,6 +2271,10 @@ msgstr "" msgid "UID:" msgstr "" +#: shared-bindings/usb_audio/USBMicrophone.c +msgid "USB audio not enabled in boot.py" +msgstr "" + #: shared-module/usb_hid/Device.c msgid "USB busy" msgstr "" diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 76f468beee65b..c3fc727d88990 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -26,6 +26,7 @@ CIRCUITPY_ROTARYIO_SOFTENCODER = 1 CIRCUITPY_SYNTHIO_MAX_CHANNELS = 24 CIRCUITPY_USB_HOST ?= 1 CIRCUITPY_USB_VIDEO ?= 1 +CIRCUITPY_USB_AUDIO ?= 1 # Things that need to be implemented. CIRCUITPY_FREQUENCYIO = 0 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 67be45325d565..67801216c4b48 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -450,6 +450,9 @@ endif ifeq ($(CIRCUITPY_USB_VIDEO),1) SRC_PATTERNS += usb_video/% endif +ifeq ($(CIRCUITPY_USB_AUDIO),1) +SRC_PATTERNS += usb_audio/% +endif ifeq ($(CIRCUITPY_USB_HOST),1) SRC_PATTERNS += usb_host/% endif diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index f56813f3b9174..d8c15ba94402e 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -671,6 +671,9 @@ CFLAGS += -DCIRCUITPY_USB_HID_ENABLED_DEFAULT=$(CIRCUITPY_USB_HID_ENABLED_DEFAUL CIRCUITPY_USB_VIDEO ?= 0 CFLAGS += -DCIRCUITPY_USB_VIDEO=$(CIRCUITPY_USB_VIDEO) +CIRCUITPY_USB_AUDIO ?= 0 +CFLAGS += -DCIRCUITPY_USB_AUDIO=$(CIRCUITPY_USB_AUDIO) + CIRCUITPY_USB_HOST ?= 0 CFLAGS += -DCIRCUITPY_USB_HOST=$(CIRCUITPY_USB_HOST) diff --git a/shared-bindings/usb_audio/USBMicrophone.c b/shared-bindings/usb_audio/USBMicrophone.c new file mode 100644 index 0000000000000..2ccf7b2d17b6f --- /dev/null +++ b/shared-bindings/usb_audio/USBMicrophone.c @@ -0,0 +1,197 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for 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/USBMicrophone.h" +#include "shared-bindings/util.h" +#include "shared-module/usb_audio/__init__.h" + +//| class USBMicrophone: +//| """Streams an audio sample to the host computer as a USB Audio Class microphone. +//| +//| A ``USBMicrophone`` is a *consumer* of an audio sample, exactly like +//| `audioio.AudioOut`, `audiobusio.I2SOut` and `audiopwmio.PWMAudioOut`. The +//| samples it pulls are streamed to the host PC over USB rather than to a pin, +//| so the board appears as a microphone. +//| +//| ``usb_audio.enable()`` must have been called in ``boot.py`` before this object +//| can be constructed.""" +//| +//| def __init__(self) -> None: +//| """Create a USBMicrophone using the audio format configured in ``boot.py``.""" +//| ... +//| +static mp_obj_t usb_audio_usbmicrophone_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 is chosen by usb_audio.enable() in boot.py, which also claims + // the USB interface. Without it there is no microphone for the host to read. + if (!usb_audio_enabled()) { + mp_raise_RuntimeError(MP_ERROR_TEXT("USB audio not enabled in boot.py")); + } + + usb_audio_usbmicrophone_obj_t *self = mp_obj_malloc_with_finaliser(usb_audio_usbmicrophone_obj_t, &usb_audio_USBMicrophone_type); + common_hal_usb_audio_usbmicrophone_construct(self); + + return MP_OBJ_FROM_PTR(self); +} + +//| def deinit(self) -> None: +//| """Deinitialises the USBMicrophone and releases any resources for reuse.""" +//| ... +//| +static mp_obj_t usb_audio_usbmicrophone_deinit(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_usb_audio_usbmicrophone_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_deinit_obj, usb_audio_usbmicrophone_deinit); + +static void check_for_deinit(usb_audio_usbmicrophone_obj_t *self) { + if (common_hal_usb_audio_usbmicrophone_deinited(self)) { + raise_deinited_error(); + } +} + +//| def __enter__(self) -> USBMicrophone: +//| """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. + +//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None: +//| """Streams the sample to the host once when loop=False and continuously when +//| loop=True. Does not block. Use `playing` to block. +//| +//| Sample must be an `audiocore.WaveFile`, `audiocore.RawSample`, `audiomixer.Mixer`, +//| `audiomp3.MP3Decoder` or `synthio.Synthesizer`.""" +//| ... +//| +static mp_obj_t usb_audio_usbmicrophone_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} }, + }; + usb_audio_usbmicrophone_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_usb_audio_usbmicrophone_play(self, args[ARG_sample].u_obj, args[ARG_loop].u_bool); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(usb_audio_usbmicrophone_play_obj, 1, usb_audio_usbmicrophone_obj_play); + +//| def stop(self) -> None: +//| """Stops streaming and resets to the start of the sample.""" +//| ... +//| +static mp_obj_t usb_audio_usbmicrophone_obj_stop(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_usb_audio_usbmicrophone_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_stop_obj, usb_audio_usbmicrophone_obj_stop); + +//| playing: bool +//| """True when an audio sample is being streamed even if `paused`. (read-only)""" +//| +static mp_obj_t usb_audio_usbmicrophone_obj_get_playing(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_usb_audio_usbmicrophone_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_get_playing_obj, usb_audio_usbmicrophone_obj_get_playing); + +MP_PROPERTY_GETTER(usb_audio_usbmicrophone_playing_obj, + (mp_obj_t)&usb_audio_usbmicrophone_get_playing_obj); + +//| def pause(self) -> None: +//| """Stops streaming temporarily while remembering the position. Use `resume` to resume.""" +//| ... +//| +static mp_obj_t usb_audio_usbmicrophone_obj_pause(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + if (!common_hal_usb_audio_usbmicrophone_get_playing(self)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Not playing")); + } + common_hal_usb_audio_usbmicrophone_pause(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_pause_obj, usb_audio_usbmicrophone_obj_pause); + +//| def resume(self) -> None: +//| """Resumes streaming after :py:func:`pause`.""" +//| ... +//| +static mp_obj_t usb_audio_usbmicrophone_obj_resume(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + if (common_hal_usb_audio_usbmicrophone_get_paused(self)) { + common_hal_usb_audio_usbmicrophone_resume(self); + } + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_resume_obj, usb_audio_usbmicrophone_obj_resume); + +//| paused: bool +//| """True when streaming is paused. (read-only)""" +//| +//| +static mp_obj_t usb_audio_usbmicrophone_obj_get_paused(mp_obj_t self_in) { + usb_audio_usbmicrophone_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_usb_audio_usbmicrophone_get_paused(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(usb_audio_usbmicrophone_get_paused_obj, usb_audio_usbmicrophone_obj_get_paused); + +MP_PROPERTY_GETTER(usb_audio_usbmicrophone_paused_obj, + (mp_obj_t)&usb_audio_usbmicrophone_get_paused_obj); + +static const mp_rom_map_elem_t usb_audio_usbmicrophone_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&usb_audio_usbmicrophone_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&usb_audio_usbmicrophone_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(&usb_audio_usbmicrophone_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&usb_audio_usbmicrophone_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_pause), MP_ROM_PTR(&usb_audio_usbmicrophone_pause_obj) }, + { MP_ROM_QSTR(MP_QSTR_resume), MP_ROM_PTR(&usb_audio_usbmicrophone_resume_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&usb_audio_usbmicrophone_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_paused), MP_ROM_PTR(&usb_audio_usbmicrophone_paused_obj) }, +}; +static MP_DEFINE_CONST_DICT(usb_audio_usbmicrophone_locals_dict, usb_audio_usbmicrophone_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + usb_audio_USBMicrophone_type, + MP_QSTR_USBMicrophone, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, usb_audio_usbmicrophone_make_new, + locals_dict, &usb_audio_usbmicrophone_locals_dict + ); diff --git a/shared-bindings/usb_audio/USBMicrophone.h b/shared-bindings/usb_audio/USBMicrophone.h new file mode 100644 index 0000000000000..4fe096778bc47 --- /dev/null +++ b/shared-bindings/usb_audio/USBMicrophone.h @@ -0,0 +1,21 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/usb_audio/USBMicrophone.h" + +extern const mp_obj_type_t usb_audio_USBMicrophone_type; + +void common_hal_usb_audio_usbmicrophone_construct(usb_audio_usbmicrophone_obj_t *self); +void common_hal_usb_audio_usbmicrophone_deinit(usb_audio_usbmicrophone_obj_t *self); +bool common_hal_usb_audio_usbmicrophone_deinited(usb_audio_usbmicrophone_obj_t *self); +void common_hal_usb_audio_usbmicrophone_play(usb_audio_usbmicrophone_obj_t *self, mp_obj_t sample, bool loop); +void common_hal_usb_audio_usbmicrophone_stop(usb_audio_usbmicrophone_obj_t *self); +bool common_hal_usb_audio_usbmicrophone_get_playing(usb_audio_usbmicrophone_obj_t *self); +void common_hal_usb_audio_usbmicrophone_pause(usb_audio_usbmicrophone_obj_t *self); +void common_hal_usb_audio_usbmicrophone_resume(usb_audio_usbmicrophone_obj_t *self); +bool common_hal_usb_audio_usbmicrophone_get_paused(usb_audio_usbmicrophone_obj_t *self); diff --git a/shared-bindings/usb_audio/__init__.c b/shared-bindings/usb_audio/__init__.c new file mode 100644 index 0000000000000..d6b5e15f02e37 --- /dev/null +++ b/shared-bindings/usb_audio/__init__.c @@ -0,0 +1,98 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/usb_audio/__init__.h" +#include "shared-bindings/usb_audio/USBMicrophone.h" +#include "shared-module/usb_audio/__init__.h" +#include "shared-module/usb_audio/usb_audio_descriptors.h" + +//| """Stream audio to a host computer via USB +//| +//| This makes your CircuitPython device identify to the host computer as a USB +//| Audio Class (UAC2) microphone: the board is the audio *source* and streams +//| samples to the host over a USB isochronous IN endpoint. +//| +//| This mode requires 1 IN endpoint and 2 interfaces. Generally, microcontrollers +//| have a limit on the number of endpoints. If you exceed the number of endpoints, +//| CircuitPython will automatically enter Safe Mode. Even in this case, you may be +//| able to enable USB audio by also disabling other USB functions, such as +//| `usb_hid` or `usb_midi`. +//| +//| To enable this mode, you must configure the audio format in ``boot.py`` and then +//| create a `USBMicrophone` in ``code.py``. +//| +//| .. code-block:: py +//| +//| # boot.py +//| import usb_audio +//| usb_audio.enable(sample_rate=16000, channel_count=1, bits_per_sample=16) +//| +//| .. code-block:: py +//| +//| # code.py +//| import usb_audio +//| import synthio +//| +//| mic = usb_audio.USBMicrophone() +//| synth = synthio.Synthesizer(sample_rate=16000, channel_count=1) +//| mic.play(synth, loop=True) +//| synth.press(60) +//| +//| """ +//| +//| + +//| def enable( +//| sample_rate: int = 16000, channel_count: int = 1, bits_per_sample: int = 16 +//| ) -> None: +//| """Enable the USB audio microphone 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.""" +//| +//| +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 }; + 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_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); + + 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); + + if (!shared_module_usb_audio_enable(sample_rate, channel_count, bits_per_sample)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Cannot change USB devices now")); + } + + return mp_const_none; +} +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_USBMicrophone), MP_ROM_PTR(&usb_audio_USBMicrophone_type) }, + { MP_ROM_QSTR(MP_QSTR_enable), MP_ROM_PTR(&usb_audio_enable_obj) }, +}; + +static MP_DEFINE_CONST_DICT(usb_audio_module_globals, usb_audio_module_globals_table); + +const mp_obj_module_t usb_audio_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&usb_audio_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_usb_audio, usb_audio_module); diff --git a/shared-bindings/usb_audio/__init__.h b/shared-bindings/usb_audio/__init__.h new file mode 100644 index 0000000000000..b0c8d2b920563 --- /dev/null +++ b/shared-bindings/usb_audio/__init__.h @@ -0,0 +1,9 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/usb_audio/__init__.h" diff --git a/shared-module/usb_audio/USBMicrophone.c b/shared-module/usb_audio/USBMicrophone.c new file mode 100644 index 0000000000000..fd5309abeb90b --- /dev/null +++ b/shared-module/usb_audio/USBMicrophone.c @@ -0,0 +1,149 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/misc.h" +#include "py/runtime.h" + +#include "shared-bindings/usb_audio/USBMicrophone.h" +#include "shared-module/usb_audio/__init__.h" +#include "shared-module/audiocore/__init__.h" + +// Only one microphone may feed the single USB IN endpoint at a time. This points +// at the USBMicrophone whose play() was called most recently, or NULL when none +// is streaming. The USB background task pulls from it via +// usb_audio_usbmicrophone_background_fill(). +static usb_audio_usbmicrophone_obj_t *active_microphone = NULL; + +void common_hal_usb_audio_usbmicrophone_construct(usb_audio_usbmicrophone_obj_t *self) { + self->sample = MP_OBJ_NULL; + self->buffer = NULL; + self->buffer_length = 0; + self->loop = false; + self->playing = false; + self->paused = false; + self->deinited = false; + self->more_data = false; +} + +void common_hal_usb_audio_usbmicrophone_deinit(usb_audio_usbmicrophone_obj_t *self) { + common_hal_usb_audio_usbmicrophone_stop(self); + self->deinited = true; +} + +bool common_hal_usb_audio_usbmicrophone_deinited(usb_audio_usbmicrophone_obj_t *self) { + return self->deinited; +} + +void common_hal_usb_audio_usbmicrophone_play(usb_audio_usbmicrophone_obj_t *self, mp_obj_t sample, bool loop) { + // The negotiated USB format is fixed 16-bit signed LE PCM at the rate/channel + // count chosen by usb_audio.enable() in boot.py. Resampling and format + // conversion are out of scope, so the source must already match exactly. This + // mirrors how audiocore's audiosample_must_match() validates an output's input + // (and reuses its error messages), but checks against the USB format rather + // than another audiosample. + audiosample_base_t *sample_base = audiosample_check(sample); + if (audiosample_get_sample_rate(sample_base) != usb_audio_sample_rate) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_sample_rate); + } + if (audiosample_get_channel_count(sample_base) != usb_audio_channel_count) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_channel_count); + } + if (audiosample_get_bits_per_sample(sample_base) != usb_audio_bits_per_sample) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_bits_per_sample); + } + if (!sample_base->samples_signed) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("The sample's %q does not match"), MP_QSTR_signedness); + } + + // Start at the beginning and arm the pull loop to request the first chunk. + audiosample_reset_buffer(sample, false, 0); + self->sample = sample; + self->buffer = NULL; + self->buffer_length = 0; + self->loop = loop; + self->playing = true; + self->paused = false; + self->more_data = true; + // Take over the single USB IN endpoint from any other microphone. + active_microphone = self; +} + +void common_hal_usb_audio_usbmicrophone_stop(usb_audio_usbmicrophone_obj_t *self) { + self->sample = MP_OBJ_NULL; + self->buffer = NULL; + self->buffer_length = 0; + self->playing = false; + self->paused = false; + self->more_data = false; + if (active_microphone == self) { + active_microphone = NULL; + } +} + +bool common_hal_usb_audio_usbmicrophone_get_playing(usb_audio_usbmicrophone_obj_t *self) { + return self->playing && !self->paused; +} + +void common_hal_usb_audio_usbmicrophone_pause(usb_audio_usbmicrophone_obj_t *self) { + self->paused = true; +} + +void common_hal_usb_audio_usbmicrophone_resume(usb_audio_usbmicrophone_obj_t *self) { + self->paused = false; +} + +bool common_hal_usb_audio_usbmicrophone_get_paused(usb_audio_usbmicrophone_obj_t *self) { + return self->playing && self->paused; +} + +size_t usb_audio_usbmicrophone_background_fill(uint8_t *out, size_t max_bytes) { + usb_audio_usbmicrophone_obj_t *self = active_microphone; + if (self == NULL || !self->playing || self->paused || self->sample == MP_OBJ_NULL) { + return 0; + } + + // The negotiated USB format is 16-bit signed mono PCM. For this step the + // bound sample is assumed to already be in that format (e.g. a 16-bit signed + // mono audiocore.RawSample), so its bytes are copied straight through. + size_t filled = 0; + while (filled < max_bytes) { + if (self->buffer_length == 0) { + if (!self->more_data) { + // The previous chunk was the sample's last and we've played it + // out. Loop back to the start or stop. + if (self->loop) { + audiosample_reset_buffer(self->sample, false, 0); + self->more_data = true; + } else { + common_hal_usb_audio_usbmicrophone_stop(self); + break; + } + } + audioio_get_buffer_result_t result = + audiosample_get_buffer(self->sample, false, 0, &self->buffer, &self->buffer_length); + if (result == GET_BUFFER_ERROR) { + common_hal_usb_audio_usbmicrophone_stop(self); + break; + } + self->more_data = (result == GET_BUFFER_MORE_DATA); + if (self->buffer_length == 0) { + // No samples available right now (underrun); the caller fills the + // rest of the frame with silence and we retry next tick. + break; + } + } + + size_t n = MIN(self->buffer_length, max_bytes - filled); + memcpy(out + filled, self->buffer, n); + self->buffer += n; + self->buffer_length -= n; + filled += n; + } + + return filled; +} diff --git a/shared-module/usb_audio/USBMicrophone.h b/shared-module/usb_audio/USBMicrophone.h new file mode 100644 index 0000000000000..74f167e8e4358 --- /dev/null +++ b/shared-module/usb_audio/USBMicrophone.h @@ -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 LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +typedef struct usb_audio_usbmicrophone_obj { + mp_obj_base_t base; + // The audiosample currently bound for streaming, or MP_OBJ_NULL. + mp_obj_t sample; + // Cursor into the chunk last returned by audiosample_get_buffer(): the next + // unread byte and the number of bytes still unread in that chunk. + uint8_t *buffer; + uint32_t buffer_length; + bool loop; + bool playing; + bool paused; + bool deinited; + // False once audiosample_get_buffer() reported GET_BUFFER_DONE for the + // current chunk; the chunk is still played out, then we loop or stop. + bool more_data; +} usb_audio_usbmicrophone_obj_t; + +// Pull up to max_bytes of audio (in the negotiated USB PCM format) from whichever +// USBMicrophone is currently playing, writing it into out. Returns the number of +// bytes written, which is < max_bytes (possibly 0) when nothing is streaming or +// the source underruns; the caller pads the remainder with silence. Called from +// the USB background task in shared-module/usb_audio/__init__.c. +size_t usb_audio_usbmicrophone_background_fill(uint8_t *out, size_t max_bytes); diff --git a/shared-module/usb_audio/__init__.c b/shared-module/usb_audio/__init__.c new file mode 100644 index 0000000000000..1ce2c6691fdfe --- /dev/null +++ b/shared-module/usb_audio/__init__.c @@ -0,0 +1,275 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#include "shared-module/usb_audio/__init__.h" +#include "shared-module/usb_audio/USBMicrophone.h" +#include "shared-module/usb_audio/usb_audio_descriptors.h" + +#include + +#include "tusb.h" + +static bool usb_audio_is_enabled = false; +static bool usb_audio_is_streaming = false; + +uint32_t usb_audio_sample_rate; +uint8_t usb_audio_channel_count; +uint8_t usb_audio_bits_per_sample; + +// 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) { + if (tud_connected()) { + return false; + } + + usb_audio_sample_rate = sample_rate; + usb_audio_channel_count = channel_count; + usb_audio_bits_per_sample = bits_per_sample; + usb_audio_is_enabled = true; + + return true; +} + +bool shared_module_usb_audio_disable(void) { + if (tud_connected()) { + return false; + } + usb_audio_is_enabled = false; + return true; +} + +bool usb_audio_enabled(void) { + return usb_audio_is_enabled; +} + +bool usb_audio_streaming(void) { + return usb_audio_is_streaming; +} + +size_t usb_audio_descriptor_length(void) { + 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) { + usb_add_interface_string(*current_interface_string, "CircuitPython Microphone"); + const uint8_t usb_audio_descriptor[] = { + TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR( + /*_itfnum*/ descriptor_counts->current_interface, + /*_stridx*/ *current_interface_string, + /*_nBytesPerSample*/ USB_AUDIO_N_BYTES_PER_SAMPLE, + /*_nBitsUsedPerSample*/ USB_AUDIO_BITS_PER_SAMPLE, + /*_epin*/ descriptor_counts->current_endpoint | 0x80, + /*_epsize*/ CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX) + }; + + (*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++; + + 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; + } + + // 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 + // host's true consumption rate (its USB SOF / audio clock), keeps the FIFO + // around the level TinyUSB's flow control targets so it can send steady + // nominal-size packets, and automatically catches up after any scheduling gap + // (GCpause, other background work) in a single pass instead of underrunning. + // Underruns here showed up to the host as discrete sample-drop/insert splices, + // i.e. the erratic ticking on a held tone. + tu_fifo_t *ep_in_ff = tud_audio_get_ep_in_ff(); + uint16_t const target = tu_fifo_depth(ep_in_ff) / 2; + + // One scratch chunk (1 ms at the max rate); we loop until the FIFO reaches + // the setpoint. Sized for the highest rate enable() accepts. + static int16_t samples[USB_AUDIO_MAX_SAMPLE_RATE / 1000 * USB_AUDIO_N_CHANNELS]; + + uint16_t count; + while ((count = tu_fifo_count(ep_in_ff)) < target) { + size_t want = target - count; + if (want > sizeof(samples)) { + want = sizeof(samples); + } + + // Pull the next chunk from whichever USBMicrophone is playing. + bool underran = false; + size_t filled = usb_audio_usbmicrophone_background_fill((uint8_t *)samples, want); + if (filled == 0) { + // No source attached, paused, or fully drained: keep the endpoint + // alive with silence so the host never sees a starved stream. + memset((uint8_t *)samples, 0, want); + } else if (filled < want) { + // The source momentarily underran. Send just what it produced and + // let the FIFO cushion ride until it catches up next pass, rather + // than flooding the stream with a burst of silence. + want = filled; + underran = true; + } + + if (tud_audio_write((uint8_t *)samples, (uint16_t)want) == 0) { + break; // FIFO unexpectedly full / host not ready + } + if (underran) { + break; + } + } +} + +// --------------------------------------------------------------------+ +// TinyUSB audio class callbacks (weak symbols overridden here) +// --------------------------------------------------------------------+ + +// Host opened/closed the AudioStreaming alternate setting. +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); + 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; + return true; +} + +// Class-specific SET requests for an entity (we accept mute/volume on the feature unit). +bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request, uint8_t *pBuff) { + (void)rhport; + + uint8_t const channelNum = (uint8_t)tu_u16_low(p_request->wValue); + uint8_t const ctrlSel = (uint8_t)tu_u16_high(p_request->wValue); + uint8_t const entityID = (uint8_t)tu_u16_high(p_request->wIndex); + + // Only current-value requests are supported. + TU_VERIFY(p_request->bRequest == AUDIO_CS_REQ_CUR); + + if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT) { + if (channelNum > USB_AUDIO_N_CHANNELS) { + return false; + } + switch (ctrlSel) { + case AUDIO_FU_CTRL_MUTE: + TU_VERIFY(p_request->wLength == sizeof(audio_control_cur_1_t)); + usb_audio_mute[channelNum] = ((audio_control_cur_1_t *)pBuff)->bCur; + return true; + + case AUDIO_FU_CTRL_VOLUME: + TU_VERIFY(p_request->wLength == sizeof(audio_control_cur_2_t)); + usb_audio_volume[channelNum] = ((audio_control_cur_2_t *)pBuff)->bCur; + return true; + + default: + return false; + } + } + return false; +} + +// Class-specific GET requests for an entity (clock source, feature unit, input terminal). +bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request) { + (void)rhport; + + uint8_t const channelNum = (uint8_t)tu_u16_low(p_request->wValue); + 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) { + switch (ctrlSel) { + case AUDIO_TE_CTRL_CONNECTOR: { + audio_desc_channel_cluster_t ret; + ret.bNrChannels = USB_AUDIO_N_CHANNELS; + ret.bmChannelConfig = (audio_channel_config_t)0; + ret.iChannelNames = 0; + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &ret, sizeof(ret)); + } + default: + return false; + } + } + + // Feature unit (mute/volume). + if (entityID == USB_AUDIO_ENTITY_FEATURE_UNIT) { + if (channelNum > USB_AUDIO_N_CHANNELS) { + return false; + } + switch (ctrlSel) { + case AUDIO_FU_CTRL_MUTE: + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &usb_audio_mute[channelNum], sizeof(usb_audio_mute[channelNum])); + + case AUDIO_FU_CTRL_VOLUME: + switch (p_request->bRequest) { + case AUDIO_CS_REQ_CUR: + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &usb_audio_volume[channelNum], sizeof(usb_audio_volume[channelNum])); + + case AUDIO_CS_REQ_RANGE: { + audio_control_range_2_n_t(1) ret; + ret.wNumSubRanges = 1; + ret.subrange[0].bMin = -90 * 256; // -90 dB + ret.subrange[0].bMax = 90 * 256; // +90 dB + ret.subrange[0].bRes = 256; // 1 dB steps + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &ret, sizeof(ret)); + } + + default: + return false; + } + + default: + return false; + } + } + + // Clock source (sample rate set in usb_audio.enable()). + if (entityID == USB_AUDIO_ENTITY_CLOCK_SOURCE) { + switch (ctrlSel) { + case AUDIO_CS_CTRL_SAM_FREQ: + switch (p_request->bRequest) { + case AUDIO_CS_REQ_CUR: { + audio_control_cur_4_t cur = { .bCur = (int32_t)usb_audio_sample_rate }; + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &cur, sizeof(cur)); + } + + case AUDIO_CS_REQ_RANGE: { + audio_control_range_4_n_t(1) ret; + ret.wNumSubRanges = 1; + ret.subrange[0].bMin = (int32_t)usb_audio_sample_rate; + ret.subrange[0].bMax = (int32_t)usb_audio_sample_rate; + ret.subrange[0].bRes = 0; + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &ret, sizeof(ret)); + } + + default: + return false; + } + + case AUDIO_CS_CTRL_CLK_VALID: { + audio_control_cur_1_t cur = { .bCur = 1 }; + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, &cur, sizeof(cur)); + } + + default: + return false; + } + } + + return false; +} diff --git a/shared-module/usb_audio/__init__.h b/shared-module/usb_audio/__init__.h new file mode 100644 index 0000000000000..c2ef25d0590a9 --- /dev/null +++ b/shared-module/usb_audio/__init__.h @@ -0,0 +1,40 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#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); +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. +bool usb_audio_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; + +// 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); + +// Background task that streams samples to the host, called from +// supervisor/shared/usb/usb.c. +void usb_audio_task(void); diff --git a/shared-module/usb_audio/tusb_config.h b/shared-module/usb_audio/tusb_config.h new file mode 100644 index 0000000000000..d7bfb65b14868 --- /dev/null +++ b/shared-module/usb_audio/tusb_config.h @@ -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 LLC +// +// SPDX-License-Identifier: MIT + +#pragma once diff --git a/shared-module/usb_audio/usb_audio_descriptors.h b/shared-module/usb_audio/usb_audio_descriptors.h new file mode 100644 index 0000000000000..e2124214d418a --- /dev/null +++ b/shared-module/usb_audio/usb_audio_descriptors.h @@ -0,0 +1,26 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries LLC +// +// SPDX-License-Identifier: MIT + +#pragma once + +// 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. + +// 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) + +// 16-bit signed LE PCM, mono. +#define USB_AUDIO_N_BYTES_PER_SAMPLE (2) +#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. +#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) diff --git a/supervisor/shared/usb/tusb_config.h b/supervisor/shared/usb/tusb_config.h index f2d800b99127e..37a8a670b6b04 100644 --- a/supervisor/shared/usb/tusb_config.h +++ b/supervisor/shared/usb/tusb_config.h @@ -115,8 +115,28 @@ extern "C" { #define CFG_TUD_HID CIRCUITPY_USB_HID #define CFG_TUD_MIDI CIRCUITPY_USB_MIDI #define CFG_TUD_VENDOR CIRCUITPY_USB_VENDOR +#define CFG_TUD_AUDIO CIRCUITPY_USB_AUDIO #define CFG_TUD_CUSTOM_CLASS 0 +// ------------- AUDIO CLASS (UAC2 microphone) -------------// +#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 +// EP0 buffer for class-specific control requests (sample-freq range, volume range, ...). +#define CFG_TUD_AUDIO_FUNC_1_CTRL_BUF_SZ 64 + +#define CFG_TUD_AUDIO_ENABLE_EP_IN 1 +#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX USB_AUDIO_N_BYTES_PER_SAMPLE +#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX USB_AUDIO_N_CHANNELS +// wMaxPacketSize, sized for the highest supported sample rate. +#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) +#endif + /*------------------------------------------------------------------*/ /* CLASS DRIVER *------------------------------------------------------------------*/ diff --git a/supervisor/shared/usb/usb.c b/supervisor/shared/usb/usb.c index 5061fee00633e..e8a4422bb36fd 100644 --- a/supervisor/shared/usb/usb.c +++ b/supervisor/shared/usb/usb.c @@ -39,6 +39,10 @@ #include "shared-module/usb_video/__init__.h" #endif +#if CIRCUITPY_USB_AUDIO +#include "shared-module/usb_audio/__init__.h" +#endif + #endif #include "tusb.h" @@ -174,6 +178,9 @@ void usb_background(void) { #if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_VIDEO usb_video_task(); #endif + #if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_AUDIO + usb_audio_task(); + #endif } } diff --git a/supervisor/shared/usb/usb_desc.c b/supervisor/shared/usb/usb_desc.c index 9427171fe9b98..7a0bc4686bbab 100644 --- a/supervisor/shared/usb/usb_desc.c +++ b/supervisor/shared/usb/usb_desc.c @@ -32,6 +32,10 @@ #include "shared-module/usb_video/__init__.h" #endif +#if CIRCUITPY_USB_AUDIO +#include "shared-module/usb_audio/__init__.h" +#endif + #include "shared-bindings/microcontroller/Processor.h" @@ -170,6 +174,12 @@ static bool usb_build_configuration_descriptor(void) { } #endif + #if CIRCUITPY_USB_AUDIO + if (usb_audio_enabled()) { + total_descriptor_length += usb_audio_descriptor_length(); + } + #endif + // Now we know how big the configuration descriptor will be, so we can allocate space for it. configuration_descriptor = (uint8_t *)port_malloc(total_descriptor_length, @@ -259,6 +269,13 @@ static bool usb_build_configuration_descriptor(void) { descriptor_buf_remaining, &descriptor_counts, ¤t_interface_string); } #endif + + #if CIRCUITPY_USB_AUDIO + if (usb_audio_enabled()) { + descriptor_buf_remaining += usb_audio_add_descriptor( + descriptor_buf_remaining, &descriptor_counts, ¤t_interface_string); + } + #endif // Now we know how many interfaces have been used. configuration_descriptor[CONFIG_NUM_INTERFACES_INDEX] = descriptor_counts.current_interface; diff --git a/supervisor/supervisor.mk b/supervisor/supervisor.mk index bce47353f578f..23ae866242ca9 100644 --- a/supervisor/supervisor.mk +++ b/supervisor/supervisor.mk @@ -182,6 +182,18 @@ ifeq ($(CIRCUITPY_TINYUSB),1) CFLAGS += -DCFG_TUD_VIDEO=1 -DCFG_TUD_VIDEO_STREAMING=1 -DCFG_TUD_VIDEO_STREAMING_EP_BUFSIZE=256 -DCFG_TUD_VIDEO_STREAMING_BULK=1 endif + ifeq ($(CIRCUITPY_USB_AUDIO), 1) + SRC_SUPERVISOR += \ + shared-bindings/usb_audio/__init__.c \ + shared-module/usb_audio/__init__.c \ + shared-bindings/usb_audio/USBMicrophone.c \ + shared-module/usb_audio/USBMicrophone.c \ + lib/tinyusb/src/class/audio/audio_device.c \ + + # The CFG_TUD_AUDIO_* class driver settings are defined in + # supervisor/shared/usb/tusb_config.h, gated on CIRCUITPY_USB_AUDIO. + endif + ifeq ($(CIRCUITPY_USB_VENDOR), 1) SRC_SUPERVISOR += \