From 439015377976bb51f6d9ae6c3101b72385ecfb1a Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Thu, 16 Jul 2026 15:03:31 +0800 Subject: feat(audio): add USB Audio Host (UAC 1.0) support Add TinyUSB Host Audio class driver supporting UAC 1.0 devices. Features: - Support multiple Audio Streaming (AS) interfaces with independent format storage - Support both IN (Microphone) and OUT (Speaker) endpoints - Per-AS interface format info: channels, sample rate, bit resolution - Support Feature Unit volume control - Support sampling frequency get/set - Add host/audio_host example for STM32F407 discovery board - Support mono-to-stereo conversion for loopback Changes: - Add src/class/audio/audio_host.c and audio_host.h - Register AUDIO driver in usbh.c - Add CFG_TUH_AUDIO macro in tusb_option.h - Add host/audio_host example with CMake and Makefile build support Tested with Jabra USB headset (stereo speaker + mono microphone) on STM32F407 disco. --- examples/host/audio_host/src/app.h | 26 ++++ examples/host/audio_host/src/audio_app.c | 226 +++++++++++++++++++++++++++++ examples/host/audio_host/src/main.c | 73 ++++++++++ examples/host/audio_host/src/tusb_config.h | 103 +++++++++++++ 4 files changed, 428 insertions(+) create mode 100644 examples/host/audio_host/src/app.h create mode 100644 examples/host/audio_host/src/audio_app.c create mode 100644 examples/host/audio_host/src/main.c create mode 100644 examples/host/audio_host/src/tusb_config.h (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h new file mode 100644 index 000000000..a807aeaa6 --- /dev/null +++ b/examples/host/audio_host/src/app.h @@ -0,0 +1,26 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2025 TinyUSB contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#ifndef TUSB_TINYUSB_EXAMPLES_APP_H +#define TUSB_TINYUSB_EXAMPLES_APP_H + +#include +#include +#include + +void audio_app_task(void); + +#endif diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c new file mode 100644 index 000000000..ac7596d91 --- /dev/null +++ b/examples/host/audio_host/src/audio_app.c @@ -0,0 +1,226 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2025 TinyUSB contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include +#include "bsp/board_api.h" +#include "tusb.h" +#include "app.h" + +//--------------------------------------------------------------------+ +// MACRO TYPEDEF CONSTANT ENUM DECLARATION +//--------------------------------------------------------------------+ + +static bool audio_mounted = false; +static uint8_t audio_dev_addr = 0; +static uint8_t audio_idx = 0; +static uint8_t audio_ep_in = 0; +static uint8_t audio_ep_out = 0; +static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) +static uint8_t audio_mic_channels = 1; +static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state +static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state +static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer +static uint8_t audio_ac_itf = 0; // Audio Control interface number +static uint8_t audio_feature_unit_id = 0; // Feature Unit ID + +static uint8_t audio_rx_buffer[CFG_TUH_AUDIO_EPIN_BUFSIZE] __attribute__((aligned(4))); +static uint8_t audio_tx_buffer[CFG_TUH_AUDIO_EPOUT_BUFSIZE] __attribute__((aligned(4))); + +//--------------------------------------------------------------------+ +// Helper Functions +//--------------------------------------------------------------------+ + +// Mono (96 bytes, 48 samples) -> Stereo (192 bytes) +static void mono_to_stereo(const uint8_t *mono, uint8_t *stereo, uint16_t mono_samples) { + for (uint16_t i = 0; i < mono_samples; i++) { + // Copy 2 bytes (one int16 sample) to left channel + stereo[i * 4] = mono[i * 2]; + stereo[i * 4 + 1] = mono[i * 2 + 1]; + // Copy same 2 bytes to right channel + stereo[i * 4 + 2] = mono[i * 2]; + stereo[i * 4 + 3] = mono[i * 2 + 1]; + } +} + +// Print sampling frequency info for an AS interface +static void print_sampling_freq(const tuh_audio_as_info_t *as) { + if (as->sam_freq_type == 0) { + printf(" Sampling Freq: Continuous range %lu Hz - %lu Hz\r\n", (unsigned long)as->sam_freq_lower, + (unsigned long)as->sam_freq_upper); + } else { + printf(" Sampling Freq: Discrete, count=%u\r\n", as->sam_freq_type); + for (uint8_t j = 0; j < as->sam_freq_type && j < CFG_TUH_AUDIO_MAX_SAM_FREQ; j++) { + printf(" Freq[%u]: %lu Hz\r\n", j, (unsigned long)as->sam_freq[j]); + } + } +} + +// Print all AS interface info +static void print_as_interfaces(const tuh_audio_mount_cb_t *mount_cb_data) { + for (uint8_t i = 0; i < mount_cb_data->as_count; i++) { + const tuh_audio_as_info_t *as = &mount_cb_data->as_info[i]; + if (as->ep_dir == TUSB_DIR_IN) { + // Save microphone channel count for mono-to-stereo conversion + audio_mic_channels = as->num_channels; + printf(" --- Microphone (AS %u) ---\r\n", i); + printf(" IN EP: 0x%02x (max size: %u)\r\n", as->ep_addr, as->ep_size); + } else { + printf(" --- Speaker (AS %u) ---\r\n", i); + printf(" OUT EP: 0x%02x (max size: %u)\r\n", as->ep_addr, as->ep_size); + } + printf(" Interface: %u, Alt: %u\r\n", as->interface_num, as->alt_setting); + printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as->format_type, + as->num_channels, as->sub_frame_size, as->bit_resolution); + print_sampling_freq(as); + } +} + +// Find IN and OUT endpoints from AS interfaces, return IN sampling freq +static uint32_t find_audio_endpoints(const tuh_audio_mount_cb_t *mount_cb_data) { + uint32_t in_sam_freq = 0; + audio_ep_in = 0; + audio_ep_out = 0; + + for (uint8_t i = 0; i < mount_cb_data->as_count; i++) { + const tuh_audio_as_info_t *as = &mount_cb_data->as_info[i]; + if (as->ep_dir == TUSB_DIR_IN) { + audio_ep_in = as->ep_addr; + if (as->sam_freq_type > 0) { + in_sam_freq = as->sam_freq[0]; + } + } else { + audio_ep_out = as->ep_addr; + } + } + return in_sam_freq; +} + +// Set Feature Unit volume to un-mute +static void set_feature_unit_volume(void) { + if (audio_feature_unit_id == 0) { + return; + } + printf(" Setting Feature Unit %u volume to 0x0600\r\n", audio_feature_unit_id); + tuh_audio_feature_unit_set(audio_dev_addr, audio_ac_itf, audio_feature_unit_id, AUDIO10_FU_CTRL_VOLUME, 0, 0x0600, + NULL, 0); +} + +//--------------------------------------------------------------------+ +// Application Task +//--------------------------------------------------------------------+ +void audio_app_task(void) { + if (!audio_mounted || !audio_ready) { + return; + } + + if (!audio_rx_busy) { + if (tuh_audio_receive(audio_dev_addr, audio_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { + audio_rx_busy = true; + } + } +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + + +// Callback after IN sampling frequency is set +static void in_sampling_freq_set_cb(tuh_xfer_t *xfer) { + if (xfer->result != XFER_RESULT_SUCCESS) { + printf(" Sampling frequency set FAILED: result=%u\r\n", xfer->result); + return; + } + printf(" Sampling frequency set OK, ready for isochronous transfer\r\n"); + // Set Feature Unit volume to un-mute + set_feature_unit_volume(); + // Set OUT sampling frequency then send empty packet to kick-start device + tuh_audio_set_sampling_freq(audio_dev_addr, audio_ep_out, sampling_freq, NULL, 0); + audio_ready = true; +} + +void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data) { + if (!mount_cb_data) { + return; + } + + printf("Audio device mounted: idx=%u, daddr=%u, AS count=%u\r\n", idx, mount_cb_data->daddr, mount_cb_data->as_count); + + print_as_interfaces(mount_cb_data); + + // Feature Unit + if (mount_cb_data->feature_unit_id != 0) { + printf(" Feature Unit: ID=%u, SourceID=%u\r\n", mount_cb_data->feature_unit_id, + mount_cb_data->feature_unit_source_id); + } + + // Save device info + audio_dev_addr = mount_cb_data->daddr; + audio_idx = idx; + audio_mounted = true; + audio_ac_itf = mount_cb_data->bInterfaceNumber; + audio_feature_unit_id = mount_cb_data->feature_unit_id; + + // Find endpoints and IN sampling frequency + uint32_t in_sam_freq = find_audio_endpoints(mount_cb_data); + + // Set IN sampling frequency before starting isochronous transfer + if (audio_ep_in != 0 && in_sam_freq != 0) { + sampling_freq = in_sam_freq; + printf(" Setting IN sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); + tuh_audio_set_sampling_freq(mount_cb_data->daddr, audio_ep_in, sampling_freq, in_sampling_freq_set_cb, 0); + } +} + +// Invoked when device with Audio interface is un-mounted +void tuh_audio_umount_cb(uint8_t idx) { + printf("Audio device unmounted: idx=%u\r\n", idx); + if (audio_mounted && audio_idx == idx) { + audio_mounted = false; + audio_ready = false; + audio_rx_busy = false; + audio_tx_busy = false; + audio_dev_addr = 0; + audio_idx = 0; + } +} + +// Invoked when an isochronous IN transfer is complete +void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)idx; + (void)ep_addr; + audio_rx_busy = false; + + if (xferred_bytes > 0) { + if (audio_mic_channels == 1) { + // Mono microphone, convert to stereo and send to OUT endpoint + uint16_t samples = xferred_bytes / 2; + mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); + tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); + } else { + // Stereo microphone, send directly to OUT endpoint + tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); + } + } +} + +// Invoked when an isochronous OUT transfer is complete +void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)idx; + (void)ep_addr; + (void)xferred_bytes; + audio_tx_busy = false; +} diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c new file mode 100644 index 000000000..b80cd2938 --- /dev/null +++ b/examples/host/audio_host/src/main.c @@ -0,0 +1,73 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2025 TinyUSB contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include +#include +#include + +#include "bsp/board_api.h" +#include "tusb.h" +#include "app.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF PROTYPES +//--------------------------------------------------------------------+ +void led_blinking_task(void); + +/*------------- MAIN -------------*/ +int main(void) { + board_init(); + + printf("TinyUSB Host USB Audio Example\r\n"); + printf("Connect a USB Audio Device (UAC 1.0) to test\r\n"); + + // init host stack on configured roothub port + tusb_rhport_init_t host_init = { + .role = TUSB_ROLE_HOST, + .speed = TUSB_SPEED_AUTO + }; + tusb_init(BOARD_TUH_RHPORT, &host_init); + + board_init_after_tusb(); + + while (1) { + // tinyusb host task + tuh_task(); + led_blinking_task(); + audio_app_task(); + } +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +//--------------------------------------------------------------------+ +// Blinking Task +//--------------------------------------------------------------------+ +void led_blinking_task(void) { + const uint32_t interval_ms = 1000; + static uint32_t start_ms = 0; + + static bool led_state = false; + + // Blink every interval ms + if ( tusb_time_millis_api() - start_ms < interval_ms) return; // not enough time + start_ms += interval_ms; + + board_led_write(led_state); + led_state = 1 - led_state; // toggle +} diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h new file mode 100644 index 000000000..a4fb17fa5 --- /dev/null +++ b/examples/host/audio_host/src/tusb_config.h @@ -0,0 +1,103 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2025 TinyUSB contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//-------------------------------------------------------------------- +// Common Configuration +//-------------------------------------------------------------------- + +#ifndef CFG_TUSB_MCU + #error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS + #define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG + #define CFG_TUSB_DEBUG 0 +#endif + +#ifndef CFG_TUH_MEM_SECTION + #define CFG_TUH_MEM_SECTION +#endif + +#ifndef CFG_TUH_MEM_ALIGN + #define CFG_TUH_MEM_ALIGN __attribute__((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// Host Configuration +//-------------------------------------------------------------------- + +#define CFG_TUH_ENABLED 1 + +#if CFG_TUSB_MCU == OPT_MCU_RP2040 + #if (defined(CFG_TUH_RPI_PIO_USB) && CFG_TUH_RPI_PIO_USB) || (defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421) + #define BOARD_TUH_RHPORT 1 + #endif +#endif + +#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED + +#ifndef BOARD_TUH_RHPORT + #define BOARD_TUH_RHPORT 1 +#endif + +#ifndef BOARD_TUH_MAX_SPEED + #define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// Driver Configuration +//-------------------------------------------------------------------- + +#define CFG_TUH_ENUMERATION_BUFSIZE 512 + +#define CFG_TUH_HUB 1 +#define CFG_TUH_CDC 0 +#define CFG_TUH_HID 1 +#define CFG_TUH_MSC 0 +#define CFG_TUH_VENDOR 0 +#define CFG_TUH_AUDIO 1 + +// max device support (excluding hub device): 1 hub typically has 4 ports +#define CFG_TUH_DEVICE_MAX (3 * CFG_TUH_HUB + 1) + +//------------- Audio Host Config -------------// +#define CFG_TUH_AUDIO_MAX 2 +#define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 +#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_CONFIG_H_ */ -- cgit v1.3.1 From f76a0f2c0addb4988da2ef7d993d3253a1667d1a Mon Sep 17 00:00:00 2001 From: 代码人生 Date: Thu, 16 Jul 2026 16:09:19 +0800 Subject: Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- examples/host/audio_host/src/audio_app.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index ac7596d91..d264426fd 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -204,15 +204,20 @@ void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { (void)ep_addr; audio_rx_busy = false; - if (xferred_bytes > 0) { + if (xferred_bytes > 0 && audio_ep_out != 0 && !audio_tx_busy) { + bool ok; if (audio_mic_channels == 1) { // Mono microphone, convert to stereo and send to OUT endpoint uint16_t samples = xferred_bytes / 2; mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); - tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); + ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); } else { // Stereo microphone, send directly to OUT endpoint - tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); + ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); + } + + if (ok) { + audio_tx_busy = true; } } } -- cgit v1.3.1 From 817807dc64ae3124a315f42d67eb00271c3846e0 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 10:40:44 +0800 Subject: fix(class/audio): handle non-audio interface in enumeration and fix async control transfer buffer - Stop parsing at first non-Audio interface in audioh_open to avoid claiming unrelated interfaces - Call usbh_driver_set_config_complete for AS and unknown interfaces to allow enumeration to continue - Add global ctrl endpoint buffer to audioh_epbuf_t to fix use-after-return in feature_unit_set - Add sampling_freq NULL check and initialize to 0 in tuh_audio_get_sampling_freq - Change BOARD_TUH_RHPORT from 1 to 0 in audio_host example - Add only.txt with supported MCU/family list for audio_host example --- examples/host/audio_host/only.txt | 32 ++++++++++++++++++++++++++++++ examples/host/audio_host/src/tusb_config.h | 2 +- src/class/audio/audio_host.c | 24 +++++++++++++++++----- 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 examples/host/audio_host/only.txt (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/only.txt b/examples/host/audio_host/only.txt new file mode 100644 index 000000000..a2ff93be5 --- /dev/null +++ b/examples/host/audio_host/only.txt @@ -0,0 +1,32 @@ +family:hpmicro +family:samd21 +family:samd5x_e5x +mcu:CH32V20X +mcu:KINETIS_KL +mcu:LPC175X_6X +mcu:LPC177X_8X +mcu:LPC18XX +mcu:LPC40XX +mcu:LPC43XX +mcu:LPC54 +mcu:LPC55 +mcu:MAX3421 +mcu:MIMXRT10XX +mcu:MIMXRT11XX +mcu:MIMXRT1XXX +mcu:MSP432E4 +mcu:RAXXX +mcu:RP2040 +mcu:RW61X +mcu:RX65X +mcu:STM32C0 +mcu:STM32C5 +mcu:STM32F4 +mcu:STM32F7 +mcu:STM32G0 +mcu:STM32H5 +mcu:STM32H7 +mcu:STM32H7RS +mcu:STM32N6 +mcu:STM32U3 +mcu:STM32U5 diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h index a4fb17fa5..4a7a6ad56 100644 --- a/examples/host/audio_host/src/tusb_config.h +++ b/examples/host/audio_host/src/tusb_config.h @@ -68,7 +68,7 @@ extern "C" { #define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED #ifndef BOARD_TUH_RHPORT - #define BOARD_TUH_RHPORT 1 + #define BOARD_TUH_RHPORT 0 #endif #ifndef BOARD_TUH_MAX_SPEED diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 9414c1ab4..85c2374be 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -136,10 +136,11 @@ typedef struct { typedef struct { TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); + TUH_EPBUF_DEF(ctrl, 8); } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; - + static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; //--------------------------------------------------------------------+ // Helper //--------------------------------------------------------------------+ @@ -281,7 +282,9 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface while (tu_desc_in_bounds(p_desc, desc_end)) { if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { const tusb_desc_interface_t *itf = (const tusb_desc_interface_t *)p_desc; - if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + // Stop at the first non-Audio interface so we don't claim the rest of the configuration + if (itf->bInterfaceClass != TUSB_CLASS_AUDIO) break; + if (itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { // Found Audio Streaming Interface TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", itf->bInterfaceNumber, itf->bAlternateSetting); @@ -537,11 +540,14 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { if (idx >= CFG_TUH_AUDIO_MAX) { for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { - // AS interface, already handled by AC interface's set_config + // AS interface: configuration is driven by the AC interface, so just pass through + usbh_driver_set_config_complete(dev_addr, itf_num); return true; } } - return false; + // Not an Audio interface we own; pass through so enumeration can continue + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; } audioh_interface_t *p_audio = &_audioh_itf[idx]; @@ -644,6 +650,9 @@ bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampli bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(sampling_freq, false); + *sampling_freq = 0; + tusb_control_request_t const request = { .bmRequestType_bit = { .recipient = TUSB_REQ_RCPT_ENDPOINT, @@ -684,7 +693,12 @@ bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, .wLength = 2 }; - uint8_t val_buf[2] = { (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF) }; + uint8_t const idx = tuh_audio_itf_get_index(daddr, itf_num); + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + + uint8_t* val_buf = _audioh_epbuf[idx].ctrl; + val_buf[0] = (uint8_t)(value & 0xFF); + val_buf[1] = (uint8_t)((value >> 8) & 0xFF); tuh_xfer_t xfer = { .daddr = daddr, -- cgit v1.3.1 From e9578eb103a90d5ab059a067b2e17ca1ddcedee5 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Tue, 21 Jul 2026 11:23:27 +0800 Subject: Refactor TUH_AUDIO API and simplify multi-AS interface support This commit refactors the TUH_AUDIO (USB Audio Host) class driver to simplify its public API and improve multi-AS (Audio Streaming) interface support. The changes are focused on three files: the core driver (audio_host.c/h) and the example application (audio_app.c). Key changes in src/class/audio/audio_host.h: - Remove tuh_audio_descriptor_cb_t and tuh_audio_mount_cb_t structures. The mount callback no longer passes a large descriptor-info struct; applications query per-AS info via tuh_audio_as_get_info(). - Add tuh_audio_get_dev_addr() and tuh_audio_get_feature_unit_id() accessors to retrieve device address and feature-unit ID from an interface index. - Simplify control-transfer APIs by replacing (daddr, itf_num, unit_id) parameters with a single idx parameter: tuh_audio_set_sampling_freq(idx, as_idx, ...) tuh_audio_get_sampling_freq(idx, as_idx, ...) tuh_audio_feature_unit_set(idx, control_selector, channel, ...) tuh_audio_feature_unit_get(idx, control_selector, channel, ...) - Add synchronous wrapper APIs using TU_API_SYNC macro: tuh_audio_get_sampling_freq_sync() tuh_audio_set_sampling_freq_sync() tuh_audio_feature_unit_set_sync() tuh_audio_feature_unit_get_sync() - Update isochronous endpoint APIs to use (idx, as_idx) instead of (daddr, idx): tuh_audio_receive(idx, as_idx, buffer, len) tuh_audio_send(idx, as_idx, buffer, len) - Remove tuh_audio_descriptor_cb() weak callback. - Update tuh_audio_mount_cb() signature from mount_cb(param) to no param. - Update tuh_audio_rx_cb()/tuh_audio_tx_cb() first parameter from idx to dev_addr for consistency with other class drivers. Key changes in src/class/audio/audio_host.c: - Delete tuh_audio_descriptor_cb weak stub. - Refactor get_idx_by_ep_addr() to iterate all AS interfaces per device instead of relying on single ep_in/ep_out fields. - Add audioh_get_ep_addr_by_dir() helper to find an endpoint address by direction across multiple AS interfaces. - Simplify audioh_close() cleanup: remove now-removed single-endpoint fields (ep_in, ep_out) and rely on tu_memclr(p_audio->as, ...). - Update audioh_xfer_cb() to pass dev_addr (not idx) to rx/tx callbacks, matching the new callback signature. - Simplify audioh_open(): remove descriptor-callback emission and the temporary desc_cb structure; store only ac_itf_num instead of bInterfaceNumber + iInterface + as_interface_num. - Rename local descriptor pointers for clarity: desc_input_terminal (was desc_it) desc_output_terminal (was desc_ot) Key changes in examples/host/audio_host/src/audio_app.c: - Remove now-unnecessary globals: audio_ep_in, audio_ep_out, audio_ac_itf, audio_feature_unit_id. - Initialize audio_dev_addr, audio_idx, audiostream_in_idx, audiostream_out_idx to 0xFF (TUSB_INDEX_INVALID_8) instead of 0. - Update print_as_interfaces() to use tuh_audio_as_get_count() and tuh_audio_as_get_info() instead of accessing mount_cb_data. - Update all callback signatures and API calls to match the new driver API. --- examples/host/audio_host/src/audio_app.c | 192 +++++---- src/class/audio/audio_host.c | 681 ++++++++++++++----------------- src/class/audio/audio_host.h | 163 +++----- 3 files changed, 452 insertions(+), 584 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index d264426fd..d9134f089 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -23,18 +23,16 @@ // MACRO TYPEDEF CONSTANT ENUM DECLARATION //--------------------------------------------------------------------+ -static bool audio_mounted = false; -static uint8_t audio_dev_addr = 0; -static uint8_t audio_idx = 0; -static uint8_t audio_ep_in = 0; -static uint8_t audio_ep_out = 0; -static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) -static uint8_t audio_mic_channels = 1; -static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state -static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state -static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer -static uint8_t audio_ac_itf = 0; // Audio Control interface number -static uint8_t audio_feature_unit_id = 0; // Feature Unit ID +static bool audio_mounted = false; +static uint8_t audio_dev_addr = 0xFF; +static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer +static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state +static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state +static uint8_t audio_idx = 0xFF; +static uint8_t audiostream_in_idx = 0xFF; +static uint8_t audiostream_out_idx = 0xFF; +static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) +static uint8_t audio_mic_channels = 1; static uint8_t audio_rx_buffer[CFG_TUH_AUDIO_EPIN_BUFSIZE] __attribute__((aligned(4))); static uint8_t audio_tx_buffer[CFG_TUH_AUDIO_EPOUT_BUFSIZE] __attribute__((aligned(4))); @@ -69,55 +67,27 @@ static void print_sampling_freq(const tuh_audio_as_info_t *as) { } // Print all AS interface info -static void print_as_interfaces(const tuh_audio_mount_cb_t *mount_cb_data) { - for (uint8_t i = 0; i < mount_cb_data->as_count; i++) { - const tuh_audio_as_info_t *as = &mount_cb_data->as_info[i]; - if (as->ep_dir == TUSB_DIR_IN) { +static void print_as_interfaces(uint8_t idx) { + tuh_audio_as_info_t as = {}; + uint8_t as_count = tuh_audio_as_get_count(idx); + for (uint8_t i = 0; i < as_count; i++) { + tuh_audio_as_get_info(idx, i, &as); + if (as.ep_dir == TUSB_DIR_IN) { // Save microphone channel count for mono-to-stereo conversion - audio_mic_channels = as->num_channels; + audio_mic_channels = as.num_channels; printf(" --- Microphone (AS %u) ---\r\n", i); - printf(" IN EP: 0x%02x (max size: %u)\r\n", as->ep_addr, as->ep_size); + printf(" IN EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); } else { printf(" --- Speaker (AS %u) ---\r\n", i); - printf(" OUT EP: 0x%02x (max size: %u)\r\n", as->ep_addr, as->ep_size); + printf(" OUT EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); } - printf(" Interface: %u, Alt: %u\r\n", as->interface_num, as->alt_setting); - printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as->format_type, - as->num_channels, as->sub_frame_size, as->bit_resolution); - print_sampling_freq(as); + printf(" Interface: %u, Alt: %u\r\n", as.interface_num, as.alt_setting); + printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as.format_type, + as.num_channels, as.sub_frame_size, as.bit_resolution); + print_sampling_freq(&as); } } -// Find IN and OUT endpoints from AS interfaces, return IN sampling freq -static uint32_t find_audio_endpoints(const tuh_audio_mount_cb_t *mount_cb_data) { - uint32_t in_sam_freq = 0; - audio_ep_in = 0; - audio_ep_out = 0; - - for (uint8_t i = 0; i < mount_cb_data->as_count; i++) { - const tuh_audio_as_info_t *as = &mount_cb_data->as_info[i]; - if (as->ep_dir == TUSB_DIR_IN) { - audio_ep_in = as->ep_addr; - if (as->sam_freq_type > 0) { - in_sam_freq = as->sam_freq[0]; - } - } else { - audio_ep_out = as->ep_addr; - } - } - return in_sam_freq; -} - -// Set Feature Unit volume to un-mute -static void set_feature_unit_volume(void) { - if (audio_feature_unit_id == 0) { - return; - } - printf(" Setting Feature Unit %u volume to 0x0600\r\n", audio_feature_unit_id); - tuh_audio_feature_unit_set(audio_dev_addr, audio_ac_itf, audio_feature_unit_id, AUDIO10_FU_CTRL_VOLUME, 0, 0x0600, - NULL, 0); -} - //--------------------------------------------------------------------+ // Application Task //--------------------------------------------------------------------+ @@ -127,7 +97,7 @@ void audio_app_task(void) { } if (!audio_rx_busy) { - if (tuh_audio_receive(audio_dev_addr, audio_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { + if (tuh_audio_receive(audio_idx, audiostream_in_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { audio_rx_busy = true; } } @@ -138,82 +108,102 @@ void audio_app_task(void) { //--------------------------------------------------------------------+ -// Callback after IN sampling frequency is set -static void in_sampling_freq_set_cb(tuh_xfer_t *xfer) { - if (xfer->result != XFER_RESULT_SUCCESS) { - printf(" Sampling frequency set FAILED: result=%u\r\n", xfer->result); - return; - } - printf(" Sampling frequency set OK, ready for isochronous transfer\r\n"); - // Set Feature Unit volume to un-mute - set_feature_unit_volume(); - // Set OUT sampling frequency then send empty packet to kick-start device - tuh_audio_set_sampling_freq(audio_dev_addr, audio_ep_out, sampling_freq, NULL, 0); - audio_ready = true; -} - -void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data) { - if (!mount_cb_data) { +void tuh_audio_mount_cb(uint8_t idx) { + if (idx >= CFG_TUH_AUDIO_MAX) { + printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); return; } - printf("Audio device mounted: idx=%u, daddr=%u, AS count=%u\r\n", idx, mount_cb_data->daddr, mount_cb_data->as_count); - - print_as_interfaces(mount_cb_data); - - // Feature Unit - if (mount_cb_data->feature_unit_id != 0) { - printf(" Feature Unit: ID=%u, SourceID=%u\r\n", mount_cb_data->feature_unit_id, - mount_cb_data->feature_unit_source_id); - } + print_as_interfaces(idx); // Save device info - audio_dev_addr = mount_cb_data->daddr; - audio_idx = idx; - audio_mounted = true; - audio_ac_itf = mount_cb_data->bInterfaceNumber; - audio_feature_unit_id = mount_cb_data->feature_unit_id; + audio_dev_addr = tuh_audio_get_dev_addr(idx); + audio_idx = idx; + audio_mounted = true; // Find endpoints and IN sampling frequency - uint32_t in_sam_freq = find_audio_endpoints(mount_cb_data); + tuh_audio_as_info_t as; + for (uint8_t i = 0; i < tuh_audio_as_get_count(idx); i++) { + + tuh_audio_as_get_info(idx, i, &as); + if (as.ep_dir == TUSB_DIR_IN) { + audiostream_in_idx = i; + if (as.sam_freq_type > 0) { + sampling_freq = as.sam_freq[0]; + } + } else { + audiostream_out_idx = i; + } + } // Set IN sampling frequency before starting isochronous transfer - if (audio_ep_in != 0 && in_sam_freq != 0) { - sampling_freq = in_sam_freq; + if (audiostream_in_idx != 0xFF && sampling_freq != 0) { printf(" Setting IN sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); - tuh_audio_set_sampling_freq(mount_cb_data->daddr, audio_ep_in, sampling_freq, in_sampling_freq_set_cb, 0); + // tuh_audio_set_sampling_freq(audio_idx, audiostream_in_idx, sampling_freq, in_sampling_freq_set_cb, 0); + + tusb_xfer_result_t result; + result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_in_idx, sampling_freq); + if (result == XFER_RESULT_SUCCESS) { + tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_in_idx, &sampling_freq); + printf(" IN sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); + if (audiostream_out_idx != 0xFF) { + printf(" Setting OUT sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); + result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_out_idx, sampling_freq); + if (result == XFER_RESULT_SUCCESS) { + tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_out_idx, &sampling_freq); + printf(" OUT sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); + } else { + printf(" Setting OUT sampling frequency FAILED: result=%u\r\n", result); + } + } + } else { + printf(" Setting IN sampling frequency FAILED: result=%u\r\n", result); + } + uint16_t volume = 0x0600; + + result = tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); + if (result == XFER_RESULT_SUCCESS) { + printf(" Feature Unit volume set:volume 0x%04x\r\n", (unsigned int)volume); + tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); + } else { + printf(" Setting Feature Unit volume FAILED: result=%u\r\n", result); + } } + audio_ready = true; } // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx) { printf("Audio device unmounted: idx=%u\r\n", idx); if (audio_mounted && audio_idx == idx) { - audio_mounted = false; - audio_ready = false; - audio_rx_busy = false; - audio_tx_busy = false; - audio_dev_addr = 0; - audio_idx = 0; + audio_mounted = false; + audio_ready = false; + audio_rx_busy = false; + audio_tx_busy = false; + audio_dev_addr = 0; + audio_idx = 0; + audiostream_in_idx = 0xFF; + audiostream_out_idx = 0xFF; } } // Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)idx; +void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)dev_addr; (void)ep_addr; audio_rx_busy = false; - if (xferred_bytes > 0 && audio_ep_out != 0 && !audio_tx_busy) { + if (xferred_bytes > 0 && audiostream_out_idx != 0xFF && !audio_tx_busy) { bool ok; if (audio_mic_channels == 1) { // Mono microphone, convert to stereo and send to OUT endpoint uint16_t samples = xferred_bytes / 2; mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); - ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); + ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_tx_buffer, xferred_bytes * 2); } else { // Stereo microphone, send directly to OUT endpoint - ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); + ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_rx_buffer, xferred_bytes); } if (ok) { @@ -223,8 +213,8 @@ void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { } // Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)idx; +void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)dev_addr; (void)ep_addr; (void)xferred_bytes; audio_tx_busy = false; diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 396f75987..c74a95d48 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -4,7 +4,7 @@ * * This file is part of the TinyUSB stack. */ - + /* * This driver implements a USB Audio Host (UAC 1.0) class driver. * It supports multiple Audio Streaming (AS) interfaces with independent format storage. @@ -28,107 +28,70 @@ #if (CFG_TUH_ENABLED && CFG_TUH_AUDIO) -#include "host/usbh.h" -#include "host/usbh_pvt.h" -#include "audio_host.h" + #include "host/usbh.h" + #include "host/usbh_pvt.h" + #include "audio_host.h" -// Level where CFG_TUSB_DEBUG must be at least for this driver is logged -#ifndef CFG_TUH_AUDIO_LOG_LEVEL - #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL -#endif + // Level where CFG_TUSB_DEBUG must be at least for this driver is logged + #ifndef CFG_TUH_AUDIO_LOG_LEVEL + #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL + #endif -#define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) + #define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) //--------------------------------------------------------------------+ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ -TU_ATTR_WEAK void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data) { - (void) idx; - (void) desc_cb_data; -} -TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data) { - (void) idx; - (void) mount_cb_data; + +TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx) { + (void)idx; } TU_ATTR_WEAK void tuh_audio_umount_cb(uint8_t idx) { - (void) idx; + (void)idx; } TU_ATTR_WEAK void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void) idx; - (void) ep_addr; - (void) xferred_bytes; + (void)idx; + (void)ep_addr; + (void)xferred_bytes; } TU_ATTR_WEAK void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { - (void) idx; - (void) ep_addr; - (void) xferred_bytes; + (void)idx; + (void)ep_addr; + (void)xferred_bytes; } //--------------------------------------------------------------------+ // MACRO CONSTANT TYPEDEF //--------------------------------------------------------------------+ -// Per-AS interface internal storage -typedef struct { - uint8_t interface_num; - uint8_t alt_setting; - uint8_t ep_addr; - uint16_t ep_size; - uint8_t ep_dir; - - uint8_t format_type; - uint8_t num_channels; - uint8_t sub_frame_size; - uint8_t bit_resolution; - uint8_t sam_freq_type; - uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; - uint32_t sam_freq_lower; - uint32_t sam_freq_upper; -} audioh_as_t; +// Per-interface storage typedef struct { - uint8_t daddr; - uint8_t bInterfaceNumber; // Audio Control interface number - uint8_t iInterface; - uint8_t itf_count; // number of interfaces (AC + AS) - - // Audio Streaming Interface - uint8_t as_interface_num; // Audio Streaming interface number - uint8_t alt_setting; // current alt setting + uint8_t daddr; // device address + uint8_t ac_itf_num; // Audio Control interface number + uint8_t itf_count; // number of interfaces (AC + AS) // Terminal info (from Audio Control Interface) - uint16_t input_terminal_type; // wTerminalType of Input Terminal - uint8_t input_terminal_id; // bTerminalID of Input Terminal - uint8_t input_terminal_channels; // bNrChannels of Input Terminal - uint16_t output_terminal_type; // wTerminalType of Output Terminal - uint8_t output_terminal_id; // bTerminalID of Output Terminal + uint16_t input_terminal_type; // wTerminalType of Input Terminal + uint8_t input_terminal_id; // bTerminalID of Input Terminal + uint8_t input_terminal_channels; // bNrChannels of Input Terminal + uint16_t output_terminal_type; // wTerminalType of Output Terminal + uint8_t output_terminal_id; // bTerminalID of Output Terminal // Feature Unit info - uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) - uint8_t feature_unit_source_id; // bSourceID of Feature Unit - - // Isochronous IN endpoint - uint8_t ep_in; - uint16_t ep_in_size; - uint16_t ep_in_interval; - - // Isochronous OUT endpoint - uint8_t ep_out; - uint16_t ep_out_size; - uint16_t ep_out_interval; + uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) + uint8_t feature_unit_source_id; // bSourceID of Feature Unit // Multiple AS interfaces support - uint8_t as_interfaces[CFG_TUH_AUDIO_MAX_AS]; - uint8_t as_alt_settings[CFG_TUH_AUDIO_MAX_AS]; uint8_t as_count; uint8_t as_set_idx; // Per-AS interface independent storage (new) - audioh_as_t as[CFG_TUH_AUDIO_MAX_AS]; + tuh_audio_as_info_t as[CFG_TUH_AUDIO_MAX_AS]; // Array of Audio Streaming interface info structures bool mounted; } audioh_interface_t; @@ -140,7 +103,7 @@ typedef struct { } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; - static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; +static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; //--------------------------------------------------------------------+ // Helper //--------------------------------------------------------------------+ @@ -156,14 +119,28 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if ((p_audio->daddr == daddr) && - (ep_addr == p_audio->ep_in || ep_addr == p_audio->ep_out)) { - return idx; + if (p_audio->daddr == daddr) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + if (p_audio->as[as_idx].ep_addr == ep_addr) { + return idx; + } + } } } return TUSB_INDEX_INVALID_8; } +static uint8_t audioh_get_ep_addr_by_dir(const audioh_interface_t *p_audio, uint8_t dir) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + const tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + if (as->ep_addr != 0 && as->ep_dir == dir) { + return as->ep_addr; + } + } + + return 0; +} + //--------------------------------------------------------------------+ // USBH API //--------------------------------------------------------------------+ @@ -183,32 +160,22 @@ void audioh_close(uint8_t daddr) { TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); tuh_audio_umount_cb(idx); - p_audio->bInterfaceNumber = 0; - p_audio->as_interface_num = 0; - p_audio->alt_setting = 0; - p_audio->daddr = 0; - p_audio->mounted = false; - p_audio->ep_in = 0; - p_audio->ep_out = 0; - p_audio->as_count = 0; + p_audio->ac_itf_num = 0; + p_audio->daddr = 0; + p_audio->mounted = false; + p_audio->as_count = 0; p_audio->as_set_idx = 0; - tu_memclr(p_audio->as_interfaces, sizeof(p_audio->as_interfaces)); - tu_memclr(p_audio->as_alt_settings, sizeof(p_audio->as_alt_settings)); tu_memclr(p_audio->as, sizeof(p_audio->as)); } } } bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { - (void) result; - const uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - - if (ep_addr == p_audio->ep_in) { - tuh_audio_rx_cb(idx, ep_addr, (uint16_t) xferred_bytes); - } else if (ep_addr == p_audio->ep_out) { - tuh_audio_tx_cb(idx, ep_addr, (uint16_t) xferred_bytes); + (void)result; + if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { + tuh_audio_rx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); + } else { + tuh_audio_tx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); } return true; @@ -218,29 +185,24 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin // Enumeration //--------------------------------------------------------------------+ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { - (void) rhport; + (void)rhport; TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); TU_VERIFY(AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass, 0); const uint8_t *desc_start = (const uint8_t *)desc_itf; - const uint8_t *p_desc = desc_start; - const uint8_t *desc_end = desc_start + max_len; + const uint8_t *p_desc = desc_start; + const uint8_t *desc_end = desc_start + max_len; const uint8_t idx = find_new_audio_index(); TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->itf_count = 0; - - tuh_audio_descriptor_cb_t desc_cb = { 0 }; + p_audio->itf_count = 0; // Parse Audio Control Interface TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); - p_audio->bInterfaceNumber = desc_itf->bInterfaceNumber; - p_audio->iInterface = desc_itf->iInterface; - p_audio->itf_count = 1; - desc_cb.desc_ac_interface = desc_itf; - desc_cb.ac_interface_num = desc_itf->bInterfaceNumber; + p_audio->ac_itf_num = desc_itf->bInterfaceNumber; + p_audio->itf_count = 1; // Parse Audio Control interface descriptors (Input Terminal, Output Terminal, Feature Unit, etc.) p_desc = tu_desc_next(p_desc); @@ -248,26 +210,26 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { switch (tu_desc_subtype(p_desc)) { case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { - audio10_desc_input_terminal_t const *desc_input_terminal = (audio10_desc_input_terminal_t const *)p_desc; - p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); - p_audio->input_terminal_id = desc_input_terminal->bTerminalID; - p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; - TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", - desc_input_terminal->bTerminalID, tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); + const audio10_desc_input_terminal_t *desc_input_terminal = (const audio10_desc_input_terminal_t *)p_desc; + p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); + p_audio->input_terminal_id = desc_input_terminal->bTerminalID; + p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; + TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", desc_input_terminal->bTerminalID, + tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); break; } case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { - audio10_desc_output_terminal_t const *desc_output_terminal = (audio10_desc_output_terminal_t const *)p_desc; - p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); - p_audio->output_terminal_id = desc_output_terminal->bTerminalID; - TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", - desc_output_terminal->bTerminalID, tu_le16toh(desc_output_terminal->wTerminalType)); + const audio10_desc_output_terminal_t *desc_output_terminal = (const audio10_desc_output_terminal_t *)p_desc; + p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); + p_audio->output_terminal_id = desc_output_terminal->bTerminalID; + TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", desc_output_terminal->bTerminalID, + tu_le16toh(desc_output_terminal->wTerminalType)); break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - uint8_t const *desc_feature_unit = p_desc; - p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID - p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID + const uint8_t *desc_feature_unit = p_desc; + p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID + p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", desc_feature_unit[3], desc_feature_unit[4]); break; } @@ -281,40 +243,31 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface // Parse all remaining descriptors in this configuration looking for Audio Streaming interfaces while (tu_desc_in_bounds(p_desc, desc_end)) { if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { - tusb_desc_interface_t const *desc_interface = (tusb_desc_interface_t const *)p_desc; - // Stop at the first non-Audio interface so we don't claim the rest of the configuration - if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) break; - if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; + // Stop at the first non-Audio interface so we don't claim the rest of the configuration + if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) { + break; + } + if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { // Found Audio Streaming Interface - TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, desc_interface->bAlternateSetting); + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, + desc_interface->bAlternateSetting); if (desc_interface->bAlternateSetting == 0) { // Interface descriptor with alt setting 0 (no endpoints) - // Add to AS interfaces array + // Add to AS entries if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as_interface_num = desc_interface->bInterfaceNumber; - p_audio->as_interfaces[p_audio->as_count] = desc_interface->bInterfaceNumber; - desc_cb.desc_as_interface = desc_interface; - desc_cb.as_interface_num = desc_interface->bInterfaceNumber; - // Create new AS entry for per-interface storage p_audio->as[p_audio->as_count].interface_num = desc_interface->bInterfaceNumber; - p_audio->as[p_audio->as_count].alt_setting = 0; + p_audio->as[p_audio->as_count].alt_setting = 0; p_audio->as_count++; + } else { + TU_LOG_DRV(" Skip AS Interface %u: reach CFG_TUH_AUDIO_MAX_AS=%u\r\n", desc_interface->bInterfaceNumber, + CFG_TUH_AUDIO_MAX_AS); } } else if (desc_interface->bNumEndpoints > 0) { // Interface descriptor with alt setting > 0 (has endpoints) - // Find matching AS interface and set alt_setting + // Find matching AS entry and set alt_setting uint8_t as_entry_idx = CFG_TUH_AUDIO_MAX_AS; - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - if (p_audio->as_interfaces[as_idx] == desc_interface->bInterfaceNumber) { - p_audio->alt_setting = desc_interface->bAlternateSetting; - p_audio->as_alt_settings[as_idx] = desc_interface->bAlternateSetting; - desc_cb.alt_setting = desc_interface->bAlternateSetting; - desc_cb.desc_as_interface_alt = desc_interface; - break; - } - } - // Find or create AS entry for per-interface storage for (uint8_t i = 0; i < p_audio->as_count; i++) { if (p_audio->as[i].interface_num == desc_interface->bInterfaceNumber) { as_entry_idx = i; @@ -322,7 +275,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } if (as_entry_idx >= CFG_TUH_AUDIO_MAX_AS && p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - as_entry_idx = p_audio->as_count; + as_entry_idx = p_audio->as_count; p_audio->as[as_entry_idx].interface_num = desc_interface->bInterfaceNumber; p_audio->as_count++; } @@ -333,48 +286,49 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface // Parse the interface's descriptors p_desc = tu_desc_next(p_desc); // Temporary variables to hold format info until endpoint direction is known - uint8_t tmp_format_type = 0; - uint8_t tmp_num_channels = 0; - uint8_t tmp_sub_frame_size = 0; - uint8_t tmp_bit_resolution = 0; - uint8_t tmp_sam_freq_type = 0; + uint8_t tmp_format_type = 0; + uint8_t tmp_num_channels = 0; + uint8_t tmp_sub_frame_size = 0; + uint8_t tmp_bit_resolution = 0; + uint8_t tmp_sam_freq_type = 0; uint32_t tmp_sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; - uint32_t tmp_sam_freq_lower = 0; - uint32_t tmp_sam_freq_upper = 0; + uint32_t tmp_sam_freq_lower = 0; + uint32_t tmp_sam_freq_upper = 0; while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { switch (tu_desc_type(p_desc)) { case TUSB_DESC_CS_INTERFACE: { switch (tu_desc_subtype(p_desc)) { case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { TU_LOG_DRV(" AS General descriptor\r\n"); - desc_cb.desc_cs_as_general = p_desc; break; } case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { TU_LOG_DRV(" Format Type descriptor\r\n"); - desc_cb.desc_format_type = p_desc; + TU_ASSERT(p_desc[0] >= 8, 0); // Parse UAC 1.0 Format Type I descriptor fields into temporary variables - tmp_format_type = p_desc[3]; // bFormatType - tmp_num_channels = p_desc[4]; // bNrChannels - tmp_sub_frame_size = p_desc[5]; // bSubFrameSize - tmp_bit_resolution = p_desc[6]; // bBitResolution + tmp_format_type = p_desc[3]; // bFormatType + tmp_num_channels = p_desc[4]; // bNrChannels + tmp_sub_frame_size = p_desc[5]; // bSubFrameSize + tmp_bit_resolution = p_desc[6]; // bBitResolution // Parse sampling frequencies uint8_t bLength = p_desc[0]; if (bLength >= 8) { - tmp_sam_freq_type = p_desc[7]; // bSamFreqType + tmp_sam_freq_type = p_desc[7]; // bSamFreqType if (tmp_sam_freq_type == 0) { // Continuous range: tLowerSamFreq, tUpperSamFreq (3 bytes each) if (bLength >= 14) { - tmp_sam_freq_lower = ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); - tmp_sam_freq_upper = ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); + tmp_sam_freq_lower = + ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); + tmp_sam_freq_upper = + ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); } } else { // Discrete sampling frequencies - uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type : CFG_TUH_AUDIO_MAX_SAM_FREQ; + uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type + : CFG_TUH_AUDIO_MAX_SAM_FREQ; for (uint8_t i = 0; i < max_freqs && (8 + i * 3 + 2) < bLength; i++) { - tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | - ((uint32_t)p_desc[9 + i * 3] << 8) | + tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | ((uint32_t)p_desc[9 + i * 3] << 8) | ((uint32_t)p_desc[10 + i * 3] << 16)); } } @@ -391,21 +345,17 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { TU_LOG_DRV(" Isochronous EP %02x\r\n", desc_endpoint->bEndpointAddress); if (tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN) { - p_audio->ep_in = desc_endpoint->bEndpointAddress; - p_audio->ep_in_size = tu_edpt_packet_size(desc_endpoint); - p_audio->ep_in_interval = desc_endpoint->bInterval; - desc_cb.desc_ep_in = desc_endpoint; // Save to per-AS storage if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - audioh_as_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_IN; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; + tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); + as->ep_dir = TUSB_DIR_IN; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; as->sub_frame_size = tmp_sub_frame_size; as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_type = tmp_sam_freq_type; as->sam_freq_lower = tmp_sam_freq_lower; as->sam_freq_upper = tmp_sam_freq_upper; for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { @@ -413,21 +363,17 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } } else { - p_audio->ep_out = desc_endpoint->bEndpointAddress; - p_audio->ep_out_size = tu_edpt_packet_size(desc_endpoint); - p_audio->ep_out_interval = desc_endpoint->bInterval; - desc_cb.desc_ep_out = desc_endpoint; // Save to per-AS storage if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - audioh_as_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_OUT; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; + tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); + as->ep_dir = TUSB_DIR_OUT; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; as->sub_frame_size = tmp_sub_frame_size; as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_type = tmp_sam_freq_type; as->sam_freq_lower = tmp_sam_freq_lower; as->sam_freq_upper = tmp_sam_freq_upper; for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { @@ -449,7 +395,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface continue; } p_audio->itf_count++; - } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { + } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && + desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { // Another Audio Control interface (shouldn't happen in normal UAC 1.0) p_audio->itf_count++; } @@ -458,23 +405,22 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } p_audio->daddr = dev_addr; - tuh_audio_descriptor_cb(idx, &desc_cb); return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); } static void _audioh_mount(uint8_t dev_addr, uint8_t idx); -static void audioh_set_interface_complete(tuh_xfer_t* xfer) { - uint8_t idx = (uint8_t) xfer->user_data; +static void audioh_set_interface_complete(tuh_xfer_t *xfer) { + uint8_t idx = (uint8_t)xfer->user_data; audioh_interface_t *p_audio = &_audioh_itf[idx]; // Send SET_INTERFACE for next AS interface if any p_audio->as_set_idx++; if (p_audio->as_set_idx < p_audio->as_count) { uint8_t as_idx = p_audio->as_set_idx; - uint8_t itf = p_audio->as_interfaces[as_idx]; - uint8_t alt = p_audio->as_alt_settings[as_idx]; + uint8_t itf = p_audio->as[as_idx].interface_num; + uint8_t alt = p_audio->as[as_idx].alt_setting; if (alt > 0) { TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, xfer->daddr); tuh_interface_set(xfer->daddr, itf, alt, audioh_set_interface_complete, idx); @@ -488,49 +434,12 @@ static void audioh_set_interface_complete(tuh_xfer_t* xfer) { static void _audioh_mount(uint8_t dev_addr, uint8_t idx) { audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->mounted = true; - - tuh_audio_mount_cb_t mount_cb_data = { - .daddr = dev_addr, - .bInterfaceNumber = p_audio->bInterfaceNumber, - .bAltSetting = p_audio->alt_setting, - .input_terminal_type = p_audio->input_terminal_type, - .input_terminal_id = p_audio->input_terminal_id, - .input_terminal_channels = p_audio->input_terminal_channels, - .output_terminal_type = p_audio->output_terminal_type, - .output_terminal_id = p_audio->output_terminal_id, - .feature_unit_id = p_audio->feature_unit_id, - .feature_unit_source_id = p_audio->feature_unit_source_id, - .ep_in = p_audio->ep_in, - .ep_out = p_audio->ep_out, - .ep_in_size = p_audio->ep_in_size, - .ep_out_size = p_audio->ep_out_size, - }; - - // Fill per-AS interface info - mount_cb_data.as_count = p_audio->as_count; - for (uint8_t i = 0; i < p_audio->as_count && i < CFG_TUH_AUDIO_MAX_AS; i++) { - audioh_as_t *as = &p_audio->as[i]; - mount_cb_data.as_info[i].interface_num = as->interface_num; - mount_cb_data.as_info[i].alt_setting = as->alt_setting; - mount_cb_data.as_info[i].ep_addr = as->ep_addr; - mount_cb_data.as_info[i].ep_size = as->ep_size; - mount_cb_data.as_info[i].ep_dir = as->ep_dir; - mount_cb_data.as_info[i].format_type = as->format_type; - mount_cb_data.as_info[i].num_channels = as->num_channels; - mount_cb_data.as_info[i].sub_frame_size = as->sub_frame_size; - mount_cb_data.as_info[i].bit_resolution = as->bit_resolution; - mount_cb_data.as_info[i].sam_freq_type = as->sam_freq_type; - mount_cb_data.as_info[i].sam_freq_lower = as->sam_freq_lower; - mount_cb_data.as_info[i].sam_freq_upper = as->sam_freq_upper; - for (uint8_t j = 0; j < CFG_TUH_AUDIO_MAX_SAM_FREQ; j++) { - mount_cb_data.as_info[i].sam_freq[j] = as->sam_freq[j]; - } - } + p_audio->mounted = true; - tuh_audio_mount_cb(idx, &mount_cb_data); - usbh_driver_set_config_complete(dev_addr, p_audio->bInterfaceNumber); + tuh_audio_mount_cb(idx); + + usbh_driver_set_config_complete(dev_addr, p_audio->ac_itf_num); } bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { @@ -539,24 +448,30 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { // If not found, check if this is an AS interface that belongs to a known AC interface if (idx >= CFG_TUH_AUDIO_MAX) { for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { - if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { - // AS interface: configuration is driven by the AC interface, so just pass through - usbh_driver_set_config_complete(dev_addr, itf_num); - return true; + const audioh_interface_t *p_audio = &_audioh_itf[i]; + if (p_audio->daddr == dev_addr) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + if (p_audio->as[as_idx].interface_num == itf_num) { + // AS interface: configuration is driven by the AC interface, so just pass through + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + } } } - // Not an Audio interface we own; pass through so enumeration can continue - usbh_driver_set_config_complete(dev_addr, itf_num); - return true; + // Not an Audio interface we own; pass through so enumeration can continue + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; } audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->as_count <= CFG_TUH_AUDIO_MAX_AS, false); // Send SET_INTERFACE for all AS interfaces with alt_setting > 0 if (p_audio->as_count > 0) { p_audio->as_set_idx = 0; - uint8_t itf = p_audio->as_interfaces[0]; - uint8_t alt = p_audio->as_alt_settings[0]; + uint8_t itf = p_audio->as[0].interface_num; + uint8_t alt = p_audio->as[0].alt_setting; if (alt > 0) { TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, dev_addr); tuh_interface_set(dev_addr, itf, alt, audioh_set_interface_complete, idx); @@ -577,10 +492,21 @@ bool tuh_audio_mounted(uint8_t idx) { return p_audio->mounted; } +uint8_t tuh_audio_get_dev_addr(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->daddr; +} + + +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->feature_unit_id; +} + uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr && p_audio->bInterfaceNumber == itf_num) { + if (p_audio->daddr == daddr && p_audio->ac_itf_num == itf_num) { return idx; } } @@ -595,16 +521,19 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { // re-construct descriptor tusb_desc_interface_t *desc_interface = &info->desc; - desc_interface->bLength = sizeof(tusb_desc_interface_t); - desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; + desc_interface->bLength = sizeof(tusb_desc_interface_t); + desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; + + uint8_t ep_in = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_IN); + uint8_t ep_out = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_OUT); - desc_interface->bInterfaceNumber = p_audio->bInterfaceNumber; - desc_interface->bAlternateSetting = 0; - desc_interface->bNumEndpoints = (uint8_t)((p_audio->ep_in ? 1u : 0u) + (p_audio->ep_out ? 1u : 0u)); - desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; + desc_interface->bInterfaceNumber = p_audio->ac_itf_num; + desc_interface->bAlternateSetting = 0; + desc_interface->bNumEndpoints = (uint8_t)((ep_in ? 1u : 0u) + (ep_out ? 1u : 0u)); + desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; desc_interface->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; desc_interface->bInterfaceProtocol = 0; - desc_interface->iInterface = p_audio->iInterface; + desc_interface->iInterface = 0; return true; } @@ -612,22 +541,22 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { //--------------------------------------------------------------------+ // Control Endpoint API //--------------------------------------------------------------------+ -bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - uint8_t const idx = get_idx_by_ep_addr(daddr, ep_addr); +bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - uint8_t* freq_buf = _audioh_epbuf[idx].ctrl; - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_ENDPOINT, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_OUT - }, - .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t) ep_addr), - .wLength = 3 - }; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio && as_idx < p_audio->as_count, false); + + uint8_t ep_addr = p_audio->as[as_idx].ep_addr; + uint8_t daddr = p_audio->daddr; + uint8_t *freq_buf = _audioh_epbuf[idx].ctrl; + + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t)ep_addr), + .wLength = 3}; // UAC 1.0 sampling frequency is 3 bytes little-endian // uint8_t freq_buf[3] = { @@ -635,109 +564,97 @@ bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampli // (uint8_t)((sampling_freq >> 8) & 0xFF), // (uint8_t)((sampling_freq >> 16) & 0xFF) // }; - freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); - freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); - freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = freq_buf, - .complete_cb = complete_cb, - .user_data = user_data - }; + freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); + freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); + freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = freq_buf, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } -bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - TU_VERIFY(sampling_freq, false); +bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio && as_idx < p_audio->as_count && sampling_freq, false); + uint8_t ep_addr = p_audio->as[as_idx].ep_addr; + uint8_t daddr = p_audio->daddr; + *sampling_freq = 0; - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_ENDPOINT, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_IN - }, - .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t) ep_addr), - .wLength = 3 - }; + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t)ep_addr), + .wLength = 3}; // Application needs to parse 3-byte little-endian sampling frequency from buffer - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = (uint8_t *)sampling_freq, - .complete_cb = complete_cb, - .user_data = user_data - }; + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)sampling_freq, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } -bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_OUT - }, - .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = 2 - }; - - uint8_t const idx = tuh_audio_itf_get_index(daddr, itf_num); +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - - uint8_t* val_buf = _audioh_epbuf[idx].ctrl; - val_buf[0] = (uint8_t)(value & 0xFF); - val_buf[1] = (uint8_t)((value >> 8) & 0xFF); - - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = val_buf, - .complete_cb = complete_cb, - .user_data = user_data - }; + uint8_t daddr = _audioh_itf[idx].daddr; + uint8_t itf_num = _audioh_itf[idx].ac_itf_num; + uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), + .wLength = 2}; + + uint8_t *val_buf = _audioh_epbuf[idx].ctrl; + val_buf[0] = (uint8_t)(value & 0xFF); + val_buf[1] = (uint8_t)((value >> 8) & 0xFF); + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = val_buf, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } -bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - void *buffer, uint8_t len, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_IN - }, - .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = len - }; - - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = buffer, - .complete_cb = complete_cb, - .user_data = user_data - }; +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *buffer, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + uint8_t daddr = _audioh_itf[idx].daddr; + uint8_t itf_num = _audioh_itf[idx].ac_itf_num; + uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_IN}, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), + .wLength = 2}; + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)buffer, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } @@ -755,17 +672,17 @@ bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *inf TU_VERIFY(as_idx < _audioh_itf[idx].as_count, false); TU_VERIFY(info, false); - audioh_as_t *as = &_audioh_itf[idx].as[as_idx]; - info->interface_num = as->interface_num; - info->alt_setting = as->alt_setting; - info->ep_addr = as->ep_addr; - info->ep_size = as->ep_size; - info->ep_dir = as->ep_dir; - info->format_type = as->format_type; - info->num_channels = as->num_channels; + tuh_audio_as_info_t *as = &_audioh_itf[idx].as[as_idx]; + info->interface_num = as->interface_num; + info->alt_setting = as->alt_setting; + info->ep_addr = as->ep_addr; + info->ep_size = as->ep_size; + info->ep_dir = as->ep_dir; + info->format_type = as->format_type; + info->num_channels = as->num_channels; info->sub_frame_size = as->sub_frame_size; info->bit_resolution = as->bit_resolution; - info->sam_freq_type = as->sam_freq_type; + info->sam_freq_type = as->sam_freq_type; info->sam_freq_lower = as->sam_freq_lower; info->sam_freq_upper = as->sam_freq_upper; memcpy(info->sam_freq, as->sam_freq, sizeof(info->sam_freq)); @@ -775,49 +692,43 @@ bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *inf //--------------------------------------------------------------------+ // Isochronous Endpoint API //--------------------------------------------------------------------+ -bool tuh_audio_receive(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len) { +bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->daddr == daddr); - TU_VERIFY(p_audio->ep_in != 0); + tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + TU_VERIFY(as->ep_addr != 0); - return usbh_edpt_xfer(daddr, p_audio->ep_in, buffer, len); + return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, buffer, len); } -bool tuh_audio_send(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len) { +bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->daddr == daddr); - TU_VERIFY(p_audio->ep_out != 0); + tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + TU_VERIFY(as->ep_addr != 0); - return usbh_edpt_xfer(daddr, p_audio->ep_out, (uint8_t *)buffer, len); + return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, (uint8_t *)buffer, len); } //--------------------------------------------------------------------+ // Set Interface //--------------------------------------------------------------------+ -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - tusb_control_request_t const request = { - .bmRequestType_bit = { - .recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_STANDARD, - .direction = TUSB_DIR_OUT - }, - .bRequest = TUSB_REQ_SET_INTERFACE, - .wValue = alt_setting, - .wIndex = itf_num, - .wLength = 0 - }; - - tuh_xfer_t xfer = { - .daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = NULL, - .complete_cb = complete_cb, - .user_data = user_data - }; +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_STANDARD, + .direction = TUSB_DIR_OUT}, + .bRequest = TUSB_REQ_SET_INTERFACE, + .wValue = alt_setting, + .wIndex = itf_num, + .wLength = 0}; + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = NULL, + .complete_cb = complete_cb, + .user_data = user_data}; return tuh_control_xfer(&xfer); } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 5c5a8b3df..aafd58f43 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -17,14 +17,15 @@ extern "C" { //--------------------------------------------------------------------+ // Class Driver Configuration //--------------------------------------------------------------------+ +// Maximum number of Audio interfaces per Audio device #ifndef CFG_TUH_AUDIO_MAX #define CFG_TUH_AUDIO_MAX 1 #endif - +// Maximum number of Audio Streaming interfaces per Audio device #ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 #endif - +// Maximum number of Audio Streaming interfaces per Audio device #ifndef CFG_TUH_AUDIO_MAX_AS #define CFG_TUH_AUDIO_MAX_AS 4 #endif @@ -33,18 +34,18 @@ extern "C" { // AS Interface Info (per-interface independent storage) //--------------------------------------------------------------------+ typedef struct { - uint8_t interface_num; // AS interface number - uint8_t alt_setting; // Current alt setting - uint8_t ep_addr; // Endpoint address - uint16_t ep_size; // Max packet size - uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT + uint8_t interface_num; // AS interface number + uint8_t alt_setting; // Current alt setting + uint8_t ep_addr; // Endpoint address + uint16_t ep_size; // Max packet size + uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT // Format info - uint8_t format_type; - uint8_t num_channels; - uint8_t sub_frame_size; - uint8_t bit_resolution; - uint8_t sam_freq_type; + uint8_t format_type; + uint8_t num_channels; + uint8_t sub_frame_size; + uint8_t bit_resolution; + uint8_t sam_freq_type; uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; uint32_t sam_freq_lower; uint32_t sam_freq_upper; @@ -58,73 +59,16 @@ typedef struct { #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 #endif -//--------------------------------------------------------------------+ -// Descriptor Information -//--------------------------------------------------------------------+ -// Information about parsed UAC 1.0 descriptors passed to the application -// during enumeration (via tuh_audio_descriptor_cb) -typedef struct { - // Audio Control Interface descriptor - const tusb_desc_interface_t *desc_ac_interface; - - // Audio Streaming Interface descriptor (alt setting 0) - const tusb_desc_interface_t *desc_as_interface; - - // Audio Streaming Interface alt setting (with endpoints) - const tusb_desc_interface_t *desc_as_interface_alt; - - // Format Type descriptor - const uint8_t *desc_format_type; - - // Class-Specific AS Interface (AS General) descriptor - const uint8_t *desc_cs_as_general; - - // Standard Isochronous Endpoint descriptor (IN) - const tusb_desc_endpoint_t *desc_ep_in; - - // Standard Isochronous Endpoint descriptor (OUT) - const tusb_desc_endpoint_t *desc_ep_out; - - // Audio function information - uint8_t ac_interface_num; // Audio Control interface number - uint8_t as_interface_num; // Audio Streaming interface number - uint8_t alt_setting; // Current alt setting with endpoints -} tuh_audio_descriptor_cb_t; - -typedef struct { - uint8_t daddr; - uint8_t bInterfaceNumber; - uint8_t bAltSetting; - - // Terminal info (from Audio Control Interface) - uint16_t input_terminal_type; // wTerminalType of Input Terminal (0x0201 = Mic, etc.) - uint8_t input_terminal_id; // bTerminalID of Input Terminal - uint8_t input_terminal_channels; // bNrChannels of Input Terminal - uint16_t output_terminal_type; // wTerminalType of Output Terminal (0x0301 = Speaker, etc.) - uint8_t output_terminal_id; // bTerminalID of Output Terminal - - // Feature Unit info - uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) - uint8_t feature_unit_source_id; // bSourceID of Feature Unit - - // Endpoint info - uint8_t ep_in; - uint8_t ep_out; - uint16_t ep_in_size; - uint16_t ep_out_size; - - // Multi-AS support (per AS interface independent storage) - uint8_t as_count; - tuh_audio_as_info_t as_info[CFG_TUH_AUDIO_MAX_AS]; -} tuh_audio_mount_cb_t; - //--------------------------------------------------------------------+ // Application API //--------------------------------------------------------------------+ // Check if Audio interface is mounted bool tuh_audio_mounted(uint8_t idx); - +// Get device address of Audio interface +uint8_t tuh_audio_get_dev_addr(uint8_t idx); +// Get Feature Unit ID +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); // Get Interface index from device address + interface number // return TUSB_INDEX_INVALID_8 (0xFF) if not found uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num); @@ -141,8 +85,8 @@ uint8_t tuh_audio_as_get_count(uint8_t idx); bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info); // Set Audio Streaming interface alternate setting (to enable/disable endpoints) -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); //--------------------------------------------------------------------+ // Control Endpoint API @@ -150,54 +94,77 @@ bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting // Set current sampling frequency on an isochronous endpoint (UAC 1.0) // Sampling frequency is 3 bytes little-endian -bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). +bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); // Get current sampling frequency from an isochronous endpoint (UAC 1.0) -bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). +bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); // Set current/mute/volume etc. for a feature unit (UAC 1.0) -bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); // Get current/mute/volume etc. from a feature unit (UAC 1.0) -bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, - uint8_t control_selector, uint8_t channel, - void *buffer, uint8_t len, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Control Request Sync API +// Each Function will make a USB control transfer request to/from device the function will block until request is +// complete. The function will return the transfer request result +//--------------------------------------------------------------------+ +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_get_sampling_freq_sync(uint8_t idx, uint8_t as_idx, + uint32_t *sampling_freq) { + TU_API_SYNC(tuh_audio_get_sampling_freq, idx, as_idx, sampling_freq); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_set_sampling_freq_sync(uint8_t idx, uint8_t as_idx, + uint32_t sampling_freq) { + TU_API_SYNC(tuh_audio_set_sampling_freq, idx, as_idx, sampling_freq); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t +tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value) { + TU_API_SYNC(tuh_audio_feature_unit_set, idx, control_selector, channel, value); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t +tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value) { + TU_API_SYNC(tuh_audio_feature_unit_get, idx, control_selector, channel, value); +} //--------------------------------------------------------------------+ // Interrupt/Isochronous Endpoint API //--------------------------------------------------------------------+ -// Submit an isochronous transfer to receive audio data from IN endpoint -bool tuh_audio_receive(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len); +// Submit an isochronous transfer to receive audio data from a default IN endpoint. +// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. +// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. +bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); -// Submit an isochronous transfer to send audio data to OUT endpoint -bool tuh_audio_send(uint8_t daddr, uint8_t idx, uint8_t *buffer, uint16_t len); +// Submit an isochronous transfer to send audio data to a default OUT endpoint. +// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. +// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. +bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); //--------------------------------------------------------------------+ // Callbacks (Weak is optional) //--------------------------------------------------------------------+ -// Invoked when Audio interface descriptor is detected during enumeration. -// Application can copy/parse descriptor if needed. -// Note: may be fired before tuh_audio_mount_cb(), therefore audio interface is not mounted/ready. -void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data); - // Invoked when device with Audio interface is mounted -void tuh_audio_mount_cb(uint8_t idx, const tuh_audio_mount_cb_t *mount_cb_data); +void tuh_audio_mount_cb(uint8_t idx); // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx); // Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes); +void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); // Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes); +void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); //--------------------------------------------------------------------+ // Internal Class Driver API -- cgit v1.3.1 From c950109bcef6404971c3609a6e4caeb0421120b8 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 14 Aug 2026 11:10:23 +0800 Subject: feat(class/audio): rework TUH_AUDIO into a WASAPI/ALSA-like stream API Provide a high-level audio streaming API over UAC 1.0 devices while keeping the USB topology private: applications select supported {format, sample_rate, channels} configurations per logical stream, and the driver owns the mapping to AS interface, alternate setting, and endpoint. - One logical stream per direction per instance; multiple AS interfaces and alternate settings in a direction are merged into the stream's configuration list (discrete tuples; continuous ranges exposed as a single configuration at the top rate) - Asynchronous tuh_audio_configure(): SET_INTERFACE to the selected alternate setting, open/reconfigure the endpoint, set the sampling frequency when supported, initialize the FIFO and packet scheduler, then invoke the completion callback - Frame-based FIFO streaming: tuh_audio_read()/tuh_audio_write() queue whole frames; the driver owns transfer replenishment and fractional packet scheduling (44.1 kHz pays back the 0.1 frame/ms remainder via an accumulator for exact average pacing) - tuh_audio_start()/tuh_audio_stop() activate/deactivate the stream interface through SET_INTERFACE (alt n / alt 0) Driver correctness fixes: - Parse only the AC header's interface collection; MIDI Streaming and other subclasses are skipped - Keep every discrete format as a separate configuration; endpoints are opened only for the alternate setting selected by tuh_audio_configure() - Check tuh_interface_set() return values and SET_INTERFACE transfer results instead of ignoring failures - Validate instance state, direction, buffers, and frame counts in every transfer API - Feature Unit requests use the control's real width (mute/AGC/loudness 1 byte, others 2 bytes) and convert multibyte values to host order - Failed/stalled/aborted isochronous transfers reach only the error callback, never the capture/playback callbacks The audio_host example uses the new API: 48 kHz stereo by default, automatic stream restart on error callbacks, a sine test tone on the playback stream, and periodic mic-only / spk-only / echo phase switching. --- examples/host/CMakeLists.txt | 1 + examples/host/audio_host/README.md | 67 +- examples/host/audio_host/src/app.h | 5 +- examples/host/audio_host/src/audio_app.c | 566 +++++++--- examples/host/audio_host/src/main.c | 32 +- examples/host/audio_host/src/tusb_config.h | 8 +- src/class/audio/audio_host.c | 1576 +++++++++++++++++++--------- src/class/audio/audio_host.h | 247 +++-- 8 files changed, 1716 insertions(+), 786 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt index 7c74e3c73..0e877cb78 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -7,6 +7,7 @@ family_initialize_project(tinyusb_host_examples ${CMAKE_CURRENT_LIST_DIR}) # family_add_subdirectory will filter what to actually add based on selected FAMILY set(EXAMPLE_LIST + audio_host bare_api cdc_msc_hid cdc_msc_hid_freertos diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 072adbbf9..2ad3f40d6 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -1,22 +1,25 @@ # USB Audio Host Example -This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) to communicate with a UAC 1.0 compatible USB Audio Device. +This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) to capture audio from a UAC 1.0 compatible USB microphone and echo it back to the speaker, using a WASAPI/ALSA-like high-level API. The application never touches USB interfaces, alternate settings, or endpoint addresses — it only selects supported `{format, sample_rate, channels}` configurations by stream index. ## Features - Enumerates and mounts USB Audio Class 1.0 devices -- Receives audio data from IN endpoint (e.g., microphone) -- Sends audio data to OUT endpoint (e.g., speaker) -- Sets sampling frequency via control requests -- Demonstrates isochronous transfer handling +- Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) +- Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 kHz fallback; stereo preferred, mono accepted) +- Echoes captured audio to an S16_LE playback stream at the same sample rate (same channel count preferred, mono/stereo conversion otherwise) +- Frame-based FIFO API: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules the 1 ms isochronous transfers +- Cycles the streams through three phases (5 s each): mic-only (capture, data dropped), spk-only (sine test tone), and echo (capture looped back to playback) ## Supported Devices -This example supports any UAC 1.0 compliant USB Audio device, such as: +This example supports any UAC 1.0 compliant USB audio device with a discrete sampling-frequency capture stream, such as: - USB microphones -- USB speakers/headphones +- USB headsets (mono microphone + speaker) - USB audio interfaces +The echo needs a matching S16_LE playback stream at the capture sample rate; devices without one run capture-only. The sample rate and channel preferences are configured by the `SAMPLE_RATES` / `AUDIO_MAX_CHANNELS` macros in `src/audio_app.c` (48 kHz stereo by default). Continuous sampling-frequency ranges are exposed as a single configuration at the range's highest frequency (e.g. a 8000–48000 Hz speaker appears as 48000 Hz); non-PCM formats are rejected by the driver. + ## Building ### Using CMake (recommended) @@ -53,46 +56,40 @@ make BOARD= flash 2. Connect a USB Audio device (UAC 1.0) to the USB host port 3. Open a serial terminal to view output 4. The example will: - - Print device information when mounted - - Set sampling frequency based on the device's advertised capabilities - - Receive audio samples from the device (IN endpoint) - - Loop back received audio to the device (OUT endpoint) for testing + - Print each stream's supported configurations when mounted + - Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 kHz fallback; stereo preferred, mono accepted) and configure it + - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) + - Drain the capture FIFO in `audio_app_task_read()` and queue the frames into the playback FIFO; a sine test tone plays on the playback stream when no capture stream is echoing + - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; a failed stream is restarted automatically 100 ms after the error callback ## Serial Output Example ``` TinyUSB Host USB Audio Example Connect a USB Audio Device (UAC 1.0) to test -Audio device mounted: idx=0, daddr=1 - --- Microphone --- - IN EP: 0x81 (max size: 192) - Input Terminal: ID=1, Type=0x0201, Channels=1 - Format Type: 1, Channels: 1, SubFrameSize: 2, BitResolution: 16 - Sampling Freq: Discrete, count=4 - Freq[0]: 44100 Hz - Freq[1]: 48000 Hz - Freq[2]: 96000 Hz - Freq[3]: 192000 Hz - --- Speaker --- - OUT EP: 0x02 (max size: 192) - Output Terminal: ID=2, Type=0x0301 - Format Type: 1, Channels: 2, SubFrameSize: 2, BitResolution: 16 - Sampling Freq: Continuous range 8000 Hz - 48000 Hz - Feature Unit: ID=3, SourceID=1 - Setting IN sampling frequency to 48000 Hz - Setting OUT sampling frequency to 48000 Hz - Sampling frequency set OK, ready for isochronous transfer +Audio device mounted: idx=0 addr=1 + capture stream 1 configurations: 2 + [0] format=1 rate=44100 channels=2 + [1] format=1 rate=48000 channels=2 + playback stream 0 configurations: 2 + [0] format=1 rate=44100 channels=2 + [1] format=1 rate=48000 channels=2 + Configuring 48 kHz S16_LE capture (2 channels) + Microphone configured, starting capture + Configuring 48 kHz S16_LE playback (2 channels) + Speaker configured, starting playback ``` ## Configuration Edit `src/tusb_config.h` to modify: - `CFG_TUH_AUDIO_MAX`: Maximum number of audio devices supported -- `CFG_TUH_AUDIO_EPIN_BUFSIZE`: IN endpoint buffer size -- `CFG_TUH_AUDIO_EPOUT_BUFSIZE`: OUT endpoint buffer size +- `CFG_TUH_AUDIO_EPIN_BUFSIZE`: Maximum size of one capture transfer the driver submits (configurations needing a larger per-poll-interval packet are rejected) +- `CFG_TUH_AUDIO_EPOUT_BUFSIZE`: Maximum size of one playback transfer the driver submits +- `CFG_TUH_AUDIO_STREAM_BUFSIZE`: Per-stream FIFO depth in bytes (default 1024, i.e. four 256 B packets) ## Notes -- This example uses isochronous transfers which require precise timing -- For production applications, synchronize audio transfers with the device's audio clock -- The example sends a simple sine wave for testing; replace with actual audio data in real applications +- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers are naturally paced at the 1 ms USB frame rate. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` report each completed transfer; `tuh_audio_err_cb()` reports failures. The example restarts the failed stream automatically 100 ms after the error callback. +- `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually queued/read (0 when the FIFO is empty/full or the stream is not running), and `tuh_audio_read_available()` / `tuh_audio_write_available()` report the FIFO occupancy in frames. +- Isochronous transfers require the host to poll `tuh_task()` continuously; the capture FIFO absorbs short scheduling gaps, but frames are dropped when it overflows. diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h index a807aeaa6..3ebb5c16d 100644 --- a/examples/host/audio_host/src/app.h +++ b/examples/host/audio_host/src/app.h @@ -21,6 +21,7 @@ #include #include -void audio_app_task(void); - +void audio_app_task_read(void); +void audio_app_task_write(void); +void defer_queue_task(void); #endif diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index d9134f089..40ad620bf 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -15,6 +15,7 @@ */ #include +#include #include "bsp/board_api.h" #include "tusb.h" #include "app.h" @@ -23,199 +24,488 @@ // MACRO TYPEDEF CONSTANT ENUM DECLARATION //--------------------------------------------------------------------+ -static bool audio_mounted = false; -static uint8_t audio_dev_addr = 0xFF; -static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer -static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state -static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state -static uint8_t audio_idx = 0xFF; -static uint8_t audiostream_in_idx = 0xFF; -static uint8_t audiostream_out_idx = 0xFF; -static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) -static uint8_t audio_mic_channels = 1; +// Default configuration of this example, adjust to the target device: +// - AUDIO_MAX_FRAME_COUNT: buffer holds up to 48 frames (1 ms of 48 kHz) +// - AUDIO_MAX_CHANNELS: maximum channels of the capture/playback stream +// - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default) +#define AUDIO_MAX_FRAME_COUNT 48 +#define AUDIO_MAX_CHANNELS 2 +#define SAMPLE_RATES {48000, 44100} +static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device +static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index +static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index +static bool mic_ready = false; // capture stream is running +static bool spk_ready = false; // playback stream is running +static int16_t mic_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // capture FIFO read buffer +static int16_t spk_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // playback FIFO write buffer +static tuh_audio_stream_config_t mic_config; // selected capture configuration +static tuh_audio_stream_config_t spk_config; // selected playback configuration +static uint32_t audio_frame_count = AUDIO_MAX_FRAME_COUNT; // frames per ms of the selected rate +static uint32_t spk_cb_count = 0; // count of playback callbacks (for debug) +static uint32_t mic_cb_count = 0; // count of capture callbacks (for debug) +static uint32_t err_cb_count = 0; // count of error callbacks (for debug) -static uint8_t audio_rx_buffer[CFG_TUH_AUDIO_EPIN_BUFSIZE] __attribute__((aligned(4))); -static uint8_t audio_tx_buffer[CFG_TUH_AUDIO_EPOUT_BUFSIZE] __attribute__((aligned(4))); //--------------------------------------------------------------------+ // Helper Functions //--------------------------------------------------------------------+ -// Mono (96 bytes, 48 samples) -> Stereo (192 bytes) -static void mono_to_stereo(const uint8_t *mono, uint8_t *stereo, uint16_t mono_samples) { - for (uint16_t i = 0; i < mono_samples; i++) { - // Copy 2 bytes (one int16 sample) to left channel - stereo[i * 4] = mono[i * 2]; - stereo[i * 4 + 1] = mono[i * 2 + 1]; - // Copy same 2 bytes to right channel - stereo[i * 4 + 2] = mono[i * 2]; - stereo[i * 4 + 3] = mono[i * 2 + 1]; +//--------------------------------------------------------------------+ +// Async Deferred Call Queue +//--------------------------------------------------------------------+ +// Schedules one-shot callbacks to be invoked after a given delay in ms. +// Processed by defer_queue_task() in the main loop, no dynamic allocation. + +#define APP_DEFER_QUEUE_SZ 4 + +typedef void (*app_defer_func_t)(uintptr_t param); + +typedef struct { + app_defer_func_t func; + uintptr_t arg; + uint32_t at_ms; +} app_defer_t; + +static app_defer_t _defer_q[APP_DEFER_QUEUE_SZ]; + +// Clear all pending deferred callbacks. +static void app_defer_queue_clear(void) { + memset(_defer_q, 0, sizeof(_defer_q)); +} + +// Schedule func to be called after 'ms' milliseconds, returns false if queue is full +static bool app_defer_ms_async(uint32_t ms, app_defer_func_t func, uintptr_t arg) { + for (uint8_t i = 0; i < APP_DEFER_QUEUE_SZ; i++) { + if (_defer_q[i].func == NULL) { + _defer_q[i].func = func; + _defer_q[i].arg = arg; + // add one to ensure we wait at least 'ms' milliseconds + _defer_q[i].at_ms = tusb_time_millis_api() + ms + 1; + return true; + } } + return false; // queue full } -// Print sampling frequency info for an AS interface -static void print_sampling_freq(const tuh_audio_as_info_t *as) { - if (as->sam_freq_type == 0) { - printf(" Sampling Freq: Continuous range %lu Hz - %lu Hz\r\n", (unsigned long)as->sam_freq_lower, - (unsigned long)as->sam_freq_upper); - } else { - printf(" Sampling Freq: Discrete, count=%u\r\n", as->sam_freq_type); - for (uint8_t j = 0; j < as->sam_freq_type && j < CFG_TUH_AUDIO_MAX_SAM_FREQ; j++) { - printf(" Freq[%u]: %lu Hz\r\n", j, (unsigned long)as->sam_freq[j]); +// Invoke all callbacks whose delay has expired, must be called periodically from main loop +void defer_queue_task(void) { + const uint32_t now_ms = tusb_time_millis_api(); + for (uint8_t i = 0; i < APP_DEFER_QUEUE_SZ; i++) { + if (_defer_q[i].func != NULL && (int32_t)(_defer_q[i].at_ms - now_ms) <= 0) { + const app_defer_func_t func = _defer_q[i].func; + const uintptr_t arg = _defer_q[i].arg; + _defer_q[i].func = NULL; // free slot before invoking, callback may re-schedule + func(arg); } } } -// Print all AS interface info -static void print_as_interfaces(uint8_t idx) { - tuh_audio_as_info_t as = {}; - uint8_t as_count = tuh_audio_as_get_count(idx); - for (uint8_t i = 0; i < as_count; i++) { - tuh_audio_as_get_info(idx, i, &as); - if (as.ep_dir == TUSB_DIR_IN) { - // Save microphone channel count for mono-to-stereo conversion - audio_mic_channels = as.num_channels; - printf(" --- Microphone (AS %u) ---\r\n", i); - printf(" IN EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); - } else { - printf(" --- Speaker (AS %u) ---\r\n", i); - printf(" OUT EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); +// Duplicate each mono sample to both channels (mono mic -> stereo speaker) +static void mono_to_stereo(const int16_t *mono, int16_t *stereo, uint32_t frames) { + for (uint32_t i = 0; i < frames; i++) { + stereo[i * 2] = mono[i]; + stereo[i * 2 + 1] = mono[i]; + } +} + +// Average both channels into one sample (stereo mic -> mono speaker) +static void stereo_to_mono(const int16_t *stereo, int16_t *mono, uint32_t frames) { + for (uint32_t i = 0; i < frames; i++) { + mono[i] = (int16_t)(((int32_t)stereo[i * 2] + stereo[i * 2 + 1]) / 2); + } +} + +// One period of an 8 kHz sine (6 samples at 48 kHz), scaled to ~8-bit +// amplitude. The test tone plays only when no capture stream is echoing. +static const int16_t sine_period[6] = {0, 221, 221, 0, -221, -221}; + +// Precompute a sine wave into the playback buffer +static void spk_init_sine(void) { + for (uint32_t i = 0; i < AUDIO_MAX_FRAME_COUNT; i++) { + const int16_t sample = sine_period[i % 6]; + for (uint8_t ch = 0; ch < spk_config.channels; ch++) { + spk_samples[i * AUDIO_MAX_CHANNELS + ch] = sample; } - printf(" Interface: %u, Alt: %u\r\n", as.interface_num, as.alt_setting); - printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as.format_type, - as.num_channels, as.sub_frame_size, as.bit_resolution); - print_sampling_freq(&as); } } +// Frames to queue this millisecond at the given sample rate: rate / 1000, +// with the fractional remainder (0.1 frame per ms at 44.1 kHz) accumulated +// and paid back as one extra frame, matching the driver's playback pacing. +static uint32_t frame_rem_acc = 0; +static uint32_t audio_frames_this_ms(uint32_t sample_rate) { + uint32_t frames = sample_rate / 1000; + frame_rem_acc += sample_rate % 1000; + if (frame_rem_acc >= 1000) { + frame_rem_acc -= 1000; + frames++; + } + return frames; +} + //--------------------------------------------------------------------+ -// Application Task +// Periodic Stream Switching //--------------------------------------------------------------------+ -void audio_app_task(void) { - if (!audio_mounted || !audio_ready) { - return; +// Cycles through three phases with tuh_audio_start()/stop(). The driver +// activates/deactivates the stream's interface (SET_INTERFACE alt setting) +// on each switch. +// 1. mic only (3 s): capture runs, captured data is dropped +// 2. spk only (5 s): playback plays the sine test tone +// 3. echo (5 s): both streams run, captured audio is echoed back +#define APP_PHASE_MIC_ONLY_MS 5000 +#define APP_PHASE_SPK_ONLY_MS 5000 +#define APP_PHASE_ECHO_MS 5000 + +enum { + APP_PHASE_MIC_ONLY = 0, + APP_PHASE_SPK_ONLY, + APP_PHASE_ECHO, + APP_PHASE_COUNT +}; + +static uint8_t app_audio_phase = APP_PHASE_MIC_ONLY; +static const uint32_t app_phase_ms[APP_PHASE_COUNT] = {APP_PHASE_MIC_ONLY_MS, APP_PHASE_SPK_ONLY_MS, APP_PHASE_ECHO_MS}; + +// Start or stop the capture/playback streams according to the current phase. +// The app tasks already behave per phase: with mic_ready false the sine tone +// plays, with the playback stream stopped the echo write returns 0 (dropped). +static void app_audio_phase_apply(void) { + switch (app_audio_phase) { + case APP_PHASE_MIC_ONLY: + if (!mic_ready) { + mic_ready = tuh_audio_start(audio_idx, cap_stream_idx); + } + if (spk_ready) { + spk_ready = !tuh_audio_stop(audio_idx, spk_stream_idx); + } + printf(" Phase %u: mic on, spk off (data dropped)\r\n", app_audio_phase); + break; + case APP_PHASE_SPK_ONLY: + if (mic_ready) { + mic_ready = !tuh_audio_stop(audio_idx, cap_stream_idx); + } + if (!spk_ready) { + spk_ready = tuh_audio_start(audio_idx, spk_stream_idx); + } + printf(" Phase %u: mic off, spk on (sine)\r\n", app_audio_phase); + break; + case APP_PHASE_ECHO: + if (!mic_ready) { + mic_ready = tuh_audio_start(audio_idx, cap_stream_idx); + } + if (!spk_ready) { + spk_ready = tuh_audio_start(audio_idx, spk_stream_idx); + } + printf(" Phase %u: mic + spk on (echo)\r\n", app_audio_phase); + break; + default: + break; } +} - if (!audio_rx_busy) { - if (tuh_audio_receive(audio_idx, audiostream_in_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { - audio_rx_busy = true; - } +// Enter a phase, then schedule the next switch after this phase's duration +static void app_audio_phase_enter(uintptr_t phase) { + app_audio_phase = (uint8_t)phase; + // Cancel stale deferred callbacks (e.g. a stream restart scheduled on a + // transfer error) so they cannot re-start a stream this phase stops. + app_defer_queue_clear(); + app_audio_phase_apply(); + const uint8_t next_phase = (uint8_t)((app_audio_phase + 1) % APP_PHASE_COUNT); + app_defer_ms_async(app_phase_ms[app_audio_phase], (app_defer_func_t)app_audio_phase_enter, next_phase); +} + + +//--------------------------------------------------------------------+ +// Blinking Task +//--------------------------------------------------------------------+ +void led_blinking_task(void) { + const uint32_t interval_ms = 1000; + static uint32_t start_ms = 0; + + static bool led_state = false; + + // Blink every interval ms + if (tusb_time_millis_api() - start_ms < interval_ms) { + return; // not enough time } + start_ms += interval_ms; + + board_led_write(led_state); + led_state = 1 - led_state; // toggle +#if 1 + printf(" MIC CB=%lu SPK CB=%lu ERR CB=%lu\r\n", (unsigned long)mic_cb_count, (unsigned long)spk_cb_count, + (unsigned long)err_cb_count); + mic_cb_count = 0; + spk_cb_count = 0; + err_cb_count = 0; + +#endif +#if 0 + // Print the current Feature Unit volume, which is set to 0x0600 in mic_configured() and can be changed by the device. + uint16_t volume = 0x0001; + tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); + uint16_t mute = 0x0000; + tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_MUTE, 0, &mute); + mute=!mute; // toggle mute for demonstration + tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_MUTE, 0, mute); + printf(" Feature Unit mute set: 0x%04x\r\n", (unsigned int)mute); +#endif } //--------------------------------------------------------------------+ -// TinyUSB Callbacks +// Application Task //--------------------------------------------------------------------+ -void tuh_audio_mount_cb(uint8_t idx) { - if (idx >= CFG_TUH_AUDIO_MAX) { - printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); +// Echo the captured audio back to the playback stream: drain the capture +// FIFO into mic_samples, convert, and queue the frames into the playback +// FIFO. The driver schedules the actual isochronous transfers. + +void audio_app_task_read(void) { + if (!mic_ready) { return; } - print_as_interfaces(idx); + const uint32_t frames = + tuh_audio_read(audio_idx, cap_stream_idx, mic_samples, audio_frames_this_ms(mic_config.sample_rate)); + if (frames == 0) { + return; + } - // Save device info - audio_dev_addr = tuh_audio_get_dev_addr(idx); - audio_idx = idx; - audio_mounted = true; + if (spk_config.channels == mic_config.channels) { + memcpy(spk_samples, mic_samples, frames * mic_config.channels * sizeof(int16_t)); + } else if (mic_config.channels == 1 && spk_config.channels == 2) { + mono_to_stereo(mic_samples, spk_samples, frames); + } else { + stereo_to_mono(mic_samples, spk_samples, frames); + } - // Find endpoints and IN sampling frequency - tuh_audio_as_info_t as; - for (uint8_t i = 0; i < tuh_audio_as_get_count(idx); i++) { + (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); +} - tuh_audio_as_get_info(idx, i, &as); - if (as.ep_dir == TUSB_DIR_IN) { - audiostream_in_idx = i; - if (as.sam_freq_type > 0) { - sampling_freq = as.sam_freq[0]; - } - } else { - audiostream_out_idx = i; - } +void audio_app_task_write(void) { + // Fallback: the sine test tone when no capture stream is echoing + if (mic_ready || !spk_ready) { + return; } - // Set IN sampling frequency before starting isochronous transfer - if (audiostream_in_idx != 0xFF && sampling_freq != 0) { - printf(" Setting IN sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); - // tuh_audio_set_sampling_freq(audio_idx, audiostream_in_idx, sampling_freq, in_sampling_freq_set_cb, 0); + const uint32_t frames = audio_frames_this_ms(spk_config.sample_rate); + if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) { + (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); + } +} - tusb_xfer_result_t result; - result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_in_idx, sampling_freq); - if (result == XFER_RESULT_SUCCESS) { - tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_in_idx, &sampling_freq); - printf(" IN sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); - if (audiostream_out_idx != 0xFF) { - printf(" Setting OUT sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); - result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_out_idx, sampling_freq); - if (result == XFER_RESULT_SUCCESS) { - tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_out_idx, &sampling_freq); - printf(" OUT sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); - } else { - printf(" Setting OUT sampling frequency FAILED: result=%u\r\n", result); - } - } - } else { - printf(" Setting IN sampling frequency FAILED: result=%u\r\n", result); +// Invoked when an isochronous IN transfer completes: the captured data is +// already queued into the capture FIFO and drained by audio_app_task_read(). +void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; + mic_cb_count++; +} + +// Invoked when an isochronous OUT transfer completes: the next queued packet +// is submitted from the playback FIFO by the driver. +void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; + spk_cb_count++; +} + +// Re-open a stream stopped by a transfer error: the driver keeps the stream +// configured, so tuh_audio_start() resumes it. Invoked deferred so repeated +// errors cannot stall the main loop. +static void audio_app_restart_stream(uintptr_t param) { + const uint8_t idx = (uint8_t)(param >> 8); + const uint8_t stream_idx = (uint8_t)param; + if (!tuh_audio_mounted(idx)) { + return; // device is gone + } + if (stream_idx == cap_stream_idx) { + printf(" Restarting capture stream %u\r\n", stream_idx); + mic_ready = tuh_audio_start(idx, stream_idx); + } else if (stream_idx == spk_stream_idx) { + printf(" Restarting playback stream %u\r\n", stream_idx); + spk_ready = tuh_audio_start(idx, stream_idx); + } +} + +// Invoked when an isochronous transfer fails: the stream was stopped by the +// driver, re-open it after a short delay so the device can recover. +void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)xferred_bytes; + err_cb_count++; + printf(" AUDIO transfer error: addr=%u stream=%u xferred_bytes=%u\r\n", idx, stream_idx, (unsigned)xferred_bytes); + app_defer_ms_async(100, (app_defer_func_t)audio_app_restart_stream, ((uintptr_t)idx << 8) | stream_idx); +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +// Print all supported stream configurations +static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { + const tuh_audio_direction_t dir = tuh_audio_stream_direction(idx, stream_idx); + const char *dir_name = (dir == TUH_AUDIO_STREAM_CAPTURE) ? "capture" : "playback"; + printf(" %s stream %u configurations: %u\r\n", dir_name, stream_idx, tuh_audio_config_count(idx, stream_idx)); + for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { + tuh_audio_stream_config_t config; + if (tuh_audio_config_get(idx, stream_idx, i, &config)) { + printf(" [%u] format=%u rate=%lu channels=%u\r\n", i, (unsigned)config.format, + (unsigned long)config.sample_rate, (unsigned)config.channels); } - uint16_t volume = 0x0600; + } +} + +// Invoked when the configuration selected by tuh_audio_configure() completes +static void mic_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { + (void)user_data; + + if (idx == audio_idx && stream_idx == cap_stream_idx && result == XFER_RESULT_SUCCESS) { + printf(" Microphone configured, starting capture\r\n"); + mic_ready = tuh_audio_start(idx, stream_idx); - result = tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); + uint16_t volume = 0x0600; + result = tuh_audio_feature_unit_set_sync(idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); if (result == XFER_RESULT_SUCCESS) { printf(" Feature Unit volume set:volume 0x%04x\r\n", (unsigned int)volume); - tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + tuh_audio_feature_unit_get_sync(idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); } else { printf(" Setting Feature Unit volume FAILED: result=%u\r\n", result); } + } else { + printf(" Microphone configuration failed: result=%u\r\n", result); } - audio_ready = true; } +// Invoked when the playback configuration selected by tuh_audio_configure() completes +static void spk_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { + (void)user_data; + if (idx == audio_idx && stream_idx == spk_stream_idx && result == XFER_RESULT_SUCCESS) { + printf(" Speaker configured, starting playback\r\n"); + spk_ready = tuh_audio_start(idx, stream_idx); + // playback-only device: set the frame cadence from the selected rate + audio_frame_count = spk_config.sample_rate / 1000; + spk_init_sine(); // fallback test tone while no capture stream is echoing + + // both streams running: start the periodic phase switching demo + if (mic_ready && spk_ready) { + app_audio_phase_enter(APP_PHASE_MIC_ONLY); + } + } else { + printf(" Speaker configuration failed: result=%u\r\n", result); + } +} // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx) { printf("Audio device unmounted: idx=%u\r\n", idx); - if (audio_mounted && audio_idx == idx) { - audio_mounted = false; - audio_ready = false; - audio_rx_busy = false; - audio_tx_busy = false; - audio_dev_addr = 0; - audio_idx = 0; - audiostream_in_idx = 0xFF; - audiostream_out_idx = 0xFF; - } -} - -// Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)dev_addr; - (void)ep_addr; - audio_rx_busy = false; - - if (xferred_bytes > 0 && audiostream_out_idx != 0xFF && !audio_tx_busy) { - bool ok; - if (audio_mic_channels == 1) { - // Mono microphone, convert to stereo and send to OUT endpoint - uint16_t samples = xferred_bytes / 2; - mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); - ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_tx_buffer, xferred_bytes * 2); - } else { - // Stereo microphone, send directly to OUT endpoint - ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_rx_buffer, xferred_bytes); + if (idx == audio_idx) { + app_defer_queue_clear(); + audio_idx = TUSB_INDEX_INVALID_8; + cap_stream_idx = TUSB_INDEX_INVALID_8; + spk_stream_idx = TUSB_INDEX_INVALID_8; + mic_ready = false; + spk_ready = false; + } +} + +void tuh_audio_mount_async(uintptr_t param) { + uint8_t idx = (uint8_t)param; + if (idx >= CFG_TUH_AUDIO_MAX) { + printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); + return; + } + + printf("Audio device mounted: idx=%u addr=%u\r\n", idx, tuh_audio_get_dev_addr(idx)); + + // Inspect every stream and print its supported configurations + for (uint8_t stream_idx = 0; stream_idx < tuh_audio_stream_count(idx); stream_idx++) { + if (!tuh_audio_stream_exists(idx, stream_idx)) { + continue; } + print_stream_configs(idx, stream_idx); + } - if (ok) { - audio_tx_busy = true; + // Select a supported 48 kHz S16_LE capture configuration without + // accessing USB interfaces, alternate settings, or endpoint addresses. + // Sample rates are tried in SAMPLE_RATES order (44.1 kHz first), stereo is + // preferred, mono is accepted. + static const uint32_t sample_rates[] = SAMPLE_RATES; + bool capture_found = false; + for (uint8_t r = 0; r < TU_ARRAY_SIZE(sample_rates) && !capture_found; r++) { + const uint32_t sample_rate = sample_rates[r]; + for (uint8_t stream_idx = 0; stream_idx < tuh_audio_stream_count(idx) && !capture_found; stream_idx++) { + // Only consider capture streams, ignore playback streams + if (tuh_audio_stream_direction(idx, stream_idx) != TUH_AUDIO_STREAM_CAPTURE) { + continue; + } + for (uint8_t ch = AUDIO_MAX_CHANNELS; ch >= 1 && !capture_found; ch--) { + for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { + tuh_audio_stream_config_t config; + // Check for a matching sample rate S16_LE configuration with the desired channel count + if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE && + config.sample_rate == sample_rate && config.channels == ch) { + audio_idx = idx; + cap_stream_idx = stream_idx; + mic_config = config; + // one ms of audio at the selected rate, rounded down to whole frames + audio_frame_count = sample_rate / 1000; + printf(" Configuring %u S16_LE capture (%u channels)\r\n", (unsigned)sample_rate, config.channels); + // Configure the selected capture stream and start it through the callback. + (void)tuh_audio_configure(idx, stream_idx, i, mic_configured, 0); + capture_found = true; + break; + } + } + } + } + } + if (!capture_found) { + printf(" No supported 48/44.1 kHz S16_LE capture configuration found\r\n"); + } + + // The echo needs a playback stream at the capture sample rate (or at any + // preferred rate when no capture stream exists, for the sine fallback). + // Prefer the same channel count as the capture stream (direct echo), then + // the other one (converted). + uint8_t playback_config_idx = TUSB_INDEX_INVALID_8; + for (uint8_t r = 0; r < TU_ARRAY_SIZE(sample_rates) && playback_config_idx == TUSB_INDEX_INVALID_8; r++) { + const uint32_t sample_rate = capture_found ? mic_config.sample_rate : sample_rates[r]; + for (uint8_t stream_idx = 0; + stream_idx < tuh_audio_stream_count(idx) && playback_config_idx == TUSB_INDEX_INVALID_8; stream_idx++) { + // Only consider playback streams, ignore capture streams + if (tuh_audio_stream_direction(idx, stream_idx) != TUH_AUDIO_STREAM_PLAYBACK) { + continue; + } + for (uint8_t n = 0; n < 2 && playback_config_idx == TUSB_INDEX_INVALID_8; n++) { + const uint8_t ch = (n == 0) ? mic_config.channels : (uint8_t)(mic_config.channels == 1 ? 2 : 1); + for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { + tuh_audio_stream_config_t config; + if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE && + config.sample_rate == sample_rate && config.channels == ch) { + spk_stream_idx = stream_idx; + spk_config = config; + playback_config_idx = i; + break; + } + } + } } } + if (playback_config_idx == TUSB_INDEX_INVALID_8) { + printf(" No supported %u S16_LE playback configuration, echo disabled\r\n", + (unsigned)(capture_found ? mic_config.sample_rate : sample_rates[0])); + return; + } + printf(" Configuring %u S16_LE playback (%u channels)\r\n", (unsigned)spk_config.sample_rate, spk_config.channels); + // Configure the selected playback stream and start it through the callback. + (void)tuh_audio_configure(idx, spk_stream_idx, playback_config_idx, spk_configured, 0); } -// Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { - (void)dev_addr; - (void)ep_addr; - (void)xferred_bytes; - audio_tx_busy = false; +// Invoked when device with Audio interface is mounted +void tuh_audio_mount_cb(uint8_t idx) { + app_defer_ms_async(100, (app_defer_func_t)tuh_audio_mount_async, idx); } diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c index b80cd2938..77c28cf41 100644 --- a/examples/host/audio_host/src/main.c +++ b/examples/host/audio_host/src/main.c @@ -35,39 +35,17 @@ int main(void) { printf("Connect a USB Audio Device (UAC 1.0) to test\r\n"); // init host stack on configured roothub port - tusb_rhport_init_t host_init = { - .role = TUSB_ROLE_HOST, - .speed = TUSB_SPEED_AUTO - }; + tusb_rhport_init_t host_init = {.role = TUSB_ROLE_HOST, .speed = TUSB_SPEED_AUTO}; tusb_init(BOARD_TUH_RHPORT, &host_init); board_init_after_tusb(); - + uint32_t last_ms = tusb_time_millis_api(); while (1) { // tinyusb host task tuh_task(); led_blinking_task(); - audio_app_task(); + audio_app_task_read(); + audio_app_task_write(); + defer_queue_task(); } } - -//--------------------------------------------------------------------+ -// TinyUSB Callbacks -//--------------------------------------------------------------------+ - -//--------------------------------------------------------------------+ -// Blinking Task -//--------------------------------------------------------------------+ -void led_blinking_task(void) { - const uint32_t interval_ms = 1000; - static uint32_t start_ms = 0; - - static bool led_state = false; - - // Blink every interval ms - if ( tusb_time_millis_api() - start_ms < interval_ms) return; // not enough time - start_ms += interval_ms; - - board_led_write(led_state); - led_state = 1 - led_state; // toggle -} diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h index 4a7a6ad56..9b7f3c94b 100644 --- a/examples/host/audio_host/src/tusb_config.h +++ b/examples/host/audio_host/src/tusb_config.h @@ -81,9 +81,9 @@ extern "C" { #define CFG_TUH_ENUMERATION_BUFSIZE 512 -#define CFG_TUH_HUB 1 +#define CFG_TUH_HUB 0 #define CFG_TUH_CDC 0 -#define CFG_TUH_HID 1 +#define CFG_TUH_HID 0 #define CFG_TUH_MSC 0 #define CFG_TUH_VENDOR 0 #define CFG_TUH_AUDIO 1 @@ -93,8 +93,8 @@ extern "C" { //------------- Audio Host Config -------------// #define CFG_TUH_AUDIO_MAX 2 -#define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 -#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 +#define CFG_TUH_AUDIO_EPIN_BUFSIZE 256 // max capture transfer the application submits +#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256 // max playback transfer the application submits #ifdef __cplusplus } diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index c74a95d48..f943a5b2f 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -6,23 +6,43 @@ */ /* - * This driver implements a USB Audio Host (UAC 1.0) class driver. - * It supports multiple Audio Streaming (AS) interfaces with independent format storage. - * Each AS interface can have its own sample rate, channel count, bit resolution, - * and endpoint configuration. + * This driver implements a USB Audio Host (UAC 1.0) class driver with a + * WASAPI/ALSA-like high-level streaming API. The USB Audio topology (Audio + * Control interface, Audio Streaming interfaces, alternate settings, and + * endpoints) is kept private to the driver. * - * The driver handles: - * 1. Audio Control (AC) interface parsing — Input Terminal, Output Terminal, - * and Feature Unit descriptors. - * 2. Audio Streaming (AS) interface enumeration — multiple AS interfaces with - * alternate settings, each storing its own format information. - * 3. Isochronous IN/OUT endpoint management for audio data transfer. - * 4. Asynchronous control transfers for sample frequency get/set. + * Each instance (Audio Control interface) provides at most one logical stream + * per direction: + * - capture stream (TUSB_DIR_IN): device -> host, filled by isochronous IN + * transfers scheduled by the driver into a FIFO, drained by the application + * with tuh_audio_read() + * - playback stream (TUSB_DIR_OUT): host -> device, drained by isochronous + * OUT transfers from a FIFO filled by the application with tuh_audio_write() * - * In case you need to adjust the number of supported AS interfaces, change - * CFG_TUH_AUDIO_MAX_AS in your tusb_config.h. + * While a stream is running, the driver keeps one isochronous transfer in + * flight (a natural 1 ms frame cadence) and re-submits on completion. The + * FIFO + endpoint-claim pattern is modeled after the tu_edpt_stream helper + * used by the MIDI host driver: the application's frame-based read/write is + * decoupled from the USB transfer cadence, and only whole frames are ever + * queued or transferred. Completion of each transfer is reported through + * tuh_audio_capture_cb()/tuh_audio_playback_cb(), failures through + * tuh_audio_err_cb(). * - * */ + * The supported configurations of all Audio Streaming interfaces and alternate + * settings in one direction are combined into a flat list of discrete + * {format, sample_rate, channels} tuples. The driver keeps the mapping from + * each configuration to its interface, alternate setting, and endpoint, and + * applies it when the application calls tuh_audio_configure(). + * + * Non-PCM formats are rejected explicitly during enumeration. A continuous + * sampling-frequency range is exposed as a single configuration at the + * range's highest sampling frequency. + * + * The driver owns: + * 1. Endpoint selection and opening (only the alternate setting selected by + * tuh_audio_configure() is ever activated). + * 2. Endpoint sampling-frequency control (SET_CUR, 3 bytes little-endian). + */ #include "tusb_option.h" @@ -43,7 +63,6 @@ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ - TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx) { (void)idx; } @@ -52,58 +71,128 @@ TU_ATTR_WEAK void tuh_audio_umount_cb(uint8_t idx) { (void)idx; } -TU_ATTR_WEAK void tuh_audio_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { +TU_ATTR_WEAK void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)idx; - (void)ep_addr; + (void)stream_idx; (void)xferred_bytes; } -TU_ATTR_WEAK void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { +TU_ATTR_WEAK void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)idx; - (void)ep_addr; + (void)stream_idx; (void)xferred_bytes; } -//--------------------------------------------------------------------+ -// MACRO CONSTANT TYPEDEF -//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; +} + + //--------------------------------------------------------------------+ + // MACRO CONSTANT TYPEDEF + //--------------------------------------------------------------------+ + + // Maximum number of supported configurations per stream (per direction) + #define AUDIOH_MAX_CONFIGS (CFG_TUH_AUDIO_MAX_AS * CFG_TUH_AUDIO_MAX_SAM_FREQ) + // Maximum number of interfaces in the AC header's interface collection + #define AUDIOH_MAX_COLLECTION 16 -// Per-interface storage +// Stream state machine +enum { + STREAM_STATE_IDLE = 0, // not configured, no configuration in progress + STREAM_STATE_CONFIG, // tuh_audio_configure() sequence in progress + STREAM_STATE_READY // configured, ready to start/stop +}; + +// Hardware mapping of one supported configuration +typedef struct { + uint8_t itf_num; // Audio Streaming interface number + uint8_t alt_setting; // alternate setting that provides this configuration + uint8_t ep_addr; // isochronous endpoint address + uint16_t ep_size; // endpoint max packet size + uint8_t ep_interval; // endpoint bInterval + uint8_t ep_sync; // bmAttributes sync type + uint8_t ep_usage; // bmAttributes usage type + bool sam_freq_ctrl; // endpoint supports sampling-frequency control +} audioh_stream_map_t; + +// One logical stream (capture or playback) +typedef struct { + // instance info (set at init, preserved across close/open) + uint8_t idx; // instance index + uint8_t stream_idx; // logical stream index within the instance + tusb_dir_t dir; // TUSB_DIR_IN = capture, TUSB_DIR_OUT = playback + + // device owning this stream (0 = no device) + uint8_t daddr; + + // Supported configurations (parsed during enumeration) + uint8_t config_count; + tuh_audio_stream_config_t config[AUDIOH_MAX_CONFIGS]; + audioh_stream_map_t map[AUDIOH_MAX_CONFIGS]; + + // Active stream state + uint8_t active_config; // index into config[]/map[], TUSB_INDEX_INVALID_8 when not configured + uint8_t state; // STREAM_STATE_* + bool running; // tuh_audio_start() called, transfers may be submitted + + // Size in bytes of one frame (all channels) of the active configuration + uint8_t frame_bytes; + + // Playback pacing: frames the device consumes per USB frame + // (sample_rate / 1000), with the fractional remainder (0.1 frame per ms at + // 44.1 kHz) accumulated on each submission and paid back as one extra frame + uint16_t frames_per_ms; + uint16_t frames_rem; + uint16_t rem_acc; + + // Configure state machine + tuh_audio_configure_cb_t complete_cb; + uintptr_t user_data; + + // FIFO + endpoint transfer helper (see tu_edpt_stream, used by the MIDI + // host driver): the FIFO decouples the application's frame-based read/write + // from the 1 ms isochronous transfer cadence. ep_buf is bound at init from + // _audioh_epbuf[], the endpoint is bound by tu_edpt_stream_open() when the + // stream is configured. + tu_edpt_stream_t edpt; + uint8_t ff_buf[CFG_TUH_AUDIO_STREAM_BUFSIZE]; + + TUH_EPBUF_DEF(ctrl, 4); // sampling-frequency SET data +} tuh_audio_stream_t; + +// Per-instance (Audio device) storage typedef struct { - uint8_t daddr; // device address + uint8_t daddr; // device address (0 = free slot) uint8_t ac_itf_num; // Audio Control interface number - uint8_t itf_count; // number of interfaces (AC + AS) - // Terminal info (from Audio Control Interface) - uint16_t input_terminal_type; // wTerminalType of Input Terminal - uint8_t input_terminal_id; // bTerminalID of Input Terminal - uint8_t input_terminal_channels; // bNrChannels of Input Terminal - uint16_t output_terminal_type; // wTerminalType of Output Terminal - uint8_t output_terminal_id; // bTerminalID of Output Terminal + // Logical streams: playback first, then capture (stream index order) + tuh_audio_stream_t out_stream; + tuh_audio_stream_t in_stream; + uint8_t stream_count; // number of streams with supported configurations // Feature Unit info - uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) - uint8_t feature_unit_source_id; // bSourceID of Feature Unit - - // Multiple AS interfaces support - uint8_t as_count; - uint8_t as_set_idx; - - // Per-AS interface independent storage (new) - tuh_audio_as_info_t as[CFG_TUH_AUDIO_MAX_AS]; // Array of Audio Streaming interface info structures + uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) bool mounted; } audioh_interface_t; typedef struct { - TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); - TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); - TUH_EPBUF_DEF(ctrl, 8); + TUH_EPBUF_DEF(ctrl, 8); // feature-unit SET data + TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); // capture transfer buffer + TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); // playback transfer buffer + // Feature-unit GET chain state: only one GET in flight per device + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + uint16_t *value; + uint8_t width; } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; + //--------------------------------------------------------------------+ // Helper //--------------------------------------------------------------------+ @@ -116,29 +205,266 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { return TUSB_INDEX_INVALID_8; } -static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { +static tuh_audio_stream_t *audioh_get_stream(audioh_interface_t *p_audio, tusb_dir_t direction) { + switch (direction) { + case TUSB_DIR_IN: + return &p_audio->in_stream; + case TUSB_DIR_OUT: + return &p_audio->out_stream; + default: + return NULL; + } +} + +// Look up a stream by its logical index within the instance +static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, uint8_t stream_idx) { + for (uint8_t i = 0; i < 2; i++) { + tuh_audio_stream_t *s = (i == 0) ? &p_audio->out_stream : &p_audio->in_stream; + if (s->config_count > 0 && s->stream_idx == stream_idx) { + return s; + } + } + return NULL; +} + +// Map a UAC 1.0 (subframe size, bit resolution) pair to a supported format +static bool audioh_format_from_uac1(uint8_t subframe_size, uint8_t bit_resolution, tuh_audio_format_t *format) { + if (subframe_size == 1 && bit_resolution == 8) { + *format = TUH_AUDIO_FORMAT_S8; + } else if (subframe_size == 2 && bit_resolution == 16) { + *format = TUH_AUDIO_FORMAT_S16_LE; + } else if (subframe_size == 3 && bit_resolution == 24) { + *format = TUH_AUDIO_FORMAT_S24_3LE; + } else if (subframe_size == 4 && bit_resolution == 24) { + *format = TUH_AUDIO_FORMAT_S24_LE; + } else if (subframe_size == 4 && bit_resolution == 32) { + *format = TUH_AUDIO_FORMAT_S32_LE; + } else { + return false; + } + return true; +} + +// Endpoint poll interval in microseconds: full-speed bInterval is in 1 ms +// frames, high-speed isochronous bInterval is a power-of-2 exponent of +// 125 us microframes +static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) { + if (tuh_speed_get(daddr) == TUSB_SPEED_HIGH) { + return ((uint32_t)1u << (ep_interval - 1)) * 125u; + } + return (uint32_t)ep_interval * 1000u; +} + +// UAC 1.0 feature-unit control value width: mute/AGC/loudness are 1 byte, the rest 2 bytes +static uint8_t audioh_fu_control_width(uint8_t control_selector) { + switch (control_selector) { + case AUDIO10_FU_CTRL_MUTE: + case AUDIO10_FU_CTRL_AGC: + case AUDIO10_FU_CTRL_LOUDNESS: + return 1; + default: + return 2; + } +} + +// Reset a stream to its unconfigured state (keeps idx, dir, and FIFO configuration) +static void audioh_stream_reset(tuh_audio_stream_t *s) { + s->daddr = 0; + s->stream_idx = TUSB_INDEX_INVALID_8; + s->config_count = 0; + s->active_config = TUSB_INDEX_INVALID_8; + s->state = STREAM_STATE_IDLE; + s->running = false; + s->frame_bytes = 0; + s->frames_per_ms = 0; + s->frames_rem = 0; + s->rem_acc = 0; + s->complete_cb = NULL; + tu_edpt_stream_close(&s->edpt); + tu_edpt_stream_clear(&s->edpt); +} + +// Find the stream owning an endpoint (used to dispatch transfer completion) +static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { - const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr) { - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - if (p_audio->as[as_idx].ep_addr == ep_addr) { - return idx; - } + audioh_interface_t *p_audio = &_audioh_itf[idx]; + for (uint8_t s = 0; s < 2; s++) { + tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; + if (stream->daddr == dev_addr && stream->active_config != TUSB_INDEX_INVALID_8 && + stream->map[stream->active_config].ep_addr == ep_addr) { + return stream; } } } - return TUSB_INDEX_INVALID_8; + return NULL; } -static uint8_t audioh_get_ep_addr_by_dir(const audioh_interface_t *p_audio, uint8_t dir) { - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - const tuh_audio_as_info_t *as = &p_audio->as[as_idx]; - if (as->ep_addr != 0 && as->ep_dir == dir) { - return as->ep_addr; - } +//--------------------------------------------------------------------+ +// Packet scheduler +//--------------------------------------------------------------------+ + +// Re-arm the capture endpoint: request one full packet (the device sends at +// most its max packet size per poll interval). Only submit while the whole +// packet fits into the FIFO — otherwise the frame is lost anyway and the +// transfer would be wasted; the stream resumes when tuh_audio_read() frees +// FIFO space. +static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); + + const audioh_stream_map_t *map = &s->map[s->active_config]; + TU_VERIFY(tu_fifo_remaining(&s->edpt.ff) >= map->ep_size, ); + TU_VERIFY(usbh_edpt_claim(s->daddr, map->ep_addr), ); // one transfer in flight + + // ep_size is guaranteed <= CFG_TUH_AUDIO_EPIN_BUFSIZE by enumeration + TU_ASSERT(usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, map->ep_size), ); +} + +// Submit the next queued playback packet. The device consumes +// sample_rate / 1000 frames per USB frame; the fractional remainder +// (0.1 frame per ms at 44.1 kHz) is accumulated on each successful +// submission and paid back as one extra frame, keeping the average data +// rate exactly at the sample rate. Whole frames only, limited by the +// queued data, one endpoint packet, and the transfer buffer. +static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); + + const audioh_stream_map_t *map = &s->map[s->active_config]; + TU_VERIFY(usbh_edpt_claim(s->daddr, map->ep_addr), ); // one transfer in flight + + uint16_t frames = s->frames_per_ms; + s->rem_acc += s->frames_rem; + if (s->rem_acc >= 1000) { + s->rem_acc -= 1000; + frames++; + } + + frames = TU_MIN(frames, (uint16_t)(tu_fifo_count(&s->edpt.ff) / s->frame_bytes)); + frames = TU_MIN(frames, (uint16_t)(map->ep_size / s->frame_bytes)); + frames = TU_MIN(frames, (uint16_t)(CFG_TUH_AUDIO_EPOUT_BUFSIZE / s->frame_bytes)); + if (frames == 0) { + // nothing queued: the stream stays idle until the application writes again + usbh_edpt_release(s->daddr, map->ep_addr); + return; } - return 0; + const uint16_t bytes = frames * s->frame_bytes; + tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes); + TU_ASSERT(usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, bytes), ); +} + +//--------------------------------------------------------------------+ +// Configure state machine +//--------------------------------------------------------------------+ + +static void audioh_stream_fail(tuh_audio_stream_t *s, tusb_xfer_result_t result) { + s->state = STREAM_STATE_IDLE; + s->active_config = TUSB_INDEX_INVALID_8; + s->running = false; + + tuh_audio_configure_cb_t cb = s->complete_cb; + uintptr_t user_data = s->user_data; + s->complete_cb = NULL; + if (cb != NULL) { + cb(s->idx, s->stream_idx, result, user_data); + } +} + +static void audioh_stream_ready(tuh_audio_stream_t *s) { + s->state = STREAM_STATE_READY; + + tuh_audio_configure_cb_t cb = s->complete_cb; + uintptr_t user_data = s->user_data; + s->complete_cb = NULL; + if (cb != NULL) { + cb(s->idx, s->stream_idx, XFER_RESULT_SUCCESS, user_data); + } +} + +static void audioh_stream_set_freq_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_CONFIG) { + return; // device is gone or configuration was aborted + } + + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); + audioh_stream_fail(s, xfer->result); + return; + } + audioh_stream_ready(s); +} + +// Set the endpoint sampling frequency (3 bytes little-endian) when supported +static void audioh_stream_set_freq(tuh_audio_stream_t *s) { + const audioh_stream_map_t *map = &s->map[s->active_config]; + const tuh_audio_stream_config_t *cfg = &s->config[s->active_config]; + + s->ctrl[0] = (uint8_t)(cfg->sample_rate & 0xFF); + s->ctrl[1] = (uint8_t)((cfg->sample_rate >> 8) & 0xFF); + s->ctrl[2] = (uint8_t)((cfg->sample_rate >> 16) & 0xFF); + + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // control selector, channel 0 + .wIndex = tu_htole16(map->ep_addr), + .wLength = 3}; + + tuh_xfer_t xfer = {.daddr = s->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = s->ctrl, + .complete_cb = audioh_stream_set_freq_complete, + .user_data = (uintptr_t)s}; + if (!tuh_control_xfer(&xfer)) { + audioh_stream_fail(s, XFER_RESULT_FAILED); + } +} + +// Reconstruct the endpoint descriptor of the selected configuration and open it +static void audioh_stream_open_ep(tuh_audio_stream_t *s) { + const audioh_stream_map_t *map = &s->map[s->active_config]; + + const tusb_desc_endpoint_t desc_ep = {.bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = map->ep_addr, + .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS, + .sync = map->ep_sync, + .usage = map->ep_usage}, + .wMaxPacketSize = tu_htole16(map->ep_size), + .bInterval = map->ep_interval}; + + if (!tuh_edpt_open(s->daddr, &desc_ep)) { + TU_LOG_DRV(" AUDIO open endpoint failed: addr=%u ep=%02x\r\n", s->daddr, map->ep_addr); + audioh_stream_fail(s, XFER_RESULT_FAILED); + return; + } + + // Bind the transfer helper to the endpoint and start with an empty FIFO + const uint16_t xfer_len = (s->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + tu_edpt_stream_open(&s->edpt, s->daddr, &desc_ep, xfer_len); + tu_edpt_stream_clear(&s->edpt); + + if (map->sam_freq_ctrl) { + audioh_stream_set_freq(s); + } else { + audioh_stream_ready(s); + } +} + +static void audioh_stream_set_interface_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_CONFIG) { + return; // device is gone or configuration was aborted + } + + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO SET_INTERFACE failed: itf=%u alt=%u result=%u\r\n", s->map[s->active_config].itf_num, + s->map[s->active_config].alt_setting, xfer->result); + audioh_stream_fail(s, xfer->result); + return; + } + audioh_stream_open_ep(s); } //--------------------------------------------------------------------+ @@ -146,44 +472,352 @@ static uint8_t audioh_get_ep_addr_by_dir(const audioh_interface_t *p_audio, uint //--------------------------------------------------------------------+ bool audioh_init(void) { tu_memclr(&_audioh_itf, sizeof(_audioh_itf)); + + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + tuh_audio_stream_t *in = &_audioh_itf[idx].in_stream; + tuh_audio_stream_t *out = &_audioh_itf[idx].out_stream; + + in->idx = idx; + in->dir = TUSB_DIR_IN; + out->idx = idx; + out->dir = TUSB_DIR_OUT; + + // Bind FIFO buffer and transfer buffer (see tu_edpt_stream_init) + TU_VERIFY(tu_edpt_stream_init(&in->edpt, true, false, false, in->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, + _audioh_epbuf[idx].epin)); + TU_VERIFY(tu_edpt_stream_init(&out->edpt, true, true, false, out->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, + _audioh_epbuf[idx].epout)); + + audioh_stream_reset(in); + audioh_stream_reset(out); + } return true; } bool audioh_deinit(void) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + tu_edpt_stream_deinit(&_audioh_itf[idx].in_stream.edpt); + tu_edpt_stream_deinit(&_audioh_itf[idx].out_stream.edpt); + } return true; } void audioh_close(uint8_t daddr) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr) { - TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); + if (p_audio->daddr != daddr) { + continue; + } + + TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); + if (p_audio->mounted) { tuh_audio_umount_cb(idx); + } - p_audio->ac_itf_num = 0; - p_audio->daddr = 0; - p_audio->mounted = false; - p_audio->as_count = 0; - p_audio->as_set_idx = 0; - tu_memclr(p_audio->as, sizeof(p_audio->as)); + // Abort a configuration in progress so the application callback still fires + for (uint8_t s = 0; s < 2; s++) { + tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; + if (stream->state == STREAM_STATE_CONFIG && stream->complete_cb != NULL) { + audioh_stream_fail(stream, XFER_RESULT_ABORTED); + } + audioh_stream_reset(stream); } + + _audioh_epbuf[idx].complete_cb = NULL; // drop a pending feature-unit GET + + p_audio->stream_count = 0; + p_audio->daddr = 0; + p_audio->mounted = false; } } bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { - (void)result; - if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { - tuh_audio_rx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); - } else { - tuh_audio_tx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); + tuh_audio_stream_t *s = audioh_find_stream(dev_addr, ep_addr); + if (s == NULL) { + return false; + } + + // Failed, stalled, or aborted transfers never carry valid audio data + if (result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO transfer failed: addr=%u ep=%02x result=%u\r\n", dev_addr, ep_addr, result); + s->running = false; + tu_edpt_stream_clear(&s->edpt); // discard queued data + tuh_audio_err_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + return true; } + // Stopped stream: the in-flight transfer completes and its data is discarded + if (!s->running) { + return true; + } + + if (s->dir == TUSB_DIR_IN) { + // Capture: move the received bytes into the FIFO (whole frames only), + // notify, then re-arm for the next packet + const uint16_t bytes = (uint16_t)(xferred_bytes - (xferred_bytes % s->frame_bytes)); + if (bytes > 0) { + tu_fifo_write_n(&s->edpt.ff, s->edpt.ep_buf, bytes); + } + tuh_audio_capture_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + audioh_stream_capture_xfer(s); + } else { + // Playback: notify, then submit the next queued packet + tuh_audio_playback_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + audioh_stream_playback_xfer(s); + } return true; } //--------------------------------------------------------------------+ // Enumeration //--------------------------------------------------------------------+ + +// AC header interface collection (baInterfaceNr) bounds-checked +typedef struct TU_ATTR_PACKED { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubType; + uint16_t bcdADC; + uint16_t wTotalLength; + uint8_t bInCollection; + uint8_t baInterfaceNr[AUDIOH_MAX_COLLECTION]; +} audioh_ac_header_t; + +static bool audioh_itf_in_collection(const audioh_ac_header_t *header, uint8_t itf_num) { + for (uint8_t i = 0; i < header->bInCollection; i++) { + if (header->baInterfaceNr[i] == itf_num) { + return true; + } + } + return false; +} + +// Parse one Audio Streaming interface alternate setting and register its +// supported configurations into the matching stream. Returns the descriptor +// pointer of the next interface. +static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_desc_interface_t *desc_itf, + const uint8_t *p_desc, const uint8_t *desc_end) { + const uint8_t itf_num = desc_itf->bInterfaceNumber; + const uint8_t alt = desc_itf->bAlternateSetting; + + p_desc = tu_desc_next(p_desc); + + // Alternate setting 0 has no endpoints: nothing to stream + if (alt == 0 || desc_itf->bNumEndpoints == 0) { + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + p_desc = tu_desc_next(p_desc); + } + return p_desc; + } + + // Parse the class-specific and endpoint descriptors of this alternate setting + uint16_t format_tag = 0; + uint8_t num_channels = 0; + uint8_t subframe_size = 0; + uint8_t bit_res = 0; + uint8_t sam_freq_type = 0; + uint8_t sam_freq_count = 0; // 1 for a continuous range + uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; + + // An alternate setting can expose an endpoint in each direction + typedef struct { + uint8_t ep_addr; + uint16_t ep_size; + uint8_t ep_interval; + uint8_t ep_sync; + uint8_t ep_usage; + bool sam_freq_ctrl; + } audioh_ep_info_t; + audioh_ep_info_t ep_info[2] = {0}; + uint8_t ep_count = 0; + // The CS_ENDPOINT descriptor carries the sampling-frequency control bit of + // its endpoint. Devices differ in whether it precedes or follows the + // standard endpoint descriptor, so attribute it in either order. + bool pending_sam_freq_ctrl = false; // CS_ENDPOINT seen, applies to the next endpoint + bool unassigned_ep = false; // endpoint seen, applies to the next CS_ENDPOINT + + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_CS_INTERFACE: { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { + const audio10_desc_cs_as_interface_t *desc_as_general = (const audio10_desc_cs_as_interface_t *)p_desc; + if (desc_as_general->bLength >= 5) { + format_tag = tu_le16toh(desc_as_general->wFormatTag); + } + break; + } + case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { + TU_ASSERT(p_desc[0] >= 8, p_desc); + if (p_desc[3] != AUDIO10_FORMAT_TYPE_I) { + break; // only Type I (PCM) is supported + } + num_channels = p_desc[4]; + subframe_size = p_desc[5]; + bit_res = p_desc[6]; + sam_freq_type = p_desc[7]; + if (sam_freq_type == 0) { + // Continuous range: expose a single configuration at the + // highest supported sampling frequency (tSamFreq[0] is the + // lower bound, tSamFreq[1] the upper bound) + if (p_desc[0] >= 14) { + sam_freq_count = 1; + sam_freq[0] = ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); + TU_LOG_DRV(" AUDIO AS itf %u: continuous range %lu-%lu Hz, using %lu Hz\r\n", itf_num, + (unsigned long)((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | + ((uint32_t)p_desc[10] << 16)), + (unsigned long)sam_freq[0], (unsigned long)sam_freq[0]); + } + } else { + sam_freq_count = TU_MIN(sam_freq_type, CFG_TUH_AUDIO_MAX_SAM_FREQ); + for (uint8_t i = 0; i < sam_freq_count && (8 + i * 3 + 2) < p_desc[0]; i++) { + sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | ((uint32_t)p_desc[9 + i * 3] << 8) | + ((uint32_t)p_desc[10 + i * 3] << 16)); + } + } + break; + } + default: + break; + } + break; + } + case TUSB_DESC_CS_ENDPOINT: { + if (tu_desc_subtype(p_desc) == AUDIO10_CS_EP_SUBTYPE_GENERAL && p_desc[0] >= 4) { + const audio10_desc_cs_as_iso_data_ep_t *desc_ep = (const audio10_desc_cs_as_iso_data_ep_t *)p_desc; + const bool sam_freq_ctrl = (desc_ep->bmAttributes & 0x01) != 0; + if (unassigned_ep) { + // Standard order: the CS_ENDPOINT follows its endpoint descriptor + ep_info[ep_count - 1].sam_freq_ctrl = sam_freq_ctrl; + unassigned_ep = false; + } else { + // Non-standard order: the CS_ENDPOINT precedes its endpoint descriptor + pending_sam_freq_ctrl = sam_freq_ctrl; + } + } + break; + } + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; + if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS && ep_count < 2) { + audioh_ep_info_t *ep = &ep_info[ep_count]; + ep->ep_addr = desc_endpoint->bEndpointAddress; + ep->ep_size = tu_edpt_packet_size(desc_endpoint); + ep->ep_interval = desc_endpoint->bInterval; + // bInterval must be in [1, 16] for isochronous endpoints + if (ep->ep_interval == 0 || ep->ep_interval > 16) { + ep->ep_interval = 1; + } + ep->ep_sync = desc_endpoint->bmAttributes.sync; + ep->ep_usage = desc_endpoint->bmAttributes.usage; + ep->sam_freq_ctrl = pending_sam_freq_ctrl; + pending_sam_freq_ctrl = false; + unassigned_ep = !ep->sam_freq_ctrl; + ep_count++; + } + break; + } + default: + break; + } + p_desc = tu_desc_next(p_desc); + } + + if (ep_count == 0) { + return p_desc; + } + + // Reject unsupported formats explicitly + if (format_tag != AUDIO10_DATA_FORMAT_TYPE_I_PCM) { + TU_LOG_DRV(" AUDIO AS itf %u: format tag 0x%04x not supported\r\n", itf_num, format_tag); + return p_desc; + } + tuh_audio_format_t format; + if (!audioh_format_from_uac1(subframe_size, bit_res, &format)) { + TU_LOG_DRV(" AUDIO AS itf %u: subframe %u bits %u not supported\r\n", itf_num, subframe_size, bit_res); + return p_desc; + } + if (num_channels == 0) { + TU_LOG_DRV(" AUDIO AS itf %u: zero channels not supported\r\n", itf_num); + return p_desc; + } + + // Register one configuration per (endpoint, discrete sampling frequency) + const uint8_t frame_bytes = num_channels * tuh_audio_format_bytes(format); + for (uint8_t e = 0; e < ep_count; e++) { + const audioh_ep_info_t *ep = &ep_info[e]; + tuh_audio_stream_t *stream = audioh_get_stream(p_audio, tu_edpt_dir(ep->ep_addr)); + if (stream == NULL) { + continue; + } + + const uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + + // Capture: the device can deliver up to its max packet size per poll + // interval, the transfer buffer must fit it + if (stream->dir == TUSB_DIR_IN && ep->ep_size > epbuf_size) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds transfer buffer %u\r\n", itf_num, alt, + ep->ep_size, epbuf_size); + continue; + } + + for (uint8_t i = 0; i < sam_freq_count; i++) { + if (sam_freq[i] == 0) { + continue; + } + + // Playback: the device accepts any packet up to its max packet size + // (often advertised larger than the audio rate needs), but the largest + // scheduled packet must still fit the transfer buffer + if (stream->dir == TUSB_DIR_OUT) { + const uint64_t per_interval = + (uint64_t)sam_freq[i] * frame_bytes * audioh_interval_us(ep->ep_interval, p_audio->daddr); + const uint32_t need = (uint32_t)((per_interval + 999999u) / 1000000u); + if (need > epbuf_size) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: playback needs %u B per interval, transfer buffer is %u\r\n", itf_num, + alt, (unsigned)need, epbuf_size); + continue; + } + } + + // Skip duplicate configurations + bool duplicate = false; + for (uint8_t j = 0; j < stream->config_count; j++) { + if (stream->config[j].format == format && stream->config[j].sample_rate == sam_freq[i] && + stream->config[j].channels == num_channels) { + duplicate = true; + break; + } + } + if (duplicate) { + continue; + } + + if (stream->config_count >= AUDIOH_MAX_CONFIGS) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: reach max configurations %u\r\n", itf_num, alt, AUDIOH_MAX_CONFIGS); + return p_desc; + } + + stream->config[stream->config_count].dir = + (stream->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; + stream->config[stream->config_count].format = format; + stream->config[stream->config_count].sample_rate = sam_freq[i]; + stream->config[stream->config_count].channels = num_channels; + stream->map[stream->config_count].itf_num = itf_num; + stream->map[stream->config_count].alt_setting = alt; + stream->map[stream->config_count].ep_addr = ep->ep_addr; + stream->map[stream->config_count].ep_size = ep->ep_size; + stream->map[stream->config_count].ep_interval = ep->ep_interval; + stream->map[stream->config_count].ep_sync = ep->ep_sync; + stream->map[stream->config_count].ep_usage = ep->ep_usage; + stream->map[stream->config_count].sam_freq_ctrl = ep->sam_freq_ctrl; + stream->config_count++; + } + } + + return p_desc; +} + uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { (void)rhport; @@ -197,40 +831,44 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface const uint8_t idx = find_new_audio_index(); TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->itf_count = 0; + p_audio->daddr = dev_addr; + p_audio->ac_itf_num = desc_itf->bInterfaceNumber; + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + p_audio->in_stream.daddr = dev_addr; + p_audio->out_stream.daddr = dev_addr; - // Parse Audio Control Interface TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); - p_audio->ac_itf_num = desc_itf->bInterfaceNumber; - p_audio->itf_count = 1; - // Parse Audio Control interface descriptors (Input Terminal, Output Terminal, Feature Unit, etc.) + // Parse the Audio Control interface descriptors and the interface collection + audioh_ac_header_t header = {0}; + bool have_header = false; + p_desc = tu_desc_next(p_desc); while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { switch (tu_desc_subtype(p_desc)) { - case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { - const audio10_desc_input_terminal_t *desc_input_terminal = (const audio10_desc_input_terminal_t *)p_desc; - p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); - p_audio->input_terminal_id = desc_input_terminal->bTerminalID; - p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; - TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", desc_input_terminal->bTerminalID, - tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); - break; - } - case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { - const audio10_desc_output_terminal_t *desc_output_terminal = (const audio10_desc_output_terminal_t *)p_desc; - p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); - p_audio->output_terminal_id = desc_output_terminal->bTerminalID; - TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", desc_output_terminal->bTerminalID, - tu_le16toh(desc_output_terminal->wTerminalType)); + case AUDIO10_CS_AC_INTERFACE_HEADER: { + const audioh_ac_header_t *desc_header = (const audioh_ac_header_t *)p_desc; + if (desc_header->bLength >= 8) { + header.bInCollection = desc_header->bInCollection; + // The collection array must not extend past the descriptor itself + const uint8_t max_collection = TU_MIN((uint8_t)(desc_header->bLength - 8), (uint8_t)AUDIOH_MAX_COLLECTION); + if (header.bInCollection > max_collection) { + TU_LOG_DRV(" AUDIO AC header collection truncated to %u interfaces\r\n", max_collection); + header.bInCollection = max_collection; + } + if (header.bInCollection > 0) { + memcpy(header.baInterfaceNr, desc_header->baInterfaceNr, header.bInCollection); + // An empty collection falls back to the interface-class heuristic + have_header = true; + } + } break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - const uint8_t *desc_feature_unit = p_desc; - p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID - p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID - TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", desc_feature_unit[3], desc_feature_unit[4]); + p_audio->feature_unit_id = p_desc[3]; // bUnitID + TU_LOG_DRV(" Feature Unit: ID=%u\r\n", p_audio->feature_unit_id); break; } default: @@ -240,246 +878,72 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface p_desc = tu_desc_next(p_desc); } - // Parse all remaining descriptors in this configuration looking for Audio Streaming interfaces + // Parse the Audio Streaming interfaces of this audio function. Interfaces + // outside the AC header's collection (e.g. MIDI Streaming interfaces) are + // left for other class drivers. while (tu_desc_in_bounds(p_desc, desc_end)) { - if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { - const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; - // Stop at the first non-Audio interface so we don't claim the rest of the configuration - if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) { - break; - } - if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { - // Found Audio Streaming Interface - TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, - desc_interface->bAlternateSetting); - - if (desc_interface->bAlternateSetting == 0) { - // Interface descriptor with alt setting 0 (no endpoints) - // Add to AS entries - if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as[p_audio->as_count].interface_num = desc_interface->bInterfaceNumber; - p_audio->as[p_audio->as_count].alt_setting = 0; - p_audio->as_count++; - } else { - TU_LOG_DRV(" Skip AS Interface %u: reach CFG_TUH_AUDIO_MAX_AS=%u\r\n", desc_interface->bInterfaceNumber, - CFG_TUH_AUDIO_MAX_AS); - } - } else if (desc_interface->bNumEndpoints > 0) { - // Interface descriptor with alt setting > 0 (has endpoints) - // Find matching AS entry and set alt_setting - uint8_t as_entry_idx = CFG_TUH_AUDIO_MAX_AS; - for (uint8_t i = 0; i < p_audio->as_count; i++) { - if (p_audio->as[i].interface_num == desc_interface->bInterfaceNumber) { - as_entry_idx = i; - break; - } - } - if (as_entry_idx >= CFG_TUH_AUDIO_MAX_AS && p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - as_entry_idx = p_audio->as_count; - p_audio->as[as_entry_idx].interface_num = desc_interface->bInterfaceNumber; - p_audio->as_count++; - } - if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as[as_entry_idx].alt_setting = desc_interface->bAlternateSetting; - } - - // Parse the interface's descriptors - p_desc = tu_desc_next(p_desc); - // Temporary variables to hold format info until endpoint direction is known - uint8_t tmp_format_type = 0; - uint8_t tmp_num_channels = 0; - uint8_t tmp_sub_frame_size = 0; - uint8_t tmp_bit_resolution = 0; - uint8_t tmp_sam_freq_type = 0; - uint32_t tmp_sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; - uint32_t tmp_sam_freq_lower = 0; - uint32_t tmp_sam_freq_upper = 0; - while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { - switch (tu_desc_type(p_desc)) { - case TUSB_DESC_CS_INTERFACE: { - switch (tu_desc_subtype(p_desc)) { - case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { - TU_LOG_DRV(" AS General descriptor\r\n"); - break; - } - case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { - TU_LOG_DRV(" Format Type descriptor\r\n"); - TU_ASSERT(p_desc[0] >= 8, 0); - // Parse UAC 1.0 Format Type I descriptor fields into temporary variables - tmp_format_type = p_desc[3]; // bFormatType - tmp_num_channels = p_desc[4]; // bNrChannels - tmp_sub_frame_size = p_desc[5]; // bSubFrameSize - tmp_bit_resolution = p_desc[6]; // bBitResolution - - // Parse sampling frequencies - uint8_t bLength = p_desc[0]; - if (bLength >= 8) { - tmp_sam_freq_type = p_desc[7]; // bSamFreqType - if (tmp_sam_freq_type == 0) { - // Continuous range: tLowerSamFreq, tUpperSamFreq (3 bytes each) - if (bLength >= 14) { - tmp_sam_freq_lower = - ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); - tmp_sam_freq_upper = - ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); - } - } else { - // Discrete sampling frequencies - uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type - : CFG_TUH_AUDIO_MAX_SAM_FREQ; - for (uint8_t i = 0; i < max_freqs && (8 + i * 3 + 2) < bLength; i++) { - tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | ((uint32_t)p_desc[9 + i * 3] << 8) | - ((uint32_t)p_desc[10 + i * 3] << 16)); - } - } - } - break; - } - default: - break; - } - break; - } - case TUSB_DESC_ENDPOINT: { - const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; - if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { - TU_LOG_DRV(" Isochronous EP %02x\r\n", desc_endpoint->bEndpointAddress); - if (tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN) { - // Save to per-AS storage - if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_IN; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; - as->sub_frame_size = tmp_sub_frame_size; - as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; - as->sam_freq_lower = tmp_sam_freq_lower; - as->sam_freq_upper = tmp_sam_freq_upper; - for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { - as->sam_freq[i] = tmp_sam_freq[i]; - } - } - } else { - // Save to per-AS storage - if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { - tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; - as->ep_addr = desc_endpoint->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(desc_endpoint); - as->ep_dir = TUSB_DIR_OUT; - as->format_type = tmp_format_type; - as->num_channels = tmp_num_channels; - as->sub_frame_size = tmp_sub_frame_size; - as->bit_resolution = tmp_bit_resolution; - as->sam_freq_type = tmp_sam_freq_type; - as->sam_freq_lower = tmp_sam_freq_lower; - as->sam_freq_upper = tmp_sam_freq_upper; - for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { - as->sam_freq[i] = tmp_sam_freq[i]; - } - } - } - TU_ASSERT(tuh_edpt_open(dev_addr, desc_endpoint), 0); - } - break; - } - default: - break; - } - p_desc = tu_desc_next(p_desc); - } - // Continue to parse other AS interfaces (don't break, device may have both IN and OUT) - // break; // Removed: allow parsing multiple AS interfaces (e.g. mic + speaker) - continue; - } - p_audio->itf_count++; - } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && - desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { - // Another Audio Control interface (shouldn't happen in normal UAC 1.0) - p_audio->itf_count++; - } + if (tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + p_desc = tu_desc_next(p_desc); + continue; } - p_desc = tu_desc_next(p_desc); - } - - p_audio->daddr = dev_addr; - return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); -} - -static void _audioh_mount(uint8_t dev_addr, uint8_t idx); - -static void audioh_set_interface_complete(tuh_xfer_t *xfer) { - uint8_t idx = (uint8_t)xfer->user_data; - audioh_interface_t *p_audio = &_audioh_itf[idx]; + const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; + const bool in_collection = have_header ? audioh_itf_in_collection(&header, desc_interface->bInterfaceNumber) + : desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO; + if (!in_collection) { + break; + } - // Send SET_INTERFACE for next AS interface if any - p_audio->as_set_idx++; - if (p_audio->as_set_idx < p_audio->as_count) { - uint8_t as_idx = p_audio->as_set_idx; - uint8_t itf = p_audio->as[as_idx].interface_num; - uint8_t alt = p_audio->as[as_idx].alt_setting; - if (alt > 0) { - TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, xfer->daddr); - tuh_interface_set(xfer->daddr, itf, alt, audioh_set_interface_complete, idx); - return; + if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, + desc_interface->bAlternateSetting); + p_desc = audioh_parse_as(p_audio, desc_interface, p_desc, desc_end); + } else { + // MIDI Streaming or another subclass: not our interface + break; } } - // All SET_INTERFACE done, mount the device - _audioh_mount(xfer->daddr, idx); -} - -static void _audioh_mount(uint8_t dev_addr, uint8_t idx) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->mounted = true; - - - tuh_audio_mount_cb(idx); + // Assign stream indices: playback first, then capture, so the application + // can iterate [0, stream_count) without gaps + uint8_t stream_idx = 0; + if (p_audio->out_stream.config_count > 0) { + p_audio->out_stream.stream_idx = stream_idx++; + } + if (p_audio->in_stream.config_count > 0) { + p_audio->in_stream.stream_idx = stream_idx++; + } + p_audio->stream_count = stream_idx; - usbh_driver_set_config_complete(dev_addr, p_audio->ac_itf_num); + return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); } +//--------------------------------------------------------------------+ +// Set Configuration +//--------------------------------------------------------------------+ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { - uint8_t idx = tuh_audio_itf_get_index(dev_addr, itf_num); - - // If not found, check if this is an AS interface that belongs to a known AC interface - if (idx >= CFG_TUH_AUDIO_MAX) { - for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { - const audioh_interface_t *p_audio = &_audioh_itf[i]; - if (p_audio->daddr == dev_addr) { - for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { - if (p_audio->as[as_idx].interface_num == itf_num) { - // AS interface: configuration is driven by the AC interface, so just pass through - usbh_driver_set_config_complete(dev_addr, itf_num); - return true; - } - } - } + uint8_t idx = TUSB_INDEX_INVALID_8; + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { + if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].ac_itf_num == itf_num) { + idx = i; + break; } - // Not an Audio interface we own; pass through so enumeration can continue + } + + if (idx == TUSB_INDEX_INVALID_8) { + // Audio Streaming interface (or another driver's interface): nothing to do at mount. + // Alternate settings are activated by tuh_audio_configure(). usbh_driver_set_config_complete(dev_addr, itf_num); return true; } audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->as_count <= CFG_TUH_AUDIO_MAX_AS, false); - - // Send SET_INTERFACE for all AS interfaces with alt_setting > 0 - if (p_audio->as_count > 0) { - p_audio->as_set_idx = 0; - uint8_t itf = p_audio->as[0].interface_num; - uint8_t alt = p_audio->as[0].alt_setting; - if (alt > 0) { - TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, dev_addr); - tuh_interface_set(dev_addr, itf, alt, audioh_set_interface_complete, idx); - return true; - } - } + p_audio->mounted = true; + TU_LOG_DRV(" AUDIO mounted: addr = %u index = %u\r\n", dev_addr, idx); + + tuh_audio_mount_cb(idx); - _audioh_mount(dev_addr, idx); + usbh_driver_set_config_complete(dev_addr, itf_num); return true; } @@ -488,143 +952,308 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { //--------------------------------------------------------------------+ bool tuh_audio_mounted(uint8_t idx) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - return p_audio->mounted; + return _audioh_itf[idx].mounted; } uint8_t tuh_audio_get_dev_addr(uint8_t idx) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - return p_audio->daddr; + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + return _audioh_itf[idx].daddr; } - uint8_t tuh_audio_get_feature_unit_id(uint8_t idx) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - return p_audio->feature_unit_id; + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + return _audioh_itf[idx].feature_unit_id; } -uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num) { - for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { - const audioh_interface_t *p_audio = &_audioh_itf[idx]; - if (p_audio->daddr == daddr && p_audio->ac_itf_num == itf_num) { - return idx; +uint8_t tuh_audio_stream_count(uint8_t dev_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + return p_audio->stream_count; +} + +bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, false); + return audioh_get_stream_by_idx(p_audio, stream_idx) != NULL; +} + +tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUH_AUDIO_STREAM_DIRECTION_COUNT); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, TUH_AUDIO_STREAM_DIRECTION_COUNT); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, TUH_AUDIO_STREAM_DIRECTION_COUNT); + return (s->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; +} + +uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, 0); + return s->config_count; +} +uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUSB_INDEX_INVALID_8); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, TUSB_INDEX_INVALID_8); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, TUSB_INDEX_INVALID_8); + return s->active_config; +} +bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && config, false); + TU_VERIFY(config_idx < s->config_count, false); + + *config = s->config[config_idx]; + return true; +} + +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_configure_cb_t complete_cb, + uintptr_t user_data) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && complete_cb, false); + TU_VERIFY(config_idx < s->config_count, false); + // Reconfiguration is allowed from a stopped stream; only one configuration + // may be in progress + TU_VERIFY(s->state != STREAM_STATE_CONFIG && !s->running, false); + if (s->state == STREAM_STATE_READY) { + // Wait for any in-flight transfer to complete and be discarded + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].ep_addr), false); + } + + // A shared AS interface must not be left in two different alternate settings + tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; + if (other->active_config != TUSB_INDEX_INVALID_8) { + const audioh_stream_map_t *m1 = &s->map[config_idx]; + const audioh_stream_map_t *m2 = &other->map[other->active_config]; + if (m1->itf_num == m2->itf_num && m1->alt_setting != m2->alt_setting) { + TU_LOG_DRV(" AUDIO configure failed: shared AS itf %u in conflicting alt settings\r\n", m1->itf_num); + return false; } } - return TUSB_INDEX_INVALID_8; + + s->active_config = config_idx; + s->frame_bytes = (uint8_t)tuh_audio_config_frame_size(&s->config[config_idx]); + s->frames_per_ms = (uint16_t)(s->config[config_idx].sample_rate / 1000); + s->frames_rem = (uint16_t)(s->config[config_idx].sample_rate % 1000); + s->rem_acc = 0; + s->complete_cb = complete_cb; + s->user_data = user_data; + s->state = STREAM_STATE_CONFIG; + + const audioh_stream_map_t *map = &s->map[config_idx]; + TU_LOG_DRV(" AUDIO configure %s stream %u: itf %u alt %u ep %02x\r\n", + (s->dir == TUSB_DIR_IN) ? "capture" : "playback", s->stream_idx, map->itf_num, map->alt_setting, + map->ep_addr); + + if (!tuh_interface_set(s->daddr, map->itf_num, map->alt_setting, audioh_stream_set_interface_complete, + (uintptr_t)s)) { + audioh_stream_fail(s, XFER_RESULT_FAILED); + return false; + } + return true; } -bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { - audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio && info); +// Invoked when the SET_INTERFACE activating the stream's interface completes: +// the interface is active, start submitting transfers +static void audioh_stream_start_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || !s->running) { + return; // device is gone or the stream was stopped meanwhile + } + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO SET_INTERFACE activate failed: result=%u\r\n", xfer->result); + s->running = false; + return; + } + if (s->dir == TUSB_DIR_IN) { + audioh_stream_capture_xfer(s); // feed the capture endpoint + } else { + audioh_stream_playback_xfer(s); // flush queued frames, if any + } +} - info->daddr = p_audio->daddr; +bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, false); + TU_VERIFY(s->state == STREAM_STATE_READY && !s->running, false); + // Wait for any in-flight transfer to complete and be discarded + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].ep_addr), false); + + // Activate the interface's alternate setting asynchronously: transfers + // begin once SET_INTERFACE completes (audioh_stream_start_complete) + s->running = true; + const audioh_stream_map_t *map = &s->map[s->active_config]; + if (!tuh_interface_set(s->daddr, map->itf_num, map->alt_setting, audioh_stream_start_complete, (uintptr_t)s)) { + s->running = false; + return false; + } + return true; +} - // re-construct descriptor - tusb_desc_interface_t *desc_interface = &info->desc; - desc_interface->bLength = sizeof(tusb_desc_interface_t); - desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; +// Invoked when the SET_INTERFACE deactivating the stream's interface (alt 0) +// completes +static void audioh_stream_stop_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr) { + return; + } + TU_LOG_DRV(" AUDIO SET_INTERFACE deactivate done: result=%u\r\n", xfer->result); +} - uint8_t ep_in = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_IN); - uint8_t ep_out = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_OUT); +bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted, false); - desc_interface->bInterfaceNumber = p_audio->ac_itf_num; - desc_interface->bAlternateSetting = 0; - desc_interface->bNumEndpoints = (uint8_t)((ep_in ? 1u : 0u) + (ep_out ? 1u : 0u)); - desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; - desc_interface->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; - desc_interface->bInterfaceProtocol = 0; - desc_interface->iInterface = 0; + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->running, false); - return true; + // The in-flight transfer (if any) completes and its data is discarded; + // queued frames are dropped as well. The interface is deactivated (alt 0) + // so the device stops transferring. + s->running = false; + tu_edpt_stream_clear(&s->edpt); + s->rem_acc = 0; // restart the pacing accumulator on the next tuh_audio_start() + + const audioh_stream_map_t *map = &s->map[s->active_config]; + return tuh_interface_set(s->daddr, map->itf_num, 0, audioh_stream_stop_complete, (uintptr_t)s); } -//--------------------------------------------------------------------+ -// Control Endpoint API -//--------------------------------------------------------------------+ -bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio && as_idx < p_audio->as_count, false); +uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted && buffer, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + // Writes are only accepted by the playback stream + TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + TU_VERIFY(frame_count > 0, 0); + + // Queue as many whole frames as the FIFO can hold + const uint32_t frames = TU_MIN(frame_count, tu_fifo_remaining(&s->edpt.ff) / s->frame_bytes); + if (frames == 0) { + return 0; + } + tu_fifo_write_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); - uint8_t ep_addr = p_audio->as[as_idx].ep_addr; - uint8_t daddr = p_audio->daddr; - uint8_t *freq_buf = _audioh_epbuf[idx].ctrl; + // Flush a packet when the FIFO holds at least one; the scheduler drains + // the rest on completion + audioh_stream_playback_xfer(s); - const tusb_control_request_t request = - {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, - .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t)ep_addr), - .wLength = 3}; - - // UAC 1.0 sampling frequency is 3 bytes little-endian - // uint8_t freq_buf[3] = { - // (uint8_t)(sampling_freq & 0xFF), - // (uint8_t)((sampling_freq >> 8) & 0xFF), - // (uint8_t)((sampling_freq >> 16) & 0xFF) - // }; - freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); - freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); - freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); - tuh_xfer_t xfer = {.daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = freq_buf, - .complete_cb = complete_cb, - .user_data = user_data}; + return frames; +} - return tuh_control_xfer(&xfer); +uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->mounted && buffer, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + // Reads are only accepted by the capture stream + TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + TU_VERIFY(frame_count > 0, 0); + + // Drain as many whole frames as are queued + const uint32_t frames = TU_MIN(frame_count, tu_fifo_count(&s->edpt.ff) / s->frame_bytes); + if (frames > 0) { + tu_fifo_read_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); + audioh_stream_capture_xfer(s); // re-arm: the FIFO has room again + } + return frames; } -bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio && as_idx < p_audio->as_count && sampling_freq, false); - uint8_t ep_addr = p_audio->as[as_idx].ep_addr; - uint8_t daddr = p_audio->daddr; +uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); - *sampling_freq = 0; + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + return tu_edpt_stream_write_available(&s->edpt) / s->frame_bytes; +} - const tusb_control_request_t request = - {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, - .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_htole16((uint16_t)ep_addr), - .wLength = 3}; - - // Application needs to parse 3-byte little-endian sampling frequency from buffer - tuh_xfer_t xfer = {.daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = (uint8_t *)sampling_freq, - .complete_cb = complete_cb, - .user_data = user_data}; +uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { + TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); + audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; + TU_VERIFY(p_audio->daddr != 0, 0); - return tuh_control_xfer(&xfer); + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0); + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0); + return tu_edpt_stream_read_available(&s->edpt) / s->frame_bytes; +} + +//--------------------------------------------------------------------+ +// Feature Unit Control API +//--------------------------------------------------------------------+ + +// Convert the raw control value to host order and chain to the application callback +static void audioh_fu_get_complete(tuh_xfer_t *xfer) { + const uint8_t idx = (uint8_t)xfer->user_data; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + tuh_xfer_cb_t app_cb = epbuf->complete_cb; + uintptr_t user_data = epbuf->user_data; + uint16_t *value = epbuf->value; + const uint8_t width = epbuf->width; + epbuf->complete_cb = NULL; + + if (app_cb != NULL && value != NULL && xfer->result == XFER_RESULT_SUCCESS) { + const uint8_t *raw = (const uint8_t *)value; + // The raw bytes are little-endian on the wire: rebuild the host-order value + *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8)); + } + + xfer->user_data = user_data; + if (app_cb != NULL) { + app_cb(xfer); + } } bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - uint8_t daddr = _audioh_itf[idx].daddr; - uint8_t itf_num = _audioh_itf[idx].ac_itf_num; - uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0, false); + + const uint8_t width = audioh_fu_control_width(control_selector); const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, .bRequest = AUDIO10_CS_REQ_SET_CUR, .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = 2}; + .wIndex = tu_htole16(tu_u16(p_audio->feature_unit_id, p_audio->ac_itf_num)), + .wLength = width}; uint8_t *val_buf = _audioh_epbuf[idx].ctrl; val_buf[0] = (uint8_t)(value & 0xFF); val_buf[1] = (uint8_t)((value >> 8) & 0xFF); - tuh_xfer_t xfer = {.daddr = daddr, + tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, .buffer = val_buf, @@ -634,103 +1263,62 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c return tuh_control_xfer(&xfer); } -bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *buffer, +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - uint8_t daddr = _audioh_itf[idx].daddr; - uint8_t itf_num = _audioh_itf[idx].ac_itf_num; - uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0 && value, false); + + const uint8_t width = audioh_fu_control_width(control_selector); const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, .bRequest = AUDIO10_CS_REQ_GET_CUR, .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), - .wLength = 2}; - - tuh_xfer_t xfer = {.daddr = daddr, - .ep_addr = 0, - .setup = &request, - .buffer = (uint8_t *)buffer, - .complete_cb = complete_cb, - .user_data = user_data}; - - return tuh_control_xfer(&xfer); -} - -//--------------------------------------------------------------------+ -// Multi-AS interface API -//--------------------------------------------------------------------+ -uint8_t tuh_audio_as_get_count(uint8_t idx) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); - return _audioh_itf[idx].as_count; -} - -bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); - TU_VERIFY(as_idx < _audioh_itf[idx].as_count, false); - TU_VERIFY(info, false); - - tuh_audio_as_info_t *as = &_audioh_itf[idx].as[as_idx]; - info->interface_num = as->interface_num; - info->alt_setting = as->alt_setting; - info->ep_addr = as->ep_addr; - info->ep_size = as->ep_size; - info->ep_dir = as->ep_dir; - info->format_type = as->format_type; - info->num_channels = as->num_channels; - info->sub_frame_size = as->sub_frame_size; - info->bit_resolution = as->bit_resolution; - info->sam_freq_type = as->sam_freq_type; - info->sam_freq_lower = as->sam_freq_lower; - info->sam_freq_upper = as->sam_freq_upper; - memcpy(info->sam_freq, as->sam_freq, sizeof(info->sam_freq)); - return true; -} - -//--------------------------------------------------------------------+ -// Isochronous Endpoint API -//--------------------------------------------------------------------+ -bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - tuh_audio_as_info_t *as = &p_audio->as[as_idx]; - TU_VERIFY(as->ep_addr != 0); - - return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, buffer, len); -} + .wIndex = tu_htole16(tu_u16(p_audio->feature_unit_id, p_audio->ac_itf_num)), + .wLength = width}; + + if (complete_cb == NULL) { + // Sync (blocking) path: user_data points to a tusb_xfer_result_t, the raw + // bytes are converted to host order after the transfer completes + tuh_xfer_t xfer = {.daddr = p_audio->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)value, + .complete_cb = NULL, + .user_data = user_data}; + if (!tuh_control_xfer(&xfer)) { + return false; + } + if (xfer.result == XFER_RESULT_SUCCESS) { + const uint8_t *raw = (const uint8_t *)value; + *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8)); + } + return true; + } -bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - tuh_audio_as_info_t *as = &p_audio->as[as_idx]; - TU_VERIFY(as->ep_addr != 0); + // Async path: chain the host-order conversion to the application callback + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + TU_VERIFY(epbuf->complete_cb == NULL, false); // one feature-unit GET in flight per device - return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, (uint8_t *)buffer, len); -} + epbuf->complete_cb = complete_cb; + epbuf->user_data = user_data; + epbuf->value = value; + epbuf->width = width; -//--------------------------------------------------------------------+ -// Set Interface -//--------------------------------------------------------------------+ -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, - uintptr_t user_data) { - const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_STANDARD, - .direction = TUSB_DIR_OUT}, - .bRequest = TUSB_REQ_SET_INTERFACE, - .wValue = alt_setting, - .wIndex = itf_num, - .wLength = 0}; - - tuh_xfer_t xfer = {.daddr = daddr, + tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, - .buffer = NULL, - .complete_cb = complete_cb, - .user_data = user_data}; + .buffer = (uint8_t *)value, // raw bytes, converted in audioh_fu_get_complete() + .complete_cb = audioh_fu_get_complete, + .user_data = (uintptr_t)idx}; - return tuh_control_xfer(&xfer); + if (!tuh_control_xfer(&xfer)) { + epbuf->complete_cb = NULL; + return false; + } + return true; } #endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index aafd58f43..aaa65b671 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -17,11 +17,11 @@ extern "C" { //--------------------------------------------------------------------+ // Class Driver Configuration //--------------------------------------------------------------------+ -// Maximum number of Audio interfaces per Audio device +// Maximum number of Audio devices #ifndef CFG_TUH_AUDIO_MAX #define CFG_TUH_AUDIO_MAX 1 #endif -// Maximum number of Audio Streaming interfaces per Audio device +// Maximum number of discrete sampling frequencies per Audio Streaming interface #ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 #endif @@ -30,84 +30,177 @@ extern "C" { #define CFG_TUH_AUDIO_MAX_AS 4 #endif -//--------------------------------------------------------------------+ -// AS Interface Info (per-interface independent storage) -//--------------------------------------------------------------------+ -typedef struct { - uint8_t interface_num; // AS interface number - uint8_t alt_setting; // Current alt setting - uint8_t ep_addr; // Endpoint address - uint16_t ep_size; // Max packet size - uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT - - // Format info - uint8_t format_type; - uint8_t num_channels; - uint8_t sub_frame_size; - uint8_t bit_resolution; - uint8_t sam_freq_type; - uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; - uint32_t sam_freq_lower; - uint32_t sam_freq_upper; -} tuh_audio_as_info_t; - +// Maximum size of one capture (IN) isochronous transfer the driver submits. +// Configurations needing a larger per-poll-interval packet are rejected. +// 256 covers 2-ch 48 kHz S16_LE (192 B) and common endpoint padding (208 B). #ifndef CFG_TUH_AUDIO_EPIN_BUFSIZE - #define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 + #define CFG_TUH_AUDIO_EPIN_BUFSIZE 256 #endif +// Maximum size of one playback (OUT) isochronous transfer the driver submits. +// Configurations needing a larger per-poll-interval packet are rejected. #ifndef CFG_TUH_AUDIO_EPOUT_BUFSIZE - #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 + #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256 +#endif + +// Depth in bytes of the per-stream data FIFO. The FIFO decouples the +// application's read/write calls from the 1 ms isochronous transfer cadence +// and absorbs rate differences. 1024 bytes hold 4 default (256 B) packets. +#ifndef CFG_TUH_AUDIO_STREAM_BUFSIZE + #define CFG_TUH_AUDIO_STREAM_BUFSIZE 1024 #endif //--------------------------------------------------------------------+ -// Application API +// Types //--------------------------------------------------------------------+ -// Check if Audio interface is mounted -bool tuh_audio_mounted(uint8_t idx); -// Get device address of Audio interface -uint8_t tuh_audio_get_dev_addr(uint8_t idx); -// Get Feature Unit ID -uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); -// Get Interface index from device address + interface number -// return TUSB_INDEX_INVALID_8 (0xFF) if not found -uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num); +// Fixed transfer direction of a logical stream. +typedef enum { + TUH_AUDIO_STREAM_PLAYBACK = 0, // Host -> Device (OUT) + TUH_AUDIO_STREAM_CAPTURE = 1, // Device -> Host (IN) + TUH_AUDIO_STREAM_DIRECTION_COUNT +} tuh_audio_direction_t; + +// Discrete sample format. Only discrete configurations are supported +// initially; continuous sample-rate ranges are ignored by the driver. +typedef enum { + TUH_AUDIO_FORMAT_S8 = 0, // signed 8-bit + TUH_AUDIO_FORMAT_S16_LE, // signed 16-bit little-endian + TUH_AUDIO_FORMAT_S24_3LE, // signed 24-bit packed in 3 bytes, LE + TUH_AUDIO_FORMAT_S24_LE, // signed 24-bit in 32-bit container, LE + TUH_AUDIO_FORMAT_S32_LE, // signed 32-bit little-endian + TUH_AUDIO_FORMAT_COUNT +} tuh_audio_format_t; + +// One complete supported discrete configuration tuple. +// Each entry is a full (format, sample_rate, channels) combination, +// avoiding invalid mixes between independent format/rate/channel lists. +// dir is constant for all configs of a given (dev_idx, stream_idx) and +// equals the result of tuh_audio_stream_direction(). +typedef struct { + tuh_audio_direction_t dir; + tuh_audio_format_t format; + uint32_t sample_rate; + uint8_t channels; +} tuh_audio_stream_config_t; + +// Asynchronous completion callback of tuh_audio_configure(). +typedef void (*tuh_audio_configure_cb_t)(uint8_t dev_idx, uint8_t stream_idx, tusb_xfer_result_t result, + uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Stream Enumeration +//--------------------------------------------------------------------+ + +// Number of logical audio streams exposed by one mounted device. The +// application iterates stream indices [0, tuh_audio_stream_count()) and +// inspects each with tuh_audio_stream_exists()/tuh_audio_stream_direction(). +uint8_t tuh_audio_stream_count(uint8_t dev_idx); + +// True if (dev_idx, stream_idx) identifies an existing stream. +bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx); + +// Fixed transfer direction of the stream. +tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx); + +//--------------------------------------------------------------------+ +// Configuration Enumeration +//--------------------------------------------------------------------+ + +// Number of supported discrete configurations of the stream. +uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx); + +// Active configuration index of the stream, or TUSB_INDEX_INVALID_8 if none. +uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx); + +// Retrieve one discrete configuration tuple into *config. +bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config); + +//--------------------------------------------------------------------+ +// Configuration (ALSA hw_params analogue, asynchronous) +//--------------------------------------------------------------------+ + +// Configure the stream with the discrete configuration identified by +// config_idx. The driver asynchronously: +// 1. resolves the AS interface and alternate setting, +// 2. issues SET_INTERFACE (checking submission and transfer result), +// 3. opens / reconfigures only the selected endpoint, +// 4. sets the endpoint sampling frequency when supported, +// 5. initializes the FIFO and packet scheduler. +// complete_cb is invoked with the final XFER_RESULT_* status. +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_configure_cb_t complete_cb, + uintptr_t user_data); -// Get Interface information -// return true if index is correct and interface is currently mounted -bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info); +//--------------------------------------------------------------------+ +// Stream Control / Frame-based Data +//--------------------------------------------------------------------+ -// Get number of AS interfaces for an audio device -uint8_t tuh_audio_as_get_count(uint8_t idx); +// Start/stop transferring data on a configured stream. +bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx); +bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx); -// Get AS interface info by index -// as_idx: 0 to (as_count - 1) -bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info); +// Frame-based transfer. One frame = channels * bytes per sample. +// tuh_audio_write() is valid only for TUH_AUDIO_STREAM_PLAYBACK streams, +// tuh_audio_read() only for TUH_AUDIO_STREAM_CAPTURE streams. +// Returns the number of frames actually written/read (0 on any error, +// including wrong direction, unconfigured/stopped stream, or full/empty FIFO). +uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count); +uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count); -// Set Audio Streaming interface alternate setting (to enable/disable endpoints) -bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, - uintptr_t user_data); +// FIFO occupancy in frames available for a non-blocking write/read. +uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx); +uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx); //--------------------------------------------------------------------+ -// Control Endpoint API +// Helpers //--------------------------------------------------------------------+ -// Set current sampling frequency on an isochronous endpoint (UAC 1.0) -// Sampling frequency is 3 bytes little-endian -// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). -bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data); +// Container size in bytes of one sample for a given format. +static inline uint8_t tuh_audio_format_bytes(tuh_audio_format_t format) { + switch (format) { + case TUH_AUDIO_FORMAT_S8: + return 1; + case TUH_AUDIO_FORMAT_S16_LE: + return 2; + case TUH_AUDIO_FORMAT_S24_3LE: + return 3; + case TUH_AUDIO_FORMAT_S24_LE: + case TUH_AUDIO_FORMAT_S32_LE: + return 4; + default: + return 0; + } +} + +// Size in bytes of one frame (all channels) for a configuration. +static inline uint32_t tuh_audio_config_frame_size(const tuh_audio_stream_config_t *config) { + TU_ASSERT(config != NULL); + return (uint32_t)tuh_audio_format_bytes(config->format) * config->channels; +} + +//--------------------------------------------------------------------+ +// Device Info +//--------------------------------------------------------------------+ -// Get current sampling frequency from an isochronous endpoint (UAC 1.0) -// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). -bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, - uintptr_t user_data); +// Check if Audio device is mounted +bool tuh_audio_mounted(uint8_t idx); +// Get device address of Audio device +uint8_t tuh_audio_get_dev_addr(uint8_t idx); +// Get Feature Unit ID +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); -// Set current/mute/volume etc. for a feature unit (UAC 1.0) +//--------------------------------------------------------------------+ +// Control Request API +//--------------------------------------------------------------------+ + +// Set a Feature Unit control (mute, volume, ...) of the Audio device (UAC 1.0) +// The request length follows the control selector: mute/AGC/loudness are 1 byte, the rest are 2 bytes bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); -// Get current/mute/volume etc. from a feature unit (UAC 1.0) +// Get a Feature Unit control (mute, volume, ...) of the Audio device (UAC 1.0) +// The value is converted to host byte order before complete_cb is invoked. +// Only one feature unit GET may be in flight per device. bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); @@ -116,16 +209,6 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t c // Each Function will make a USB control transfer request to/from device the function will block until request is // complete. The function will return the transfer request result //--------------------------------------------------------------------+ -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_get_sampling_freq_sync(uint8_t idx, uint8_t as_idx, - uint32_t *sampling_freq) { - TU_API_SYNC(tuh_audio_get_sampling_freq, idx, as_idx, sampling_freq); -} - -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_set_sampling_freq_sync(uint8_t idx, uint8_t as_idx, - uint32_t sampling_freq) { - TU_API_SYNC(tuh_audio_set_sampling_freq, idx, as_idx, sampling_freq); -} - TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value) { TU_API_SYNC(tuh_audio_feature_unit_set, idx, control_selector, channel, value); @@ -136,20 +219,6 @@ tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t control_selector, uint8_t c TU_API_SYNC(tuh_audio_feature_unit_get, idx, control_selector, channel, value); } -//--------------------------------------------------------------------+ -// Interrupt/Isochronous Endpoint API -//--------------------------------------------------------------------+ - -// Submit an isochronous transfer to receive audio data from a default IN endpoint. -// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. -// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. -bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); - -// Submit an isochronous transfer to send audio data to a default OUT endpoint. -// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. -// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. -bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); - //--------------------------------------------------------------------+ // Callbacks (Weak is optional) //--------------------------------------------------------------------+ @@ -160,11 +229,17 @@ void tuh_audio_mount_cb(uint8_t idx); // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx); -// Invoked when an isochronous IN transfer is complete -void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); +// Invoked when an isochronous IN transfer completes successfully: the +// received data is already queued into the stream's capture FIFO. +void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); + +// Invoked when an isochronous OUT transfer completes successfully: the +// next queued packet is submitted from the stream's playback FIFO. +void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); -// Invoked when an isochronous OUT transfer is complete -void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); +// Invoked when an isochronous transfer fails. The stream is stopped +// (tuh_audio_start() must be called again to resume). +void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); //--------------------------------------------------------------------+ // Internal Class Driver API -- cgit v1.3.1 From 3db8be8ba1a79b7a38814237508108eff9bcd5d6 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:35 +0200 Subject: fix(audio): repair the host audio example build Add the missing preset and shared declarations, remove duplicate initialization, and correct callback call sites so the new example builds through the normal CMake flow. Signed-off-by: HiFiPHile --- examples/host/audio_host/CMakePresets.json | 6 ++++++ examples/host/audio_host/src/app.h | 1 + examples/host/audio_host/src/audio_app.c | 4 ++-- examples/host/audio_host/src/main.c | 2 -- 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 examples/host/audio_host/CMakePresets.json (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/CMakePresets.json b/examples/host/audio_host/CMakePresets.json new file mode 100644 index 000000000..5cd8971e9 --- /dev/null +++ b/examples/host/audio_host/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h index 3ebb5c16d..474544a19 100644 --- a/examples/host/audio_host/src/app.h +++ b/examples/host/audio_host/src/app.h @@ -21,6 +21,7 @@ #include #include +void led_blinking_task(void); void audio_app_task_read(void); void audio_app_task_write(void); void defer_queue_task(void); diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 40ad620bf..c3bd59abf 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -240,7 +240,7 @@ void led_blinking_task(void) { err_cb_count = 0; #endif -#if 0 +#if 0 // Print the current Feature Unit volume, which is set to 0x0600 in mic_configured() and can be changed by the device. uint16_t volume = 0x0001; tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); @@ -412,7 +412,7 @@ void tuh_audio_umount_cb(uint8_t idx) { } } -void tuh_audio_mount_async(uintptr_t param) { +static void tuh_audio_mount_async(uintptr_t param) { uint8_t idx = (uint8_t)param; if (idx >= CFG_TUH_AUDIO_MAX) { printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c index 77c28cf41..3a3e18ab7 100644 --- a/examples/host/audio_host/src/main.c +++ b/examples/host/audio_host/src/main.c @@ -25,7 +25,6 @@ //--------------------------------------------------------------------+ // MACRO CONSTANT TYPEDEF PROTYPES //--------------------------------------------------------------------+ -void led_blinking_task(void); /*------------- MAIN -------------*/ int main(void) { @@ -39,7 +38,6 @@ int main(void) { tusb_init(BOARD_TUH_RHPORT, &host_init); board_init_after_tusb(); - uint32_t last_ms = tusb_time_millis_api(); while (1) { // tinyusb host task tuh_task(); -- cgit v1.3.1 From b26958c47e70d659620ec391731b37c6d1c66ea7 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: feat(audio): track a Feature Unit per stream direction Associate one directly connected Feature Unit with each capture and playback stream. This lets applications control microphone and speaker paths independently. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 5 ++ examples/host/audio_host/src/audio_app.c | 10 +-- src/class/audio/audio_host.c | 104 ++++++++++++++++++++----------- src/class/audio/audio_host.h | 32 +++++----- 4 files changed, 97 insertions(+), 54 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 2ad3f40d6..c7cad5bfa 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -20,6 +20,11 @@ This example supports any UAC 1.0 compliant USB audio device with a discrete sam The echo needs a matching S16_LE playback stream at the capture sample rate; devices without one run capture-only. The sample rate and channel preferences are configured by the `SAMPLE_RATES` / `AUDIO_MAX_CHANNELS` macros in `src/audio_app.c` (48 kHz stereo by default). Continuous sampling-frequency ranges are exposed as a single configuration at the range's highest frequency (e.g. a 8000–48000 Hz speaker appears as 48000 Hz); non-PCM formats are rejected by the driver. +## Limitations and trade-offs + +- The Feature Unit API exposes one directly connected Feature Unit per logical capture/playback stream. If several Feature Units map to the same logical stream, the first mapping is retained. +- The UAC1 `MaxPacketsOnly` endpoint attribute is not supported. OUT transfers are not padded to `wMaxPacketSize`, and padding in IN transfers is not removed from the reported audio data. + ## Building ### Using CMake (recommended) diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index c3bd59abf..6639b7ec5 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -243,12 +243,12 @@ void led_blinking_task(void) { #if 0 // Print the current Feature Unit volume, which is set to 0x0600 in mic_configured() and can be changed by the device. uint16_t volume = 0x0001; - tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + tuh_audio_feature_unit_get_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); uint16_t mute = 0x0000; - tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_MUTE, 0, &mute); + tuh_audio_feature_unit_get_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_MUTE, 0, &mute); mute=!mute; // toggle mute for demonstration - tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_MUTE, 0, mute); + tuh_audio_feature_unit_set_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute); printf(" Feature Unit mute set: 0x%04x\r\n", (unsigned int)mute); #endif } @@ -368,10 +368,10 @@ static void mic_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t r mic_ready = tuh_audio_start(idx, stream_idx); uint16_t volume = 0x0600; - result = tuh_audio_feature_unit_set_sync(idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); + result = tuh_audio_feature_unit_set_sync(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); if (result == XFER_RESULT_SUCCESS) { printf(" Feature Unit volume set:volume 0x%04x\r\n", (unsigned int)volume); - tuh_audio_feature_unit_get_sync(idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + tuh_audio_feature_unit_get_sync(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); } else { printf(" Setting Feature Unit volume FAILED: result=%u\r\n", result); diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 4f4c1e0d0..951656aa7 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -138,6 +138,9 @@ typedef struct { uint8_t state; // STREAM_STATE_* bool running; // tuh_audio_start() called, transfers may be submitted + // One Feature Unit associated with this logical stream (0 = none) + uint8_t feature_unit_id; + // Size in bytes of one frame (all channels) of the active configuration uint8_t frame_bytes; @@ -171,9 +174,6 @@ typedef struct { tuh_audio_stream_t in_stream; uint8_t stream_count; // number of streams with supported configurations - // Feature Unit info - uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) - bool mounted; } audioh_interface_t; @@ -277,17 +277,18 @@ static uint8_t audioh_fu_control_width(uint8_t control_selector) { // Reset a stream to its unconfigured state (keeps idx, dir, and FIFO configuration) static void audioh_stream_reset(tuh_audio_stream_t *s) { - s->daddr = 0; - s->stream_idx = TUSB_INDEX_INVALID_8; - s->config_count = 0; - s->active_config = TUSB_INDEX_INVALID_8; - s->state = STREAM_STATE_IDLE; - s->running = false; - s->frame_bytes = 0; - s->frames_per_ms = 0; - s->frames_rem = 0; - s->rem_acc = 0; - s->complete_cb = NULL; + s->daddr = 0; + s->stream_idx = TUSB_INDEX_INVALID_8; + s->config_count = 0; + s->active_config = TUSB_INDEX_INVALID_8; + s->state = STREAM_STATE_IDLE; + s->running = false; + s->feature_unit_id = 0; + s->frame_bytes = 0; + s->frames_per_ms = 0; + s->frames_rem = 0; + s->rem_acc = 0; + s->complete_cb = NULL; tu_edpt_stream_close(&s->edpt); tu_edpt_stream_clear(&s->edpt); } @@ -551,10 +552,9 @@ void audioh_close(uint8_t daddr) { _audioh_epbuf[idx].complete_cb = NULL; // drop a pending feature-unit request _audioh_epbuf[idx].fu_busy = false; - p_audio->feature_unit_id = 0; - p_audio->stream_count = 0; - p_audio->daddr = 0; - p_audio->mounted = false; + p_audio->stream_count = 0; + p_audio->daddr = 0; + p_audio->mounted = false; } } @@ -671,7 +671,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de switch (tu_desc_subtype(p_desc)) { case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { const audio10_desc_cs_as_interface_t *desc_as_general = (const audio10_desc_cs_as_interface_t *)p_desc; - if (desc_as_general->bLength >= 5) { + if (desc_as_general->bLength >= sizeof(audio10_desc_cs_as_interface_t)) { format_tag = tu_le16toh(desc_as_general->wFormatTag); } break; @@ -825,7 +825,6 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de continue; } } - // Skip duplicate configurations bool duplicate = false; for (uint8_t j = 0; j < stream->config_count; j++) { @@ -859,6 +858,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de stream->map[stream->config_count].sam_freq_ctrl = ep->sam_freq_ctrl; stream->config_count++; } + } return p_desc; @@ -879,7 +879,6 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface audioh_interface_t *p_audio = &_audioh_itf[idx]; p_audio->daddr = dev_addr; p_audio->ac_itf_num = desc_itf->bInterfaceNumber; - p_audio->feature_unit_id = 0; audioh_stream_reset(&p_audio->in_stream); audioh_stream_reset(&p_audio->out_stream); p_audio->in_stream.daddr = dev_addr; @@ -888,8 +887,10 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); // Parse the Audio Control interface descriptors and the interface collection - audioh_ac_header_t header = {0}; - bool have_header = false; + audioh_ac_header_t header = {0}; + uint8_t usb_input_terminal_id = 0; + uint8_t usb_output_source_id = 0; + bool have_header = false; p_desc = tu_desc_next(p_desc); while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { @@ -913,9 +914,33 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } break; } + case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { + const audio10_desc_input_terminal_t *terminal = (const audio10_desc_input_terminal_t *)p_desc; + if (terminal->bLength >= sizeof(audio10_desc_input_terminal_t) && + tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING && usb_input_terminal_id == 0) { + usb_input_terminal_id = terminal->bTerminalID; + } + break; + } + case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { + const audio10_desc_output_terminal_t *terminal = (const audio10_desc_output_terminal_t *)p_desc; + if (terminal->bLength >= sizeof(audio10_desc_output_terminal_t) && + tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING && usb_output_source_id == 0) { + usb_output_source_id = terminal->bSourceID; + } + break; + } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - p_audio->feature_unit_id = p_desc[3]; // bUnitID - TU_LOG_DRV(" Feature Unit: ID=%u\r\n", p_audio->feature_unit_id); + if (p_desc[0] >= 5) { + if (usb_input_terminal_id != 0 && p_audio->out_stream.feature_unit_id == 0 && + p_desc[4] == usb_input_terminal_id) { + p_audio->out_stream.feature_unit_id = p_desc[3]; + } + if (usb_output_source_id != 0 && p_audio->in_stream.feature_unit_id == 0 && + p_desc[3] == usb_output_source_id) { + p_audio->in_stream.feature_unit_id = p_desc[3]; + } + } break; } default: @@ -1007,9 +1032,14 @@ uint8_t tuh_audio_get_dev_addr(uint8_t idx) { return _audioh_itf[idx].daddr; } -uint8_t tuh_audio_get_feature_unit_id(uint8_t idx) { +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); - return _audioh_itf[idx].feature_unit_id; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->daddr != 0, 0); + + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s, 0); + return s->feature_unit_id; } uint8_t tuh_audio_stream_count(uint8_t dev_idx) { @@ -1309,11 +1339,13 @@ static void audioh_fu_get_complete(tuh_xfer_t *xfer) { } } -bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, + uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0, false); + TU_VERIFY(p_audio->mounted, false); + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->feature_unit_id != 0, false); const uint8_t width = audioh_fu_control_width(control_selector); TU_VERIFY(width != 0, false); @@ -1327,7 +1359,7 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c .direction = TUSB_DIR_OUT}, .bRequest = AUDIO10_CS_REQ_SET_CUR, .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(p_audio->feature_unit_id, p_audio->ac_itf_num)), + .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = width}; uint8_t *val_buf = epbuf->fu_ctrl; @@ -1362,11 +1394,13 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c return true; } -bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, + uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0 && value, false); + TU_VERIFY(p_audio->mounted && value, false); + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s && s->feature_unit_id != 0, false); const uint8_t width = audioh_fu_control_width(control_selector); TU_VERIFY(width != 0, false); @@ -1380,7 +1414,7 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t c .direction = TUSB_DIR_IN}, .bRequest = AUDIO10_CS_REQ_GET_CUR, .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(p_audio->feature_unit_id, p_audio->ac_itf_num)), + .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = width}; if (complete_cb == NULL) { diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 0133c24f2..367e58003 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -186,39 +186,43 @@ static inline uint32_t tuh_audio_config_frame_size(const tuh_audio_stream_config bool tuh_audio_mounted(uint8_t idx); // Get device address of Audio device uint8_t tuh_audio_get_dev_addr(uint8_t idx); -// Get Feature Unit ID -uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); +// Get the Feature Unit ID associated with a stream (0 = none) +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx); //--------------------------------------------------------------------+ // Control Request API //--------------------------------------------------------------------+ -// Set a Feature Unit control (mute, volume, ...) of the Audio device (UAC 1.0) +// Set a Feature Unit control (mute, volume, ...) associated with an Audio stream (UAC 1.0) // Mute/bass/mid/treble/AGC/bass boost/loudness use one byte; volume/delay use two. // Graphic EQ and unknown selectors are unsupported. -bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, + uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); -// Get a Feature Unit control (mute, volume, ...) of the Audio device (UAC 1.0) +// Get a Feature Unit control (mute, volume, ...) associated with an Audio stream (UAC 1.0) // The value is converted to host byte order before complete_cb is invoked. // Graphic EQ and unknown selectors are unsupported. // Only one Feature Unit GET or SET request may be in flight per device. -bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, - tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, + uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); //--------------------------------------------------------------------+ // Control Request Sync API // Each Function will make a USB control transfer request to/from device the function will block until request is // complete. The function will return the transfer request result //--------------------------------------------------------------------+ -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t -tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value) { - TU_API_SYNC(tuh_audio_feature_unit_set, idx, control_selector, channel, value); +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t stream_idx, + uint8_t control_selector, + uint8_t channel, + uint16_t value) { + TU_API_SYNC(tuh_audio_feature_unit_set, idx, stream_idx, control_selector, channel, value); } -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t -tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value) { - TU_API_SYNC(tuh_audio_feature_unit_get, idx, control_selector, channel, value); +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t stream_idx, + uint8_t control_selector, + uint8_t channel, + uint16_t *value) { + TU_API_SYNC(tuh_audio_feature_unit_get, idx, stream_idx, control_selector, channel, value); } //--------------------------------------------------------------------+ -- cgit v1.3.1 From 59b5f9be92cbb74e79bf35daf2aa08bc52a21f94 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: fix(audio): configure playback-only example devices Select capture and playback configurations independently in the example. Speaker-only devices can now start playback without a capture configuration. Signed-off-by: HiFiPHile --- examples/host/audio_host/src/audio_app.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 6639b7ec5..0110bdde8 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -480,8 +480,9 @@ static void tuh_audio_mount_async(uintptr_t param) { if (tuh_audio_stream_direction(idx, stream_idx) != TUH_AUDIO_STREAM_PLAYBACK) { continue; } + const uint8_t preferred_channels = capture_found ? mic_config.channels : 2; for (uint8_t n = 0; n < 2 && playback_config_idx == TUSB_INDEX_INVALID_8; n++) { - const uint8_t ch = (n == 0) ? mic_config.channels : (uint8_t)(mic_config.channels == 1 ? 2 : 1); + const uint8_t ch = (n == 0) ? preferred_channels : (uint8_t)(preferred_channels == 1 ? 2 : 1); for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { tuh_audio_stream_config_t config; if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE && -- cgit v1.3.1 From e590b45fcf51f9ddace73178074e4fe6d691e319 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:37 +0200 Subject: feat(audio): demonstrate speaker volume control Look up the playback Feature Unit in the host example and set its master volume after configuration. The output also reports when a stream has no controllable Feature Unit. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 14 ++++++---- examples/host/audio_host/src/audio_app.c | 44 ++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 21 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index bef78c7d3..88c948ccd 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -6,6 +6,7 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) - Enumerates and mounts USB Audio Class 1.0 devices - Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) +- Reports the Feature Unit ID associated with each stream and sets the master volume when one is available - Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 kHz fallback; stereo preferred, mono accepted) - Echoes captured audio to an S16_LE playback stream at the same sample rate (same channel count preferred, mono/stereo conversion otherwise) - Frame-based FIFO API: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules transfers at the endpoint's polling interval @@ -64,9 +65,10 @@ make BOARD= flash 2. Connect a USB Audio device (UAC 1.0) to the USB host port 3. Open a serial terminal to view output 4. The example will: - - Print each stream's supported configurations when mounted + - Print each stream's Feature Unit ID and supported configurations when mounted - Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 kHz fallback; stereo preferred, mono accepted) and configure it - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) + - Set the microphone and speaker master volume to `0x0600` when their streams have a Feature Unit - Drain the capture FIFO in `audio_app_task_read()` and queue the frames into the playback FIFO; a sine test tone plays on the playback stream when no capture stream is echoing - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; a failed stream is restarted automatically 100 ms after the error callback @@ -76,16 +78,18 @@ make BOARD= flash TinyUSB Host USB Audio Example Connect a USB Audio Device (UAC 1.0) to test Audio device mounted: idx=0 addr=1 - capture stream 1 configurations: 2 + capture stream 1 Feature Unit ID: 5, configurations: 2 [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 - playback stream 0 configurations: 2 + playback stream 0 Feature Unit ID: 2, configurations: 2 [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 Configuring 48 kHz S16_LE capture (2 channels) - Microphone configured, starting capture + Microphone configured + Microphone Feature Unit 5 master volume set: 0x0600 Configuring 48 kHz S16_LE playback (2 channels) - Speaker configured, starting playback + Speaker configured + Speaker Feature Unit 2 master volume set: 0x0600 ``` ## Configuration diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 0110bdde8..3a782e146 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -31,6 +31,8 @@ #define AUDIO_MAX_FRAME_COUNT 48 #define AUDIO_MAX_CHANNELS 2 #define SAMPLE_RATES {48000, 44100} +// UAC1 volume values are signed 1/256 dB; 0x0600 selects +6 dB. +#define FEATURE_UNIT_VOLUME 0x0600 static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index @@ -347,9 +349,11 @@ void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { // Print all supported stream configurations static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { - const tuh_audio_direction_t dir = tuh_audio_stream_direction(idx, stream_idx); - const char *dir_name = (dir == TUH_AUDIO_STREAM_CAPTURE) ? "capture" : "playback"; - printf(" %s stream %u configurations: %u\r\n", dir_name, stream_idx, tuh_audio_config_count(idx, stream_idx)); + const tuh_audio_direction_t dir = tuh_audio_stream_direction(idx, stream_idx); + const char *dir_name = (dir == TUH_AUDIO_STREAM_CAPTURE) ? "capture" : "playback"; + const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx); + printf(" %s stream %u Feature Unit ID: %u, configurations: %u\r\n", dir_name, stream_idx, feature_unit_id, + tuh_audio_config_count(idx, stream_idx)); for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { tuh_audio_stream_config_t config; if (tuh_audio_config_get(idx, stream_idx, i, &config)) { @@ -359,23 +363,30 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { } } +static void set_stream_volume(uint8_t idx, uint8_t stream_idx, const char *stream_name) { + const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx); + if (feature_unit_id == 0) { + printf(" %s stream has no Feature Unit\r\n", stream_name); + return; + } + + uint16_t volume = FEATURE_UNIT_VOLUME; + tusb_xfer_result_t result = tuh_audio_feature_unit_set_sync(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); + if (result == XFER_RESULT_SUCCESS) { + printf(" %s Feature Unit %u master volume set: 0x%04x\r\n", stream_name, feature_unit_id, (unsigned int)volume); + } else { + printf(" Setting %s Feature Unit %u volume failed: result=%u\r\n", stream_name, feature_unit_id, result); + } +} + // Invoked when the configuration selected by tuh_audio_configure() completes static void mic_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { (void)user_data; if (idx == audio_idx && stream_idx == cap_stream_idx && result == XFER_RESULT_SUCCESS) { - printf(" Microphone configured, starting capture\r\n"); + printf(" Microphone configured\r\n"); + set_stream_volume(idx, stream_idx, "Microphone"); mic_ready = tuh_audio_start(idx, stream_idx); - - uint16_t volume = 0x0600; - result = tuh_audio_feature_unit_set_sync(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); - if (result == XFER_RESULT_SUCCESS) { - printf(" Feature Unit volume set:volume 0x%04x\r\n", (unsigned int)volume); - tuh_audio_feature_unit_get_sync(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); - printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); - } else { - printf(" Setting Feature Unit volume FAILED: result=%u\r\n", result); - } } else { printf(" Microphone configuration failed: result=%u\r\n", result); } @@ -385,11 +396,12 @@ static void mic_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t r static void spk_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { (void)user_data; if (idx == audio_idx && stream_idx == spk_stream_idx && result == XFER_RESULT_SUCCESS) { - printf(" Speaker configured, starting playback\r\n"); - spk_ready = tuh_audio_start(idx, stream_idx); + printf(" Speaker configured\r\n"); // playback-only device: set the frame cadence from the selected rate audio_frame_count = spk_config.sample_rate / 1000; spk_init_sine(); // fallback test tone while no capture stream is echoing + set_stream_volume(idx, stream_idx, "Speaker"); + spk_ready = tuh_audio_start(idx, stream_idx); // both streams running: start the periodic phase switching demo if (mic_ready && spk_ready) { -- cgit v1.3.1 From aab34b8d0dafb360213fc40bd3ce33813fdf5cad Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:28:12 +0200 Subject: refactor(audio): separate stream configuration and start Make configuration a synchronous local operation that selects and opens the endpoint. Start now activates the alternate setting and sets the sampling frequency afterward, including every restart, so devices cannot reset the selected rate with SET_INTERFACE. Signed-off-by: HiFiPHile --- examples/host/audio_host/src/audio_app.c | 61 ++++----- src/class/audio/audio_host.c | 164 +++++++++-------------- src/class/audio/audio_host.h | 24 ++-- test/unit-test/test/host/audio/test_audio_host.c | 97 ++++++++------ 4 files changed, 151 insertions(+), 195 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 3a782e146..553e8f20e 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -379,38 +379,6 @@ static void set_stream_volume(uint8_t idx, uint8_t stream_idx, const char *strea } } -// Invoked when the configuration selected by tuh_audio_configure() completes -static void mic_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { - (void)user_data; - - if (idx == audio_idx && stream_idx == cap_stream_idx && result == XFER_RESULT_SUCCESS) { - printf(" Microphone configured\r\n"); - set_stream_volume(idx, stream_idx, "Microphone"); - mic_ready = tuh_audio_start(idx, stream_idx); - } else { - printf(" Microphone configuration failed: result=%u\r\n", result); - } -} - -// Invoked when the playback configuration selected by tuh_audio_configure() completes -static void spk_configured(uint8_t idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { - (void)user_data; - if (idx == audio_idx && stream_idx == spk_stream_idx && result == XFER_RESULT_SUCCESS) { - printf(" Speaker configured\r\n"); - // playback-only device: set the frame cadence from the selected rate - audio_frame_count = spk_config.sample_rate / 1000; - spk_init_sine(); // fallback test tone while no capture stream is echoing - set_stream_volume(idx, stream_idx, "Speaker"); - spk_ready = tuh_audio_start(idx, stream_idx); - - // both streams running: start the periodic phase switching demo - if (mic_ready && spk_ready) { - app_audio_phase_enter(APP_PHASE_MIC_ONLY); - } - } else { - printf(" Speaker configuration failed: result=%u\r\n", result); - } -} // Invoked when device with Audio interface is un-mounted void tuh_audio_umount_cb(uint8_t idx) { printf("Audio device unmounted: idx=%u\r\n", idx); @@ -460,14 +428,19 @@ static void tuh_audio_mount_async(uintptr_t param) { // Check for a matching sample rate S16_LE configuration with the desired channel count if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE && config.sample_rate == sample_rate && config.channels == ch) { + printf(" Configuring %u S16_LE capture (%u channels)\r\n", (unsigned)sample_rate, config.channels); + if (!tuh_audio_configure(idx, stream_idx, i)) { + printf(" Microphone configuration failed\r\n"); + continue; + } audio_idx = idx; cap_stream_idx = stream_idx; mic_config = config; // one ms of audio at the selected rate, rounded down to whole frames audio_frame_count = sample_rate / 1000; - printf(" Configuring %u S16_LE capture (%u channels)\r\n", (unsigned)sample_rate, config.channels); - // Configure the selected capture stream and start it through the callback. - (void)tuh_audio_configure(idx, stream_idx, i, mic_configured, 0); + printf(" Microphone configured\r\n"); + set_stream_volume(idx, stream_idx, "Microphone"); + mic_ready = tuh_audio_start(idx, stream_idx); capture_found = true; break; } @@ -514,8 +487,22 @@ static void tuh_audio_mount_async(uintptr_t param) { return; } printf(" Configuring %u S16_LE playback (%u channels)\r\n", (unsigned)spk_config.sample_rate, spk_config.channels); - // Configure the selected playback stream and start it through the callback. - (void)tuh_audio_configure(idx, spk_stream_idx, playback_config_idx, spk_configured, 0); + if (!tuh_audio_configure(idx, spk_stream_idx, playback_config_idx)) { + printf(" Speaker configuration failed\r\n"); + return; + } + audio_idx = idx; + printf(" Speaker configured\r\n"); + // playback-only device: set the frame cadence from the selected rate + audio_frame_count = spk_config.sample_rate / 1000; + spk_init_sine(); // fallback test tone while no capture stream is echoing + set_stream_volume(idx, spk_stream_idx, "Speaker"); + spk_ready = tuh_audio_start(idx, spk_stream_idx); + + // both streams running: start the periodic phase switching demo + if (mic_ready && spk_ready) { + app_audio_phase_enter(APP_PHASE_MIC_ONLY); + } } // Invoked when device with Audio interface is mounted diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index b682d2d8a..9115ce4ca 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -38,8 +38,8 @@ * supported configurations during enumeration. * * The driver owns: - * 1. Endpoint selection and opening (only the alternate setting selected by - * tuh_audio_configure() is ever activated). + * 1. Endpoint selection and opening; only the alternate setting selected by + * tuh_audio_configure() is activated by tuh_audio_start(). * 2. Endpoint sampling-frequency control (SET_CUR, 3 bytes little-endian). */ @@ -100,8 +100,7 @@ TU_ATTR_WEAK void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xfe // Stream state machine enum { - STREAM_STATE_IDLE = 0, // not configured, no configuration in progress - STREAM_STATE_CONFIG, // tuh_audio_configure() sequence in progress + STREAM_STATE_IDLE = 0, // not configured STREAM_STATE_READY // configured, ready to start/stop }; @@ -149,10 +148,6 @@ typedef struct { uint32_t frames_rem; uint32_t rem_acc; - // Configure state machine - tuh_audio_configure_cb_t complete_cb; - uintptr_t user_data; - // FIFO + endpoint transfer helper (see tu_edpt_stream, used by the MIDI // host driver): the FIFO decouples the application's frame-based read/write // from the endpoint's isochronous transfer cadence. ep_buf is bound at init from @@ -286,7 +281,6 @@ static void audioh_stream_reset(tuh_audio_stream_t *s) { s->frames_per_interval = 0; s->frames_rem = 0; s->rem_acc = 0; - s->complete_cb = NULL; tu_edpt_stream_close(&s->edpt); tu_edpt_stream_clear(&s->edpt); } @@ -374,47 +368,15 @@ static bool audioh_stream_close_ep(tuh_audio_stream_t *s) { return true; } -static void audioh_stream_fail(tuh_audio_stream_t *s, tusb_xfer_result_t result) { +static void audioh_stream_fail(tuh_audio_stream_t *s) { (void)audioh_stream_close_ep(s); s->state = STREAM_STATE_IDLE; s->active_config = TUSB_INDEX_INVALID_8; s->running = false; - - tuh_audio_configure_cb_t cb = s->complete_cb; - uintptr_t user_data = s->user_data; - s->complete_cb = NULL; - if (cb != NULL) { - cb(s->idx, s->stream_idx, result, user_data); - } -} - -static void audioh_stream_ready(tuh_audio_stream_t *s) { - s->state = STREAM_STATE_READY; - - tuh_audio_configure_cb_t cb = s->complete_cb; - uintptr_t user_data = s->user_data; - s->complete_cb = NULL; - if (cb != NULL) { - cb(s->idx, s->stream_idx, XFER_RESULT_SUCCESS, user_data); - } -} - -static void audioh_stream_set_freq_complete(tuh_xfer_t *xfer) { - tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; - if (s->daddr != xfer->daddr || s->state != STREAM_STATE_CONFIG) { - return; // device is gone or configuration was aborted - } - - if (xfer->result != XFER_RESULT_SUCCESS) { - TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); - audioh_stream_fail(s, xfer->result); - return; - } - audioh_stream_ready(s); } -// Set the endpoint sampling frequency (3 bytes little-endian) when supported -static void audioh_stream_set_freq(tuh_audio_stream_t *s) { +// Set the endpoint sampling frequency (3 bytes little-endian) +static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete_cb) { const audioh_stream_map_t *map = &s->map[s->active_config]; const tuh_audio_stream_config_t *cfg = &s->config[s->active_config]; uint8_t *ctrl = _audioh_epbuf[s->idx].sam_freq; @@ -434,15 +396,13 @@ static void audioh_stream_set_freq(tuh_audio_stream_t *s) { .ep_addr = 0, .setup = &request, .buffer = ctrl, - .complete_cb = audioh_stream_set_freq_complete, + .complete_cb = complete_cb, .user_data = (uintptr_t)s}; - if (!tuh_control_xfer(&xfer)) { - audioh_stream_fail(s, XFER_RESULT_FAILED); - } + return tuh_control_xfer(&xfer); } // Reconstruct the endpoint descriptor of the selected configuration and open it -static void audioh_stream_open_ep(tuh_audio_stream_t *s) { +static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { const audioh_stream_map_t *map = &s->map[s->active_config]; const tusb_desc_endpoint_t desc_ep = {.bLength = sizeof(tusb_desc_endpoint_t), @@ -456,35 +416,16 @@ static void audioh_stream_open_ep(tuh_audio_stream_t *s) { if (!tuh_edpt_open(s->daddr, &desc_ep)) { TU_LOG_DRV(" AUDIO open endpoint failed: addr=%u ep=%02x\r\n", s->daddr, map->ep_addr); - audioh_stream_fail(s, XFER_RESULT_FAILED); - return; + audioh_stream_fail(s); + return false; } // Bind the transfer helper to the endpoint and start with an empty FIFO const uint16_t xfer_len = (s->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; tu_edpt_stream_open(&s->edpt, s->daddr, &desc_ep, xfer_len); tu_edpt_stream_clear(&s->edpt); - - if (map->sam_freq_ctrl) { - audioh_stream_set_freq(s); - } else { - audioh_stream_ready(s); - } -} - -static void audioh_stream_set_interface_complete(tuh_xfer_t *xfer) { - tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; - if (s->daddr != xfer->daddr || s->state != STREAM_STATE_CONFIG) { - return; // device is gone or configuration was aborted - } - - if (xfer->result != XFER_RESULT_SUCCESS) { - TU_LOG_DRV(" AUDIO SET_INTERFACE failed: itf=%u alt=%u result=%u\r\n", s->map[s->active_config].itf_num, - s->map[s->active_config].alt_setting, xfer->result); - audioh_stream_fail(s, xfer->result); - return; - } - audioh_stream_open_ep(s); + s->state = STREAM_STATE_READY; + return true; } //--------------------------------------------------------------------+ @@ -534,12 +475,8 @@ void audioh_close(uint8_t daddr) { tuh_audio_umount_cb(idx); } - // Abort a configuration in progress so the application callback still fires for (uint8_t s = 0; s < 2; s++) { tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; - if (stream->state == STREAM_STATE_CONFIG && stream->complete_cb != NULL) { - audioh_stream_fail(stream, XFER_RESULT_ABORTED); - } audioh_stream_reset(stream); } @@ -1013,7 +950,7 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { if (idx == TUSB_INDEX_INVALID_8) { // Audio Streaming interface (or another driver's interface): nothing to do at mount. - // Alternate settings are activated by tuh_audio_configure(). + // Alternate settings are activated by tuh_audio_start(). usbh_driver_set_config_complete(dev_addr, itf_num); return true; } @@ -1106,19 +1043,17 @@ bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_id return true; } -bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_configure_cb_t complete_cb, - uintptr_t user_data) { +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx) { TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); - TU_VERIFY(s && complete_cb, false); + TU_VERIFY(s, false); TU_VERIFY(config_idx < s->config_count, false); const tuh_audio_stream_config_t *cfg = &s->config[config_idx]; - // Reconfiguration is allowed from a stopped stream; only one configuration - // may be in progress - TU_VERIFY(s->state != STREAM_STATE_CONFIG && !s->running, false); + // Reconfiguration is allowed from a stopped stream. + TU_VERIFY(!s->running, false); if (s->state == STREAM_STATE_READY) { // Wait for any in-flight transfer to complete and be discarded TU_VERIFY(!usbh_edpt_busy(s->daddr, s->edpt.ep_addr), false); @@ -1153,33 +1088,64 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx s->frames_per_interval = (uint16_t)(frames_numerator / 1000000u); s->frames_rem = (uint32_t)(frames_numerator % 1000000u); s->rem_acc = 0; - s->complete_cb = complete_cb; - s->user_data = user_data; - s->state = STREAM_STATE_CONFIG; + s->state = STREAM_STATE_IDLE; if (s->dir == TUSB_DIR_IN) { // A byte FIFO can overwrite only complete audio frames when its depth is // an exact multiple of the configured frame size. const uint16_t fifo_depth = CFG_TUH_AUDIO_STREAM_BUFSIZE - (CFG_TUH_AUDIO_STREAM_BUFSIZE % s->frame_bytes); - TU_VERIFY(tu_fifo_config(&s->edpt.ff, s->ff_buf, fifo_depth, true), false); + if (!tu_fifo_config(&s->edpt.ff, s->ff_buf, fifo_depth, true)) { + audioh_stream_fail(s); + return false; + } } TU_LOG_DRV(" AUDIO configure %s stream %u: itf %u alt %u ep %02x\r\n", (s->dir == TUSB_DIR_IN) ? "capture" : "playback", s->stream_idx, map->itf_num, map->alt_setting, map->ep_addr); - if (!tuh_interface_set(s->daddr, map->itf_num, map->alt_setting, audioh_stream_set_interface_complete, - (uintptr_t)s)) { - audioh_stream_fail(s, XFER_RESULT_FAILED); - return false; + return audioh_stream_open_ep(s); +} + +// Invoked when the SET_INTERFACE activating the stream's interface completes: +// the interface is active, set its sampling frequency before submitting +// transfers +static void audioh_stream_start_xfer(tuh_audio_stream_t *s) { + if (s->dir == TUSB_DIR_IN) { + audioh_stream_capture_xfer(s); // feed the capture endpoint + } else { + audioh_stream_playback_xfer(s); // start the continuous playback transfer chain + } +} + +static void audioh_stream_start_set_freq_complete(tuh_xfer_t *xfer) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_READY || !s->running) { + return; // device is gone or the stream was stopped meanwhile + } + if (xfer->result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); + s->running = false; + return; + } + audioh_stream_start_xfer(s); +} + +static bool audioh_stream_start_active(tuh_audio_stream_t *s) { + const audioh_stream_map_t *map = &s->map[s->active_config]; + if (map->sam_freq_ctrl) { + if (!audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete)) { + s->running = false; + return false; + } + } else { + audioh_stream_start_xfer(s); } return true; } -// Invoked when the SET_INTERFACE activating the stream's interface completes: -// the interface is active, start submitting transfers static void audioh_stream_start_complete(tuh_xfer_t *xfer) { tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; - if (s->daddr != xfer->daddr || !s->running) { + if (s->daddr != xfer->daddr || s->state != STREAM_STATE_READY || !s->running) { return; // device is gone or the stream was stopped meanwhile } if (xfer->result != XFER_RESULT_SUCCESS) { @@ -1187,11 +1153,7 @@ static void audioh_stream_start_complete(tuh_xfer_t *xfer) { s->running = false; return; } - if (s->dir == TUSB_DIR_IN) { - audioh_stream_capture_xfer(s); // feed the capture endpoint - } else { - audioh_stream_playback_xfer(s); // start the continuous playback transfer chain - } + (void)audioh_stream_start_active(s); } bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { @@ -1205,9 +1167,9 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { // Wait for any in-flight transfer to complete and be discarded TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].ep_addr), false); + s->running = true; // Activate the interface's alternate setting asynchronously: transfers - // begin once SET_INTERFACE completes (audioh_stream_start_complete) - s->running = true; + // begin once SET_INTERFACE and sampling-frequency control complete. const audioh_stream_map_t *map = &s->map[s->active_config]; if (!tuh_interface_set(s->daddr, map->itf_num, map->alt_setting, audioh_stream_start_complete, (uintptr_t)s)) { s->running = false; diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 9fcbe2577..b1e2a9169 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -85,10 +85,6 @@ typedef struct { uint8_t channels; } tuh_audio_stream_config_t; -// Asynchronous completion callback of tuh_audio_configure(). -typedef void (*tuh_audio_configure_cb_t)(uint8_t dev_idx, uint8_t stream_idx, tusb_xfer_result_t result, - uintptr_t user_data); - //--------------------------------------------------------------------+ // Stream Enumeration //--------------------------------------------------------------------+ @@ -118,26 +114,24 @@ uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx); bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config); //--------------------------------------------------------------------+ -// Configuration (ALSA hw_params analogue, asynchronous) +// Configuration (ALSA hw_params analogue) //--------------------------------------------------------------------+ -// Configure the stream with the discrete configuration identified by -// config_idx. The driver asynchronously: +// Synchronously configure the stream with the discrete configuration identified by +// config_idx. The driver: // 1. resolves the AS interface and alternate setting, -// 2. issues SET_INTERFACE (checking submission and transfer result), -// 3. opens / reconfigures only the selected endpoint, -// 4. sets the endpoint sampling frequency when supported, -// 5. initializes the FIFO and packet scheduler. -// complete_cb is invoked with the final XFER_RESULT_* status. -bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_configure_cb_t complete_cb, - uintptr_t user_data); +// 2. initializes the FIFO and packet scheduler, +// 3. opens / reconfigures only the selected endpoint. +bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx); //--------------------------------------------------------------------+ // Stream Control / Frame-based Data //--------------------------------------------------------------------+ -// Start/stop transferring data on a configured stream. +// Start transferring data by activating the alternate setting selected by +// configure, then set the endpoint sampling frequency when supported. bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx); +// Stop transferring and deactivate the Audio Streaming interface (alt 0). bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx); // Frame-based transfer. One frame = channels * bytes per sample. diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index b87f94bc9..ec1816084 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -26,6 +26,7 @@ enum { static bool interface_set_result; static tuh_xfer_t interface_xfer; static uint8_t interface_alt; +static uint8_t interface_set_count; static bool control_xfer_result; static tuh_xfer_t control_xfer; @@ -93,6 +94,7 @@ bool tuh_interface_set(uint8_t daddr, uint8_t itf_num, uint8_t itf_alt, tuh_xfer interface_xfer.ep_addr = 0; interface_xfer.complete_cb = complete_cb; interface_xfer.user_data = user_data; + interface_set_count++; return interface_set_result; } @@ -343,20 +345,9 @@ static const uint8_t playback_44100_max_packets_only[] = { TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_MAX_PACKETS_ONLY), }; -static uint8_t configure_cb_count; -static tusb_xfer_result_t configure_cb_result; - static uint8_t fu_cb_count; static uintptr_t fu_cb_user_data; -static void configure_complete(uint8_t dev_idx, uint8_t stream_idx, tusb_xfer_result_t result, uintptr_t user_data) { - (void)dev_idx; - (void)stream_idx; - (void)user_data; - configure_cb_count++; - configure_cb_result = result; -} - static void feature_unit_complete(tuh_xfer_t *xfer) { fu_cb_count++; fu_cb_user_data = xfer->user_data; @@ -381,7 +372,8 @@ static void complete_control_xfer(tusb_xfer_result_t result) { void setUp(void) { interface_set_result = true; memset(&interface_xfer, 0, sizeof(interface_xfer)); - interface_alt = 0; + interface_alt = 0; + interface_set_count = 0; control_xfer_result = true; memset(&control_xfer, 0, sizeof(control_xfer)); @@ -404,10 +396,8 @@ void setUp(void) { memset(edpt_xfer_buffer, 0, sizeof(edpt_xfer_buffer)); edpt_xfer_count = 0; - configure_cb_count = 0; - configure_cb_result = XFER_RESULT_INVALID; - fu_cb_count = 0; - fu_cb_user_data = 0; + fu_cb_count = 0; + fu_cb_user_data = 0; TEST_ASSERT_TRUE(audioh_init()); } @@ -456,11 +446,10 @@ void test_audio_host_maps_capture_fu_declared_before_usb_output_terminal(void) { TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_CAPTURE, tuh_audio_stream_direction(0, 0)); TEST_ASSERT_EQUAL_UINT8(CAPTURE_FU, tuh_audio_get_feature_unit_id(0, 0)); - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); - complete_interface_set(XFER_RESULT_SUCCESS); - complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); complete_interface_set(XFER_RESULT_SUCCESS); + complete_control_xfer(XFER_RESULT_SUCCESS); TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); // Keep polling without application reads. Once full, the capture FIFO must @@ -480,6 +469,40 @@ void test_audio_host_maps_capture_fu_declared_before_usb_output_terminal(void) { TEST_ASSERT_EACH_EQUAL_UINT8(11, captured + fifo_depth - 96, 96); } +void test_audio_host_sets_sampling_frequency_after_each_stream_activation(void) { + mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_EQUAL_UINT8(0, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(0, interface_set_count); + + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, interface_set_count); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(0, edpt_xfer_count); + TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){U24_TO_U8S_LE(32000)}), control_buffer, 3); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + + TEST_ASSERT_TRUE(tuh_audio_stop(0, 0)); + TEST_ASSERT_EQUAL_UINT8(0, interface_alt); + TEST_ASSERT_EQUAL_UINT8(2, interface_set_count); + complete_interface_set(XFER_RESULT_SUCCESS); + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96)); + + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, interface_alt); + TEST_ASSERT_EQUAL_UINT8(3, interface_set_count); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){U24_TO_U8S_LE(32000)}), control_buffer, 3); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count); +} + void test_audio_host_parses_discrete_frequencies_with_interval_greater_than_one(void) { tuh_audio_stream_config_t config; open_descriptors(playback_with_two_frequencies, sizeof(playback_with_two_frequencies)); @@ -503,50 +526,42 @@ void test_audio_host_rejects_overflowed_frame_size_for_large_channel_count(void) TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_stream_count(0)); } -void test_audio_host_associates_cs_endpoint_declared_before_data_endpoint(void) { +void test_audio_host_uses_cs_endpoint_declared_before_data_endpoint_on_start(void) { mount_descriptors(playback_with_cs_ep_before_data_ep, sizeof(playback_with_cs_ep_before_data_ep)); - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); - complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_EQUAL_UINT8(0, control_xfer_count); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, interface_set_count); + complete_interface_set(XFER_RESULT_SUCCESS); TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); TEST_ASSERT_EQUAL_UINT16(3, tu_le16toh(control_request.wLength)); TEST_ASSERT_EQUAL_UINT16(0x01, tu_le16toh(control_request.wIndex)); complete_control_xfer(XFER_RESULT_SUCCESS); - TEST_ASSERT_EQUAL_UINT8(1, configure_cb_count); - TEST_ASSERT_EQUAL(XFER_RESULT_SUCCESS, configure_cb_result); } void test_audio_host_closes_old_endpoint_and_cleans_up_failed_reconfiguration(void) { mount_descriptors(playback_with_two_alternates, sizeof(playback_with_two_alternates)); - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); - complete_interface_set(XFER_RESULT_SUCCESS); - complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); TEST_ASSERT_EQUAL_UINT8(1, edpt_open_count); TEST_ASSERT_EQUAL_HEX8(0x01, opened_ep[0].bEndpointAddress); TEST_ASSERT_EQUAL_UINT16(192, tu_edpt_packet_size(&opened_ep[0])); - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 1, configure_complete, 0)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 1)); TEST_ASSERT_EQUAL_UINT8(1, edpt_close_count); TEST_ASSERT_EQUAL_HEX8(0x01, closed_ep[0]); - complete_interface_set(XFER_RESULT_SUCCESS); - complete_control_xfer(XFER_RESULT_SUCCESS); TEST_ASSERT_EQUAL_UINT8(2, edpt_open_count); TEST_ASSERT_EQUAL_HEX8(0x02, opened_ep[1].bEndpointAddress); TEST_ASSERT_EQUAL_UINT16(384, tu_edpt_packet_size(&opened_ep[1])); - control_xfer_result = false; - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); + edpt_open_result = false; + TEST_ASSERT_FALSE(tuh_audio_configure(0, 0, 0)); TEST_ASSERT_EQUAL_UINT8(2, edpt_close_count); TEST_ASSERT_EQUAL_HEX8(0x02, closed_ep[1]); - complete_interface_set(XFER_RESULT_SUCCESS); TEST_ASSERT_EQUAL_UINT8(3, edpt_open_count); - TEST_ASSERT_EQUAL_UINT8(3, edpt_close_count); - TEST_ASSERT_EQUAL_HEX8(0x01, closed_ep[2]); - TEST_ASSERT_EQUAL_UINT8(3, configure_cb_count); - TEST_ASSERT_EQUAL(XFER_RESULT_FAILED, configure_cb_result); TEST_ASSERT_EQUAL_UINT8(TUSB_INDEX_INVALID_8, tuh_audio_active_config(0, 0)); } @@ -590,13 +605,12 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only uint8_t samples[441 * 4] = {0}; mount_descriptors(playback_44100_max_packets_only, sizeof(playback_44100_max_packets_only)); - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); - complete_interface_set(XFER_RESULT_SUCCESS); - TEST_ASSERT_EQUAL_UINT8(1, configure_cb_count); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); TEST_ASSERT_EQUAL_UINT8(0, control_xfer_count); TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); TEST_ASSERT_EQUAL_UINT8(1, interface_alt); + TEST_ASSERT_EQUAL_UINT8(1, interface_set_count); TEST_ASSERT_EQUAL_UINT32(256, tuh_audio_write(0, 0, samples, 256)); TEST_ASSERT_EQUAL_UINT8(0, edpt_xfer_count); complete_interface_set(XFER_RESULT_SUCCESS); @@ -624,8 +638,7 @@ void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) { memset(more_samples, 0x55, sizeof(more_samples)); mount_descriptors(playback_44100_max_packets_only, sizeof(playback_44100_max_packets_only)); - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); - complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); complete_interface_set(XFER_RESULT_SUCCESS); -- cgit v1.3.1 From 9863f0235d2bbd76532933ea7ffc24117d015181 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:28:12 +0200 Subject: fix(audio): avoid playback alt-setting glitch at startup Enter the microphone-only phase before starting playback when both directions exist. This avoids briefly selecting the playback alternate setting and immediately returning it to alt 0. Signed-off-by: HiFiPHile --- examples/host/audio_host/src/audio_app.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 553e8f20e..b7421878b 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -497,11 +497,13 @@ static void tuh_audio_mount_async(uintptr_t param) { audio_frame_count = spk_config.sample_rate / 1000; spk_init_sine(); // fallback test tone while no capture stream is echoing set_stream_volume(idx, spk_stream_idx, "Speaker"); - spk_ready = tuh_audio_start(idx, spk_stream_idx); - // both streams running: start the periodic phase switching demo - if (mic_ready && spk_ready) { + if (capture_found) { + // Start in the mic-only phase without briefly activating playback first. app_audio_phase_enter(APP_PHASE_MIC_ONLY); + } else { + // Playback-only device: start the sine test tone immediately. + spk_ready = tuh_audio_start(idx, spk_stream_idx); } } -- cgit v1.3.1 From 6028a4dfca34cca13d28a72a2e6f0643864bc742 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:28:13 +0200 Subject: fix(audio): generate a continuous high-resolution test tone Replace the six-sample waveform with a 64-entry sine table and phase accumulator. The example now generates a continuous 1 kHz tone at the selected rate and packs the configured channel count correctly. Signed-off-by: HiFiPHile --- examples/host/audio_host/src/audio_app.c | 34 ++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index b7421878b..941248e99 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -117,16 +117,29 @@ static void stereo_to_mono(const int16_t *stereo, int16_t *mono, uint32_t frames } } -// One period of an 8 kHz sine (6 samples at 48 kHz), scaled to ~8-bit -// amplitude. The test tone plays only when no capture stream is echoing. -static const int16_t sine_period[6] = {0, 221, 221, 0, -221, -221}; - -// Precompute a sine wave into the playback buffer -static void spk_init_sine(void) { - for (uint32_t i = 0; i < AUDIO_MAX_FRAME_COUNT; i++) { - const int16_t sample = sine_period[i % 6]; +#define SINE_TONE_HZ 1000u +#define SINE_LUT_BITS 6u +#define SINE_LUT_SIZE (1u << SINE_LUT_BITS) + +// One sine period with a peak amplitude of 4096 (-18 dBFS). +static const int16_t sine_lut[SINE_LUT_SIZE] = { + 0, 401, 799, 1189, 1567, 1931, 2276, 2598, 2896, 3166, 3406, 3612, 3784, 3920, 4017, 4076, + 4096, 4076, 4017, 3920, 3784, 3612, 3406, 3166, 2896, 2598, 2276, 1931, 1567, 1189, 799, 401, + 0, -401, -799, -1189, -1567, -1931, -2276, -2598, -2896, -3166, -3406, -3612, -3784, -3920, -4017, -4076, + -4096, -4076, -4017, -3920, -3784, -3612, -3406, -3166, -2896, -2598, -2276, -1931, -1567, -1189, -799, -401, +}; + +static uint32_t sine_phase; + +// Generate a continuous tone at the selected sample rate and pack each frame +// according to the actual playback channel count. +static void spk_fill_sine(uint32_t frames) { + const uint32_t phase_step = (uint32_t)(((uint64_t)SINE_TONE_HZ << 32) / spk_config.sample_rate); + for (uint32_t i = 0; i < frames; i++) { + const int16_t sample = sine_lut[sine_phase >> (32u - SINE_LUT_BITS)]; + sine_phase += phase_step; for (uint8_t ch = 0; ch < spk_config.channels; ch++) { - spk_samples[i * AUDIO_MAX_CHANNELS + ch] = sample; + spk_samples[i * spk_config.channels + ch] = sample; } } } @@ -294,6 +307,7 @@ void audio_app_task_write(void) { const uint32_t frames = audio_frames_this_ms(spk_config.sample_rate); if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) { + spk_fill_sine(frames); (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); } } @@ -495,7 +509,7 @@ static void tuh_audio_mount_async(uintptr_t param) { printf(" Speaker configured\r\n"); // playback-only device: set the frame cadence from the selected rate audio_frame_count = spk_config.sample_rate / 1000; - spk_init_sine(); // fallback test tone while no capture stream is echoing + sine_phase = 0; set_stream_volume(idx, spk_stream_idx, "Speaker"); if (capture_found) { -- cgit v1.3.1 From 62243107cec5e48f447661ea7a17969d05f76adb Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:07 +0200 Subject: fix(audio): keep stream state consistent on control failures Route asynchronous activation and sampling-frequency failures through the stream error callback. Keep a running stream active when SET_INTERFACE alt 0 cannot be submitted so stop can be retried without diverging from device state. Signed-off-by: HiFiPHile --- examples/host/audio_host/src/audio_app.c | 12 ++++-- src/class/audio/audio_host.c | 30 +++++++------ src/class/audio/audio_host.h | 5 ++- test/unit-test/test/host/audio/test_audio_host.c | 55 ++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 17 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 941248e99..0c4ed8a23 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -348,12 +348,18 @@ static void audio_app_restart_stream(uintptr_t param) { } } -// Invoked when an isochronous transfer fails: the stream was stopped by the -// driver, re-open it after a short delay so the device can recover. +// Invoked when stream activation or an isochronous transfer fails: the stream +// was stopped by the driver, re-open it after a short delay so the device can +// recover. void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)xferred_bytes; err_cb_count++; - printf(" AUDIO transfer error: addr=%u stream=%u xferred_bytes=%u\r\n", idx, stream_idx, (unsigned)xferred_bytes); + if (stream_idx == cap_stream_idx) { + mic_ready = false; + } else if (stream_idx == spk_stream_idx) { + spk_ready = false; + } + printf(" AUDIO stream error: idx=%u stream=%u xferred_bytes=%u\r\n", idx, stream_idx, (unsigned)xferred_bytes); app_defer_ms_async(100, (app_defer_func_t)audio_app_restart_stream, ((uintptr_t)idx << 8) | stream_idx); } diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 57aaf7d52..78ecd9525 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -373,6 +373,12 @@ static void audioh_stream_fail(tuh_audio_stream_t *s) { s->running = false; } +static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes) { + s->running = false; + tu_edpt_stream_clear(&s->edpt); + tuh_audio_err_cb(s->idx, s->stream_idx, xferred_bytes); +} + // Set the endpoint sampling frequency (3 bytes little-endian) static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete_cb) { const audioh_stream_map_t *map = &s->map[s->active_config]; @@ -496,9 +502,7 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin // Failed, stalled, or aborted transfers never carry valid audio data if (result != XFER_RESULT_SUCCESS) { TU_LOG_DRV(" AUDIO transfer failed: addr=%u ep=%02x result=%u\r\n", dev_addr, ep_addr, result); - s->running = false; - tu_edpt_stream_clear(&s->edpt); // discard queued data - tuh_audio_err_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); + audioh_stream_error(s, (uint16_t)xferred_bytes); return true; } @@ -1080,7 +1084,7 @@ static void audioh_stream_start_set_freq_complete(tuh_xfer_t *xfer) { } if (xfer->result != XFER_RESULT_SUCCESS) { TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); - s->running = false; + audioh_stream_error(s, 0); return; } audioh_stream_start_xfer(s); @@ -1090,7 +1094,7 @@ static bool audioh_stream_start_active(tuh_audio_stream_t *s) { const audioh_stream_map_t *map = &s->map[s->active_config]; if (map->sam_freq_ctrl) { if (!audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete)) { - s->running = false; + audioh_stream_error(s, 0); return false; } } else { @@ -1106,7 +1110,7 @@ static void audioh_stream_start_complete(tuh_xfer_t *xfer) { } if (xfer->result != XFER_RESULT_SUCCESS) { TU_LOG_DRV(" AUDIO SET_INTERFACE activate failed: result=%u\r\n", xfer->result); - s->running = false; + audioh_stream_error(s, 0); return; } (void)audioh_stream_start_active(s); @@ -1152,16 +1156,18 @@ bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s && s->state == STREAM_STATE_READY, false); + const audioh_stream_map_t *map = &s->map[s->active_config]; + // Leave the stream running if SET_INTERFACE cannot be submitted, so the + // caller can retry without the host and device states diverging. + TU_VERIFY(tuh_interface_set(s->daddr, map->itf_num, 0, audioh_stream_stop_complete, (uintptr_t)s), false); + // The in-flight transfer (if any) completes and its data is discarded; - // queued frames are dropped as well. The interface is deactivated (alt 0) - // so the device stops transferring. + // queued frames are dropped as well. The interface is being deactivated so + // the device stops transferring. s->running = false; tu_edpt_stream_clear(&s->edpt); s->rem_acc = 0; // restart the pacing accumulator on the next tuh_audio_start() - - // Keep stop retryable if EP0 is busy and SET_INTERFACE cannot be submitted. - const audioh_stream_map_t *map = &s->map[s->active_config]; - return tuh_interface_set(s->daddr, map->itf_num, 0, audioh_stream_stop_complete, (uintptr_t)s); + return true; } uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count) { diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index b1e2a9169..98e103208 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -239,8 +239,9 @@ void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_byte // a complete packet is not queued. void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); -// Invoked when an isochronous transfer fails. The stream is stopped -// (tuh_audio_start() must be called again to resume). +// Invoked when asynchronous stream activation or an isochronous transfer +// fails. The stream is stopped (tuh_audio_start() must be called again to +// resume). xferred_bytes is zero for an activation failure. void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); //--------------------------------------------------------------------+ diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index 63f59c454..51342bfac 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -50,6 +50,18 @@ static uint8_t edpt_xfer_data[16][8]; static uint8_t *edpt_xfer_buffer[16]; static uint8_t edpt_xfer_count; +static uint8_t err_cb_count; +static uint8_t err_cb_idx; +static uint8_t err_cb_stream_idx; +static uint16_t err_cb_xferred_bytes; + +void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + err_cb_count++; + err_cb_idx = idx; + err_cb_stream_idx = stream_idx; + err_cb_xferred_bytes = xferred_bytes; +} + tusb_speed_t tuh_speed_get(uint8_t daddr) { (void)daddr; return TUSB_SPEED_FULL; @@ -439,6 +451,11 @@ void setUp(void) { memset(edpt_xfer_buffer, 0, sizeof(edpt_xfer_buffer)); edpt_xfer_count = 0; + err_cb_count = 0; + err_cb_idx = TUSB_INDEX_INVALID_8; + err_cb_stream_idx = TUSB_INDEX_INVALID_8; + err_cb_xferred_bytes = 0; + fu_cb_count = 0; fu_cb_user_data = 0; @@ -558,6 +575,44 @@ void test_audio_host_sets_sampling_frequency_after_each_stream_activation(void) TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count); } +void test_audio_host_reports_asynchronous_start_failures(void) { + mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_FAILED); + TEST_ASSERT_EQUAL_UINT8(1, err_cb_count); + TEST_ASSERT_EQUAL_UINT8(0, err_cb_idx); + TEST_ASSERT_EQUAL_UINT8(0, err_cb_stream_idx); + TEST_ASSERT_EQUAL_UINT16(0, err_cb_xferred_bytes); + + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + complete_control_xfer(XFER_RESULT_STALLED); + TEST_ASSERT_EQUAL_UINT8(2, err_cb_count); + + control_xfer_result = false; + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(3, err_cb_count); +} + +void test_audio_host_keeps_running_when_stop_cannot_be_submitted(void) { + mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + + interface_set_result = false; + TEST_ASSERT_FALSE(tuh_audio_stop(0, 0)); + + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96)); + TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count); +} + void test_audio_host_parses_discrete_frequencies_with_interval_greater_than_one(void) { tuh_audio_stream_config_t config; open_descriptors(playback_with_two_frequencies, sizeof(playback_with_two_frequencies)); -- cgit v1.3.1 From ea7d4ef12a4cd2b6fb6986a091888bc838b12574 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:07 +0200 Subject: fix(audio): discard capture data without playback Continue draining the capture FIFO but skip channel conversion and writes when playback is unavailable or intentionally stopped. This keeps capture running safely in microphone-only phases. Signed-off-by: HiFiPHile --- examples/host/audio_host/src/audio_app.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 0c4ed8a23..3102c28c9 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -287,6 +287,9 @@ void audio_app_task_read(void) { if (frames == 0) { return; } + if (!spk_ready) { + return; // capture-only device or playback intentionally stopped + } if (spk_config.channels == mic_config.channels) { memcpy(spk_samples, mic_samples, frames * mic_config.channels * sizeof(int16_t)); -- cgit v1.3.1 From b45f1292dd468a542197c48c3f87b471dd64a3ef Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:08 +0200 Subject: feat(audio): discover stream mute and volume controls Inspect each associated Feature Unit during mount, cache master mute support and the common MIN, MAX, and RES volume range, and ignore units with neither control. Add typed asynchronous and synchronous mute and volume APIs and demonstrate them in the host example. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 20 +- examples/host/audio_host/src/audio_app.c | 73 ++++-- src/class/audio/audio_host.c | 315 +++++++++++++++++++---- src/class/audio/audio_host.h | 46 +++- test/unit-test/test/host/audio/test_audio_host.c | 166 +++++++++++- 5 files changed, 539 insertions(+), 81 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index bbb3a7684..5413895a4 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -6,7 +6,7 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) - Enumerates and mounts USB Audio Class 1.0 devices - Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) -- Reports the Feature Unit ID associated with each stream and sets the master volume when one is available +- Reports each stream's master mute/volume capabilities and cached volume range - Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 kHz fallback; stereo preferred, mono accepted) - Echoes captured audio to an S16_LE playback stream at the same sample rate (same channel count preferred, mono/stereo conversion otherwise) - Frame-based FIFO API: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules transfers at the endpoint's polling interval @@ -26,7 +26,7 @@ The echo needs a matching S16_LE playback stream at the capture sample rate; dev - Explicit feedback endpoint data is ignored. Asynchronous playback still uses the nominal sample rate, but device/host clock drift is not corrected and may cause underruns, overruns, or audible pops and clicks. An implicit-feedback IN endpoint is treated as an ordinary audio-data endpoint and is not used to pace playback. - UAC1 Type I Format descriptors with `bSamFreqType == 0` are unsupported; the driver requires a list of discrete sampling frequencies. -- Feature Unit mute, bass, mid, treble, volume, delay, AGC, bass boost, and loudness controls are supported. Graphic EQ and unknown control widths are rejected, and only one Feature Unit request may be in flight per device. +- Master mute and volume controls are discovered before the mount callback, including the volume MIN/MAX/RES range. Feature Units without master mute or volume are ignored. The typed API controls the master channel; the lower-level Feature Unit API remains available for fixed-width UAC1 controls on the associated unit. - The UAC1 `MaxPacketsOnly` endpoint attribute is not supported. OUT transfers are not padded to `wMaxPacketSize`, and padding in IN transfers is not removed from the reported audio data. ## Building @@ -65,10 +65,10 @@ make BOARD= flash 2. Connect a USB Audio device (UAC 1.0) to the USB host port 3. Open a serial terminal to view output 4. The example will: - - Print each stream's Feature Unit ID and supported configurations when mounted + - Print each stream's Feature Unit ID, master mute/volume capabilities, cached volume range, and supported configurations when mounted - Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 kHz fallback; stereo preferred, mono accepted) and configure it - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) - - Set the microphone and speaker master volume to `0x0600` when their streams have a Feature Unit + - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB - Drain the capture FIFO in `audio_app_task_read()` and queue the frames into the playback FIFO; a sine test tone plays on the playback stream when no capture stream is echoing - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; a failed stream is restarted automatically 100 ms after the error callback @@ -79,17 +79,25 @@ TinyUSB Host USB Audio Example Connect a USB Audio Device (UAC 1.0) to test Audio device mounted: idx=0 addr=1 capture stream 1 Feature Unit ID: 5, configurations: 2 + master mute supported + master volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 playback stream 0 Feature Unit ID: 2, configurations: 2 + master mute supported + master volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 Configuring 48 kHz S16_LE capture (2 channels) Microphone configured - Microphone Feature Unit 5 master volume set: 0x0600 + Microphone Feature Unit 5 master mute: off + Microphone Feature Unit 5 master volume: 0 (1/256 dB) + Microphone master volume set: -1536 (1/256 dB) Configuring 48 kHz S16_LE playback (2 channels) Speaker configured - Speaker Feature Unit 2 master volume set: 0x0600 + Speaker Feature Unit 2 master mute: off + Speaker Feature Unit 2 master volume: 0 (1/256 dB) + Speaker master volume set: -1536 (1/256 dB) ``` ## Configuration diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 3102c28c9..9a1434284 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -28,11 +28,10 @@ // - AUDIO_MAX_FRAME_COUNT: buffer holds up to 48 frames (1 ms of 48 kHz) // - AUDIO_MAX_CHANNELS: maximum channels of the capture/playback stream // - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default) -#define AUDIO_MAX_FRAME_COUNT 48 -#define AUDIO_MAX_CHANNELS 2 -#define SAMPLE_RATES {48000, 44100} -// UAC1 volume values are signed 1/256 dB; 0x0600 selects +6 dB. -#define FEATURE_UNIT_VOLUME 0x0600 +#define AUDIO_MAX_FRAME_COUNT 48 +#define AUDIO_MAX_CHANNELS 2 +#define SAMPLE_RATES {48000, 44100} +#define FEATURE_UNIT_VOLUME_DB (-6 * 256) static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index @@ -255,17 +254,6 @@ void led_blinking_task(void) { err_cb_count = 0; #endif -#if 0 - // Print the current Feature Unit volume, which is set to 0x0600 in mic_configured() and can be changed by the device. - uint16_t volume = 0x0001; - tuh_audio_feature_unit_get_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); - printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); - uint16_t mute = 0x0000; - tuh_audio_feature_unit_get_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_MUTE, 0, &mute); - mute=!mute; // toggle mute for demonstration - tuh_audio_feature_unit_set_sync(audio_idx, cap_stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute); - printf(" Feature Unit mute set: 0x%04x\r\n", (unsigned int)mute); -#endif } //--------------------------------------------------------------------+ @@ -377,6 +365,14 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx); printf(" %s stream %u Feature Unit ID: %u, configurations: %u\r\n", dir_name, stream_idx, feature_unit_id, tuh_audio_config_count(idx, stream_idx)); + tuh_audio_volume_range_t range; + if (tuh_audio_mute_supported(idx, stream_idx)) { + printf(" master mute supported\r\n"); + } + if (tuh_audio_volume_range_get(idx, stream_idx, &range)) { + printf(" master volume range: min=%d max=%d res=%u (1/256 dB)\r\n", (int)range.min, (int)range.max, + (unsigned)range.res); + } for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { tuh_audio_stream_config_t config; if (tuh_audio_config_get(idx, stream_idx, i, &config)) { @@ -386,19 +382,44 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { } } -static void set_stream_volume(uint8_t idx, uint8_t stream_idx, const char *stream_name) { +static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const char *stream_name) { const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx); if (feature_unit_id == 0) { - printf(" %s stream has no Feature Unit\r\n", stream_name); + printf(" %s stream has no master mute/volume Feature Unit\r\n", stream_name); return; } - uint16_t volume = FEATURE_UNIT_VOLUME; - tusb_xfer_result_t result = tuh_audio_feature_unit_set_sync(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); - if (result == XFER_RESULT_SUCCESS) { - printf(" %s Feature Unit %u master volume set: 0x%04x\r\n", stream_name, feature_unit_id, (unsigned int)volume); - } else { - printf(" Setting %s Feature Unit %u volume failed: result=%u\r\n", stream_name, feature_unit_id, result); + if (tuh_audio_mute_supported(idx, stream_idx)) { + bool mute; + tusb_xfer_result_t result = tuh_audio_mute_get_sync(idx, stream_idx, &mute); + if (result == XFER_RESULT_SUCCESS) { + printf(" %s Feature Unit %u master mute: %s\r\n", stream_name, feature_unit_id, mute ? "on" : "off"); + result = tuh_audio_mute_set_sync(idx, stream_idx, false); + } + if (result != XFER_RESULT_SUCCESS) { + printf(" Accessing %s master mute failed: result=%u\r\n", stream_name, result); + } + } + + tuh_audio_volume_range_t range; + if (tuh_audio_volume_range_get(idx, stream_idx, &range)) { + int16_t volume; + tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, &volume); + if (result == XFER_RESULT_SUCCESS) { + printf(" %s Feature Unit %u master volume: %d (1/256 dB)\r\n", stream_name, feature_unit_id, volume); + int32_t target = FEATURE_UNIT_VOLUME_DB; + target = TU_MAX(target, range.min); + target = TU_MIN(target, range.max); + target = range.min + ((target - range.min + range.res / 2) / range.res) * range.res; + target = TU_MIN(target, range.max); + result = tuh_audio_volume_set_sync(idx, stream_idx, (int16_t)target); + if (result == XFER_RESULT_SUCCESS) { + printf(" %s master volume set: %d (1/256 dB)\r\n", stream_name, (int)target); + } + } + if (result != XFER_RESULT_SUCCESS) { + printf(" Accessing %s master volume failed: result=%u\r\n", stream_name, result); + } } } @@ -462,7 +483,7 @@ static void tuh_audio_mount_async(uintptr_t param) { // one ms of audio at the selected rate, rounded down to whole frames audio_frame_count = sample_rate / 1000; printf(" Microphone configured\r\n"); - set_stream_volume(idx, stream_idx, "Microphone"); + configure_stream_controls(idx, stream_idx, "Microphone"); mic_ready = tuh_audio_start(idx, stream_idx); capture_found = true; break; @@ -519,7 +540,7 @@ static void tuh_audio_mount_async(uintptr_t param) { // playback-only device: set the frame cadence from the selected rate audio_frame_count = spk_config.sample_rate / 1000; sine_phase = 0; - set_stream_volume(idx, spk_stream_idx, "Speaker"); + configure_stream_controls(idx, spk_stream_idx, "Speaker"); if (capture_found) { // Start in the mic-only phase without briefly activating playback first. diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index c12493a5c..2a8df9075 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -135,7 +135,10 @@ typedef struct { bool running; // tuh_audio_start() called, transfers may be submitted // One Feature Unit associated with this logical stream (0 = none) - uint8_t feature_unit_id; + uint8_t feature_unit_id; + bool mute_supported; + bool volume_supported; + tuh_audio_volume_range_t volume_range; // Size in bytes of one frame (all channels) of the active configuration uint16_t frame_bytes; @@ -173,12 +176,16 @@ typedef struct { TUH_EPBUF_DEF(fu_ctrl, 8); // feature-unit SET data TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); // capture transfer buffer TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); // playback transfer buffer - // Feature-unit request state: only one GET or SET in flight per device - tuh_xfer_cb_t complete_cb; - uintptr_t user_data; - uint16_t *value; - uint8_t width; - bool fu_busy; + // Feature-unit request state: only one operation in flight per device + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + void *value; + tuh_audio_volume_range_t volume_range_temp; + uint8_t width; + uint8_t value_type; + uint8_t volume_stream_idx; + uint8_t volume_range_step; + bool fu_busy; } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; @@ -229,6 +236,12 @@ static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, return NULL; } +static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_id, uint8_t controls) { + s->feature_unit_id = unit_id; + s->mute_supported = (controls & AUDIO10_FU_CONTROL_BM_MUTE) != 0; + s->volume_supported = (controls & AUDIO10_FU_CONTROL_BM_VOLUME) != 0; +} + // Map a UAC 1.0 (subframe size, bit resolution) pair to a supported format static bool audioh_format_from_uac1(uint8_t subframe_size, uint8_t bit_resolution, tuh_audio_format_t *format) { if (subframe_size == 1 && bit_resolution == 8) { @@ -282,6 +295,9 @@ static void audioh_stream_reset(tuh_audio_stream_t *s) { s->state = STREAM_STATE_IDLE; s->running = false; s->feature_unit_id = 0; + s->mute_supported = false; + s->volume_supported = false; + s->volume_range = (tuh_audio_volume_range_t){0}; s->frame_bytes = 0; s->frames_per_interval = 0; s->frames_rem = 0; @@ -811,6 +827,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface typedef struct { uint8_t id; uint8_t source_id; + uint8_t controls; } audioh_fu_info_t; audioh_fu_info_t pending_fu[TUH_AUDIO_STREAM_DIRECTION_COUNT] = {0}; @@ -837,9 +854,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface usb_input_terminal_id = terminal->bTerminalID; for (uint8_t i = 0; i < TU_ARRAY_SIZE(pending_fu); i++) { if (pending_fu[i].source_id == usb_input_terminal_id) { - p_audio->out_stream.feature_unit_id = pending_fu[i].id; - pending_fu[i].id = 0; - pending_fu[i].source_id = 0; + audioh_stream_set_feature_unit(&p_audio->out_stream, pending_fu[i].id, pending_fu[i].controls); + pending_fu[i] = (audioh_fu_info_t){0}; break; } } @@ -855,9 +871,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface usb_output_source_id = terminal->bSourceID; for (uint8_t i = 0; i < TU_ARRAY_SIZE(pending_fu); i++) { if (pending_fu[i].id == usb_output_source_id) { - p_audio->in_stream.feature_unit_id = pending_fu[i].id; - pending_fu[i].id = 0; - pending_fu[i].source_id = 0; + audioh_stream_set_feature_unit(&p_audio->in_stream, pending_fu[i].id, pending_fu[i].controls); + pending_fu[i] = (audioh_fu_info_t){0}; break; } } @@ -865,18 +880,28 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 5)) { + if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 7)) { + goto open_failed; + } + const uint8_t control_size = p_desc[5]; + if (!TUH_VALIDATE_BASIC(control_size > 0) || + !TUH_VALIDATE_BASIC(control_size <= (uint8_t)(tu_desc_len(p_desc) - 7))) { goto open_failed; } + const uint8_t controls = p_desc[6] & (AUDIO10_FU_CONTROL_BM_MUTE | AUDIO10_FU_CONTROL_BM_VOLUME); + if (controls == 0) { + break; + } + bool mapped = false; if (usb_input_terminal_id != 0 && p_audio->out_stream.feature_unit_id == 0 && p_desc[4] == usb_input_terminal_id) { - p_audio->out_stream.feature_unit_id = p_desc[3]; + audioh_stream_set_feature_unit(&p_audio->out_stream, p_desc[3], controls); mapped = true; } if (usb_output_source_id != 0 && p_audio->in_stream.feature_unit_id == 0 && p_desc[3] == usb_output_source_id) { - p_audio->in_stream.feature_unit_id = p_desc[3]; + audioh_stream_set_feature_unit(&p_audio->in_stream, p_desc[3], controls); mapped = true; } if (!mapped) { @@ -884,6 +909,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface if (pending_fu[i].id == 0) { pending_fu[i].id = p_desc[3]; pending_fu[i].source_id = p_desc[4]; + pending_fu[i].controls = controls; break; } } @@ -956,6 +982,8 @@ open_failed: //--------------------------------------------------------------------+ // Set Configuration //--------------------------------------------------------------------+ +static void audioh_mount_feature_unit_next(uint8_t idx); + bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { uint8_t idx = TUSB_INDEX_INVALID_8; for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { @@ -972,13 +1000,9 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { return true; } - audioh_interface_t *p_audio = &_audioh_itf[idx]; - p_audio->mounted = true; - TU_LOG_DRV(" AUDIO mounted: addr = %u index = %u\r\n", dev_addr, idx); - - tuh_audio_mount_cb(idx); - - usbh_driver_set_config_complete(dev_addr, itf_num); + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + epbuf->volume_stream_idx = 0; + audioh_mount_feature_unit_next(idx); return true; } @@ -1005,6 +1029,24 @@ uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx) { return s->feature_unit_id; } +bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted, false); + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + return s != NULL && s->mute_supported; +} + +bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volume_range_t *range) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX && range != NULL, false); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted, false); + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s != NULL && s->volume_supported, false); + *range = s->volume_range; + return true; +} + uint8_t tuh_audio_stream_count(uint8_t dev_idx) { TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0); audioh_interface_t *p_audio = &_audioh_itf[dev_idx]; @@ -1300,21 +1342,42 @@ static void audioh_fu_set_complete(tuh_xfer_t *xfer) { } } +enum { + AUDIOH_FU_VALUE_U16, + AUDIOH_FU_VALUE_BOOL, + AUDIOH_FU_VALUE_I16 +}; + +static void audioh_fu_value_store(audioh_epbuf_t *epbuf) { + if (epbuf->value_type == AUDIOH_FU_VALUE_BOOL) { + *((bool *)epbuf->value) = epbuf->fu_ctrl[0] != 0; + } else if (epbuf->width == 1) { + *((uint16_t *)epbuf->value) = epbuf->fu_ctrl[0]; + } else { + const uint16_t value = tu_le16toh(tu_unaligned_read16(epbuf->fu_ctrl)); + if (epbuf->value_type == AUDIOH_FU_VALUE_I16) { + *((int16_t *)epbuf->value) = (int16_t)value; + } else { + *((uint16_t *)epbuf->value) = value; + } + } +} + // Convert the raw control value to host order and chain to the application callback static void audioh_fu_get_complete(tuh_xfer_t *xfer) { const uint8_t idx = (uint8_t)xfer->user_data; audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; tuh_xfer_cb_t app_cb = epbuf->complete_cb; uintptr_t user_data = epbuf->user_data; - uint16_t *value = epbuf->value; - const uint8_t width = epbuf->width; epbuf->complete_cb = NULL; epbuf->fu_busy = false; - if (app_cb != NULL && value != NULL && xfer->result == XFER_RESULT_SUCCESS) { - const uint8_t *raw = (const uint8_t *)value; - // The raw bytes are little-endian on the wire: rebuild the host-order value - *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8)); + if (epbuf->value != NULL && xfer->result == XFER_RESULT_SUCCESS) { + if (xfer->actual_len == epbuf->width) { + audioh_fu_value_store(epbuf); + } else { + xfer->result = XFER_RESULT_FAILED; + } } xfer->user_data = user_data; @@ -1323,6 +1386,133 @@ static void audioh_fu_get_complete(tuh_xfer_t *xfer) { } } +enum { + AUDIOH_VOLUME_RANGE_MIN, + AUDIOH_VOLUME_RANGE_MAX, + AUDIOH_VOLUME_RANGE_RES, + AUDIOH_VOLUME_RANGE_COUNT +}; + +static uint8_t audioh_fu_volume_range_request(uint8_t step) { + switch (step) { + case AUDIOH_VOLUME_RANGE_MIN: + return AUDIO10_CS_REQ_GET_MIN; + case AUDIOH_VOLUME_RANGE_MAX: + return AUDIO10_CS_REQ_GET_MAX; + case AUDIOH_VOLUME_RANGE_RES: + return AUDIO10_CS_REQ_GET_RES; + default: + return AUDIO10_CS_REQ_UNDEF; + } +} + +static void audioh_fu_volume_range_store(audioh_epbuf_t *epbuf) { + const uint16_t value = tu_le16toh(tu_unaligned_read16(epbuf->fu_ctrl)); + switch (epbuf->volume_range_step) { + case AUDIOH_VOLUME_RANGE_MIN: + epbuf->volume_range_temp.min = (int16_t)value; + break; + case AUDIOH_VOLUME_RANGE_MAX: + epbuf->volume_range_temp.max = (int16_t)value; + break; + case AUDIOH_VOLUME_RANGE_RES: + epbuf->volume_range_temp.res = value; + break; + default: + break; + } +} + +static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer); + +static bool audioh_mount_feature_unit_submit(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->volume_stream_idx); + TU_ASSERT(s != NULL); + + const tusb_control_request_t request = { + .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, + .bRequest = audioh_fu_volume_range_request(epbuf->volume_range_step), + .wValue = tu_htole16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0)), + .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), + .wLength = tu_htole16(2), + }; + tuh_xfer_t xfer = {.daddr = p_audio->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = epbuf->fu_ctrl, + .complete_cb = audioh_mount_feature_unit_complete, + .user_data = (uintptr_t)idx}; + return tuh_control_xfer(&xfer); +} + +static void audioh_mount_feature_unit_next(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + + while (epbuf->volume_stream_idx < p_audio->stream_count) { + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->volume_stream_idx); + TU_ASSERT(s != NULL, ); + if (s->volume_supported) { + epbuf->volume_range_temp = (tuh_audio_volume_range_t){0}; + epbuf->volume_range_step = AUDIOH_VOLUME_RANGE_MIN; + epbuf->fu_busy = true; + if (audioh_mount_feature_unit_submit(idx)) { + return; + } + s->volume_supported = false; + if (!s->mute_supported) { + s->feature_unit_id = 0; + } + epbuf->fu_busy = false; + } + epbuf->volume_stream_idx++; + } + + p_audio->mounted = true; + TU_LOG_DRV(" AUDIO mounted: addr = %u index = %u\r\n", p_audio->daddr, idx); + tuh_audio_mount_cb(idx); + usbh_driver_set_config_complete(p_audio->daddr, p_audio->ac_itf_num); +} + +static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { + const uint8_t idx = (uint8_t)xfer->user_data; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + if (!epbuf->fu_busy) { + return; + } + audioh_interface_t *p_audio = &_audioh_itf[idx]; + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->volume_stream_idx); + TU_ASSERT(s != NULL, ); + + if (xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 2) { + audioh_fu_volume_range_store(epbuf); + epbuf->volume_range_step++; + + if (epbuf->volume_range_step < AUDIOH_VOLUME_RANGE_COUNT) { + if (audioh_mount_feature_unit_submit(idx)) { + return; + } + xfer->result = XFER_RESULT_FAILED; + } else if (epbuf->volume_range_temp.min <= epbuf->volume_range_temp.max && epbuf->volume_range_temp.res > 0) { + s->volume_range = epbuf->volume_range_temp; + } else { + xfer->result = XFER_RESULT_FAILED; + } + } + + if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != 2) { + s->volume_supported = false; + if (!s->mute_supported) { + s->feature_unit_id = 0; + } + } + epbuf->fu_busy = false; + epbuf->volume_stream_idx++; + audioh_mount_feature_unit_next(idx); +} + bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); @@ -1378,8 +1568,8 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control return true; } -bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, - uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { +static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, + void *value, uint8_t value_type, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted && value, false); @@ -1391,7 +1581,10 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; TU_VERIFY(!epbuf->fu_busy, false); - epbuf->fu_busy = true; + epbuf->fu_busy = true; + epbuf->value = value; + epbuf->width = width; + epbuf->value_type = value_type; const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, @@ -1407,16 +1600,17 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, - .buffer = (uint8_t *)value, + .buffer = epbuf->fu_ctrl, .complete_cb = NULL, .user_data = user_data}; if (!tuh_control_xfer(&xfer)) { epbuf->fu_busy = false; return false; } - if (xfer.result == XFER_RESULT_SUCCESS) { - const uint8_t *raw = (const uint8_t *)value; - *value = (width == 1) ? (uint16_t)raw[0] : (uint16_t)((uint16_t)raw[0] | ((uint16_t)raw[1] << 8)); + if (xfer.result == XFER_RESULT_SUCCESS && xfer.actual_len == width) { + audioh_fu_value_store(epbuf); + } else if (xfer.result == XFER_RESULT_SUCCESS && user_data != 0) { + *((tusb_xfer_result_t *)user_data) = XFER_RESULT_FAILED; } epbuf->fu_busy = false; return true; @@ -1425,15 +1619,12 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control // Async path: chain the host-order conversion to the application callback epbuf->complete_cb = complete_cb; epbuf->user_data = user_data; - epbuf->value = value; - epbuf->width = width; - - tuh_xfer_t xfer = {.daddr = p_audio->daddr, - .ep_addr = 0, - .setup = &request, - .buffer = (uint8_t *)value, // raw bytes, converted in audioh_fu_get_complete() - .complete_cb = audioh_fu_get_complete, - .user_data = (uintptr_t)idx}; + tuh_xfer_t xfer = {.daddr = p_audio->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = epbuf->fu_ctrl, + .complete_cb = audioh_fu_get_complete, + .user_data = (uintptr_t)idx}; if (!tuh_control_xfer(&xfer)) { epbuf->complete_cb = NULL; @@ -1443,4 +1634,38 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control return true; } +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, + uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + return audioh_feature_unit_get(idx, stream_idx, control_selector, channel, value, AUDIOH_FU_VALUE_U16, complete_cb, + user_data); +} + +bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(tuh_audio_mute_supported(idx, stream_idx), false); + return tuh_audio_feature_unit_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute ? 1 : 0, complete_cb, user_data); +} + +bool tuh_audio_mute_get(uint8_t idx, uint8_t stream_idx, bool *mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(mute != NULL && tuh_audio_mute_supported(idx, stream_idx), false); + return audioh_feature_unit_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute, AUDIOH_FU_VALUE_BOOL, complete_cb, + user_data); +} + +bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + tuh_audio_volume_range_t range; + TU_VERIFY(tuh_audio_volume_range_get(idx, stream_idx, &range), false); + TU_VERIFY(volume >= range.min && volume <= range.max, false); + return tuh_audio_feature_unit_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, (uint16_t)volume, complete_cb, + user_data); +} + +bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + tuh_audio_volume_range_t range; + TU_VERIFY(volume != NULL && tuh_audio_volume_range_get(idx, stream_idx, &range), false); + return audioh_feature_unit_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume, AUDIOH_FU_VALUE_I16, complete_cb, + user_data); +} + #endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 98e103208..e850e4b7c 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -85,6 +85,17 @@ typedef struct { uint8_t channels; } tuh_audio_stream_config_t; +// Volume values are signed 1/256 dB. INT16_MIN represents silence. +#define TUH_AUDIO_VOLUME_SILENCE INT16_MIN + +// One continuous volume range. This matches UAC1 MIN/MAX/RES and the common +// UAC2 RANGE response containing one subrange. +typedef struct { + int16_t min; + int16_t max; + uint16_t res; +} tuh_audio_volume_range_t; + //--------------------------------------------------------------------+ // Stream Enumeration //--------------------------------------------------------------------+ @@ -183,6 +194,10 @@ bool tuh_audio_mounted(uint8_t idx); uint8_t tuh_audio_get_dev_addr(uint8_t idx); // Get the Feature Unit ID associated with a stream (0 = none) uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx); +// True when the stream's Feature Unit supports master mute control. +bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx); +// Get the cached master volume range. Returns false when volume is unsupported. +bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volume_range_t *range); //--------------------------------------------------------------------+ // Control Request API @@ -197,10 +212,19 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control // Get a Feature Unit control (mute, volume, ...) associated with an Audio stream (UAC 1.0) // The value is converted to host byte order before complete_cb is invoked. // Graphic EQ and unknown selectors are unsupported. -// Only one Feature Unit GET or SET request may be in flight per device. +// Only one Feature Unit operation may be in flight per device. bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); +// Master mute and volume controls. Capability and range information is cached +// before tuh_audio_mount_cb() is invoked. +bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_mute_get(uint8_t idx, uint8_t stream_idx, bool *mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data); +bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); +bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); + //--------------------------------------------------------------------+ // Control Request Sync API // Each Function will make a USB control transfer request to/from device the function will block until request is @@ -220,6 +244,26 @@ TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_ge TU_API_SYNC(tuh_audio_feature_unit_get, idx, stream_idx, control_selector, channel, value); } +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_mute_set_sync(uint8_t idx, uint8_t stream_idx, + bool mute) { + TU_API_SYNC(tuh_audio_mute_set, idx, stream_idx, mute); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_mute_get_sync(uint8_t idx, uint8_t stream_idx, + bool *mute) { + TU_API_SYNC(tuh_audio_mute_get, idx, stream_idx, mute); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_volume_set_sync(uint8_t idx, uint8_t stream_idx, + int16_t volume) { + TU_API_SYNC(tuh_audio_volume_set, idx, stream_idx, volume); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_volume_get_sync(uint8_t idx, uint8_t stream_idx, + int16_t *volume) { + TU_API_SYNC(tuh_audio_volume_get, idx, stream_idx, volume); +} + //--------------------------------------------------------------------+ // Callbacks (Weak is optional) //--------------------------------------------------------------------+ diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index 2b0f2fa3d..f02c44095 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -199,9 +199,11 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { 12, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL, _id, U16_TO_U8S_LE(_type), 0, _channels, \ U16_TO_U8S_LE(AUDIO10_CHANNEL_CONFIG_NON_PREDEFINED), 0, 0 -#define TEST_UAC1_FEATURE_UNIT(_id, _source_id) \ - 9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, 2, \ - U16_TO_U8S_LE(AUDIO10_FU_CONTROL_BM_VOLUME), 0 +#define TEST_UAC1_FEATURE_UNIT_CTRL(_id, _source_id, _controls) \ + 9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, 2, U16_TO_U8S_LE(_controls), 0 + +#define TEST_UAC1_FEATURE_UNIT(_id, _source_id) \ + TEST_UAC1_FEATURE_UNIT_CTRL(_id, _source_id, AUDIO10_FU_CONTROL_BM_MUTE | AUDIO10_FU_CONTROL_BM_VOLUME) #define TEST_UAC1_OUTPUT_TERM(_id, _type, _source_id) \ 9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL, _id, U16_TO_U8S_LE(_type), 0, _source_id, 0 @@ -311,6 +313,19 @@ static const uint8_t playback_fu_before_terminal[] = { TEST_UAC1_CS_DATA_EP, }; +static const uint8_t playback_fu_without_mute_volume[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_FEATURE_UNIT_CTRL(PLAYBACK_FU, PLAYBACK_INPUT_TERM, AUDIO10_FU_CONTROL_BM_BASS), + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU), + TEST_UAC1_AS_ALT0, + TEST_UAC1_AS_INTERFACE(1, 1), + TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), + TEST_UAC1_FORMAT(2, 2, 16, 48000), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 192, 1), + TEST_UAC1_CS_DATA_EP, +}; + static const uint8_t capture_fu_before_usb_output[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(CAPTURE_INPUT_TERM, AUDIO_TERM_TYPE_IN_GENERIC_MIC, 1), @@ -504,6 +519,13 @@ static void complete_control_xfer(tusb_xfer_result_t result) { xfer.complete_cb(&xfer); } +static void complete_control_xfer_with_u16(uint16_t value) { + TEST_ASSERT_NOT_NULL(control_xfer.buffer); + tu_unaligned_write16(control_xfer.buffer, tu_htole16(value)); + control_xfer.actual_len = 2; + complete_control_xfer(XFER_RESULT_SUCCESS); +} + void setUp(void) { interface_set_result = true; memset(&interface_xfer, 0, sizeof(interface_xfer)); @@ -553,7 +575,24 @@ static void open_descriptors(const uint8_t *desc, uint16_t desc_len) { static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) { open_descriptors(desc, desc_len); TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + while (!tuh_audio_mounted(0)) { + switch (control_request.bRequest) { + case AUDIO10_CS_REQ_GET_MIN: + complete_control_xfer_with_u16((uint16_t)(-90 * 256)); + break; + case AUDIO10_CS_REQ_GET_MAX: + complete_control_xfer_with_u16(6 * 256); + break; + case AUDIO10_CS_REQ_GET_RES: + complete_control_xfer_with_u16(256); + break; + default: + TEST_FAIL_MESSAGE("Unexpected Feature Unit mount request"); + break; + } + } TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + control_xfer_count = 0; } void test_audio_host_ignores_explicit_feedback_endpoint(void) { @@ -638,6 +677,22 @@ void test_audio_host_maps_duplex_fus_declared_before_usb_terminals(void) { TEST_ASSERT_EQUAL_UINT8(CAPTURE_FU, tuh_audio_get_feature_unit_id(0, 1)); } +void test_audio_host_reads_volume_ranges_for_both_streams_before_mount(void) { + const uint8_t feature_units[] = {PLAYBACK_FU, CAPTURE_FU}; + open_descriptors(duplex_fus_before_usb_terminals, sizeof(duplex_fus_before_usb_terminals)); + + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + for (uint8_t i = 0; i < TU_ARRAY_SIZE(feature_units); i++) { + TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_MIN, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(feature_units[i], AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + complete_control_xfer_with_u16((uint16_t)(-90 * 256)); + complete_control_xfer_with_u16(6 * 256); + complete_control_xfer_with_u16(256); + TEST_ASSERT_EQUAL(i == TU_ARRAY_SIZE(feature_units) - 1, tuh_audio_mounted(0)); + } + TEST_ASSERT_EQUAL_UINT8(6, control_xfer_count); +} + void test_audio_host_sets_sampling_frequency_after_each_stream_activation(void) { mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); @@ -836,6 +891,111 @@ void test_audio_host_uses_correct_feature_unit_widths_and_serializes_requests(vo TEST_ASSERT_EQUAL_HEX32(0x5678, fu_cb_user_data); } +void test_audio_host_reads_and_caches_feature_unit_controls_before_mount(void) { + tuh_audio_volume_range_t range; + open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); + + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + TEST_ASSERT_FALSE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_MIN, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + TEST_ASSERT_EQUAL_UINT16(2, tu_le16toh(control_request.wLength)); + + complete_control_xfer_with_u16((uint16_t)(-90 * 256)); + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_MAX, control_request.bRequest); + TEST_ASSERT_FALSE(tuh_audio_mounted(0)); + + complete_control_xfer_with_u16(6 * 256); + TEST_ASSERT_EQUAL_UINT8(3, control_xfer_count); + TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_RES, control_request.bRequest); + TEST_ASSERT_FALSE(tuh_audio_mounted(0)); + + complete_control_xfer_with_u16(256); + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0)); + TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range)); + TEST_ASSERT_EQUAL_INT16(-90 * 256, range.min); + TEST_ASSERT_EQUAL_INT16(6 * 256, range.max); + TEST_ASSERT_EQUAL_UINT16(256, range.res); +} + +void test_audio_host_mounts_with_mute_only_when_volume_range_fails(void) { + tuh_audio_volume_range_t range; + open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); + + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_control_xfer_with_u16((uint16_t)(-40 * 256)); + complete_control_xfer(XFER_RESULT_STALLED); + + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0)); + TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range)); + TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); +} + +void test_audio_host_rejects_invalid_cached_volume_range(void) { + tuh_audio_volume_range_t range; + open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); + + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_control_xfer_with_u16((uint16_t)(-20 * 256)); + complete_control_xfer_with_u16(0); + complete_control_xfer_with_u16(0); + + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0)); + TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range)); +} + +void test_audio_host_ignores_feature_unit_without_master_mute_or_volume(void) { + tuh_audio_volume_range_t range; + mount_descriptors(playback_fu_without_mute_volume, sizeof(playback_fu_without_mute_volume)); + + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_feature_unit_id(0, 0)); + TEST_ASSERT_FALSE(tuh_audio_mute_supported(0, 0)); + TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range)); +} + +void test_audio_host_typed_mute_and_volume_controls(void) { + bool mute = false; + int16_t volume = 0; + tuh_audio_volume_range_t range; + mount_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); + + TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range)); + TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, (int16_t)(range.max + 1), NULL, 0)); + + TEST_ASSERT_TRUE(tuh_audio_mute_set(0, 0, true, feature_unit_complete, 0x1234)); + TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_SET_CUR, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_MUTE, 0), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_UINT16(1, tu_le16toh(control_request.wLength)); + TEST_ASSERT_EQUAL_HEX8(1, control_buffer[0]); + complete_control_xfer(XFER_RESULT_SUCCESS); + + TEST_ASSERT_TRUE(tuh_audio_mute_get(0, 0, &mute, feature_unit_complete, 0x2345)); + TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_GET_CUR, control_request.bRequest); + control_xfer.buffer[0] = 1; + control_xfer.actual_len = 1; + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_TRUE(mute); + + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, -6 * 256, feature_unit_complete, 0x3456)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_UINT16(2, tu_le16toh(control_request.wLength)); + TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0xFA}), control_buffer, 2); + complete_control_xfer(XFER_RESULT_SUCCESS); + + TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, &volume, feature_unit_complete, 0x4567)); + complete_control_xfer_with_u16((uint16_t)(-12 * 256)); + TEST_ASSERT_EQUAL_INT16(-12 * 256, volume); + TEST_ASSERT_EQUAL_UINT8(4, fu_cb_count); + TEST_ASSERT_EQUAL_HEX32(0x4567, fu_cb_user_data); +} + void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only(void) { uint8_t samples[441 * 4] = {0}; mount_descriptors(playback_44100_max_packets_only, sizeof(playback_44100_max_packets_only)); -- cgit v1.3.1 From cca648556dd0c096aa7210ee5a2b962efae36440 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 26 Aug 2026 15:54:38 +0200 Subject: audio: add UAC2 host streaming support Add protocol-selectable UAC1/UAC2 parsing, UAC2 terminal and Feature Unit topology, Clock Source discovery, sampling-frequency ranges, and protocol-specific stream controls. Cover UAC2 playback, capture, control discovery, and malformed descriptors with unit tests. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 18 +- examples/host/audio_host/src/audio_app.c | 2 +- examples/host/audio_host/src/main.c | 2 +- examples/host/audio_host/src/tusb_config.h | 3 + src/class/audio/audio_host.c | 1163 +++++++++++++++++----- src/class/audio/audio_host.h | 36 +- test/unit-test/CMakeLists.txt | 1 + test/unit-test/project.yml | 1 + test/unit-test/test/host/audio/test_audio_host.c | 304 +++++- 9 files changed, 1240 insertions(+), 290 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 06cc4f197..2b5df7785 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -1,10 +1,10 @@ # USB Audio Host Example -This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) to capture audio from a UAC 1.0 compatible USB microphone and echo it back to the speaker, using a WASAPI/ALSA-like high-level API. The application never touches USB interfaces, alternate settings, or endpoint addresses — it only selects supported `{format, sample_rate, channels}` configurations by stream index. +This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) to capture audio from a UAC 1.0 or UAC 2.0 USB microphone and echo it back to the speaker, using a WASAPI/ALSA-like high-level API. The application never touches USB interfaces, alternate settings, or endpoint addresses — it only selects supported `{format, sample_rate, channels}` configurations by stream index. ## Features -- Enumerates and mounts USB Audio Class 1.0 devices +- Enumerates and mounts USB Audio Class 1.0 and 2.0 devices - Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) - Reports each stream's master mute/volume capabilities and cached volume range - Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 kHz fallback; stereo preferred, mono accepted) @@ -14,7 +14,7 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) ## Supported Devices -This example supports UAC 1.0 devices whose Type I Format descriptor lists discrete sampling frequencies (`bSamFreqType > 0`), such as: +This example supports UAC1 devices whose Type I Format descriptor lists discrete sampling frequencies (`bSamFreqType > 0`) and UAC2 devices using a directly connected Clock Source, such as: - USB microphones - USB headsets (mono microphone + speaker) @@ -26,8 +26,10 @@ The echo needs a matching S16_LE playback stream at the capture sample rate; dev - Explicit feedback endpoints are supported with both 10.14 and 16.16 feedback values. An implicit-feedback IN endpoint is treated as an ordinary audio-data endpoint and is not used to pace playback. - UAC1 Type I Format descriptors with `bSamFreqType == 0` are unsupported; the driver requires a list of discrete sampling frequencies. -- Master mute and volume controls are discovered before the mount callback, including the volume MIN/MAX/RES range. Feature Units without master mute or volume are ignored. The typed API controls the master channel; the lower-level Feature Unit API remains available for fixed-width UAC1 controls on the associated unit. -- The UAC1 `MaxPacketsOnly` endpoint attribute is not supported. OUT transfers are not padded to `wMaxPacketSize`, and padding in IN transfers is not removed from the reported audio data. +- UAC2 supports direct Clock Sources. Clock Selectors, Clock Multipliers, Sampling Rate Converters, Clock Validity, and Valid Alternate Settings controls are not handled. +- UAC2 sampling-frequency RANGE responses are expanded into at most `CFG_TUH_AUDIO_MAX_SAM_FREQ` discrete configurations. A read-only Clock Source exposes only its current frequency. +- Master mute and volume controls are discovered before the mount callback, including the volume range. Feature Units without master mute or volume are ignored. UAC2 volume discovery supports the common RANGE response containing one subrange. +- The `MaxPacketsOnly` endpoint attribute is not supported. OUT transfers are not padded to `wMaxPacketSize`, and padding in IN transfers is not removed from the reported audio data. ## Building @@ -63,7 +65,7 @@ make BOARD= flash ## Usage 1. Build and flash the example to your board -2. Connect a USB Audio device (UAC 1.0) to the USB host port +2. Connect a USB Audio device (UAC 1.0 or 2.0) to the USB host port 3. Open a serial terminal to view output 4. The example will: - Print each stream's Feature Unit ID, master mute/volume capabilities, cached volume range, and supported configurations when mounted @@ -77,7 +79,7 @@ make BOARD= flash ``` TinyUSB Host USB Audio Example -Connect a USB Audio Device (UAC 1.0) to test +Connect a USB Audio Device (UAC 1.0 or 2.0) to test Audio device mounted: idx=0 addr=1 capture stream 1 Feature Unit ID: 5, configurations: 2 master mute supported @@ -105,6 +107,8 @@ Audio device mounted: idx=0 addr=1 Edit `src/tusb_config.h` to modify: - `CFG_TUH_AUDIO_MAX`: Maximum number of audio devices supported +- `CFG_TUH_AUDIO_PROTOCOLS`: Bitmask selecting UAC1 and/or UAC2 support; the example enables both +- `CFG_TUH_AUDIO_MAX_SAM_FREQ`: Maximum number of discrete frequencies retained per alternate setting or UAC2 Clock Source - `CFG_TUH_AUDIO_EPIN_BUFSIZE`: Maximum size of one capture transfer the driver submits (configurations needing a larger per-poll-interval packet are rejected) - `CFG_TUH_AUDIO_EPOUT_BUFSIZE`: Maximum size of one playback transfer the driver submits - `CFG_TUH_AUDIO_STREAM_BUFSIZE`: Per-stream FIFO depth in bytes (default 1024, i.e. four 256 B packets); capture overwrites the oldest frames when full diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 9a1434284..4623dff5a 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -30,7 +30,7 @@ // - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default) #define AUDIO_MAX_FRAME_COUNT 48 #define AUDIO_MAX_CHANNELS 2 -#define SAMPLE_RATES {48000, 44100} +#define SAMPLE_RATES {44100, 48000} #define FEATURE_UNIT_VOLUME_DB (-6 * 256) static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c index 3a3e18ab7..45c468f23 100644 --- a/examples/host/audio_host/src/main.c +++ b/examples/host/audio_host/src/main.c @@ -31,7 +31,7 @@ int main(void) { board_init(); printf("TinyUSB Host USB Audio Example\r\n"); - printf("Connect a USB Audio Device (UAC 1.0) to test\r\n"); + printf("Connect a USB Audio Device (UAC 1.0 or 2.0) to test\r\n"); // init host stack on configured roothub port tusb_rhport_init_t host_init = {.role = TUSB_ROLE_HOST, .speed = TUSB_SPEED_AUTO}; diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h index 9b7f3c94b..5c2d54993 100644 --- a/examples/host/audio_host/src/tusb_config.h +++ b/examples/host/audio_host/src/tusb_config.h @@ -87,6 +87,9 @@ extern "C" { #define CFG_TUH_MSC 0 #define CFG_TUH_VENDOR 0 #define CFG_TUH_AUDIO 1 +#ifndef CFG_TUH_AUDIO_PROTOCOLS + #define CFG_TUH_AUDIO_PROTOCOLS (TUH_AUDIO_PROTOCOL_UAC1 | TUH_AUDIO_PROTOCOL_UAC2) +#endif // max device support (excluding hub device): 1 hub typically has 4 ports #define CFG_TUH_DEVICE_MAX (3 * CFG_TUH_HUB + 1) diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index a28cad156..e676a3354 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -1,15 +1,16 @@ /* * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-FileCopyrightText: Copyright (c) 2026 HiFiPhile (Zixun LI) * SPDX-License-Identifier: MIT * * This file is part of the TinyUSB stack. */ /* - * This driver implements a USB Audio Host (UAC 1.0) class driver with a - * WASAPI/ALSA-like high-level streaming API. The USB Audio topology (Audio - * Control interface, Audio Streaming interfaces, alternate settings, and - * endpoints) is kept private to the driver. + * This driver implements a USB Audio Host (UAC1/UAC2) class driver with a + * WASAPI/ALSA-like high-level streaming API. + * The USB Audio topology (Audio Control interface, Audio Streaming interfaces, alternate settings, and endpoints) is + * kept private to the driver. * * Each instance (Audio Control interface) provides at most one logical stream * per direction: @@ -34,13 +35,13 @@ * grouped by alternate setting so their format and endpoint properties are * stored only once. The selected mapping is applied by tuh_audio_configure(). * - * Non-PCM formats and sampling-frequency ranges are not registered as - * supported configurations during enumeration. - * + * Non-PCM formats are not registered as supported configurations during + * enumeration. UAC1 continuous + * sampling-frequency ranges are unsupported. * The driver owns: * 1. Endpoint selection and opening; only the alternate setting selected by * tuh_audio_configure() is activated by tuh_audio_start(). - * 2. Endpoint sampling-frequency control (SET_CUR, 3 bytes little-endian). + * 2. Protocol-specific sampling-frequency control. */ #include "tusb_option.h" @@ -99,10 +100,30 @@ enum { STREAM_STATE_READY // configured, ready to start/stop }; +enum { + AUDIOH_CTRL_NONE = 0, + AUDIOH_CTRL_READ = 1, + AUDIOH_CTRL_READ_WRITE = 3 +}; + +#if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + #define AUDIOH_MAX_RATE_SOURCES (2 * CFG_TUH_AUDIO_MAX_AS) +#else + #define AUDIOH_MAX_RATE_SOURCES TUH_AUDIO_STREAM_DIRECTION_COUNT +#endif + +// A UAC1 alternate setting owns one descriptor-provided rate source. UAC2 +// alternate settings attached to the same Clock Source share one rate source. +typedef struct { + uint32_t sample_rate[CFG_TUH_AUDIO_MAX_SAM_FREQ]; + uint8_t control_id; // UAC1 endpoint address or UAC2 Clock Source ID + uint8_t sample_rate_count; + uint8_t frequency_access; +} audioh_rate_source_t; + // One Audio Streaming alternate setting. Format, channels, and endpoint // properties are shared by all of its discrete sampling frequencies. typedef struct { - uint32_t sample_rate[CFG_TUH_AUDIO_MAX_SAM_FREQ]; uint16_t ep_size; // endpoint max packet size uint8_t itf_num; uint8_t alt_setting; @@ -111,8 +132,9 @@ typedef struct { uint8_t ep_attr; // bmAttributes synchronization and usage bits uint8_t format; uint8_t channels; - uint8_t sample_rate_count; - bool sam_freq_ctrl; + uint8_t terminal_id; + uint8_t rate_source_idx; + uint8_t rate_count; } audioh_as_config_t; // Explicit feedback exists only for playback alternate settings. @@ -163,8 +185,8 @@ typedef struct { // One Feature Unit associated with this logical stream (0 = none) uint8_t feature_unit_id; - bool mute_supported; - bool volume_supported; + uint8_t mute_access; + uint8_t volume_access; tuh_audio_volume_range_t volume_range; // Size in bytes of one frame (all channels) of the active configuration @@ -183,19 +205,30 @@ typedef struct { typedef struct { uint8_t daddr; // device address (0 = free slot) uint8_t ac_itf_num; // Audio Control interface number + uint8_t protocol; // AUDIO_INT_PROTOCOL_CODE_V1/V2 uint8_t stream_count; + uint8_t rate_source_count; bool mounted; + audioh_rate_source_t rate_source[AUDIOH_MAX_RATE_SOURCES]; + // Logical streams: playback first, then capture (stream index order) tuh_audio_stream_t out_stream; tuh_audio_stream_t in_stream; audioh_playback_t playback; } audioh_interface_t; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + #define AUDIOH_CLOCK_RANGE_BUFSIZE (2 + 12 * CFG_TUH_AUDIO_MAX_SAM_FREQ) + #endif + typedef struct { // Sampling-frequency SET completes before feedback starts, so both // transfers can use one stable DMA buffer. TUH_EPBUF_DEF(rate_feedback, 4); + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + TUH_EPBUF_DEF(clock_range, AUDIOH_CLOCK_RANGE_BUFSIZE); + #endif TUH_EPBUF_DEF(fu_ctrl, 8); // feature-unit SET data TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); // capture transfer buffer TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); // playback transfer buffer @@ -213,6 +246,12 @@ typedef struct { uint8_t range_step; } mount; } fu; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + struct { + uint8_t rate_source_idx; + bool read_cur; + } clock; + #endif bool fu_busy; } audioh_epbuf_t; @@ -242,6 +281,17 @@ static bool audioh_desc_valid(const uint8_t *p_desc, const uint8_t *desc_end, ui TUH_VALIDATE_BASIC(tu_desc_len(p_desc) <= remaining); } +static bool audioh_protocol_enabled(uint8_t protocol) { + switch (protocol) { + case AUDIO_INT_PROTOCOL_CODE_V1: + return (CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1) != 0; + case AUDIO_INT_PROTOCOL_CODE_V2: + return (CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2) != 0; + default: + return false; + } +} + static tuh_audio_stream_t *audioh_get_stream(audioh_interface_t *p_audio, tusb_dir_t direction) { switch (direction) { case TUSB_DIR_IN: @@ -257,7 +307,7 @@ static tuh_audio_stream_t *audioh_get_stream(audioh_interface_t *p_audio, tusb_d static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, uint8_t stream_idx) { for (uint8_t i = 0; i < 2; i++) { tuh_audio_stream_t *s = (i == 0) ? &p_audio->out_stream : &p_audio->in_stream; - if (s->config_count > 0 && s->stream_idx == stream_idx) { + if (s->as_count > 0 && s->stream_idx == stream_idx) { return s; } } @@ -272,28 +322,47 @@ TU_ATTR_ALWAYS_INLINE static inline audioh_as_config_t *audioh_stream_active_as( return &s->as[s->active_as]; } +TU_ATTR_ALWAYS_INLINE static inline audioh_rate_source_t *audioh_as_rate_source(const tuh_audio_stream_t *s, + const audioh_as_config_t *as) { + return &_audioh_itf[s->idx].rate_source[as->rate_source_idx]; +} + +static bool audioh_as_rate_fits(const audioh_interface_t *p_audio, const tuh_audio_stream_t *stream, + const audioh_as_config_t *as, uint32_t sample_rate); + static bool audioh_stream_resolve_config(const tuh_audio_stream_t *s, uint8_t config_idx, uint8_t *as_idx, uint8_t *rate_idx) { TU_VERIFY(config_idx < s->config_count, false); for (uint8_t i = 0; i < s->as_count; i++) { - if (config_idx < s->as[i].sample_rate_count) { - *as_idx = i; - *rate_idx = config_idx; - return true; + if (config_idx < s->as[i].rate_count) { + const audioh_as_config_t *as = &s->as[i]; + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + for (uint8_t source_rate_idx = 0; source_rate_idx < rate_source->sample_rate_count; source_rate_idx++) { + if (audioh_as_rate_fits(&_audioh_itf[s->idx], s, as, rate_source->sample_rate[source_rate_idx])) { + if (config_idx == 0) { + *as_idx = i; + *rate_idx = source_rate_idx; + return true; + } + config_idx--; + } + } + return false; } - config_idx -= s->as[i].sample_rate_count; + config_idx -= s->as[i].rate_count; } return false; } static void audioh_stream_config_fill(const tuh_audio_stream_t *s, uint8_t as_idx, uint8_t rate_idx, tuh_audio_stream_config_t *config) { - const audioh_as_config_t *as = &s->as[as_idx]; - config->dir = (s->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; - config->format = (tuh_audio_format_t)as->format; - config->sample_rate = as->sample_rate[rate_idx]; - config->channels = as->channels; + const audioh_as_config_t *as = &s->as[as_idx]; + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + config->dir = (s->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK; + config->format = (tuh_audio_format_t)as->format; + config->sample_rate = rate_source->sample_rate[rate_idx]; + config->channels = as->channels; } static bool audioh_stream_config_get(const tuh_audio_stream_t *s, uint8_t config_idx, @@ -306,23 +375,24 @@ static bool audioh_stream_config_get(const tuh_audio_stream_t *s, uint8_t config return true; } -static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_id, uint8_t controls) { - s->feature_unit_id = unit_id; - s->mute_supported = (controls & AUDIO10_FU_CONTROL_BM_MUTE) != 0; - s->volume_supported = (controls & AUDIO10_FU_CONTROL_BM_VOLUME) != 0; +static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_id, uint8_t mute_access, + uint8_t volume_access) { + s->feature_unit_id = unit_id; + s->mute_access = mute_access; + s->volume_access = volume_access; } -// Map a UAC 1.0 (subframe size, bit resolution) pair to a supported format -static bool audioh_format_from_uac1(uint8_t subframe_size, uint8_t bit_resolution, tuh_audio_format_t *format) { - if (subframe_size == 1 && bit_resolution == 8) { +// Map a Type-I PCM (subslot size, bit resolution) pair to a supported format. +static bool audioh_format_from_pcm(uint8_t subslot_size, uint8_t bit_resolution, tuh_audio_format_t *format) { + if (subslot_size == 1 && bit_resolution == 8) { *format = TUH_AUDIO_FORMAT_S8; - } else if (subframe_size == 2 && bit_resolution == 16) { + } else if (subslot_size == 2 && bit_resolution == 16) { *format = TUH_AUDIO_FORMAT_S16_LE; - } else if (subframe_size == 3 && bit_resolution == 24) { + } else if (subslot_size == 3 && bit_resolution == 24) { *format = TUH_AUDIO_FORMAT_S24_3LE; - } else if (subframe_size == 4 && bit_resolution == 24) { + } else if (subslot_size == 4 && bit_resolution == 24) { *format = TUH_AUDIO_FORMAT_S24_LE; - } else if (subframe_size == 4 && bit_resolution == 32) { + } else if (subslot_size == 4 && bit_resolution == 32) { *format = TUH_AUDIO_FORMAT_S32_LE; } else { return false; @@ -345,7 +415,7 @@ static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_inter return (uint32_t)((numerator + 500000u) / 1000000u); } -// UAC 1.0 feature-unit control value width (0 = unsupported variable/unknown width) +// Supported Feature Unit control widths (0 = unsupported variable/unknown width). static uint8_t audioh_fu_control_width(uint8_t control_selector) { switch (control_selector) { case AUDIO10_FU_CTRL_MUTE: @@ -366,20 +436,20 @@ static uint8_t audioh_fu_control_width(uint8_t control_selector) { // Reset a stream to its unconfigured state (keeps idx, dir, and FIFO configuration) static void audioh_stream_reset(tuh_audio_stream_t *s) { - s->daddr = 0; - s->stream_idx = TUSB_INDEX_INVALID_8; - s->as_count = 0; - s->config_count = 0; - s->active_config = TUSB_INDEX_INVALID_8; - s->active_as = TUSB_INDEX_INVALID_8; - s->active_rate = TUSB_INDEX_INVALID_8; - s->state = STREAM_STATE_IDLE; - s->running = false; - s->feature_unit_id = 0; - s->mute_supported = false; - s->volume_supported = false; - s->volume_range = (tuh_audio_volume_range_t){0}; - s->frame_bytes = 0; + s->daddr = 0; + s->stream_idx = TUSB_INDEX_INVALID_8; + s->as_count = 0; + s->config_count = 0; + s->active_config = TUSB_INDEX_INVALID_8; + s->active_as = TUSB_INDEX_INVALID_8; + s->active_rate = TUSB_INDEX_INVALID_8; + s->state = STREAM_STATE_IDLE; + s->running = false; + s->feature_unit_id = 0; + s->mute_access = AUDIOH_CTRL_NONE; + s->volume_access = AUDIOH_CTRL_NONE; + s->volume_range = (tuh_audio_volume_range_t){0}; + s->frame_bytes = 0; tu_edpt_stream_close(&s->edpt); tu_edpt_stream_clear(&s->edpt); } @@ -535,22 +605,44 @@ static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes) { tuh_audio_err_cb(s->idx, s->stream_idx, xferred_bytes); } -// Set the endpoint sampling frequency (3 bytes little-endian) +// Submit the protocol-specific sampling-frequency control request. static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete_cb) { - const audioh_as_config_t *as = audioh_stream_active_as(s); - const uint32_t sample_rate = as->sample_rate[s->active_rate]; - uint8_t *ctrl = _audioh_epbuf[s->idx].rate_feedback; + const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + const uint32_t sample_rate = rate_source->sample_rate[s->active_rate]; + uint8_t *ctrl = _audioh_epbuf[s->idx].rate_feedback; + tusb_control_request_t request = {0}; ctrl[0] = (uint8_t)(sample_rate & 0xFF); ctrl[1] = (uint8_t)((sample_rate >> 8) & 0xFF); ctrl[2] = (uint8_t)((sample_rate >> 16) & 0xFF); - - const tusb_control_request_t request = - {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, - .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // control selector, channel 0 - .wIndex = tu_htole16(as->ep_addr), - .wLength = 3}; + switch (_audioh_itf[s->idx].protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_ENDPOINT; + request.bmRequestType_bit.type = TUSB_REQ_TYPE_CLASS; + request.bmRequestType_bit.direction = TUSB_DIR_OUT; + request.bRequest = AUDIO10_CS_REQ_SET_CUR; + request.wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)); + request.wIndex = tu_htole16(rate_source->control_id); + request.wLength = 3; + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_INTERFACE; + request.bmRequestType_bit.type = TUSB_REQ_TYPE_CLASS; + request.bmRequestType_bit.direction = TUSB_DIR_OUT; + request.bRequest = AUDIO20_CS_REQ_CUR; + request.wValue = tu_htole16(tu_u16(AUDIO20_CS_CTRL_SAM_FREQ, 0)); + request.wIndex = tu_htole16(tu_u16(rate_source->control_id, _audioh_itf[s->idx].ac_itf_num)); + request.wLength = 4; + ctrl[3] = (uint8_t)(sample_rate >> 24); + break; + #endif + default: + return false; + } tuh_xfer_t xfer = {.daddr = s->daddr, .ep_addr = 0, @@ -667,9 +759,11 @@ void audioh_close(uint8_t daddr) { _audioh_epbuf[idx].complete_cb = NULL; // drop a pending feature-unit request _audioh_epbuf[idx].fu_busy = false; - p_audio->stream_count = 0; - p_audio->daddr = 0; - p_audio->mounted = false; + p_audio->stream_count = 0; + p_audio->daddr = 0; + p_audio->protocol = 0; + p_audio->rate_source_count = 0; + p_audio->mounted = false; } } @@ -762,11 +856,355 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin // Enumeration //--------------------------------------------------------------------+ + #define AUDIOH_MAX_AC_ENTITIES TUH_AUDIO_STREAM_DIRECTION_COUNT + +typedef struct { + uint8_t id; + uint8_t source_id; + uint8_t clock_id; + uint8_t stream_dir; +} audioh_terminal_info_t; + +typedef struct { + uint8_t id; + uint8_t source_id; + uint8_t mute_access; + uint8_t volume_access; +} audioh_fu_info_t; + +typedef struct { + uint8_t id; + uint8_t frequency_access; +} audioh_clock_info_t; + +typedef struct { + audioh_terminal_info_t terminal[AUDIOH_MAX_AC_ENTITIES]; + audioh_fu_info_t feature_unit[AUDIOH_MAX_AC_ENTITIES]; + audioh_clock_info_t clock[AUDIOH_MAX_AC_ENTITIES]; + uint8_t terminal_count; + uint8_t feature_unit_count; + uint8_t clock_count; +} audioh_ac_map_t; + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static uint8_t audioh_uac2_control_access(uint32_t controls, uint8_t position) { + const uint8_t access = (uint8_t)((controls >> position) & 0x03u); + return (access == AUDIOH_CTRL_READ || access == AUDIOH_CTRL_READ_WRITE) ? access : AUDIOH_CTRL_NONE; +} + #endif + +static bool audioh_as_rate_fits(const audioh_interface_t *p_audio, const tuh_audio_stream_t *stream, + const audioh_as_config_t *as, uint32_t sample_rate) { + const uint32_t frame_bytes = (uint32_t)as->channels * tuh_audio_format_bytes((tuh_audio_format_t)as->format); + const uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + const uint64_t frames_numerator = (uint64_t)sample_rate * audioh_interval_us(as->ep_interval, p_audio->daddr); + const uint64_t max_frames = (frames_numerator + 999999u) / 1000000u; + const uint64_t packet_bytes = max_frames * frame_bytes; + + return packet_bytes > 0 && packet_bytes <= as->ep_size && + (stream->dir != TUSB_DIR_OUT || (packet_bytes <= epbuf_size && packet_bytes <= CFG_TUH_AUDIO_STREAM_BUFSIZE)); +} + +static void audioh_ac_terminal_add(audioh_ac_map_t *map, uint8_t id, uint8_t source_id, uint8_t clock_id, + tusb_dir_t stream_dir) { + if (map->terminal_count < TU_ARRAY_SIZE(map->terminal)) { + map->terminal[map->terminal_count++] = + (audioh_terminal_info_t){.id = id, .source_id = source_id, .clock_id = clock_id, .stream_dir = stream_dir}; + } +} + +static void audioh_ac_feature_unit_add(audioh_ac_map_t *map, uint8_t id, uint8_t source_id, uint8_t mute_access, + uint8_t volume_access) { + if ((mute_access != AUDIOH_CTRL_NONE || volume_access != AUDIOH_CTRL_NONE) && + map->feature_unit_count < TU_ARRAY_SIZE(map->feature_unit)) { + map->feature_unit[map->feature_unit_count++] = + (audioh_fu_info_t){.id = id, .source_id = source_id, .mute_access = mute_access, .volume_access = volume_access}; + } +} + +static const audioh_terminal_info_t *audioh_ac_terminal_find(const audioh_ac_map_t *map, uint8_t id) { + for (uint8_t i = 0; i < map->terminal_count; i++) { + if (map->terminal[i].id == id) { + return &map->terminal[i]; + } + } + return NULL; +} + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static const audioh_clock_info_t *audioh_ac_clock_find(const audioh_ac_map_t *map, uint8_t id) { + for (uint8_t i = 0; i < map->clock_count; i++) { + if (map->clock[i].id == id) { + return &map->clock[i]; + } + } + return NULL; +} + #endif + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 +static bool audioh_uac1_parse_ac_entity(audioh_ac_map_t *map, const uint8_t *p_desc) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_input_terminal_t)), false); + const audio10_desc_input_terminal_t *terminal = (const audio10_desc_input_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + audioh_ac_terminal_add(map, terminal->bTerminalID, 0, 0, TUSB_DIR_OUT); + } + break; + } + case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_output_terminal_t)), false); + const audio10_desc_output_terminal_t *terminal = (const audio10_desc_output_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + audioh_ac_terminal_add(map, terminal->bTerminalID, terminal->bSourceID, 0, TUSB_DIR_IN); + } + break; + } + case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 7), false); + const uint8_t control_size = p_desc[5]; + TU_VERIFY(TUH_VALIDATE_BASIC(control_size > 0), false); + TU_VERIFY(TUH_VALIDATE_BASIC(control_size <= (uint8_t)(tu_desc_len(p_desc) - 7)), false); + const uint8_t controls = p_desc[6]; + audioh_ac_feature_unit_add(map, p_desc[3], p_desc[4], + (controls & AUDIO10_FU_CONTROL_BM_MUTE) ? AUDIOH_CTRL_READ_WRITE : AUDIOH_CTRL_NONE, + (controls & AUDIO10_FU_CONTROL_BM_VOLUME) ? AUDIOH_CTRL_READ_WRITE : AUDIOH_CTRL_NONE); + break; + } + default: + break; + } + return true; +} + #endif + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static bool audioh_uac2_parse_ac_entity(audioh_ac_map_t *map, const uint8_t *p_desc) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO20_CS_AC_INTERFACE_INPUT_TERMINAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_input_terminal_t)), false); + const audio20_desc_input_terminal_t *terminal = (const audio20_desc_input_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + audioh_ac_terminal_add(map, terminal->bTerminalID, 0, terminal->bCSourceID, TUSB_DIR_OUT); + } + break; + } + case AUDIO20_CS_AC_INTERFACE_OUTPUT_TERMINAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_output_terminal_t)), false); + const audio20_desc_output_terminal_t *terminal = (const audio20_desc_output_terminal_t *)p_desc; + if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING) { + audioh_ac_terminal_add(map, terminal->bTerminalID, terminal->bSourceID, terminal->bCSourceID, TUSB_DIR_IN); + } + break; + } + case AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 10), false); + const uint32_t controls = tu_le32toh(tu_unaligned_read32(&p_desc[5])); + audioh_ac_feature_unit_add(map, p_desc[3], p_desc[4], + audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_MUTE_POS), + audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS)); + break; + } + case AUDIO20_CS_AC_INTERFACE_CLOCK_SOURCE: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_clock_source_t)), false); + const audio20_desc_clock_source_t *clock = (const audio20_desc_clock_source_t *)p_desc; + if (map->clock_count < TU_ARRAY_SIZE(map->clock)) { + map->clock[map->clock_count++] = + (audioh_clock_info_t){.id = clock->bClockID, + .frequency_access = + audioh_uac2_control_access(clock->bmControls, AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS)}; + } + break; + } + default: + break; + } + return true; +} + #endif + +static bool audioh_parse_ac_entity(audioh_interface_t *p_audio, audioh_ac_map_t *map, const uint8_t *p_desc) { + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + return audioh_uac1_parse_ac_entity(map, p_desc); + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + return audioh_uac2_parse_ac_entity(map, p_desc); + #endif + default: + return false; + } +} + +static void audioh_link_feature_units(audioh_interface_t *p_audio, const audioh_ac_map_t *map) { + for (uint8_t direction = TUSB_DIR_OUT; direction <= TUSB_DIR_IN; direction++) { + tuh_audio_stream_t *stream = audioh_get_stream(p_audio, (tusb_dir_t)direction); + if (stream == NULL || stream->as_count == 0) { + continue; + } + const audioh_terminal_info_t *terminal = audioh_ac_terminal_find(map, stream->as[0].terminal_id); + if (terminal == NULL || terminal->stream_dir != direction) { + continue; + } + for (uint8_t i = 0; i < map->feature_unit_count; i++) { + const audioh_fu_info_t *fu = &map->feature_unit[i]; + const bool linked = + (direction == TUSB_DIR_OUT) ? (fu->source_id == terminal->id) : (fu->id == terminal->source_id); + if (linked) { + audioh_stream_set_feature_unit(stream, fu->id, fu->mute_access, fu->volume_access); + break; + } + } + } +} + +typedef struct { + uint32_t format_bitmap; + uint16_t format_tag; + const uint8_t *sample_rate_data; + uint8_t terminal_id; + uint8_t format_type; + uint8_t channels; + uint8_t subslot_size; + uint8_t bit_resolution; + uint8_t sample_rate_count; +} audioh_as_class_info_t; + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 +static bool audioh_uac1_parse_as_interface(const uint8_t *p_desc, audioh_as_class_info_t *info) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_cs_as_interface_t)), false); + const audio10_desc_cs_as_interface_t *general = (const audio10_desc_cs_as_interface_t *)p_desc; + info->terminal_id = general->bTerminalLink; + info->format_tag = tu_le16toh(general->wFormatTag); + break; + } + case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 8), false); + info->format_type = p_desc[3]; + if (info->format_type != AUDIO10_FORMAT_TYPE_I) { + break; + } + info->channels = p_desc[4]; + info->subslot_size = p_desc[5]; + info->bit_resolution = p_desc[6]; + info->sample_rate_count = 0; + info->sample_rate_data = NULL; + if (p_desc[7] > 0) { + TU_VERIFY(TUH_VALIDATE_BASIC(p_desc[7] <= (tu_desc_len(p_desc) - 8u) / 3u), false); + info->sample_rate_count = TU_MIN(p_desc[7], CFG_TUH_AUDIO_MAX_SAM_FREQ); + info->sample_rate_data = &p_desc[8]; + } + break; + default: + break; + } + return true; +} + #endif + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static bool audioh_uac2_parse_as_interface(const uint8_t *p_desc, audioh_as_class_info_t *info) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO20_CS_AS_INTERFACE_AS_GENERAL: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_cs_as_interface_t)), false); + const audio20_desc_cs_as_interface_t *general = (const audio20_desc_cs_as_interface_t *)p_desc; + info->terminal_id = general->bTerminalLink; + info->format_type = general->bFormatType; + info->format_bitmap = tu_le32toh(general->bmFormats); + info->channels = general->bNrChannels; + break; + } + case AUDIO20_CS_AS_INTERFACE_FORMAT_TYPE: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_type_I_format_t)), false); + const audio20_desc_type_I_format_t *format = (const audio20_desc_type_I_format_t *)p_desc; + if (format->bFormatType == AUDIO20_FORMAT_TYPE_I) { + info->subslot_size = format->bSubslotSize; + info->bit_resolution = format->bBitResolution; + } + break; + } + default: + break; + } + return true; +} + #endif + +static bool audioh_parse_as_interface(audioh_interface_t *p_audio, const uint8_t *p_desc, + audioh_as_class_info_t *info) { + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + return audioh_uac1_parse_as_interface(p_desc, info); + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + return audioh_uac2_parse_as_interface(p_desc, info); + #endif + default: + return false; + } +} + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static int8_t audioh_uac2_rate_source_get(audioh_interface_t *p_audio, const audioh_ac_map_t *map, + const audioh_terminal_info_t *terminal) { + if (terminal->clock_id == 0) { + return -1; + } + const audioh_clock_info_t *clock = audioh_ac_clock_find(map, terminal->clock_id); + if (clock == NULL || clock->frequency_access == AUDIOH_CTRL_NONE) { + return -1; + } + for (uint8_t i = 0; i < p_audio->rate_source_count; i++) { + if (p_audio->rate_source[i].control_id == clock->id) { + return (int8_t)i; + } + } + if (p_audio->rate_source_count >= AUDIOH_MAX_RATE_SOURCES) { + return -1; + } + const uint8_t idx = p_audio->rate_source_count++; + p_audio->rate_source[idx] = + (audioh_rate_source_t){.control_id = clock->id, .frequency_access = clock->frequency_access}; + return (int8_t)idx; +} + #endif + + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 +static bool audioh_uac1_rates_store(const audioh_interface_t *p_audio, const tuh_audio_stream_t *stream, + audioh_as_config_t *as, const audioh_as_class_info_t *info, + audioh_rate_source_t *rate_source) { + for (uint8_t i = 0; i < info->sample_rate_count; i++) { + const uint8_t *rate_data = &info->sample_rate_data[i * 3u]; + const uint32_t sample_rate = + (uint32_t)rate_data[0] | ((uint32_t)rate_data[1] << 8) | ((uint32_t)rate_data[2] << 16); + if (sample_rate == 0 || !audioh_as_rate_fits(p_audio, stream, as, sample_rate)) { + continue; + } + + const uint8_t rate_idx = rate_source->sample_rate_count++; + rate_source->sample_rate[rate_idx] = sample_rate; + as->rate_count++; + } + return as->rate_count > 0; +} + #endif + // Parse one Audio Streaming interface alternate setting and register its // supported configurations into the matching stream. Returns the descriptor // pointer of the next interface. -static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_desc_interface_t *desc_itf, - const uint8_t *p_desc, const uint8_t *desc_end) { +static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ac_map_t *ac_map, + const tusb_desc_interface_t *desc_itf, const uint8_t *p_desc, + const uint8_t *desc_end) { + #if !(CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2) + (void)ac_map; + #endif TU_VERIFY(audioh_desc_valid(p_desc, desc_end, sizeof(tusb_desc_interface_t)), NULL); const uint8_t itf_num = desc_itf->bInterfaceNumber; @@ -787,12 +1225,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de } // Parse the class-specific and endpoint descriptors of this alternate setting - uint16_t format_tag = 0; - uint8_t num_channels = 0; - uint8_t subframe_size = 0; - uint8_t bit_res = 0; - uint8_t sam_freq_count = 0; - const uint8_t *sam_freq_data = NULL; + audioh_as_class_info_t class_info = {0}; // An AS alternate setting has one audio data endpoint and may have one // explicit feedback endpoint. Implicit-feedback endpoints are data endpoints @@ -818,38 +1251,13 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de switch (tu_desc_type(p_desc)) { case TUSB_DESC_CS_INTERFACE: { TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 3), NULL); - switch (tu_desc_subtype(p_desc)) { - case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { - TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_cs_as_interface_t)), NULL); - const audio10_desc_cs_as_interface_t *desc_as_general = (const audio10_desc_cs_as_interface_t *)p_desc; - format_tag = tu_le16toh(desc_as_general->wFormatTag); - break; - } - case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { - TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 8), NULL); - if (p_desc[3] != AUDIO10_FORMAT_TYPE_I) { - break; // only Type I (PCM) is supported - } - num_channels = p_desc[4]; - subframe_size = p_desc[5]; - bit_res = p_desc[6]; - sam_freq_count = 0; - sam_freq_data = NULL; - if (p_desc[7] > 0) { - TU_VERIFY(TUH_VALIDATE_BASIC(p_desc[7] <= (tu_desc_len(p_desc) - 8u) / 3u), NULL); - sam_freq_count = TU_MIN(p_desc[7], CFG_TUH_AUDIO_MAX_SAM_FREQ); - sam_freq_data = &p_desc[8]; - } - break; - } - default: - break; - } + TU_VERIFY(audioh_parse_as_interface(p_audio, p_desc, &class_info), NULL); break; } case TUSB_DESC_CS_ENDPOINT: { TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 3), NULL); - if (tu_desc_subtype(p_desc) == AUDIO10_CS_EP_SUBTYPE_GENERAL) { + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V1 && + tu_desc_subtype(p_desc) == AUDIO10_CS_EP_SUBTYPE_GENERAL) { TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 4), NULL); const audio10_desc_cs_as_iso_data_ep_t *desc_ep = (const audio10_desc_cs_as_iso_data_ep_t *)p_desc; ep_info.sam_freq_ctrl = (desc_ep->bmAttributes & AUDIO10_CS_AS_ISO_DATA_EP_ATT_SAMPLING_FRQ) != 0; @@ -921,35 +1329,57 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de return p_desc; } - // Reject unsupported formats explicitly - if (format_tag != AUDIO10_DATA_FORMAT_TYPE_I_PCM) { - TU_LOG_DRV(" AUDIO AS itf %u: format tag 0x%04x not supported\r\n", itf_num, format_tag); + // Reject unsupported formats explicitly. + bool pcm_supported = false; + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + pcm_supported = + class_info.format_type == AUDIO10_FORMAT_TYPE_I && class_info.format_tag == AUDIO10_DATA_FORMAT_TYPE_I_PCM; + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + pcm_supported = class_info.format_type == AUDIO20_FORMAT_TYPE_I && + (class_info.format_bitmap & AUDIO20_DATA_FORMAT_TYPE_I_PCM) != 0; + break; + #endif + default: + break; + } + if (!pcm_supported) { + TU_LOG_DRV(" AUDIO AS itf %u: Type-I PCM format not supported\r\n", itf_num); return p_desc; } tuh_audio_format_t format; - if (!audioh_format_from_uac1(subframe_size, bit_res, &format)) { - TU_LOG_DRV(" AUDIO AS itf %u: subframe %u bits %u not supported\r\n", itf_num, subframe_size, bit_res); + if (!audioh_format_from_pcm(class_info.subslot_size, class_info.bit_resolution, &format)) { + TU_LOG_DRV(" AUDIO AS itf %u: subslot %u bits %u not supported\r\n", itf_num, class_info.subslot_size, + class_info.bit_resolution); return p_desc; } - if (num_channels == 0) { + if (class_info.channels == 0) { TU_LOG_DRV(" AUDIO AS itf %u: zero channels not supported\r\n", itf_num); return p_desc; } const uint16_t iso_xfer_size = (tuh_speed_get(p_audio->daddr) == TUSB_SPEED_HIGH) ? TUSB_EPSIZE_ISO_HS_MAX : TUSB_EPSIZE_ISO_FS_MAX; - const uint32_t frame_bytes_32 = (uint32_t)num_channels * tuh_audio_format_bytes(format); + const uint32_t frame_bytes_32 = (uint32_t)class_info.channels * tuh_audio_format_bytes(format); if (frame_bytes_32 == 0 || frame_bytes_32 > iso_xfer_size) { TU_LOG_DRV(" AUDIO AS itf %u: frame size %lu not supported\r\n", itf_num, (unsigned long)frame_bytes_32); return p_desc; } - const uint16_t frame_bytes = (uint16_t)frame_bytes_32; - // Register one AS alternate setting containing its discrete sampling // frequencies. The public API flattens these entries when requested. const audioh_ep_info_t *ep = &ep_info; tuh_audio_stream_t *stream = audioh_get_stream(p_audio, tu_edpt_dir(ep->ep_addr)); TU_ASSERT(stream != NULL, p_desc); + const audioh_terminal_info_t *terminal = audioh_ac_terminal_find(ac_map, class_info.terminal_id); + if (terminal == NULL || terminal->stream_dir != stream->dir) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: terminal %u does not match endpoint direction\r\n", itf_num, alt, + class_info.terminal_id); + return p_desc; + } const uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; @@ -965,48 +1395,63 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de return p_desc; } - audioh_as_config_t as_config = {.ep_size = ep->ep_size, - .itf_num = itf_num, - .alt_setting = alt, - .ep_addr = ep->ep_addr, - .ep_interval = ep->ep_interval, - .ep_attr = ep->ep_attr, - .format = (uint8_t)format, - .channels = num_channels, - .sam_freq_ctrl = ep->sam_freq_ctrl}; - - for (uint8_t i = 0; i < sam_freq_count; i++) { - const uint8_t *rate_data = &sam_freq_data[i * 3u]; - const uint32_t sample_rate = - (uint32_t)rate_data[0] | ((uint32_t)rate_data[1] << 8) | ((uint32_t)rate_data[2] << 16); - if (sample_rate == 0) { - continue; - } - - // The largest whole-frame packet for one poll interval must fit the - // endpoint. Playback must also stage it in the transfer buffer and FIFO. - const uint64_t frames_numerator = (uint64_t)sample_rate * audioh_interval_us(ep->ep_interval, p_audio->daddr); - const uint64_t max_frames = (frames_numerator + 999999u) / 1000000u; - const uint64_t packet_bytes = max_frames * frame_bytes; - if (packet_bytes == 0 || packet_bytes > ep->ep_size || - (stream->dir == TUSB_DIR_OUT && (packet_bytes > epbuf_size || packet_bytes > CFG_TUH_AUDIO_STREAM_BUFSIZE))) { - TU_LOG_DRV(" AUDIO AS itf %u alt %u: packet per interval does not fit endpoint/buffers (ep size %u)\r\n", - itf_num, alt, ep->ep_size); - continue; + audioh_as_config_t as_config = {.ep_size = ep->ep_size, + .itf_num = itf_num, + .alt_setting = alt, + .ep_addr = ep->ep_addr, + .ep_interval = ep->ep_interval, + .ep_attr = ep->ep_attr, + .format = (uint8_t)format, + .channels = class_info.channels, + .terminal_id = class_info.terminal_id}; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + audioh_rate_source_t rate_source = {0}; + #endif + + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + rate_source.control_id = ep->ep_addr; + rate_source.frequency_access = ep->sam_freq_ctrl ? AUDIOH_CTRL_READ_WRITE : AUDIOH_CTRL_NONE; + if (!audioh_uac1_rates_store(p_audio, stream, &as_config, &class_info, &rate_source)) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: no supported sampling frequency\r\n", itf_num, alt); + return p_desc; + } + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: { + const int8_t rate_source_idx = audioh_uac2_rate_source_get(p_audio, ac_map, terminal); + if (rate_source_idx < 0) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: direct Clock Source not found\r\n", itf_num, alt); + return p_desc; + } + as_config.rate_source_idx = (uint8_t)rate_source_idx; + break; } - as_config.sample_rate[as_config.sample_rate_count++] = sample_rate; - } - - if (as_config.sample_rate_count == 0) { - return p_desc; + #endif + default: + return p_desc; } if (stream->as_count >= CFG_TUH_AUDIO_MAX_AS) { TU_LOG_DRV(" AUDIO AS itf %u alt %u: reach max alternate settings %u\r\n", itf_num, alt, CFG_TUH_AUDIO_MAX_AS); return p_desc; } + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V1 && p_audio->rate_source_count >= AUDIOH_MAX_RATE_SOURCES) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: reach max rate sources %u\r\n", itf_num, alt, AUDIOH_MAX_RATE_SOURCES); + return p_desc; + } + #endif const uint8_t as_idx = stream->as_count; - stream->as[as_idx] = as_config; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V1) { + as_config.rate_source_idx = p_audio->rate_source_count; + p_audio->rate_source[p_audio->rate_source_count++] = rate_source; + } + #endif + stream->as[as_idx] = as_config; if (stream->dir == TUSB_DIR_OUT && has_feedback_ep) { audioh_feedback_ep_t *feedback = &p_audio->playback.feedback[as_idx]; feedback->ep_addr = fb_info.ep_addr; @@ -1015,7 +1460,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de feedback->ep_attr = fb_info.ep_attr; } stream->as_count++; - stream->config_count += as_config.sample_rate_count; + stream->config_count += as_config.rate_count; return p_desc; } @@ -1032,13 +1477,16 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_type(desc_itf) == TUSB_DESC_INTERFACE), 0); TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); TU_VERIFY(AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass, 0); - TU_VERIFY(AUDIO_INT_PROTOCOL_CODE_V1 == desc_itf->bInterfaceProtocol, 0); + TU_VERIFY(audioh_protocol_enabled(desc_itf->bInterfaceProtocol), 0); const uint8_t idx = find_new_audio_index(); TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); audioh_interface_t *p_audio = &_audioh_itf[idx]; p_audio->daddr = dev_addr; p_audio->ac_itf_num = desc_itf->bInterfaceNumber; + p_audio->protocol = desc_itf->bInterfaceProtocol; + p_audio->rate_source_count = 0; + tu_memclr(p_audio->rate_source, sizeof(p_audio->rate_source)); audioh_stream_reset(&p_audio->in_stream); audioh_stream_reset(&p_audio->out_stream); audioh_playback_reset(&p_audio->playback); @@ -1047,15 +1495,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); - uint8_t usb_input_terminal_id = 0; - uint8_t usb_output_source_id = 0; - // A Feature Unit may precede the USB terminal that identifies its stream. - typedef struct { - uint8_t id; - uint8_t source_id; - uint8_t controls; - } audioh_fu_info_t; - audioh_fu_info_t pending_fu[TUH_AUDIO_STREAM_DIRECTION_COUNT] = {0}; + audioh_ac_map_t ac_map = {0}; p_desc = tu_desc_next(p_desc); while (p_desc < desc_end) { @@ -1070,80 +1510,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface if (!audioh_desc_valid(p_desc, desc_end, 3)) { goto open_failed; } - switch (tu_desc_subtype(p_desc)) { - case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { - if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_input_terminal_t))) { - goto open_failed; - } - const audio10_desc_input_terminal_t *terminal = (const audio10_desc_input_terminal_t *)p_desc; - if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING && usb_input_terminal_id == 0) { - usb_input_terminal_id = terminal->bTerminalID; - for (uint8_t i = 0; i < TU_ARRAY_SIZE(pending_fu); i++) { - if (pending_fu[i].source_id == usb_input_terminal_id) { - audioh_stream_set_feature_unit(&p_audio->out_stream, pending_fu[i].id, pending_fu[i].controls); - pending_fu[i] = (audioh_fu_info_t){0}; - break; - } - } - } - break; - } - case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { - if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_output_terminal_t))) { - goto open_failed; - } - const audio10_desc_output_terminal_t *terminal = (const audio10_desc_output_terminal_t *)p_desc; - if (tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING && usb_output_source_id == 0) { - usb_output_source_id = terminal->bSourceID; - for (uint8_t i = 0; i < TU_ARRAY_SIZE(pending_fu); i++) { - if (pending_fu[i].id == usb_output_source_id) { - audioh_stream_set_feature_unit(&p_audio->in_stream, pending_fu[i].id, pending_fu[i].controls); - pending_fu[i] = (audioh_fu_info_t){0}; - break; - } - } - } - break; - } - case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 7)) { - goto open_failed; - } - const uint8_t control_size = p_desc[5]; - if (!TUH_VALIDATE_BASIC(control_size > 0) || - !TUH_VALIDATE_BASIC(control_size <= (uint8_t)(tu_desc_len(p_desc) - 7))) { - goto open_failed; - } - const uint8_t controls = p_desc[6] & (AUDIO10_FU_CONTROL_BM_MUTE | AUDIO10_FU_CONTROL_BM_VOLUME); - if (controls == 0) { - break; - } - - bool mapped = false; - if (usb_input_terminal_id != 0 && p_audio->out_stream.feature_unit_id == 0 && - p_desc[4] == usb_input_terminal_id) { - audioh_stream_set_feature_unit(&p_audio->out_stream, p_desc[3], controls); - mapped = true; - } - if (usb_output_source_id != 0 && p_audio->in_stream.feature_unit_id == 0 && - p_desc[3] == usb_output_source_id) { - audioh_stream_set_feature_unit(&p_audio->in_stream, p_desc[3], controls); - mapped = true; - } - if (!mapped) { - for (uint8_t i = 0; i < TU_ARRAY_SIZE(pending_fu); i++) { - if (pending_fu[i].id == 0) { - pending_fu[i].id = p_desc[3]; - pending_fu[i].source_id = p_desc[4]; - pending_fu[i].controls = controls; - break; - } - } - } - break; - } - default: - break; + if (!audioh_parse_ac_entity(p_audio, &ac_map, p_desc)) { + goto open_failed; } } p_desc = tu_desc_next(p_desc); @@ -1164,31 +1532,34 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO || - desc_interface->bInterfaceSubClass != AUDIO_SUBCLASS_STREAMING) { + desc_interface->bInterfaceSubClass != AUDIO_SUBCLASS_STREAMING || + desc_interface->bInterfaceProtocol != p_audio->protocol) { break; } TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, desc_interface->bAlternateSetting); - p_desc = audioh_parse_as(p_audio, desc_interface, p_desc, desc_end); + p_desc = audioh_parse_as(p_audio, &ac_map, desc_interface, p_desc, desc_end); if (p_desc == NULL) { goto open_failed; } } - // Release the tentative instance when no supported stream configuration was - // collected, including MIDI-only and unsupported Audio functions. - if (p_audio->in_stream.config_count == 0 && p_audio->out_stream.config_count == 0) { + audioh_link_feature_units(p_audio, &ac_map); + + // UAC2 configurations receive their rates asynchronously during mount. + // Release the tentative instance when no supported AS alternate was found. + if (p_audio->in_stream.as_count == 0 && p_audio->out_stream.as_count == 0) { goto open_failed; } // Assign stream indices: playback first, then capture, so the application // can iterate [0, stream_count) without gaps uint8_t stream_idx = 0; - if (p_audio->out_stream.config_count > 0) { + if (p_audio->out_stream.as_count > 0) { p_audio->out_stream.stream_idx = stream_idx++; } - if (p_audio->in_stream.config_count > 0) { + if (p_audio->in_stream.as_count > 0) { p_audio->in_stream.stream_idx = stream_idx++; } p_audio->stream_count = stream_idx; @@ -1199,10 +1570,12 @@ open_failed: audioh_stream_reset(&p_audio->in_stream); audioh_stream_reset(&p_audio->out_stream); audioh_playback_reset(&p_audio->playback); - p_audio->daddr = 0; - p_audio->ac_itf_num = 0; - p_audio->stream_count = 0; - p_audio->mounted = false; + p_audio->daddr = 0; + p_audio->ac_itf_num = 0; + p_audio->protocol = 0; + p_audio->stream_count = 0; + p_audio->rate_source_count = 0; + p_audio->mounted = false; return 0; } @@ -1211,6 +1584,174 @@ open_failed: //--------------------------------------------------------------------+ static void audioh_mount_feature_unit_next(uint8_t idx); + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 +static void audioh_mount_clock_complete(tuh_xfer_t *xfer); + +static void audioh_uac2_configs_rebuild(audioh_interface_t *p_audio) { + p_audio->in_stream.config_count = 0; + p_audio->out_stream.config_count = 0; + + for (uint8_t direction = TUSB_DIR_OUT; direction <= TUSB_DIR_IN; direction++) { + tuh_audio_stream_t *stream = audioh_get_stream(p_audio, (tusb_dir_t)direction); + TU_ASSERT(stream != NULL, ); + for (uint8_t as_idx = 0; as_idx < stream->as_count; as_idx++) { + audioh_as_config_t *as = &stream->as[as_idx]; + audioh_rate_source_t *rate_source = audioh_as_rate_source(stream, as); + as->rate_count = 0; + for (uint8_t rate_idx = 0; rate_idx < rate_source->sample_rate_count; rate_idx++) { + if (audioh_as_rate_fits(p_audio, stream, as, rate_source->sample_rate[rate_idx])) { + as->rate_count++; + } + } + stream->config_count += as->rate_count; + } + } + + uint8_t stream_idx = 0; + p_audio->out_stream.stream_idx = TUSB_INDEX_INVALID_8; + p_audio->in_stream.stream_idx = TUSB_INDEX_INVALID_8; + if (p_audio->out_stream.config_count > 0) { + p_audio->out_stream.stream_idx = stream_idx++; + } + if (p_audio->in_stream.config_count > 0) { + p_audio->in_stream.stream_idx = stream_idx++; + } + p_audio->stream_count = stream_idx; +} + +static bool audioh_uac2_clock_range_store(audioh_rate_source_t *rate_source, const uint8_t *buffer, uint16_t length) { + TU_VERIFY(length >= 2, false); + const uint16_t subrange_count = tu_le16toh(tu_unaligned_read16(buffer)); + const uint16_t available = (uint16_t)((length - 2u) / 12u); + TU_VERIFY(subrange_count > 0 && available > 0, false); + + rate_source->sample_rate_count = 0; + const uint16_t parsed_count = TU_MIN(subrange_count, available); + for (uint16_t i = 0; i < parsed_count && rate_source->sample_rate_count < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { + const uint8_t *subrange = &buffer[2u + 12u * i]; + const uint32_t min = tu_le32toh(tu_unaligned_read32(&subrange[0])); + const uint32_t max = tu_le32toh(tu_unaligned_read32(&subrange[4])); + const uint32_t res = tu_le32toh(tu_unaligned_read32(&subrange[8])); + if (min == 0 || min > max || (min != max && res == 0)) { + continue; + } + if (min == max) { + rate_source->sample_rate[rate_source->sample_rate_count++] = min; + continue; + } + for (uint32_t rate = min; rate <= max && rate_source->sample_rate_count < CFG_TUH_AUDIO_MAX_SAM_FREQ;) { + rate_source->sample_rate[rate_source->sample_rate_count++] = rate; + if (max - rate < res) { + break; + } + rate += res; + } + } + return rate_source->sample_rate_count > 0; +} + +static bool audioh_mount_clock_submit(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + audioh_rate_source_t *rate_source = &p_audio->rate_source[epbuf->clock.rate_source_idx]; + const uint16_t length = epbuf->clock.read_cur ? 4u : (uint16_t)sizeof(epbuf->clock_range); + + const tusb_control_request_t request = { + .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, + .bRequest = epbuf->clock.read_cur ? AUDIO20_CS_REQ_CUR : AUDIO20_CS_REQ_RANGE, + .wValue = tu_htole16(tu_u16(AUDIO20_CS_CTRL_SAM_FREQ, 0)), + .wIndex = tu_htole16(tu_u16(rate_source->control_id, p_audio->ac_itf_num)), + .wLength = tu_htole16(length), + }; + tuh_xfer_t xfer = {.daddr = p_audio->daddr, + .ep_addr = 0, + .setup = &request, + .buffer = epbuf->clock_range, + .complete_cb = audioh_mount_clock_complete, + .user_data = (uintptr_t)idx}; + return tuh_control_xfer(&xfer); +} + +static void audioh_mount_clock_finish(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + epbuf->fu_busy = false; + audioh_uac2_configs_rebuild(p_audio); + + if (p_audio->stream_count == 0) { + const uint8_t daddr = p_audio->daddr; + const uint8_t itf_num = p_audio->ac_itf_num; + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + audioh_playback_reset(&p_audio->playback); + p_audio->daddr = 0; + p_audio->ac_itf_num = 0; + p_audio->protocol = 0; + p_audio->rate_source_count = 0; + usbh_driver_set_config_complete(daddr, itf_num); + return; + } + + epbuf->fu.mount.stream_idx = 0; + audioh_mount_feature_unit_next(idx); +} + +static void audioh_mount_clock_next(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + + while (epbuf->clock.rate_source_idx < p_audio->rate_source_count) { + epbuf->clock.read_cur = false; + epbuf->fu_busy = true; + if (audioh_mount_clock_submit(idx)) { + return; + } + p_audio->rate_source[epbuf->clock.rate_source_idx].sample_rate_count = 0; + epbuf->clock.rate_source_idx++; + } + audioh_mount_clock_finish(idx); +} + +static void audioh_mount_clock_complete(tuh_xfer_t *xfer) { + const uint8_t idx = (uint8_t)xfer->user_data; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + if (!epbuf->fu_busy) { + return; + } + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_rate_source_t *rate_source = &p_audio->rate_source[epbuf->clock.rate_source_idx]; + + bool success = xfer->result == XFER_RESULT_SUCCESS; + if (success && epbuf->clock.read_cur) { + success = xfer->actual_len == 4; + if (success) { + const uint32_t current = tu_le32toh(tu_unaligned_read32(epbuf->clock_range)); + success = current > 0; + if (success) { + rate_source->sample_rate[0] = current; + rate_source->sample_rate_count = 1; + } + } + } else if (success) { + success = audioh_uac2_clock_range_store(rate_source, epbuf->clock_range, (uint16_t)xfer->actual_len); + if (success && rate_source->frequency_access == AUDIOH_CTRL_READ) { + epbuf->clock.read_cur = true; + if (audioh_mount_clock_submit(idx)) { + return; + } + success = false; + } + } + + if (!success) { + rate_source->sample_rate_count = 0; + } + epbuf->fu_busy = false; + epbuf->clock.rate_source_idx++; + audioh_mount_clock_next(idx); +} + #endif + bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { uint8_t idx = TUSB_INDEX_INVALID_8; for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { @@ -1227,9 +1768,17 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { return true; } - audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; - epbuf->fu.mount.stream_idx = 0; - audioh_mount_feature_unit_next(idx); + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + if (_audioh_itf[idx].protocol == AUDIO_INT_PROTOCOL_CODE_V2) { + epbuf->clock.rate_source_idx = 0; + audioh_mount_clock_next(idx); + } else + #endif + { + epbuf->fu.mount.stream_idx = 0; + audioh_mount_feature_unit_next(idx); + } return true; } @@ -1261,7 +1810,7 @@ bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx) { audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); - return s != NULL && s->mute_supported; + return s != NULL && s->mute_access != AUDIOH_CTRL_NONE; } bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volume_range_t *range) { @@ -1269,7 +1818,7 @@ bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volum audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); - TU_VERIFY(s != NULL && s->volume_supported, false); + TU_VERIFY(s != NULL && s->volume_access != AUDIOH_CTRL_NONE, false); *range = s->volume_range; return true; } @@ -1417,6 +1966,13 @@ static void audioh_stream_start_xfer(tuh_audio_stream_t *s) { } } +static void audioh_stream_start_complete(tuh_xfer_t *xfer); + +static bool audioh_stream_activate(tuh_audio_stream_t *s) { + const audioh_as_config_t *as = audioh_stream_active_as(s); + return tuh_interface_set(s->daddr, as->itf_num, as->alt_setting, audioh_stream_start_complete, (uintptr_t)s); +} + static void audioh_stream_start_set_freq_complete(tuh_xfer_t *xfer) { tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; if (s->daddr != xfer->daddr || s->state != STREAM_STATE_READY || !s->running) { @@ -1427,17 +1983,31 @@ static void audioh_stream_start_set_freq_complete(tuh_xfer_t *xfer) { audioh_stream_error(s, 0); return; } - audioh_stream_start_xfer(s); + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + if (_audioh_itf[s->idx].protocol == AUDIO_INT_PROTOCOL_CODE_V2) { + if (!audioh_stream_activate(s)) { + audioh_stream_error(s, 0); + } + } else + #endif + { + audioh_stream_start_xfer(s); + } } static bool audioh_stream_start_active(tuh_audio_stream_t *s) { - const audioh_as_config_t *as = audioh_stream_active_as(s); - if (as->sam_freq_ctrl) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + if (_audioh_itf[s->idx].protocol == AUDIO_INT_PROTOCOL_CODE_V1 && + rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) { if (!audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete)) { audioh_stream_error(s, 0); return false; } - } else { + } else + #endif + { audioh_stream_start_xfer(s); } return true; @@ -1477,9 +2047,19 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { p_audio->playback.rem_acc = 0; } s->running = true; - // Activate the interface's alternate setting asynchronously: transfers - // begin once SET_INTERFACE and sampling-frequency control complete. - if (!tuh_interface_set(s->daddr, as->itf_num, as->alt_setting, audioh_stream_start_complete, (uintptr_t)s)) { + // UAC2 changes the Clock Source before selecting the alternate setting; + // UAC1 selects the alternate first because its control targets the endpoint. + bool submitted = false; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2 && rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) { + submitted = audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete); + } else + #endif + { + submitted = audioh_stream_activate(s); + } + if (!submitted) { s->running = false; return false; } @@ -1609,6 +2189,36 @@ enum { AUDIOH_FU_VALUE_I16 }; +static uint8_t audioh_fu_cur_request(uint8_t protocol, tusb_dir_t direction) { + #if !(CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1) + (void)direction; + #endif + switch (protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + return (direction == TUSB_DIR_IN) ? AUDIO10_CS_REQ_GET_CUR : AUDIO10_CS_REQ_SET_CUR; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + return AUDIO20_CS_REQ_CUR; + #endif + default: + return 0; + } +} + +static bool audioh_fu_selector_supported(uint8_t protocol, uint8_t control_selector) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + if (protocol == AUDIO_INT_PROTOCOL_CODE_V2) { + return control_selector == AUDIO20_FU_CTRL_MUTE || control_selector == AUDIO20_FU_CTRL_VOLUME; + } + #else + (void)protocol; + (void)control_selector; + #endif + return true; +} + static void audioh_fu_value_store(audioh_epbuf_t *epbuf) { if (epbuf->fu.control.value_type == AUDIOH_FU_VALUE_BOOL) { *((bool *)epbuf->value) = epbuf->fu_ctrl[0] != 0; @@ -1692,12 +2302,13 @@ static bool audioh_mount_feature_unit_submit(uint8_t idx) { tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->fu.mount.stream_idx); TU_ASSERT(s != NULL); + const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; const tusb_control_request_t request = { .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, - .bRequest = audioh_fu_volume_range_request(epbuf->fu.mount.range_step), + .bRequest = uac2 ? AUDIO20_CS_REQ_RANGE : audioh_fu_volume_range_request(epbuf->fu.mount.range_step), .wValue = tu_htole16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0)), .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), - .wLength = tu_htole16(2), + .wLength = tu_htole16(uac2 ? 8u : 2u), }; tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, @@ -1715,15 +2326,15 @@ static void audioh_mount_feature_unit_next(uint8_t idx) { while (epbuf->fu.mount.stream_idx < p_audio->stream_count) { tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->fu.mount.stream_idx); TU_ASSERT(s != NULL, ); - if (s->volume_supported) { + if (s->volume_access != AUDIOH_CTRL_NONE) { s->volume_range = (tuh_audio_volume_range_t){0}; epbuf->fu.mount.range_step = AUDIOH_VOLUME_RANGE_MIN; epbuf->fu_busy = true; if (audioh_mount_feature_unit_submit(idx)) { return; } - s->volume_supported = false; - if (!s->mute_supported) { + s->volume_access = AUDIOH_CTRL_NONE; + if (s->mute_access == AUDIOH_CTRL_NONE) { s->feature_unit_id = 0; } epbuf->fu_busy = false; @@ -1747,7 +2358,16 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->fu.mount.stream_idx); TU_ASSERT(s != NULL, ); - if (xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 2) { + const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; + if (uac2 && xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 8 && + tu_le16toh(tu_unaligned_read16(epbuf->fu_ctrl)) == 1) { + s->volume_range.min = (int16_t)tu_le16toh(tu_unaligned_read16(&epbuf->fu_ctrl[2])); + s->volume_range.max = (int16_t)tu_le16toh(tu_unaligned_read16(&epbuf->fu_ctrl[4])); + s->volume_range.res = tu_le16toh(tu_unaligned_read16(&epbuf->fu_ctrl[6])); + if (s->volume_range.min > s->volume_range.max || s->volume_range.res == 0) { + xfer->result = XFER_RESULT_FAILED; + } + } else if (!uac2 && xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 2) { audioh_fu_volume_range_store(s, epbuf); epbuf->fu.mount.range_step++; @@ -1761,10 +2381,11 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { } } - if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != 2) { - s->volume_supported = false; - s->volume_range = (tuh_audio_volume_range_t){0}; - if (!s->mute_supported) { + const uint32_t expected_len = uac2 ? 8u : 2u; + if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != expected_len) { + s->volume_access = AUDIOH_CTRL_NONE; + s->volume_range = (tuh_audio_volume_range_t){0}; + if (s->mute_access == AUDIOH_CTRL_NONE) { s->feature_unit_id = 0; } } @@ -1780,10 +2401,18 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s && s->feature_unit_id != 0, false); + TU_VERIFY(audioh_fu_selector_supported(p_audio->protocol, control_selector), false); const uint8_t width = audioh_fu_control_width(control_selector); TU_VERIFY(width != 0, false); + if (control_selector == AUDIO10_FU_CTRL_MUTE) { + TU_VERIFY(s->mute_access == AUDIOH_CTRL_READ_WRITE, false); + } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) { + TU_VERIFY(s->volume_access == AUDIOH_CTRL_READ_WRITE, false); + } + const uint8_t request_code = audioh_fu_cur_request(p_audio->protocol, TUSB_DIR_OUT); + TU_VERIFY(request_code != 0, false); audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; TU_VERIFY(!epbuf->fu_busy, false); epbuf->fu_busy = true; // reserve the request state and fu_ctrl before writing @@ -1791,7 +2420,7 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, - .bRequest = AUDIO10_CS_REQ_SET_CUR, + .bRequest = request_code, .wValue = tu_htole16(tu_u16(control_selector, channel)), .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = width}; @@ -1835,10 +2464,18 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con TU_VERIFY(p_audio->mounted && value, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s && s->feature_unit_id != 0, false); + TU_VERIFY(audioh_fu_selector_supported(p_audio->protocol, control_selector), false); const uint8_t width = audioh_fu_control_width(control_selector); TU_VERIFY(width != 0, false); + if (control_selector == AUDIO10_FU_CTRL_MUTE) { + TU_VERIFY(s->mute_access != AUDIOH_CTRL_NONE, false); + } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) { + TU_VERIFY(s->volume_access != AUDIOH_CTRL_NONE, false); + } + const uint8_t request_code = audioh_fu_cur_request(p_audio->protocol, TUSB_DIR_IN); + TU_VERIFY(request_code != 0, false); audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; TU_VERIFY(!epbuf->fu_busy, false); epbuf->fu_busy = true; @@ -1849,7 +2486,7 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, - .bRequest = AUDIO10_CS_REQ_GET_CUR, + .bRequest = request_code, .wValue = tu_htole16(tu_u16(control_selector, channel)), .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = width}; diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index f8b6a80c8..42cf4b4e5 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-FileCopyrightText: Copyright (c) 2026 HiFiPhile (Zixun LI) * SPDX-License-Identifier: MIT * * This file is part of the TinyUSB stack. @@ -17,6 +18,24 @@ extern "C" { //--------------------------------------------------------------------+ // Class Driver Configuration //--------------------------------------------------------------------+ + +// Audio Class protocol versions compiled into the host driver. Multiple +// versions can be enabled so UAC1 and UAC2 devices can be mounted together. +#define TUH_AUDIO_PROTOCOL_UAC1 TU_BIT(0) +#define TUH_AUDIO_PROTOCOL_UAC2 TU_BIT(1) + +#ifndef CFG_TUH_AUDIO_PROTOCOLS + #define CFG_TUH_AUDIO_PROTOCOLS TUH_AUDIO_PROTOCOL_UAC1 +#endif + +#if !(CFG_TUH_AUDIO_PROTOCOLS & (TUH_AUDIO_PROTOCOL_UAC1 | TUH_AUDIO_PROTOCOL_UAC2)) + #error CFG_TUH_AUDIO_PROTOCOLS must enable UAC1 and/or UAC2 +#endif + +#if CFG_TUH_AUDIO_PROTOCOLS & ~(TUH_AUDIO_PROTOCOL_UAC1 | TUH_AUDIO_PROTOCOL_UAC2) + #error CFG_TUH_AUDIO_PROTOCOLS contains an unsupported protocol bit +#endif + // Maximum number of Audio devices #ifndef CFG_TUH_AUDIO_MAX #define CFG_TUH_AUDIO_MAX 1 @@ -62,8 +81,8 @@ typedef enum { TUH_AUDIO_STREAM_DIRECTION_COUNT } tuh_audio_direction_t; -// Discrete sample format. Only UAC1 configurations with bSamFreqType > 0 are -// supported; sampling-frequency ranges are ignored by the driver. +// Discrete Type-I PCM sample format. UAC1 requires bSamFreqType > 0; UAC2 +// configurations are built from the directly connected Clock Source RANGE. typedef enum { TUH_AUDIO_FORMAT_S8 = 0, // signed 8-bit TUH_AUDIO_FORMAT_S16_LE, // signed 16-bit little-endian @@ -139,8 +158,9 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // Stream Control / Frame-based Data //--------------------------------------------------------------------+ -// Start transferring data by activating the alternate setting selected by -// configure, then set the endpoint sampling frequency when supported. +// Start transferring data with the configuration selected by configure(). +// UAC1 activates the alternate setting before setting an endpoint frequency; +// UAC2 sets a writable Clock Source before activating the alternate setting. bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx); // Stop transferring and deactivate the Audio Streaming interface (alt 0). bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx); @@ -203,13 +223,17 @@ bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volum // Control Request API //--------------------------------------------------------------------+ -// Set a Feature Unit control (mute, volume, ...) associated with an Audio stream (UAC 1.0) +// Set a Feature Unit control associated with an Audio stream. UAC2 supports +// mute and volume through this low-level API; the other fixed-width selectors +// below are UAC1-only. // Mute/bass/mid/treble/AGC/bass boost/loudness use one byte; volume/delay use two. // Graphic EQ and unknown selectors are unsupported. bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); -// Get a Feature Unit control (mute, volume, ...) associated with an Audio stream (UAC 1.0) +// Get a Feature Unit control associated with an Audio stream. UAC2 supports +// mute and volume through this low-level API; the other fixed-width selectors +// below are UAC1-only. // The value is converted to host byte order before complete_cb is invoked. // Graphic EQ and unknown selectors are unsupported. // Only one Feature Unit operation may be in flight per device. diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index cea9fe33d..06cff5b09 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -137,6 +137,7 @@ add_ceedling_test( target_compile_definitions(test_audio_host PRIVATE CFG_TUH_ENABLED=1 CFG_TUH_AUDIO=1 + CFG_TUH_AUDIO_PROTOCOLS=3 CFG_TUSB_DEBUG=0 CFG_TUH_AUDIO_EPIN_BUFSIZE=1024 CFG_TUH_AUDIO_EPOUT_BUFSIZE=1024 diff --git a/test/unit-test/project.yml b/test/unit-test/project.yml index 9dba70d20..599dabc21 100644 --- a/test/unit-test/project.yml +++ b/test/unit-test/project.yml @@ -135,6 +135,7 @@ :test_audio_host: - CFG_TUH_ENABLED=1 - CFG_TUH_AUDIO=1 + - CFG_TUH_AUDIO_PROTOCOLS=3 - CFG_TUSB_DEBUG=0 - CFG_TUH_AUDIO_EPIN_BUFSIZE=1024 - CFG_TUH_AUDIO_EPOUT_BUFSIZE=1024 diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index 0d7569090..5e312e457 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -22,6 +22,8 @@ enum { CAPTURE_INPUT_TERM = 11, CAPTURE_FU = 12, CAPTURE_OUTPUT_TERM = 13, + + PLAYBACK_CLOCK = 10, }; static bool interface_set_result; @@ -32,7 +34,7 @@ static uint8_t interface_set_count; static bool control_xfer_result; static tuh_xfer_t control_xfer; static tusb_control_request_t control_request; -static uint8_t control_buffer[3]; +static uint8_t control_buffer[8]; static uint8_t control_xfer_count; static bool edpt_open_result; @@ -247,6 +249,47 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { #define TEST_UAC1_CS_DATA_EP TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_SAMPLING_FRQ) +#define TEST_UAC2_AC_HEADER(_total_len) \ + 9, TUSB_DESC_INTERFACE, AUDIO_AC_ITF, 0, 0, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_CONTROL, AUDIO_INT_PROTOCOL_CODE_V2, 0, \ + 9, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_HEADER, U16_TO_U8S_LE(0x0200), 0, U16_TO_U8S_LE(_total_len), 0 + +#define TEST_UAC2_CLOCK_SOURCE(_id, _access) \ + 8, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_CLOCK_SOURCE, _id, AUDIO20_CLOCK_SOURCE_ATT_INT_PRO_CLK, _access, \ + 0, 0 + +#define TEST_UAC2_INPUT_TERM(_id, _type, _clock_id, _channels) \ + 17, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_INPUT_TERMINAL, _id, U16_TO_U8S_LE(_type), 0, _clock_id, \ + _channels, U32_TO_U8S_LE(AUDIO20_CHANNEL_CONFIG_NON_PREDEFINED), 0, U16_TO_U8S_LE(0), 0 + +#define TEST_UAC2_OUTPUT_TERM(_id, _type, _source_id, _clock_id) \ + 12, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_OUTPUT_TERMINAL, _id, U16_TO_U8S_LE(_type), 0, _source_id, \ + _clock_id, U16_TO_U8S_LE(0), 0 + + +#define TEST_UAC2_FEATURE_UNIT_STEREO(_id, _source_id, _master_controls) \ + 18, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, U32_TO_U8S_LE(_master_controls), \ + U32_TO_U8S_LE(0), U32_TO_U8S_LE(0), 0 + + +#define TEST_UAC2_AS_INTERFACE_NUM(_itf, _alt, _ep_count) \ + 9, TUSB_DESC_INTERFACE, _itf, _alt, _ep_count, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_STREAMING, \ + AUDIO_INT_PROTOCOL_CODE_V2, 0 + +#define TEST_UAC2_AS_INTERFACE(_alt, _ep_count) TEST_UAC2_AS_INTERFACE_NUM(AUDIO_AS_ITF, _alt, _ep_count) + +#define TEST_UAC2_AS_ALT0 TEST_UAC2_AS_INTERFACE(0, 0) + +#define TEST_UAC2_AS_GENERAL(_term_id, _channels) \ + 16, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AS_INTERFACE_AS_GENERAL, _term_id, 0, AUDIO20_FORMAT_TYPE_I, \ + U32_TO_U8S_LE(AUDIO20_DATA_FORMAT_TYPE_I_PCM), _channels, U32_TO_U8S_LE(AUDIO20_CHANNEL_CONFIG_NON_PREDEFINED), 0 + +#define TEST_UAC2_FORMAT(_bytes, _bits) \ + 6, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AS_INTERFACE_FORMAT_TYPE, AUDIO20_FORMAT_TYPE_I, _bytes, _bits + +#define TEST_UAC2_CS_DATA_EP \ + 8, TUSB_DESC_CS_ENDPOINT, AUDIO20_CS_EP_SUBTYPE_GENERAL, AUDIO20_CS_AS_ISO_DATA_EP_ATT_NON_MAX_PACKETS_OK, 0, \ + AUDIO20_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_UNDEFINED, U16_TO_U8S_LE(0) + static const uint8_t playback_with_explicit_feedback[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -278,6 +321,73 @@ static const uint8_t uac2_control_interface[] = { 9, TUSB_DESC_INTERFACE, AUDIO_AC_ITF, 0, 0, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_CONTROL, AUDIO_INT_PROTOCOL_CODE_V2, 0, }; +static const uint8_t uac2_playback[] = { + TEST_UAC2_AC_HEADER(64), + // Deliberately reorder AC entities to verify association is ID-based. + TEST_UAC2_FEATURE_UNIT_STEREO(PLAYBACK_FU, PLAYBACK_INPUT_TERM, + (AUDIO20_CTRL_RW << AUDIO20_FEATURE_UNIT_CTRL_MUTE_POS) | + (AUDIO20_CTRL_RW << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS)), + TEST_UAC2_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU, PLAYBACK_CLOCK), + TEST_UAC2_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, PLAYBACK_CLOCK, 2), + TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_RW << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), + TEST_UAC2_AS_ALT0, + TEST_UAC2_AS_INTERFACE(1, 1), + TEST_UAC2_AS_GENERAL(PLAYBACK_INPUT_TERM, 2), + TEST_UAC2_FORMAT(2, 16), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 192, 1), + TEST_UAC2_CS_DATA_EP, +}; + +static const uint8_t uac2_playback_read_only_feature_unit[] = { + TEST_UAC2_AC_HEADER(64), + TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_RW << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), + TEST_UAC2_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, PLAYBACK_CLOCK, 2), + TEST_UAC2_FEATURE_UNIT_STEREO(PLAYBACK_FU, PLAYBACK_INPUT_TERM, + (AUDIO20_CTRL_R << AUDIO20_FEATURE_UNIT_CTRL_MUTE_POS) | + (AUDIO20_CTRL_R << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS)), + TEST_UAC2_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU, PLAYBACK_CLOCK), + TEST_UAC2_AS_ALT0, + TEST_UAC2_AS_INTERFACE(1, 1), + TEST_UAC2_AS_GENERAL(PLAYBACK_INPUT_TERM, 2), + TEST_UAC2_FORMAT(2, 16), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 192, 1), + TEST_UAC2_CS_DATA_EP, +}; + +static const uint8_t uac2_playback_read_only_clock[] = { + TEST_UAC2_AC_HEADER(46), + TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_R << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), + TEST_UAC2_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, PLAYBACK_CLOCK, 2), + TEST_UAC2_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_INPUT_TERM, PLAYBACK_CLOCK), + TEST_UAC2_AS_ALT0, + TEST_UAC2_AS_INTERFACE(1, 1), + TEST_UAC2_AS_GENERAL(PLAYBACK_INPUT_TERM, 2), + TEST_UAC2_FORMAT(2, 16), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 192, 1), + TEST_UAC2_CS_DATA_EP, +}; + +static const uint8_t uac2_duplex_shared_clock[] = { + TEST_UAC2_AC_HEADER(75), + TEST_UAC2_OUTPUT_TERM(CAPTURE_OUTPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, CAPTURE_INPUT_TERM, PLAYBACK_CLOCK), + TEST_UAC2_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, PLAYBACK_CLOCK, 2), + TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_RW << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), + TEST_UAC2_INPUT_TERM(CAPTURE_INPUT_TERM, AUDIO_TERM_TYPE_IN_GENERIC_MIC, PLAYBACK_CLOCK, 1), + TEST_UAC2_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_INPUT_TERM, PLAYBACK_CLOCK), + TEST_UAC2_AS_ALT0, + TEST_UAC2_AS_INTERFACE(1, 1), + TEST_UAC2_AS_GENERAL(PLAYBACK_INPUT_TERM, 2), + TEST_UAC2_FORMAT(2, 16), + TEST_UAC2_CS_DATA_EP, + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 192, 1), + TEST_UAC2_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 0, 0), + TEST_UAC2_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 1, 1), + TEST_UAC2_AS_GENERAL(CAPTURE_OUTPUT_TERM, 1), + TEST_UAC2_FORMAT(2, 16), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 96, 1), + TEST_UAC2_CS_DATA_EP, +}; + static const uint8_t playback_with_implicit_feedback[] = { TEST_UAC1_AC_HEADER_2, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -445,7 +555,8 @@ static const uint8_t playback_with_cs_ep_before_data_ep[] = { static const uint8_t malformed_zero_length_ac_descriptor[] = { TEST_UAC1_AC_HEADER, - 0, TUSB_DESC_CS_INTERFACE, + 0, + TUSB_DESC_CS_INTERFACE, }; static const uint8_t malformed_sampling_frequency_list[] = { @@ -454,7 +565,14 @@ static const uint8_t malformed_sampling_frequency_list[] = { TEST_UAC1_AS_ALT0, TEST_UAC1_AS_INTERFACE(1, 1), TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), - 11, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE, AUDIO10_FORMAT_TYPE_I, 2, 2, 16, 2, + 11, + TUSB_DESC_CS_INTERFACE, + AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE, + AUDIO10_FORMAT_TYPE_I, + 2, + 2, + 16, + 2, U24_TO_U8S_LE(48000), }; @@ -465,7 +583,11 @@ static const uint8_t malformed_short_endpoint[] = { TEST_UAC1_AS_INTERFACE(1, 1), TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), TEST_UAC1_FORMAT(2, 2, 16, 48000), - 6, TUSB_DESC_ENDPOINT, 0x01, (TUSB_XFER_ISOCHRONOUS | TUSB_ISO_EP_ATT_ADAPTIVE), U16_TO_U8S_LE(192), + 6, + TUSB_DESC_ENDPOINT, + 0x01, + (TUSB_XFER_ISOCHRONOUS | TUSB_ISO_EP_ATT_ADAPTIVE), + U16_TO_U8S_LE(192), }; static const uint8_t playback_with_two_alternates[] = { @@ -561,6 +683,36 @@ static void complete_control_xfer_with_u16(uint16_t value) { complete_control_xfer(XFER_RESULT_SUCCESS); } +static void complete_control_xfer_with_u32(uint32_t value) { + TEST_ASSERT_NOT_NULL(control_xfer.buffer); + tu_unaligned_write32(control_xfer.buffer, tu_htole32(value)); + control_xfer.actual_len = 4; + complete_control_xfer(XFER_RESULT_SUCCESS); +} + +static void complete_uac2_clock_range(uint32_t rate0, uint32_t rate1) { + TEST_ASSERT_NOT_NULL(control_xfer.buffer); + tu_unaligned_write16(control_xfer.buffer, tu_htole16(2)); + tu_unaligned_write32(&control_xfer.buffer[2], tu_htole32(rate0)); + tu_unaligned_write32(&control_xfer.buffer[6], tu_htole32(rate0)); + tu_unaligned_write32(&control_xfer.buffer[10], 0); + tu_unaligned_write32(&control_xfer.buffer[14], tu_htole32(rate1)); + tu_unaligned_write32(&control_xfer.buffer[18], tu_htole32(rate1)); + tu_unaligned_write32(&control_xfer.buffer[22], 0); + control_xfer.actual_len = 26; + complete_control_xfer(XFER_RESULT_SUCCESS); +} + +static void complete_uac2_volume_range(int16_t min, int16_t max, uint16_t res) { + TEST_ASSERT_NOT_NULL(control_xfer.buffer); + tu_unaligned_write16(control_xfer.buffer, tu_htole16(1)); + tu_unaligned_write16(&control_xfer.buffer[2], tu_htole16((uint16_t)min)); + tu_unaligned_write16(&control_xfer.buffer[4], tu_htole16((uint16_t)max)); + tu_unaligned_write16(&control_xfer.buffer[6], tu_htole16(res)); + control_xfer.actual_len = 8; + complete_control_xfer(XFER_RESULT_SUCCESS); +} + static uint16_t edpt_xfer_bytes_at(uint8_t ep_addr, uint8_t ep_xfer_idx) { uint8_t found = 0; for (uint8_t i = 0; i < edpt_xfer_count; i++) { @@ -702,6 +854,135 @@ void test_audio_host_rejects_uac2_interface_without_consuming_instance(void) { TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); } +void test_audio_host_mounts_uac2_and_sets_clock_before_activating_stream(void) { + open_descriptors(uac2_playback, sizeof(uac2_playback)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + + TEST_ASSERT_FALSE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_RANGE, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_CS_CTRL_SAM_FREQ, 0), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_CLOCK, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_FALSE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_RANGE, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + complete_uac2_volume_range((int16_t)(-90 * 256), (int16_t)(6 * 256), 256); + + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); + TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 0)); + TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); + TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0)); + tuh_audio_volume_range_t range; + TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range)); + TEST_ASSERT_EQUAL_INT16(-90 * 256, range.min); + TEST_ASSERT_EQUAL_INT16(6 * 256, range.max); + TEST_ASSERT_EQUAL_UINT16(256, range.res); + + control_xfer_count = 0; + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, (int16_t)(-6 * 256), feature_unit_complete, 42)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_UINT16((uint16_t)(-6 * 256), tu_le16toh(tu_unaligned_read16(control_buffer))); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count); + TEST_ASSERT_EQUAL_UINT32(42, fu_cb_user_data); + + tuh_audio_stream_config_t config; + TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 1, &config)); + TEST_ASSERT_EQUAL_UINT32(48000, config.sample_rate); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 1)); + + control_xfer_count = 0; + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(0, interface_set_count); + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest); + TEST_ASSERT_EQUAL_UINT16(4, tu_le16toh(control_request.wLength)); + TEST_ASSERT_EQUAL_UINT32(48000, tu_le32toh(tu_unaligned_read32(control_buffer))); + + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, interface_set_count); + TEST_ASSERT_EQUAL_UINT8(1, interface_alt); + TEST_ASSERT_EQUAL_UINT8(0, edpt_xfer_count); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + TEST_ASSERT_EQUAL_UINT16(192, edpt_xfer_bytes[0]); +} + +void test_audio_host_uac2_read_only_clock_exposes_cur_rate_without_setting_it(void) { + open_descriptors(uac2_playback_read_only_clock, sizeof(uac2_playback_read_only_clock)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest); + TEST_ASSERT_EQUAL_UINT16(4, tu_le16toh(control_request.wLength)); + complete_control_xfer_with_u32(48000); + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_config_count(0, 0)); + + tuh_audio_stream_config_t config; + TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 0, &config)); + TEST_ASSERT_EQUAL_UINT32(48000, config.sample_rate); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + + control_xfer_count = 0; + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT8(0, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(1, interface_set_count); + TEST_ASSERT_EQUAL_UINT8(1, interface_alt); +} + +void test_audio_host_uac2_read_only_feature_controls_reject_set_and_allow_get(void) { + open_descriptors(uac2_playback_read_only_feature_unit, sizeof(uac2_playback_read_only_feature_unit)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + complete_uac2_volume_range((int16_t)(-90 * 256), (int16_t)(6 * 256), 256); + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + + TEST_ASSERT_FALSE(tuh_audio_mute_set(0, 0, false, feature_unit_complete, 0)); + TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, feature_unit_complete, 0)); + + int16_t volume = 0; + TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, &volume, feature_unit_complete, 7)); + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest); + TEST_ASSERT_EQUAL(TUSB_DIR_IN, control_request.bmRequestType_bit.direction); + complete_control_xfer_with_u16((uint16_t)(-12 * 256)); + TEST_ASSERT_EQUAL_INT16(-12 * 256, volume); + TEST_ASSERT_EQUAL_UINT32(7, fu_cb_user_data); +} + +void test_audio_host_uac2_shared_clock_is_discovered_once_for_both_streams(void) { + open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_stream_count(0)); + TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_PLAYBACK, tuh_audio_stream_direction(0, 0)); + TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_CAPTURE, tuh_audio_stream_direction(0, 1)); + TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 0)); + TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 1)); +} + +void test_audio_host_rejects_malformed_uac2_clock_range_and_releases_instance(void) { + open_descriptors(uac2_playback, sizeof(uac2_playback)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + tu_unaligned_write16(control_xfer.buffer, tu_htole16(1)); + control_xfer.actual_len = 2; + complete_control_xfer(XFER_RESULT_SUCCESS); + + TEST_ASSERT_FALSE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); +} + void test_audio_host_treats_implicit_feedback_as_audio_in_endpoint(void) { open_descriptors(playback_with_implicit_feedback, sizeof(playback_with_implicit_feedback)); @@ -925,8 +1206,9 @@ void test_audio_host_keeps_supported_stream_when_other_as_format_is_unsupported( } void test_audio_host_rejects_sampling_frequency_range_and_releases_instance(void) { - TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)playback_with_frequency_range, - sizeof(playback_with_frequency_range))); + TEST_ASSERT_EQUAL_UINT16(0, + audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)playback_with_frequency_range, + sizeof(playback_with_frequency_range))); TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); open_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); @@ -951,8 +1233,8 @@ void test_audio_host_rejects_malformed_descriptors_and_releases_instance(void) { }; for (uint8_t i = 0; i < TU_ARRAY_SIZE(malformed); i++) { - TEST_ASSERT_EQUAL_UINT16( - 0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)malformed[i].desc, malformed[i].len)); + TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)malformed[i].desc, + malformed[i].len)); TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); } } @@ -1154,14 +1436,12 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only while (edpt_xfer_count < 5) { complete_edpt(0x01); - TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, - edpt_xfer_bytes[edpt_xfer_count - 1])); + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1])); } TEST_ASSERT_EQUAL_UINT32(185, tuh_audio_write(0, 0, samples, 185)); while (edpt_xfer_count < 10) { complete_edpt(0x01); - TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, - edpt_xfer_bytes[edpt_xfer_count - 1])); + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1])); } for (uint8_t i = 0; i < 9; i++) { -- cgit v1.3.1 From b23e33a1ce07297df2f37219861d78d2a4cc9809 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 06:21:18 +0200 Subject: audio: expose generic Audio Control requests Remove the narrow public Feature Unit request API while retaining managed mute and volume helpers. Expose validated Audio Control descriptors during enumeration and provide raw asynchronous and synchronous entity requests for advanced controls. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 15 +- examples/host/audio_host/src/audio_app.c | 24 +-- src/class/audio/audio_host.c | 205 ++++++++++------------- src/class/audio/audio_host.h | 61 +++---- test/unit-test/test/host/audio/test_audio_host.c | 197 ++++++++++++++++------ 5 files changed, 282 insertions(+), 220 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 326b469e8..e7140bf67 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -68,7 +68,7 @@ make BOARD= flash 2. Connect a USB Audio device (UAC 1.0 or 2.0) to the USB host port 3. Open a serial terminal to view output 4. The example will: - - Print each stream's Feature Unit ID, master mute/volume capabilities, cached volume range, and supported configurations when mounted + - Print each stream's master mute/volume capabilities, cached volume range, and supported configurations when mounted - Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 kHz fallback; stereo preferred, mono accepted) and configure it - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB @@ -81,25 +81,25 @@ make BOARD= flash TinyUSB Host USB Audio Example Connect a USB Audio Device (UAC 1.0 or 2.0) to test Audio device mounted: idx=0 addr=1 - capture stream 1 Feature Unit ID: 5, configurations: 2 + capture stream 1, configurations: 2 master mute supported master volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 - playback stream 0 Feature Unit ID: 2, configurations: 2 + playback stream 0, configurations: 2 master mute supported master volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 Configuring 48 kHz S16_LE capture (2 channels) Microphone configured - Microphone Feature Unit 5 master mute: off - Microphone Feature Unit 5 master volume: 0 (1/256 dB) + Microphone master mute: off + Microphone master volume: 0 (1/256 dB) Microphone master volume set: -1536 (1/256 dB) Configuring 48 kHz S16_LE playback (2 channels) Speaker configured - Speaker Feature Unit 2 master mute: off - Speaker Feature Unit 2 master volume: 0 (1/256 dB) + Speaker master mute: off + Speaker master volume: 0 (1/256 dB) Speaker master volume set: -1536 (1/256 dB) ``` @@ -115,6 +115,7 @@ Edit `src/tusb_config.h` to modify: ## Notes +- `tuh_audio_descriptor_cb()` exposes the validated Audio Control descriptor block during enumeration. Applications that need raw entity controls must copy the required entity IDs or descriptor fields before the callback returns, then use `tuh_audio_control_xfer()` after the device mounts. - While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers follow the endpoint's `bInterval`. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` report each completed transfer; `tuh_audio_err_cb()` reports failures. The example restarts the failed stream automatically 100 ms after the error callback. - Capture and playback streams running concurrently in the same Audio Control instance must use the same sample rate. - `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually queued/read (0 when the FIFO is empty/full or the stream is not running), and `tuh_audio_read_available()` / `tuh_audio_write_available()` report the FIFO occupancy in frames. `tuh_audio_write()` only queues data; the playback transfer-completion chain sends it, or sends silence when the FIFO does not contain a complete polling interval without consuming the partial data. diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 4623dff5a..8852ca58c 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -360,11 +360,9 @@ void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { // Print all supported stream configurations static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { - const tuh_audio_direction_t dir = tuh_audio_stream_direction(idx, stream_idx); - const char *dir_name = (dir == TUH_AUDIO_STREAM_CAPTURE) ? "capture" : "playback"; - const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx); - printf(" %s stream %u Feature Unit ID: %u, configurations: %u\r\n", dir_name, stream_idx, feature_unit_id, - tuh_audio_config_count(idx, stream_idx)); + const tuh_audio_direction_t dir = tuh_audio_stream_direction(idx, stream_idx); + const char *dir_name = (dir == TUH_AUDIO_STREAM_CAPTURE) ? "capture" : "playback"; + printf(" %s stream %u, configurations: %u\r\n", dir_name, stream_idx, tuh_audio_config_count(idx, stream_idx)); tuh_audio_volume_range_t range; if (tuh_audio_mute_supported(idx, stream_idx)) { printf(" master mute supported\r\n"); @@ -383,17 +381,14 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { } static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const char *stream_name) { - const uint8_t feature_unit_id = tuh_audio_get_feature_unit_id(idx, stream_idx); - if (feature_unit_id == 0) { - printf(" %s stream has no master mute/volume Feature Unit\r\n", stream_name); - return; - } + bool has_control = false; if (tuh_audio_mute_supported(idx, stream_idx)) { + has_control = true; bool mute; tusb_xfer_result_t result = tuh_audio_mute_get_sync(idx, stream_idx, &mute); if (result == XFER_RESULT_SUCCESS) { - printf(" %s Feature Unit %u master mute: %s\r\n", stream_name, feature_unit_id, mute ? "on" : "off"); + printf(" %s master mute: %s\r\n", stream_name, mute ? "on" : "off"); result = tuh_audio_mute_set_sync(idx, stream_idx, false); } if (result != XFER_RESULT_SUCCESS) { @@ -403,10 +398,11 @@ static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const cha tuh_audio_volume_range_t range; if (tuh_audio_volume_range_get(idx, stream_idx, &range)) { + has_control = true; int16_t volume; tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, &volume); if (result == XFER_RESULT_SUCCESS) { - printf(" %s Feature Unit %u master volume: %d (1/256 dB)\r\n", stream_name, feature_unit_id, volume); + printf(" %s master volume: %d (1/256 dB)\r\n", stream_name, volume); int32_t target = FEATURE_UNIT_VOLUME_DB; target = TU_MAX(target, range.min); target = TU_MIN(target, range.max); @@ -421,6 +417,10 @@ static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const cha printf(" Accessing %s master volume failed: result=%u\r\n", stream_name, result); } } + + if (!has_control) { + printf(" %s stream has no master mute/volume control\r\n", stream_name); + } } // Invoked when device with Audio interface is un-mounted diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 370958aaf..432109022 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -229,6 +229,11 @@ CFG_TUH_MEM_SECTION static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; // WEAK APPLICATION CALLBACKS //--------------------------------------------------------------------+ +TU_ATTR_WEAK void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data) { + (void)idx; + (void)desc_cb_data; +} + TU_ATTR_WEAK void tuh_audio_mount_cb(uint8_t idx) { (void)idx; } @@ -417,25 +422,6 @@ static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_inter return (uint32_t)((numerator + 500000u) / 1000000u); } -// Feature Unit controls with variable or unknown width return zero. -static uint8_t audioh_fu_control_width(uint8_t control_selector) { - switch (control_selector) { - case AUDIO10_FU_CTRL_MUTE: - case AUDIO10_FU_CTRL_BASS: - case AUDIO10_FU_CTRL_MID: - case AUDIO10_FU_CTRL_TREBLE: - case AUDIO10_FU_CTRL_AGC: - case AUDIO10_FU_CTRL_BASS_BOOST: - case AUDIO10_FU_CTRL_LOUDNESS: - return 1; - case AUDIO10_FU_CTRL_VOLUME: - case AUDIO10_FU_CTRL_DELAY: - return 2; - default: - return 0; - } -} - // Preserve the stream identity and FIFO allocation while clearing device state. static void audioh_stream_reset(tuh_audio_stream_t *s) { s->daddr = 0; @@ -1577,6 +1563,13 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } p_audio->stream_count = stream_idx; + const tuh_audio_descriptor_cb_t desc_cb_data = { + .desc_audio_control = desc_itf, + .desc_cs_audio_control = ac_desc.desc_start, + .desc_cs_audio_control_len = (uint16_t)(ac_desc.desc_end - ac_desc.desc_start), + }; + tuh_audio_descriptor_cb(idx, &desc_cb_data); + return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); open_failed: @@ -1810,16 +1803,6 @@ uint8_t tuh_audio_get_dev_addr(uint8_t idx) { return _audioh_itf[idx].daddr; } -uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx) { - TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); - audioh_interface_t *p_audio = &_audioh_itf[idx]; - TU_VERIFY(p_audio->daddr != 0, 0); - - tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); - TU_VERIFY(s, 0); - return s->feature_unit_id; -} - bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; @@ -2176,7 +2159,7 @@ uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { } //--------------------------------------------------------------------+ -// FEATURE UNIT CONTROLS +// AUDIO CONTROL REQUESTS //--------------------------------------------------------------------+ // Release driver-owned request state before invoking the application callback, @@ -2196,12 +2179,11 @@ static void audioh_fu_set_complete(tuh_xfer_t *xfer) { } enum { - AUDIOH_FU_VALUE_U16, AUDIOH_FU_VALUE_BOOL, AUDIOH_FU_VALUE_I16 }; -static uint8_t audioh_fu_cur_request(uint8_t protocol, tusb_dir_t direction) { +static uint8_t audioh_control_cur_request(uint8_t protocol, tusb_dir_t direction) { #if !(CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1) (void)direction; #endif @@ -2219,30 +2201,60 @@ static uint8_t audioh_fu_cur_request(uint8_t protocol, tusb_dir_t direction) { } } -static bool audioh_fu_selector_supported(uint8_t protocol, uint8_t control_selector) { - #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 - if (protocol == AUDIO_INT_PROTOCOL_CODE_V2) { - return control_selector == AUDIO20_FU_CTRL_MUTE || control_selector == AUDIO20_FU_CTRL_VOLUME; +static bool audioh_control_submit(uint8_t idx, uint8_t entity_id, tusb_dir_t direction, uint8_t request, + uint8_t control_selector, uint8_t channel, void *buffer, uint16_t length, + tuh_xfer_t *xfer) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX && entity_id != 0 && request != 0, false); + TU_VERIFY(direction == TUSB_DIR_OUT || direction == TUSB_DIR_IN, false); + TU_VERIFY(buffer != NULL || length == 0, false); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->mounted, false); + + const tusb_control_request_t setup = { + .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = direction}, + .bRequest = request, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(entity_id, p_audio->ac_itf_num)), + .wLength = tu_htole16(length), + }; + xfer->daddr = p_audio->daddr; + xfer->ep_addr = 0; + xfer->setup = &setup; + xfer->buffer = buffer; + return tuh_control_xfer(xfer); +} + +bool tuh_audio_control_xfer(uint8_t idx, uint8_t entity_id, tusb_dir_t direction, uint8_t request, + uint8_t control_selector, uint8_t channel, void *buffer, uint16_t length, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + tuh_xfer_t xfer = {.complete_cb = complete_cb, .user_data = user_data}; + return audioh_control_submit(idx, entity_id, direction, request, control_selector, channel, buffer, length, &xfer); +} + +tusb_xfer_result_t tuh_audio_control_xfer_sync(uint8_t idx, uint8_t entity_id, tusb_dir_t direction, uint8_t request, + uint8_t control_selector, uint8_t channel, void *buffer, uint16_t length, + uint32_t *actual_len) { + if (actual_len != NULL) { + *actual_len = 0; } - #else - (void)protocol; - (void)control_selector; - #endif - return true; + + tuh_xfer_t xfer = {0}; + if (!audioh_control_submit(idx, entity_id, direction, request, control_selector, channel, buffer, length, &xfer)) { + return XFER_RESULT_TIMEOUT; + } + + if (actual_len != NULL) { + *actual_len = xfer.actual_len; + } + return xfer.result; } static void audioh_fu_value_store(audioh_ctrl_state_t *ctrl, audioh_epbuf_t *epbuf) { if (ctrl->fu.control.value_type == AUDIOH_FU_VALUE_BOOL) { *((bool *)ctrl->value) = audioh_fu_ctrl(epbuf)[0] != 0; - } else if (ctrl->fu.control.width == 1) { - *((uint16_t *)ctrl->value) = audioh_fu_ctrl(epbuf)[0]; } else { - const uint16_t value = tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))); - if (ctrl->fu.control.value_type == AUDIOH_FU_VALUE_I16) { - *((int16_t *)ctrl->value) = (int16_t)value; - } else { - *((uint16_t *)ctrl->value) = value; - } + const uint16_t value = tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))); + *((int16_t *)ctrl->value) = (int16_t)value; } } @@ -2317,11 +2329,12 @@ static bool audioh_mount_feature_unit_submit(uint8_t idx) { tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, ctrl->fu.mount.stream_idx); TU_ASSERT(s != NULL); - const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; - const tusb_control_request_t request = { + const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; + const uint8_t selector = uac2 ? AUDIO20_FU_CTRL_VOLUME : AUDIO10_FU_CTRL_VOLUME; + const tusb_control_request_t request = { .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, .bRequest = uac2 ? AUDIO20_CS_REQ_RANGE : audioh_fu_volume_range_request(ctrl->fu.mount.range_step), - .wValue = tu_htole16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0)), + .wValue = tu_htole16(tu_u16(selector, 0)), .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = tu_htole16(uac2 ? 8u : 2u), }; @@ -2411,24 +2424,20 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { audioh_mount_feature_unit_next(idx); } -bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, - uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { +static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint16_t value, uint8_t width, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s && s->feature_unit_id != 0, false); - TU_VERIFY(audioh_fu_selector_supported(p_audio->protocol, control_selector), false); - - const uint8_t width = audioh_fu_control_width(control_selector); - TU_VERIFY(width != 0, false); if (control_selector == AUDIO10_FU_CTRL_MUTE) { TU_VERIFY(s->mute_access == AUDIOH_CTRL_READ_WRITE, false); } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) { TU_VERIFY(s->volume_access == AUDIOH_CTRL_READ_WRITE, false); } - const uint8_t request_code = audioh_fu_cur_request(p_audio->protocol, TUSB_DIR_OUT); + const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_OUT); TU_VERIFY(request_code != 0, false); audioh_ctrl_state_t *ctrl = &p_audio->ctrl; audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; @@ -2436,29 +2445,17 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control // Reserve both bookkeeping and payload storage before populating the request. ctrl->fu_busy = true; - const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_OUT}, - .bRequest = request_code, - .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), - .wLength = width}; - uint8_t *val_buf = audioh_fu_ctrl(epbuf); val_buf[0] = (uint8_t)(value & 0xFF); if (width == 2) { val_buf[1] = (uint8_t)((value >> 8) & 0xFF); } - tuh_xfer_t xfer = {.daddr = p_audio->daddr, - .ep_addr = 0, - .setup = &request, - .buffer = val_buf, - .complete_cb = complete_cb, - .user_data = user_data}; + tuh_xfer_t xfer = {.complete_cb = complete_cb, .user_data = user_data}; if (complete_cb == NULL) { - const bool result = tuh_control_xfer(&xfer); + const bool result = audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, 0, + val_buf, width, &xfer); ctrl->fu_busy = false; return result; } @@ -2468,7 +2465,8 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control xfer.complete_cb = audioh_fu_set_complete; xfer.user_data = (uintptr_t)idx; - if (!tuh_control_xfer(&xfer)) { + if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, 0, val_buf, width, + &xfer)) { ctrl->complete_cb = NULL; ctrl->fu_busy = false; return false; @@ -2476,24 +2474,20 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control return true; } -static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, - void *value, uint8_t value_type, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { +static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, void *value, uint8_t width, + uint8_t value_type, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted && value, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s && s->feature_unit_id != 0, false); - TU_VERIFY(audioh_fu_selector_supported(p_audio->protocol, control_selector), false); - - const uint8_t width = audioh_fu_control_width(control_selector); - TU_VERIFY(width != 0, false); if (control_selector == AUDIO10_FU_CTRL_MUTE) { TU_VERIFY(s->mute_access != AUDIOH_CTRL_NONE, false); } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) { TU_VERIFY(s->volume_access != AUDIOH_CTRL_NONE, false); } - const uint8_t request_code = audioh_fu_cur_request(p_audio->protocol, TUSB_DIR_IN); + const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_IN); TU_VERIFY(request_code != 0, false); audioh_ctrl_state_t *ctrl = &p_audio->ctrl; audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; @@ -2503,24 +2497,12 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con ctrl->fu.control.width = width; ctrl->fu.control.value_type = value_type; - const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, - .type = TUSB_REQ_TYPE_CLASS, - .direction = TUSB_DIR_IN}, - .bRequest = request_code, - .wValue = tu_htole16(tu_u16(control_selector, channel)), - .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), - .wLength = width}; - if (complete_cb == NULL) { // The synchronous transfer completes before its driver-owned response is // converted to host order. - tuh_xfer_t xfer = {.daddr = p_audio->daddr, - .ep_addr = 0, - .setup = &request, - .buffer = audioh_fu_ctrl(epbuf), - .complete_cb = NULL, - .user_data = user_data}; - if (!tuh_control_xfer(&xfer)) { + tuh_xfer_t xfer = {.complete_cb = NULL, .user_data = user_data}; + if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, 0, + audioh_fu_ctrl(epbuf), width, &xfer)) { ctrl->fu_busy = false; return false; } @@ -2536,14 +2518,10 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con // The asynchronous wrapper converts the response before calling the application. ctrl->complete_cb = complete_cb; ctrl->user_data = user_data; - tuh_xfer_t xfer = {.daddr = p_audio->daddr, - .ep_addr = 0, - .setup = &request, - .buffer = audioh_fu_ctrl(epbuf), - .complete_cb = audioh_fu_get_complete, - .user_data = (uintptr_t)idx}; - - if (!tuh_control_xfer(&xfer)) { + tuh_xfer_t xfer = {.complete_cb = audioh_fu_get_complete, .user_data = (uintptr_t)idx}; + + if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, 0, + audioh_fu_ctrl(epbuf), width, &xfer)) { ctrl->complete_cb = NULL; ctrl->fu_busy = false; return false; @@ -2551,21 +2529,14 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con return true; } -bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, - uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - return audioh_feature_unit_get(idx, stream_idx, control_selector, channel, value, AUDIOH_FU_VALUE_U16, complete_cb, - user_data); -} - bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(tuh_audio_mute_supported(idx, stream_idx), false); - return tuh_audio_feature_unit_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute ? 1 : 0, complete_cb, user_data); + return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, mute ? 1 : 0, 1, complete_cb, user_data); } bool tuh_audio_mute_get(uint8_t idx, uint8_t stream_idx, bool *mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(mute != NULL && tuh_audio_mute_supported(idx, stream_idx), false); - return audioh_feature_unit_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute, AUDIOH_FU_VALUE_BOOL, complete_cb, - user_data); + return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, mute, 1, AUDIOH_FU_VALUE_BOOL, complete_cb, user_data); } bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_xfer_cb_t complete_cb, @@ -2582,16 +2553,14 @@ bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_x } volume = (int16_t)rounded; } - return tuh_audio_feature_unit_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, (uint16_t)volume, complete_cb, - user_data); + return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, (uint16_t)volume, 2, complete_cb, user_data); } bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { tuh_audio_volume_range_t range; TU_VERIFY(volume != NULL && tuh_audio_volume_range_get(idx, stream_idx, &range), false); - return audioh_feature_unit_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume, AUDIOH_FU_VALUE_I16, complete_cb, - user_data); + return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, volume, 2, AUDIOH_FU_VALUE_I16, complete_cb, user_data); } #endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 4807d376a..2e778c184 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -115,6 +115,14 @@ typedef struct { uint16_t res; } tuh_audio_volume_range_t; +// Audio Control descriptors reported during enumeration. Descriptor pointers +// are valid only for the duration of tuh_audio_descriptor_cb(). +typedef struct { + const tusb_desc_interface_t *desc_audio_control; + const uint8_t *desc_cs_audio_control; + uint16_t desc_cs_audio_control_len; +} tuh_audio_descriptor_cb_t; + //--------------------------------------------------------------------+ // Stream Enumeration //--------------------------------------------------------------------+ @@ -212,8 +220,6 @@ static inline uint32_t tuh_audio_config_frame_size(const tuh_audio_stream_config bool tuh_audio_mounted(uint8_t idx); // Get device address of Audio device uint8_t tuh_audio_get_dev_addr(uint8_t idx); -// Get the Feature Unit ID associated with a stream (0 = none) -uint8_t tuh_audio_get_feature_unit_id(uint8_t idx, uint8_t stream_idx); // True when the stream's Feature Unit supports master mute control. bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx); // Get the cached master volume range. Returns false when volume is unsupported. @@ -223,22 +229,13 @@ bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volum // Control Request API //--------------------------------------------------------------------+ -// Set a Feature Unit control associated with an Audio stream. UAC2 supports -// mute and volume through this low-level API; the other fixed-width selectors -// below are UAC1-only. -// Mute/bass/mid/treble/AGC/bass boost/loudness use one byte; volume/delay use two. -// Graphic EQ and unknown selectors are unsupported. -bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, - uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); - -// Get a Feature Unit control associated with an Audio stream. UAC2 supports -// mute and volume through this low-level API; the other fixed-width selectors -// below are UAC1-only. -// The value is converted to host byte order before complete_cb is invoked. -// Graphic EQ and unknown selectors are unsupported. -// Only one Feature Unit operation may be in flight per device. -bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, - uint16_t *value, tuh_xfer_cb_t complete_cb, uintptr_t user_data); +// Submit a class-specific request to an entity on the Audio Control interface. +// request is the protocol-specific UAC request code. buffer contains the raw +// little-endian control payload. For an asynchronous transfer, buffer must +// remain valid until complete_cb is invoked. +bool tuh_audio_control_xfer(uint8_t idx, uint8_t entity_id, tusb_dir_t direction, uint8_t request, + uint8_t control_selector, uint8_t channel, void *buffer, uint16_t length, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); // Master mute and volume controls. Capability and range information is cached // before tuh_audio_mount_cb() is invoked. Volume SET accepts @@ -252,23 +249,12 @@ bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_ uintptr_t user_data); //--------------------------------------------------------------------+ -// Control Request Sync API -// Each Function will make a USB control transfer request to/from device the function will block until request is -// complete. The function will return the transfer request result +// Synchronous control requests block until the transfer completes and return +// its result. actual_len may be NULL when the received length is not needed. //--------------------------------------------------------------------+ -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t stream_idx, - uint8_t control_selector, - uint8_t channel, - uint16_t value) { - TU_API_SYNC(tuh_audio_feature_unit_set, idx, stream_idx, control_selector, channel, value); -} - -TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t stream_idx, - uint8_t control_selector, - uint8_t channel, - uint16_t *value) { - TU_API_SYNC(tuh_audio_feature_unit_get, idx, stream_idx, control_selector, channel, value); -} +tusb_xfer_result_t tuh_audio_control_xfer_sync(uint8_t idx, uint8_t entity_id, tusb_dir_t direction, uint8_t request, + uint8_t control_selector, uint8_t channel, void *buffer, uint16_t length, + uint32_t *actual_len); TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_mute_set_sync(uint8_t idx, uint8_t stream_idx, bool mute) { @@ -294,6 +280,13 @@ TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_volume_get_sync // Callbacks (Weak is optional) //--------------------------------------------------------------------+ +// Invoked after the Audio Control and Streaming descriptors have been +// validated during enumeration, before tuh_audio_mount_cb(). The interface is +// not mounted yet and control requests must not be submitted from this +// callback. Applications may inspect or copy descriptors needed for later raw +// entity control requests. +void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb_data); + // Invoked when device with Audio interface is mounted void tuh_audio_mount_cb(uint8_t idx); diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index 9f8957984..2ede6b662 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -24,13 +24,13 @@ enum { CAPTURE_FU = 12, CAPTURE_OUTPUT_TERM = 13, - PLAYBACK_CLOCK = 10, - UNRELATED_CLOCK_0 = 20, - UNRELATED_CLOCK_1 = 21, - UNRELATED_TERM_0 = 22, - UNRELATED_TERM_1 = 23, - UNRELATED_FU_0 = 24, - UNRELATED_FU_1 = 25, + PLAYBACK_CLOCK = 10, + UNRELATED_CLOCK_0 = 20, + UNRELATED_CLOCK_1 = 21, + UNRELATED_TERM_0 = 22, + UNRELATED_TERM_1 = 23, + UNRELATED_FU_0 = 24, + UNRELATED_FU_1 = 25, }; static bool interface_set_result; @@ -43,6 +43,7 @@ static tuh_xfer_t control_xfer; static tusb_control_request_t control_request; static uint8_t control_buffer[8]; static uint8_t control_xfer_count; +static uint32_t control_sync_actual_len; static bool edpt_open_result; static tusb_desc_endpoint_t opened_ep[4]; @@ -67,6 +68,13 @@ static uint8_t err_cb_idx; static uint8_t err_cb_stream_idx; static uint16_t err_cb_xferred_bytes; +static uint8_t descriptor_cb_count; +static uint8_t descriptor_cb_idx; +static uint8_t descriptor_cb_protocol; +static uint8_t descriptor_cb_ac_itf; +static uint16_t descriptor_cb_cs_len; +static bool descriptor_cb_has_playback_fu; + static uint32_t edpt_mask(uint8_t ep_addr) { const uint8_t bit = tu_edpt_number(ep_addr) + (tu_edpt_dir(ep_addr) == TUSB_DIR_IN ? 16 : 0); return 1u << bit; @@ -83,6 +91,27 @@ void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { err_cb_xferred_bytes = xferred_bytes; } +void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb) { + descriptor_cb_count++; + descriptor_cb_idx = idx; + descriptor_cb_protocol = desc_cb->desc_audio_control->bInterfaceProtocol; + descriptor_cb_ac_itf = desc_cb->desc_audio_control->bInterfaceNumber; + descriptor_cb_cs_len = desc_cb->desc_cs_audio_control_len; + + const uint8_t *p_desc = desc_cb->desc_cs_audio_control; + const uint8_t *end = p_desc + desc_cb->desc_cs_audio_control_len; + while (p_desc < end && tu_desc_len(p_desc) >= 4 && tu_desc_len(p_desc) <= (uint16_t)(end - p_desc)) { + const uint8_t fu_subtype = (descriptor_cb_protocol == AUDIO_INT_PROTOCOL_CODE_V2) + ? AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT + : AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT; + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE && tu_desc_subtype(p_desc) == fu_subtype && + p_desc[3] == PLAYBACK_FU) { + descriptor_cb_has_playback_fu = true; + } + p_desc = tu_desc_next(p_desc); + } +} + tusb_speed_t tuh_speed_get(uint8_t daddr) { (void)daddr; return test_speed; @@ -96,7 +125,8 @@ bool tuh_control_xfer(tuh_xfer_t *xfer) { if (xfer->buffer != NULL) { memcpy(control_buffer, xfer->buffer, TU_MIN(sizeof(control_buffer), control_request.wLength)); } - xfer->result = XFER_RESULT_SUCCESS; + xfer->result = XFER_RESULT_SUCCESS; + xfer->actual_len = control_sync_actual_len; return control_xfer_result; } @@ -844,7 +874,8 @@ void setUp(void) { memset(&control_xfer, 0, sizeof(control_xfer)); memset(&control_request, 0, sizeof(control_request)); memset(control_buffer, 0, sizeof(control_buffer)); - control_xfer_count = 0; + control_xfer_count = 0; + control_sync_actual_len = 0; edpt_open_result = true; memset(opened_ep, 0, sizeof(opened_ep)); @@ -867,6 +898,13 @@ void setUp(void) { err_cb_stream_idx = TUSB_INDEX_INVALID_8; err_cb_xferred_bytes = 0; + descriptor_cb_count = 0; + descriptor_cb_idx = TUSB_INDEX_INVALID_8; + descriptor_cb_protocol = 0; + descriptor_cb_ac_itf = TUSB_INDEX_INVALID_8; + descriptor_cb_cs_len = 0; + descriptor_cb_has_playback_fu = false; + fu_cb_count = 0; fu_cb_user_data = 0; @@ -881,9 +919,7 @@ static void open_descriptors(const uint8_t *desc, uint16_t desc_len) { TEST_ASSERT_EQUAL_UINT16(desc_len, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)desc, desc_len)); } -static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) { - open_descriptors(desc, desc_len); - TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); +static void complete_mount(void) { while (!tuh_audio_mounted(0)) { switch (control_request.bRequest) { case AUDIO10_CS_REQ_GET_MIN: @@ -904,6 +940,12 @@ static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) { control_xfer_count = 0; } +static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) { + open_descriptors(desc, desc_len); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_mount(); +} + void test_audio_host_rejects_midi1_collection_without_consuming_instance(void) { TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)midi1_only_collection, sizeof(midi1_only_collection))); @@ -916,6 +958,18 @@ void test_audio_host_rejects_midi2_collection_without_consuming_instance(void) { TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); } +void test_audio_host_exposes_audio_control_descriptors_during_enumeration(void) { + open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); + + TEST_ASSERT_EQUAL_UINT8(1, descriptor_cb_count); + TEST_ASSERT_EQUAL_UINT8(0, descriptor_cb_idx); + TEST_ASSERT_EQUAL_UINT8(AUDIO_INT_PROTOCOL_CODE_V1, descriptor_cb_protocol); + TEST_ASSERT_EQUAL_UINT8(AUDIO_AC_ITF, descriptor_cb_ac_itf); + TEST_ASSERT_GREATER_THAN_UINT16(0, descriptor_cb_cs_len); + TEST_ASSERT_TRUE(descriptor_cb_has_playback_fu); + TEST_ASSERT_FALSE(tuh_audio_mounted(0)); +} + void test_audio_host_saves_and_opens_explicit_feedback_endpoint(void) { mount_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); @@ -940,6 +994,8 @@ void test_audio_host_rejects_uac2_interface_without_consuming_instance(void) { void test_audio_host_mounts_uac2_and_sets_clock_before_activating_stream(void) { open_descriptors(uac2_playback, sizeof(uac2_playback)); + TEST_ASSERT_EQUAL_UINT8(AUDIO_INT_PROTOCOL_CODE_V2, descriptor_cb_protocol); + TEST_ASSERT_TRUE(descriptor_cb_has_playback_fu); TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); TEST_ASSERT_FALSE(tuh_audio_mounted(0)); @@ -959,7 +1015,6 @@ void test_audio_host_mounts_uac2_and_sets_clock_before_activating_stream(void) { TEST_ASSERT_TRUE(tuh_audio_mounted(0)); TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 0)); - TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0)); tuh_audio_volume_range_t range; TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range)); @@ -999,6 +1054,31 @@ void test_audio_host_mounts_uac2_and_sets_clock_before_activating_stream(void) { TEST_ASSERT_EQUAL_UINT16(192, edpt_xfer_bytes[0]); } +void test_audio_host_generic_control_request_supports_uac2_clock_entity(void) { + uint8_t clock_valid = 0; + open_descriptors(uac2_playback, sizeof(uac2_playback)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + complete_uac2_volume_range((int16_t)(-90 * 256), (int16_t)(6 * 256), 256); + + control_xfer_count = 0; + TEST_ASSERT_TRUE(tuh_audio_control_xfer(0, PLAYBACK_CLOCK, TUSB_DIR_IN, AUDIO20_CS_REQ_CUR, AUDIO20_CS_CTRL_CLK_VALID, + 0, &clock_valid, sizeof(clock_valid), feature_unit_complete, 77)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL(TUSB_DIR_IN, control_request.bmRequestType_bit.direction); + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_CS_CTRL_CLK_VALID, 0), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_CLOCK, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + TEST_ASSERT_EQUAL_UINT16(1, tu_le16toh(control_request.wLength)); + + control_xfer.buffer[0] = 1; + control_xfer.actual_len = 1; + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count); + TEST_ASSERT_EQUAL_UINT32(77, fu_cb_user_data); + TEST_ASSERT_EQUAL_UINT8(1, clock_valid); +} + void test_audio_host_uac2_read_only_clock_exposes_cur_rate_without_setting_it(void) { open_descriptors(uac2_playback_read_only_clock, sizeof(uac2_playback_read_only_clock)); TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); @@ -1138,23 +1218,28 @@ void test_audio_host_ignores_second_data_endpoint_in_same_as_interface(void) { void test_audio_host_maps_playback_fu_declared_before_usb_input_terminal(void) { open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); - TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); } void test_audio_host_maps_relevant_terminal_and_fu_after_unrelated_entities(void) { open_descriptors(playback_after_unrelated_ac_entities, sizeof(playback_after_unrelated_ac_entities)); TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); - TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); } void test_audio_host_maps_capture_fu_declared_before_usb_output_terminal(void) { uint8_t captured[CFG_TUH_AUDIO_STREAM_BUFSIZE]; const uint16_t fifo_depth = CFG_TUH_AUDIO_STREAM_BUFSIZE - (CFG_TUH_AUDIO_STREAM_BUFSIZE % 3); - mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + open_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(CAPTURE_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + complete_mount(); TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_CAPTURE, tuh_audio_stream_direction(0, 0)); - TEST_ASSERT_EQUAL_UINT8(CAPTURE_FU, tuh_audio_get_feature_unit_id(0, 0)); + TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0)); TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); @@ -1183,8 +1268,12 @@ void test_audio_host_maps_duplex_fus_declared_before_usb_terminals(void) { open_descriptors(duplex_fus_before_usb_terminals, sizeof(duplex_fus_before_usb_terminals)); TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_stream_count(0)); - TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); - TEST_ASSERT_EQUAL_UINT8(CAPTURE_FU, tuh_audio_get_feature_unit_id(0, 1)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + complete_control_xfer_with_u16((uint16_t)(-90 * 256)); + complete_control_xfer_with_u16(6 * 256); + complete_control_xfer_with_u16(256); + TEST_ASSERT_EQUAL_HEX16(tu_u16(CAPTURE_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); } void test_audio_host_reads_volume_ranges_for_both_streams_before_mount(void) { @@ -1422,40 +1511,51 @@ void test_audio_host_closes_old_endpoint_and_cleans_up_failed_reconfiguration(vo TEST_ASSERT_EQUAL_UINT8(TUSB_INDEX_INVALID_8, tuh_audio_active_config(0, 0)); } -void test_audio_host_uses_correct_feature_unit_widths_and_serializes_requests(void) { - static const uint8_t one_byte_controls[] = { - AUDIO10_FU_CTRL_MUTE, AUDIO10_FU_CTRL_BASS, AUDIO10_FU_CTRL_MID, AUDIO10_FU_CTRL_TREBLE, - AUDIO10_FU_CTRL_AGC, AUDIO10_FU_CTRL_BASS_BOOST, AUDIO10_FU_CTRL_LOUDNESS, - }; - static const uint8_t two_byte_controls[] = {AUDIO10_FU_CTRL_VOLUME, AUDIO10_FU_CTRL_DELAY}; - +void test_audio_host_submits_generic_entity_control_request(void) { + uint8_t payload[] = {0x34, 0x12, 0x56}; mount_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); - for (uint8_t i = 0; i < TU_ARRAY_SIZE(one_byte_controls); i++) { - TEST_ASSERT_TRUE(tuh_audio_feature_unit_set(0, 0, one_byte_controls[i], 0, 0x1234, NULL, 0)); - TEST_ASSERT_EQUAL_UINT16(1, tu_le16toh(control_request.wLength)); - TEST_ASSERT_EQUAL_HEX8(0x34, control_buffer[0]); - } - for (uint8_t i = 0; i < TU_ARRAY_SIZE(two_byte_controls); i++) { - TEST_ASSERT_TRUE(tuh_audio_feature_unit_set(0, 0, two_byte_controls[i], 0, 0x1234, NULL, 0)); - TEST_ASSERT_EQUAL_UINT16(2, tu_le16toh(control_request.wLength)); - TEST_ASSERT_EQUAL_HEX8(0x34, control_buffer[0]); - TEST_ASSERT_EQUAL_HEX8(0x12, control_buffer[1]); - } - TEST_ASSERT_FALSE(tuh_audio_feature_unit_set(0, 0, AUDIO10_FU_CTRL_GRAPHIC_EQUALIZER, 0, 0, NULL, 0)); - - const uint8_t previous_xfer_count = control_xfer_count; - TEST_ASSERT_TRUE(tuh_audio_feature_unit_set(0, 0, AUDIO10_FU_CTRL_MUTE, 0, 1, feature_unit_complete, 0x1234)); - TEST_ASSERT_FALSE(tuh_audio_feature_unit_set(0, 0, AUDIO10_FU_CTRL_VOLUME, 0, 2, feature_unit_complete, 0x5678)); - TEST_ASSERT_EQUAL_UINT8(previous_xfer_count + 1, control_xfer_count); + TEST_ASSERT_TRUE(tuh_audio_control_xfer(0, PLAYBACK_FU, TUSB_DIR_OUT, AUDIO10_CS_REQ_SET_CUR, + AUDIO10_FU_CTRL_GRAPHIC_EQUALIZER, 2, payload, sizeof(payload), + feature_unit_complete, 0x1234)); + TEST_ASSERT_EQUAL(TUSB_DIR_OUT, control_request.bmRequestType_bit.direction); + TEST_ASSERT_EQUAL(TUSB_REQ_TYPE_CLASS, control_request.bmRequestType_bit.type); + TEST_ASSERT_EQUAL(TUSB_REQ_RCPT_INTERFACE, control_request.bmRequestType_bit.recipient); + TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_SET_CUR, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_GRAPHIC_EQUALIZER, 2), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_FU, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + TEST_ASSERT_EQUAL_UINT16(sizeof(payload), tu_le16toh(control_request.wLength)); + TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, control_buffer, sizeof(payload)); complete_control_xfer(XFER_RESULT_SUCCESS); TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count); TEST_ASSERT_EQUAL_HEX32(0x1234, fu_cb_user_data); - TEST_ASSERT_TRUE(tuh_audio_feature_unit_set(0, 0, AUDIO10_FU_CTRL_VOLUME, 0, 2, feature_unit_complete, 0x5678)); - complete_control_xfer(XFER_RESULT_SUCCESS); - TEST_ASSERT_EQUAL_UINT8(2, fu_cb_count); - TEST_ASSERT_EQUAL_HEX32(0x5678, fu_cb_user_data); + + TEST_ASSERT_FALSE(tuh_audio_control_xfer(0, 0, TUSB_DIR_OUT, AUDIO10_CS_REQ_SET_CUR, AUDIO10_FU_CTRL_MUTE, 0, payload, + 1, NULL, 0)); + TEST_ASSERT_FALSE(tuh_audio_control_xfer(0, PLAYBACK_FU, TUSB_DIR_OUT, AUDIO10_CS_REQ_SET_CUR, AUDIO10_FU_CTRL_MUTE, + 0, NULL, 1, NULL, 0)); +} + +void test_audio_host_sync_entity_control_request_returns_actual_length(void) { + uint8_t payload[8] = {0}; + uint32_t actual_len = UINT32_MAX; + mount_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); + + control_sync_actual_len = 3; + TEST_ASSERT_EQUAL(XFER_RESULT_SUCCESS, + tuh_audio_control_xfer_sync(0, PLAYBACK_FU, TUSB_DIR_IN, AUDIO10_CS_REQ_GET_CUR, + AUDIO10_FU_CTRL_GRAPHIC_EQUALIZER, 0, payload, sizeof(payload), + &actual_len)); + TEST_ASSERT_EQUAL_UINT32(3, actual_len); + + control_xfer_result = false; + actual_len = UINT32_MAX; + TEST_ASSERT_EQUAL(XFER_RESULT_TIMEOUT, + tuh_audio_control_xfer_sync(0, PLAYBACK_FU, TUSB_DIR_IN, AUDIO10_CS_REQ_GET_CUR, + AUDIO10_FU_CTRL_GRAPHIC_EQUALIZER, 0, payload, sizeof(payload), + &actual_len)); + TEST_ASSERT_EQUAL_UINT32(0, actual_len); } void test_audio_host_reads_and_caches_feature_unit_controls_before_mount(void) { @@ -1501,7 +1601,6 @@ void test_audio_host_mounts_with_mute_only_when_volume_range_fails(void) { TEST_ASSERT_TRUE(tuh_audio_mounted(0)); TEST_ASSERT_TRUE(tuh_audio_mute_supported(0, 0)); TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range)); - TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); } void test_audio_host_rejects_invalid_cached_volume_range(void) { @@ -1522,7 +1621,6 @@ void test_audio_host_ignores_feature_unit_without_master_mute_or_volume(void) { tuh_audio_volume_range_t range; mount_descriptors(playback_fu_without_mute_volume, sizeof(playback_fu_without_mute_volume)); - TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_feature_unit_id(0, 0)); TEST_ASSERT_FALSE(tuh_audio_mute_supported(0, 0)); TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range)); } @@ -1541,6 +1639,7 @@ void test_audio_host_typed_mute_and_volume_controls(void) { TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_MUTE, 0), tu_le16toh(control_request.wValue)); TEST_ASSERT_EQUAL_UINT16(1, tu_le16toh(control_request.wLength)); TEST_ASSERT_EQUAL_HEX8(1, control_buffer[0]); + TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, -6 * 256, feature_unit_complete, 0)); complete_control_xfer(XFER_RESULT_SUCCESS); TEST_ASSERT_TRUE(tuh_audio_mute_get(0, 0, &mute, feature_unit_complete, 0x2345)); -- cgit v1.3.1 From ed70f7206694e2ee5dfc0e7b3b947c27413deba0 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 09:58:09 +0200 Subject: audio: service example FIFOs at watermarks Move capture and playback FIFO servicing into the main-loop task at half-full and half-drained watermarks. Use transfer callbacks only for diagnostics, share one conversion buffer, and demonstrate that application reads and writes need not be synchronized to USB callbacks. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 18 ++-- examples/host/audio_host/src/app.h | 3 +- examples/host/audio_host/src/audio_app.c | 152 ++++++++++++++----------------- examples/host/audio_host/src/main.c | 3 +- 4 files changed, 78 insertions(+), 98 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index e7140bf67..706752a05 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -7,9 +7,9 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) - Enumerates and mounts USB Audio Class 1.0 and 2.0 devices - Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) - Reports each stream's master mute/volume capabilities and cached volume range -- Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 kHz fallback; stereo preferred, mono accepted) +- Configures and starts an S16_LE capture stream (44.1 kHz preferred, 48 kHz fallback; stereo preferred, mono accepted) - Echoes captured audio to an S16_LE playback stream at the same sample rate (same channel count preferred, mono/stereo conversion otherwise) -- Frame-based FIFO API: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules transfers at the endpoint's polling interval +- Frame-based FIFO API: the main loop reads capture when its FIFO is half full and fills playback when its FIFO is half drained; USB transfer callbacks are not used for FIFO servicing - Cycles the streams through three phases (5 s each): mic-only (capture, data dropped), spk-only (sine test tone), and echo (capture looped back to playback) ## Supported Devices @@ -20,7 +20,7 @@ This example supports UAC1 devices whose Type I Format descriptor lists discrete - USB headsets (mono microphone + speaker) - USB audio interfaces -The echo needs a matching S16_LE playback stream at the capture sample rate; devices without one run capture-only. The sample rate and channel preferences are configured by the `SAMPLE_RATES` / `AUDIO_MAX_CHANNELS` macros in `src/audio_app.c` (48 kHz stereo by default). Non-PCM formats are rejected by the driver. +The echo needs a matching S16_LE playback stream at the capture sample rate; devices without one run capture-only. The sample rate and channel preferences are configured by the `SAMPLE_RATES` / `AUDIO_MAX_CHANNELS` macros in `src/audio_app.c` (44.1 kHz stereo by default). Non-PCM formats are rejected by the driver. ## Limitations and trade-offs @@ -69,10 +69,10 @@ make BOARD= flash 3. Open a serial terminal to view output 4. The example will: - Print each stream's master mute/volume capabilities, cached volume range, and supported configurations when mounted - - Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 kHz fallback; stereo preferred, mono accepted) and configure it + - Look for an S16_LE capture configuration at a preferred sample rate (44.1 kHz first, 48 kHz fallback; stereo preferred, mono accepted) and configure it - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB - - Drain the capture FIFO in `audio_app_task_read()` and queue the frames into the playback FIFO; a sine test tone plays on the playback stream when no capture stream is echoing + - Service both FIFOs from `audio_app_task()` at their half-full/half-drained watermarks; a sine test tone plays on the playback stream when no capture stream is echoing - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; a failed stream is restarted automatically 100 ms after the error callback ## Serial Output Example @@ -91,12 +91,12 @@ Audio device mounted: idx=0 addr=1 master volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 - Configuring 48 kHz S16_LE capture (2 channels) + Configuring 44100 S16_LE capture (2 channels) Microphone configured Microphone master mute: off Microphone master volume: 0 (1/256 dB) Microphone master volume set: -1536 (1/256 dB) - Configuring 48 kHz S16_LE playback (2 channels) + Configuring 44100 S16_LE playback (2 channels) Speaker configured Speaker master mute: off Speaker master volume: 0 (1/256 dB) @@ -116,7 +116,7 @@ Edit `src/tusb_config.h` to modify: ## Notes - `tuh_audio_descriptor_cb()` exposes the validated Audio Control descriptor block during enumeration. Applications that need raw entity controls must copy the required entity IDs or descriptor fields before the callback returns, then use `tuh_audio_control_xfer()` after the device mounts. -- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers follow the endpoint's `bInterval`. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` report each completed transfer; `tuh_audio_err_cb()` reports failures. The example restarts the failed stream automatically 100 ms after the error callback. +- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers follow the endpoint's `bInterval`. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` only count completed transfers; `audio_app_task()` services the FIFOs independently from the main loop. `tuh_audio_err_cb()` reports failures, and the example restarts the failed stream automatically 100 ms later. - Capture and playback streams running concurrently in the same Audio Control instance must use the same sample rate. -- `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually queued/read (0 when the FIFO is empty/full or the stream is not running), and `tuh_audio_read_available()` / `tuh_audio_write_available()` report the FIFO occupancy in frames. `tuh_audio_write()` only queues data; the playback transfer-completion chain sends it, or sends silence when the FIFO does not contain a complete polling interval without consuming the partial data. +- `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually read/queued. `tuh_audio_read_available()` reports captured frames ready to read; `tuh_audio_write_available()` reports free playback capacity. `tuh_audio_write()` only queues data; the playback transfer-completion chain sends it, or sends silence when the FIFO does not contain a complete polling interval without consuming the partial data. - Isochronous transfers require the host to poll `tuh_task()` continuously; the capture FIFO absorbs short scheduling gaps and overwrites the oldest frames when full. diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h index 474544a19..45ba52dd2 100644 --- a/examples/host/audio_host/src/app.h +++ b/examples/host/audio_host/src/app.h @@ -22,7 +22,6 @@ #include void led_blinking_task(void); -void audio_app_task_read(void); -void audio_app_task_write(void); +void audio_app_task(void); void defer_queue_task(void); #endif diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 8852ca58c..d3cc79c41 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -25,26 +25,23 @@ //--------------------------------------------------------------------+ // Default configuration of this example, adjust to the target device: -// - AUDIO_MAX_FRAME_COUNT: buffer holds up to 48 frames (1 ms of 48 kHz) // - AUDIO_MAX_CHANNELS: maximum channels of the capture/playback stream // - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default) -#define AUDIO_MAX_FRAME_COUNT 48 -#define AUDIO_MAX_CHANNELS 2 -#define SAMPLE_RATES {44100, 48000} -#define FEATURE_UNIT_VOLUME_DB (-6 * 256) -static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device -static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index -static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index -static bool mic_ready = false; // capture stream is running -static bool spk_ready = false; // playback stream is running -static int16_t mic_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // capture FIFO read buffer -static int16_t spk_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // playback FIFO write buffer -static tuh_audio_stream_config_t mic_config; // selected capture configuration -static tuh_audio_stream_config_t spk_config; // selected playback configuration -static uint32_t audio_frame_count = AUDIO_MAX_FRAME_COUNT; // frames per ms of the selected rate -static uint32_t spk_cb_count = 0; // count of playback callbacks (for debug) -static uint32_t mic_cb_count = 0; // count of capture callbacks (for debug) -static uint32_t err_cb_count = 0; // count of error callbacks (for debug) +#define AUDIO_MAX_CHANNELS 2 +#define SAMPLE_RATES {44100, 48000} +#define FEATURE_UNIT_VOLUME_DB (-6 * 256) +#define AUDIO_BUFFER_SAMPLE_COUNT (CFG_TUH_AUDIO_STREAM_BUFSIZE / 2 / sizeof(int16_t)) +static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device +static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index +static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index +static bool mic_ready = false; // capture stream is running +static bool spk_ready = false; // playback stream is running +static int16_t audio_samples[AUDIO_BUFFER_SAMPLE_COUNT]; // shared capture/playback buffer +static tuh_audio_stream_config_t mic_config; // selected capture configuration +static tuh_audio_stream_config_t spk_config; // selected playback configuration +static uint32_t spk_cb_count = 0; // playback callbacks (for debug) +static uint32_t mic_cb_count = 0; // capture callbacks (for debug) +static uint32_t err_cb_count = 0; // error callbacks (for debug) //--------------------------------------------------------------------+ @@ -101,18 +98,19 @@ void defer_queue_task(void) { } } -// Duplicate each mono sample to both channels (mono mic -> stereo speaker) -static void mono_to_stereo(const int16_t *mono, int16_t *stereo, uint32_t frames) { - for (uint32_t i = 0; i < frames; i++) { - stereo[i * 2] = mono[i]; - stereo[i * 2 + 1] = mono[i]; +// Expand backward so mono capture can be converted to stereo in place. +static void mono_to_stereo(int16_t *samples, uint32_t frames) { + for (uint32_t i = frames; i-- > 0;) { + const int16_t sample = samples[i]; + samples[i * 2] = sample; + samples[i * 2 + 1] = sample; } } -// Average both channels into one sample (stereo mic -> mono speaker) -static void stereo_to_mono(const int16_t *stereo, int16_t *mono, uint32_t frames) { +// Contract forward so stereo capture can be converted to mono in place. +static void stereo_to_mono(int16_t *samples, uint32_t frames) { for (uint32_t i = 0; i < frames; i++) { - mono[i] = (int16_t)(((int32_t)stereo[i * 2] + stereo[i * 2 + 1]) / 2); + samples[i] = (int16_t)(((int32_t)samples[i * 2] + samples[i * 2 + 1]) / 2); } } @@ -138,23 +136,16 @@ static void spk_fill_sine(uint32_t frames) { const int16_t sample = sine_lut[sine_phase >> (32u - SINE_LUT_BITS)]; sine_phase += phase_step; for (uint8_t ch = 0; ch < spk_config.channels; ch++) { - spk_samples[i * spk_config.channels + ch] = sample; + audio_samples[i * spk_config.channels + ch] = sample; } } } -// Frames to queue this millisecond at the given sample rate: rate / 1000, -// with the fractional remainder (0.1 frame per ms at 44.1 kHz) accumulated -// and paid back as one extra frame, matching the driver's playback pacing. -static uint32_t frame_rem_acc = 0; -static uint32_t audio_frames_this_ms(uint32_t sample_rate) { - uint32_t frames = sample_rate / 1000; - frame_rem_acc += sample_rate % 1000; - if (frame_rem_acc >= 1000) { - frame_rem_acc -= 1000; - frames++; - } - return frames; +// One application buffer is half of the driver's stream FIFO. Servicing the +// FIFO at this watermark leaves the other half available to absorb scheduling +// jitter between main-loop iterations. +static uint32_t audio_half_fifo_frames(const tuh_audio_stream_config_t *config) { + return TU_ARRAY_SIZE(audio_samples) / config->channels; } //--------------------------------------------------------------------+ @@ -163,7 +154,7 @@ static uint32_t audio_frames_this_ms(uint32_t sample_rate) { // Cycles through three phases with tuh_audio_start()/stop(). The driver // activates/deactivates the stream's interface (SET_INTERFACE alt setting) // on each switch. -// 1. mic only (3 s): capture runs, captured data is dropped +// 1. mic only (5 s): capture runs, captured data is dropped // 2. spk only (5 s): playback plays the sine test tone // 3. echo (5 s): both streams run, captured audio is echoed back #define APP_PHASE_MIC_ONLY_MS 5000 @@ -181,8 +172,8 @@ static uint8_t app_audio_phase = APP_PHASE_MIC_ONLY; static const uint32_t app_phase_ms[APP_PHASE_COUNT] = {APP_PHASE_MIC_ONLY_MS, APP_PHASE_SPK_ONLY_MS, APP_PHASE_ECHO_MS}; // Start or stop the capture/playback streams according to the current phase. -// The app tasks already behave per phase: with mic_ready false the sine tone -// plays, with the playback stream stopped the echo write returns 0 (dropped). +// The app task discards capture in mic-only mode, generates sine in speaker-only +// mode, and echoes capture when both streams run. static void app_audio_phase_apply(void) { switch (app_audio_phase) { case APP_PHASE_MIC_ONLY: @@ -261,50 +252,47 @@ void led_blinking_task(void) { //--------------------------------------------------------------------+ -// Echo the captured audio back to the playback stream: drain the capture -// FIFO into mic_samples, convert, and queue the frames into the playback -// FIFO. The driver schedules the actual isochronous transfers. - -void audio_app_task_read(void) { - if (!mic_ready) { +// Echo one chunk after capture is at least half full and playback is at least +// half drained. This runs from the main loop, independently of USB callbacks. +static void audio_echo_task(void) { + const uint32_t mic_frames = audio_half_fifo_frames(&mic_config); + const uint32_t spk_frames = audio_half_fifo_frames(&spk_config); + if (tuh_audio_read_available(audio_idx, cap_stream_idx) < mic_frames || + tuh_audio_write_available(audio_idx, spk_stream_idx) < spk_frames) { return; } - const uint32_t frames = - tuh_audio_read(audio_idx, cap_stream_idx, mic_samples, audio_frames_this_ms(mic_config.sample_rate)); - if (frames == 0) { - return; - } - if (!spk_ready) { - return; // capture-only device or playback intentionally stopped + const uint32_t frames = TU_MIN(mic_frames, spk_frames); + (void)tuh_audio_read(audio_idx, cap_stream_idx, audio_samples, frames); + if (mic_config.channels == 1 && spk_config.channels == 2) { + mono_to_stereo(audio_samples, frames); + } else if (mic_config.channels == 2 && spk_config.channels == 1) { + stereo_to_mono(audio_samples, frames); } - if (spk_config.channels == mic_config.channels) { - memcpy(spk_samples, mic_samples, frames * mic_config.channels * sizeof(int16_t)); - } else if (mic_config.channels == 1 && spk_config.channels == 2) { - mono_to_stereo(mic_samples, spk_samples, frames); - } else { - stereo_to_mono(mic_samples, spk_samples, frames); - } - - (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); + (void)tuh_audio_write(audio_idx, spk_stream_idx, audio_samples, frames); } -void audio_app_task_write(void) { - // Fallback: the sine test tone when no capture stream is echoing - if (mic_ready || !spk_ready) { - return; - } - - const uint32_t frames = audio_frames_this_ms(spk_config.sample_rate); - if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) { - spk_fill_sine(frames); - (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); +void audio_app_task(void) { + if (mic_ready && spk_ready) { + audio_echo_task(); + } else if (mic_ready) { + const uint32_t frames = audio_half_fifo_frames(&mic_config); + if (tuh_audio_read_available(audio_idx, cap_stream_idx) >= frames) { + (void)tuh_audio_read(audio_idx, cap_stream_idx, audio_samples, frames); + } + } else if (spk_ready) { + const uint32_t frames = audio_half_fifo_frames(&spk_config); + if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) { + spk_fill_sine(frames); + (void)tuh_audio_write(audio_idx, spk_stream_idx, audio_samples, frames); + } } } -// Invoked when an isochronous IN transfer completes: the captured data is -// already queued into the capture FIFO and drained by audio_app_task_read(). +// Transfer callbacks are intentionally not used to service the FIFOs. The +// main-loop audio_app_task() reads and writes independently at half-FIFO +// watermarks; these callbacks only collect diagnostic counts. void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)idx; (void)stream_idx; @@ -312,8 +300,6 @@ void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_byte mic_cb_count++; } -// Invoked when an isochronous OUT transfer completes: the next queued packet -// is submitted from the playback FIFO by the driver. void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)idx; (void)stream_idx; @@ -453,7 +439,7 @@ static void tuh_audio_mount_async(uintptr_t param) { print_stream_configs(idx, stream_idx); } - // Select a supported 48 kHz S16_LE capture configuration without + // Select a preferred S16_LE capture configuration without // accessing USB interfaces, alternate settings, or endpoint addresses. // Sample rates are tried in SAMPLE_RATES order (44.1 kHz first), stereo is // preferred, mono is accepted. @@ -480,8 +466,6 @@ static void tuh_audio_mount_async(uintptr_t param) { audio_idx = idx; cap_stream_idx = stream_idx; mic_config = config; - // one ms of audio at the selected rate, rounded down to whole frames - audio_frame_count = sample_rate / 1000; printf(" Microphone configured\r\n"); configure_stream_controls(idx, stream_idx, "Microphone"); mic_ready = tuh_audio_start(idx, stream_idx); @@ -493,7 +477,7 @@ static void tuh_audio_mount_async(uintptr_t param) { } } if (!capture_found) { - printf(" No supported 48/44.1 kHz S16_LE capture configuration found\r\n"); + printf(" No supported 44.1/48 kHz S16_LE capture configuration found\r\n"); } // The echo needs a playback stream at the capture sample rate (or at any @@ -537,9 +521,7 @@ static void tuh_audio_mount_async(uintptr_t param) { } audio_idx = idx; printf(" Speaker configured\r\n"); - // playback-only device: set the frame cadence from the selected rate - audio_frame_count = spk_config.sample_rate / 1000; - sine_phase = 0; + sine_phase = 0; configure_stream_controls(idx, spk_stream_idx, "Speaker"); if (capture_found) { diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c index 45c468f23..c7800958b 100644 --- a/examples/host/audio_host/src/main.c +++ b/examples/host/audio_host/src/main.c @@ -42,8 +42,7 @@ int main(void) { // tinyusb host task tuh_task(); led_blinking_task(); - audio_app_task_read(); - audio_app_task_write(); + audio_app_task(); defer_queue_task(); } } -- cgit v1.3.1 From 95a99e18d4a46d4bec2994a74ffd1818d28d1dc4 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 15:28:07 +0200 Subject: Improve audio host stream event reporting Report asynchronous start and stop completion with transfer results. Replace the ambiguous stream error callback and byte count with explicit transfer-failure events, and ignore stale completions after stopping. --- examples/host/audio_host/README.md | 4 +- examples/host/audio_host/src/audio_app.c | 33 +++-- src/class/audio/audio_host.c | 157 +++++++++++++---------- src/class/audio/audio_host.h | 23 +++- test/unit-test/test/host/audio/test_audio_host.c | 101 +++++++++++---- 5 files changed, 207 insertions(+), 111 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 706752a05..3d4307b42 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -73,7 +73,7 @@ make BOARD= flash - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB - Service both FIFOs from `audio_app_task()` at their half-full/half-drained watermarks; a sine test tone plays on the playback stream when no capture stream is echoing - - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; a failed stream is restarted automatically 100 ms after the error callback + - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; their asynchronous results are printed from `tuh_audio_event_cb()`, and a failed stream is restarted automatically after 100 ms ## Serial Output Example @@ -116,7 +116,7 @@ Edit `src/tusb_config.h` to modify: ## Notes - `tuh_audio_descriptor_cb()` exposes the validated Audio Control descriptor block during enumeration. Applications that need raw entity controls must copy the required entity IDs or descriptor fields before the callback returns, then use `tuh_audio_control_xfer()` after the device mounts. -- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers follow the endpoint's `bInterval`. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` only count completed transfers; `audio_app_task()` services the FIFOs independently from the main loop. `tuh_audio_err_cb()` reports failures, and the example restarts the failed stream automatically 100 ms later. +- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers follow the endpoint's `bInterval`. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` only count completed transfers; `audio_app_task()` services the FIFOs independently from the main loop. `tuh_audio_event_cb()` reports asynchronous start/stop results and unrecoverable transfer failures. The example restarts a failed stream automatically 100 ms later. - Capture and playback streams running concurrently in the same Audio Control instance must use the same sample rate. - `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually read/queued. `tuh_audio_read_available()` reports captured frames ready to read; `tuh_audio_write_available()` reports free playback capacity. `tuh_audio_write()` only queues data; the playback transfer-completion chain sends it, or sends silence when the FIFO does not contain a complete polling interval without consuming the partial data. - Isochronous transfers require the host to poll `tuh_task()` continuously; the capture FIFO absorbs short scheduling gaps and overwrites the oldest frames when full. diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index d3cc79c41..55efe3295 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -41,7 +41,7 @@ static tuh_audio_stream_config_t mic_config; // se static tuh_audio_stream_config_t spk_config; // selected playback configuration static uint32_t spk_cb_count = 0; // playback callbacks (for debug) static uint32_t mic_cb_count = 0; // capture callbacks (for debug) -static uint32_t err_cb_count = 0; // error callbacks (for debug) +static uint32_t fail_event_count = 0; // transfer/start failures (for debug) //--------------------------------------------------------------------+ @@ -238,11 +238,11 @@ void led_blinking_task(void) { board_led_write(led_state); led_state = 1 - led_state; // toggle #if 1 - printf(" MIC CB=%lu SPK CB=%lu ERR CB=%lu\r\n", (unsigned long)mic_cb_count, (unsigned long)spk_cb_count, - (unsigned long)err_cb_count); + printf(" MIC CB=%lu SPK CB=%lu FAIL EVENT=%lu\r\n", (unsigned long)mic_cb_count, (unsigned long)spk_cb_count, + (unsigned long)fail_event_count); mic_cb_count = 0; spk_cb_count = 0; - err_cb_count = 0; + fail_event_count = 0; #endif } @@ -325,18 +325,29 @@ static void audio_app_restart_stream(uintptr_t param) { } } -// Invoked when stream activation or an isochronous transfer fails: the stream -// was stopped by the driver, re-open it after a short delay so the device can -// recover. -void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { - (void)xferred_bytes; - err_cb_count++; +void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event, tusb_xfer_result_t result) { + const char *stream_name = (stream_idx == cap_stream_idx) ? "capture" : "playback"; + + if (event == TUH_AUDIO_EVENT_START_COMPLETE) { + printf(" %s start %s: result=%u\r\n", stream_name, result == XFER_RESULT_SUCCESS ? "complete" : "failed", + result); + if (result == XFER_RESULT_SUCCESS) { + return; + } + } else if (event == TUH_AUDIO_EVENT_STOP_COMPLETE) { + printf(" %s stop %s: result=%u\r\n", stream_name, result == XFER_RESULT_SUCCESS ? "complete" : "failed", + result); + return; + } else { + printf(" %s transfer failed: result=%u\r\n", stream_name, result); + } + + fail_event_count++; if (stream_idx == cap_stream_idx) { mic_ready = false; } else if (stream_idx == spk_stream_idx) { spk_ready = false; } - printf(" AUDIO stream error: idx=%u stream=%u xferred_bytes=%u\r\n", idx, stream_idx, (unsigned)xferred_bytes); app_defer_ms_async(100, (app_defer_func_t)audio_app_restart_stream, ((uintptr_t)idx << 8) | stream_idx); } diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 432109022..00d01b391 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -21,9 +21,9 @@ * While a stream is running, the driver keeps one isochronous transfer in * flight and submits the next transfer from its completion callback. A FIFO * decouples application I/O from the endpoint's polling cadence; only whole - * audio frames are queued or transferred. Completion is reported through - * tuh_audio_capture_cb()/tuh_audio_playback_cb(); failures are reported through - * tuh_audio_err_cb(). + * audio frames are queued or transferred. Successful packets are reported + * through tuh_audio_capture_cb()/tuh_audio_playback_cb(); stream operation and + * transport results are reported through tuh_audio_event_cb(). * * Configurations are exposed as a flat list of discrete format, sample-rate, * and channel-count tuples. Internally, configurations are grouped by @@ -60,6 +60,12 @@ enum { STREAM_STATE_READY // Configured and ready to start. }; +enum { + AUDIOH_STREAM_OP_NONE = 0, + AUDIOH_STREAM_OP_START, + AUDIOH_STREAM_OP_STOP +}; + enum { AUDIOH_CTRL_NONE = 0, AUDIOH_CTRL_READ = 1, @@ -164,6 +170,7 @@ typedef struct { uint8_t active_as; uint8_t active_rate; uint8_t state; + uint8_t operation; bool running; // Directly associated Feature Unit, or zero when none is usable. @@ -254,10 +261,12 @@ TU_ATTR_WEAK void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_ (void)xferred_bytes; } -TU_ATTR_WEAK void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { +TU_ATTR_WEAK void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event, + tusb_xfer_result_t result) { (void)idx; (void)stream_idx; - (void)xferred_bytes; + (void)event; + (void)result; } //--------------------------------------------------------------------+ @@ -470,36 +479,31 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) // PACKET SCHEDULER //--------------------------------------------------------------------+ -static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes); +static void audioh_stream_xfer_failed(tuh_audio_stream_t *s, tusb_xfer_result_t result); -static void audioh_stream_feedback_xfer(tuh_audio_stream_t *s) { - TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); +static bool audioh_stream_feedback_xfer(tuh_audio_stream_t *s) { + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, false); const audioh_feedback_ep_t *feedback = &audioh_get_playback(s)->feedback[s->active_as]; - TU_VERIFY(feedback->ep_addr != 0, ); - TU_VERIFY(usbh_edpt_claim(s->daddr, feedback->ep_addr), ); - if (!usbh_edpt_xfer(s->daddr, feedback->ep_addr, _audioh_epbuf[s->idx].feedback, feedback->ep_size)) { - audioh_stream_error(s, 0); - } + TU_VERIFY(feedback->ep_addr != 0, false); + TU_VERIFY(usbh_edpt_claim(s->daddr, feedback->ep_addr), false); + return usbh_edpt_xfer(s->daddr, feedback->ep_addr, _audioh_epbuf[s->idx].feedback, feedback->ep_size); } -static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { - TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); +static bool audioh_stream_capture_xfer(tuh_audio_stream_t *s) { + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, false); const audioh_as_config_t *as = audioh_stream_active_as(s); - TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), ); - - if (!usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, as->ep_size)) { - audioh_stream_error(s, 0); - } + TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), false); + return usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, as->ep_size); } -static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { - TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); +static bool audioh_stream_playback_xfer(tuh_audio_stream_t *s) { + TU_VERIFY(s->state == STREAM_STATE_READY && s->running, false); const audioh_as_config_t *as = audioh_stream_active_as(s); audioh_playback_t *playback = audioh_get_playback(s); - TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), ); + TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), false); // Accumulate fractional frames across endpoint intervals so packet lengths // average to the active nominal or feedback rate. @@ -515,7 +519,7 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { const uint64_t bytes_64 = (uint64_t)frames * s->frame_bytes; TU_ASSERT(bytes_64 <= as->ep_size && bytes_64 <= CFG_TUH_AUDIO_EPOUT_BUFSIZE && - bytes_64 <= CFG_TUH_AUDIO_STREAM_BUFSIZE, ); + bytes_64 <= CFG_TUH_AUDIO_STREAM_BUFSIZE, false); const uint16_t bytes = (uint16_t)bytes_64; if (tu_fifo_count(&s->edpt.ff) < bytes) { // Isochronous OUT must continue at every interval. Send silence until a @@ -526,8 +530,7 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { } if (!usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, bytes)) { - audioh_stream_error(s, 0); - return; + return false; } playback->rem_acc = (uint16_t)next_rem_acc; if (loop_done && playback->feedback_pending) { @@ -536,6 +539,7 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { // Preserve the accumulator at the wrap boundary. Resetting it for each // feedback update would bias the average toward integer packet lengths. } + return true; } //--------------------------------------------------------------------+ @@ -573,10 +577,11 @@ static void audioh_stream_fail(tuh_audio_stream_t *s) { s->active_config = TUSB_INDEX_INVALID_8; s->active_as = TUSB_INDEX_INVALID_8; s->active_rate = TUSB_INDEX_INVALID_8; + s->operation = AUDIOH_STREAM_OP_NONE; s->running = false; } -static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes) { +static void audioh_stream_stop_xfers(tuh_audio_stream_t *s) { s->running = false; if (s->dir == TUSB_DIR_OUT) { audioh_playback_t *playback = audioh_get_playback(s); @@ -585,7 +590,11 @@ static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes) { playback->rem_acc = 0; } tu_edpt_stream_clear(&s->edpt); - tuh_audio_err_cb(s->idx, s->stream_idx, xferred_bytes); +} + +static void audioh_stream_xfer_failed(tuh_audio_stream_t *s, tusb_xfer_result_t result) { + audioh_stream_stop_xfers(s); + tuh_audio_event_cb(s->idx, s->stream_idx, TUH_AUDIO_EVENT_XFER_FAILED, result); } static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete_cb) { @@ -796,22 +805,26 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin return false; } - // Failed, stalled, and aborted transfers do not carry valid audio data. - if (result != XFER_RESULT_SUCCESS) { - TU_LOG_DRV(" AUDIO transfer failed: addr=%u ep=%02x result=%u\r\n", dev_addr, ep_addr, result); - audioh_stream_error(s, (uint16_t)xferred_bytes); + // Stopping one endpoint does not cancel every transfer that may already be + // in flight (for example, a playback data and feedback pair). Ignore those + // completions after the stream has stopped. + if (!s->running) { return true; } - // A stop does not cancel the transfer that was already in flight. - if (!s->running) { + // Failed, stalled, and aborted transfers do not carry valid audio data. + if (result != XFER_RESULT_SUCCESS) { + TU_LOG_DRV(" AUDIO transfer failed: addr=%u ep=%02x result=%u\r\n", dev_addr, ep_addr, result); + audioh_stream_xfer_failed(s, (tusb_xfer_result_t)result); return true; } const uint8_t feedback_ep = audioh_get_playback(s)->feedback[s->active_as].ep_addr; if (s->dir == TUSB_DIR_OUT && feedback_ep != 0 && ep_addr == feedback_ep) { audioh_feedback_received(s, xferred_bytes); - audioh_stream_feedback_xfer(s); + if (!audioh_stream_feedback_xfer(s)) { + audioh_stream_xfer_failed(s, XFER_RESULT_FAILED); + } return true; } @@ -822,11 +835,15 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin tu_fifo_write_n(&s->edpt.ff, s->edpt.ep_buf, bytes); } tuh_audio_capture_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); - audioh_stream_capture_xfer(s); + if (s->running && !audioh_stream_capture_xfer(s)) { + audioh_stream_xfer_failed(s, XFER_RESULT_FAILED); + } } else { // Notify the application before requesting the next playback packet. tuh_audio_playback_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes); - audioh_stream_playback_xfer(s); + if (s->running && !audioh_stream_playback_xfer(s)) { + audioh_stream_xfer_failed(s, XFER_RESULT_FAILED); + } } return true; } @@ -1936,18 +1953,28 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // Start endpoint transfers after the alternate setting and sampling frequency // are both active. -static void audioh_stream_start_xfer(tuh_audio_stream_t *s) { +static bool audioh_stream_start_xfer(tuh_audio_stream_t *s) { if (s->dir == TUSB_DIR_IN) { - audioh_stream_capture_xfer(s); + return audioh_stream_capture_xfer(s); } else { if (audioh_get_playback(s)->feedback[s->active_as].ep_addr != 0) { - audioh_stream_feedback_xfer(s); + TU_VERIFY(audioh_stream_feedback_xfer(s), false); } - if (!s->running) { - return; - } - audioh_stream_playback_xfer(s); + return audioh_stream_playback_xfer(s); + } +} + +static void audioh_stream_start_done(tuh_audio_stream_t *s, tusb_xfer_result_t result) { + if (result != XFER_RESULT_SUCCESS) { + audioh_stream_stop_xfers(s); } + s->operation = AUDIOH_STREAM_OP_NONE; + tuh_audio_event_cb(s->idx, s->stream_idx, TUH_AUDIO_EVENT_START_COMPLETE, result); +} + +static void audioh_stream_start_xfers(tuh_audio_stream_t *s) { + const tusb_xfer_result_t result = audioh_stream_start_xfer(s) ? XFER_RESULT_SUCCESS : XFER_RESULT_FAILED; + audioh_stream_start_done(s, result); } static void audioh_stream_start_complete(tuh_xfer_t *xfer); @@ -1965,37 +1992,36 @@ static void audioh_stream_start_set_freq_complete(tuh_xfer_t *xfer) { } if (xfer->result != XFER_RESULT_SUCCESS) { TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); - audioh_stream_error(s, 0); + audioh_stream_start_done(s, xfer->result); return; } #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 if (_audioh_itf[s->idx].protocol == AUDIO_INT_PROTOCOL_CODE_V2) { if (!audioh_stream_activate(s)) { - audioh_stream_error(s, 0); + audioh_stream_start_done(s, XFER_RESULT_FAILED); } } else #endif { - audioh_stream_start_xfer(s); + audioh_stream_start_xfers(s); } } -static bool audioh_stream_start_active(tuh_audio_stream_t *s) { +static void audioh_stream_start_active(tuh_audio_stream_t *s) { #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 const audioh_as_config_t *as = audioh_stream_active_as(s); const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); if (_audioh_itf[s->idx].protocol == AUDIO_INT_PROTOCOL_CODE_V1 && rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) { if (!audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete)) { - audioh_stream_error(s, 0); - return false; + audioh_stream_start_done(s, XFER_RESULT_FAILED); } + return; } else #endif { - audioh_stream_start_xfer(s); + audioh_stream_start_xfers(s); } - return true; } static void audioh_stream_start_complete(tuh_xfer_t *xfer) { @@ -2006,10 +2032,10 @@ static void audioh_stream_start_complete(tuh_xfer_t *xfer) { } if (xfer->result != XFER_RESULT_SUCCESS) { TU_LOG_DRV(" AUDIO SET_INTERFACE activate failed: result=%u\r\n", xfer->result); - audioh_stream_error(s, 0); + audioh_stream_start_done(s, xfer->result); return; } - (void)audioh_stream_start_active(s); + audioh_stream_start_active(s); } bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { @@ -2019,7 +2045,7 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s, false); - TU_VERIFY(s->state == STREAM_STATE_READY && !s->running, false); + TU_VERIFY(s->state == STREAM_STATE_READY && s->operation == AUDIOH_STREAM_OP_NONE && !s->running, false); // A stopped transfer must drain before the endpoint can be restarted. const audioh_as_config_t *as = audioh_stream_active_as(s); const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); @@ -2047,7 +2073,8 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { p_audio->playback.feedback_pending = false; p_audio->playback.rem_acc = 0; } - s->running = true; + s->running = true; + s->operation = AUDIOH_STREAM_OP_START; // UAC2 controls a Clock Source that exists before endpoint activation. UAC1 // controls the endpoint itself, so its alternate setting must be active first. bool submitted = false; @@ -2060,7 +2087,8 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { submitted = audioh_stream_activate(s); } if (!submitted) { - s->running = false; + s->operation = AUDIOH_STREAM_OP_NONE; + s->running = false; return false; } return true; @@ -2068,10 +2096,12 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { static void audioh_stream_stop_complete(tuh_xfer_t *xfer) { tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data; - if (s->daddr != xfer->daddr) { + if (s->daddr != xfer->daddr || s->operation != AUDIOH_STREAM_OP_STOP) { return; } TU_LOG_DRV(" AUDIO SET_INTERFACE deactivate done: result=%u\r\n", xfer->result); + s->operation = AUDIOH_STREAM_OP_NONE; + tuh_audio_event_cb(s->idx, s->stream_idx, TUH_AUDIO_EVENT_STOP_COMPLETE, xfer->result); } bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { @@ -2080,7 +2110,7 @@ bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); - TU_VERIFY(s && s->state == STREAM_STATE_READY, false); + TU_VERIFY(s && s->state == STREAM_STATE_READY && s->operation == AUDIOH_STREAM_OP_NONE && s->running, false); const audioh_as_config_t *as = audioh_stream_active_as(s); // Preserve running state when submission fails so the caller can retry. @@ -2088,13 +2118,8 @@ bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { // SET_INTERFACE stops future traffic. The current transfer drains, while its // data and all queued frames are discarded. - s->running = false; - if (s->dir == TUSB_DIR_OUT) { - p_audio->playback.target_frames_q16 = p_audio->playback.nominal_frames_q16; - p_audio->playback.feedback_pending = false; - p_audio->playback.rem_acc = 0; - } - tu_edpt_stream_clear(&s->edpt); + s->operation = AUDIOH_STREAM_OP_STOP; + audioh_stream_stop_xfers(s); return true; } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index f83d7ea63..3cb1bb00c 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -84,6 +84,13 @@ typedef enum { TUH_AUDIO_STREAM_DIRECTION_COUNT } tuh_audio_direction_t; +// Asynchronous stream operation and transport events. +typedef enum { + TUH_AUDIO_EVENT_START_COMPLETE = 0, + TUH_AUDIO_EVENT_STOP_COMPLETE, + TUH_AUDIO_EVENT_XFER_FAILED +} tuh_audio_event_t; + // Discrete Type-I PCM sample format. UAC1 requires bSamFreqType > 0; UAC2 // configurations are built from the directly connected Clock Source RANGE. typedef enum { @@ -173,10 +180,13 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // UAC1 activates the alternate setting before setting an endpoint frequency; // UAC2 sets a writable Clock Source before activating the alternate setting. // Startup is asynchronous: true means that the first request was submitted. -// Later startup failures are reported through tuh_audio_err_cb(). +// Completion is reported through tuh_audio_event_cb(); no event is emitted +// when this function returns false. bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx); // Stop transferring and asynchronously deactivate the Audio Streaming // interface (alt 0). true means that the deactivation request was submitted. +// Completion is reported through tuh_audio_event_cb(); no event is emitted +// when this function returns false. bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx); // Frame-based transfer. One frame = channels * bytes per sample. @@ -310,10 +320,13 @@ void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_byte // from the stream FIFO, or silence when a complete packet is unavailable. void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); -// Invoked when asynchronous stream activation or an isochronous transfer -// fails. The stream is stopped (tuh_audio_start() must be called again to -// resume). xferred_bytes is zero for an activation failure. -void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); +// Reports completion of asynchronous start/stop operations and unrecoverable +// transfer failures. START_COMPLETE is emitted after the complete activation +// sequence and initial endpoint transfers are submitted. XFER_FAILED means the +// HCD could not submit a transfer or completed it unsuccessfully; it is not a +// notification for an individual dropped isochronous packet. The driver stops +// the stream before reporting START_COMPLETE failure or XFER_FAILED. +void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event, tusb_xfer_result_t result); //--------------------------------------------------------------------+ // Internal Class Driver API diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index 2ede6b662..05b699b68 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -63,10 +63,11 @@ static uint8_t *edpt_xfer_buffer[AUDIO_TEST_MAX_XFERS]; static uint8_t edpt_xfer_count; static tusb_speed_t test_speed; -static uint8_t err_cb_count; -static uint8_t err_cb_idx; -static uint8_t err_cb_stream_idx; -static uint16_t err_cb_xferred_bytes; +static uint8_t event_cb_count; +static uint8_t event_cb_idx; +static uint8_t event_cb_stream_idx; +static tuh_audio_event_t event_cb_event; +static tusb_xfer_result_t event_cb_result; static uint8_t descriptor_cb_count; static uint8_t descriptor_cb_idx; @@ -84,11 +85,12 @@ static void complete_edpt(uint8_t ep_addr) { edpt_busy_mask &= ~edpt_mask(ep_addr); } -void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { - err_cb_count++; - err_cb_idx = idx; - err_cb_stream_idx = stream_idx; - err_cb_xferred_bytes = xferred_bytes; +void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event, tusb_xfer_result_t result) { + event_cb_count++; + event_cb_idx = idx; + event_cb_stream_idx = stream_idx; + event_cb_event = event; + event_cb_result = result; } void tuh_audio_descriptor_cb(uint8_t idx, const tuh_audio_descriptor_cb_t *desc_cb) { @@ -893,10 +895,11 @@ void setUp(void) { memset(edpt_xfer_buffer, 0, sizeof(edpt_xfer_buffer)); edpt_xfer_count = 0; - err_cb_count = 0; - err_cb_idx = TUSB_INDEX_INVALID_8; - err_cb_stream_idx = TUSB_INDEX_INVALID_8; - err_cb_xferred_bytes = 0; + event_cb_count = 0; + event_cb_idx = TUSB_INDEX_INVALID_8; + event_cb_stream_idx = TUSB_INDEX_INVALID_8; + event_cb_event = TUH_AUDIO_EVENT_START_COMPLETE; + event_cb_result = XFER_RESULT_INVALID; descriptor_cb_count = 0; descriptor_cb_idx = TUSB_INDEX_INVALID_8; @@ -1332,20 +1335,45 @@ void test_audio_host_reports_asynchronous_start_failures(void) { TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); complete_interface_set(XFER_RESULT_FAILED); - TEST_ASSERT_EQUAL_UINT8(1, err_cb_count); - TEST_ASSERT_EQUAL_UINT8(0, err_cb_idx); - TEST_ASSERT_EQUAL_UINT8(0, err_cb_stream_idx); - TEST_ASSERT_EQUAL_UINT16(0, err_cb_xferred_bytes); + TEST_ASSERT_EQUAL_UINT8(1, event_cb_count); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_idx); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_stream_idx); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_START_COMPLETE, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_FAILED, event_cb_result); TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); complete_interface_set(XFER_RESULT_SUCCESS); complete_control_xfer(XFER_RESULT_STALLED); - TEST_ASSERT_EQUAL_UINT8(2, err_cb_count); + TEST_ASSERT_EQUAL_UINT8(2, event_cb_count); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_START_COMPLETE, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_STALLED, event_cb_result); control_xfer_result = false; TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); complete_interface_set(XFER_RESULT_SUCCESS); - TEST_ASSERT_EQUAL_UINT8(3, err_cb_count); + TEST_ASSERT_EQUAL_UINT8(3, event_cb_count); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_START_COMPLETE, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_FAILED, event_cb_result); +} + +void test_audio_host_reports_asynchronous_start_and_stop_completion(void) { + mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_count); + complete_interface_set(XFER_RESULT_SUCCESS); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, event_cb_count); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_START_COMPLETE, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_SUCCESS, event_cb_result); + + TEST_ASSERT_TRUE(tuh_audio_stop(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, event_cb_count); + complete_interface_set(XFER_RESULT_STALLED); + TEST_ASSERT_EQUAL_UINT8(2, event_cb_count); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_STOP_COMPLETE, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_STALLED, event_cb_result); } void test_audio_host_reports_capture_submission_failure(void) { @@ -1357,10 +1385,11 @@ void test_audio_host_reports_capture_submission_failure(void) { complete_interface_set(XFER_RESULT_SUCCESS); complete_control_xfer(XFER_RESULT_SUCCESS); - TEST_ASSERT_EQUAL_UINT8(1, err_cb_count); - TEST_ASSERT_EQUAL_UINT8(0, err_cb_idx); - TEST_ASSERT_EQUAL_UINT8(0, err_cb_stream_idx); - TEST_ASSERT_EQUAL_UINT16(0, err_cb_xferred_bytes); + TEST_ASSERT_EQUAL_UINT8(1, event_cb_count); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_idx); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_stream_idx); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_START_COMPLETE, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_FAILED, event_cb_result); edpt_xfer_result = true; TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); @@ -1374,15 +1403,33 @@ void test_audio_host_reports_playback_submission_failure(void) { TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); complete_interface_set(XFER_RESULT_SUCCESS); - TEST_ASSERT_EQUAL_UINT8(1, err_cb_count); - TEST_ASSERT_EQUAL_UINT8(0, err_cb_idx); - TEST_ASSERT_EQUAL_UINT8(0, err_cb_stream_idx); - TEST_ASSERT_EQUAL_UINT16(0, err_cb_xferred_bytes); + TEST_ASSERT_EQUAL_UINT8(1, event_cb_count); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_idx); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_stream_idx); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_START_COMPLETE, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_FAILED, event_cb_result); edpt_xfer_result = true; TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); } +void test_audio_host_reports_runtime_transfer_failure_without_byte_count(void) { + mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, event_cb_count); + + complete_edpt(0x81); + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_TIMEOUT, 37)); + TEST_ASSERT_EQUAL_UINT8(2, event_cb_count); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_idx); + TEST_ASSERT_EQUAL_UINT8(0, event_cb_stream_idx); + TEST_ASSERT_EQUAL(TUH_AUDIO_EVENT_XFER_FAILED, event_cb_event); + TEST_ASSERT_EQUAL(XFER_RESULT_TIMEOUT, event_cb_result); +} + void test_audio_host_keeps_running_when_stop_cannot_be_submitted(void) { mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); -- cgit v1.3.1 From 1ff684a07ec5d80a89ba82276139a202288b7716 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 2 Sep 2026 15:22:07 +0200 Subject: implement per channel volume control Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 16 +- examples/host/audio_host/src/audio_app.c | 26 +- hw/bsp/stm32f7/boards/stm32f723disco/board.h | 6 +- src/class/audio/audio_host.c | 313 +++++++++++++++-------- src/class/audio/audio_host.h | 33 ++- test/unit-test/test/host/audio/test_audio_host.c | 141 +++++++++- 6 files changed, 391 insertions(+), 144 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 3d4307b42..7f72aabaf 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -6,7 +6,7 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) - Enumerates and mounts USB Audio Class 1.0 and 2.0 devices - Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) -- Reports each stream's master mute/volume capabilities and cached volume range +- Reports each stream's mute/volume capabilities and cached volume range - Configures and starts an S16_LE capture stream (44.1 kHz preferred, 48 kHz fallback; stereo preferred, mono accepted) - Echoes captured audio to an S16_LE playback stream at the same sample rate (same channel count preferred, mono/stereo conversion otherwise) - Frame-based FIFO API: the main loop reads capture when its FIFO is half full and fills playback when its FIFO is half drained; USB transfer callbacks are not used for FIFO servicing @@ -28,7 +28,7 @@ The echo needs a matching S16_LE playback stream at the capture sample rate; dev - UAC1 Type I Format descriptors with `bSamFreqType == 0` are unsupported; the driver requires a list of discrete sampling frequencies. - UAC2 supports direct Clock Sources. Clock Selectors, Clock Multipliers, Sampling Rate Converters, Clock Validity, and Valid Alternate Settings controls are not handled. - UAC2 sampling-frequency RANGE responses are expanded into at most `CFG_TUH_AUDIO_MAX_SAM_FREQ` discrete configurations. A read-only Clock Source exposes only its current frequency. -- Master mute and volume controls are discovered before the mount callback, including the volume range. Feature Units without master mute or volume are ignored. UAC2 volume discovery supports the common RANGE response containing one subrange. +- Master mute and volume controls are discovered before the mount callback. A Feature Unit with volume only on its logical channels is also supported: the range is read from the first controlled channel, and a stream-volume SET writes every logical channel when no writable master control exists. The typed API assumes all logical channels share one range; applications needing different per-channel ranges can use the raw control API. UAC2 volume discovery supports the common RANGE response containing one subrange. - The `MaxPacketsOnly` endpoint attribute is not supported. OUT transfers are not padded to `wMaxPacketSize`, and padding in IN transfers is not removed from the reported audio data. ## Building @@ -68,10 +68,10 @@ make BOARD= flash 2. Connect a USB Audio device (UAC 1.0 or 2.0) to the USB host port 3. Open a serial terminal to view output 4. The example will: - - Print each stream's master mute/volume capabilities, cached volume range, and supported configurations when mounted + - Print each stream's mute/volume capabilities, cached volume range, and supported configurations when mounted - Look for an S16_LE capture configuration at a preferred sample rate (44.1 kHz first, 48 kHz fallback; stereo preferred, mono accepted) and configure it - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) - - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB + - Read/unmute the microphone and speaker Feature Units and set supported stream volumes near -6 dB; a channel-only Feature Unit is updated one logical channel at a time - Service both FIFOs from `audio_app_task()` at their half-full/half-drained watermarks; a sine test tone plays on the playback stream when no capture stream is echoing - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; their asynchronous results are printed from `tuh_audio_event_cb()`, and a failed stream is restarted automatically after 100 ms @@ -83,24 +83,24 @@ Connect a USB Audio Device (UAC 1.0 or 2.0) to test Audio device mounted: idx=0 addr=1 capture stream 1, configurations: 2 master mute supported - master volume range: min=-23040 max=1536 res=256 (1/256 dB) + volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 playback stream 0, configurations: 2 master mute supported - master volume range: min=-23040 max=1536 res=256 (1/256 dB) + volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 Configuring 44100 S16_LE capture (2 channels) Microphone configured Microphone master mute: off Microphone master volume: 0 (1/256 dB) - Microphone master volume set: -1536 (1/256 dB) + Microphone volume set: -1536 (1/256 dB) Configuring 44100 S16_LE playback (2 channels) Speaker configured Speaker master mute: off Speaker master volume: 0 (1/256 dB) - Speaker master volume set: -1536 (1/256 dB) + Speaker volume set: -1536 (1/256 dB) ``` ## Configuration diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 55efe3295..749274f44 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -365,7 +365,7 @@ static void print_stream_configs(uint8_t idx, uint8_t stream_idx) { printf(" master mute supported\r\n"); } if (tuh_audio_volume_range_get(idx, stream_idx, &range)) { - printf(" master volume range: min=%d max=%d res=%u (1/256 dB)\r\n", (int)range.min, (int)range.max, + printf(" volume range: min=%d max=%d res=%u (1/256 dB)\r\n", (int)range.min, (int)range.max, (unsigned)range.res); } for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) { @@ -397,26 +397,26 @@ static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const cha if (tuh_audio_volume_range_get(idx, stream_idx, &range)) { has_control = true; int16_t volume; - tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, &volume); + tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, 0, &volume); if (result == XFER_RESULT_SUCCESS) { printf(" %s master volume: %d (1/256 dB)\r\n", stream_name, volume); - int32_t target = FEATURE_UNIT_VOLUME_DB; - target = TU_MAX(target, range.min); - target = TU_MIN(target, range.max); - target = range.min + ((target - range.min + range.res / 2) / range.res) * range.res; - target = TU_MIN(target, range.max); - result = tuh_audio_volume_set_sync(idx, stream_idx, (int16_t)target); - if (result == XFER_RESULT_SUCCESS) { - printf(" %s master volume set: %d (1/256 dB)\r\n", stream_name, (int)target); - } + } + int32_t target = FEATURE_UNIT_VOLUME_DB; + target = TU_MAX(target, range.min); + target = TU_MIN(target, range.max); + target = range.min + ((target - range.min + range.res / 2) / range.res) * range.res; + target = TU_MIN(target, range.max); + result = tuh_audio_volume_set_sync(idx, stream_idx, 0, (int16_t)target); + if (result == XFER_RESULT_SUCCESS) { + printf(" %s volume set: %d (1/256 dB)\r\n", stream_name, (int)target); } if (result != XFER_RESULT_SUCCESS) { - printf(" Accessing %s master volume failed: result=%u\r\n", stream_name, result); + printf(" Setting %s volume failed: result=%u\r\n", stream_name, result); } } if (!has_control) { - printf(" %s stream has no master mute/volume control\r\n", stream_name); + printf(" %s stream has no mute/volume control\r\n", stream_name); } } diff --git a/hw/bsp/stm32f7/boards/stm32f723disco/board.h b/hw/bsp/stm32f7/boards/stm32f723disco/board.h index 698a1a230..78942b884 100644 --- a/hw/bsp/stm32f7/boards/stm32f723disco/board.h +++ b/hw/bsp/stm32f7/boards/stm32f723disco/board.h @@ -103,9 +103,9 @@ static inline void board_clock_init(void) { RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM = HSE_VALUE / 1000000; - RCC_OscInitStruct.PLL.PLLN = 432; - RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; - RCC_OscInitStruct.PLL.PLLQ = 9; + RCC_OscInitStruct.PLL.PLLN = 288; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4; + RCC_OscInitStruct.PLL.PLLQ = 6; HAL_RCC_OscConfig(&RCC_OscInitStruct); /* Activate the OverDrive to reach the 216 MHz Frequency */ diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 5ad3ed960..d93ff535d 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -46,9 +46,10 @@ * * UAC1 rates come from each Format Type descriptor, whereas UAC2 rates are * queried from the Clock Sources referenced by the parsed topology. Feature - * Unit parsing records master mute access, and mount probing reads the master - * volume range. A device is reported as mounted only after these asynchronous - * probes finish. + * Unit parsing records master mute and master/logical-channel volume access. + * Mount probing reads the volume range from the master or first controlled + * logical channel. A device is reported as mounted only after these + * asynchronous probes finish. * * Stream configuration is local; stream activation is asynchronous: * @@ -198,6 +199,8 @@ typedef struct { struct { uint8_t width; uint8_t value_type; + uint8_t channel; + uint8_t last_channel; } control; struct { uint8_t stream_idx; @@ -239,7 +242,10 @@ typedef struct { // Directly associated Feature Unit, or zero when none is usable. uint8_t feature_unit_id; uint8_t mute_access; - uint8_t volume_access; + uint8_t volume_master_access; + uint8_t feature_unit_channels; + uint8_t volume_range_channel; + bool volume_all_channels_writable; tuh_audio_volume_range_t volume_range; // Bytes in one interleaved audio frame across all channels. @@ -453,10 +459,14 @@ static bool audioh_stream_config_get(const tuh_audio_stream_t *s, uint8_t config } static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_id, uint8_t mute_access, - uint8_t volume_access) { - s->feature_unit_id = unit_id; - s->mute_access = mute_access; - s->volume_access = volume_access; + uint8_t volume_master_access, uint8_t channels, uint8_t volume_range_channel, + bool volume_all_channels_writable) { + s->feature_unit_id = unit_id; + s->mute_access = mute_access; + s->volume_master_access = volume_master_access; + s->feature_unit_channels = channels; + s->volume_range_channel = volume_range_channel; + s->volume_all_channels_writable = volume_all_channels_writable; } static bool audioh_format_from_pcm(uint8_t subslot_size, uint8_t bit_resolution, tuh_audio_format_t *format) { @@ -492,20 +502,23 @@ static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_inter // Preserve the stream identity and FIFO allocation while clearing device state. static void audioh_stream_reset(tuh_audio_stream_t *s) { - s->daddr = 0; - s->stream_idx = TUSB_INDEX_INVALID_8; - s->as_count = 0; - s->config_count = 0; - s->active_config = TUSB_INDEX_INVALID_8; - s->active_as = TUSB_INDEX_INVALID_8; - s->active_rate = TUSB_INDEX_INVALID_8; - s->state = STREAM_STATE_IDLE; - s->running = false; - s->feature_unit_id = 0; - s->mute_access = AUDIOH_CTRL_NONE; - s->volume_access = AUDIOH_CTRL_NONE; - s->volume_range = (tuh_audio_volume_range_t){0}; - s->frame_bytes = 0; + s->daddr = 0; + s->stream_idx = TUSB_INDEX_INVALID_8; + s->as_count = 0; + s->config_count = 0; + s->active_config = TUSB_INDEX_INVALID_8; + s->active_as = TUSB_INDEX_INVALID_8; + s->active_rate = TUSB_INDEX_INVALID_8; + s->state = STREAM_STATE_IDLE; + s->running = false; + s->feature_unit_id = 0; + s->mute_access = AUDIOH_CTRL_NONE; + s->volume_master_access = AUDIOH_CTRL_NONE; + s->feature_unit_channels = 0; + s->volume_range_channel = TUSB_INDEX_INVALID_8; + s->volume_all_channels_writable = false; + s->volume_range = (tuh_audio_volume_range_t){0}; + s->frame_bytes = 0; tu_edpt_stream_close(&s->edpt); tu_edpt_stream_clear(&s->edpt); } @@ -905,7 +918,10 @@ typedef struct { uint8_t id; uint8_t source_id; uint8_t mute_access; - uint8_t volume_access; + uint8_t volume_master_access; + uint8_t channels; + uint8_t volume_range_channel; + bool volume_all_channels_writable; } audioh_fu_info_t; typedef struct { @@ -949,8 +965,9 @@ static bool audioh_ac_entity_valid(const audioh_interface_t *p_audio, const uint case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 7), false); const uint8_t control_size = p_desc[5]; - return TUH_VALIDATE_BASIC(control_size > 0) && - TUH_VALIDATE_BASIC(control_size <= (uint8_t)(tu_desc_len(p_desc) - 7)); + const uint8_t control_bytes = (uint8_t)(tu_desc_len(p_desc) - 7); + return TUH_VALIDATE_BASIC(control_size > 0) && TUH_VALIDATE_BASIC(control_size <= control_bytes) && + TUH_VALIDATE_BASIC(control_bytes % control_size == 0); } default: return true; @@ -965,7 +982,8 @@ static bool audioh_ac_entity_valid(const audioh_interface_t *p_audio, const uint case AUDIO20_CS_AC_INTERFACE_OUTPUT_TERMINAL: return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_output_terminal_t)); case AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT: - return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 10); + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 10) && + TUH_VALIDATE_BASIC((tu_desc_len(p_desc) - 6u) % 4u == 0); case AUDIO20_CS_AC_INTERFACE_CLOCK_SOURCE: return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_clock_source_t)); default: @@ -1033,39 +1051,72 @@ static bool audioh_ac_feature_unit_parse(const audioh_interface_t *p_audio, cons if (tu_desc_type(p_desc) != TUSB_DESC_CS_INTERFACE) { return false; } + + uint8_t control_offset; + uint8_t control_size; + uint8_t channels; + uint8_t mute_access; switch (p_audio->protocol) { #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 case AUDIO_INT_PROTOCOL_CODE_V1: - if (tu_desc_subtype(p_desc) == AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT) { - const uint8_t controls = p_desc[6]; - *info = (audioh_fu_info_t){.id = p_desc[3], - .source_id = p_desc[4], - .mute_access = (controls & AUDIO10_FU_CONTROL_BM_MUTE) ? AUDIOH_CTRL_READ_WRITE - : AUDIOH_CTRL_NONE, - .volume_access = (controls & AUDIO10_FU_CONTROL_BM_VOLUME) ? AUDIOH_CTRL_READ_WRITE - : AUDIOH_CTRL_NONE}; - return info->mute_access != AUDIOH_CTRL_NONE || info->volume_access != AUDIOH_CTRL_NONE; + if (tu_desc_subtype(p_desc) != AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT) { + return false; } + control_offset = 6; + control_size = p_desc[5]; + channels = (uint8_t)((tu_desc_len(p_desc) - 7u) / control_size - 1u); + mute_access = (p_desc[control_offset] & AUDIO10_FU_CONTROL_BM_MUTE) ? AUDIOH_CTRL_READ_WRITE : AUDIOH_CTRL_NONE; break; #endif #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 case AUDIO_INT_PROTOCOL_CODE_V2: - if (tu_desc_subtype(p_desc) == AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT) { - const uint32_t controls = tu_le32toh(tu_unaligned_read32(&p_desc[5])); - *info = - (audioh_fu_info_t){.id = p_desc[3], - .source_id = p_desc[4], - .mute_access = audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_MUTE_POS), - .volume_access = - audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS)}; - return info->mute_access != AUDIOH_CTRL_NONE || info->volume_access != AUDIOH_CTRL_NONE; + if (tu_desc_subtype(p_desc) != AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT) { + return false; } + control_offset = 5; + control_size = 4; + channels = (uint8_t)((tu_desc_len(p_desc) - 6u) / control_size - 1u); + mute_access = audioh_uac2_control_access(tu_le32toh(tu_unaligned_read32(&p_desc[control_offset])), + AUDIO20_FEATURE_UNIT_CTRL_MUTE_POS); break; #endif default: - break; + return false; } - return false; + + uint8_t volume_master_access = AUDIOH_CTRL_NONE; + uint8_t volume_range_channel = TUSB_INDEX_INVALID_8; + bool volume_all_channels_writable = channels > 0; + for (uint8_t channel = 0; channel <= channels; channel++) { + uint8_t access; + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2) { + const uint32_t controls = tu_le32toh(tu_unaligned_read32(&p_desc[control_offset + channel * control_size])); + access = audioh_uac2_control_access(controls, AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS); + } else + #endif + { + access = (p_desc[control_offset + channel * control_size] & AUDIO10_FU_CONTROL_BM_VOLUME) ? AUDIOH_CTRL_READ_WRITE + : AUDIOH_CTRL_NONE; + } + if (channel == 0) { + volume_master_access = access; + } else { + volume_all_channels_writable &= access == AUDIOH_CTRL_READ_WRITE; + } + if (access != AUDIOH_CTRL_NONE && volume_range_channel == TUSB_INDEX_INVALID_8) { + volume_range_channel = channel; + } + } + + *info = (audioh_fu_info_t){.id = p_desc[3], + .source_id = p_desc[4], + .mute_access = mute_access, + .volume_master_access = volume_master_access, + .channels = channels, + .volume_range_channel = volume_range_channel, + .volume_all_channels_writable = volume_all_channels_writable}; + return mute_access != AUDIOH_CTRL_NONE || volume_range_channel != TUSB_INDEX_INVALID_8; } #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 @@ -1103,7 +1154,8 @@ static void audioh_link_feature_units(audioh_interface_t *p_audio, const audioh_ } const bool linked = (direction == TUSB_DIR_OUT) ? (fu.source_id == terminal.id) : (fu.id == terminal.source_id); if (linked) { - audioh_stream_set_feature_unit(stream, fu.id, fu.mute_access, fu.volume_access); + audioh_stream_set_feature_unit(stream, fu.id, fu.mute_access, fu.volume_master_access, fu.channels, + fu.volume_range_channel, fu.volume_all_channels_writable); break; } } @@ -1869,7 +1921,7 @@ bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volum audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); - TU_VERIFY(s != NULL && s->volume_access != AUDIOH_CTRL_NONE, false); + TU_VERIFY(s != NULL && s->volume_range_channel != TUSB_INDEX_INVALID_8, false); *range = s->volume_range; return true; } @@ -2216,21 +2268,7 @@ uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { // AUDIO CONTROL REQUESTS //--------------------------------------------------------------------+ -// Release driver-owned request state before invoking the application callback, -// allowing the callback to submit another Feature Unit request immediately. -static void audioh_fu_set_complete(tuh_xfer_t *xfer) { - const uint8_t idx = (uint8_t)xfer->user_data; - audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl; - tuh_xfer_cb_t app_cb = ctrl->complete_cb; - uintptr_t user_data = ctrl->user_data; - ctrl->complete_cb = NULL; - ctrl->fu_busy = false; - - xfer->user_data = user_data; - if (app_cb != NULL) { - app_cb(xfer); - } -} +static void audioh_fu_set_complete(tuh_xfer_t *xfer); enum { AUDIOH_FU_VALUE_BOOL, @@ -2303,6 +2341,38 @@ tusb_xfer_result_t tuh_audio_control_xfer_sync(uint8_t idx, uint8_t entity_id, t return xfer.result; } +// Continue a master-volume request across logical channels. Driver-owned +// request state is released before the final application callback so another +// Feature Unit request can be submitted from that callback. +static void audioh_fu_set_complete(tuh_xfer_t *xfer) { + const uint8_t idx = (uint8_t)xfer->user_data; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + + if (xfer->result == XFER_RESULT_SUCCESS && ctrl->fu.control.channel < ctrl->fu.control.last_channel) { + tuh_audio_stream_t *s = (tuh_audio_stream_t *)ctrl->value; + ctrl->fu.control.channel++; + const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_OUT); + tuh_xfer_t next_xfer = {.complete_cb = audioh_fu_set_complete, .user_data = (uintptr_t)idx}; + if (audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, AUDIO10_FU_CTRL_VOLUME, + ctrl->fu.control.channel, audioh_fu_ctrl(&_audioh_epbuf[idx]), 2, &next_xfer)) { + return; + } + xfer->result = XFER_RESULT_FAILED; + } + + tuh_xfer_cb_t app_cb = ctrl->complete_cb; + uintptr_t user_data = ctrl->user_data; + ctrl->complete_cb = NULL; + ctrl->fu_busy = false; + ctrl->value = NULL; + + xfer->user_data = user_data; + if (app_cb != NULL) { + app_cb(xfer); + } +} + static void audioh_fu_value_store(audioh_ctrl_state_t *ctrl, audioh_epbuf_t *epbuf) { if (ctrl->fu.control.value_type == AUDIOH_FU_VALUE_BOOL) { *((bool *)ctrl->value) = audioh_fu_ctrl(epbuf)[0] != 0; @@ -2387,7 +2457,7 @@ static bool audioh_mount_feature_unit_submit(uint8_t idx) { const tusb_control_request_t request = { .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, .bRequest = uac2 ? AUDIO20_CS_REQ_RANGE : audioh_fu_volume_range_request(ctrl->fu.mount.range_step), - .wValue = tu_htole16(tu_u16(selector, 0)), + .wValue = tu_htole16(tu_u16(selector, s->volume_range_channel)), .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = tu_htole16(uac2 ? 8u : 2u), }; @@ -2406,14 +2476,16 @@ static void audioh_mount_feature_unit_next(uint8_t idx) { while (ctrl->fu.mount.stream_idx < p_audio->stream_count) { tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx); - if (s->volume_access != AUDIOH_CTRL_NONE) { + if (s->volume_range_channel != TUSB_INDEX_INVALID_8) { s->volume_range = (tuh_audio_volume_range_t){0}; ctrl->fu.mount.range_step = AUDIOH_VOLUME_RANGE_MIN; ctrl->fu_busy = true; if (audioh_mount_feature_unit_submit(idx)) { return; } - s->volume_access = AUDIOH_CTRL_NONE; + s->volume_master_access = AUDIOH_CTRL_NONE; + s->volume_range_channel = TUSB_INDEX_INVALID_8; + s->volume_all_channels_writable = false; if (s->mute_access == AUDIOH_CTRL_NONE) { s->feature_unit_id = 0; } @@ -2464,8 +2536,10 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { const uint32_t expected_len = uac2 ? 8u : 2u; if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != expected_len) { - s->volume_access = AUDIOH_CTRL_NONE; - s->volume_range = (tuh_audio_volume_range_t){0}; + s->volume_master_access = AUDIOH_CTRL_NONE; + s->volume_range_channel = TUSB_INDEX_INVALID_8; + s->volume_all_channels_writable = false; + s->volume_range = (tuh_audio_volume_range_t){0}; if (s->mute_access == AUDIOH_CTRL_NONE) { s->feature_unit_id = 0; } @@ -2475,17 +2549,24 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { audioh_mount_feature_unit_next(idx); } -static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint16_t value, uint8_t width, - tuh_xfer_cb_t complete_cb, uintptr_t user_data) { +static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, + uint8_t last_channel, uint16_t value, uint8_t width, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s && s->feature_unit_id != 0, false); if (control_selector == AUDIO10_FU_CTRL_MUTE) { - TU_VERIFY(s->mute_access == AUDIOH_CTRL_READ_WRITE, false); + TU_VERIFY(channel == 0 && last_channel == 0 && s->mute_access == AUDIOH_CTRL_READ_WRITE, false); } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) { - TU_VERIFY(s->volume_access == AUDIOH_CTRL_READ_WRITE, false); + TU_VERIFY(s->volume_range_channel != TUSB_INDEX_INVALID_8 && channel <= last_channel, false); + if (channel == 0) { + TU_VERIFY(last_channel == 0 && s->volume_master_access == AUDIOH_CTRL_READ_WRITE, false); + } else { + TU_VERIFY(last_channel <= s->feature_unit_channels, false); + TU_VERIFY(channel == last_channel || s->volume_all_channels_writable, false); + } } const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_OUT); @@ -2501,40 +2582,53 @@ static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selec val_buf[1] = (uint8_t)((value >> 8) & 0xFF); } - tuh_xfer_t xfer = {.complete_cb = complete_cb, .user_data = user_data}; - if (complete_cb == NULL) { - const bool result = audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, 0, - val_buf, width, &xfer); - ctrl->fu_busy = false; + bool result = true; + for (uint8_t current_channel = channel; current_channel <= last_channel; current_channel++) { + tuh_xfer_t xfer = {.complete_cb = NULL, .user_data = user_data}; + result = audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, + current_channel, val_buf, width, &xfer); + if (!result || xfer.result != XFER_RESULT_SUCCESS) { + break; + } + } + ctrl->fu_busy = false; return result; } - ctrl->complete_cb = complete_cb; - ctrl->user_data = user_data; - xfer.complete_cb = audioh_fu_set_complete; - xfer.user_data = (uintptr_t)idx; + ctrl->complete_cb = complete_cb; + ctrl->user_data = user_data; + ctrl->value = s; + ctrl->fu.control.channel = channel; + ctrl->fu.control.last_channel = last_channel; + tuh_xfer_t xfer = {.complete_cb = audioh_fu_set_complete, .user_data = (uintptr_t)idx}; - if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, 0, val_buf, width, - &xfer)) { + if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_OUT, request_code, control_selector, channel, val_buf, + width, &xfer)) { ctrl->complete_cb = NULL; + ctrl->value = NULL; ctrl->fu_busy = false; return false; } return true; } -static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, void *value, uint8_t width, - uint8_t value_type, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { +static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selector, uint8_t channel, void *value, + uint8_t width, uint8_t value_type, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); audioh_interface_t *p_audio = &_audioh_itf[idx]; TU_VERIFY(p_audio->mounted && value, false); tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s && s->feature_unit_id != 0, false); if (control_selector == AUDIO10_FU_CTRL_MUTE) { - TU_VERIFY(s->mute_access != AUDIOH_CTRL_NONE, false); + TU_VERIFY(channel == 0 && s->mute_access != AUDIOH_CTRL_NONE, false); } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) { - TU_VERIFY(s->volume_access != AUDIOH_CTRL_NONE, false); + TU_VERIFY(s->volume_range_channel != TUSB_INDEX_INVALID_8, false); + if (channel == 0) { + TU_VERIFY(s->volume_master_access != AUDIOH_CTRL_NONE, false); + } else { + TU_VERIFY(channel <= s->feature_unit_channels, false); + } } const uint8_t request_code = audioh_control_cur_request(p_audio->protocol, TUSB_DIR_IN); @@ -2550,7 +2644,7 @@ static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selec // The synchronous transfer completes before its driver-owned response is // converted to host order. tuh_xfer_t xfer = {.complete_cb = NULL, .user_data = user_data}; - if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, 0, + if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, channel, audioh_fu_ctrl(epbuf), width, &xfer)) { ctrl->fu_busy = false; return false; @@ -2569,7 +2663,7 @@ static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selec ctrl->user_data = user_data; tuh_xfer_t xfer = {.complete_cb = audioh_fu_get_complete, .user_data = (uintptr_t)idx}; - if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, 0, + if (!audioh_control_submit(idx, s->feature_unit_id, TUSB_DIR_IN, request_code, control_selector, channel, audioh_fu_ctrl(epbuf), width, &xfer)) { ctrl->complete_cb = NULL; ctrl->fu_busy = false; @@ -2579,33 +2673,52 @@ static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selec } bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, mute ? 1 : 0, 1, complete_cb, user_data); + return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, 0, mute ? 1 : 0, 1, complete_cb, user_data); } bool tuh_audio_mute_get(uint8_t idx, uint8_t stream_idx, bool *mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, mute, 1, AUDIOH_FU_VALUE_BOOL, complete_cb, user_data); + return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, 0, mute, 1, AUDIOH_FU_VALUE_BOOL, complete_cb, user_data); } -bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_xfer_cb_t complete_cb, - uintptr_t user_data) { +static bool audioh_volume_normalize(uint8_t idx, uint8_t stream_idx, int16_t *volume) { tuh_audio_volume_range_t range; TU_VERIFY(tuh_audio_volume_range_get(idx, stream_idx, &range), false); - if (volume != TUH_AUDIO_VOLUME_SILENCE) { - TU_VERIFY(volume >= range.min && volume <= range.max && range.res != 0, false); - const uint32_t offset = (uint32_t)((int32_t)volume - range.min); + if (*volume != TUH_AUDIO_VOLUME_SILENCE) { + TU_VERIFY(*volume >= range.min && *volume <= range.max && range.res != 0, false); + const uint32_t offset = (uint32_t)((int32_t)*volume - range.min); const uint32_t steps = (offset + range.res / 2u) / range.res; int32_t rounded = (int32_t)range.min + (int32_t)(steps * range.res); if (rounded > range.max) { rounded -= range.res; } - volume = (int16_t)rounded; + *volume = (int16_t)rounded; + } + return true; +} + +bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, uint8_t channel, int16_t volume, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + TU_VERIFY(audioh_volume_normalize(idx, stream_idx, &volume), false); + if (channel > 0) { + return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, channel, channel, (uint16_t)volume, 2, complete_cb, + user_data); + } + + audioh_interface_t *p_audio = &_audioh_itf[idx]; + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); + TU_VERIFY(s != NULL, false); + if (s->volume_master_access == AUDIOH_CTRL_READ_WRITE) { + return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, 0, (uint16_t)volume, 2, complete_cb, user_data); } - return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, (uint16_t)volume, 2, complete_cb, user_data); + TU_VERIFY(s->feature_unit_channels > 0 && s->volume_all_channels_writable, false); + return audioh_fu_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 1, s->feature_unit_channels, (uint16_t)volume, 2, + complete_cb, user_data); } -bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_xfer_cb_t complete_cb, +bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, uint8_t channel, int16_t *volume, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { - return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, volume, 2, AUDIOH_FU_VALUE_I16, complete_cb, user_data); + return audioh_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, channel, volume, 2, AUDIOH_FU_VALUE_I16, complete_cb, + user_data); } #endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 3cb1bb00c..9ce8410c6 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -240,7 +240,10 @@ bool tuh_audio_mounted(uint8_t idx); uint8_t tuh_audio_get_dev_addr(uint8_t idx); // True when the stream's Feature Unit supports master mute control. bool tuh_audio_mute_supported(uint8_t idx, uint8_t stream_idx); -// Get the cached master volume range. Returns false when volume is unsupported. +// Get the cached volume range. The driver reads the master channel when it +// supports volume, otherwise the first logical channel with volume control. +// This typed API assumes logical channels use the same range; applications +// needing per-channel ranges can use tuh_audio_control_xfer(). bool tuh_audio_volume_range_get(uint8_t idx, uint8_t stream_idx, tuh_audio_volume_range_t *range); //--------------------------------------------------------------------+ @@ -256,19 +259,29 @@ bool tuh_audio_control_xfer(uint8_t idx, uint8_t entity_id, tusb_dir_t direction tuh_xfer_cb_t complete_cb, uintptr_t user_data); // Master mute and volume controls. Capability and range information is cached -// before tuh_audio_mount_cb() is invoked. Volume SET accepts -// TUH_AUDIO_VOLUME_SILENCE or a value within the cached range; finite values -// are rounded to the nearest resolution step measured from the range minimum. +// before tuh_audio_mount_cb() is invoked. Volume channel 0 selects the master; +// a SET falls back to writing every logical channel when the master is not +// writable and all logical channels advertise write access. The completion +// callback is invoked once after the entire operation. A nonzero volume +// channel directly selects that 1-based Feature Unit logical channel. +// Per-channel capability is not cached; an unsupported channel is reported by +// the control transfer. +// +// Volume SET accepts TUH_AUDIO_VOLUME_SILENCE or a value within the cached +// range; finite values are rounded to the nearest resolution step measured +// from the range minimum. bool tuh_audio_mute_set(uint8_t idx, uint8_t stream_idx, bool mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data); bool tuh_audio_mute_get(uint8_t idx, uint8_t stream_idx, bool *mute, tuh_xfer_cb_t complete_cb, uintptr_t user_data); -bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_xfer_cb_t complete_cb, +bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, uint8_t channel, int16_t volume, tuh_xfer_cb_t complete_cb, uintptr_t user_data); -bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, int16_t *volume, tuh_xfer_cb_t complete_cb, +bool tuh_audio_volume_get(uint8_t idx, uint8_t stream_idx, uint8_t channel, int16_t *volume, tuh_xfer_cb_t complete_cb, uintptr_t user_data); //--------------------------------------------------------------------+ // Synchronous control requests block until the transfer completes and return // its result. actual_len may be NULL when the received length is not needed. +// Only use when audio streaming is stopped, otherwise the stream's isochronous +// transfers may be disrupted and creating audible artifacts ! //--------------------------------------------------------------------+ tusb_xfer_result_t tuh_audio_control_xfer_sync(uint8_t idx, uint8_t entity_id, tusb_dir_t direction, uint8_t request, uint8_t control_selector, uint8_t channel, void *buffer, uint16_t length, @@ -285,13 +298,13 @@ TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_mute_get_sync(u } TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_volume_set_sync(uint8_t idx, uint8_t stream_idx, - int16_t volume) { - TU_API_SYNC(tuh_audio_volume_set, idx, stream_idx, volume); + uint8_t channel, int16_t volume) { + TU_API_SYNC(tuh_audio_volume_set, idx, stream_idx, channel, volume); } TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_volume_get_sync(uint8_t idx, uint8_t stream_idx, - int16_t *volume) { - TU_API_SYNC(tuh_audio_volume_get, idx, stream_idx, volume); + uint8_t channel, int16_t *volume) { + TU_API_SYNC(tuh_audio_volume_get, idx, stream_idx, channel, volume); } //--------------------------------------------------------------------+ diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index 0f865a5bb..101a0b58b 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -259,6 +259,10 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { #define TEST_UAC1_FEATURE_UNIT(_id, _source_id) \ TEST_UAC1_FEATURE_UNIT_CTRL(_id, _source_id, AUDIO10_FU_CONTROL_BM_MUTE | AUDIO10_FU_CONTROL_BM_VOLUME) +#define TEST_UAC1_FEATURE_UNIT_STEREO(_id, _source_id, _master, _channel_1, _channel_2) \ + 10, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, 1, _master, _channel_1, \ + _channel_2, 0 + #define TEST_UAC1_OUTPUT_TERM(_id, _type, _source_id) \ 9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL, _id, U16_TO_U8S_LE(_type), 0, _source_id, 0 @@ -311,6 +315,10 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { 18, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, U32_TO_U8S_LE(_master_controls), \ U32_TO_U8S_LE(0), U32_TO_U8S_LE(0), 0 +#define TEST_UAC2_FEATURE_UNIT_STEREO_CHANNELS(_id, _source_id, _master, _channel_1, _channel_2) \ + 18, TUSB_DESC_CS_INTERFACE, AUDIO20_CS_AC_INTERFACE_FEATURE_UNIT, _id, _source_id, U32_TO_U8S_LE(_master), \ + U32_TO_U8S_LE(_channel_1), U32_TO_U8S_LE(_channel_2), 0 + #define TEST_UAC2_AS_INTERFACE_NUM(_itf, _alt, _ep_count) \ 9, TUSB_DESC_INTERFACE, _itf, _alt, _ep_count, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_STREAMING, \ @@ -431,6 +439,23 @@ static const uint8_t uac2_playback_read_only_feature_unit[] = { TEST_UAC2_CS_DATA_EP, }; +static const uint8_t uac2_playback_read_only_master_volume[] = { + TEST_UAC2_AC_HEADER(64), + TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_RW << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), + TEST_UAC2_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, PLAYBACK_CLOCK, 2), + TEST_UAC2_FEATURE_UNIT_STEREO_CHANNELS(PLAYBACK_FU, PLAYBACK_INPUT_TERM, + AUDIO20_CTRL_R << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS, + AUDIO20_CTRL_RW << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS, + AUDIO20_CTRL_RW << AUDIO20_FEATURE_UNIT_CTRL_VOLUME_POS), + TEST_UAC2_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU, PLAYBACK_CLOCK), + TEST_UAC2_AS_ALT0, + TEST_UAC2_AS_INTERFACE(1, 1), + TEST_UAC2_AS_GENERAL(PLAYBACK_INPUT_TERM, 2), + TEST_UAC2_FORMAT(2, 16), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 192, 1), + TEST_UAC2_CS_DATA_EP, +}; + static const uint8_t uac2_playback_read_only_clock[] = { TEST_UAC2_AC_HEADER(46), TEST_UAC2_CLOCK_SOURCE(PLAYBACK_CLOCK, AUDIO20_CTRL_R << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), @@ -554,6 +579,20 @@ static const uint8_t playback_fu_without_mute_volume[] = { TEST_UAC1_CS_DATA_EP, }; +static const uint8_t playback_fu_channel_volume_only[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_FEATURE_UNIT_STEREO(PLAYBACK_FU, PLAYBACK_INPUT_TERM, 0, AUDIO10_FU_CONTROL_BM_VOLUME, + AUDIO10_FU_CONTROL_BM_VOLUME), + TEST_UAC1_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU), + TEST_UAC1_AS_ALT0, + TEST_UAC1_AS_INTERFACE(1, 1), + TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), + TEST_UAC1_FORMAT(2, 2, 16, 48000), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 192, 1), + TEST_UAC1_CS_DATA_EP, +}; + static const uint8_t capture_fu_before_usb_output[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(CAPTURE_INPUT_TERM, AUDIO_TERM_TYPE_IN_GENERIC_MIC, 1), @@ -761,10 +800,12 @@ static const uint8_t playback_11025_interval4[] = { static uint8_t fu_cb_count; static uintptr_t fu_cb_user_data; +static tusb_xfer_result_t fu_cb_result; static void feature_unit_complete(tuh_xfer_t *xfer) { fu_cb_count++; fu_cb_user_data = xfer->user_data; + fu_cb_result = xfer->result; } static void complete_interface_set(tusb_xfer_result_t result) { @@ -910,6 +951,7 @@ void setUp(void) { fu_cb_count = 0; fu_cb_user_data = 0; + fu_cb_result = XFER_RESULT_INVALID; TEST_ASSERT_TRUE(audioh_init()); } @@ -1026,7 +1068,7 @@ void test_audio_host_mounts_uac2_and_sets_clock_before_activating_stream(void) { TEST_ASSERT_EQUAL_UINT16(256, range.res); control_xfer_count = 0; - TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, (int16_t)(-6 * 256), feature_unit_complete, 42)); + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, (int16_t)(-6 * 256), feature_unit_complete, 42)); TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest); TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue)); @@ -1113,10 +1155,10 @@ void test_audio_host_uac2_read_only_feature_controls_reject_set_and_allow_get(vo TEST_ASSERT_TRUE(tuh_audio_mounted(0)); TEST_ASSERT_FALSE(tuh_audio_mute_set(0, 0, false, feature_unit_complete, 0)); - TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, feature_unit_complete, 0)); + TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, 0, feature_unit_complete, 0)); int16_t volume = 0; - TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, &volume, feature_unit_complete, 7)); + TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, 0, &volume, feature_unit_complete, 7)); TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_CUR, control_request.bRequest); TEST_ASSERT_EQUAL(TUSB_DIR_IN, control_request.bmRequestType_bit.direction); complete_control_xfer_with_u16((uint16_t)(-12 * 256)); @@ -1124,6 +1166,30 @@ void test_audio_host_uac2_read_only_feature_controls_reject_set_and_allow_get(vo TEST_ASSERT_EQUAL_UINT32(7, fu_cb_user_data); } +void test_audio_host_uac2_read_only_master_volume_set_fans_out_to_writable_channels(void) { + open_descriptors(uac2_playback_read_only_master_volume, sizeof(uac2_playback_read_only_master_volume)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_RANGE, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue)); + complete_uac2_volume_range((int16_t)(-90 * 256), (int16_t)(6 * 256), 256); + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + + control_xfer_count = 0; + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x1234)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue)); + complete_control_xfer(XFER_RESULT_SUCCESS); + + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO20_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_UINT8(0, fu_cb_count); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count); + TEST_ASSERT_EQUAL_HEX32(0x1234, fu_cb_user_data); +} + void test_audio_host_uac2_shared_clock_is_discovered_once_for_both_streams(void) { open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock)); TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); @@ -1664,7 +1730,7 @@ void test_audio_host_rejects_invalid_cached_volume_range(void) { TEST_ASSERT_FALSE(tuh_audio_volume_range_get(0, 0, &range)); } -void test_audio_host_ignores_feature_unit_without_master_mute_or_volume(void) { +void test_audio_host_ignores_feature_unit_without_mute_or_volume(void) { tuh_audio_volume_range_t range; mount_descriptors(playback_fu_without_mute_volume, sizeof(playback_fu_without_mute_volume)); @@ -1679,14 +1745,14 @@ void test_audio_host_typed_mute_and_volume_controls(void) { mount_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range)); - TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, (int16_t)(range.max + 1), NULL, 0)); + TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, (int16_t)(range.max + 1), NULL, 0)); TEST_ASSERT_TRUE(tuh_audio_mute_set(0, 0, true, feature_unit_complete, 0x1234)); TEST_ASSERT_EQUAL_HEX8(AUDIO10_CS_REQ_SET_CUR, control_request.bRequest); TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_MUTE, 0), tu_le16toh(control_request.wValue)); TEST_ASSERT_EQUAL_UINT16(1, tu_le16toh(control_request.wLength)); TEST_ASSERT_EQUAL_HEX8(1, control_buffer[0]); - TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, -6 * 256, feature_unit_complete, 0)); + TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0)); complete_control_xfer(XFER_RESULT_SUCCESS); TEST_ASSERT_TRUE(tuh_audio_mute_get(0, 0, &mute, feature_unit_complete, 0x2345)); @@ -1696,30 +1762,85 @@ void test_audio_host_typed_mute_and_volume_controls(void) { complete_control_xfer(XFER_RESULT_SUCCESS); TEST_ASSERT_TRUE(mute); - TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, -6 * 256, feature_unit_complete, 0x3456)); + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x3456)); TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 0), tu_le16toh(control_request.wValue)); TEST_ASSERT_EQUAL_UINT16(2, tu_le16toh(control_request.wLength)); TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0xFA}), control_buffer, 2); complete_control_xfer(XFER_RESULT_SUCCESS); - TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, &volume, feature_unit_complete, 0x4567)); + TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, 0, &volume, feature_unit_complete, 0x4567)); complete_control_xfer_with_u16((uint16_t)(-12 * 256)); TEST_ASSERT_EQUAL_INT16(-12 * 256, volume); TEST_ASSERT_EQUAL_UINT8(4, fu_cb_count); TEST_ASSERT_EQUAL_HEX32(0x4567, fu_cb_user_data); } +void test_audio_host_uac1_channel_volume_supports_individual_and_stream_controls(void) { + int16_t volume = 0; + open_descriptors(playback_fu_channel_volume_only, sizeof(playback_fu_channel_volume_only)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue)); + complete_mount(); + + TEST_ASSERT_FALSE(tuh_audio_volume_get(0, 0, 0, &volume, feature_unit_complete, 0)); + TEST_ASSERT_TRUE(tuh_audio_volume_get(0, 0, 2, &volume, feature_unit_complete, 0x2345)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue)); + complete_control_xfer_with_u16((uint16_t)(-12 * 256)); + TEST_ASSERT_EQUAL_INT16(-12 * 256, volume); + TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count); + + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 1, -3 * 256, feature_unit_complete, 0x3456)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue)); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(2, fu_cb_count); + + const uint8_t before_stream_set = control_xfer_count; + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x4567)); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 1), tu_le16toh(control_request.wValue)); + TEST_ASSERT_FALSE(tuh_audio_volume_set(0, 0, 2, 0, feature_unit_complete, 0)); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(before_stream_set + 2, control_xfer_count); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_UINT8(2, fu_cb_count); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(3, fu_cb_count); + TEST_ASSERT_EQUAL_HEX32(0x4567, fu_cb_user_data); +} + +void test_audio_host_channel_volume_fanout_stops_on_transfer_failure(void) { + mount_descriptors(playback_fu_channel_volume_only, sizeof(playback_fu_channel_volume_only)); + + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, feature_unit_complete, 0x5678)); + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + complete_control_xfer(XFER_RESULT_STALLED); + + TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(1, fu_cb_count); + TEST_ASSERT_EQUAL_HEX32(0x5678, fu_cb_user_data); + TEST_ASSERT_EQUAL(XFER_RESULT_STALLED, fu_cb_result); +} + +void test_audio_host_channel_volume_fanout_supports_blocking_requests(void) { + tusb_xfer_result_t result = XFER_RESULT_INVALID; + mount_descriptors(playback_fu_channel_volume_only, sizeof(playback_fu_channel_volume_only)); + + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, -6 * 256, NULL, (uintptr_t)&result)); + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_HEX16(tu_u16(AUDIO10_FU_CTRL_VOLUME, 2), tu_le16toh(control_request.wValue)); + TEST_ASSERT_EQUAL_UINT8(0, fu_cb_count); +} + void test_audio_host_volume_set_accepts_silence_and_rounds_unaligned_values(void) { tuh_audio_volume_range_t range; mount_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); TEST_ASSERT_TRUE(tuh_audio_volume_range_get(0, 0, &range)); - TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, (int16_t)(-6 * 256 + 100), feature_unit_complete, 0)); + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, (int16_t)(-6 * 256 + 100), feature_unit_complete, 0)); TEST_ASSERT_EQUAL_UINT8(1, control_xfer_count); TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0xFA}), control_buffer, 2); complete_control_xfer(XFER_RESULT_SUCCESS); - TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, TUH_AUDIO_VOLUME_SILENCE, feature_unit_complete, 0)); + TEST_ASSERT_TRUE(tuh_audio_volume_set(0, 0, 0, TUH_AUDIO_VOLUME_SILENCE, feature_unit_complete, 0)); TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0x80}), control_buffer, 2); complete_control_xfer(XFER_RESULT_SUCCESS); -- cgit v1.3.1 From ef404be0337b1486b0a45acd248639ac8dea6c15 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 3 Sep 2026 04:45:48 +0200 Subject: improve example details Signed-off-by: HiFiPHile --- examples/host/audio_host/src/audio_app.c | 146 ++++++++++++++--------------- examples/host/audio_host/src/tusb_config.h | 2 +- src/class/audio/audio_host.h | 3 + 3 files changed, 76 insertions(+), 75 deletions(-) (limited to 'examples/host/audio_host/src') diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c index 749274f44..9f84290b9 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -29,24 +29,25 @@ // - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default) #define AUDIO_MAX_CHANNELS 2 #define SAMPLE_RATES {44100, 48000} -#define FEATURE_UNIT_VOLUME_DB (-6 * 256) +#define FEATURE_UNIT_VOLUME_DB (-20 * 256) #define AUDIO_BUFFER_SAMPLE_COUNT (CFG_TUH_AUDIO_STREAM_BUFSIZE / 2 / sizeof(int16_t)) -static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device -static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index -static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index -static bool mic_ready = false; // capture stream is running -static bool spk_ready = false; // playback stream is running -static int16_t audio_samples[AUDIO_BUFFER_SAMPLE_COUNT]; // shared capture/playback buffer -static tuh_audio_stream_config_t mic_config; // selected capture configuration -static tuh_audio_stream_config_t spk_config; // selected playback configuration -static uint32_t spk_cb_count = 0; // playback callbacks (for debug) -static uint32_t mic_cb_count = 0; // capture callbacks (for debug) -static uint32_t fail_event_count = 0; // transfer/start failures (for debug) +static uint8_t audio_idx = TUSB_INDEX_INVALID_8; +static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; +static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; +// True after a stream start request is accepted and until it is stopped or fails. +static bool mic_enabled; +static bool spk_enabled; -//--------------------------------------------------------------------+ -// Helper Functions -//--------------------------------------------------------------------+ +static int16_t audio_samples[AUDIO_BUFFER_SAMPLE_COUNT]; +static tuh_audio_stream_config_t mic_config; +static tuh_audio_stream_config_t spk_config; + +// Diagnostic counters printed and cleared once per second. +static uint32_t spk_cb_count; +static uint32_t mic_cb_count; +static uint32_t fail_event_count; +static uint32_t stats_start_ms; //--------------------------------------------------------------------+ // Async Deferred Call Queue @@ -71,18 +72,19 @@ static void app_defer_queue_clear(void) { memset(_defer_q, 0, sizeof(_defer_q)); } -// Schedule func to be called after 'ms' milliseconds, returns false if queue is full -static bool app_defer_ms_async(uint32_t ms, app_defer_func_t func, uintptr_t arg) { +// Schedule func to be called after 'ms' milliseconds. Four slots cover the +// phase timer, one restart per stream, and the single supported Audio device. +static void app_defer_ms_async(uint32_t ms, app_defer_func_t func, uintptr_t arg) { for (uint8_t i = 0; i < APP_DEFER_QUEUE_SZ; i++) { if (_defer_q[i].func == NULL) { _defer_q[i].func = func; _defer_q[i].arg = arg; // add one to ensure we wait at least 'ms' milliseconds _defer_q[i].at_ms = tusb_time_millis_api() + ms + 1; - return true; + return; } } - return false; // queue full + TU_ASSERT(false, ); // Queue sizing above is an application invariant. } // Invoke all callbacks whose delay has expired, must be called periodically from main loop @@ -127,14 +129,14 @@ static const int16_t sine_lut[SINE_LUT_SIZE] = { }; static uint32_t sine_phase; +static uint32_t sine_phase_step; // Generate a continuous tone at the selected sample rate and pack each frame // according to the actual playback channel count. static void spk_fill_sine(uint32_t frames) { - const uint32_t phase_step = (uint32_t)(((uint64_t)SINE_TONE_HZ << 32) / spk_config.sample_rate); for (uint32_t i = 0; i < frames; i++) { const int16_t sample = sine_lut[sine_phase >> (32u - SINE_LUT_BITS)]; - sine_phase += phase_step; + sine_phase += sine_phase_step; for (uint8_t ch = 0; ch < spk_config.channels; ch++) { audio_samples[i * spk_config.channels + ch] = sample; } @@ -177,29 +179,29 @@ static const uint32_t app_phase_ms[APP_PHASE_COUNT] = {APP_PHASE_MIC_ONLY_MS, AP static void app_audio_phase_apply(void) { switch (app_audio_phase) { case APP_PHASE_MIC_ONLY: - if (!mic_ready) { - mic_ready = tuh_audio_start(audio_idx, cap_stream_idx); + if (!mic_enabled) { + mic_enabled = tuh_audio_start(audio_idx, cap_stream_idx); } - if (spk_ready) { - spk_ready = !tuh_audio_stop(audio_idx, spk_stream_idx); + if (spk_enabled) { + spk_enabled = !tuh_audio_stop(audio_idx, spk_stream_idx); } printf(" Phase %u: mic on, spk off (data dropped)\r\n", app_audio_phase); break; case APP_PHASE_SPK_ONLY: - if (mic_ready) { - mic_ready = !tuh_audio_stop(audio_idx, cap_stream_idx); + if (mic_enabled) { + mic_enabled = !tuh_audio_stop(audio_idx, cap_stream_idx); } - if (!spk_ready) { - spk_ready = tuh_audio_start(audio_idx, spk_stream_idx); + if (!spk_enabled) { + spk_enabled = tuh_audio_start(audio_idx, spk_stream_idx); } printf(" Phase %u: mic off, spk on (sine)\r\n", app_audio_phase); break; case APP_PHASE_ECHO: - if (!mic_ready) { - mic_ready = tuh_audio_start(audio_idx, cap_stream_idx); + if (!mic_enabled) { + mic_enabled = tuh_audio_start(audio_idx, cap_stream_idx); } - if (!spk_ready) { - spk_ready = tuh_audio_start(audio_idx, spk_stream_idx); + if (!spk_enabled) { + spk_enabled = tuh_audio_start(audio_idx, spk_stream_idx); } printf(" Phase %u: mic + spk on (echo)\r\n", app_audio_phase); break; @@ -208,6 +210,13 @@ static void app_audio_phase_apply(void) { } } +static void audio_stats_reset(void) { + stats_start_ms = tusb_time_millis_api(); + mic_cb_count = 0; + spk_cb_count = 0; + fail_event_count = 0; +} + // Enter a phase, then schedule the next switch after this phase's duration static void app_audio_phase_enter(uintptr_t phase) { app_audio_phase = (uint8_t)phase; @@ -215,43 +224,37 @@ static void app_audio_phase_enter(uintptr_t phase) { // transfer error) so they cannot re-start a stream this phase stops. app_defer_queue_clear(); app_audio_phase_apply(); + audio_stats_reset(); const uint8_t next_phase = (uint8_t)((app_audio_phase + 1) % APP_PHASE_COUNT); - app_defer_ms_async(app_phase_ms[app_audio_phase], (app_defer_func_t)app_audio_phase_enter, next_phase); + app_defer_ms_async(app_phase_ms[app_audio_phase], app_audio_phase_enter, next_phase); } - //--------------------------------------------------------------------+ // Blinking Task //--------------------------------------------------------------------+ void led_blinking_task(void) { - const uint32_t interval_ms = 1000; - static uint32_t start_ms = 0; + const uint32_t interval_ms = 1000; static bool led_state = false; // Blink every interval ms - if (tusb_time_millis_api() - start_ms < interval_ms) { + if (tusb_time_millis_api() - stats_start_ms < interval_ms) { return; // not enough time } - start_ms += interval_ms; + stats_start_ms += interval_ms; board_led_write(led_state); led_state = 1 - led_state; // toggle -#if 1 - printf(" MIC CB=%lu SPK CB=%lu FAIL EVENT=%lu\r\n", (unsigned long)mic_cb_count, (unsigned long)spk_cb_count, + printf(" MIC CB=%lu | SPK CB=%lu | FAIL EVENT=%lu\r\n", (unsigned long)mic_cb_count, (unsigned long)spk_cb_count, (unsigned long)fail_event_count); - mic_cb_count = 0; - spk_cb_count = 0; + mic_cb_count = 0; + spk_cb_count = 0; fail_event_count = 0; - -#endif } //--------------------------------------------------------------------+ // Application Task //--------------------------------------------------------------------+ - - // Echo one chunk after capture is at least half full and playback is at least // half drained. This runs from the main loop, independently of USB callbacks. static void audio_echo_task(void) { @@ -274,14 +277,14 @@ static void audio_echo_task(void) { } void audio_app_task(void) { - if (mic_ready && spk_ready) { + if (mic_enabled && spk_enabled) { audio_echo_task(); - } else if (mic_ready) { + } else if (mic_enabled) { const uint32_t frames = audio_half_fifo_frames(&mic_config); if (tuh_audio_read_available(audio_idx, cap_stream_idx) >= frames) { (void)tuh_audio_read(audio_idx, cap_stream_idx, audio_samples, frames); } - } else if (spk_ready) { + } else if (spk_enabled) { const uint32_t frames = audio_half_fifo_frames(&spk_config); if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) { spk_fill_sine(frames); @@ -318,10 +321,10 @@ static void audio_app_restart_stream(uintptr_t param) { } if (stream_idx == cap_stream_idx) { printf(" Restarting capture stream %u\r\n", stream_idx); - mic_ready = tuh_audio_start(idx, stream_idx); + mic_enabled = tuh_audio_start(idx, stream_idx); } else if (stream_idx == spk_stream_idx) { printf(" Restarting playback stream %u\r\n", stream_idx); - spk_ready = tuh_audio_start(idx, stream_idx); + spk_enabled = tuh_audio_start(idx, stream_idx); } } @@ -329,14 +332,15 @@ void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event const char *stream_name = (stream_idx == cap_stream_idx) ? "capture" : "playback"; if (event == TUH_AUDIO_EVENT_START_COMPLETE) { - printf(" %s start %s: result=%u\r\n", stream_name, result == XFER_RESULT_SUCCESS ? "complete" : "failed", - result); + printf(" %s start %s: result=%u\r\n", stream_name, result == XFER_RESULT_SUCCESS ? "complete" : "failed", result); if (result == XFER_RESULT_SUCCESS) { return; } } else if (event == TUH_AUDIO_EVENT_STOP_COMPLETE) { - printf(" %s stop %s: result=%u\r\n", stream_name, result == XFER_RESULT_SUCCESS ? "complete" : "failed", - result); + printf(" %s stop %s: result=%u\r\n", stream_name, result == XFER_RESULT_SUCCESS ? "complete" : "failed", result); + if (result != XFER_RESULT_SUCCESS) { + fail_event_count++; + } return; } else { printf(" %s transfer failed: result=%u\r\n", stream_name, result); @@ -344,11 +348,11 @@ void tuh_audio_event_cb(uint8_t idx, uint8_t stream_idx, tuh_audio_event_t event fail_event_count++; if (stream_idx == cap_stream_idx) { - mic_ready = false; + mic_enabled = false; } else if (stream_idx == spk_stream_idx) { - spk_ready = false; + spk_enabled = false; } - app_defer_ms_async(100, (app_defer_func_t)audio_app_restart_stream, ((uintptr_t)idx << 8) | stream_idx); + app_defer_ms_async(100, audio_app_restart_stream, ((uintptr_t)idx << 8) | stream_idx); } //--------------------------------------------------------------------+ @@ -397,16 +401,14 @@ static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const cha if (tuh_audio_volume_range_get(idx, stream_idx, &range)) { has_control = true; int16_t volume; - tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, 0, &volume); + tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, TUH_AUDIO_CHANNEL_MASTER, &volume); if (result == XFER_RESULT_SUCCESS) { printf(" %s master volume: %d (1/256 dB)\r\n", stream_name, volume); } int32_t target = FEATURE_UNIT_VOLUME_DB; target = TU_MAX(target, range.min); target = TU_MIN(target, range.max); - target = range.min + ((target - range.min + range.res / 2) / range.res) * range.res; - target = TU_MIN(target, range.max); - result = tuh_audio_volume_set_sync(idx, stream_idx, 0, (int16_t)target); + result = tuh_audio_volume_set_sync(idx, stream_idx, TUH_AUDIO_CHANNEL_MASTER, (int16_t)target); if (result == XFER_RESULT_SUCCESS) { printf(" %s volume set: %d (1/256 dB)\r\n", stream_name, (int)target); } @@ -428,18 +430,13 @@ void tuh_audio_umount_cb(uint8_t idx) { audio_idx = TUSB_INDEX_INVALID_8; cap_stream_idx = TUSB_INDEX_INVALID_8; spk_stream_idx = TUSB_INDEX_INVALID_8; - mic_ready = false; - spk_ready = false; + mic_enabled = false; + spk_enabled = false; } } static void tuh_audio_mount_async(uintptr_t param) { uint8_t idx = (uint8_t)param; - if (idx >= CFG_TUH_AUDIO_MAX) { - printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); - return; - } - printf("Audio device mounted: idx=%u addr=%u\r\n", idx, tuh_audio_get_dev_addr(idx)); // Inspect every stream and print its supported configurations @@ -479,7 +476,7 @@ static void tuh_audio_mount_async(uintptr_t param) { mic_config = config; printf(" Microphone configured\r\n"); configure_stream_controls(idx, stream_idx, "Microphone"); - mic_ready = tuh_audio_start(idx, stream_idx); + mic_enabled = tuh_audio_start(idx, stream_idx); capture_found = true; break; } @@ -532,7 +529,8 @@ static void tuh_audio_mount_async(uintptr_t param) { } audio_idx = idx; printf(" Speaker configured\r\n"); - sine_phase = 0; + sine_phase = 0; + sine_phase_step = (uint32_t)(((uint64_t)SINE_TONE_HZ << 32) / spk_config.sample_rate); configure_stream_controls(idx, spk_stream_idx, "Speaker"); if (capture_found) { @@ -540,11 +538,11 @@ static void tuh_audio_mount_async(uintptr_t param) { app_audio_phase_enter(APP_PHASE_MIC_ONLY); } else { // Playback-only device: start the sine test tone immediately. - spk_ready = tuh_audio_start(idx, spk_stream_idx); + spk_enabled = tuh_audio_start(idx, spk_stream_idx); } } // Invoked when device with Audio interface is mounted void tuh_audio_mount_cb(uint8_t idx) { - app_defer_ms_async(100, (app_defer_func_t)tuh_audio_mount_async, idx); + app_defer_ms_async(100, tuh_audio_mount_async, idx); } diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h index 5c2d54993..60d0fee65 100644 --- a/examples/host/audio_host/src/tusb_config.h +++ b/examples/host/audio_host/src/tusb_config.h @@ -95,7 +95,7 @@ extern "C" { #define CFG_TUH_DEVICE_MAX (3 * CFG_TUH_HUB + 1) //------------- Audio Host Config -------------// -#define CFG_TUH_AUDIO_MAX 2 +#define CFG_TUH_AUDIO_MAX 1 #define CFG_TUH_AUDIO_EPIN_BUFSIZE 256 // max capture transfer the application submits #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256 // max playback transfer the application submits diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 9ce8410c6..95911ab3b 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -114,6 +114,9 @@ typedef struct { uint8_t channels; } tuh_audio_stream_config_t; +// Feature Unit channel zero selects the master channel. +#define TUH_AUDIO_CHANNEL_MASTER 0 + // Volume values are signed 1/256 dB. INT16_MIN represents silence. #define TUH_AUDIO_VOLUME_SILENCE INT16_MIN -- cgit v1.3.1