summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiFiPHile <[email protected]>2026-08-25 09:29:07 +0200
committerHiFiPHile <[email protected]>2026-08-25 09:29:07 +0200
commitad9ecddd7f51ede415992397b82399d7a2639c0f (patch)
tree748be25df046f87485f61db940455f971a1e95d5
parent3f0a82a6bbe35135ed04749b4022ca6563073d5d (diff)
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 <[email protected]>
-rw-r--r--src/class/audio/audio_host.c38
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c30
2 files changed, 56 insertions, 12 deletions
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));