summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorHiFiPHile <[email protected]>2026-08-25 09:27:36 +0200
committerHiFiPHile <[email protected]>2026-08-25 09:27:36 +0200
commit61e3544b249dd1e40d9cbb106361541091b3ee78 (patch)
treec3fa15fc31c6f2ea5802f36160af0df3c38cc16e /test
parentef631435ce8c3dd0fde720f097ffea317ad6f06a (diff)
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 <[email protected]>
Diffstat (limited to 'test')
-rw-r--r--test/unit-test/CMakeLists.txt14
-rw-r--r--test/unit-test/project.yml15
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c587
-rw-r--r--test/unit-test/test/support/audio_host_test.h35
4 files changed, 647 insertions, 4 deletions
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