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(-) 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