From a0249ada9096365697340031a7b4a285beb18a2b Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 12 Aug 2026 22:36:49 +0700 Subject: usbd: don't leak the queued-setup counter when the event queue is full A SETUP arriving while the event queue is full is silently dropped by queue_event(), but _usbd_queued_setup has already been incremented. The leaked count makes the event handler skip every subsequent SETUP ("Skipped since there is other SETUP in queue") forever: EP0 stays deaf until tud_init() while the device otherwise looks alive - enumerated, endpoints armed. Undo the increment when the enqueue fails. Unit test: fill the queue so a SETUP is dropped, then verify the next SETUP still completes a GET_DESCRIPTOR control transfer. --- test/unit-test/test/device/usbd/test_usbd.c | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'test/unit-test') diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c index 7f3c3f5b2..935a20221 100644 --- a/test/unit-test/test/device/usbd/test_usbd.c +++ b/test/unit-test/test/device/usbd/test_usbd.c @@ -270,6 +270,44 @@ void test_usbd_control_in_zlp(void) tud_task(); } +//--------------------------------------------------------------------+ +// SETUP dropped by full event queue +//--------------------------------------------------------------------+ + +// When the event queue is full, queue_event() drops the SETUP event. The queued-setup +// counter must not keep the dropped SETUP's increment: a leaked count makes the handler +// skip every later SETUP ("other SETUP in queue") forever, leaving EP0 permanently deaf. +void test_usbd_setup_dropped_by_full_queue_recovers(void) +{ + // fillers drain through usbd_reset -> class reset + mscd_reset_Ignore(); + + // fill the queue to the brim, then post one more SETUP: queue_event() drops it + for (unsigned i = 0; i < CFG_TUD_TASK_QUEUE_SZ; i++) { + dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, false); + } + dcd_event_setup_received(rhport, (uint8_t*) &req_get_desc_device, false); + + // drain all fillers (each tud_task pass handles at most CFG_TUD_TASK_EVENTS_PER_RUN + // events); the dropped SETUP never arrives + for (unsigned i = 0; i < (CFG_TUD_TASK_QUEUE_SZ / CFG_TUD_TASK_EVENTS_PER_RUN) + 1; i++) { + tud_task(); + } + + // the next SETUP must still be answered + desc_device = (uint8_t const*) &data_desc_device; + dcd_event_setup_received(rhport, (uint8_t*) &req_get_desc_device, false); + + dcd_edpt_xfer_ExpectWithArrayAndReturn(rhport, 0x80, (uint8_t*) &data_desc_device, sizeof(tusb_desc_device_t), sizeof(tusb_desc_device_t), false, true); + dcd_event_xfer_complete(rhport, EDPT_CTRL_IN, sizeof(tusb_desc_device_t), 0, false); + + dcd_edpt_xfer_ExpectAndReturn(rhport, EDPT_CTRL_OUT, NULL, 0, false, true); + dcd_event_xfer_complete(rhport, EDPT_CTRL_OUT, 0, 0, false); + dcd_edpt0_status_complete_ExpectWithArray(rhport, &req_get_desc_device, 1); + + tud_task(); +} + //--------------------------------------------------------------------+ // Control OUT data stage host overrun //--------------------------------------------------------------------+ -- cgit v1.3.1 From 91fbbd192ca9539221d3dc096f00ce77836a5d3d Mon Sep 17 00:00:00 2001 From: hathach Date: Wed, 12 Aug 2026 23:05:59 +0700 Subject: usbd: clear endpoint busy/claimed when a completion event is dropped An XFER_COMPLETE dropped by a full event queue leaves its endpoint's BUSY|CLAIMED state set forever - the consumer that normally clears it never sees the event, so usbd_edpt_claim()/usbd_edpt_xfer() fail from then on and the class never re-arms the endpoint. Clear both flags when the enqueue fails: the completion is lost either way, but the endpoint stays usable. Unit test: arm a bulk endpoint, drop its completion against a full queue, verify the endpoint can be claimed and re-armed. --- src/device/usbd.c | 16 +++++++--- test/unit-test/test/device/usbd/test_usbd.c | 47 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) (limited to 'test/unit-test') diff --git a/src/device/usbd.c b/src/device/usbd.c index 79802e70f..f5c3046d6 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -1475,10 +1475,18 @@ TU_ATTR_FAST_FUNC void dcd_event_handler(dcd_event_t const* event, bool in_isr) break; } - if (send && !queue_event(event, in_isr) && event->event_id == DCD_EVENT_SETUP_RECEIVED) { - // dropped by a full queue: undo the increment, else every later SETUP is skipped as - // "other SETUP in queue" and EP0 is deaf until re-init - _usbd_queued_setup--; + if (send && !queue_event(event, in_isr)) { + // event dropped by a full queue: undo state that would otherwise wedge permanently + if (event->event_id == DCD_EVENT_SETUP_RECEIVED) { + // undo the increment, else every later SETUP is skipped as "other SETUP in queue" + // and EP0 is deaf until re-init + _usbd_queued_setup--; + } else if (event->event_id == DCD_EVENT_XFER_COMPLETE) { + // clear busy + claimed, else the endpoint can never be claimed or re-armed again + uint8_t const epnum = tu_edpt_number(event->xfer_complete.ep_addr); + uint8_t const ep_dir = tu_edpt_dir(event->xfer_complete.ep_addr); + _usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); + } } } diff --git a/test/unit-test/test/device/usbd/test_usbd.c b/test/unit-test/test/device/usbd/test_usbd.c index 935a20221..849097326 100644 --- a/test/unit-test/test/device/usbd/test_usbd.c +++ b/test/unit-test/test/device/usbd/test_usbd.c @@ -29,6 +29,7 @@ #include "tusb_fifo.h" #include "tusb.h" #include "usbd.h" +#include "device/usbd_pvt.h" TEST_SOURCE_FILE("usbd.c") // Mock File @@ -308,6 +309,52 @@ void test_usbd_setup_dropped_by_full_queue_recovers(void) tud_task(); } +//--------------------------------------------------------------------+ +// Transfer completion dropped by full event queue +//--------------------------------------------------------------------+ + +// When the event queue is full, queue_event() drops the XFER_COMPLETE event. The endpoint's +// busy/claimed state must not survive the dropped completion: a leaked BUSY makes every later +// usbd_edpt_claim()/usbd_edpt_xfer() on that endpoint fail, so the class never re-arms it. +void test_usbd_xfer_complete_dropped_by_full_queue_recovers(void) +{ + // fillers drain through usbd_reset -> class reset + mscd_reset_Ignore(); + + // open + claim + arm a bulk OUT endpoint the way a class driver would + tusb_desc_endpoint_t desc_ep = { + .bLength = sizeof(tusb_desc_endpoint_t), + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = 0x01, + .bmAttributes = { .xfer = TUSB_XFER_BULK }, + .wMaxPacketSize = 64, + .bInterval = 0 + }; + static uint8_t xfer_buf[64]; + + dcd_edpt_open_ExpectAndReturn(rhport, &desc_ep, true); + TEST_ASSERT_TRUE(usbd_edpt_open(rhport, &desc_ep)); + TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, 0x01)); + dcd_edpt_xfer_ExpectAndReturn(rhport, 0x01, xfer_buf, 64, false, true); + TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport, 0x01, xfer_buf, 64, false)); + + // fill the queue to the brim, then complete the transfer: queue_event() drops it + for (unsigned i = 0; i < CFG_TUD_TASK_QUEUE_SZ; i++) { + dcd_event_bus_signal(rhport, DCD_EVENT_UNPLUGGED, false); + } + dcd_event_xfer_complete(rhport, 0x01, 64, XFER_RESULT_SUCCESS, false); + + // the endpoint must be re-armable: the dropped completion must not leak busy/claimed + TEST_ASSERT_TRUE(usbd_edpt_claim(rhport, 0x01)); + dcd_edpt_xfer_ExpectAndReturn(rhport, 0x01, xfer_buf, 64, false, true); + TEST_ASSERT_TRUE(usbd_edpt_xfer(rhport, 0x01, xfer_buf, 64, false)); + + // drain the fillers so later tests start from an empty queue + for (unsigned i = 0; i < (CFG_TUD_TASK_QUEUE_SZ / CFG_TUD_TASK_EVENTS_PER_RUN) + 1; i++) { + tud_task(); + } +} + //--------------------------------------------------------------------+ // Control OUT data stage host overrun //--------------------------------------------------------------------+ -- cgit v1.3.1 From 61e3544b249dd1e40d9cbb106361541091b3ee78 Mon Sep 17 00:00:00 2001 From: HiFiPHile Date: Tue, 25 Aug 2026 09:27:36 +0200 Subject: test(audio): add UAC1 host driver coverage Add a Ceedling harness for Audio Host descriptor parsing, stream configuration, controls, and packet scheduling. The suite provides regression coverage for the review fixes. Signed-off-by: HiFiPHile --- test/unit-test/CMakeLists.txt | 14 + test/unit-test/project.yml | 15 +- test/unit-test/test/host/audio/test_audio_host.c | 587 +++++++++++++++++++++++ test/unit-test/test/support/audio_host_test.h | 35 ++ 4 files changed, 647 insertions(+), 4 deletions(-) create mode 100644 test/unit-test/test/host/audio/test_audio_host.c create mode 100644 test/unit-test/test/support/audio_host_test.h (limited to 'test/unit-test') diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index a33af4563..cea9fe33d 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -128,4 +128,18 @@ add_ceedling_test( "${CEEDLING_BUILD_DIR}/test/mocks/test_msc_device/mock_dcd.c" ) +add_ceedling_test( + test_audio_host + ${CEEDLING_WORKDIR}/test/host/audio/test_audio_host.c + "${CEEDLING_WORKDIR}/../../src/class/audio/audio_host.c;${CEEDLING_WORKDIR}/../../src/common/tusb_fifo.c" + "" + ) +target_compile_definitions(test_audio_host PRIVATE + CFG_TUH_ENABLED=1 + CFG_TUH_AUDIO=1 + CFG_TUSB_DEBUG=0 + CFG_TUH_AUDIO_EPIN_BUFSIZE=1024 + CFG_TUH_AUDIO_EPOUT_BUFSIZE=1024 + ) + enable_testing() diff --git a/test/unit-test/project.yml b/test/unit-test/project.yml index be3fc3de0..9dba70d20 100644 --- a/test/unit-test/project.yml +++ b/test/unit-test/project.yml @@ -127,10 +127,17 @@ # - Specifying symbols used during test preprocessing :defines: :test: - - _UNITY_TEST_ - - CFG_TUD_EDPT_DEDICATED_HWFIFO=1 - - CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE=6 - - CFG_TUSB_FIFO_HWFIFO_ADDR_STRIDE=0 + :*: + - _UNITY_TEST_ + - CFG_TUD_EDPT_DEDICATED_HWFIFO=1 + - CFG_TUSB_FIFO_HWFIFO_DATA_STRIDE=6 + - CFG_TUSB_FIFO_HWFIFO_ADDR_STRIDE=0 + :test_audio_host: + - CFG_TUH_ENABLED=1 + - CFG_TUH_AUDIO=1 + - CFG_TUSB_DEBUG=0 + - CFG_TUH_AUDIO_EPIN_BUFSIZE=1024 + - CFG_TUH_AUDIO_EPOUT_BUFSIZE=1024 :release: [] # Enable to inject name of a test as a unique compilation symbol into its respective executable build. diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c new file mode 100644 index 000000000..728bcdf29 --- /dev/null +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -0,0 +1,587 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 TinyUSB contributors + * SPDX-License-Identifier: MIT + */ + +#include "unity.h" +#include "audio_host_test.h" + +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, + + PLAYBACK_INPUT_TERM = 1, + PLAYBACK_FU = 2, + PLAYBACK_OUTPUT_TERM = 3, + + CAPTURE_INPUT_TERM = 11, + CAPTURE_FU = 12, + CAPTURE_OUTPUT_TERM = 13, +}; + +static bool interface_set_result; +static tuh_xfer_t interface_xfer; +static uint8_t interface_alt; + +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_xfer_count; + +static bool edpt_open_result; +static tusb_desc_endpoint_t opened_ep[4]; +static uint8_t edpt_open_count; + +static bool edpt_close_result; +static uint8_t closed_ep[4]; +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_count; + +tusb_speed_t tuh_speed_get(uint8_t daddr) { + (void)daddr; + return TUSB_SPEED_FULL; +} + +bool tuh_control_xfer(tuh_xfer_t *xfer) { + control_xfer_count++; + control_request = *xfer->setup; + control_xfer = *xfer; + control_xfer.setup = &control_request; + if (xfer->buffer != NULL) { + memcpy(control_buffer, xfer->buffer, TU_MIN(sizeof(control_buffer), control_request.wLength)); + } + xfer->result = XFER_RESULT_SUCCESS; + return control_xfer_result; +} + +bool tuh_edpt_open(uint8_t daddr, const tusb_desc_endpoint_t *desc_ep) { + (void)daddr; + if (edpt_open_count < TU_ARRAY_SIZE(opened_ep)) { + opened_ep[edpt_open_count++] = *desc_ep; + } + return edpt_open_result; +} + +bool tuh_edpt_close(uint8_t daddr, uint8_t ep_addr) { + (void)daddr; + if (edpt_close_count < TU_ARRAY_SIZE(closed_ep)) { + closed_ep[edpt_close_count++] = ep_addr; + } + if (edpt_close_result) { + edpt_busy = false; + } + return edpt_close_result; +} + +bool tuh_interface_set(uint8_t daddr, uint8_t itf_num, uint8_t itf_alt, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + (void)daddr; + (void)itf_num; + interface_alt = itf_alt; + interface_xfer.daddr = daddr; + interface_xfer.ep_addr = 0; + interface_xfer.complete_cb = complete_cb; + interface_xfer.user_data = user_data; + return interface_set_result; +} + +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)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; + } + 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) { + return false; + } + edpt_busy = true; + return true; +} + +bool usbh_edpt_release(uint8_t dev_addr, uint8_t ep_addr) { + (void)dev_addr; + (void)ep_addr; + edpt_busy = false; + return true; +} + +bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) { + (void)dev_addr; + (void)ep_addr; + return edpt_busy; +} + +void usbh_driver_set_config_complete(uint8_t dev_addr, uint8_t itf_num) { + (void)dev_addr; + (void)itf_num; +} + +bool tu_edpt_stream_init(tu_edpt_stream_t *s, bool is_host, bool is_tx, bool overwritable, void *ff_buf, + uint16_t ff_bufsize, uint8_t *ep_buf) { + (void)is_tx; + s->is_host = is_host; + s->ep_buf = ep_buf; + return tu_fifo_config(&s->ff, ff_buf, ff_bufsize, overwritable); +} + +uint32_t tu_edpt_stream_write(tu_edpt_stream_t *s, const void *buffer, uint32_t bufsize) { + (void)s; + (void)buffer; + (void)bufsize; + return 0; +} + +uint32_t tu_edpt_stream_write_available(tu_edpt_stream_t *s) { + (void)s; + return 0; +} + +uint32_t tu_edpt_stream_read(tu_edpt_stream_t *s, void *buffer, uint32_t bufsize) { + (void)s; + (void)buffer; + (void)bufsize; + return 0; +} + +uint32_t tu_edpt_stream_read_xfer(tu_edpt_stream_t *s) { + (void)s; + return 0; +} + +#define TEST_UAC1_AC_HEADER \ + 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_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 + +#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_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, \ + AUDIO_INT_PROTOCOL_CODE_V1, 0 + +#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, \ + U16_TO_U8S_LE(AUDIO10_DATA_FORMAT_TYPE_I_PCM) + +#define TEST_UAC1_FORMAT(_channels, _bytes, _bits, ...) \ + (8 + 3 * TU_ARGS_NUM(__VA_ARGS__)), TUSB_DESC_CS_INTERFACE, AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE, \ + 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_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, \ + 0, 0 + +#define TEST_UAC1_CS_DATA_EP TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_SAMPLING_FRQ) + +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), + 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, 2), + 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_DATA_EP(0x81, TUSB_ISO_EP_ATT_EXPLICIT_FB, 3, 1), +}; + +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), + TEST_UAC1_OUTPUT_TERM(PLAYBACK_OUTPUT_TERM, AUDIO_TERM_TYPE_OUT_HEADPHONES, PLAYBACK_INPUT_TERM), + TEST_UAC1_AS_ALT0, + TEST_UAC1_AS_INTERFACE(1, 2), + 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_DATA_EP(0x81, TUSB_ISO_EP_ATT_IMPLICIT_FB, 192, 1), +}; + +static const uint8_t playback_fu_before_terminal[] = { + TEST_UAC1_AC_HEADER, + TEST_UAC1_FEATURE_UNIT(PLAYBACK_FU, PLAYBACK_INPUT_TERM), + 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), + TEST_UAC1_FEATURE_UNIT(CAPTURE_FU, CAPTURE_INPUT_TERM), + TEST_UAC1_OUTPUT_TERM(CAPTURE_OUTPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, CAPTURE_FU), + 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_CS_DATA_EP, + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 96, 1), +}; + +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), + 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_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 384, 2), + TEST_UAC1_CS_DATA_EP, +}; + +static const uint8_t playback_with_frequency_range[] = { + 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), + 14, + TUSB_DESC_CS_INTERFACE, + AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE, + AUDIO10_FORMAT_TYPE_I, + 2, + 2, + 16, + 0, + U24_TO_U8S_LE(44100), + U24_TO_U8S_LE(48000), + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 192, 1), + TEST_UAC1_CS_DATA_EP, +}; + +static const uint8_t capture_with_large_channel_count[] = { + TEST_UAC1_AC_HEADER, + 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(CAPTURE_OUTPUT_TERM), + TEST_UAC1_FORMAT(255, 4, 32, 100), + TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 500, 1), + TEST_UAC1_CS_DATA_EP, +}; + +static const uint8_t playback_with_cs_ep_before_data_ep[] = { + 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), + TEST_UAC1_CS_DATA_EP, + TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 192, 1), +}; + +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), + 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(2, 1), + TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM), + TEST_UAC1_FORMAT(2, 2, 16, 96000), + TEST_UAC1_DATA_EP(0x02, TUSB_ISO_EP_ATT_ADAPTIVE, 384, 1), + TEST_UAC1_CS_DATA_EP, +}; + +static const uint8_t playback_44100_max_packets_only[] = { + 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, 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), +}; + +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; +} + +static void complete_interface_set(tusb_xfer_result_t result) { + tuh_xfer_t xfer = interface_xfer; + interface_xfer.complete_cb = NULL; + xfer.result = result; + TEST_ASSERT_NOT_NULL(xfer.complete_cb); + xfer.complete_cb(&xfer); +} + +static void complete_control_xfer(tusb_xfer_result_t result) { + tuh_xfer_t xfer = control_xfer; + control_xfer.complete_cb = NULL; + xfer.result = result; + TEST_ASSERT_NOT_NULL(xfer.complete_cb); + xfer.complete_cb(&xfer); +} + +void setUp(void) { + interface_set_result = true; + memset(&interface_xfer, 0, sizeof(interface_xfer)); + interface_alt = 0; + + control_xfer_result = true; + 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; + + edpt_open_result = true; + memset(opened_ep, 0, sizeof(opened_ep)); + edpt_open_count = 0; + + edpt_close_result = true; + memset(closed_ep, 0, sizeof(closed_ep)); + edpt_close_count = 0; + + edpt_busy = false; + edpt_xfer_result = true; + memset(edpt_xfer_bytes, 0, sizeof(edpt_xfer_bytes)); + edpt_xfer_count = 0; + + configure_cb_count = 0; + configure_cb_result = XFER_RESULT_INVALID; + fu_cb_count = 0; + fu_cb_user_data = 0; + + TEST_ASSERT_TRUE(audioh_init()); +} + +void tearDown(void) { + TEST_ASSERT_TRUE(audioh_deinit()); +} + +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)); + TEST_ASSERT_TRUE(tuh_audio_mounted(0)); +} + +void test_audio_host_ignores_explicit_feedback_endpoint(void) { + open_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)); +} + +void test_audio_host_treats_implicit_feedback_as_audio_in_endpoint(void) { + open_descriptors(playback_with_implicit_feedback, sizeof(playback_with_implicit_feedback)); + + 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)); +} + +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)); +} + +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)); + + 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)); +} + +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)); + + 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); +} + +void test_audio_host_rejects_sampling_frequency_range(void) { + open_descriptors(playback_with_frequency_range, sizeof(playback_with_frequency_range)); + + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_stream_count(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)); + + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_stream_count(0)); +} + +void test_audio_host_associates_cs_endpoint_declared_before_data_endpoint(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_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_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_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)); + 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)); +} + +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}; + + 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); + + 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); +} + +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)); + + 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_EQUAL_UINT8(0, control_xfer_count); + + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, interface_alt); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(0, 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])); + } + 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]); +} diff --git a/test/unit-test/test/support/audio_host_test.h b/test/unit-test/test/support/audio_host_test.h new file mode 100644 index 000000000..29e46cbd0 --- /dev/null +++ b/test/unit-test/test/support/audio_host_test.h @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 TinyUSB contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef TUSB_AUDIO_HOST_TEST_H_ +#define TUSB_AUDIO_HOST_TEST_H_ + +#include "common/tusb_common.h" + +typedef struct tuh_xfer_s tuh_xfer_t; +typedef void (*tuh_xfer_cb_t)(tuh_xfer_t *xfer); + +struct tuh_xfer_s { + uint8_t daddr; + uint8_t ep_addr; + uint8_t TU_RESERVED; + xfer_result_t result; + uint32_t actual_len; + union { + const tusb_control_request_t *setup; + uint32_t buflen; + }; + uint8_t *buffer; + tuh_xfer_cb_t complete_cb; + uintptr_t user_data; +}; + +#define TU_API_SYNC(...) return false +#include "class/audio/audio_host.h" +#undef TU_API_SYNC + +#include "host/usbh_pvt.h" + +#endif -- 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 'test/unit-test') 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 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 'test/unit-test') 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 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 'test/unit-test') 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 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 'test/unit-test') 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 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 'test/unit-test') 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