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. --- src/class/audio/audio_host.c | 809 +++++++++++++++++++++++++++++++++++++++++++ src/class/audio/audio_host.h | 235 +++++++++++++ 2 files changed, 1044 insertions(+) create mode 100644 src/class/audio/audio_host.c create mode 100644 src/class/audio/audio_host.h (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c new file mode 100644 index 000000000..9414c1ab4 --- /dev/null +++ b/src/class/audio/audio_host.c @@ -0,0 +1,809 @@ +/* + * 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. + * + * This file is part of the TinyUSB stack. + */ + +#include "tusb_option.h" + +#if (CFG_TUH_ENABLED && CFG_TUH_AUDIO) + +#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 + +#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_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) { + (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; +} + +//--------------------------------------------------------------------+ +// 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; + +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 + + // 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 + + // 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; + + // 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]; + + 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); +} audioh_epbuf_t; + +static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; + +//--------------------------------------------------------------------+ +// Helper +//--------------------------------------------------------------------+ +TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + if (_audioh_itf[idx].daddr == 0) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +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; + } + } + return TUSB_INDEX_INVALID_8; +} + +//--------------------------------------------------------------------+ +// USBH API +//--------------------------------------------------------------------+ +bool audioh_init(void) { + tu_memclr(&_audioh_itf, sizeof(_audioh_itf)); + return true; +} + +bool audioh_deinit(void) { + 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); + 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->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); + } + + return true; +} + +//--------------------------------------------------------------------+ +// 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; + + 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 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 }; + + // 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; + + // Parse Audio Control interface descriptors (Input Terminal, Output Terminal, Feature Unit, etc.) + 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 *it = (const audio10_desc_input_terminal_t *)p_desc; + p_audio->input_terminal_type = tu_le16toh(it->wTerminalType); + p_audio->input_terminal_id = it->bTerminalID; + p_audio->input_terminal_channels = it->bNrChannels; + TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", + it->bTerminalID, tu_le16toh(it->wTerminalType), it->bNrChannels); + break; + } + case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { + const audio10_desc_output_terminal_t *ot = (const audio10_desc_output_terminal_t *)p_desc; + p_audio->output_terminal_type = tu_le16toh(ot->wTerminalType); + p_audio->output_terminal_id = ot->bTerminalID; + TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", + ot->bTerminalID, tu_le16toh(ot->wTerminalType)); + break; + } + case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { + const uint8_t *fu = p_desc; + p_audio->feature_unit_id = fu[3]; // bUnitID + p_audio->feature_unit_source_id = fu[4]; // bSourceID + TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", fu[3], fu[4]); + break; + } + default: + break; + } + } + p_desc = tu_desc_next(p_desc); + } + + // 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) { + const tusb_desc_interface_t *itf = (const tusb_desc_interface_t *)p_desc; + if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + // Found Audio Streaming Interface + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", itf->bInterfaceNumber, itf->bAlternateSetting); + + if (itf->bAlternateSetting == 0) { + // Interface descriptor with alt setting 0 (no endpoints) + // Add to AS interfaces array + if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { + p_audio->as_interface_num = itf->bInterfaceNumber; + p_audio->as_interfaces[p_audio->as_count] = itf->bInterfaceNumber; + desc_cb.desc_as_interface = itf; + desc_cb.as_interface_num = itf->bInterfaceNumber; + // Create new AS entry for per-interface storage + p_audio->as[p_audio->as_count].interface_num = itf->bInterfaceNumber; + p_audio->as[p_audio->as_count].alt_setting = 0; + p_audio->as_count++; + } + } else if (itf->bNumEndpoints > 0) { + // Interface descriptor with alt setting > 0 (has endpoints) + // Find matching AS interface 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] == itf->bInterfaceNumber) { + p_audio->alt_setting = itf->bAlternateSetting; + p_audio->as_alt_settings[as_idx] = itf->bAlternateSetting; + desc_cb.alt_setting = itf->bAlternateSetting; + desc_cb.desc_as_interface_alt = itf; + 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 == itf->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 = itf->bInterfaceNumber; + p_audio->as_count++; + } + if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { + p_audio->as[as_entry_idx].alt_setting = itf->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"); + 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; + // 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 *p_ep = (const tusb_desc_endpoint_t *)p_desc; + if (p_ep->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { + TU_LOG_DRV(" Isochronous EP %02x\r\n", p_ep->bEndpointAddress); + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { + p_audio->ep_in = p_ep->bEndpointAddress; + p_audio->ep_in_size = tu_edpt_packet_size(p_ep); + p_audio->ep_in_interval = p_ep->bInterval; + desc_cb.desc_ep_in = p_ep; + // 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 = p_ep->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(p_ep); + 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 { + p_audio->ep_out = p_ep->bEndpointAddress; + p_audio->ep_out_size = tu_edpt_packet_size(p_ep); + p_audio->ep_out_interval = p_ep->bInterval; + desc_cb.desc_ep_out = p_ep; + // 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 = p_ep->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(p_ep); + 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, p_ep), 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 (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { + // Another Audio Control interface (shouldn't happen in normal UAC 1.0) + p_audio->itf_count++; + } + } + p_desc = tu_desc_next(p_desc); + } + + 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; + 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]; + 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; + } + } + + // 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_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]; + } + } + + tuh_audio_mount_cb(idx, &mount_cb_data); + + usbh_driver_set_config_complete(dev_addr, p_audio->bInterfaceNumber); +} + +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++) { + 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 + return true; + } + } + return false; + } + + audioh_interface_t *p_audio = &_audioh_itf[idx]; + + // 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]; + 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; + } + } + + _audioh_mount(dev_addr, idx); + return true; +} + +//--------------------------------------------------------------------+ +// Application API +//--------------------------------------------------------------------+ +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; +} + +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) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +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); + + info->daddr = p_audio->daddr; + + // re-construct descriptor + tusb_desc_interface_t *desc = &info->desc; + desc->bLength = sizeof(tusb_desc_interface_t); + desc->bDescriptorType = TUSB_DESC_INTERFACE; + + desc->bInterfaceNumber = p_audio->bInterfaceNumber; + desc->bAlternateSetting = 0; + desc->bNumEndpoints = (uint8_t)((p_audio->ep_in ? 1u : 0u) + (p_audio->ep_out ? 1u : 0u)); + desc->bInterfaceClass = TUSB_CLASS_AUDIO; + desc->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; + desc->bInterfaceProtocol = 0; + desc->iInterface = p_audio->iInterface; + + return true; +} + +//--------------------------------------------------------------------+ +// 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) { + static uint8_t freq_buf[3] = {0}; + 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_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_u16_low(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 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) { + 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_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_u16_low(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 + }; + + 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_u16(control_selector, channel), + .wIndex = tu_u16(itf_num, unit_id), + .wLength = 2 + }; + + uint8_t val_buf[2] = { (uint8_t)(value & 0xFF), (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_u16(control_selector, channel), + .wIndex = tu_u16(itf_num, unit_id), + .wLength = len + }; + + tuh_xfer_t xfer = { + .daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = 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); + + 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; + 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 daddr, uint8_t 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); + + return usbh_edpt_xfer(daddr, p_audio->ep_in, buffer, len); +} + +bool tuh_audio_send(uint8_t daddr, uint8_t 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); + + return usbh_edpt_xfer(daddr, p_audio->ep_out, (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 + }; + + return tuh_control_xfer(&xfer); +} + +#endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h new file mode 100644 index 000000000..6dd44f830 --- /dev/null +++ b/src/class/audio/audio_host.h @@ -0,0 +1,235 @@ +/* + * 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. + * + * This file is part of the TinyUSB stack. + */ + +#ifndef TUSB_AUDIO_HOST_H_ +#define TUSB_AUDIO_HOST_H_ + +#include "audio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ +#ifndef CFG_TUH_AUDIO_MAX + #define CFG_TUH_AUDIO_MAX 1 +#endif + +#ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ + #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 +#endif + +#ifndef CFG_TUH_AUDIO_MAX_AS + #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; + +#ifndef CFG_TUH_AUDIO_EPIN_BUFSIZE + #define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 +#endif + +#ifndef CFG_TUH_AUDIO_EPOUT_BUFSIZE + #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 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); + +// 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); + +// Get number of AS interfaces for an audio device +uint8_t tuh_audio_as_get_count(uint8_t 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); + +// 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); + +//--------------------------------------------------------------------+ +// Control Endpoint API +//--------------------------------------------------------------------+ + +// 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); + +// 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); + +// 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); + +// 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); + +//--------------------------------------------------------------------+ +// 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 send audio data to OUT endpoint +bool tuh_audio_send(uint8_t daddr, uint8_t 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); + +// 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); + +// Invoked when an isochronous OUT transfer is complete +void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +bool audioh_init(void); +bool audioh_deinit(void); +uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len); +bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num); +bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +void audioh_close(uint8_t daddr); + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_AUDIO_HOST_H_ */ -- 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 'src/class') 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 70113a39a3ffac7b310ff7d661fe744c35364c50 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 13:47:18 +0800 Subject: fix(class/audio): fix control request byte order and buffer usage in audio host - Fix missing tu_htole16() conversions for wValue and wIndex in tuh_audio_set_sampling_freq, tuh_audio_get_sampling_freq, tuh_audio_feature_unit_set, and tuh_audio_feature_unit_get - Fix incorrect wIndex parameter order in feature unit requests (unit_id and itf_num were swapped) - Replace static freq_buf with per-endpoint ctrl buffer in tuh_audio_set_sampling_freq to avoid concurrency issues - Update audio_host README to match actual example behavior --- examples/host/audio_host/README.md | 4 ++-- src/class/audio/audio_host.c | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 4d8f66b71..072adbbf9 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -54,9 +54,9 @@ make BOARD= flash 3. Open a serial terminal to view output 4. The example will: - Print device information when mounted - - Set sampling frequency to 48kHz + - Set sampling frequency based on the device's advertised capabilities - Receive audio samples from the device (IN endpoint) - - Send test sine wave audio to the device (OUT endpoint) + - Loop back received audio to the device (OUT endpoint) for testing ## Serial Output Example diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 85c2374be..4b25835f4 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -614,7 +614,9 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { //--------------------------------------------------------------------+ 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) { - static uint8_t freq_buf[3] = {0}; + uint8_t const idx = get_idx_by_ep_addr(daddr, ep_addr); + 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, @@ -622,8 +624,8 @@ bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampli .direction = TUSB_DIR_OUT }, .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_u16_low(ep_addr), + .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 }; @@ -660,8 +662,8 @@ bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampl .direction = TUSB_DIR_IN }, .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 - .wIndex = tu_u16_low(ep_addr), + .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 }; @@ -688,8 +690,8 @@ bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, .direction = TUSB_DIR_OUT }, .bRequest = AUDIO10_CS_REQ_SET_CUR, - .wValue = tu_u16(control_selector, channel), - .wIndex = tu_u16(itf_num, unit_id), + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), .wLength = 2 }; @@ -723,8 +725,8 @@ bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, .direction = TUSB_DIR_IN }, .bRequest = AUDIO10_CS_REQ_GET_CUR, - .wValue = tu_u16(control_selector, channel), - .wIndex = tu_u16(itf_num, unit_id), + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), .wLength = len }; -- cgit v1.3.1 From d4dfed4106fdfa5af21935f0ad84ed814b23a9f7 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 14:41:36 +0800 Subject: fix(class/audio): fix indentation in audioh_set_config after UAC 1.0 changes --- src/class/audio/audio_host.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 4b25835f4..99d0f428e 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -136,7 +136,7 @@ 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); + TUH_EPBUF_DEF(ctrl, 8); } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; @@ -541,7 +541,7 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { 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); + usbh_driver_set_config_complete(dev_addr, itf_num); return true; } } -- cgit v1.3.1 From 786eef5fd29d0a1ba46eda1e907399aee5af16d0 Mon Sep 17 00:00:00 2001 From: "Zhang, Zhenjiang" Date: Fri, 17 Jul 2026 15:17:16 +0800 Subject: style(class/audio): rename descriptor variables to desc_ prefix and unify const style Rename descriptor pointer variables to use desc_ prefix for consistency with audio_device.c: - it -> desc_input_terminal - ot -> desc_output_terminal - fu -> desc_feature_unit - itf -> desc_interface - p_ep -> desc_endpoint Unify const qualifier placement to type_t const * style. Add file header comment describing UAC 1.0 host driver capabilities. Switch license header to SPDX identifier. --- src/class/audio/audio_host.c | 162 +++++++++++++++++++++---------------------- src/class/audio/audio_host.h | 23 +----- 2 files changed, 83 insertions(+), 102 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 99d0f428e..396f75987 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -1,28 +1,28 @@ /* - * The MIT License (MIT) + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-License-Identifier: 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: + * 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. + * Each AS interface can have its own sample rate, channel count, bit resolution, + * and endpoint configuration. * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. + * 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. * - * 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. + * In case you need to adjust the number of supported AS interfaces, change + * CFG_TUH_AUDIO_MAX_AS in your tusb_config.h. * - * This file is part of the TinyUSB stack. - */ + * */ #include "tusb_option.h" @@ -248,27 +248,27 @@ 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: { - const audio10_desc_input_terminal_t *it = (const audio10_desc_input_terminal_t *)p_desc; - p_audio->input_terminal_type = tu_le16toh(it->wTerminalType); - p_audio->input_terminal_id = it->bTerminalID; - p_audio->input_terminal_channels = it->bNrChannels; + 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", - it->bTerminalID, tu_le16toh(it->wTerminalType), it->bNrChannels); + 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 *ot = (const audio10_desc_output_terminal_t *)p_desc; - p_audio->output_terminal_type = tu_le16toh(ot->wTerminalType); - p_audio->output_terminal_id = ot->bTerminalID; + 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", - ot->bTerminalID, tu_le16toh(ot->wTerminalType)); + desc_output_terminal->bTerminalID, tu_le16toh(desc_output_terminal->wTerminalType)); break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - const uint8_t *fu = p_desc; - p_audio->feature_unit_id = fu[3]; // bUnitID - p_audio->feature_unit_source_id = fu[4]; // bSourceID - TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", fu[3], fu[4]); + 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 + TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", desc_feature_unit[3], desc_feature_unit[4]); break; } default: @@ -281,53 +281,53 @@ 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) { - const tusb_desc_interface_t *itf = (const tusb_desc_interface_t *)p_desc; + 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 (itf->bInterfaceClass != TUSB_CLASS_AUDIO) break; - if (itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + 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", itf->bInterfaceNumber, itf->bAlternateSetting); + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, desc_interface->bAlternateSetting); - if (itf->bAlternateSetting == 0) { + if (desc_interface->bAlternateSetting == 0) { // Interface descriptor with alt setting 0 (no endpoints) // Add to AS interfaces array if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { - p_audio->as_interface_num = itf->bInterfaceNumber; - p_audio->as_interfaces[p_audio->as_count] = itf->bInterfaceNumber; - desc_cb.desc_as_interface = itf; - desc_cb.as_interface_num = itf->bInterfaceNumber; + 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 = itf->bInterfaceNumber; + 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 if (itf->bNumEndpoints > 0) { + } else if (desc_interface->bNumEndpoints > 0) { // Interface descriptor with alt setting > 0 (has endpoints) // Find matching AS interface 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] == itf->bInterfaceNumber) { - p_audio->alt_setting = itf->bAlternateSetting; - p_audio->as_alt_settings[as_idx] = itf->bAlternateSetting; - desc_cb.alt_setting = itf->bAlternateSetting; - desc_cb.desc_as_interface_alt = itf; + 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 == itf->bInterfaceNumber) { + 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 = itf->bInterfaceNumber; + 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 = itf->bAlternateSetting; + p_audio->as[as_entry_idx].alt_setting = desc_interface->bAlternateSetting; } // Parse the interface's descriptors @@ -387,19 +387,19 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface break; } case TUSB_DESC_ENDPOINT: { - const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *)p_desc; - if (p_ep->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { - TU_LOG_DRV(" Isochronous EP %02x\r\n", p_ep->bEndpointAddress); - if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { - p_audio->ep_in = p_ep->bEndpointAddress; - p_audio->ep_in_size = tu_edpt_packet_size(p_ep); - p_audio->ep_in_interval = p_ep->bInterval; - desc_cb.desc_ep_in = p_ep; + 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) { + 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 = p_ep->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(p_ep); + 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; @@ -413,15 +413,15 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } } else { - p_audio->ep_out = p_ep->bEndpointAddress; - p_audio->ep_out_size = tu_edpt_packet_size(p_ep); - p_audio->ep_out_interval = p_ep->bInterval; - desc_cb.desc_ep_out = p_ep; + 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 = p_ep->bEndpointAddress; - as->ep_size = tu_edpt_packet_size(p_ep); + 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; @@ -435,7 +435,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } } } - TU_ASSERT(tuh_edpt_open(dev_addr, p_ep), 0); + TU_ASSERT(tuh_edpt_open(dev_addr, desc_endpoint), 0); } break; } @@ -449,7 +449,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface continue; } p_audio->itf_count++; - } else if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->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++; } @@ -594,17 +594,17 @@ bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { info->daddr = p_audio->daddr; // re-construct descriptor - tusb_desc_interface_t *desc = &info->desc; - desc->bLength = sizeof(tusb_desc_interface_t); - desc->bDescriptorType = TUSB_DESC_INTERFACE; - - desc->bInterfaceNumber = p_audio->bInterfaceNumber; - desc->bAlternateSetting = 0; - desc->bNumEndpoints = (uint8_t)((p_audio->ep_in ? 1u : 0u) + (p_audio->ep_out ? 1u : 0u)); - desc->bInterfaceClass = TUSB_CLASS_AUDIO; - desc->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; - desc->bInterfaceProtocol = 0; - desc->iInterface = p_audio->iInterface; + tusb_desc_interface_t *desc_interface = &info->desc; + desc_interface->bLength = sizeof(tusb_desc_interface_t); + desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; + + 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->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; + desc_interface->bInterfaceProtocol = 0; + desc_interface->iInterface = p_audio->iInterface; return true; } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 6dd44f830..5c5a8b3df 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -1,25 +1,6 @@ /* - * 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. + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-License-Identifier: MIT * * This file is part of the TinyUSB stack. */ -- 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 'src/class') 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 'src/class') 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 367693fe985ee64a6eb4c15828683befdc77ab95 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:35 +0200 Subject: fix(audio): distinguish data and feedback endpoints Associate UAC1 AudioStreaming endpoints by their usage type. Keep data and implicit-feedback IN endpoints as streams while leaving explicit feedback endpoints unopened until feedback scheduling is supported. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index f943a5b2f..e3e0afc22 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -619,7 +619,9 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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 + // An alternate setting can expose an endpoint in each direction. Explicit + // feedback endpoints are skipped; implicit-feedback data endpoints remain + // normal audio endpoints. typedef struct { uint8_t ep_addr; uint16_t ep_size; @@ -699,7 +701,24 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de } 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) { + if (desc_endpoint->bmAttributes.xfer != TUSB_XFER_ISOCHRONOUS) { + break; + } + + const uint8_t usage = desc_endpoint->bmAttributes.usage; + const bool implicit_feedback = + usage == (TUSB_ISO_EP_ATT_IMPLICIT_FB >> 4) && tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN; + const bool explicit_feedback = + usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4) || + (usage == (TUSB_ISO_EP_ATT_DATA >> 4) && desc_endpoint->bmAttributes.sync == TUSB_ISO_EP_ATT_NO_SYNC); + + if (explicit_feedback) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: explicit feedback ep %02x ignored\r\n", itf_num, alt, + desc_endpoint->bEndpointAddress); + break; + } + + if ((usage == (TUSB_ISO_EP_ATT_DATA >> 4) || implicit_feedback) && 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); -- cgit v1.3.1 From 5fb0a3d04509d6904feff387d4669e2d6184cdb9 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:35 +0200 Subject: fix(audio): place transfer buffers in configured memory Apply the host memory section and alignment attributes to audio transfer storage. This keeps buffers accessible to controllers that require DMA-capable memory. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index e3e0afc22..d8b5324f2 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -159,8 +159,6 @@ typedef struct { // 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 @@ -180,7 +178,8 @@ typedef struct { } audioh_interface_t; typedef struct { - TUH_EPBUF_DEF(ctrl, 8); // feature-unit SET data + TUH_EPBUF_DEF(sam_freq, 4); // shared sampling-frequency SET data + 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 GET chain state: only one GET in flight per device @@ -191,7 +190,8 @@ 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]; + +CFG_TUH_MEM_SECTION static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; //--------------------------------------------------------------------+ // Helper @@ -396,12 +396,13 @@ static void audioh_stream_set_freq_complete(tuh_xfer_t *xfer) { // 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]; + 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; - 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); + ctrl[0] = (uint8_t)(cfg->sample_rate & 0xFF); + ctrl[1] = (uint8_t)((cfg->sample_rate >> 8) & 0xFF); + 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}, @@ -413,7 +414,7 @@ static void audioh_stream_set_freq(tuh_audio_stream_t *s) { tuh_xfer_t xfer = {.daddr = s->daddr, .ep_addr = 0, .setup = &request, - .buffer = s->ctrl, + .buffer = ctrl, .complete_cb = audioh_stream_set_freq_complete, .user_data = (uintptr_t)s}; if (!tuh_control_xfer(&xfer)) { @@ -1048,6 +1049,7 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx 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); + 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); @@ -1059,6 +1061,13 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // 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 tuh_audio_stream_config_t *other_cfg = &other->config[other->active_config]; + if (cfg->sample_rate != other_cfg->sample_rate) { + TU_LOG_DRV(" AUDIO configure failed: capture/playback sample rates must match (%lu != %lu)\r\n", + (unsigned long)cfg->sample_rate, (unsigned long)other_cfg->sample_rate); + return false; + } + 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) { @@ -1068,9 +1077,9 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx } 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->frame_bytes = (uint8_t)tuh_audio_config_frame_size(cfg); + s->frames_per_ms = (uint16_t)(cfg->sample_rate / 1000); + s->frames_rem = (uint16_t)(cfg->sample_rate % 1000); s->rem_acc = 0; s->complete_cb = complete_cb; s->user_data = user_data; @@ -1268,7 +1277,7 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c .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; + uint8_t *val_buf = _audioh_epbuf[idx].fu_ctrl; val_buf[0] = (uint8_t)(value & 0xFF); val_buf[1] = (uint8_t)((value >> 8) & 0xFF); -- cgit v1.3.1 From 3e3de770979b1afa375153de7c789176e39342bd Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:35 +0200 Subject: fix(audio): close endpoints before stream reconfiguration Close the previously selected HCD endpoint before opening another alternate setting. This prevents stale endpoint size and controller state from surviving a format change. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index d8b5324f2..72c00576f 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -356,7 +356,23 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { // Configure state machine //--------------------------------------------------------------------+ +static bool audioh_stream_close_ep(tuh_audio_stream_t *s) { + if (!tu_edpt_stream_is_opened(&s->edpt)) { + return true; + } + + const uint8_t ep_addr = s->edpt.ep_addr; + if (!tuh_edpt_close(s->daddr, ep_addr)) { + TU_LOG_DRV(" AUDIO close endpoint failed: addr=%u ep=%02x\r\n", s->daddr, ep_addr); + return false; + } + + tu_edpt_stream_close(&s->edpt); + return true; +} + static void audioh_stream_fail(tuh_audio_stream_t *s, tusb_xfer_result_t result) { + (void)audioh_stream_close_ep(s); s->state = STREAM_STATE_IDLE; s->active_config = TUSB_INDEX_INVALID_8; s->running = false; @@ -1055,7 +1071,7 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx 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); + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->edpt.ep_addr), false); } // A shared AS interface must not be left in two different alternate settings @@ -1076,6 +1092,10 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx } } + // The HCD endpoint must be reopened even when the new configuration uses + // the same address, since its packet size and interval may have changed. + TU_VERIFY(audioh_stream_close_ep(s), false); + s->active_config = config_idx; s->frame_bytes = (uint8_t)tuh_audio_config_frame_size(cfg); s->frames_per_ms = (uint16_t)(cfg->sample_rate / 1000); -- cgit v1.3.1 From edb883a0c017ebc6b96f617ab032ffebe9789525 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: fix(audio): retain stream state when stop fails Clear the started state only after SET_INTERFACE alt 0 succeeds. A failed control transfer can then be retried instead of leaving software and device state inconsistent. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 72c00576f..6e1a6a68c 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -1175,7 +1175,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->running, false); + TU_VERIFY(s && s->state == STREAM_STATE_READY, 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) @@ -1184,6 +1184,7 @@ bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { 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); } -- cgit v1.3.1 From 350b4fe5ba4754fa606aabc447795d038c10a623 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: fix(audio): serialize Feature Unit requests Keep Feature Unit request data in driver-owned storage until asynchronous completion and use the width defined for each control. This avoids overlapping request state and malformed one-byte controls. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 73 ++++++++++++++++++++++++++++++++++++++------ src/class/audio/audio_host.h | 6 ++-- 2 files changed, 67 insertions(+), 12 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 6e1a6a68c..338b6d94a 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -182,11 +182,12 @@ 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 GET chain state: only one GET in flight per device + // 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; } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; @@ -255,15 +256,22 @@ static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) { return (uint32_t)ep_interval * 1000u; } -// UAC 1.0 feature-unit control value width: mute/AGC/loudness are 1 byte, the rest 2 bytes +// UAC 1.0 feature-unit control value width (0 = unsupported variable/unknown width) 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; - default: + case AUDIO10_FU_CTRL_VOLUME: + case AUDIO10_FU_CTRL_DELAY: return 2; + default: + return 0; } } @@ -540,7 +548,8 @@ void audioh_close(uint8_t daddr) { audioh_stream_reset(stream); } - _audioh_epbuf[idx].complete_cb = NULL; // drop a pending feature-unit GET + _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; @@ -1260,6 +1269,21 @@ uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { // Feature Unit Control API //--------------------------------------------------------------------+ +// Release the stable SET buffer and chain to the application callback +static void audioh_fu_set_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; + epbuf->complete_cb = NULL; + epbuf->fu_busy = false; + + xfer->user_data = user_data; + if (app_cb != NULL) { + app_cb(xfer); + } +} + // 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; @@ -1269,6 +1293,7 @@ static void audioh_fu_get_complete(tuh_xfer_t *xfer) { 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; @@ -1289,6 +1314,11 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0, false); const uint8_t width = audioh_fu_control_width(control_selector); + TU_VERIFY(width != 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 const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, @@ -1298,9 +1328,11 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c .wIndex = tu_htole16(tu_u16(p_audio->feature_unit_id, p_audio->ac_itf_num)), .wLength = width}; - uint8_t *val_buf = _audioh_epbuf[idx].fu_ctrl; + uint8_t *val_buf = epbuf->fu_ctrl; val_buf[0] = (uint8_t)(value & 0xFF); - val_buf[1] = (uint8_t)((value >> 8) & 0xFF); + if (width == 2) { + val_buf[1] = (uint8_t)((value >> 8) & 0xFF); + } tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, @@ -1309,7 +1341,23 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t c .complete_cb = complete_cb, .user_data = user_data}; - return tuh_control_xfer(&xfer); + if (complete_cb == NULL) { + const bool result = tuh_control_xfer(&xfer); + epbuf->fu_busy = false; + return result; + } + + epbuf->complete_cb = complete_cb; + epbuf->user_data = user_data; + xfer.complete_cb = audioh_fu_set_complete; + xfer.user_data = (uintptr_t)idx; + + if (!tuh_control_xfer(&xfer)) { + epbuf->complete_cb = NULL; + epbuf->fu_busy = false; + return false; + } + return true; } bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, @@ -1319,6 +1367,11 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t c TU_VERIFY(p_audio->mounted && p_audio->feature_unit_id != 0 && value, false); const uint8_t width = audioh_fu_control_width(control_selector); + TU_VERIFY(width != 0, false); + + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + TU_VERIFY(!epbuf->fu_busy, false); + epbuf->fu_busy = true; const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, @@ -1338,19 +1391,18 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t c .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)); } + epbuf->fu_busy = false; return true; } // 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 - epbuf->complete_cb = complete_cb; epbuf->user_data = user_data; epbuf->value = value; @@ -1365,6 +1417,7 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t c if (!tuh_control_xfer(&xfer)) { epbuf->complete_cb = NULL; + epbuf->fu_busy = false; return false; } return true; diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index aaa65b671..0133c24f2 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -194,13 +194,15 @@ uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); //--------------------------------------------------------------------+ // 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 +// 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); // 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. +// 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); -- cgit v1.3.1 From ebec43ef01258e1abd37a08f603c940690257951 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: fix(audio): clear stale Feature Unit associations Reset Feature Unit state when parsing a new audio function. Devices without a matching unit must not inherit an ID collected from an earlier interface or device. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 338b6d94a..4f4c1e0d0 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -551,9 +551,10 @@ 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->feature_unit_id = 0; + p_audio->stream_count = 0; + p_audio->daddr = 0; + p_audio->mounted = false; } } @@ -878,6 +879,7 @@ 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; -- 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 'src/class') 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 f849af38c58b0c7b395619311d2797fe9e0d90fb Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: fix(audio): honor full-speed isochronous intervals Convert full-speed bInterval values to the scheduler microframe timebase and size transfers for the complete service interval. Endpoints with bInterval greater than one no longer run at a one-frame cadence. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 4 +- src/class/audio/audio_host.c | 134 ++++++++++++++++++++----------------- src/class/audio/audio_host.h | 2 +- 3 files changed, 74 insertions(+), 66 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index c7cad5bfa..22990d197 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -8,7 +8,7 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) - 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 +- Frame-based FIFO API: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules transfers at the endpoint's polling interval - 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 @@ -95,6 +95,6 @@ Edit `src/tusb_config.h` to modify: ## Notes -- 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. +- 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. - `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/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 951656aa7..da163703b 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -20,7 +20,7 @@ * OUT transfers from a FIFO filled by the application with tuh_audio_write() * * 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 + * flight at the endpoint's polling 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 @@ -142,14 +142,13 @@ typedef struct { uint8_t feature_unit_id; // Size in bytes of one frame (all channels) of the active configuration - uint8_t frame_bytes; + uint16_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; + // Playback pacing per endpoint poll interval. Fractional frames are + // accumulated in millionths because the interval is expressed in us. + uint16_t frames_per_interval; + uint32_t frames_rem; + uint32_t rem_acc; // Configure state machine tuh_audio_configure_cb_t complete_cb; @@ -157,7 +156,7 @@ typedef struct { // 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 + // from the endpoint's 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; @@ -277,18 +276,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->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; + 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_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); } @@ -328,37 +327,36 @@ static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { 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. +// Submit the next queued playback packet. Fractional frames per endpoint poll +// interval are accumulated on each successful submission, keeping the average +// data rate exactly at the sample rate. 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; + uint32_t frames = s->frames_per_interval; + uint32_t next_rem_acc = s->rem_acc + s->frames_rem; + if (next_rem_acc >= 1000000) { + next_rem_acc -= 1000000; 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 + const uint64_t bytes_64 = (uint64_t)frames * s->frame_bytes; + TU_ASSERT(bytes_64 <= map->ep_size && bytes_64 <= CFG_TUH_AUDIO_EPOUT_BUFSIZE && + bytes_64 <= CFG_TUH_AUDIO_STREAM_BUFSIZE, ); + const uint16_t bytes = (uint16_t)bytes_64; + if (tu_fifo_count(&s->edpt.ff) < bytes) { + // Wait until one complete poll interval is queued. This is required when + // bInterval is greater than one frame and also avoids short audio packets. usbh_edpt_release(s->daddr, map->ep_addr); return; } - 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), ); + s->rem_acc = next_rem_acc; } //--------------------------------------------------------------------+ @@ -788,8 +786,16 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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); + 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 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)); @@ -799,11 +805,15 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de const uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + if (ep->ep_size == 0 || ep->ep_size > iso_xfer_size) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid isochronous ep size %u\r\n", itf_num, alt, ep->ep_size); + continue; + } + // 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); + if (stream->dir == TUSB_DIR_IN && (ep->ep_size > epbuf_size || ep->ep_size > CFG_TUH_AUDIO_STREAM_BUFSIZE)) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds buffer capacity\r\n", itf_num, alt, ep->ep_size); continue; } @@ -812,18 +822,16 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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; - } + // 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)sam_freq[i] * 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; } // Skip duplicate configurations bool duplicate = false; @@ -858,7 +866,6 @@ 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; @@ -1137,16 +1144,17 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // the same address, since its packet size and interval may have changed. TU_VERIFY(audioh_stream_close_ep(s), false); - s->active_config = config_idx; - s->frame_bytes = (uint8_t)tuh_audio_config_frame_size(cfg); - s->frames_per_ms = (uint16_t)(cfg->sample_rate / 1000); - s->frames_rem = (uint16_t)(cfg->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]; + const uint64_t frames_numerator = (uint64_t)cfg->sample_rate * audioh_interval_us(map->ep_interval, s->daddr); + s->active_config = config_idx; + s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(cfg); + 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; - 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); diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 367e58003..282314f62 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -44,7 +44,7 @@ extern "C" { #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 +// application's read/write calls from the endpoint's isochronous polling 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 -- cgit v1.3.1 From a57307b3c20fb25c95eee143fa1a36254c354922 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: fix(audio): reject continuous UAC1 sample-rate ranges Collect only format descriptors with discrete sampling frequencies. The configuration API exposes concrete tuples and cannot safely represent a continuous range as one selectable rate. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 3 ++- src/class/audio/audio_host.c | 25 +++++-------------------- src/class/audio/audio_host.h | 4 ++-- 3 files changed, 9 insertions(+), 23 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 22990d197..9fa84f8fd 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -13,7 +13,8 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) ## Supported Devices -This example supports any UAC 1.0 compliant USB audio device with a discrete sampling-frequency capture stream, such as: +This example supports UAC 1.0 devices whose Type I Format descriptor lists discrete sampling frequencies (`bSamFreqType > 0`), such as: + - USB microphones - USB headsets (mono microphone + speaker) - USB audio interfaces diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index da163703b..968e8da3c 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -34,9 +34,8 @@ * 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. + * Non-PCM formats and sampling-frequency ranges are not registered as + * supported configurations during enumeration. * * The driver owns: * 1. Endpoint selection and opening (only the alternate setting selected by @@ -640,8 +639,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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 + uint8_t sam_freq_count = 0; uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; // An alternate setting can expose an endpoint in each direction. Explicit @@ -682,21 +680,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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); + if (p_desc[7] > 0) { + sam_freq_count = TU_MIN(p_desc[7], 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)); diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 282314f62..74dfdcdb6 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -61,8 +61,8 @@ typedef enum { 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. +// Discrete sample format. Only UAC1 configurations with bSamFreqType > 0 are +// supported; sampling-frequency 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 -- cgit v1.3.1 From ef631435ce8c3dd0fde720f097ffea317ad6f06a Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: fix(audio): resolve Feature Units independent of order Record terminal and Feature Unit links while parsing the AudioControl block, then resolve the stream association after all entities are known. Common capture and playback descriptor orders are both supported. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 6 ++++-- src/class/audio/audio_host.c | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 9fa84f8fd..bef78c7d3 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -19,11 +19,13 @@ This example supports UAC 1.0 devices whose Type I Format descriptor lists discr - 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. +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. ## 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. +- 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. - 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 diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 968e8da3c..8918ca86e 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -882,6 +882,9 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface audioh_ac_header_t header = {0}; 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. + uint8_t pending_fu_id = 0; + uint8_t pending_fu_source_id = 0; bool have_header = false; p_desc = tu_desc_next(p_desc); @@ -911,6 +914,11 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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; + if (pending_fu_source_id == usb_input_terminal_id) { + p_audio->out_stream.feature_unit_id = pending_fu_id; + pending_fu_id = 0; + pending_fu_source_id = 0; + } } break; } @@ -919,18 +927,30 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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; + if (pending_fu_id == usb_output_source_id) { + p_audio->in_stream.feature_unit_id = pending_fu_id; + pending_fu_id = 0; + pending_fu_source_id = 0; + } } break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { if (p_desc[0] >= 5) { + 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]; + 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]; + mapped = true; + } + if (!mapped) { + pending_fu_id = p_desc[3]; + pending_fu_source_id = p_desc[4]; } } break; -- cgit v1.3.1 From ddf4bad89017dab131eddf2fa27d835218e7f72e Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:28:12 +0200 Subject: fix(audio): drive streams from transfer completion Keep capture and playback transfers continuously armed from their completion callbacks. Capture overwrites the oldest complete frames when full, while playback sends silence on underrun without consuming partial queued audio. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 6 +- src/class/audio/audio_host.c | 36 ++++++----- src/class/audio/audio_host.h | 6 +- test/unit-test/test/host/audio/test_audio_host.c | 76 ++++++++++++++++++++++-- 4 files changed, 94 insertions(+), 30 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 88c948ccd..bbb3a7684 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -98,10 +98,10 @@ Edit `src/tusb_config.h` to modify: - `CFG_TUH_AUDIO_MAX`: Maximum number of audio devices supported - `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) +- `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 ## Notes - 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. -- `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. +- `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. +- 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/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 8918ca86e..b682d2d8a 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -311,15 +311,12 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) //--------------------------------------------------------------------+ // 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. +// most its max packet size per poll interval). The overwritable FIFO retains +// the newest capture frames when the application cannot drain it in time. 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 @@ -347,13 +344,13 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { bytes_64 <= CFG_TUH_AUDIO_STREAM_BUFSIZE, ); const uint16_t bytes = (uint16_t)bytes_64; if (tu_fifo_count(&s->edpt.ff) < bytes) { - // Wait until one complete poll interval is queued. This is required when - // bInterval is greater than one frame and also avoids short audio packets. - usbh_edpt_release(s->daddr, map->ep_addr); - return; + // Keep the isochronous stream active without consuming a partial frame. + // The queued audio is sent once a complete poll interval is available. + tu_memclr(s->edpt.ep_buf, bytes); + } else { + tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, 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), ); s->rem_acc = next_rem_acc; } @@ -506,7 +503,7 @@ bool audioh_init(void) { 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, + TU_VERIFY(tu_edpt_stream_init(&in->edpt, true, false, true, 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)); @@ -883,8 +880,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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. - uint8_t pending_fu_id = 0; - uint8_t pending_fu_source_id = 0; + uint8_t pending_fu_id = 0; + uint8_t pending_fu_source_id = 0; bool have_header = false; p_desc = tu_desc_next(p_desc); @@ -1159,6 +1156,12 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx s->complete_cb = complete_cb; s->user_data = user_data; s->state = STREAM_STATE_CONFIG; + 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); + } 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, @@ -1187,7 +1190,7 @@ static void audioh_stream_start_complete(tuh_xfer_t *xfer) { 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 + audioh_stream_playback_xfer(s); // start the continuous playback transfer chain } } @@ -1261,10 +1264,6 @@ uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer } tu_fifo_write_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); - // Flush a packet when the FIFO holds at least one; the scheduler drains - // the rest on completion - audioh_stream_playback_xfer(s); - return frames; } @@ -1283,7 +1282,6 @@ uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint3 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; } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 74dfdcdb6..9fcbe2577 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -45,7 +45,8 @@ extern "C" { // Depth in bytes of the per-stream data FIFO. The FIFO decouples the // application's read/write calls from the endpoint's isochronous polling cadence -// and absorbs rate differences. 1024 bytes hold 4 default (256 B) packets. +// and absorbs rate differences. Capture overwrites the oldest frames when full. +// 1024 bytes hold 4 default (256 B) packets. #ifndef CFG_TUH_AUDIO_STREAM_BUFSIZE #define CFG_TUH_AUDIO_STREAM_BUFSIZE 1024 #endif @@ -240,7 +241,8 @@ void tuh_audio_umount_cb(uint8_t idx); 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. +// next playback packet is submitted from the stream FIFO, or as silence when +// 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 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 728bcdf29..b87f94bc9 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -44,6 +44,8 @@ static uint8_t edpt_close_count; static bool edpt_busy; static bool edpt_xfer_result; static uint16_t edpt_xfer_bytes[16]; +static uint8_t edpt_xfer_data[16][8]; +static uint8_t *edpt_xfer_buffer[16]; static uint8_t edpt_xfer_count; tusb_speed_t tuh_speed_get(uint8_t daddr) { @@ -98,11 +100,13 @@ bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t *bu tuh_xfer_cb_t complete_cb, uintptr_t user_data) { (void)dev_addr; (void)ep_addr; - (void)buffer; (void)complete_cb; (void)user_data; if (edpt_xfer_count < TU_ARRAY_SIZE(edpt_xfer_bytes)) { - edpt_xfer_bytes[edpt_xfer_count++] = total_bytes; + edpt_xfer_bytes[edpt_xfer_count] = total_bytes; + edpt_xfer_buffer[edpt_xfer_count] = buffer; + memcpy(edpt_xfer_data[edpt_xfer_count], buffer, TU_MIN(sizeof(edpt_xfer_data[0]), total_bytes)); + edpt_xfer_count++; } return edpt_xfer_result; } @@ -254,7 +258,7 @@ static const uint8_t capture_fu_before_usb_output[] = { TEST_UAC1_AS_ALT0, TEST_UAC1_AS_INTERFACE(1, 1), TEST_UAC1_AS_GENERAL(CAPTURE_OUTPUT_TERM), - TEST_UAC1_FORMAT(1, 2, 16, 48000), + TEST_UAC1_FORMAT(1, 3, 24, 32000), TEST_UAC1_CS_DATA_EP, TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 96, 1), }; @@ -396,6 +400,8 @@ void setUp(void) { edpt_busy = false; edpt_xfer_result = true; memset(edpt_xfer_bytes, 0, sizeof(edpt_xfer_bytes)); + memset(edpt_xfer_data, 0, sizeof(edpt_xfer_data)); + memset(edpt_xfer_buffer, 0, sizeof(edpt_xfer_buffer)); edpt_xfer_count = 0; configure_cb_count = 0; @@ -443,10 +449,35 @@ void test_audio_host_maps_playback_fu_declared_before_usb_input_terminal(void) { } void test_audio_host_maps_capture_fu_declared_before_usb_output_terminal(void) { - open_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + 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)); 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_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + + // Keep polling without application reads. Once full, the capture FIFO must + // overwrite its oldest frames while retaining the newest complete packets. + for (uint8_t packet = 0; packet < 11; packet++) { + TEST_ASSERT_NOT_NULL(edpt_xfer_buffer[packet]); + memset(edpt_xfer_buffer[packet], packet + 1, 96); + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96)); + TEST_ASSERT_EQUAL_UINT8(packet + 2, edpt_xfer_count); + } + + TEST_ASSERT_EQUAL_UINT32(fifo_depth / 3, tuh_audio_read_available(0, 0)); + TEST_ASSERT_EQUAL_UINT32(fifo_depth / 3, tuh_audio_read(0, 0, captured, fifo_depth / 3)); + TEST_ASSERT_EACH_EQUAL_UINT8(1, captured, 63); + TEST_ASSERT_EACH_EQUAL_UINT8(2, captured + 63, 96); + TEST_ASSERT_EACH_EQUAL_UINT8(11, captured + fifo_depth - 96, 96); } void test_audio_host_parses_discrete_frequencies_with_interval_greater_than_one(void) { @@ -566,10 +597,11 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); TEST_ASSERT_EQUAL_UINT8(1, interface_alt); - complete_interface_set(XFER_RESULT_SUCCESS); + 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); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); - TEST_ASSERT_EQUAL_UINT32(256, tuh_audio_write(0, 0, samples, 256)); while (edpt_xfer_count < 5) { edpt_busy = false; TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1])); @@ -585,3 +617,35 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only } TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes[9]); } + +void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) { + uint8_t sample[4] = {1, 2, 3, 4}; + uint8_t more_samples[43 * 4]; + 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_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[0]); + TEST_ASSERT_EACH_EQUAL_HEX8(0, edpt_xfer_data[0], sizeof(edpt_xfer_data[0])); + + TEST_ASSERT_EQUAL_UINT32(1, tuh_audio_write(0, 0, sample, 1)); + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[0])); + + TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[1]); + TEST_ASSERT_EACH_EQUAL_HEX8(0, edpt_xfer_data[1], sizeof(edpt_xfer_data[1])); + + TEST_ASSERT_EQUAL_UINT32(43, tuh_audio_write(0, 0, more_samples, 43)); + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[1])); + + TEST_ASSERT_EQUAL_UINT8(3, edpt_xfer_count); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[2]); + TEST_ASSERT_EQUAL_UINT8_ARRAY(sample, edpt_xfer_data[2], sizeof(sample)); +} -- 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 'src/class') 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 8fdf71b46b270f60bc66489f60ea85f7b527408c Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:06 +0200 Subject: refactor(audio): simplify UAC1 interface discovery Parse the contiguous AudioStreaming interfaces by class and subclass instead of copying baInterfaceNr. This removes the UAC1-only collection limit and prevents MIDI AudioControl interfaces from leaving a half-allocated Audio Host instance. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 117 ++++++++--------------- test/unit-test/test/host/audio/test_audio_host.c | 18 ++++ 2 files changed, 58 insertions(+), 77 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 9115ce4ca..722b6fcbc 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -47,16 +47,24 @@ #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__) + +//--------------------------------------------------------------------+ +// 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) //--------------------------------------------------------------------+ // Weak stubs: invoked if no strong implementation is available @@ -88,16 +96,6 @@ TU_ATTR_WEAK void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xfe (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 - // Stream state machine enum { STREAM_STATE_IDLE = 0, // not configured @@ -530,26 +528,6 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin // 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. @@ -812,37 +790,17 @@ 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}; - uint8_t usb_input_terminal_id = 0; - uint8_t usb_output_source_id = 0; + uint8_t usb_input_terminal_id = 0; + uint8_t usb_output_source_id = 0; + bool found_as_interface = false; // A Feature Unit may precede the USB terminal that identifies its stream. - uint8_t pending_fu_id = 0; - uint8_t pending_fu_source_id = 0; - bool have_header = false; + uint8_t pending_fu_id = 0; + uint8_t pending_fu_source_id = 0; 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_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_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) && @@ -896,9 +854,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface p_desc = tu_desc_next(p_desc); } - // 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. + // Parse the contiguous Audio Streaming interfaces of this audio function. while (tu_desc_in_bounds(p_desc, desc_end)) { if (tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { p_desc = tu_desc_next(p_desc); @@ -906,20 +862,27 @@ 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; - const bool in_collection = have_header ? audioh_itf_in_collection(&header, desc_interface->bInterfaceNumber) - : desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO; - if (!in_collection) { + if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO || + desc_interface->bInterfaceSubClass != AUDIO_SUBCLASS_STREAMING) { break; } - 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; - } + found_as_interface = true; + 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); + } + + // This AC interface belongs to MIDI or another Audio subclass. Release the + // tentative instance and let the next class driver claim the interface. + if (!found_as_interface) { + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + p_audio->daddr = 0; + p_audio->ac_itf_num = 0; + p_audio->stream_count = 0; + p_audio->mounted = false; + return 0; } // Assign stream indices: playback first, then capture, so the application 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 ec1816084..4137642b3 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -226,6 +226,19 @@ static const uint8_t playback_with_explicit_feedback[] = { TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_EXPLICIT_FB, 3, 1), }; +static const uint8_t midi_only_collection[] = { + TEST_UAC1_AC_HEADER, + 9, + TUSB_DESC_INTERFACE, + AUDIO_AS_ITF, + 0, + 0, + TUSB_CLASS_AUDIO, + AUDIO_SUBCLASS_MIDI_STREAMING, + AUDIO_INT_PROTOCOL_CODE_V1, + 0, +}; + static const uint8_t playback_with_implicit_feedback[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -417,6 +430,11 @@ static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) { } void test_audio_host_ignores_explicit_feedback_endpoint(void) { + // A MIDI-only AC collection must not consume an Audio Host instance. + TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)midi_only_collection, + sizeof(midi_only_collection))); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); + open_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); -- cgit v1.3.1 From 0fd39954d7800777774c17617e41825e5a3561da Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:06 +0200 Subject: fix(audio): accept one data endpoint per AS alternate Collect one audio data endpoint for each alternate setting and ignore explicit feedback endpoints until feedback scheduling is implemented. Validate the maximum packet for every discrete rate against the endpoint, transfer buffer, and FIFO. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 171 +++++++++++------------ test/unit-test/test/host/audio/test_audio_host.c | 53 +++++-- 2 files changed, 127 insertions(+), 97 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 722b6fcbc..57aaf7d52 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -554,9 +554,9 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de uint8_t sam_freq_count = 0; uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; - // An alternate setting can expose an endpoint in each direction. Explicit - // feedback endpoints are skipped; implicit-feedback data endpoints remain - // normal audio endpoints. + // An AS alternate setting has one audio data endpoint and may have one + // explicit feedback endpoint. Implicit-feedback endpoints are data endpoints + // and are handled normally when they are the AS interface's data endpoint. typedef struct { uint8_t ep_addr; uint16_t ep_size; @@ -565,8 +565,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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; + audioh_ep_info_t ep_info = {0}; + bool has_data_ep = false; // 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. @@ -612,8 +612,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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; + ep_info.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; @@ -640,21 +640,26 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de break; } - if ((usage == (TUSB_ISO_EP_ATT_DATA >> 4) || implicit_feedback) && 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; + if (usage == (TUSB_ISO_EP_ATT_DATA >> 4) || implicit_feedback) { + if (has_data_ep) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: extra data ep %02x ignored\r\n", itf_num, alt, + desc_endpoint->bEndpointAddress); + break; + } + + ep_info.ep_addr = desc_endpoint->bEndpointAddress; + ep_info.ep_size = tu_edpt_packet_size(desc_endpoint); + ep_info.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; + if (ep_info.ep_interval == 0 || ep_info.ep_interval > 16) { + ep_info.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; + ep_info.ep_sync = desc_endpoint->bmAttributes.sync; + ep_info.ep_usage = desc_endpoint->bmAttributes.usage; + ep_info.sam_freq_ctrl = pending_sam_freq_ctrl; pending_sam_freq_ctrl = false; - unassigned_ep = !ep->sam_freq_ctrl; - ep_count++; + unassigned_ep = !ep_info.sam_freq_ctrl; + has_data_ep = true; } break; } @@ -664,7 +669,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de p_desc = tu_desc_next(p_desc); } - if (ep_count == 0) { + if (!has_data_ep) { return p_desc; } @@ -692,77 +697,73 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de } const uint16_t frame_bytes = (uint16_t)frame_bytes_32; - // Register one configuration per (endpoint, discrete sampling frequency) - 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; - } + // Register one configuration per discrete sampling frequency + 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 uint16_t epbuf_size = (stream->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE; + + if (ep->ep_size == 0 || ep->ep_size > iso_xfer_size) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid isochronous ep size %u\r\n", itf_num, alt, ep->ep_size); + return p_desc; + } - 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 || ep->ep_size > CFG_TUH_AUDIO_STREAM_BUFSIZE)) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds buffer capacity\r\n", itf_num, alt, ep->ep_size); + return p_desc; + } - if (ep->ep_size == 0 || ep->ep_size > iso_xfer_size) { - TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid isochronous ep size %u\r\n", itf_num, alt, ep->ep_size); + for (uint8_t i = 0; i < sam_freq_count; i++) { + if (sam_freq[i] == 0) { continue; } - // 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 || ep->ep_size > CFG_TUH_AUDIO_STREAM_BUFSIZE)) { - TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds buffer capacity\r\n", itf_num, alt, ep->ep_size); + // 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)sam_freq[i] * 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; } - - for (uint8_t i = 0; i < sam_freq_count; i++) { - if (sam_freq[i] == 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)sam_freq[i] * 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; - } - // 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; + // 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; + } - 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++; + 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; @@ -1022,7 +1023,6 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx TU_VERIFY(!usbh_edpt_busy(s->daddr, s->edpt.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 tuh_audio_stream_config_t *other_cfg = &other->config[other->active_config]; @@ -1031,13 +1031,6 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx (unsigned long)cfg->sample_rate, (unsigned long)other_cfg->sample_rate); return false; } - - 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; - } } // The HCD endpoint must be reopened even when the new configuration uses 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 4137642b3..63f59c454 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -10,9 +10,10 @@ TEST_SOURCE_FILE("audio_host.c") TEST_SOURCE_FILE("tusb_fifo.c") enum { - AUDIO_DEV_ADDR = 1, - AUDIO_AC_ITF = 0, - AUDIO_AS_ITF = 1, + AUDIO_DEV_ADDR = 1, + AUDIO_AC_ITF = 0, + AUDIO_AS_ITF = 1, + AUDIO_AS_IN_ITF = 2, PLAYBACK_INPUT_TERM = 1, PLAYBACK_FU = 2, @@ -177,6 +178,11 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { 9, TUSB_DESC_INTERFACE, AUDIO_AC_ITF, 0, 0, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_CONTROL, AUDIO_INT_PROTOCOL_CODE_V1, 0, \ 9, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_HEADER, 0x00, 0x01, 0x09, 0x00, 1, AUDIO_AS_ITF +#define TEST_UAC1_AC_HEADER_2 \ + 9, TUSB_DESC_INTERFACE, AUDIO_AC_ITF, 0, 0, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_CONTROL, AUDIO_INT_PROTOCOL_CODE_V1, 0, \ + 10, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AC_INTERFACE_HEADER, 0x00, 0x01, 0x34, 0x00, 2, AUDIO_AS_ITF, \ + AUDIO_AS_IN_ITF + #define TEST_UAC1_INPUT_TERM(_id, _type, _channels) \ 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 @@ -188,11 +194,13 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { #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 -#define TEST_UAC1_AS_INTERFACE(_alt, _ep_count) \ - 9, TUSB_DESC_INTERFACE, AUDIO_AS_ITF, _alt, _ep_count, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_STREAMING, \ +#define TEST_UAC1_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_V1, 0 -#define TEST_UAC1_AS_ALT0 TEST_UAC1_AS_INTERFACE(0, 0) +#define TEST_UAC1_AS_INTERFACE(_alt, _ep_count) TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_ITF, _alt, _ep_count) + +#define TEST_UAC1_AS_ALT0 TEST_UAC1_AS_INTERFACE(0, 0) #define TEST_UAC1_AS_GENERAL(_term_id) \ 7, TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AS_INTERFACE_AS_GENERAL, _term_id, 1, \ @@ -203,8 +211,10 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { AUDIO10_FORMAT_TYPE_I, _channels, _bytes, _bits, TU_ARGS_NUM(__VA_ARGS__), \ TU_ARGS_APPLY_EXPAND(U24_TO_U8S_LE, __VA_ARGS__) -#define TEST_UAC1_DATA_EP(_ep, _attr, _size, _interval) \ - 9, TUSB_DESC_ENDPOINT, _ep, (TUSB_XFER_ISOCHRONOUS | (_attr)), U16_TO_U8S_LE(_size), _interval, 0, 0 +#define TEST_UAC1_DATA_EP_SYNC(_ep, _attr, _size, _interval, _sync_ep) \ + 9, TUSB_DESC_ENDPOINT, _ep, (TUSB_XFER_ISOCHRONOUS | (_attr)), U16_TO_U8S_LE(_size), _interval, 0, _sync_ep + +#define TEST_UAC1_DATA_EP(_ep, _attr, _size, _interval) TEST_UAC1_DATA_EP_SYNC(_ep, _attr, _size, _interval, 0) #define TEST_UAC1_CS_DATA_EP_ATTR(_attr) \ 7, TUSB_DESC_CS_ENDPOINT, AUDIO10_CS_EP_SUBTYPE_GENERAL, _attr, AUDIO10_CS_AS_ISO_DATA_EP_LOCK_DELAY_UNIT_UNDEFINED, \ @@ -240,6 +250,26 @@ static const uint8_t midi_only_collection[] = { }; 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), + TEST_UAC1_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_INPUT_TERM), + TEST_UAC1_INPUT_TERM(CAPTURE_INPUT_TERM, AUDIO_TERM_TYPE_IN_GENERIC_MIC, 1), + TEST_UAC1_OUTPUT_TERM(CAPTURE_OUTPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, CAPTURE_INPUT_TERM), + 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_SYNC(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 192, 1, 0x81), + TEST_UAC1_CS_DATA_EP, + TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 0, 0), + TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 1, 1), + TEST_UAC1_AS_GENERAL(CAPTURE_OUTPUT_TERM), + TEST_UAC1_FORMAT(1, 2, 16, 48000), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_IMPLICIT_FB, 96, 1), + TEST_UAC1_CS_DATA_EP, +}; + +static const uint8_t playback_with_extra_data_endpoint[] = { TEST_UAC1_AC_HEADER, 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_INPUT_TERM), @@ -450,6 +480,13 @@ void test_audio_host_treats_implicit_feedback_as_audio_in_endpoint(void) { TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_CAPTURE, tuh_audio_stream_direction(0, 1)); } +void test_audio_host_ignores_second_data_endpoint_in_same_as_interface(void) { + open_descriptors(playback_with_extra_data_endpoint, sizeof(playback_with_extra_data_endpoint)); + + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); + TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_PLAYBACK, tuh_audio_stream_direction(0, 0)); +} + void test_audio_host_maps_playback_fu_declared_before_usb_input_terminal(void) { open_descriptors(playback_fu_before_terminal, sizeof(playback_fu_before_terminal)); -- 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 'src/class') 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 3f0a82a6bbe35135ed04749b4022ca6563073d5d Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:07 +0200 Subject: fix(audio): reject unsupported UAC2 interfaces Require the UAC1 AudioControl protocol before allocating an instance. The current parser assumes UAC1 descriptor layouts, so accepting UAC2 would misinterpret entities and consume a host slot. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 1 + test/unit-test/test/host/audio/test_audio_host.c | 13 +++++++++++++ 2 files changed, 14 insertions(+) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 78ecd9525..2c18a22d8 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -778,6 +778,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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); const uint8_t *desc_start = (const uint8_t *)desc_itf; const uint8_t *p_desc = desc_start; 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 51342bfac..72026e682 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -261,6 +261,10 @@ static const uint8_t midi_only_collection[] = { 0, }; +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 playback_with_implicit_feedback[] = { TEST_UAC1_AC_HEADER_2, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -489,6 +493,15 @@ void test_audio_host_ignores_explicit_feedback_endpoint(void) { TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_config_count(0, 0)); } +void test_audio_host_rejects_uac2_interface_without_consuming_instance(void) { + TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)uac2_control_interface, + sizeof(uac2_control_interface))); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); + + open_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); +} + void test_audio_host_treats_implicit_feedback_as_audio_in_endpoint(void) { open_descriptors(playback_with_implicit_feedback, sizeof(playback_with_implicit_feedback)); -- cgit v1.3.1 From ad9ecddd7f51ede415992397b82399d7a2639c0f Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:07 +0200 Subject: fix(audio): retain multiple pending Feature Units Keep one unresolved Feature Unit candidate per stream direction while parsing AudioControl entities. Duplex functions are now associated correctly even when both Feature Units precede their USB terminals. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 38 ++++++++++++++++-------- test/unit-test/test/host/audio/test_audio_host.c | 30 +++++++++++++++++++ 2 files changed, 56 insertions(+), 12 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 2c18a22d8..2aad4040e 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -800,8 +800,11 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface uint8_t usb_output_source_id = 0; bool found_as_interface = false; // A Feature Unit may precede the USB terminal that identifies its stream. - uint8_t pending_fu_id = 0; - uint8_t pending_fu_source_id = 0; + typedef struct { + uint8_t id; + uint8_t source_id; + } audioh_fu_info_t; + audioh_fu_info_t pending_fu[TUH_AUDIO_STREAM_DIRECTION_COUNT] = {0}; p_desc = tu_desc_next(p_desc); while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { @@ -812,10 +815,13 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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; - if (pending_fu_source_id == usb_input_terminal_id) { - p_audio->out_stream.feature_unit_id = pending_fu_id; - pending_fu_id = 0; - pending_fu_source_id = 0; + 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; + break; + } } } break; @@ -825,10 +831,13 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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; - if (pending_fu_id == usb_output_source_id) { - p_audio->in_stream.feature_unit_id = pending_fu_id; - pending_fu_id = 0; - pending_fu_source_id = 0; + 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; + break; + } } } break; @@ -847,8 +856,13 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface mapped = true; } if (!mapped) { - pending_fu_id = p_desc[3]; - pending_fu_source_id = p_desc[4]; + 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]; + break; + } + } } } break; 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 72026e682..b11d5a766 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -324,6 +324,28 @@ static const uint8_t capture_fu_before_usb_output[] = { TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 96, 1), }; +static const uint8_t duplex_fus_before_usb_terminals[] = { + TEST_UAC1_AC_HEADER_2, + TEST_UAC1_FEATURE_UNIT(PLAYBACK_FU, PLAYBACK_INPUT_TERM), + TEST_UAC1_FEATURE_UNIT(CAPTURE_FU, CAPTURE_INPUT_TERM), + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_OUTPUT_TERM(CAPTURE_OUTPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, CAPTURE_FU), + TEST_UAC1_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_FU), + TEST_UAC1_INPUT_TERM(CAPTURE_INPUT_TERM, AUDIO_TERM_TYPE_IN_GENERIC_MIC, 1), + 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, + TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 0, 0), + TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 1, 1), + TEST_UAC1_AS_GENERAL(CAPTURE_OUTPUT_TERM), + TEST_UAC1_FORMAT(1, 2, 16, 48000), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 96, 1), + TEST_UAC1_CS_DATA_EP, +}; + static const uint8_t playback_with_two_frequencies[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -554,6 +576,14 @@ 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_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)); +} + 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)); -- cgit v1.3.1 From 87511ce87280e986545149e4ad017ccf2392671c Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:07 +0200 Subject: fix(audio): release instances without supported streams Retain an audio function when at least one direction has a supported configuration, but release the tentative instance when neither direction does. Unsupported and MIDI-only functions no longer consume host slots or emit mount callbacks. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 8 ++--- test/unit-test/test/host/audio/test_audio_host.c | 41 ++++++++++++++++++++---- 2 files changed, 38 insertions(+), 11 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 2aad4040e..c29db658c 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -798,7 +798,6 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface uint8_t usb_input_terminal_id = 0; uint8_t usb_output_source_id = 0; - bool found_as_interface = false; // A Feature Unit may precede the USB terminal that identifies its stream. typedef struct { uint8_t id; @@ -887,15 +886,14 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface break; } - found_as_interface = true; 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); } - // This AC interface belongs to MIDI or another Audio subclass. Release the - // tentative instance and let the next class driver claim the interface. - if (!found_as_interface) { + // 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_stream_reset(&p_audio->in_stream); audioh_stream_reset(&p_audio->out_stream); p_audio->daddr = 0; 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 b11d5a766..37b35dae8 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -346,6 +346,24 @@ static const uint8_t duplex_fus_before_usb_terminals[] = { TEST_UAC1_CS_DATA_EP, }; +static const uint8_t playback_with_unsupported_capture[] = { + TEST_UAC1_AC_HEADER_2, + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_OUTPUT_TERM(CAPTURE_OUTPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, CAPTURE_INPUT_TERM), + 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, + TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 0, 0), + TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 1, 1), + TEST_UAC1_AS_GENERAL(CAPTURE_OUTPUT_TERM), + TEST_UAC1_FORMAT(1, 2, 12, 48000), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 96, 1), + TEST_UAC1_CS_DATA_EP, +}; + static const uint8_t playback_with_two_frequencies[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -667,16 +685,27 @@ void test_audio_host_parses_discrete_frequencies_with_interval_greater_than_one( TEST_ASSERT_EQUAL_UINT32(48000, config.sample_rate); } -void test_audio_host_rejects_sampling_frequency_range(void) { - open_descriptors(playback_with_frequency_range, sizeof(playback_with_frequency_range)); +void test_audio_host_keeps_supported_stream_when_other_as_format_is_unsupported(void) { + open_descriptors(playback_with_unsupported_capture, sizeof(playback_with_unsupported_capture)); - TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_stream_count(0)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); + TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_PLAYBACK, tuh_audio_stream_direction(0, 0)); } -void test_audio_host_rejects_overflowed_frame_size_for_large_channel_count(void) { - open_descriptors(capture_with_large_channel_count, sizeof(capture_with_large_channel_count)); +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_UINT8(0, tuh_audio_get_dev_addr(0)); - TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_stream_count(0)); + open_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); +} + +void test_audio_host_rejects_overflowed_frame_size_for_large_channel_count(void) { + TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, + (const tusb_desc_interface_t *)capture_with_large_channel_count, + sizeof(capture_with_large_channel_count))); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); } void test_audio_host_uses_cs_endpoint_declared_before_data_endpoint_on_start(void) { -- cgit v1.3.1 From b0bd26eb5f0b941da2120e8e90a27bae48f29278 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:07 +0200 Subject: fix(audio): decode full-speed isochronous bInterval Treat isochronous bInterval as a power-of-two exponent for both full and high speed, using the appropriate frame unit. Packet sizing and fractional scheduling now use the actual service interval. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 11 +++---- test/unit-test/test/host/audio/test_audio_host.c | 38 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index c29db658c..571680885 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -237,14 +237,11 @@ static bool audioh_format_from_uac1(uint8_t subframe_size, uint8_t bit_resolutio 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 +// Isochronous bInterval is a power-of-2 exponent in 1 ms full-speed frames +// or 125 us high-speed 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; + const uint32_t unit_us = (tuh_speed_get(daddr) == TUSB_SPEED_HIGH) ? 125u : 1000u; + return ((uint32_t)1u << (ep_interval - 1)) * unit_us; } // UAC 1.0 feature-unit control value width (0 = unsupported variable/unknown width) 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 37b35dae8..b64335910 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -444,6 +444,17 @@ 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 const uint8_t playback_11025_interval4[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_AS_ALT0, + TEST_UAC1_AS_INTERFACE(1, 1), + TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), + TEST_UAC1_FORMAT(2, 2, 16, 11025), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 180, 3), + TEST_UAC1_CS_DATA_EP, +}; + static uint8_t fu_cb_count; static uintptr_t fu_cb_user_data; @@ -814,6 +825,33 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes[9]); } +void test_audio_host_uses_exponential_full_speed_iso_interval(void) { + uint8_t samples[441 * 4] = {0}; + mount_descriptors(playback_11025_interval4, sizeof(playback_11025_interval4)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT32(256, tuh_audio_write(0, 0, samples, 256)); + complete_interface_set(XFER_RESULT_SUCCESS); + + while (edpt_xfer_count < 5) { + edpt_busy = false; + 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) { + edpt_busy = false; + 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++) { + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[i]); + } + TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes[9]); +} + void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) { uint8_t sample[4] = {1, 2, 3, 4}; uint8_t more_samples[43 * 4]; -- cgit v1.3.1 From 28a63e7c75c7e430ed3820d2b3658614411ca307 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:07 +0200 Subject: refactor(audio): simplify class endpoint association Keep the sampling-frequency control flag in the alternate-setting parser state shared by its single data endpoint. This supports either descriptor order without separate pending and unassigned endpoint flags. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 571680885..28f228dac 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -568,11 +568,6 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de } audioh_ep_info_t; audioh_ep_info_t ep_info = {0}; bool has_data_ep = false; - // 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)) { @@ -609,16 +604,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de } 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.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; - } + 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; } break; } @@ -657,9 +644,6 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de } ep_info.ep_sync = desc_endpoint->bmAttributes.sync; ep_info.ep_usage = desc_endpoint->bmAttributes.usage; - ep_info.sam_freq_ctrl = pending_sam_freq_ctrl; - pending_sam_freq_ctrl = false; - unassigned_ep = !ep_info.sam_freq_ctrl; has_data_ep = true; } break; -- cgit v1.3.1 From d494b7c5f1fb690ea559dd3f09bb9f84c0c6d80f Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:29:08 +0200 Subject: fix(audio): validate descriptors before parsing Check descriptor bounds, minimum lengths, frequency counts, and interface types with the host validation helpers before accessing fields. Malformed UAC1 functions now fail enumeration without out-of-bounds reads. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 141 ++++++++++++++++------- test/unit-test/test/host/audio/test_audio_host.c | 42 +++++++ 2 files changed, 140 insertions(+), 43 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 28f228dac..c12493a5c 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -197,6 +197,16 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { return TUSB_INDEX_INVALID_8; } +static bool audioh_desc_valid(const uint8_t *p_desc, const uint8_t *desc_end, uint8_t min_len) { + if (p_desc >= desc_end) { + return false; + } + + const size_t remaining = (size_t)(desc_end - p_desc); + return TUH_VALIDATE_BASIC(remaining >= min_len) && TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= min_len) && + TUH_VALIDATE_BASIC(tu_desc_len(p_desc) <= remaining); +} + static tuh_audio_stream_t *audioh_get_stream(audioh_interface_t *p_audio, tusb_dir_t direction) { switch (direction) { case TUSB_DIR_IN: @@ -534,6 +544,8 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin // 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) { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, sizeof(tusb_desc_interface_t)), NULL); + const uint8_t itf_num = desc_itf->bInterfaceNumber; const uint8_t alt = desc_itf->bAlternateSetting; @@ -541,7 +553,11 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de // 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) { + while (p_desc < desc_end) { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL); + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } p_desc = tu_desc_next(p_desc); } return p_desc; @@ -569,19 +585,24 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de audioh_ep_info_t ep_info = {0}; bool has_data_ep = false; - while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + while (p_desc < desc_end) { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL); + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + 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; - if (desc_as_general->bLength >= sizeof(audio10_desc_cs_as_interface_t)) { - format_tag = tu_le16toh(desc_as_general->wFormatTag); - } + format_tag = tu_le16toh(desc_as_general->wFormatTag); break; } case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { - TU_ASSERT(p_desc[0] >= 8, p_desc); + 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 } @@ -589,8 +610,9 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de subframe_size = p_desc[5]; bit_res = p_desc[6]; 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); - for (uint8_t i = 0; i < sam_freq_count && (8 + i * 3 + 2) < p_desc[0]; i++) { + for (uint8_t i = 0; i < sam_freq_count; 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)); } @@ -603,13 +625,16 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de break; } case TUSB_DESC_CS_ENDPOINT: { - if (tu_desc_subtype(p_desc) == AUDIO10_CS_EP_SUBTYPE_GENERAL && p_desc[0] >= 4) { + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 3), NULL); + if (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; } break; } case TUSB_DESC_ENDPOINT: { + TU_VERIFY(TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(tusb_desc_endpoint_t)), NULL); const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; if (desc_endpoint->bmAttributes.xfer != TUSB_XFER_ISOCHRONOUS) { break; @@ -757,13 +782,16 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { (void)rhport; - 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(TUH_VALIDATE_BASIC(max_len >= sizeof(tusb_desc_interface_t)), 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; + TU_VERIFY(audioh_desc_valid(p_desc, desc_end, sizeof(tusb_desc_interface_t)), 0); + 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); const uint8_t idx = find_new_audio_index(); TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); @@ -787,13 +815,25 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface audioh_fu_info_t pending_fu[TUH_AUDIO_STREAM_DIRECTION_COUNT] = {0}; p_desc = tu_desc_next(p_desc); - while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + while (p_desc < desc_end) { + if (!audioh_desc_valid(p_desc, desc_end, 2)) { + goto open_failed; + } + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + if (tu_desc_type(p_desc) == TUSB_DESC_CS_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 (terminal->bLength >= sizeof(audio10_desc_input_terminal_t) && - tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING && usb_input_terminal_id == 0) { + 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) { @@ -807,9 +847,11 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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 (terminal->bLength >= sizeof(audio10_desc_output_terminal_t) && - tu_le16toh(terminal->wTerminalType) == AUDIO_TERM_TYPE_USB_STREAMING && usb_output_source_id == 0) { + 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) { @@ -823,25 +865,26 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface break; } case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { - if (p_desc[0] >= 5) { - 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]; - 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]; - 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]; - break; - } + if (!TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= 5)) { + goto open_failed; + } + 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]; + 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]; + 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]; + break; } } } @@ -855,12 +898,18 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } // Parse the contiguous Audio Streaming interfaces of this audio function. - while (tu_desc_in_bounds(p_desc, desc_end)) { + while (p_desc < desc_end) { + if (!audioh_desc_valid(p_desc, desc_end, 2)) { + goto open_failed; + } if (tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { p_desc = tu_desc_next(p_desc); continue; } + if (!audioh_desc_valid(p_desc, desc_end, sizeof(tusb_desc_interface_t))) { + goto open_failed; + } 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) { @@ -870,18 +919,15 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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); + 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_stream_reset(&p_audio->in_stream); - audioh_stream_reset(&p_audio->out_stream); - p_audio->daddr = 0; - p_audio->ac_itf_num = 0; - p_audio->stream_count = 0; - p_audio->mounted = false; - return 0; + goto open_failed; } // Assign stream indices: playback first, then capture, so the application @@ -896,6 +942,15 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface p_audio->stream_count = stream_idx; return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); + +open_failed: + audioh_stream_reset(&p_audio->in_stream); + audioh_stream_reset(&p_audio->out_stream); + p_audio->daddr = 0; + p_audio->ac_itf_num = 0; + p_audio->stream_count = 0; + p_audio->mounted = false; + return 0; } //--------------------------------------------------------------------+ 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 b64335910..2b0f2fa3d 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -417,6 +417,31 @@ static const uint8_t playback_with_cs_ep_before_data_ep[] = { TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 192, 1), }; +static const uint8_t malformed_zero_length_ac_descriptor[] = { + TEST_UAC1_AC_HEADER, + 0, TUSB_DESC_CS_INTERFACE, +}; + +static const uint8_t malformed_sampling_frequency_list[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + 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, + U24_TO_U8S_LE(48000), +}; + +static const uint8_t malformed_short_endpoint[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_AS_ALT0, + 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), +}; + static const uint8_t playback_with_two_alternates[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -719,6 +744,23 @@ void test_audio_host_rejects_overflowed_frame_size_for_large_channel_count(void) TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); } +void test_audio_host_rejects_malformed_descriptors_and_releases_instance(void) { + const struct { + const uint8_t *desc; + uint16_t len; + } malformed[] = { + {malformed_zero_length_ac_descriptor, sizeof(malformed_zero_length_ac_descriptor)}, + {malformed_sampling_frequency_list, sizeof(malformed_sampling_frequency_list)}, + {malformed_short_endpoint, sizeof(malformed_short_endpoint)}, + }; + + 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_UINT8(0, tuh_audio_get_dev_addr(0)); + } +} + 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)); -- 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 'src/class') 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 e2c6dcf20a55b74c763411ba2162884a346f52a6 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 11:30:51 +0200 Subject: address copilot review findings Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 6 ++-- src/class/audio/audio_host.c | 11 +++++-- test/unit-test/test/host/audio/test_audio_host.c | 38 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 5413895a4..27e6425b8 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -52,8 +52,9 @@ make BOARD= all ## Flashing ```bash -# Using CMake -ninja flash +# Using CMake: list the board-specific flash targets, then select one +ninja -t targets +ninja audio_host-jlink # example for a board with J-Link support # Using Make make BOARD= flash @@ -111,5 +112,6 @@ Edit `src/tusb_config.h` to modify: ## Notes - 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 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. - 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/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 2a8df9075..98822b9c0 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -325,6 +325,8 @@ 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); + // Re-arm the capture endpoint: request one full packet (the device sends at // most its max packet size per poll interval). The overwritable FIFO retains // the newest capture frames when the application cannot drain it in time. @@ -335,7 +337,9 @@ static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { 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), ); + if (!usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, map->ep_size)) { + audioh_stream_error(s, 0); + } } // Submit the next queued playback packet. Fractional frames per endpoint poll @@ -366,7 +370,10 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { 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), ); + if (!usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, bytes)) { + audioh_stream_error(s, 0); + return; + } s->rem_acc = next_rem_acc; } 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 f02c44095..e5056bc2c 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -123,6 +123,9 @@ bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t *bu memcpy(edpt_xfer_data[edpt_xfer_count], buffer, TU_MIN(sizeof(edpt_xfer_data[0]), total_bytes)); edpt_xfer_count++; } + if (!edpt_xfer_result) { + edpt_busy = false; // match usbh_edpt_xfer() cleanup after HCD rejection + } return edpt_xfer_result; } @@ -749,6 +752,41 @@ void test_audio_host_reports_asynchronous_start_failures(void) { TEST_ASSERT_EQUAL_UINT8(3, err_cb_count); } +void test_audio_host_reports_capture_submission_failure(void) { + mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + + edpt_xfer_result = false; + 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, 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); + + edpt_xfer_result = true; + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); +} + +void test_audio_host_reports_playback_submission_failure(void) { + mount_descriptors(playback_44100_max_packets_only, sizeof(playback_44100_max_packets_only)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + + edpt_xfer_result = false; + 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); + + edpt_xfer_result = true; + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); +} + 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 485d8a245dad8acc2fb6e91b66cfe86f4fa3a022 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 26 Aug 2026 09:16:49 +0200 Subject: implement explicit feeback support Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 2 +- src/class/audio/audio_host.c | 260 +++++++++++++++++++---- test/unit-test/test/host/audio/test_audio_host.c | 196 ++++++++++++++--- 3 files changed, 383 insertions(+), 75 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 27e6425b8..06cc4f197 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -24,7 +24,7 @@ The echo needs a matching S16_LE playback stream at the capture sample rate; dev ## Limitations and trade-offs -- 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. +- 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. diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 98822b9c0..b34b7534b 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -112,6 +112,10 @@ typedef struct { uint8_t ep_sync; // bmAttributes sync type uint8_t ep_usage; // bmAttributes usage type bool sam_freq_ctrl; // endpoint supports sampling-frequency control + uint8_t fb_ep_addr; // explicit feedback endpoint address (0 = none) + uint8_t fb_ep_size; // feedback endpoint max packet size (3 or 4) + uint8_t fb_ep_interval; + uint8_t fb_ep_attr; } audioh_stream_map_t; // One logical stream (capture or playback) @@ -143,11 +147,17 @@ typedef struct { // Size in bytes of one frame (all channels) of the active configuration uint16_t frame_bytes; - // Playback pacing per endpoint poll interval. Fractional frames are - // accumulated in millionths because the interval is expressed in us. - uint16_t frames_per_interval; - uint32_t frames_rem; - uint32_t rem_acc; + // Playback pacing in Q16.16 frames per data-endpoint poll interval. + // Feedback is latched only when the current fractional scheduling cycle + // wraps, so one cycle is never generated from two different rates. + uint32_t nominal_frames_q16; + uint32_t target_frames_q16; + uint32_t pending_frames_q16; + uint32_t feedback_min_q16; + uint32_t feedback_max_q16; + uint16_t rem_acc; + bool feedback_pending; + bool feedback_opened; // FIFO + endpoint transfer helper (see tu_edpt_stream, used by the MIDI // host driver): the FIFO decouples the application's frame-based read/write @@ -174,6 +184,7 @@ typedef struct { typedef struct { TUH_EPBUF_DEF(sam_freq, 4); // shared sampling-frequency SET data TUH_EPBUF_DEF(fu_ctrl, 8); // feature-unit SET data + TUH_EPBUF_DEF(feedback, 4); // explicit-feedback transfer buffer 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 operation in flight per device @@ -267,6 +278,14 @@ static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) { return ((uint32_t)1u << (ep_interval - 1)) * unit_us; } +// Convert a nominal sample rate to Q16.16 frames per data endpoint poll +// interval. Round to the nearest representable value to preserve common +// fractional rates such as 44.1 frames/ms. +static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_interval, uint8_t daddr) { + const uint64_t numerator = (uint64_t)sample_rate * audioh_interval_us(ep_interval, daddr) * 65536u; + return (uint32_t)((numerator + 500000u) / 1000000u); +} + // UAC 1.0 feature-unit control value width (0 = unsupported variable/unknown width) static uint8_t audioh_fu_control_width(uint8_t control_selector) { switch (control_selector) { @@ -288,20 +307,25 @@ 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->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; - s->rem_acc = 0; + 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->mute_supported = false; + s->volume_supported = false; + s->volume_range = (tuh_audio_volume_range_t){0}; + s->frame_bytes = 0; + s->nominal_frames_q16 = 0; + s->target_frames_q16 = 0; + s->pending_frames_q16 = 0; + s->feedback_min_q16 = 0; + s->feedback_max_q16 = 0; + s->rem_acc = 0; + s->feedback_pending = false; + s->feedback_opened = false; tu_edpt_stream_close(&s->edpt); tu_edpt_stream_clear(&s->edpt); } @@ -312,9 +336,11 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) 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; + if (stream->daddr == dev_addr && stream->active_config != TUSB_INDEX_INVALID_8) { + const audioh_stream_map_t *map = &stream->map[stream->active_config]; + if (map->ep_addr == ep_addr || (map->fb_ep_addr != 0 && map->fb_ep_addr == ep_addr)) { + return stream; + } } } } @@ -327,6 +353,17 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes); +static void audioh_stream_feedback_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(map->fb_ep_addr != 0, ); + TU_VERIFY(usbh_edpt_claim(s->daddr, map->fb_ep_addr), ); + if (!usbh_edpt_xfer(s->daddr, map->fb_ep_addr, _audioh_epbuf[s->idx].feedback, map->fb_ep_size)) { + audioh_stream_error(s, 0); + } +} + // Re-arm the capture endpoint: request one full packet (the device sends at // most its max packet size per poll interval). The overwritable FIFO retains // the newest capture frames when the application cannot drain it in time. @@ -344,18 +381,21 @@ static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { // Submit the next queued playback packet. Fractional frames per endpoint poll // interval are accumulated on each successful submission, keeping the average -// data rate exactly at the sample rate. +// data rate at the active nominal or feedback target. 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 - uint32_t frames = s->frames_per_interval; - uint32_t next_rem_acc = s->rem_acc + s->frames_rem; - if (next_rem_acc >= 1000000) { - next_rem_acc -= 1000000; + uint32_t frames = s->target_frames_q16 >> 16; + const uint32_t fraction = s->target_frames_q16 & 0xFFFFu; + uint32_t next_rem_acc = s->rem_acc + fraction; + bool loop_done = (fraction == 0); + if (next_rem_acc >= 65536u) { + next_rem_acc -= 65536u; frames++; + loop_done = true; } const uint64_t bytes_64 = (uint64_t)frames * s->frame_bytes; @@ -374,7 +414,12 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { audioh_stream_error(s, 0); return; } - s->rem_acc = next_rem_acc; + s->rem_acc = (uint16_t)next_rem_acc; + if (loop_done && s->feedback_pending) { + s->target_frames_q16 = s->pending_frames_q16; + s->feedback_pending = false; + s->rem_acc = 0; + } } //--------------------------------------------------------------------+ @@ -382,6 +427,15 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { //--------------------------------------------------------------------+ static bool audioh_stream_close_ep(tuh_audio_stream_t *s) { + if (s->feedback_opened) { + const uint8_t fb_ep_addr = s->map[s->active_config].fb_ep_addr; + if (!tuh_edpt_close(s->daddr, fb_ep_addr)) { + TU_LOG_DRV(" AUDIO close feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, fb_ep_addr); + return false; + } + s->feedback_opened = false; + } + if (!tu_edpt_stream_is_opened(&s->edpt)) { return true; } @@ -404,7 +458,10 @@ static void audioh_stream_fail(tuh_audio_stream_t *s) { } static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes) { - s->running = false; + s->running = false; + s->target_frames_q16 = s->nominal_frames_q16; + s->feedback_pending = false; + s->rem_acc = 0; tu_edpt_stream_clear(&s->edpt); tuh_audio_err_cb(s->idx, s->stream_idx, xferred_bytes); } @@ -458,6 +515,24 @@ static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { 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->fb_ep_addr != 0) { + const tusb_desc_endpoint_t desc_fb = {.bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = map->fb_ep_addr, + .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS, + .sync = (map->fb_ep_attr >> 2) & 0x03u, + .usage = (map->fb_ep_attr >> 4) & 0x03u}, + .wMaxPacketSize = tu_htole16(map->fb_ep_size), + .bInterval = map->fb_ep_interval}; + if (!tuh_edpt_open(s->daddr, &desc_fb)) { + TU_LOG_DRV(" AUDIO open feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, map->fb_ep_addr); + audioh_stream_fail(s); + return false; + } + s->feedback_opened = true; + } + s->state = STREAM_STATE_READY; return true; } @@ -523,6 +598,46 @@ void audioh_close(uint8_t daddr) { } } +static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_bytes) { + const uint8_t *fb = _audioh_epbuf[s->idx].feedback; + uint32_t feedback_q16; + if (xferred_bytes == 3) { + // Full-speed feedback is normally Q10.14. Keep the scheduler in Q16.16. + feedback_q16 = ((uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16)) << 2; + } else if (xferred_bytes == 4) { + feedback_q16 = (uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16) | ((uint32_t)fb[3] << 24); + } else { + TU_LOG_DRV(" AUDIO invalid feedback length: %lu\r\n", (unsigned long)xferred_bytes); + return; + } + + const audioh_stream_map_t *map = &s->map[s->active_config]; + if (feedback_q16 < s->feedback_min_q16 || feedback_q16 > s->feedback_max_q16) { + TU_LOG_DRV(" AUDIO feedback out of range: 0x%08lx\r\n", (unsigned long)feedback_q16); + return; + } + + // Feedback is expressed per USB frame/microframe. Scale it to the data + // endpoint's polling interval before handing it to the packet scheduler. + const uint64_t target_q16_64 = (uint64_t)feedback_q16 << (map->ep_interval - 1u); + if (target_q16_64 > UINT32_MAX) { + return; + } + + const uint32_t target_q16 = (uint32_t)target_q16_64; + const uint64_t max_bytes = (((uint64_t)target_q16 + 0xFFFFu) >> 16) * s->frame_bytes; + if (max_bytes == 0 || max_bytes > map->ep_size || max_bytes > CFG_TUH_AUDIO_EPOUT_BUFSIZE || + max_bytes > CFG_TUH_AUDIO_STREAM_BUFSIZE) { + TU_LOG_DRV(" AUDIO feedback exceeds playback packet capacity: 0x%08lx\r\n", (unsigned long)feedback_q16); + return; + } + + // Keep only the newest feedback sample. The packet scheduler promotes it at + // the end of its current fractional cycle. + s->pending_frames_q16 = target_q16; + s->feedback_pending = true; +} + bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { tuh_audio_stream_t *s = audioh_find_stream(dev_addr, ep_addr); if (s == NULL) { @@ -541,6 +656,13 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin return true; } + const audioh_stream_map_t *map = &s->map[s->active_config]; + if (ep_addr == map->fb_ep_addr) { + audioh_feedback_received(s, xferred_bytes); + audioh_stream_feedback_xfer(s); + 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 @@ -605,8 +727,10 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de uint8_t ep_usage; bool sam_freq_ctrl; } audioh_ep_info_t; - audioh_ep_info_t ep_info = {0}; - bool has_data_ep = false; + audioh_ep_info_t ep_info = {0}; + audioh_ep_info_t fb_info = {0}; + bool has_data_ep = false; + bool has_feedback_ep = false; while (p_desc < desc_end) { TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL); @@ -667,12 +791,27 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de const bool implicit_feedback = usage == (TUSB_ISO_EP_ATT_IMPLICIT_FB >> 4) && tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN; const bool explicit_feedback = - usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4) || - (usage == (TUSB_ISO_EP_ATT_DATA >> 4) && desc_endpoint->bmAttributes.sync == TUSB_ISO_EP_ATT_NO_SYNC); + tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN && + (usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4) || + (usage == (TUSB_ISO_EP_ATT_DATA >> 4) && desc_endpoint->bmAttributes.sync == TUSB_ISO_EP_ATT_NO_SYNC)); if (explicit_feedback) { - TU_LOG_DRV(" AUDIO AS itf %u alt %u: explicit feedback ep %02x ignored\r\n", itf_num, alt, - desc_endpoint->bEndpointAddress); + const uint16_t fb_ep_size = tu_edpt_packet_size(desc_endpoint); + if (has_feedback_ep || (fb_ep_size != 3 && fb_ep_size != 4)) { + TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid/extra feedback ep %02x ignored\r\n", itf_num, alt, + desc_endpoint->bEndpointAddress); + break; + } + + fb_info.ep_addr = desc_endpoint->bEndpointAddress; + fb_info.ep_size = fb_ep_size; + fb_info.ep_interval = desc_endpoint->bInterval; + if (fb_info.ep_interval == 0 || fb_info.ep_interval > 16) { + fb_info.ep_interval = 1; + } + fb_info.ep_sync = desc_endpoint->bmAttributes.sync; + fb_info.ep_usage = desc_endpoint->bmAttributes.usage; + has_feedback_ep = true; break; } @@ -796,6 +935,16 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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->map[stream->config_count].fb_ep_addr = 0; + stream->map[stream->config_count].fb_ep_size = 0; + stream->map[stream->config_count].fb_ep_interval = 0; + stream->map[stream->config_count].fb_ep_attr = 0; + if (stream->dir == TUSB_DIR_OUT && has_feedback_ep) { + stream->map[stream->config_count].fb_ep_addr = fb_info.ep_addr; + stream->map[stream->config_count].fb_ep_size = (uint8_t)fb_info.ep_size; + stream->map[stream->config_count].fb_ep_interval = fb_info.ep_interval; + stream->map[stream->config_count].fb_ep_attr = (uint8_t)((fb_info.ep_sync << 2) | (fb_info.ep_usage << 4)); + } stream->config_count++; } @@ -1123,6 +1272,9 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx 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); + if (s->feedback_opened) { + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].fb_ep_addr), false); + } } tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; @@ -1139,14 +1291,18 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // the same address, since its packet size and interval may have changed. TU_VERIFY(audioh_stream_close_ep(s), false); - const audioh_stream_map_t *map = &s->map[config_idx]; - const uint64_t frames_numerator = (uint64_t)cfg->sample_rate * audioh_interval_us(map->ep_interval, s->daddr); - s->active_config = config_idx; - s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(cfg); - s->frames_per_interval = (uint16_t)(frames_numerator / 1000000u); - s->frames_rem = (uint32_t)(frames_numerator % 1000000u); - s->rem_acc = 0; - s->state = STREAM_STATE_IDLE; + const audioh_stream_map_t *map = &s->map[config_idx]; + const uint32_t frame_div = (tuh_speed_get(s->daddr) == TUSB_SPEED_HIGH) ? 8000u : 1000u; + s->active_config = config_idx; + s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(cfg); + s->nominal_frames_q16 = audioh_nominal_frames_q16(cfg->sample_rate, map->ep_interval, s->daddr); + s->target_frames_q16 = s->nominal_frames_q16; + s->pending_frames_q16 = 0; + s->feedback_min_q16 = ((cfg->sample_rate - 1u) / frame_div) << 16; + s->feedback_max_q16 = (cfg->sample_rate / frame_div + 1u) << 16; + s->feedback_pending = false; + s->rem_acc = 0; + 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. @@ -1171,6 +1327,12 @@ 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 { + if (s->map[s->active_config].fb_ep_addr != 0) { + audioh_stream_feedback_xfer(s); + } + if (!s->running) { + return; + } audioh_stream_playback_xfer(s); // start the continuous playback transfer chain } } @@ -1224,8 +1386,14 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { 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); + if (s->feedback_opened) { + TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].fb_ep_addr), false); + } - s->running = true; + s->target_frames_q16 = s->nominal_frames_q16; + s->feedback_pending = false; + s->rem_acc = 0; + s->running = true; // Activate the interface's alternate setting asynchronously: transfers // begin once SET_INTERFACE and sampling-frequency control complete. const audioh_stream_map_t *map = &s->map[s->active_config]; @@ -1262,7 +1430,9 @@ bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { // The in-flight transfer (if any) completes and its data is discarded; // queued frames are dropped as well. The interface is being deactivated so // the device stops transferring. - s->running = false; + s->running = false; + s->target_frames_q16 = s->nominal_frames_q16; + s->feedback_pending = false; tu_edpt_stream_clear(&s->edpt); s->rem_acc = 0; // restart the pacing accumulator on the next tuh_audio_start() return true; 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 e5056bc2c..3e11e733c 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -43,8 +43,9 @@ static bool edpt_close_result; static uint8_t closed_ep[4]; static uint8_t edpt_close_count; -static bool edpt_busy; +static uint32_t edpt_busy_mask; static bool edpt_xfer_result; +static uint8_t edpt_xfer_ep[16]; static uint16_t edpt_xfer_bytes[16]; static uint8_t edpt_xfer_data[16][8]; static uint8_t *edpt_xfer_buffer[16]; @@ -55,6 +56,15 @@ static uint8_t err_cb_idx; static uint8_t err_cb_stream_idx; static uint16_t err_cb_xferred_bytes; +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; +} + +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; @@ -93,7 +103,7 @@ bool tuh_edpt_close(uint8_t daddr, uint8_t ep_addr) { closed_ep[edpt_close_count++] = ep_addr; } if (edpt_close_result) { - edpt_busy = false; + complete_edpt(ep_addr); } return edpt_close_result; } @@ -114,42 +124,40 @@ bool tuh_interface_set(uint8_t daddr, uint8_t itf_num, uint8_t itf_alt, tuh_xfer bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { (void)dev_addr; - (void)ep_addr; (void)complete_cb; (void)user_data; if (edpt_xfer_count < TU_ARRAY_SIZE(edpt_xfer_bytes)) { + edpt_xfer_ep[edpt_xfer_count] = ep_addr; edpt_xfer_bytes[edpt_xfer_count] = total_bytes; edpt_xfer_buffer[edpt_xfer_count] = buffer; memcpy(edpt_xfer_data[edpt_xfer_count], buffer, TU_MIN(sizeof(edpt_xfer_data[0]), total_bytes)); edpt_xfer_count++; } if (!edpt_xfer_result) { - edpt_busy = false; // match usbh_edpt_xfer() cleanup after HCD rejection + complete_edpt(ep_addr); // match usbh_edpt_xfer() cleanup after HCD rejection } return edpt_xfer_result; } bool usbh_edpt_claim(uint8_t dev_addr, uint8_t ep_addr) { (void)dev_addr; - (void)ep_addr; - if (edpt_busy) { + const uint32_t mask = edpt_mask(ep_addr); + if (edpt_busy_mask & mask) { return false; } - edpt_busy = true; + edpt_busy_mask |= mask; return true; } bool usbh_edpt_release(uint8_t dev_addr, uint8_t ep_addr) { (void)dev_addr; - (void)ep_addr; - edpt_busy = false; + complete_edpt(ep_addr); return true; } bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) { (void)dev_addr; - (void)ep_addr; - return edpt_busy; + return (edpt_busy_mask & edpt_mask(ep_addr)) != 0; } void usbh_driver_set_config_complete(uint8_t dev_addr, uint8_t itf_num) { @@ -487,6 +495,30 @@ 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 const uint8_t playback_44100_with_feedback_10_14[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_AS_ALT0, + TEST_UAC1_AS_INTERFACE(1, 2), + TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), + TEST_UAC1_FORMAT(2, 2, 16, 44100), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 180, 1), + TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_MAX_PACKETS_ONLY), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_EXPLICIT_FB, 3, 1), +}; + +static const uint8_t playback_44100_with_feedback_16_16[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_AS_ALT0, + TEST_UAC1_AS_INTERFACE(1, 2), + TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), + TEST_UAC1_FORMAT(2, 2, 16, 44100), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 180, 1), + TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_MAX_PACKETS_ONLY), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_EXPLICIT_FB, 4, 1), +}; + static const uint8_t playback_11025_interval4[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), @@ -529,6 +561,50 @@ static void complete_control_xfer_with_u16(uint16_t value) { 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++) { + if (edpt_xfer_ep[i] == ep_addr && found++ == ep_xfer_idx) { + return edpt_xfer_bytes[i]; + } + } + TEST_FAIL_MESSAGE("Endpoint transfer not found"); + return 0; +} + +static uint8_t *edpt_xfer_buffer_at(uint8_t ep_addr, uint8_t ep_xfer_idx) { + uint8_t found = 0; + for (uint8_t i = 0; i < edpt_xfer_count; i++) { + if (edpt_xfer_ep[i] == ep_addr && found++ == ep_xfer_idx) { + return edpt_xfer_buffer[i]; + } + } + TEST_FAIL_MESSAGE("Endpoint transfer not found"); + return NULL; +} + +static void complete_playback_xfer(void) { + complete_edpt(0x01); + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, 0)); +} + +static void complete_feedback_xfer(uint32_t feedback_q16, uint8_t length) { + uint8_t *buffer = edpt_xfer_buffer_at(0x81, 0); + if (length == 3) { + const uint32_t feedback_q14 = feedback_q16 >> 2; + buffer[0] = (uint8_t)feedback_q14; + buffer[1] = (uint8_t)(feedback_q14 >> 8); + buffer[2] = (uint8_t)(feedback_q14 >> 16); + } else { + buffer[0] = (uint8_t)feedback_q16; + buffer[1] = (uint8_t)(feedback_q16 >> 8); + buffer[2] = (uint8_t)(feedback_q16 >> 16); + buffer[3] = (uint8_t)(feedback_q16 >> 24); + } + complete_edpt(0x81); + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, length)); +} + void setUp(void) { interface_set_result = true; memset(&interface_xfer, 0, sizeof(interface_xfer)); @@ -549,8 +625,9 @@ void setUp(void) { memset(closed_ep, 0, sizeof(closed_ep)); edpt_close_count = 0; - edpt_busy = false; + edpt_busy_mask = 0; edpt_xfer_result = true; + memset(edpt_xfer_ep, 0, sizeof(edpt_xfer_ep)); memset(edpt_xfer_bytes, 0, sizeof(edpt_xfer_bytes)); memset(edpt_xfer_data, 0, sizeof(edpt_xfer_data)); memset(edpt_xfer_buffer, 0, sizeof(edpt_xfer_buffer)); @@ -598,17 +675,22 @@ static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) { control_xfer_count = 0; } -void test_audio_host_ignores_explicit_feedback_endpoint(void) { +void test_audio_host_saves_and_opens_explicit_feedback_endpoint(void) { // A MIDI-only AC collection must not consume an Audio Host instance. TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)midi_only_collection, sizeof(midi_only_collection))); TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); - open_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); + mount_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_PLAYBACK, tuh_audio_stream_direction(0, 0)); TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_config_count(0, 0)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_EQUAL_UINT8(2, edpt_open_count); + TEST_ASSERT_EQUAL_HEX8(0x01, opened_ep[0].bEndpointAddress); + TEST_ASSERT_EQUAL_HEX8(0x81, opened_ep[1].bEndpointAddress); + TEST_ASSERT_EQUAL_UINT16(3, tu_edpt_packet_size(&opened_ep[1])); } void test_audio_host_rejects_uac2_interface_without_consuming_instance(void) { @@ -660,7 +742,7 @@ void test_audio_host_maps_capture_fu_declared_before_usb_output_terminal(void) { for (uint8_t packet = 0; packet < 11; packet++) { TEST_ASSERT_NOT_NULL(edpt_xfer_buffer[packet]); memset(edpt_xfer_buffer[packet], packet + 1, 96); - edpt_busy = false; + complete_edpt(0x81); TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96)); TEST_ASSERT_EQUAL_UINT8(packet + 2, edpt_xfer_count); } @@ -716,7 +798,7 @@ void test_audio_host_sets_sampling_frequency_after_each_stream_activation(void) TEST_ASSERT_EQUAL_UINT8(0, interface_alt); TEST_ASSERT_EQUAL_UINT8(2, interface_set_count); complete_interface_set(XFER_RESULT_SUCCESS); - edpt_busy = false; + complete_edpt(0x81); TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96)); TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); @@ -798,7 +880,7 @@ void test_audio_host_keeps_running_when_stop_cannot_be_submitted(void) { interface_set_result = false; TEST_ASSERT_FALSE(tuh_audio_stop(0, 0)); - edpt_busy = false; + complete_edpt(0x81); TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96)); TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count); } @@ -1050,13 +1132,15 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); while (edpt_xfer_count < 5) { - edpt_busy = false; - TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1])); + 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_EQUAL_UINT32(185, tuh_audio_write(0, 0, samples, 185)); while (edpt_xfer_count < 10) { - edpt_busy = false; - TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1])); + complete_edpt(0x01); + 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++) { @@ -1065,6 +1149,62 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes[9]); } +void test_audio_host_applies_10_14_feedback_after_fractional_scheduling_loop(void) { + mount_descriptors(playback_44100_with_feedback_10_14, sizeof(playback_44100_with_feedback_10_14)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT16(3, edpt_xfer_bytes_at(0x81, 0)); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 0)); + + // Request 44 frames/ms after the first nominal 44.1-kHz packet. The old + // cycle must still emit its 45-frame correction packet before changing rate. + complete_feedback_xfer(44u << 16, 3); + for (uint8_t i = 0; i < 10; i++) { + complete_playback_xfer(); + } + + for (uint8_t i = 0; i < 9; i++) { + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, i)); + } + TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9)); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 10)); +} + +void test_audio_host_accepts_16_16_feedback(void) { + mount_descriptors(playback_44100_with_feedback_16_16, sizeof(playback_44100_with_feedback_16_16)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT16(4, edpt_xfer_bytes_at(0x81, 0)); + + complete_feedback_xfer(45u << 16, 4); + for (uint8_t i = 0; i < 10; i++) { + complete_playback_xfer(); + } + + TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9)); + TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 10)); +} + +void test_audio_host_ignores_out_of_range_feedback(void) { + mount_descriptors(playback_44100_with_feedback_16_16, sizeof(playback_44100_with_feedback_16_16)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + + complete_feedback_xfer(50u << 16, 4); + for (uint8_t i = 0; i < 10; i++) { + complete_playback_xfer(); + } + + TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9)); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 10)); +} + void test_audio_host_uses_exponential_full_speed_iso_interval(void) { uint8_t samples[441 * 4] = {0}; mount_descriptors(playback_11025_interval4, sizeof(playback_11025_interval4)); @@ -1075,15 +1215,13 @@ void test_audio_host_uses_exponential_full_speed_iso_interval(void) { complete_interface_set(XFER_RESULT_SUCCESS); while (edpt_xfer_count < 5) { - edpt_busy = false; - TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, - edpt_xfer_bytes[edpt_xfer_count - 1])); + 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_EQUAL_UINT32(185, tuh_audio_write(0, 0, samples, 185)); while (edpt_xfer_count < 10) { - edpt_busy = false; - TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, - edpt_xfer_bytes[edpt_xfer_count - 1])); + complete_edpt(0x01); + 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++) { @@ -1107,7 +1245,7 @@ void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) { TEST_ASSERT_EACH_EQUAL_HEX8(0, edpt_xfer_data[0], sizeof(edpt_xfer_data[0])); TEST_ASSERT_EQUAL_UINT32(1, tuh_audio_write(0, 0, sample, 1)); - edpt_busy = false; + complete_edpt(0x01); TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[0])); TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count); @@ -1115,7 +1253,7 @@ void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) { TEST_ASSERT_EACH_EQUAL_HEX8(0, edpt_xfer_data[1], sizeof(edpt_xfer_data[1])); TEST_ASSERT_EQUAL_UINT32(43, tuh_audio_write(0, 0, more_samples, 43)); - edpt_busy = false; + complete_edpt(0x01); TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[1])); TEST_ASSERT_EQUAL_UINT8(3, edpt_xfer_count); -- cgit v1.3.1 From d57637f9c251135a1ab6791a627aaf195f1e84a7 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 26 Aug 2026 11:00:40 +0200 Subject: audio: compact alternate-setting storage Store each Audio Streaming alternate setting once and flatten its sample rates only at the public API boundary. Keep feedback and pacing state playback-only, eliminating duplicate configuration fields from capture streams. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 638 +++++++++++++---------- src/class/audio/audio_host.h | 2 +- test/unit-test/test/host/audio/test_audio_host.c | 8 +- 3 files changed, 380 insertions(+), 268 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index b34b7534b..8caecf3d2 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -29,10 +29,10 @@ * 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(). + * settings in one direction are presented as a flat list of discrete + * {format, sample_rate, channels} tuples. Internally, configurations are + * 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. @@ -63,9 +63,6 @@ // 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) - //--------------------------------------------------------------------+ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ @@ -102,21 +99,45 @@ enum { STREAM_STATE_READY // configured, ready to start/stop }; -// Hardware mapping of one supported configuration +// 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; + uint8_t ep_addr; + uint8_t ep_interval; + uint8_t ep_attr; // bmAttributes synchronization and usage bits + uint8_t format; + uint8_t channels; + uint8_t sample_rate_count; + bool sam_freq_ctrl; +} audioh_as_config_t; + +// Explicit feedback exists only for playback alternate settings. 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 - uint8_t fb_ep_addr; // explicit feedback endpoint address (0 = none) - uint8_t fb_ep_size; // feedback endpoint max packet size (3 or 4) - uint8_t fb_ep_interval; - uint8_t fb_ep_attr; -} audioh_stream_map_t; + uint8_t ep_addr; + uint8_t ep_size; // feedback endpoint max packet size (3 or 4) + uint8_t ep_interval; + uint8_t ep_attr; +} audioh_feedback_ep_t; + +typedef struct { + audioh_feedback_ep_t feedback[CFG_TUH_AUDIO_MAX_AS]; + + // Playback pacing in Q16.16 frames per data-endpoint poll interval. + // Feedback is latched only when the current fractional scheduling cycle + // wraps, so one cycle is never generated from two different rates. + uint32_t nominal_frames_q16; + uint32_t target_frames_q16; + uint32_t pending_frames_q16; + uint16_t feedback_min_frames; + uint16_t feedback_max_frames; + uint16_t rem_acc; + bool feedback_pending; + bool feedback_opened; +} audioh_playback_t; // One logical stream (capture or playback) typedef struct { @@ -129,12 +150,14 @@ typedef struct { 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]; + uint8_t as_count; + uint8_t config_count; + audioh_as_config_t as[CFG_TUH_AUDIO_MAX_AS]; // Active stream state - uint8_t active_config; // index into config[]/map[], TUSB_INDEX_INVALID_8 when not configured + uint8_t active_config; // flattened public configuration index + uint8_t active_as; + uint8_t active_rate; uint8_t state; // STREAM_STATE_* bool running; // tuh_audio_start() called, transfers may be submitted @@ -147,18 +170,6 @@ typedef struct { // Size in bytes of one frame (all channels) of the active configuration uint16_t frame_bytes; - // Playback pacing in Q16.16 frames per data-endpoint poll interval. - // Feedback is latched only when the current fractional scheduling cycle - // wraps, so one cycle is never generated from two different rates. - uint32_t nominal_frames_q16; - uint32_t target_frames_q16; - uint32_t pending_frames_q16; - uint32_t feedback_min_q16; - uint32_t feedback_max_q16; - uint16_t rem_acc; - bool feedback_pending; - bool feedback_opened; - // 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 @@ -172,31 +183,37 @@ typedef struct { typedef struct { uint8_t daddr; // device address (0 = free slot) uint8_t ac_itf_num; // Audio Control interface number + uint8_t stream_count; + bool mounted; // 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 - - bool mounted; + audioh_playback_t playback; } audioh_interface_t; typedef struct { - TUH_EPBUF_DEF(sam_freq, 4); // shared sampling-frequency SET data + // Sampling-frequency SET completes before feedback starts, so both + // transfers can use one stable DMA buffer. + TUH_EPBUF_DEF(rate_feedback, 4); TUH_EPBUF_DEF(fu_ctrl, 8); // feature-unit SET data - TUH_EPBUF_DEF(feedback, 4); // explicit-feedback transfer buffer 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 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; + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + void *value; + union { + struct { + uint8_t width; + uint8_t value_type; + } control; + struct { + uint8_t stream_idx; + uint8_t range_step; + } mount; + } fu; + bool fu_busy; } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; @@ -247,6 +264,48 @@ static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, return NULL; } +TU_ATTR_ALWAYS_INLINE static inline audioh_playback_t *audioh_get_playback(const tuh_audio_stream_t *s) { + return &_audioh_itf[s->idx].playback; +} + +TU_ATTR_ALWAYS_INLINE static inline audioh_as_config_t *audioh_stream_active_as(tuh_audio_stream_t *s) { + return &s->as[s->active_as]; +} + +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; + } + config_idx -= s->as[i].sample_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; +} + +static bool audioh_stream_config_get(const tuh_audio_stream_t *s, uint8_t config_idx, + tuh_audio_stream_config_t *config) { + uint8_t as_idx; + uint8_t rate_idx; + TU_VERIFY(audioh_stream_resolve_config(s, config_idx, &as_idx, &rate_idx), false); + + audioh_stream_config_fill(s, as_idx, rate_idx, 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; @@ -307,29 +366,28 @@ 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->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->nominal_frames_q16 = 0; - s->target_frames_q16 = 0; - s->pending_frames_q16 = 0; - s->feedback_min_q16 = 0; - s->feedback_max_q16 = 0; - s->rem_acc = 0; - s->feedback_pending = false; - s->feedback_opened = false; + 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; tu_edpt_stream_close(&s->edpt); tu_edpt_stream_clear(&s->edpt); } +static void audioh_playback_reset(audioh_playback_t *playback) { + tu_memclr(playback, sizeof(*playback)); +} + // 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++) { @@ -337,8 +395,12 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) 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) { - const audioh_stream_map_t *map = &stream->map[stream->active_config]; - if (map->ep_addr == ep_addr || (map->fb_ep_addr != 0 && map->fb_ep_addr == ep_addr)) { + const audioh_as_config_t *as = audioh_stream_active_as(stream); + if (as->ep_addr == ep_addr) { + return stream; + } + const uint8_t feedback_ep = p_audio->playback.feedback[stream->active_as].ep_addr; + if (stream->dir == TUSB_DIR_OUT && feedback_ep != 0 && feedback_ep == ep_addr) { return stream; } } @@ -356,10 +418,10 @@ static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes); static void audioh_stream_feedback_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(map->fb_ep_addr != 0, ); - TU_VERIFY(usbh_edpt_claim(s->daddr, map->fb_ep_addr), ); - if (!usbh_edpt_xfer(s->daddr, map->fb_ep_addr, _audioh_epbuf[s->idx].feedback, map->fb_ep_size)) { + 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].rate_feedback, feedback->ep_size)) { audioh_stream_error(s, 0); } } @@ -370,11 +432,11 @@ static void audioh_stream_feedback_xfer(tuh_audio_stream_t *s) { 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(usbh_edpt_claim(s->daddr, map->ep_addr), ); // one transfer in flight + const audioh_as_config_t *as = audioh_stream_active_as(s); + TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), ); // one transfer in flight // ep_size is guaranteed <= CFG_TUH_AUDIO_EPIN_BUFSIZE by enumeration - if (!usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, map->ep_size)) { + if (!usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, as->ep_size)) { audioh_stream_error(s, 0); } } @@ -385,12 +447,13 @@ static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { 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 + 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), ); // one transfer in flight - uint32_t frames = s->target_frames_q16 >> 16; - const uint32_t fraction = s->target_frames_q16 & 0xFFFFu; - uint32_t next_rem_acc = s->rem_acc + fraction; + uint32_t frames = playback->target_frames_q16 >> 16; + const uint32_t fraction = playback->target_frames_q16 & 0xFFFFu; + uint32_t next_rem_acc = playback->rem_acc + fraction; bool loop_done = (fraction == 0); if (next_rem_acc >= 65536u) { next_rem_acc -= 65536u; @@ -399,7 +462,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 <= map->ep_size && bytes_64 <= CFG_TUH_AUDIO_EPOUT_BUFSIZE && + TU_ASSERT(bytes_64 <= as->ep_size && bytes_64 <= CFG_TUH_AUDIO_EPOUT_BUFSIZE && bytes_64 <= CFG_TUH_AUDIO_STREAM_BUFSIZE, ); const uint16_t bytes = (uint16_t)bytes_64; if (tu_fifo_count(&s->edpt.ff) < bytes) { @@ -410,15 +473,15 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes); } - if (!usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, bytes)) { + if (!usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, bytes)) { audioh_stream_error(s, 0); return; } - s->rem_acc = (uint16_t)next_rem_acc; - if (loop_done && s->feedback_pending) { - s->target_frames_q16 = s->pending_frames_q16; - s->feedback_pending = false; - s->rem_acc = 0; + playback->rem_acc = (uint16_t)next_rem_acc; + if (loop_done && playback->feedback_pending) { + playback->target_frames_q16 = playback->pending_frames_q16; + playback->feedback_pending = false; + playback->rem_acc = 0; } } @@ -427,13 +490,14 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { //--------------------------------------------------------------------+ static bool audioh_stream_close_ep(tuh_audio_stream_t *s) { - if (s->feedback_opened) { - const uint8_t fb_ep_addr = s->map[s->active_config].fb_ep_addr; + audioh_playback_t *playback = (s->dir == TUSB_DIR_OUT) ? audioh_get_playback(s) : NULL; + if (playback != NULL && playback->feedback_opened) { + const uint8_t fb_ep_addr = playback->feedback[s->active_as].ep_addr; if (!tuh_edpt_close(s->daddr, fb_ep_addr)) { TU_LOG_DRV(" AUDIO close feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, fb_ep_addr); return false; } - s->feedback_opened = false; + playback->feedback_opened = false; } if (!tu_edpt_stream_is_opened(&s->edpt)) { @@ -454,33 +518,38 @@ 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->active_as = TUSB_INDEX_INVALID_8; + s->active_rate = TUSB_INDEX_INVALID_8; s->running = false; } static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes) { - s->running = false; - s->target_frames_q16 = s->nominal_frames_q16; - s->feedback_pending = false; - s->rem_acc = 0; + s->running = false; + if (s->dir == TUSB_DIR_OUT) { + audioh_playback_t *playback = audioh_get_playback(s); + playback->target_frames_q16 = playback->nominal_frames_q16; + playback->feedback_pending = false; + playback->rem_acc = 0; + } 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]; - const tuh_audio_stream_config_t *cfg = &s->config[s->active_config]; - uint8_t *ctrl = _audioh_epbuf[s->idx].sam_freq; + 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; - ctrl[0] = (uint8_t)(cfg->sample_rate & 0xFF); - ctrl[1] = (uint8_t)((cfg->sample_rate >> 8) & 0xFF); - ctrl[2] = (uint8_t)((cfg->sample_rate >> 16) & 0xFF); + 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(map->ep_addr), + .wIndex = tu_htole16(as->ep_addr), .wLength = 3}; tuh_xfer_t xfer = {.daddr = s->daddr, @@ -494,19 +563,19 @@ static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete // Reconstruct the endpoint descriptor of the selected configuration and open it static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { - const audioh_stream_map_t *map = &s->map[s->active_config]; + const audioh_as_config_t *as = audioh_stream_active_as(s); const tusb_desc_endpoint_t desc_ep = {.bLength = sizeof(tusb_desc_endpoint_t), .bDescriptorType = TUSB_DESC_ENDPOINT, - .bEndpointAddress = map->ep_addr, + .bEndpointAddress = as->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}; + .sync = (as->ep_attr >> 2) & 0x03u, + .usage = (as->ep_attr >> 4) & 0x03u}, + .wMaxPacketSize = tu_htole16(as->ep_size), + .bInterval = as->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); + TU_LOG_DRV(" AUDIO open endpoint failed: addr=%u ep=%02x\r\n", s->daddr, as->ep_addr); audioh_stream_fail(s); return false; } @@ -516,21 +585,25 @@ static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { tu_edpt_stream_open(&s->edpt, s->daddr, &desc_ep, xfer_len); tu_edpt_stream_clear(&s->edpt); - if (map->fb_ep_addr != 0) { - const tusb_desc_endpoint_t desc_fb = {.bLength = sizeof(tusb_desc_endpoint_t), - .bDescriptorType = TUSB_DESC_ENDPOINT, - .bEndpointAddress = map->fb_ep_addr, - .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS, - .sync = (map->fb_ep_attr >> 2) & 0x03u, - .usage = (map->fb_ep_attr >> 4) & 0x03u}, - .wMaxPacketSize = tu_htole16(map->fb_ep_size), - .bInterval = map->fb_ep_interval}; - if (!tuh_edpt_open(s->daddr, &desc_fb)) { - TU_LOG_DRV(" AUDIO open feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, map->fb_ep_addr); - audioh_stream_fail(s); - return false; + if (s->dir == TUSB_DIR_OUT) { + audioh_playback_t *playback = audioh_get_playback(s); + const audioh_feedback_ep_t *feedback = &playback->feedback[s->active_as]; + if (feedback->ep_addr != 0) { + const tusb_desc_endpoint_t desc_fb = {.bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = feedback->ep_addr, + .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS, + .sync = (feedback->ep_attr >> 2) & 0x03u, + .usage = (feedback->ep_attr >> 4) & 0x03u}, + .wMaxPacketSize = tu_htole16(feedback->ep_size), + .bInterval = feedback->ep_interval}; + if (!tuh_edpt_open(s->daddr, &desc_fb)) { + TU_LOG_DRV(" AUDIO open feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, feedback->ep_addr); + audioh_stream_fail(s); + return false; + } + playback->feedback_opened = true; } - s->feedback_opened = true; } s->state = STREAM_STATE_READY; @@ -560,6 +633,7 @@ bool audioh_init(void) { audioh_stream_reset(in); audioh_stream_reset(out); + audioh_playback_reset(&_audioh_itf[idx].playback); } return true; } @@ -588,6 +662,7 @@ void audioh_close(uint8_t daddr) { tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream; audioh_stream_reset(stream); } + audioh_playback_reset(&p_audio->playback); _audioh_epbuf[idx].complete_cb = NULL; // drop a pending feature-unit request _audioh_epbuf[idx].fu_busy = false; @@ -599,8 +674,9 @@ void audioh_close(uint8_t daddr) { } static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_bytes) { - const uint8_t *fb = _audioh_epbuf[s->idx].feedback; - uint32_t feedback_q16; + const uint8_t *fb = _audioh_epbuf[s->idx].rate_feedback; + audioh_playback_t *playback = audioh_get_playback(s); + uint32_t feedback_q16; if (xferred_bytes == 3) { // Full-speed feedback is normally Q10.14. Keep the scheduler in Q16.16. feedback_q16 = ((uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16)) << 2; @@ -611,22 +687,24 @@ static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_byt return; } - const audioh_stream_map_t *map = &s->map[s->active_config]; - if (feedback_q16 < s->feedback_min_q16 || feedback_q16 > s->feedback_max_q16) { + const uint32_t feedback_min_q16 = (uint32_t)playback->feedback_min_frames << 16; + const uint32_t feedback_max_q16 = (uint32_t)playback->feedback_max_frames << 16; + if (feedback_q16 < feedback_min_q16 || feedback_q16 > feedback_max_q16) { TU_LOG_DRV(" AUDIO feedback out of range: 0x%08lx\r\n", (unsigned long)feedback_q16); return; } // Feedback is expressed per USB frame/microframe. Scale it to the data // endpoint's polling interval before handing it to the packet scheduler. - const uint64_t target_q16_64 = (uint64_t)feedback_q16 << (map->ep_interval - 1u); + const audioh_as_config_t *as = audioh_stream_active_as(s); + const uint64_t target_q16_64 = (uint64_t)feedback_q16 << (as->ep_interval - 1u); if (target_q16_64 > UINT32_MAX) { return; } const uint32_t target_q16 = (uint32_t)target_q16_64; const uint64_t max_bytes = (((uint64_t)target_q16 + 0xFFFFu) >> 16) * s->frame_bytes; - if (max_bytes == 0 || max_bytes > map->ep_size || max_bytes > CFG_TUH_AUDIO_EPOUT_BUFSIZE || + if (max_bytes == 0 || max_bytes > as->ep_size || max_bytes > CFG_TUH_AUDIO_EPOUT_BUFSIZE || max_bytes > CFG_TUH_AUDIO_STREAM_BUFSIZE) { TU_LOG_DRV(" AUDIO feedback exceeds playback packet capacity: 0x%08lx\r\n", (unsigned long)feedback_q16); return; @@ -634,8 +712,8 @@ static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_byt // Keep only the newest feedback sample. The packet scheduler promotes it at // the end of its current fractional cycle. - s->pending_frames_q16 = target_q16; - s->feedback_pending = true; + playback->pending_frames_q16 = target_q16; + playback->feedback_pending = true; } bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { @@ -656,8 +734,8 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin return true; } - const audioh_stream_map_t *map = &s->map[s->active_config]; - if (ep_addr == map->fb_ep_addr) { + 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); return true; @@ -709,12 +787,12 @@ 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; - uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; + 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; // An AS alternate setting has one audio data endpoint and may have one // explicit feedback endpoint. Implicit-feedback endpoints are data endpoints @@ -723,8 +801,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de uint8_t ep_addr; uint16_t ep_size; uint8_t ep_interval; - uint8_t ep_sync; - uint8_t ep_usage; + uint8_t ep_attr; bool sam_freq_ctrl; } audioh_ep_info_t; audioh_ep_info_t ep_info = {0}; @@ -745,7 +822,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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); + format_tag = tu_le16toh(desc_as_general->wFormatTag); break; } case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { @@ -753,16 +830,15 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de 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]; + 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); - for (uint8_t i = 0; i < sam_freq_count; 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)); - } + sam_freq_data = &p_desc[8]; } break; } @@ -809,9 +885,9 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de if (fb_info.ep_interval == 0 || fb_info.ep_interval > 16) { fb_info.ep_interval = 1; } - fb_info.ep_sync = desc_endpoint->bmAttributes.sync; - fb_info.ep_usage = desc_endpoint->bmAttributes.usage; - has_feedback_ep = true; + fb_info.ep_attr = + (uint8_t)((desc_endpoint->bmAttributes.sync << 2) | (desc_endpoint->bmAttributes.usage << 4)); + has_feedback_ep = true; break; } @@ -829,9 +905,9 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de if (ep_info.ep_interval == 0 || ep_info.ep_interval > 16) { ep_info.ep_interval = 1; } - ep_info.ep_sync = desc_endpoint->bmAttributes.sync; - ep_info.ep_usage = desc_endpoint->bmAttributes.usage; - has_data_ep = true; + ep_info.ep_attr = + (uint8_t)((desc_endpoint->bmAttributes.sync << 2) | (desc_endpoint->bmAttributes.usage << 4)); + has_data_ep = true; } break; } @@ -869,7 +945,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de } const uint16_t frame_bytes = (uint16_t)frame_bytes_32; - // Register one configuration per discrete sampling frequency + // 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); @@ -888,14 +965,27 @@ 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++) { - if (sam_freq[i] == 0) { + 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)sam_freq[i] * audioh_interval_us(ep->ep_interval, p_audio->daddr); + 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 || @@ -904,49 +994,51 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de itf_num, alt, ep->ep_size); continue; } - // Skip duplicate configurations + // Skip duplicate configurations already retained from another AS or from + // an earlier frequency entry in this descriptor. 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) { + for (uint8_t as_idx = 0; as_idx < stream->as_count && !duplicate; as_idx++) { + const audioh_as_config_t *existing = &stream->as[as_idx]; + if (existing->format == (uint8_t)format && existing->channels == num_channels) { + for (uint8_t rate_idx = 0; rate_idx < existing->sample_rate_count; rate_idx++) { + if (existing->sample_rate[rate_idx] == sample_rate) { + duplicate = true; + break; + } + } + } + } + for (uint8_t rate_idx = 0; rate_idx < as_config.sample_rate_count && !duplicate; rate_idx++) { + if (as_config.sample_rate[rate_idx] == sample_rate) { 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; - } + as_config.sample_rate[as_config.sample_rate_count++] = sample_rate; + } - 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->map[stream->config_count].fb_ep_addr = 0; - stream->map[stream->config_count].fb_ep_size = 0; - stream->map[stream->config_count].fb_ep_interval = 0; - stream->map[stream->config_count].fb_ep_attr = 0; - if (stream->dir == TUSB_DIR_OUT && has_feedback_ep) { - stream->map[stream->config_count].fb_ep_addr = fb_info.ep_addr; - stream->map[stream->config_count].fb_ep_size = (uint8_t)fb_info.ep_size; - stream->map[stream->config_count].fb_ep_interval = fb_info.ep_interval; - stream->map[stream->config_count].fb_ep_attr = (uint8_t)((fb_info.ep_sync << 2) | (fb_info.ep_usage << 4)); - } - stream->config_count++; + if (as_config.sample_rate_count == 0) { + 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; + } + + const uint8_t as_idx = stream->as_count; + 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; + feedback->ep_size = (uint8_t)fb_info.ep_size; + feedback->ep_interval = fb_info.ep_interval; + feedback->ep_attr = fb_info.ep_attr; + } + stream->as_count++; + stream->config_count += as_config.sample_rate_count; return p_desc; } @@ -972,6 +1064,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface p_audio->ac_itf_num = desc_itf->bInterfaceNumber; audioh_stream_reset(&p_audio->in_stream); audioh_stream_reset(&p_audio->out_stream); + audioh_playback_reset(&p_audio->playback); p_audio->in_stream.daddr = dev_addr; p_audio->out_stream.daddr = dev_addr; @@ -1128,6 +1221,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface 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; @@ -1156,8 +1250,8 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { return true; } - audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; - epbuf->volume_stream_idx = 0; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + epbuf->fu.mount.stream_idx = 0; audioh_mount_feature_unit_next(idx); return true; } @@ -1254,8 +1348,7 @@ bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_id TU_VERIFY(s && config, false); TU_VERIFY(config_idx < s->config_count, false); - *config = s->config[config_idx]; - return true; + return audioh_stream_config_get(s, config_idx, config); } bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx) { @@ -1266,23 +1359,28 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s, false); TU_VERIFY(config_idx < s->config_count, false); - const tuh_audio_stream_config_t *cfg = &s->config[config_idx]; + tuh_audio_stream_config_t cfg; + uint8_t as_idx; + uint8_t rate_idx; + TU_VERIFY(audioh_stream_resolve_config(s, config_idx, &as_idx, &rate_idx), false); + audioh_stream_config_fill(s, as_idx, rate_idx, &cfg); // 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); - if (s->feedback_opened) { - TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].fb_ep_addr), false); + if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) { + TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); } } 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 tuh_audio_stream_config_t *other_cfg = &other->config[other->active_config]; - if (cfg->sample_rate != other_cfg->sample_rate) { + tuh_audio_stream_config_t other_cfg; + audioh_stream_config_fill(other, other->active_as, other->active_rate, &other_cfg); + if (cfg.sample_rate != other_cfg.sample_rate) { TU_LOG_DRV(" AUDIO configure failed: capture/playback sample rates must match (%lu != %lu)\r\n", - (unsigned long)cfg->sample_rate, (unsigned long)other_cfg->sample_rate); + (unsigned long)cfg.sample_rate, (unsigned long)other_cfg.sample_rate); return false; } } @@ -1291,18 +1389,23 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // the same address, since its packet size and interval may have changed. TU_VERIFY(audioh_stream_close_ep(s), false); - const audioh_stream_map_t *map = &s->map[config_idx]; - const uint32_t frame_div = (tuh_speed_get(s->daddr) == TUSB_SPEED_HIGH) ? 8000u : 1000u; - s->active_config = config_idx; - s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(cfg); - s->nominal_frames_q16 = audioh_nominal_frames_q16(cfg->sample_rate, map->ep_interval, s->daddr); - s->target_frames_q16 = s->nominal_frames_q16; - s->pending_frames_q16 = 0; - s->feedback_min_q16 = ((cfg->sample_rate - 1u) / frame_div) << 16; - s->feedback_max_q16 = (cfg->sample_rate / frame_div + 1u) << 16; - s->feedback_pending = false; - s->rem_acc = 0; - s->state = STREAM_STATE_IDLE; + const audioh_as_config_t *as = &s->as[as_idx]; + s->active_config = config_idx; + s->active_as = as_idx; + s->active_rate = rate_idx; + s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(&cfg); + s->state = STREAM_STATE_IDLE; + if (s->dir == TUSB_DIR_OUT) { + const uint32_t frame_div = (tuh_speed_get(s->daddr) == TUSB_SPEED_HIGH) ? 8000u : 1000u; + audioh_playback_t *playback = &p_audio->playback; + playback->nominal_frames_q16 = audioh_nominal_frames_q16(cfg.sample_rate, as->ep_interval, s->daddr); + playback->target_frames_q16 = playback->nominal_frames_q16; + playback->pending_frames_q16 = 0; + playback->feedback_min_frames = (uint16_t)((cfg.sample_rate - 1u) / frame_div); + playback->feedback_max_frames = (uint16_t)(cfg.sample_rate / frame_div + 1u); + playback->feedback_pending = false; + playback->rem_acc = 0; + } 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. @@ -1314,8 +1417,8 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t 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); + (s->dir == TUSB_DIR_IN) ? "capture" : "playback", s->stream_idx, as->itf_num, as->alt_setting, + as->ep_addr); return audioh_stream_open_ep(s); } @@ -1325,9 +1428,9 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // 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 + audioh_stream_capture_xfer(s); // feed the capture endpoint } else { - if (s->map[s->active_config].fb_ep_addr != 0) { + if (audioh_get_playback(s)->feedback[s->active_as].ep_addr != 0) { audioh_stream_feedback_xfer(s); } if (!s->running) { @@ -1351,8 +1454,8 @@ static void audioh_stream_start_set_freq_complete(tuh_xfer_t *xfer) { } 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) { + const audioh_as_config_t *as = audioh_stream_active_as(s); + if (as->sam_freq_ctrl) { if (!audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete)) { audioh_stream_error(s, 0); return false; @@ -1385,19 +1488,21 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t 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); - if (s->feedback_opened) { - TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].fb_ep_addr), false); + const audioh_as_config_t *as = audioh_stream_active_as(s); + TU_VERIFY(!usbh_edpt_busy(s->daddr, as->ep_addr), false); + if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) { + TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); } - s->target_frames_q16 = s->nominal_frames_q16; - s->feedback_pending = false; - s->rem_acc = 0; - s->running = true; + 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; + } + s->running = true; // Activate the interface's alternate setting asynchronously: transfers // 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)) { + if (!tuh_interface_set(s->daddr, as->itf_num, as->alt_setting, audioh_stream_start_complete, (uintptr_t)s)) { s->running = false; return false; } @@ -1422,19 +1527,21 @@ 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]; + const audioh_as_config_t *as = audioh_stream_active_as(s); // 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); + TU_VERIFY(tuh_interface_set(s->daddr, as->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 being deactivated so // the device stops transferring. - s->running = false; - s->target_frames_q16 = s->nominal_frames_q16; - s->feedback_pending = false; + 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->rem_acc = 0; // restart the pacing accumulator on the next tuh_audio_start() return true; } @@ -1526,13 +1633,13 @@ enum { }; static void audioh_fu_value_store(audioh_epbuf_t *epbuf) { - if (epbuf->value_type == AUDIOH_FU_VALUE_BOOL) { + if (epbuf->fu.control.value_type == AUDIOH_FU_VALUE_BOOL) { *((bool *)epbuf->value) = epbuf->fu_ctrl[0] != 0; - } else if (epbuf->width == 1) { + } else if (epbuf->fu.control.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) { + if (epbuf->fu.control.value_type == AUDIOH_FU_VALUE_I16) { *((int16_t *)epbuf->value) = (int16_t)value; } else { *((uint16_t *)epbuf->value) = value; @@ -1550,7 +1657,7 @@ static void audioh_fu_get_complete(tuh_xfer_t *xfer) { epbuf->fu_busy = false; if (epbuf->value != NULL && xfer->result == XFER_RESULT_SUCCESS) { - if (xfer->actual_len == epbuf->width) { + if (xfer->actual_len == epbuf->fu.control.width) { audioh_fu_value_store(epbuf); } else { xfer->result = XFER_RESULT_FAILED; @@ -1583,17 +1690,17 @@ static uint8_t audioh_fu_volume_range_request(uint8_t step) { } } -static void audioh_fu_volume_range_store(audioh_epbuf_t *epbuf) { +static void audioh_fu_volume_range_store(tuh_audio_stream_t *s, audioh_epbuf_t *epbuf) { const uint16_t value = tu_le16toh(tu_unaligned_read16(epbuf->fu_ctrl)); - switch (epbuf->volume_range_step) { + switch (epbuf->fu.mount.range_step) { case AUDIOH_VOLUME_RANGE_MIN: - epbuf->volume_range_temp.min = (int16_t)value; + s->volume_range.min = (int16_t)value; break; case AUDIOH_VOLUME_RANGE_MAX: - epbuf->volume_range_temp.max = (int16_t)value; + s->volume_range.max = (int16_t)value; break; case AUDIOH_VOLUME_RANGE_RES: - epbuf->volume_range_temp.res = value; + s->volume_range.res = value; break; default: break; @@ -1605,12 +1712,12 @@ 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); + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->fu.mount.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), + .bRequest = 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), @@ -1628,13 +1735,13 @@ 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); + 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) { - epbuf->volume_range_temp = (tuh_audio_volume_range_t){0}; - epbuf->volume_range_step = AUDIOH_VOLUME_RANGE_MIN; - epbuf->fu_busy = true; + 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; } @@ -1644,7 +1751,7 @@ static void audioh_mount_feature_unit_next(uint8_t idx) { } epbuf->fu_busy = false; } - epbuf->volume_stream_idx++; + epbuf->fu.mount.stream_idx++; } p_audio->mounted = true; @@ -1660,33 +1767,32 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { 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); + 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) { - audioh_fu_volume_range_store(epbuf); - epbuf->volume_range_step++; + audioh_fu_volume_range_store(s, epbuf); + epbuf->fu.mount.range_step++; - if (epbuf->volume_range_step < AUDIOH_VOLUME_RANGE_COUNT) { + if (epbuf->fu.mount.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 { + } else if (s->volume_range.min > s->volume_range.max || s->volume_range.res == 0) { xfer->result = XFER_RESULT_FAILED; } } 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) { s->feature_unit_id = 0; } } epbuf->fu_busy = false; - epbuf->volume_stream_idx++; + epbuf->fu.mount.stream_idx++; audioh_mount_feature_unit_next(idx); } @@ -1758,10 +1864,10 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; TU_VERIFY(!epbuf->fu_busy, false); - epbuf->fu_busy = true; - epbuf->value = value; - epbuf->width = width; - epbuf->value_type = value_type; + epbuf->fu_busy = true; + epbuf->value = value; + epbuf->fu.control.width = width; + epbuf->fu.control.value_type = value_type; const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index e850e4b7c..f8b6a80c8 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -79,9 +79,9 @@ typedef enum { // dir is constant for all configs of a given (dev_idx, stream_idx) and // equals the result of tuh_audio_stream_direction(). typedef struct { + uint32_t sample_rate; tuh_audio_direction_t dir; tuh_audio_format_t format; - uint32_t sample_rate; uint8_t channels; } tuh_audio_stream_config_t; 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 3e11e733c..62119f7be 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -887,13 +887,19 @@ void test_audio_host_keeps_running_when_stop_cannot_be_submitted(void) { 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)); + mount_descriptors(playback_with_two_frequencies, sizeof(playback_with_two_frequencies)); TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 0)); TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 0, &config)); TEST_ASSERT_EQUAL_UINT32(44100, config.sample_rate); TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 1, &config)); TEST_ASSERT_EQUAL_UINT32(48000, config.sample_rate); + + // Both public configurations share one AS mapping but retain their own rate. + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 1)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){U24_TO_U8S_LE(48000)}), control_buffer, 3); } void test_audio_host_keeps_supported_stream_when_other_as_format_is_unsupported(void) { -- cgit v1.3.1 From 804e54f9e59dc2b6442844d1194581e2b39c35b1 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 26 Aug 2026 11:33:46 +0200 Subject: audio: preserve descriptor sample-rate entries Retain every valid discrete rate in descriptor order, including duplicate public tuples. Alternate settings remain independently selectable without disruptive validation or deduplication during enumeration. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 23 ---------------------- test/unit-test/test/host/audio/test_audio_host.c | 25 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 28 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 8caecf3d2..a28cad156 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -994,29 +994,6 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de itf_num, alt, ep->ep_size); continue; } - // Skip duplicate configurations already retained from another AS or from - // an earlier frequency entry in this descriptor. - bool duplicate = false; - for (uint8_t as_idx = 0; as_idx < stream->as_count && !duplicate; as_idx++) { - const audioh_as_config_t *existing = &stream->as[as_idx]; - if (existing->format == (uint8_t)format && existing->channels == num_channels) { - for (uint8_t rate_idx = 0; rate_idx < existing->sample_rate_count; rate_idx++) { - if (existing->sample_rate[rate_idx] == sample_rate) { - duplicate = true; - break; - } - } - } - } - for (uint8_t rate_idx = 0; rate_idx < as_config.sample_rate_count && !duplicate; rate_idx++) { - if (as_config.sample_rate[rate_idx] == sample_rate) { - duplicate = true; - } - } - if (duplicate) { - continue; - } - as_config.sample_rate[as_config.sample_rate_count++] = sample_rate; } 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 62119f7be..0d7569090 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -396,7 +396,7 @@ static const uint8_t playback_with_two_frequencies[] = { TEST_UAC1_AS_ALT0, TEST_UAC1_AS_INTERFACE(1, 1), TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), - TEST_UAC1_FORMAT(2, 2, 16, 44100, 48000), + TEST_UAC1_FORMAT(2, 2, 16, 44100, 44100, 48000), TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 384, 2), TEST_UAC1_CS_DATA_EP, }; @@ -479,7 +479,7 @@ static const uint8_t playback_with_two_alternates[] = { TEST_UAC1_CS_DATA_EP, TEST_UAC1_AS_INTERFACE(2, 1), TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), - TEST_UAC1_FORMAT(2, 2, 16, 96000), + TEST_UAC1_FORMAT(2, 2, 16, 48000, 96000), TEST_UAC1_DATA_EP(0x02, TUSB_ISO_EP_ATT_ADAPTIVE, 384, 1), TEST_UAC1_CS_DATA_EP, }; @@ -889,19 +889,34 @@ void test_audio_host_parses_discrete_frequencies_with_interval_greater_than_one( tuh_audio_stream_config_t config; mount_descriptors(playback_with_two_frequencies, sizeof(playback_with_two_frequencies)); - TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 0)); + TEST_ASSERT_EQUAL_UINT8(3, tuh_audio_config_count(0, 0)); TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 0, &config)); TEST_ASSERT_EQUAL_UINT32(44100, config.sample_rate); TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 1, &config)); + TEST_ASSERT_EQUAL_UINT32(44100, config.sample_rate); + TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 2, &config)); TEST_ASSERT_EQUAL_UINT32(48000, config.sample_rate); - // Both public configurations share one AS mapping but retain their own rate. - TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 1)); + // All public configurations share one AS mapping and preserve descriptor order. + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 2)); TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); complete_interface_set(XFER_RESULT_SUCCESS); TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){U24_TO_U8S_LE(48000)}), control_buffer, 3); } +void test_audio_host_retains_same_rate_in_different_alternate_settings(void) { + tuh_audio_stream_config_t config; + mount_descriptors(playback_with_two_alternates, sizeof(playback_with_two_alternates)); + + TEST_ASSERT_EQUAL_UINT8(3, tuh_audio_config_count(0, 0)); + TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 0, &config)); + TEST_ASSERT_EQUAL_UINT32(48000, config.sample_rate); + TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 1, &config)); + TEST_ASSERT_EQUAL_UINT32(48000, config.sample_rate); + TEST_ASSERT_TRUE(tuh_audio_config_get(0, 0, 2, &config)); + TEST_ASSERT_EQUAL_UINT32(96000, config.sample_rate); +} + void test_audio_host_keeps_supported_stream_when_other_as_format_is_unsupported(void) { open_descriptors(playback_with_unsupported_capture, sizeof(playback_with_unsupported_capture)); -- 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 'src/class') 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 ac851fd0eab94a2d0ad51b25f90d71230b3af784 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 26 Aug 2026 16:07:50 +0200 Subject: audio: preserve feedback scheduler remainder Keep the fractional packet accumulator when a new explicit-feedback value is latched. Repeated feedback updates no longer bias high-speed fractional rates toward alternating undersized and oversized packets. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 3 +- test/unit-test/test/host/audio/test_audio_host.c | 43 ++++++++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index e676a3354..157f15545 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -551,7 +551,8 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { if (loop_done && playback->feedback_pending) { playback->target_frames_q16 = playback->pending_frames_q16; playback->feedback_pending = false; - playback->rem_acc = 0; + // Keep the remainder from the completed cycle. Clearing it for every + // feedback update biases the average toward the integer packet sizes. } } 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 5e312e457..4c8d21245 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -47,11 +47,13 @@ static uint8_t edpt_close_count; static uint32_t edpt_busy_mask; static bool edpt_xfer_result; -static uint8_t edpt_xfer_ep[16]; -static uint16_t edpt_xfer_bytes[16]; -static uint8_t edpt_xfer_data[16][8]; -static uint8_t *edpt_xfer_buffer[16]; -static uint8_t edpt_xfer_count; +#define AUDIO_TEST_MAX_XFERS 128 +static uint8_t edpt_xfer_ep[AUDIO_TEST_MAX_XFERS]; +static uint16_t edpt_xfer_bytes[AUDIO_TEST_MAX_XFERS]; +static uint8_t edpt_xfer_data[AUDIO_TEST_MAX_XFERS][8]; +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; @@ -76,7 +78,7 @@ void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { tusb_speed_t tuh_speed_get(uint8_t daddr) { (void)daddr; - return TUSB_SPEED_FULL; + return test_speed; } bool tuh_control_xfer(tuh_xfer_t *xfer) { @@ -758,6 +760,8 @@ static void complete_feedback_xfer(uint32_t feedback_q16, uint8_t length) { } void setUp(void) { + test_speed = TUSB_SPEED_FULL; + interface_set_result = true; memset(&interface_xfer, 0, sizeof(interface_xfer)); interface_alt = 0; @@ -1490,6 +1494,33 @@ void test_audio_host_accepts_16_16_feedback(void) { TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 10)); } +void test_audio_host_preserves_high_speed_feedback_fraction_across_updates(void) { + test_speed = TUSB_SPEED_HIGH; + mount_descriptors(playback_44100_with_feedback_16_16, sizeof(playback_44100_with_feedback_16_16)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + + // The captured feedback is 5.557 frames per microframe. Repeating the + // update every millisecond must not discard the scheduler's remainder. + const uint32_t feedback_q16 = 0x00058eafu; + for (uint8_t packet = 0; packet < 80; packet++) { + if ((packet % 8u) == 0) { + complete_feedback_xfer(feedback_q16, 4); + } + if (packet < 79) { + complete_playback_xfer(); + } + } + + uint16_t total_frames = 0; + for (uint8_t packet = 0; packet < 80; packet++) { + total_frames += edpt_xfer_bytes_at(0x01, packet) / 4u; + } + TEST_ASSERT_EQUAL_UINT16(444, total_frames); +} + void test_audio_host_ignores_out_of_range_feedback(void) { mount_descriptors(playback_44100_with_feedback_16_16, sizeof(playback_44100_with_feedback_16_16)); -- cgit v1.3.1 From 4893a14be173e9ab3fadb606222b440ab94e55b4 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 26 Aug 2026 16:48:37 +0200 Subject: audio: reduce USB-accessible control storage Move control-transfer bookkeeping out of USB memory and overlay mount-only Clock RANGE storage with cache-aligned runtime control payload buffers. Keep explicit feedback separate because it may overlap runtime requests. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 326 +++++++++++++++++++++++-------------------- 1 file changed, 176 insertions(+), 150 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 157f15545..9ca075925 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -161,6 +161,30 @@ typedef struct { bool feedback_opened; } audioh_playback_t; +// Control-transfer state does not require USB-accessible memory. +typedef struct { + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; + void *value; + union { + struct { + uint8_t width; + uint8_t value_type; + } control; + struct { + uint8_t stream_idx; + 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_ctrl_state_t; + // One logical stream (capture or playback) typedef struct { // instance info (set at init, preserved across close/open) @@ -213,9 +237,10 @@ typedef struct { 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; + tuh_audio_stream_t out_stream; + tuh_audio_stream_t in_stream; + audioh_playback_t playback; + audioh_ctrl_state_t ctrl; } audioh_interface_t; #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 @@ -223,42 +248,35 @@ typedef struct { #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); + // Clock discovery completes before mount. Afterwards its storage is reused + // by independently cache-aligned sampling-frequency and Feature Unit buffers. + union { #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 - TUH_EPBUF_DEF(clock_range, AUDIOH_CLOCK_RANGE_BUFSIZE); + TUH_EPBUF_DEF(clock_range, AUDIOH_CLOCK_RANGE_BUFSIZE); #endif - TUH_EPBUF_DEF(fu_ctrl, 8); // feature-unit SET data + struct { + TUH_EPBUF_DEF(rate_ctrl, 4); + TUH_EPBUF_DEF(fu_ctrl, 8); + } runtime; + } control; + // Explicit feedback can overlap both runtime control transfers. + TUH_EPBUF_DEF(feedback, 4); 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 operation in flight per device - tuh_xfer_cb_t complete_cb; - uintptr_t user_data; - void *value; - union { - struct { - uint8_t width; - uint8_t value_type; - } control; - struct { - uint8_t stream_idx; - 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; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; CFG_TUH_MEM_SECTION static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; +TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_rate_ctrl(audioh_epbuf_t *epbuf) { + return epbuf->control.runtime.rate_ctrl; +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_fu_ctrl(audioh_epbuf_t *epbuf) { + return epbuf->control.runtime.fu_ctrl; +} + //--------------------------------------------------------------------+ // Helper //--------------------------------------------------------------------+ @@ -491,7 +509,7 @@ static void audioh_stream_feedback_xfer(tuh_audio_stream_t *s) { 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].rate_feedback, feedback->ep_size)) { + if (!usbh_edpt_xfer(s->daddr, feedback->ep_addr, _audioh_epbuf[s->idx].feedback, feedback->ep_size)) { audioh_stream_error(s, 0); } } @@ -611,7 +629,7 @@ static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete 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; + uint8_t *ctrl = audioh_rate_ctrl(&_audioh_epbuf[s->idx]); tusb_control_request_t request = {0}; ctrl[0] = (uint8_t)(sample_rate & 0xFF); @@ -757,8 +775,7 @@ void audioh_close(uint8_t daddr) { } audioh_playback_reset(&p_audio->playback); - _audioh_epbuf[idx].complete_cb = NULL; // drop a pending feature-unit request - _audioh_epbuf[idx].fu_busy = false; + tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl)); // drop pending control state p_audio->stream_count = 0; p_audio->daddr = 0; @@ -769,7 +786,7 @@ void audioh_close(uint8_t daddr) { } static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_bytes) { - const uint8_t *fb = _audioh_epbuf[s->idx].rate_feedback; + const uint8_t *fb = _audioh_epbuf[s->idx].feedback; audioh_playback_t *playback = audioh_get_playback(s); uint32_t feedback_q16; if (xferred_bytes == 3) { @@ -1488,6 +1505,7 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface p_audio->protocol = desc_itf->bInterfaceProtocol; p_audio->rate_source_count = 0; tu_memclr(p_audio->rate_source, sizeof(p_audio->rate_source)); + tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl)); audioh_stream_reset(&p_audio->in_stream); audioh_stream_reset(&p_audio->out_stream); audioh_playback_reset(&p_audio->playback); @@ -1654,12 +1672,13 @@ static bool audioh_uac2_clock_range_store(audioh_rate_source_t *rate_source, con 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); + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_rate_source_t *rate_source = &p_audio->rate_source[ctrl->clock.rate_source_idx]; + const uint16_t length = ctrl->clock.read_cur ? 4u : (uint16_t)sizeof(epbuf->control.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, + .bRequest = ctrl->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), @@ -1667,16 +1686,16 @@ static bool audioh_mount_clock_submit(uint8_t idx) { tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, - .buffer = epbuf->clock_range, + .buffer = epbuf->control.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_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + ctrl->fu_busy = false; audioh_uac2_configs_rebuild(p_audio); if (p_audio->stream_count == 0) { @@ -1693,40 +1712,41 @@ static void audioh_mount_clock_finish(uint8_t idx) { return; } - epbuf->fu.mount.stream_idx = 0; + ctrl->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]; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; - while (epbuf->clock.rate_source_idx < p_audio->rate_source_count) { - epbuf->clock.read_cur = false; - epbuf->fu_busy = true; + while (ctrl->clock.rate_source_idx < p_audio->rate_source_count) { + ctrl->clock.read_cur = false; + ctrl->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++; + p_audio->rate_source[ctrl->clock.rate_source_idx].sample_rate_count = 0; + ctrl->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) { + 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; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + if (!ctrl->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]; + audioh_rate_source_t *rate_source = &p_audio->rate_source[ctrl->clock.rate_source_idx]; bool success = xfer->result == XFER_RESULT_SUCCESS; - if (success && epbuf->clock.read_cur) { + if (success && ctrl->clock.read_cur) { success = xfer->actual_len == 4; if (success) { - const uint32_t current = tu_le32toh(tu_unaligned_read32(epbuf->clock_range)); + const uint32_t current = tu_le32toh(tu_unaligned_read32(epbuf->control.clock_range)); success = current > 0; if (success) { rate_source->sample_rate[0] = current; @@ -1734,9 +1754,9 @@ static void audioh_mount_clock_complete(tuh_xfer_t *xfer) { } } } else if (success) { - success = audioh_uac2_clock_range_store(rate_source, epbuf->clock_range, (uint16_t)xfer->actual_len); + success = audioh_uac2_clock_range_store(rate_source, epbuf->control.clock_range, (uint16_t)xfer->actual_len); if (success && rate_source->frequency_access == AUDIOH_CTRL_READ) { - epbuf->clock.read_cur = true; + ctrl->clock.read_cur = true; if (audioh_mount_clock_submit(idx)) { return; } @@ -1747,8 +1767,8 @@ static void audioh_mount_clock_complete(tuh_xfer_t *xfer) { if (!success) { rate_source->sample_rate_count = 0; } - epbuf->fu_busy = false; - epbuf->clock.rate_source_idx++; + ctrl->fu_busy = false; + ctrl->clock.rate_source_idx++; audioh_mount_clock_next(idx); } #endif @@ -1769,15 +1789,15 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { return true; } - audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl; #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; + ctrl->clock.rate_source_idx = 0; audioh_mount_clock_next(idx); } else #endif { - epbuf->fu.mount.stream_idx = 0; + ctrl->fu.mount.stream_idx = 0; audioh_mount_feature_unit_next(idx); } return true; @@ -2171,12 +2191,12 @@ uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { // Release the stable SET buffer and chain to the application callback static void audioh_fu_set_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; - epbuf->complete_cb = NULL; - epbuf->fu_busy = false; + 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) { @@ -2220,33 +2240,34 @@ static bool audioh_fu_selector_supported(uint8_t protocol, uint8_t control_selec 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; - } else if (epbuf->fu.control.width == 1) { - *((uint16_t *)epbuf->value) = epbuf->fu_ctrl[0]; +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(epbuf->fu_ctrl)); - if (epbuf->fu.control.value_type == AUDIOH_FU_VALUE_I16) { - *((int16_t *)epbuf->value) = (int16_t)value; + 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 *)epbuf->value) = value; + *((uint16_t *)ctrl->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; - epbuf->complete_cb = NULL; - epbuf->fu_busy = false; - - if (epbuf->value != NULL && xfer->result == XFER_RESULT_SUCCESS) { - if (xfer->actual_len == epbuf->fu.control.width) { - audioh_fu_value_store(epbuf); + const uint8_t idx = (uint8_t)xfer->user_data; + audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + tuh_xfer_cb_t app_cb = ctrl->complete_cb; + uintptr_t user_data = ctrl->user_data; + ctrl->complete_cb = NULL; + ctrl->fu_busy = false; + + if (ctrl->value != NULL && xfer->result == XFER_RESULT_SUCCESS) { + if (xfer->actual_len == ctrl->fu.control.width) { + audioh_fu_value_store(ctrl, epbuf); } else { xfer->result = XFER_RESULT_FAILED; } @@ -2278,9 +2299,9 @@ static uint8_t audioh_fu_volume_range_request(uint8_t step) { } } -static void audioh_fu_volume_range_store(tuh_audio_stream_t *s, audioh_epbuf_t *epbuf) { - const uint16_t value = tu_le16toh(tu_unaligned_read16(epbuf->fu_ctrl)); - switch (epbuf->fu.mount.range_step) { +static void audioh_fu_volume_range_store(tuh_audio_stream_t *s, audioh_ctrl_state_t *ctrl, audioh_epbuf_t *epbuf) { + const uint16_t value = tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))); + switch (ctrl->fu.mount.range_step) { case AUDIOH_VOLUME_RANGE_MIN: s->volume_range.min = (int16_t)value; break; @@ -2298,15 +2319,16 @@ static void audioh_fu_volume_range_store(tuh_audio_stream_t *s, audioh_epbuf_t * 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->fu.mount.stream_idx); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[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 = { .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(epbuf->fu.mount.range_step), + .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)), .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = tu_htole16(uac2 ? 8u : 2u), @@ -2314,23 +2336,23 @@ static bool audioh_mount_feature_unit_submit(uint8_t idx) { tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, - .buffer = epbuf->fu_ctrl, + .buffer = audioh_fu_ctrl(epbuf), .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]; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; - 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); + while (ctrl->fu.mount.stream_idx < p_audio->stream_count) { + tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, ctrl->fu.mount.stream_idx); TU_ASSERT(s != NULL, ); 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; + 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; } @@ -2338,9 +2360,9 @@ static void audioh_mount_feature_unit_next(uint8_t idx) { if (s->mute_access == AUDIOH_CTRL_NONE) { s->feature_unit_id = 0; } - epbuf->fu_busy = false; + ctrl->fu_busy = false; } - epbuf->fu.mount.stream_idx++; + ctrl->fu.mount.stream_idx++; } p_audio->mounted = true; @@ -2350,29 +2372,31 @@ static void audioh_mount_feature_unit_next(uint8_t idx) { } 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) { + 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; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + if (!ctrl->fu_busy) { return; } - audioh_interface_t *p_audio = &_audioh_itf[idx]; - tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, epbuf->fu.mount.stream_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; 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])); + tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))) == 1) { + uint8_t *fu_ctrl = audioh_fu_ctrl(epbuf); + s->volume_range.min = (int16_t)tu_le16toh(tu_unaligned_read16(&fu_ctrl[2])); + s->volume_range.max = (int16_t)tu_le16toh(tu_unaligned_read16(&fu_ctrl[4])); + s->volume_range.res = tu_le16toh(tu_unaligned_read16(&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++; + audioh_fu_volume_range_store(s, ctrl, epbuf); + ctrl->fu.mount.range_step++; - if (epbuf->fu.mount.range_step < AUDIOH_VOLUME_RANGE_COUNT) { + if (ctrl->fu.mount.range_step < AUDIOH_VOLUME_RANGE_COUNT) { if (audioh_mount_feature_unit_submit(idx)) { return; } @@ -2390,8 +2414,8 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { s->feature_unit_id = 0; } } - epbuf->fu_busy = false; - epbuf->fu.mount.stream_idx++; + ctrl->fu_busy = false; + ctrl->fu.mount.stream_idx++; audioh_mount_feature_unit_next(idx); } @@ -2414,9 +2438,10 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control 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 + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + TU_VERIFY(!ctrl->fu_busy, false); + ctrl->fu_busy = true; // reserve the request state and fu_ctrl before writing const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, @@ -2426,7 +2451,7 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)), .wLength = width}; - uint8_t *val_buf = epbuf->fu_ctrl; + 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); @@ -2441,18 +2466,18 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control if (complete_cb == NULL) { const bool result = tuh_control_xfer(&xfer); - epbuf->fu_busy = false; + ctrl->fu_busy = false; return result; } - epbuf->complete_cb = complete_cb; - epbuf->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; + xfer.complete_cb = audioh_fu_set_complete; + xfer.user_data = (uintptr_t)idx; if (!tuh_control_xfer(&xfer)) { - epbuf->complete_cb = NULL; - epbuf->fu_busy = false; + ctrl->complete_cb = NULL; + ctrl->fu_busy = false; return false; } return true; @@ -2477,12 +2502,13 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con 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; - epbuf->value = value; - epbuf->fu.control.width = width; - epbuf->fu.control.value_type = value_type; + audioh_ctrl_state_t *ctrl = &p_audio->ctrl; + audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; + TU_VERIFY(!ctrl->fu_busy, false); + ctrl->fu_busy = true; + ctrl->value = value; + 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, @@ -2498,35 +2524,35 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con tuh_xfer_t xfer = {.daddr = p_audio->daddr, .ep_addr = 0, .setup = &request, - .buffer = epbuf->fu_ctrl, + .buffer = audioh_fu_ctrl(epbuf), .complete_cb = NULL, .user_data = user_data}; if (!tuh_control_xfer(&xfer)) { - epbuf->fu_busy = false; + ctrl->fu_busy = false; return false; } if (xfer.result == XFER_RESULT_SUCCESS && xfer.actual_len == width) { - audioh_fu_value_store(epbuf); + audioh_fu_value_store(ctrl, epbuf); } else if (xfer.result == XFER_RESULT_SUCCESS && user_data != 0) { *((tusb_xfer_result_t *)user_data) = XFER_RESULT_FAILED; } - epbuf->fu_busy = false; + ctrl->fu_busy = false; return true; } // Async path: chain the host-order conversion to the application callback - epbuf->complete_cb = complete_cb; - epbuf->user_data = user_data; - 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}; + 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)) { - epbuf->complete_cb = NULL; - epbuf->fu_busy = false; + ctrl->complete_cb = NULL; + ctrl->fu_busy = false; return false; } return true; -- cgit v1.3.1 From 0782fb796d7981c36a3d2a11485596f288dd6bd5 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 26 Aug 2026 16:56:42 +0200 Subject: audio: allow stopped duplex streams to change rate Enforce a shared capture/playback sample rate only while the opposite stream is running. This lets both stopped streams be configured in either order while preventing a live shared clock from changing underneath a transfer. Signed-off-by: HiFiPHile --- examples/host/audio_host/README.md | 2 +- src/class/audio/audio_host.c | 30 ++++++++++++++---------- test/unit-test/test/host/audio/test_audio_host.c | 30 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 14 deletions(-) (limited to 'src/class') diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 2b5df7785..326b469e8 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -116,6 +116,6 @@ Edit `src/tusb_config.h` to modify: ## Notes - 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 in the same Audio Control instance must use the same sample rate. +- 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. - 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/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 9ca075925..82d725329 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -1921,17 +1921,6 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx } } - 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) { - tuh_audio_stream_config_t other_cfg; - audioh_stream_config_fill(other, other->active_as, other->active_rate, &other_cfg); - if (cfg.sample_rate != other_cfg.sample_rate) { - TU_LOG_DRV(" AUDIO configure failed: capture/playback sample rates must match (%lu != %lu)\r\n", - (unsigned long)cfg.sample_rate, (unsigned long)other_cfg.sample_rate); - return false; - } - } - // The HCD endpoint must be reopened even when the new configuration uses // the same address, since its packet size and interval may have changed. TU_VERIFY(audioh_stream_close_ep(s), false); @@ -2056,12 +2045,28 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t 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 - const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); TU_VERIFY(!usbh_edpt_busy(s->daddr, as->ep_addr), false); if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) { TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); } + // Do not change a device-wide/shared clock while the other direction is + // running. Stopped streams may be reconfigured independently in either order. + tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; + if (other->running) { + const audioh_as_config_t *other_as = audioh_stream_active_as(other); + const audioh_rate_source_t *other_rate_source = audioh_as_rate_source(other, other_as); + const uint32_t sample_rate = rate_source->sample_rate[s->active_rate]; + const uint32_t other_sample_rate = other_rate_source->sample_rate[other->active_rate]; + if (sample_rate != other_sample_rate) { + TU_LOG_DRV(" AUDIO start failed: capture/playback sample rates must match (%lu != %lu)\r\n", + (unsigned long)sample_rate, (unsigned long)other_sample_rate); + return 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; @@ -2072,7 +2077,6 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { // 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 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 4c8d21245..2f92b232f 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -976,6 +976,36 @@ void test_audio_host_uac2_shared_clock_is_discovered_once_for_both_streams(void) TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 1)); } +void test_audio_host_reconfigures_stopped_duplex_streams_to_a_new_common_rate(void) { + open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 1)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 1, 1)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_active_config(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_active_config(0, 1)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 1, 0)); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_active_config(0, 1)); +} + +void test_audio_host_rejects_start_at_different_rate_from_running_peer(void) { + open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 1, 1)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_control_xfer(XFER_RESULT_SUCCESS); + complete_interface_set(XFER_RESULT_SUCCESS); + + TEST_ASSERT_FALSE(tuh_audio_start(0, 1)); + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(1, interface_set_count); +} + 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)); -- cgit v1.3.1 From 6019e538125e9863a4fa4bcf0b026d3e639a1a4d Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 03:10:37 +0200 Subject: audio: harden streaming descriptor parsing Validate descriptor lengths before typed access, keep supported alternate settings when neighboring formats are unsupported, and reject MIDI 1.0 and 2.0 streaming interfaces from the Audio host parser. Extend parser coverage for malformed and mixed descriptor layouts. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 295 ++++++++++++----------- test/unit-test/test/host/audio/test_audio_host.c | 95 +++++++- 2 files changed, 241 insertions(+), 149 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 82d725329..ad2fd677f 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -874,8 +874,6 @@ 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; @@ -896,13 +894,9 @@ typedef struct { } 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; + const uint8_t *desc_start; + const uint8_t *desc_end; +} audioh_ac_desc_range_t; #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 static uint8_t audioh_uac2_control_access(uint32_t controls, uint8_t position) { @@ -923,156 +917,173 @@ static bool audioh_as_rate_fits(const audioh_interface_t *p_audio, const tuh_aud (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]; +static bool audioh_ac_entity_valid(const audioh_interface_t *p_audio, const uint8_t *p_desc) { + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_input_terminal_t)); + case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio10_desc_output_terminal_t)); + 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)); + } + default: + return true; + } } + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + switch (tu_desc_subtype(p_desc)) { + case AUDIO20_CS_AC_INTERFACE_INPUT_TERMINAL: + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_input_terminal_t)); + 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); + case AUDIO20_CS_AC_INTERFACE_CLOCK_SOURCE: + return TUH_VALIDATE_BASIC(tu_desc_len(p_desc) >= sizeof(audio20_desc_clock_source_t)); + default: + return true; + } + #endif + default: + return false; } - return NULL; } +static bool audioh_ac_terminal_find(const audioh_interface_t *p_audio, const audioh_ac_desc_range_t *range, uint8_t id, + audioh_terminal_info_t *info) { + for (const uint8_t *p_desc = range->desc_start; p_desc < range->desc_end; p_desc = tu_desc_next(p_desc)) { + if (tu_desc_type(p_desc) != TUSB_DESC_CS_INTERFACE || tu_desc_len(p_desc) < 4 || p_desc[3] != id) { + continue; + } + 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_INPUT_TERMINAL) { + 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) { + *info = (audioh_terminal_info_t){.id = id, .stream_dir = TUSB_DIR_OUT}; + return true; + } + } else if (tu_desc_subtype(p_desc) == AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL) { + 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) { + *info = (audioh_terminal_info_t){.id = id, .source_id = terminal->bSourceID, .stream_dir = TUSB_DIR_IN}; + return true; + } + } + break; + #endif #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]; + case AUDIO_INT_PROTOCOL_CODE_V2: + if (tu_desc_subtype(p_desc) == AUDIO20_CS_AC_INTERFACE_INPUT_TERMINAL) { + 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) { + *info = (audioh_terminal_info_t){.id = id, .clock_id = terminal->bCSourceID, .stream_dir = TUSB_DIR_OUT}; + return true; + } + } else if (tu_desc_subtype(p_desc) == AUDIO20_CS_AC_INTERFACE_OUTPUT_TERMINAL) { + 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) { + *info = (audioh_terminal_info_t){.id = id, + .source_id = terminal->bSourceID, + .clock_id = terminal->bCSourceID, + .stream_dir = TUSB_DIR_IN}; + return true; + } + } + break; + #endif + default: + return false; } } - return NULL; + return false; } - #endif +static bool audioh_ac_feature_unit_parse(const audioh_interface_t *p_audio, const uint8_t *p_desc, + audioh_fu_info_t *info) { + if (tu_desc_type(p_desc) != TUSB_DESC_CS_INTERFACE) { + return false; + } + switch (p_audio->protocol) { #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); + 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; } 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); + #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; } 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; - } + #endif default: break; } - return true; + return false; } - #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); +static bool audioh_ac_clock_find(const audioh_ac_desc_range_t *range, uint8_t id, audioh_clock_info_t *info) { + for (const uint8_t *p_desc = range->desc_start; p_desc < range->desc_end; p_desc = tu_desc_next(p_desc)) { + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE && + tu_desc_subtype(p_desc) == AUDIO20_CS_AC_INTERFACE_CLOCK_SOURCE && p_desc[3] == id) { 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; + *info = + (audioh_clock_info_t){.id = id, + .frequency_access = + audioh_uac2_control_access(clock->bmControls, AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS)}; + return true; } - default: - break; } - return true; + return false; } #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) { +static void audioh_link_feature_units(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *range) { 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) { + audioh_terminal_info_t terminal; + if (!audioh_ac_terminal_find(p_audio, range, stream->as[0].terminal_id, &terminal) || + 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); + for (const uint8_t *p_desc = range->desc_start; p_desc < range->desc_end; p_desc = tu_desc_next(p_desc)) { + audioh_fu_info_t fu; + if (!audioh_ac_feature_unit_parse(p_audio, p_desc, &fu)) { + continue; + } + 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_access); break; } } @@ -1170,17 +1181,17 @@ static bool audioh_parse_as_interface(audioh_interface_t *p_audio, const uint8_t } #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, +static int8_t audioh_uac2_rate_source_get(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *range, 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) { + audioh_clock_info_t clock; + if (!audioh_ac_clock_find(range, terminal->clock_id, &clock) || 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) { + if (p_audio->rate_source[i].control_id == clock.id) { return (int8_t)i; } } @@ -1189,7 +1200,7 @@ static int8_t audioh_uac2_rate_source_get(audioh_interface_t *p_audio, const aud } 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}; + (audioh_rate_source_t){.control_id = clock.id, .frequency_access = clock.frequency_access}; return (int8_t)idx; } #endif @@ -1217,12 +1228,9 @@ static bool audioh_uac1_rates_store(const audioh_interface_t *p_audio, const tuh // 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 audioh_ac_map_t *ac_map, +static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *ac_desc, 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; @@ -1392,8 +1400,9 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ 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) { + audioh_terminal_info_t terminal; + if (!audioh_ac_terminal_find(p_audio, ac_desc, class_info.terminal_id, &terminal) || + 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; @@ -1439,7 +1448,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ #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); + const int8_t rate_source_idx = audioh_uac2_rate_source_get(p_audio, ac_desc, &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; @@ -1514,9 +1523,8 @@ 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); - audioh_ac_map_t ac_map = {0}; - - p_desc = tu_desc_next(p_desc); + p_desc = tu_desc_next(p_desc); + audioh_ac_desc_range_t ac_desc = {.desc_start = p_desc}; while (p_desc < desc_end) { if (!audioh_desc_valid(p_desc, desc_end, 2)) { goto open_failed; @@ -1529,12 +1537,13 @@ 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; } - if (!audioh_parse_ac_entity(p_audio, &ac_map, p_desc)) { + if (!audioh_ac_entity_valid(p_audio, p_desc)) { goto open_failed; } } p_desc = tu_desc_next(p_desc); } + ac_desc.desc_end = p_desc; // Parse the contiguous Audio Streaming interfaces of this audio function. while (p_desc < desc_end) { @@ -1558,13 +1567,13 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, desc_interface->bAlternateSetting); - p_desc = audioh_parse_as(p_audio, &ac_map, desc_interface, p_desc, desc_end); + p_desc = audioh_parse_as(p_audio, &ac_desc, desc_interface, p_desc, desc_end); if (p_desc == NULL) { goto open_failed; } } - audioh_link_feature_units(p_audio, &ac_map); + audioh_link_feature_units(p_audio, &ac_desc); // UAC2 configurations receive their rates asynchronously during mount. // Release the tentative instance when no supported AS alternate was found. 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 2f92b232f..55b5e8ef6 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -5,6 +5,7 @@ #include "unity.h" #include "audio_host_test.h" +#include "class/midi/midi.h" TEST_SOURCE_FILE("audio_host.c") TEST_SOURCE_FILE("tusb_fifo.c") @@ -23,7 +24,13 @@ enum { CAPTURE_FU = 12, CAPTURE_OUTPUT_TERM = 13, - PLAYBACK_CLOCK = 10, + 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; @@ -306,7 +313,7 @@ static const uint8_t playback_with_explicit_feedback[] = { TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_EXPLICIT_FB, 3, 1), }; -static const uint8_t midi_only_collection[] = { +static const uint8_t midi1_only_collection[] = { TEST_UAC1_AC_HEADER, 9, TUSB_DESC_INTERFACE, @@ -319,6 +326,24 @@ static const uint8_t midi_only_collection[] = { 0, }; +static const uint8_t midi2_only_collection[] = { + TEST_UAC1_AC_HEADER, + 9, + TUSB_DESC_INTERFACE, + AUDIO_AS_ITF, + 1, + 0, + TUSB_CLASS_AUDIO, + AUDIO_SUBCLASS_MIDI_STREAMING, + AUDIO_FUNC_PROTOCOL_CODE_UNDEF, + 0, + 7, + TUSB_DESC_CS_INTERFACE, + MIDI_CS_INTERFACE_HEADER, + U16_TO_U8S_LE(MIDI_VERSION_2_0), + U16_TO_U8S_LE(7), +}; + 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, }; @@ -340,6 +365,21 @@ static const uint8_t uac2_playback[] = { TEST_UAC2_CS_DATA_EP, }; +static const uint8_t uac2_playback_after_unrelated_clocks[] = { + TEST_UAC2_AC_HEADER(62), + TEST_UAC2_CLOCK_SOURCE(UNRELATED_CLOCK_0, AUDIO20_CTRL_RW << AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS), + TEST_UAC2_CLOCK_SOURCE(UNRELATED_CLOCK_1, 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_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_INPUT_TERM, PLAYBACK_CLOCK), + 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), @@ -436,6 +476,23 @@ static const uint8_t playback_fu_before_terminal[] = { TEST_UAC1_CS_DATA_EP, }; +static const uint8_t playback_after_unrelated_ac_entities[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_INPUT_TERM(UNRELATED_TERM_0, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_INPUT_TERM(UNRELATED_TERM_1, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_FEATURE_UNIT(UNRELATED_FU_0, UNRELATED_TERM_0), + TEST_UAC1_FEATURE_UNIT(UNRELATED_FU_1, UNRELATED_TERM_1), + TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2), + TEST_UAC1_FEATURE_UNIT(PLAYBACK_FU, PLAYBACK_INPUT_TERM), + 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 playback_fu_without_mute_volume[] = { TEST_UAC1_AC_HEADER, TEST_UAC1_FEATURE_UNIT_CTRL(PLAYBACK_FU, PLAYBACK_INPUT_TERM, AUDIO10_FU_CONTROL_BM_BASS), @@ -831,12 +888,19 @@ static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) { control_xfer_count = 0; } -void test_audio_host_saves_and_opens_explicit_feedback_endpoint(void) { - // A MIDI-only AC collection must not consume an Audio Host instance. - TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)midi_only_collection, - sizeof(midi_only_collection))); +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))); TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); +} + +void test_audio_host_rejects_midi2_collection_without_consuming_instance(void) { + TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)midi2_only_collection, + sizeof(midi2_only_collection))); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0)); +} +void test_audio_host_saves_and_opens_explicit_feedback_endpoint(void) { mount_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback)); TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); @@ -976,6 +1040,18 @@ void test_audio_host_uac2_shared_clock_is_discovered_once_for_both_streams(void) TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 1)); } +void test_audio_host_uac2_finds_clock_source_after_unrelated_clocks(void) { + open_descriptors(uac2_playback_after_unrelated_clocks, sizeof(uac2_playback_after_unrelated_clocks)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + + TEST_ASSERT_EQUAL_UINT8(AUDIO20_CS_REQ_RANGE, control_request.bRequest); + TEST_ASSERT_EQUAL_HEX16(tu_u16(PLAYBACK_CLOCK, AUDIO_AC_ITF), tu_le16toh(control_request.wIndex)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 0)); +} + void test_audio_host_reconfigures_stopped_duplex_streams_to_a_new_common_rate(void) { open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock)); TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); @@ -1038,6 +1114,13 @@ void test_audio_host_maps_playback_fu_declared_before_usb_input_terminal(void) { TEST_ASSERT_EQUAL_UINT8(PLAYBACK_FU, tuh_audio_get_feature_unit_id(0, 0)); } +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)); +} + 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); -- cgit v1.3.1 From 178426720cbe2f170872cc1469329570d37726a3 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 03:31:01 +0200 Subject: audio: normalize Feature Unit volume requests Accept the defined silence value and round ordinary volume requests to the nearest supported resolution step within the cached range. Add tests for silence, clamping, and step alignment. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 11 ++++++++++- src/class/audio/audio_host.h | 4 +++- test/unit-test/test/host/audio/test_audio_host.c | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index ad2fd677f..c73e9f225 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -2592,7 +2592,16 @@ bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_x 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); + 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; + } return tuh_audio_feature_unit_set(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, 0, (uint16_t)volume, complete_cb, user_data); } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 42cf4b4e5..4807d376a 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -241,7 +241,9 @@ bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t control 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. +// 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. 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, 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 55b5e8ef6..aa2f70f9b 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -1536,6 +1536,22 @@ void test_audio_host_typed_mute_and_volume_controls(void) { TEST_ASSERT_EQUAL_HEX32(0x4567, fu_cb_user_data); } +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_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_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_HEX8_ARRAY(((uint8_t[]){0x00, 0x80}), control_buffer, 2); + complete_control_xfer(XFER_RESULT_SUCCESS); +} + 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 544ba0453a02bbee3243f592a088eb6838614dfd Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 03:36:11 +0200 Subject: audio: classify feedback endpoints by protocol Use UAC1 synchronization attributes and UAC2 endpoint usage fields according to the active protocol. This prevents UAC2 audio-data IN endpoints from being mistaken for explicit feedback endpoints. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 33 +++++++++++++++++------- test/unit-test/test/host/audio/test_audio_host.c | 29 ++++++++++++++++++++- 2 files changed, 51 insertions(+), 11 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index c73e9f225..5c7ce5f8e 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -1297,15 +1297,28 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ break; } - const uint8_t usage = desc_endpoint->bmAttributes.usage; - const bool implicit_feedback = - usage == (TUSB_ISO_EP_ATT_IMPLICIT_FB >> 4) && tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN; - const bool explicit_feedback = - tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN && - (usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4) || - (usage == (TUSB_ISO_EP_ATT_DATA >> 4) && desc_endpoint->bmAttributes.sync == TUSB_ISO_EP_ATT_NO_SYNC)); - - if (explicit_feedback) { + bool is_data_ep = false; + bool is_explicit_feedback = false; + switch (p_audio->protocol) { + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 + case AUDIO_INT_PROTOCOL_CODE_V1: + is_data_ep = desc_endpoint->bmAttributes.sync != TUSB_ISO_EP_ATT_NO_SYNC; + is_explicit_feedback = tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN && !is_data_ep; + break; + #endif + #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 + case AUDIO_INT_PROTOCOL_CODE_V2: + is_data_ep = desc_endpoint->bmAttributes.usage == (TUSB_ISO_EP_ATT_DATA >> 4) || + desc_endpoint->bmAttributes.usage == (TUSB_ISO_EP_ATT_IMPLICIT_FB >> 4); + is_explicit_feedback = tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN && + desc_endpoint->bmAttributes.usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4); + break; + #endif + default: + break; + } + + if (is_explicit_feedback) { const uint16_t fb_ep_size = tu_edpt_packet_size(desc_endpoint); if (has_feedback_ep || (fb_ep_size != 3 && fb_ep_size != 4)) { TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid/extra feedback ep %02x ignored\r\n", itf_num, alt, @@ -1325,7 +1338,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ break; } - if (usage == (TUSB_ISO_EP_ATT_DATA >> 4) || implicit_feedback) { + if (is_data_ep) { if (has_data_ep) { TU_LOG_DRV(" AUDIO AS itf %u alt %u: extra data ep %02x ignored\r\n", itf_num, alt, desc_endpoint->bEndpointAddress); 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 aa2f70f9b..9f8957984 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -295,6 +295,9 @@ uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { #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_DATA_EP(_ep, _attr, _size, _interval) \ + 7, TUSB_DESC_ENDPOINT, _ep, (TUSB_XFER_ISOCHRONOUS | (_attr)), U16_TO_U8S_LE(_size), _interval + #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) @@ -430,6 +433,19 @@ static const uint8_t uac2_duplex_shared_clock[] = { TEST_UAC2_CS_DATA_EP, }; +static const uint8_t uac2_capture_no_sync_data[] = { + TEST_UAC2_AC_HEADER(46), + 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(CAPTURE_OUTPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, CAPTURE_INPUT_TERM, PLAYBACK_CLOCK), + TEST_UAC2_AS_ALT0, + TEST_UAC2_AS_INTERFACE(1, 1), + TEST_UAC2_AS_GENERAL(CAPTURE_OUTPUT_TERM, 1), + TEST_UAC2_FORMAT(2, 16), + TEST_UAC2_DATA_EP(0x81, TUSB_ISO_EP_ATT_DATA, 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), @@ -446,7 +462,7 @@ static const uint8_t playback_with_implicit_feedback[] = { TEST_UAC1_AS_INTERFACE_NUM(AUDIO_AS_IN_ITF, 1, 1), TEST_UAC1_AS_GENERAL(CAPTURE_OUTPUT_TERM), TEST_UAC1_FORMAT(1, 2, 16, 48000), - TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_IMPLICIT_FB, 96, 1), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS | TUSB_ISO_EP_ATT_IMPLICIT_FB, 96, 1), TEST_UAC1_CS_DATA_EP, }; @@ -1101,6 +1117,17 @@ void test_audio_host_treats_implicit_feedback_as_audio_in_endpoint(void) { TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_CAPTURE, tuh_audio_stream_direction(0, 1)); } +void test_audio_host_uac2_treats_no_sync_data_usage_as_audio_in_endpoint(void) { + open_descriptors(uac2_capture_no_sync_data, sizeof(uac2_capture_no_sync_data)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0)); + TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_CAPTURE, tuh_audio_stream_direction(0, 0)); + TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 0)); +} + void test_audio_host_ignores_second_data_endpoint_in_same_as_interface(void) { open_descriptors(playback_with_extra_data_endpoint, sizeof(playback_with_extra_data_endpoint)); -- cgit v1.3.1 From fa6283249362278b887364ae51f9d866fcb96ad3 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 06:01:12 +0200 Subject: audio: clarify host driver comments Group comments with the structures and helpers they describe, remove review-specific wording, and explain feedback-rate staging and control-buffer lifetimes in terms of runtime behavior. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 393 ++++++++++++++++++++----------------------- 1 file changed, 180 insertions(+), 213 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 5c7ce5f8e..370958aaf 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -7,13 +7,11 @@ */ /* - * 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. + * USB Audio Host driver for UAC1 and UAC2 devices. The public API exposes + * logical capture and playback streams while keeping the USB Audio topology + * private. * - * Each instance (Audio Control interface) provides at most one logical stream - * per direction: + * Each Audio Control interface provides at most one 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() @@ -21,83 +19,45 @@ * OUT transfers from a FIFO filled by the application with tuh_audio_write() * * While a stream is running, the driver keeps one isochronous transfer in - * flight at the endpoint's polling 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 + * 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(). * - * The supported configurations of all Audio Streaming interfaces and alternate - * settings in one direction are presented as a flat list of discrete - * {format, sample_rate, channels} tuples. Internally, configurations are - * grouped by alternate setting so their format and endpoint properties are - * stored only once. The selected mapping is applied by tuh_audio_configure(). + * Configurations are exposed as a flat list of discrete format, sample-rate, + * and channel-count tuples. Internally, configurations are grouped by + * alternate setting so shared endpoint properties are stored only once. * - * 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. Protocol-specific sampling-frequency control. + * Only Type-I PCM formats and discrete UAC1 sampling frequencies are + * registered. tuh_audio_configure() selects and opens endpoints; + * tuh_audio_start() activates the alternate setting and performs the + * protocol-specific sampling-frequency control. */ #include "tusb_option.h" #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 - -#define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) + // Driver-specific log level; defaults to the host-stack log level. + #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__) -//--------------------------------------------------------------------+ -// MACRO CONSTANT TYPEDEF -//--------------------------------------------------------------------+ //--------------------------------------------------------------------+ -// Weak stubs: invoked if no strong implementation is available +// MACROS, CONSTANTS, AND TYPES //--------------------------------------------------------------------+ -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; -} - -TU_ATTR_WEAK void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { - (void)idx; - (void)stream_idx; - (void)xferred_bytes; -} - -TU_ATTR_WEAK void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { - (void)idx; - (void)stream_idx; - (void)xferred_bytes; -} - -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; -} - -// Stream state machine enum { - STREAM_STATE_IDLE = 0, // not configured - STREAM_STATE_READY // configured, ready to start/stop + STREAM_STATE_IDLE = 0, // No active configuration. + STREAM_STATE_READY // Configured and ready to start. }; enum { @@ -106,30 +66,29 @@ enum { 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 + #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. +// UAC1 stores one rate source per alternate setting. UAC2 alternate settings +// that reference 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 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. +// Properties shared by every sampling frequency of one AS alternate setting. typedef struct { - uint16_t ep_size; // endpoint max packet size + uint16_t ep_size; uint8_t itf_num; uint8_t alt_setting; uint8_t ep_addr; uint8_t ep_interval; - uint8_t ep_attr; // bmAttributes synchronization and usage bits + uint8_t ep_attr; // Synchronization and usage fields from bmAttributes. uint8_t format; uint8_t channels; uint8_t terminal_id; @@ -137,10 +96,10 @@ typedef struct { uint8_t rate_count; } audioh_as_config_t; -// Explicit feedback exists only for playback alternate settings. +// Explicit-feedback endpoint associated with a playback alternate setting. typedef struct { uint8_t ep_addr; - uint8_t ep_size; // feedback endpoint max packet size (3 or 4) + uint8_t ep_size; uint8_t ep_interval; uint8_t ep_attr; } audioh_feedback_ep_t; @@ -148,9 +107,9 @@ typedef struct { typedef struct { audioh_feedback_ep_t feedback[CFG_TUH_AUDIO_MAX_AS]; - // Playback pacing in Q16.16 frames per data-endpoint poll interval. - // Feedback is latched only when the current fractional scheduling cycle - // wraps, so one cycle is never generated from two different rates. + // Packet rates use Q16.16 audio frames per data-endpoint poll interval. + // A new feedback rate is staged in pending_frames_q16 and adopted when + // rem_acc wraps. uint32_t nominal_frames_q16; uint32_t target_frames_q16; uint32_t pending_frames_q16; @@ -161,7 +120,7 @@ typedef struct { bool feedback_opened; } audioh_playback_t; -// Control-transfer state does not require USB-accessible memory. +// Control-transfer bookkeeping; transfer payloads are stored in audioh_epbuf_t. typedef struct { tuh_xfer_cb_t complete_cb; uintptr_t user_data; @@ -185,58 +144,55 @@ typedef struct { bool fu_busy; } audioh_ctrl_state_t; -// One logical stream (capture or playback) +// One logical capture or playback stream. 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 + // Identity is initialized once and preserved when the stream is reset. + uint8_t idx; + uint8_t stream_idx; + tusb_dir_t dir; // TUSB_DIR_IN is capture; TUSB_DIR_OUT is playback. - // device owning this stream (0 = no device) + // Device address, or zero while this stream slot is unused. uint8_t daddr; - // Supported configurations (parsed during enumeration) + // Configurations discovered during enumeration. uint8_t as_count; uint8_t config_count; audioh_as_config_t as[CFG_TUH_AUDIO_MAX_AS]; - // Active stream state - uint8_t active_config; // flattened public configuration index + // Selected configuration and runtime state. + uint8_t active_config; // Index in the flattened public configuration list. uint8_t active_as; uint8_t active_rate; - uint8_t state; // STREAM_STATE_* - bool running; // tuh_audio_start() called, transfers may be submitted + uint8_t state; + bool running; - // One Feature Unit associated with this logical stream (0 = none) + // Directly associated Feature Unit, or zero when none is usable. uint8_t feature_unit_id; 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 + // Bytes in one interleaved audio frame across all channels. uint16_t frame_bytes; - // 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 - // _audioh_epbuf[], the endpoint is bound by tu_edpt_stream_open() when the - // stream is configured. + // The FIFO decouples application I/O from isochronous transfers. ep_buf is + // assigned during driver initialization and the endpoint during configure. tu_edpt_stream_t edpt; uint8_t ff_buf[CFG_TUH_AUDIO_STREAM_BUFSIZE]; } tuh_audio_stream_t; -// Per-instance (Audio device) storage +// State owned by one Audio Control interface. 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 daddr; // Device address, or zero for a free instance. + uint8_t ac_itf_num; + uint8_t protocol; 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) + // Public stream indices are assigned in playback-then-capture order. tuh_audio_stream_t out_stream; tuh_audio_stream_t in_stream; audioh_playback_t playback; @@ -248,8 +204,8 @@ typedef struct { #endif typedef struct { - // Clock discovery completes before mount. Afterwards its storage is reused - // by independently cache-aligned sampling-frequency and Feature Unit buffers. + // Clock discovery finishes before mount, so its buffer can be reused by + // runtime sampling-frequency and Feature Unit requests. union { #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 TUH_EPBUF_DEF(clock_range, AUDIOH_CLOCK_RANGE_BUFSIZE); @@ -259,16 +215,50 @@ typedef struct { TUH_EPBUF_DEF(fu_ctrl, 8); } runtime; } control; - // Explicit feedback can overlap both runtime control transfers. + // Feedback transfers may overlap runtime control transfers, so the feedback buffer is separate. TUH_EPBUF_DEF(feedback, 4); - TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); // capture transfer buffer - TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); // playback transfer buffer + TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); + TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); } audioh_epbuf_t; static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; CFG_TUH_MEM_SECTION static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; +//--------------------------------------------------------------------+ +// WEAK APPLICATION CALLBACKS +//--------------------------------------------------------------------+ + +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; +} + +TU_ATTR_WEAK void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; +} + +TU_ATTR_WEAK void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { + (void)idx; + (void)stream_idx; + (void)xferred_bytes; +} + +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; +} + +//--------------------------------------------------------------------+ +// HELPERS +//--------------------------------------------------------------------+ + TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_rate_ctrl(audioh_epbuf_t *epbuf) { return epbuf->control.runtime.rate_ctrl; } @@ -277,9 +267,6 @@ TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_fu_ctrl(audioh_epbuf_t *epbu return epbuf->control.runtime.fu_ctrl; } -//--------------------------------------------------------------------+ -// Helper -//--------------------------------------------------------------------+ TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) { for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { if (_audioh_itf[idx].daddr == 0) { @@ -321,7 +308,6 @@ static tuh_audio_stream_t *audioh_get_stream(audioh_interface_t *p_audio, tusb_d } } -// 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; @@ -400,7 +386,6 @@ static void audioh_stream_set_feature_unit(tuh_audio_stream_t *s, uint8_t unit_i s->volume_access = volume_access; } -// 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; @@ -418,8 +403,7 @@ static bool audioh_format_from_pcm(uint8_t subslot_size, uint8_t bit_resolution, return true; } -// Isochronous bInterval is a power-of-2 exponent in 1 ms full-speed frames -// or 125 us high-speed microframes. +// bInterval encodes 2^(bInterval-1) full-speed frames or high-speed microframes. static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) { const uint32_t unit_us = (tuh_speed_get(daddr) == TUSB_SPEED_HIGH) ? 125u : 1000u; return ((uint32_t)1u << (ep_interval - 1)) * unit_us; @@ -433,7 +417,7 @@ static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_inter return (uint32_t)((numerator + 500000u) / 1000000u); } -// Supported Feature Unit control widths (0 = unsupported variable/unknown width). +// 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: @@ -452,7 +436,7 @@ static uint8_t audioh_fu_control_width(uint8_t control_selector) { } } -// Reset a stream to its unconfigured state (keeps idx, dir, and FIFO configuration) +// 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; @@ -476,7 +460,6 @@ static void audioh_playback_reset(audioh_playback_t *playback) { tu_memclr(playback, sizeof(*playback)); } -// 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++) { audioh_interface_t *p_audio = &_audioh_itf[idx]; @@ -498,7 +481,7 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) } //--------------------------------------------------------------------+ -// Packet scheduler +// PACKET SCHEDULER //--------------------------------------------------------------------+ static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes); @@ -514,31 +497,26 @@ static void audioh_stream_feedback_xfer(tuh_audio_stream_t *s) { } } -// Re-arm the capture endpoint: request one full packet (the device sends at -// most its max packet size per poll interval). The overwritable FIFO retains -// the newest capture frames when the application cannot drain it in time. static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); const audioh_as_config_t *as = audioh_stream_active_as(s); - TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), ); // one transfer in flight + TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), ); - // ep_size is guaranteed <= CFG_TUH_AUDIO_EPIN_BUFSIZE by enumeration if (!usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, as->ep_size)) { audioh_stream_error(s, 0); } } -// Submit the next queued playback packet. Fractional frames per endpoint poll -// interval are accumulated on each successful submission, keeping the average -// data rate at the active nominal or feedback target. static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); 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), ); // one transfer in flight + TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), ); + // Accumulate fractional frames across endpoint intervals so packet lengths + // average to the active nominal or feedback rate. uint32_t frames = playback->target_frames_q16 >> 16; const uint32_t fraction = playback->target_frames_q16 & 0xFFFFu; uint32_t next_rem_acc = playback->rem_acc + fraction; @@ -554,8 +532,8 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { bytes_64 <= CFG_TUH_AUDIO_STREAM_BUFSIZE, ); const uint16_t bytes = (uint16_t)bytes_64; if (tu_fifo_count(&s->edpt.ff) < bytes) { - // Keep the isochronous stream active without consuming a partial frame. - // The queued audio is sent once a complete poll interval is available. + // Isochronous OUT must continue at every interval. Send silence until a + // complete packet is queued, leaving any partial packet in the FIFO. tu_memclr(s->edpt.ep_buf, bytes); } else { tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes); @@ -569,13 +547,13 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { if (loop_done && playback->feedback_pending) { playback->target_frames_q16 = playback->pending_frames_q16; playback->feedback_pending = false; - // Keep the remainder from the completed cycle. Clearing it for every - // feedback update biases the average toward the integer packet sizes. + // Preserve the accumulator at the wrap boundary. Resetting it for each + // feedback update would bias the average toward integer packet lengths. } } //--------------------------------------------------------------------+ -// Configure state machine +// STREAM CONFIGURATION //--------------------------------------------------------------------+ static bool audioh_stream_close_ep(tuh_audio_stream_t *s) { @@ -624,7 +602,6 @@ 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); } -// 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 audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); @@ -672,7 +649,6 @@ static bool audioh_stream_set_freq(tuh_audio_stream_t *s, tuh_xfer_cb_t complete return tuh_control_xfer(&xfer); } -// Reconstruct the endpoint descriptor of the selected configuration and open it static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { const audioh_as_config_t *as = audioh_stream_active_as(s); @@ -691,7 +667,7 @@ static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { return false; } - // Bind the transfer helper to the endpoint and start with an empty FIFO + // Bind the transfer helper to the selected endpoint and empty its 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); @@ -722,7 +698,7 @@ static bool audioh_stream_open_ep(tuh_audio_stream_t *s) { } //--------------------------------------------------------------------+ -// USBH API +// USB HOST CLASS DRIVER //--------------------------------------------------------------------+ bool audioh_init(void) { tu_memclr(&_audioh_itf, sizeof(_audioh_itf)); @@ -736,7 +712,6 @@ bool audioh_init(void) { 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, true, 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, @@ -775,7 +750,8 @@ void audioh_close(uint8_t daddr) { } audioh_playback_reset(&p_audio->playback); - tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl)); // drop pending control state + // A disconnected device cannot complete its pending control request. + tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl)); p_audio->stream_count = 0; p_audio->daddr = 0; @@ -790,7 +766,7 @@ static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_byt audioh_playback_t *playback = audioh_get_playback(s); uint32_t feedback_q16; if (xferred_bytes == 3) { - // Full-speed feedback is normally Q10.14. Keep the scheduler in Q16.16. + // Three-byte feedback is Q10.14; the scheduler uses Q16.16 throughout. feedback_q16 = ((uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16)) << 2; } else if (xferred_bytes == 4) { feedback_q16 = (uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16) | ((uint32_t)fb[3] << 24); @@ -806,8 +782,8 @@ static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_byt return; } - // Feedback is expressed per USB frame/microframe. Scale it to the data - // endpoint's polling interval before handing it to the packet scheduler. + // Feedback is measured per USB frame or microframe. Scale it to the data + // endpoint's polling interval. const audioh_as_config_t *as = audioh_stream_active_as(s); const uint64_t target_q16_64 = (uint64_t)feedback_q16 << (as->ep_interval - 1u); if (target_q16_64 > UINT32_MAX) { @@ -822,8 +798,8 @@ static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_byt return; } - // Keep only the newest feedback sample. The packet scheduler promotes it at - // the end of its current fractional cycle. + // Replace any unconsumed sample; only the newest rate matters. The scheduler + // adopts it when the current fractional packet pattern wraps. playback->pending_frames_q16 = target_q16; playback->feedback_pending = true; } @@ -834,14 +810,14 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin return false; } - // Failed, stalled, or aborted transfers never carry valid audio data + // 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); return true; } - // Stopped stream: the in-flight transfer completes and its data is discarded + // A stop does not cancel the transfer that was already in flight. if (!s->running) { return true; } @@ -854,8 +830,7 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin } 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 + // Queue whole capture frames, notify the application, then re-arm. 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); @@ -863,7 +838,7 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin 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 + // 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); } @@ -871,7 +846,7 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin } //--------------------------------------------------------------------+ -// Enumeration +// ENUMERATION //--------------------------------------------------------------------+ typedef struct { @@ -1225,9 +1200,8 @@ static bool audioh_uac1_rates_store(const audioh_interface_t *p_audio, const tuh } #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. +// Parse one AS alternate setting and return the next interface descriptor. +// Supported configurations are appended to the stream matching its endpoint. static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *ac_desc, const tusb_desc_interface_t *desc_itf, const uint8_t *p_desc, const uint8_t *desc_end) { @@ -1238,7 +1212,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ p_desc = tu_desc_next(p_desc); - // Alternate setting 0 has no endpoints: nothing to stream + // Alternate setting zero is the zero-bandwidth setting, not a configuration. if (alt == 0 || desc_itf->bNumEndpoints == 0) { while (p_desc < desc_end) { TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL); @@ -1250,12 +1224,11 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ return p_desc; } - // Parse the class-specific and endpoint descriptors of this alternate setting 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 - // and are handled normally when they are the AS interface's data endpoint. + // Retain one data endpoint and, for playback, one explicit-feedback endpoint. + // An implicit-feedback IN endpoint remains the data endpoint of its own AS + // interface and is therefore exposed as a capture stream. typedef struct { uint8_t ep_addr; uint16_t ep_size; @@ -1299,6 +1272,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ bool is_data_ep = false; bool is_explicit_feedback = false; + // UAC1 distinguishes feedback by synchronization type; UAC2 uses the + // endpoint usage field. switch (p_audio->protocol) { #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 case AUDIO_INT_PROTOCOL_CODE_V1: @@ -1348,7 +1323,7 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ ep_info.ep_addr = desc_endpoint->bEndpointAddress; ep_info.ep_size = tu_edpt_packet_size(desc_endpoint); ep_info.ep_interval = desc_endpoint->bInterval; - // bInterval must be in [1, 16] for isochronous endpoints + // Isochronous bInterval is an exponent in the inclusive range 1..16. if (ep_info.ep_interval == 0 || ep_info.ep_interval > 16) { ep_info.ep_interval = 1; } @@ -1368,7 +1343,6 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ return p_desc; } - // Reject unsupported formats explicitly. bool pcm_supported = false; switch (p_audio->protocol) { #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1 @@ -1408,8 +1382,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ TU_LOG_DRV(" AUDIO AS itf %u: frame size %lu not supported\r\n", itf_num, (unsigned long)frame_bytes_32); return p_desc; } - // Register one AS alternate setting containing its discrete sampling - // frequencies. The public API flattens these entries when requested. + // Store the alternate setting once; the public API expands its sampling + // frequencies into separate configurations. 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); @@ -1428,8 +1402,8 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ return p_desc; } - // Capture: the device can deliver up to its max packet size per poll - // interval, the transfer buffer must fit it + // Capture always requests the endpoint's maximum packet size, so both the + // transfer buffer and FIFO must hold it. if (stream->dir == TUSB_DIR_IN && (ep->ep_size > epbuf_size || ep->ep_size > CFG_TUH_AUDIO_STREAM_BUFSIZE)) { TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds buffer capacity\r\n", itf_num, alt, ep->ep_size); return p_desc; @@ -1558,7 +1532,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface } ac_desc.desc_end = p_desc; - // Parse the contiguous Audio Streaming interfaces of this audio function. + // Audio Streaming interfaces belonging to this function immediately follow + // its Audio Control descriptor block. while (p_desc < desc_end) { if (!audioh_desc_valid(p_desc, desc_end, 2)) { goto open_failed; @@ -1588,14 +1563,11 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface audioh_link_feature_units(p_audio, &ac_desc); - // 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 + // Assign contiguous public indices in playback-then-capture order. uint8_t stream_idx = 0; if (p_audio->out_stream.as_count > 0) { p_audio->out_stream.stream_idx = stream_idx++; @@ -1621,7 +1593,7 @@ open_failed: } //--------------------------------------------------------------------+ -// Set Configuration +// SET CONFIGURATION //--------------------------------------------------------------------+ static void audioh_mount_feature_unit_next(uint8_t idx); @@ -1805,8 +1777,8 @@ 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_start(). + // Only the Audio Control interface drives mounting. Streaming alternate + // settings are selected later by tuh_audio_start(). usbh_driver_set_config_complete(dev_addr, itf_num); return true; } @@ -1826,7 +1798,7 @@ bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { } //--------------------------------------------------------------------+ -// Application API +// APPLICATION API //--------------------------------------------------------------------+ bool tuh_audio_mounted(uint8_t idx) { TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); @@ -1933,18 +1905,17 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx uint8_t rate_idx; TU_VERIFY(audioh_stream_resolve_config(s, config_idx, &as_idx, &rate_idx), false); audioh_stream_config_fill(s, as_idx, rate_idx, &cfg); - // 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 + // Configuration cannot close an endpoint while its final transfer drains. TU_VERIFY(!usbh_edpt_busy(s->daddr, s->edpt.ep_addr), false); if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) { TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); } } - // The HCD endpoint must be reopened even when the new configuration uses - // the same address, since its packet size and interval may have changed. + // Reopen even when the address is unchanged: packet size and interval belong + // to the alternate setting and may differ. TU_VERIFY(audioh_stream_close_ep(s), false); const audioh_as_config_t *as = &s->as[as_idx]; @@ -1965,8 +1936,7 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx playback->rem_acc = 0; } 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. + // Overwrite mode is frame-safe only when FIFO depth is a whole-frame multiple. const uint16_t fifo_depth = CFG_TUH_AUDIO_STREAM_BUFSIZE - (CFG_TUH_AUDIO_STREAM_BUFSIZE % s->frame_bytes); if (!tu_fifo_config(&s->edpt.ff, s->ff_buf, fifo_depth, true)) { audioh_stream_fail(s); @@ -1981,12 +1951,11 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx 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 +// Start endpoint transfers after the alternate setting and sampling frequency +// are both active. 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 + audioh_stream_capture_xfer(s); } else { if (audioh_get_playback(s)->feedback[s->active_as].ep_addr != 0) { audioh_stream_feedback_xfer(s); @@ -1994,7 +1963,7 @@ static void audioh_stream_start_xfer(tuh_audio_stream_t *s) { if (!s->running) { return; } - audioh_stream_playback_xfer(s); // start the continuous playback transfer chain + audioh_stream_playback_xfer(s); } } @@ -2008,7 +1977,8 @@ static bool audioh_stream_activate(tuh_audio_stream_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) { - return; // device is gone or the stream was stopped meanwhile + // Ignore a completion delivered after disconnect or stop. + return; } if (xfer->result != XFER_RESULT_SUCCESS) { TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result); @@ -2048,7 +2018,8 @@ static bool audioh_stream_start_active(tuh_audio_stream_t *s) { 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->state != STREAM_STATE_READY || !s->running) { - return; // device is gone or the stream was stopped meanwhile + // Ignore a completion delivered after disconnect or stop. + return; } if (xfer->result != XFER_RESULT_SUCCESS) { TU_LOG_DRV(" AUDIO SET_INTERFACE activate failed: result=%u\r\n", xfer->result); @@ -2066,7 +2037,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); - // Wait for any in-flight transfer to complete and be discarded + // 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); TU_VERIFY(!usbh_edpt_busy(s->daddr, as->ep_addr), false); @@ -2074,8 +2045,7 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); } - // Do not change a device-wide/shared clock while the other direction is - // running. Stopped streams may be reconfigured independently in either order. + // Capture and playback must use the same rate while both are running. tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; if (other->running) { const audioh_as_config_t *other_as = audioh_stream_active_as(other); @@ -2095,8 +2065,8 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { p_audio->playback.rem_acc = 0; } s->running = true; - // UAC2 changes the Clock Source before selecting the alternate setting; - // UAC1 selects the alternate first because its control targets the endpoint. + // 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; #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2 && rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) { @@ -2113,8 +2083,6 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { return true; } -// 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) { @@ -2132,13 +2100,11 @@ bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) { TU_VERIFY(s && s->state == STREAM_STATE_READY, false); const audioh_as_config_t *as = audioh_stream_active_as(s); - // Leave the stream running if SET_INTERFACE cannot be submitted, so the - // caller can retry without the host and device states diverging. + // Preserve running state when submission fails so the caller can retry. TU_VERIFY(tuh_interface_set(s->daddr, as->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 being deactivated so - // the device stops transferring. + // 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; @@ -2155,12 +2121,11 @@ uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer 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 + // Never split an audio frame at the FIFO boundary. const uint32_t frames = TU_MIN(frame_count, tu_fifo_remaining(&s->edpt.ff) / s->frame_bytes); if (frames == 0) { return 0; @@ -2176,12 +2141,11 @@ uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint3 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 + // Never return a partial audio frame. 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)); @@ -2212,10 +2176,11 @@ uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) { } //--------------------------------------------------------------------+ -// Feature Unit Control API +// FEATURE UNIT CONTROLS //--------------------------------------------------------------------+ -// Release the stable SET buffer and chain to the application callback +// 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; @@ -2281,7 +2246,8 @@ static void audioh_fu_value_store(audioh_ctrl_state_t *ctrl, audioh_epbuf_t *epb } } -// Convert the raw control value to host order and chain to the application callback +// Convert the driver-owned response before releasing the request state and +// invoking the application callback. static void audioh_fu_get_complete(tuh_xfer_t *xfer) { const uint8_t idx = (uint8_t)xfer->user_data; audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl; @@ -2467,7 +2433,8 @@ bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t stream_idx, uint8_t control audioh_ctrl_state_t *ctrl = &p_audio->ctrl; audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; TU_VERIFY(!ctrl->fu_busy, false); - ctrl->fu_busy = true; // reserve the request state and fu_ctrl before writing + // 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, @@ -2545,8 +2512,8 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con .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 + // 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, @@ -2566,7 +2533,7 @@ static bool audioh_feature_unit_get(uint8_t idx, uint8_t stream_idx, uint8_t con return true; } - // Async path: chain the host-order conversion to the application callback + // 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, -- 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 'src/class') 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 fa72b235db44741ee72897a5fa252e858244b6ec Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Thu, 27 Aug 2026 09:55:15 +0200 Subject: audio: correct host API documentation Describe configuration limits by their actual storage scope, document asynchronous stream startup and shutdown, clarify that FIFO APIs are always non-blocking, and correct playback callback ordering. Signed-off-by: HiFiPHile --- src/class/audio/audio_host.h | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 2e778c184..f83d7ea63 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -40,11 +40,14 @@ extern "C" { #ifndef CFG_TUH_AUDIO_MAX #define CFG_TUH_AUDIO_MAX 1 #endif -// Maximum number of discrete sampling frequencies per Audio Streaming interface +// Maximum discrete sampling frequencies retained per rate source. UAC1 uses +// one rate source per alternate setting; UAC2 alternate settings may share a +// Clock Source. #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 +// Maximum supported nonzero-bandwidth Audio Streaming alternate settings per +// logical stream. #ifndef CFG_TUH_AUDIO_MAX_AS #define CFG_TUH_AUDIO_MAX_AS 4 #endif @@ -169,20 +172,25 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx // 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. +// Startup is asynchronous: true means that the first request was submitted. +// Later startup failures are reported through tuh_audio_err_cb(). bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx); -// Stop transferring and deactivate the Audio Streaming interface (alt 0). +// Stop transferring and asynchronously deactivate the Audio Streaming +// interface (alt 0). true means that the deactivation request was submitted. bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx); // 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. +// Both functions are non-blocking and return immediately. // 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); -// FIFO occupancy in frames available for a non-blocking write/read. +// Number of frames that can be queued immediately for playback. uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx); +// Number of captured frames that can be read immediately. uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx); //--------------------------------------------------------------------+ @@ -297,9 +305,9 @@ void tuh_audio_umount_cb(uint8_t idx); // 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 playback packet is submitted from the stream FIFO, or as silence when -// a complete packet is not queued. +// Invoked after a successful isochronous OUT transfer, before the next packet +// is prepared. After this callback returns, the driver submits queued audio +// 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 -- 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 'src/class') 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 7946c153aa6621f7c63ca72b54bdf02646c97ed2 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 1 Sep 2026 07:45:30 +0200 Subject: improve architecture documentation Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 109 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 22 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 00d01b391..11f7a5733 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -6,34 +6,99 @@ * This file is part of the TinyUSB stack. */ +// clang-format off /* - * USB Audio Host driver for UAC1 and UAC2 devices. The public API exposes - * logical capture and playback streams while keeping the USB Audio topology - * private. + * USB Audio Host driver architecture + * ================================== * - * Each Audio Control interface provides at most one 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() + * One audioh_interface_t represents an Audio Control (AC) interface and owns + * at most one logical stream in each direction. A capture stream receives + * isochronous IN data from the device; a playback stream sends isochronous OUT + * data to the device. Audio topology remains private, while applications see + * each stream as a flat list of format, sample-rate, and channel-count tuples. + * Internally, tuples using the same Audio Streaming (AS) alternate setting + * share one audioh_as_config_t and refer to a rate source by index. + * Only Type-I PCM configurations are exposed. UAC1 requires a discrete + * sampling-frequency list; UAC2 Clock Source ranges are expanded into the + * bounded public list. * - * 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. 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(). + * Mounting discovers the topology and completes any control requests needed + * to describe the public configurations: * - * Configurations are exposed as a flat list of discrete format, sample-rate, - * and channel-count tuples. Internally, configurations are grouped by - * alternate setting so shared endpoint properties are stored only once. + * USB enumeration + * audioh_open() + * +-- validate and retain the AC descriptor range + * +-- audioh_parse_as() for each consecutive AS interface + * | +-- parse the protocol-specific AS and format descriptors + * | +-- associate the data and optional feedback endpoints + * | `-- store UAC1 rates or a UAC2 Clock Source reference + * +-- audioh_link_feature_units() + * `-- tuh_audio_descriptor_cb() * - * Only Type-I PCM formats and discrete UAC1 sampling frequencies are - * registered. tuh_audio_configure() selects and opens endpoints; - * tuh_audio_start() activates the alternate setting and performs the - * protocol-specific sampling-frequency control. + * audioh_set_config() + * +-- UAC2: audioh_mount_clock_next() + * | `-- RANGE/CUR completion -> next Clock Source + * | -> rebuild public configurations + * `-- audioh_mount_feature_unit_next() + * `-- volume RANGE completion -> next logical stream + * -> tuh_audio_mount_cb() + * -> usbh_driver_set_config_complete() + * + * 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. + * + * Stream configuration is local; stream activation is asynchronous: + * + * tuh_audio_configure(stream, configuration) + * +-- resolve the public tuple to AS and rate-source indices + * +-- close endpoints from the previous configuration + * +-- initialize frame size, FIFO, and playback scheduler state + * `-- audioh_stream_open_ep() (data and optional feedback EP) + * + * tuh_audio_start(stream) + * +-- UAC1: SET_INTERFACE(non-zero alt) + * | `-- optional endpoint SET_CUR(sample rate) + * +-- UAC2: optional Clock Source CUR(sample rate) + * | `-- SET_INTERFACE(non-zero alt) + * `-- audioh_stream_start_xfer() + * `-- tuh_audio_event_cb(START_COMPLETE) + * + * tuh_audio_stop(stream) + * +-- SET_INTERFACE(alt 0) and stop local transfer resubmission + * `-- completion -> tuh_audio_event_cb(STOP_COMPLETE) + * + * A successful start/stop API return means that the first control request was + * submitted. The corresponding event reports completion of the entire chain. + * UAC1 sets the rate after activating the endpoint because its control targets + * that endpoint; UAC2 sets the Clock Source before activating the AS interface. + * + * Once started, each endpoint completion prepares and submits its successor: + * + * host controller -> audioh_xfer_cb() + * +-- capture data + * | +-- copy whole audio frames to the overwrite FIFO + * | +-- tuh_audio_capture_cb() + * | `-- audioh_stream_capture_xfer() + * +-- playback data + * | +-- tuh_audio_playback_cb() + * | `-- audioh_stream_playback_xfer() + * | +-- calculate the next fractional packet size + * | +-- read a complete packet from the FIFO, or send silence + * | `-- submit the next OUT transfer + * `-- explicit feedback + * +-- validate and stage the Q10.14 or Q16.16 rate + * `-- audioh_stream_feedback_xfer() + * + * tuh_audio_read() and tuh_audio_write() access only the stream FIFOs and do + * not need to run from transfer callbacks. The FIFOs decouple application I/O + * from USB polling cadence and never expose partial interleaved audio frames. + * A transfer failure stops resubmission and is reported through + * tuh_audio_event_cb(XFER_FAILED). */ +// clang-format on #include "tusb_option.h" -- cgit v1.3.1 From b8a97da8f8c67ed8cf987043409225465d7c41ad Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 1 Sep 2026 09:37:33 +0200 Subject: improve feedback flow control Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 41 +++++++------------ test/unit-test/test/host/audio/test_audio_host.c | 52 ++++++++++++++++-------- 2 files changed, 50 insertions(+), 43 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 11f7a5733..1a68223a5 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -178,16 +178,14 @@ typedef struct { typedef struct { audioh_feedback_ep_t feedback[CFG_TUH_AUDIO_MAX_AS]; - // Packet rates use Q16.16 audio frames per data-endpoint poll interval. - // A new feedback rate is staged in pending_frames_q16 and adopted when - // rem_acc wraps. + // Packet rates use Q16.16 audio frames per data-endpoint poll interval. The + // scheduler snapshots target_frames_q16 once per packet and retains rem_acc, + // which integrates fractional frames across feedback updates. uint32_t nominal_frames_q16; uint32_t target_frames_q16; - uint32_t pending_frames_q16; uint16_t feedback_min_frames; uint16_t feedback_max_frames; uint16_t rem_acc; - bool feedback_pending; bool feedback_opened; } audioh_playback_t; @@ -570,21 +568,22 @@ static bool audioh_stream_playback_xfer(tuh_audio_stream_t *s) { audioh_playback_t *playback = audioh_get_playback(s); 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. - uint32_t frames = playback->target_frames_q16 >> 16; - const uint32_t fraction = playback->target_frames_q16 & 0xFFFFu; + // Use one target for the entire packet calculation. Retaining the fractional + // remainder makes the scheduled total follow the sum of changing feedback + // values with less than one frame of quantization error. + const uint32_t target_q16 = playback->target_frames_q16; + uint32_t frames = target_q16 >> 16; + const uint32_t fraction = target_q16 & 0xFFFFu; uint32_t next_rem_acc = playback->rem_acc + fraction; - bool loop_done = (fraction == 0); if (next_rem_acc >= 65536u) { next_rem_acc -= 65536u; frames++; - loop_done = true; } 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, false); + 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 @@ -598,12 +597,6 @@ static bool audioh_stream_playback_xfer(tuh_audio_stream_t *s) { return false; } playback->rem_acc = (uint16_t)next_rem_acc; - if (loop_done && playback->feedback_pending) { - playback->target_frames_q16 = playback->pending_frames_q16; - playback->feedback_pending = false; - // Preserve the accumulator at the wrap boundary. Resetting it for each - // feedback update would bias the average toward integer packet lengths. - } return true; } @@ -651,7 +644,6 @@ static void audioh_stream_stop_xfers(tuh_audio_stream_t *s) { if (s->dir == TUSB_DIR_OUT) { audioh_playback_t *playback = audioh_get_playback(s); playback->target_frames_q16 = playback->nominal_frames_q16; - playback->feedback_pending = false; playback->rem_acc = 0; } tu_edpt_stream_clear(&s->edpt); @@ -858,10 +850,10 @@ static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_byt return; } - // Replace any unconsumed sample; only the newest rate matters. The scheduler - // adopts it when the current fractional packet pattern wraps. - playback->pending_frames_q16 = target_q16; - playback->feedback_pending = true; + // Host-class callbacks run serially. The playback scheduler snapshots this + // target before calculating a packet, so an update cannot split a packet + // calculation across two rates. + playback->target_frames_q16 = target_q16; } bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { @@ -1994,10 +1986,8 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx audioh_playback_t *playback = &p_audio->playback; playback->nominal_frames_q16 = audioh_nominal_frames_q16(cfg.sample_rate, as->ep_interval, s->daddr); playback->target_frames_q16 = playback->nominal_frames_q16; - playback->pending_frames_q16 = 0; playback->feedback_min_frames = (uint16_t)((cfg.sample_rate - 1u) / frame_div); playback->feedback_max_frames = (uint16_t)(cfg.sample_rate / frame_div + 1u); - playback->feedback_pending = false; playback->rem_acc = 0; } if (s->dir == TUSB_DIR_IN) { @@ -2135,7 +2125,6 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { 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; } s->running = true; 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 05b699b68..0f865a5bb 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -1756,7 +1756,7 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes[9]); } -void test_audio_host_applies_10_14_feedback_after_fractional_scheduling_loop(void) { +void test_audio_host_applies_10_14_feedback_to_next_playback_packet(void) { mount_descriptors(playback_44100_with_feedback_10_14, sizeof(playback_44100_with_feedback_10_14)); TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); @@ -1765,18 +1765,9 @@ void test_audio_host_applies_10_14_feedback_after_fractional_scheduling_loop(voi TEST_ASSERT_EQUAL_UINT16(3, edpt_xfer_bytes_at(0x81, 0)); TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 0)); - // Request 44 frames/ms after the first nominal 44.1-kHz packet. The old - // cycle must still emit its 45-frame correction packet before changing rate. - complete_feedback_xfer(44u << 16, 3); - for (uint8_t i = 0; i < 10; i++) { - complete_playback_xfer(); - } - - for (uint8_t i = 0; i < 9; i++) { - TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, i)); - } - TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9)); - TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 10)); + complete_feedback_xfer(45u << 16, 3); + complete_playback_xfer(); + TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 1)); } void test_audio_host_accepts_16_16_feedback(void) { @@ -1788,12 +1779,39 @@ void test_audio_host_accepts_16_16_feedback(void) { TEST_ASSERT_EQUAL_UINT16(4, edpt_xfer_bytes_at(0x81, 0)); complete_feedback_xfer(45u << 16, 4); - for (uint8_t i = 0; i < 10; i++) { + complete_playback_xfer(); + TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 1)); +} + +void test_audio_host_integrates_jittering_feedback_without_resetting_fraction(void) { + test_speed = TUSB_SPEED_HIGH; + mount_descriptors(playback_44100_with_feedback_16_16, sizeof(playback_44100_with_feedback_16_16)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + + const uint32_t feedback_q16[] = { + (5u << 16) + 655u, // 5.01 frames/microframe + (5u << 16) + 1311u, // 5.02 frames/microframe + (5u << 16) + 655u, // 5.01 frames/microframe + }; + uint32_t rem_acc = 0x8333u; // Fraction left by the initial nominal 5.5125-frame packet. + + for (uint8_t packet = 1; packet < 24; packet++) { + const uint32_t target = feedback_q16[(packet - 1u) % TU_ARRAY_SIZE(feedback_q16)]; + uint16_t frames = (uint16_t)(target >> 16); + uint32_t next_rem = rem_acc + (target & 0xFFFFu); + if (next_rem >= 65536u) { + next_rem -= 65536u; + frames++; + } + + complete_feedback_xfer(target, 4); complete_playback_xfer(); + TEST_ASSERT_EQUAL_UINT16((uint16_t)(frames * 4u), edpt_xfer_bytes_at(0x01, packet)); + rem_acc = next_rem; } - - TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9)); - TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 10)); } void test_audio_host_preserves_high_speed_feedback_fraction_across_updates(void) { -- cgit v1.3.1 From 93545d0bfb90b48a33a08b8b12df21a886b7a338 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Wed, 2 Sep 2026 13:30:09 +0200 Subject: remove bloat check Signed-off-by: HiFiPHile --- src/class/audio/audio_host.c | 56 +++++++++----------------------------------- 1 file changed, 11 insertions(+), 45 deletions(-) (limited to 'src/class') diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 1a68223a5..5ad3ed960 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -375,14 +375,7 @@ static bool audioh_protocol_enabled(uint8_t protocol) { } 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; - } + return (direction == TUSB_DIR_IN) ? &p_audio->in_stream : &p_audio->out_stream; } static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, uint8_t stream_idx) { @@ -395,6 +388,11 @@ static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, return NULL; } +TU_ATTR_ALWAYS_INLINE static inline tuh_audio_stream_t *audioh_get_stream_by_idx_unchecked( + audioh_interface_t *p_audio, uint8_t stream_idx) { + return (p_audio->out_stream.stream_idx == stream_idx) ? &p_audio->out_stream : &p_audio->in_stream; +} + TU_ATTR_ALWAYS_INLINE static inline audioh_playback_t *audioh_get_playback(const tuh_audio_stream_t *s) { return &_audioh_itf[s->idx].playback; } @@ -413,8 +411,6 @@ static bool audioh_as_rate_fits(const audioh_interface_t *p_audio, const tuh_aud 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].rate_count) { const audioh_as_config_t *as = &s->as[i]; @@ -545,25 +541,18 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) static void audioh_stream_xfer_failed(tuh_audio_stream_t *s, tusb_xfer_result_t result); 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, 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 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), false); return usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, as->ep_size); } 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), false); @@ -580,11 +569,7 @@ static bool audioh_stream_playback_xfer(tuh_audio_stream_t *s) { frames++; } - 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, - false); - const uint16_t bytes = (uint16_t)bytes_64; + const uint16_t bytes = (uint16_t)(frames * s->frame_bytes); if (tu_fifo_count(&s->edpt.ff) < bytes) { // Isochronous OUT must continue at every interval. Send silence until a // complete packet is queued, leaving any partial packet in the FIFO. @@ -1103,7 +1088,7 @@ static bool audioh_ac_clock_find(const audioh_ac_desc_range_t *range, uint8_t id static void audioh_link_feature_units(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *range) { 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) { + if (stream->as_count == 0) { continue; } audioh_terminal_info_t terminal; @@ -1265,8 +1250,6 @@ static bool audioh_uac1_rates_store(const audioh_interface_t *p_audio, const tuh static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *ac_desc, const tusb_desc_interface_t *desc_itf, const uint8_t *p_desc, const uint8_t *desc_end) { - TU_VERIFY(audioh_desc_valid(p_desc, desc_end, sizeof(tusb_desc_interface_t)), NULL); - const uint8_t itf_num = desc_itf->bInterfaceNumber; const uint8_t alt = desc_itf->bAlternateSetting; @@ -1446,7 +1429,6 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ // frequencies into separate configurations. 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); audioh_terminal_info_t terminal; if (!audioh_ac_terminal_find(p_audio, ac_desc, class_info.terminal_id, &terminal) || terminal.stream_dir != stream->dir) { @@ -1542,8 +1524,6 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { (void)rhport; - TU_VERIFY(TUH_VALIDATE_BASIC(max_len >= sizeof(tusb_desc_interface_t)), 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; @@ -1673,7 +1653,6 @@ static void audioh_uac2_configs_rebuild(audioh_interface_t *p_audio) { 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); @@ -1944,7 +1923,6 @@ bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_id 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); return audioh_stream_config_get(s, config_idx, config); } @@ -1956,7 +1934,6 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx); TU_VERIFY(s, false); - TU_VERIFY(config_idx < s->config_count, false); tuh_audio_stream_config_t cfg; uint8_t as_idx; uint8_t rate_idx; @@ -2185,7 +2162,6 @@ uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer 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); - TU_VERIFY(frame_count > 0, 0); // Never split an audio frame at the FIFO boundary. const uint32_t frames = TU_MIN(frame_count, tu_fifo_remaining(&s->edpt.ff) / s->frame_bytes); @@ -2205,7 +2181,6 @@ uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint3 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); - TU_VERIFY(frame_count > 0, 0); // Never return a partial audio frame. const uint32_t frames = TU_MIN(frame_count, tu_fifo_count(&s->edpt.ff) / s->frame_bytes); @@ -2405,8 +2380,7 @@ static bool audioh_mount_feature_unit_submit(uint8_t idx) { audioh_interface_t *p_audio = &_audioh_itf[idx]; audioh_ctrl_state_t *ctrl = &p_audio->ctrl; audioh_epbuf_t *epbuf = &_audioh_epbuf[idx]; - tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, ctrl->fu.mount.stream_idx); - TU_ASSERT(s != NULL); + tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx); const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; const uint8_t selector = uac2 ? AUDIO20_FU_CTRL_VOLUME : AUDIO10_FU_CTRL_VOLUME; @@ -2431,8 +2405,7 @@ static void audioh_mount_feature_unit_next(uint8_t idx) { audioh_ctrl_state_t *ctrl = &p_audio->ctrl; while (ctrl->fu.mount.stream_idx < p_audio->stream_count) { - tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, ctrl->fu.mount.stream_idx); - TU_ASSERT(s != NULL, ); + 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) { s->volume_range = (tuh_audio_volume_range_t){0}; ctrl->fu.mount.range_step = AUDIOH_VOLUME_RANGE_MIN; @@ -2463,8 +2436,7 @@ static void audioh_mount_feature_unit_complete(tuh_xfer_t *xfer) { if (!ctrl->fu_busy) { return; } - tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, ctrl->fu.mount.stream_idx); - TU_ASSERT(s != NULL, ); + tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx); const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2; if (uac2 && xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 8 && @@ -2517,7 +2489,6 @@ static bool audioh_fu_set(uint8_t idx, uint8_t stream_idx, uint8_t control_selec } 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]; TU_VERIFY(!ctrl->fu_busy, false); @@ -2567,7 +2538,6 @@ static bool audioh_fu_get(uint8_t idx, uint8_t stream_idx, uint8_t control_selec } 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]; TU_VERIFY(!ctrl->fu_busy, false); @@ -2609,12 +2579,10 @@ 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) { - TU_VERIFY(tuh_audio_mute_supported(idx, stream_idx), false); 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_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_MUTE, mute, 1, AUDIOH_FU_VALUE_BOOL, complete_cb, user_data); } @@ -2637,8 +2605,6 @@ bool tuh_audio_volume_set(uint8_t idx, uint8_t stream_idx, int16_t volume, tuh_x 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_fu_get(idx, stream_idx, AUDIO10_FU_CTRL_VOLUME, volume, 2, AUDIOH_FU_VALUE_I16, complete_cb, user_data); } -- 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 'src/class') 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 'src/class') 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