summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/host/CMakeLists.txt1
-rw-r--r--examples/host/audio_host/CMakeLists.txt30
-rw-r--r--examples/host/audio_host/CMakePresets.json6
-rw-r--r--examples/host/audio_host/Makefile14
-rw-r--r--examples/host/audio_host/README.md122
-rw-r--r--examples/host/audio_host/only.txt27
-rw-r--r--examples/host/audio_host/src/app.h27
-rw-r--r--examples/host/audio_host/src/audio_app.c548
-rw-r--r--examples/host/audio_host/src/main.c48
-rw-r--r--examples/host/audio_host/src/tusb_config.h106
-rw-r--r--hw/bsp/rp2040/family.cmake1
-rw-r--r--hw/bsp/stm32f7/boards/stm32f723disco/board.h6
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/class/audio/audio_host.c2724
-rw-r--r--src/class/audio/audio_host.h361
-rw-r--r--src/host/usbh.c12
-rw-r--r--src/tinyusb.mk1
-rw-r--r--src/tusb.h4
-rw-r--r--src/tusb_option.h4
-rw-r--r--test/hil/test/test_ci_metrics.py2
-rw-r--r--test/unit-test/CMakeLists.txt15
-rw-r--r--test/unit-test/project.yml16
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c2035
-rw-r--r--test/unit-test/test/support/audio_host_test.h35
24 files changed, 6138 insertions, 8 deletions
diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt
index 7c74e3c73..0e877cb78 100644
--- a/examples/host/CMakeLists.txt
+++ b/examples/host/CMakeLists.txt
@@ -7,6 +7,7 @@ family_initialize_project(tinyusb_host_examples ${CMAKE_CURRENT_LIST_DIR})
# family_add_subdirectory will filter what to actually add based on selected FAMILY
set(EXAMPLE_LIST
+ audio_host
bare_api
cdc_msc_hid
cdc_msc_hid_freertos
diff --git a/examples/host/audio_host/CMakeLists.txt b/examples/host/audio_host/CMakeLists.txt
new file mode 100644
index 000000000..0891f5829
--- /dev/null
+++ b/examples/host/audio_host/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.20)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake)
+
+project(audio_host C CXX ASM)
+
+# Checks this example is valid for the family and initializes the project
+family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR})
+
+# Espressif has its own cmake build system
+if(FAMILY STREQUAL "espressif")
+ return()
+endif()
+
+add_executable(${PROJECT_NAME})
+
+# Example source
+target_sources(${PROJECT_NAME} PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/audio_app.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c
+ )
+
+# Example include
+target_include_directories(${PROJECT_NAME} PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
+ )
+
+# Configure compilation flags and libraries for the example without RTOS.
+# See the corresponding function in hw/bsp/FAMILY/family.cmake for details.
+family_configure_host_example(${PROJECT_NAME} noos)
diff --git a/examples/host/audio_host/CMakePresets.json b/examples/host/audio_host/CMakePresets.json
new file mode 100644
index 000000000..5cd8971e9
--- /dev/null
+++ b/examples/host/audio_host/CMakePresets.json
@@ -0,0 +1,6 @@
+{
+ "version": 6,
+ "include": [
+ "../../../hw/bsp/BoardPresets.json"
+ ]
+}
diff --git a/examples/host/audio_host/Makefile b/examples/host/audio_host/Makefile
new file mode 100644
index 000000000..5c2e23184
--- /dev/null
+++ b/examples/host/audio_host/Makefile
@@ -0,0 +1,14 @@
+include ../../../hw/bsp/family_support.mk
+
+INC += \
+ src \
+
+
+# Example source
+EXAMPLE_SOURCE += \
+ src/audio_app.c \
+ src/main.c
+
+SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE))
+
+include ../../../hw/bsp/family_rules.mk
diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md
new file mode 100644
index 000000000..7f72aabaf
--- /dev/null
+++ b/examples/host/audio_host/README.md
@@ -0,0 +1,122 @@
+# 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 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 and 2.0 devices
+- Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only)
+- 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
+- Cycles the streams through three phases (5 s each): mic-only (capture, data dropped), spk-only (sine test tone), and echo (capture looped back to playback)
+
+## Supported Devices
+
+This example supports 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)
+- USB audio interfaces
+
+The echo needs a matching S16_LE playback stream at the capture sample rate; devices without one run capture-only. The sample rate and channel preferences are configured by the `SAMPLE_RATES` / `AUDIO_MAX_CHANNELS` macros in `src/audio_app.c` (44.1 kHz stereo by default). Non-PCM formats are rejected by the driver.
+
+## Limitations and trade-offs
+
+- 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.
+- 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. 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
+
+### Using CMake (recommended)
+
+```bash
+cd examples/host/audio_host
+mkdir -p build && cd build
+cmake -DBOARD=<your_board> -G Ninja ..
+cmake --build .
+```
+
+Replace `<your_board>` with your target board name (e.g., `raspberry_pi_pico`, `stm32f407disco`, etc.)
+
+### Using Make
+
+```bash
+cd examples/host/audio_host
+make BOARD=<your_board> all
+```
+
+## Flashing
+
+```bash
+# 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=<your_board> flash
+```
+
+## Usage
+
+1. Build and flash the example to your board
+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 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 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
+
+## Serial Output Example
+
+```
+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, configurations: 2
+ master mute supported
+ 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
+ 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 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 volume set: -1536 (1/256 dB)
+```
+
+## Configuration
+
+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
+
+## 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_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/only.txt b/examples/host/audio_host/only.txt
new file mode 100644
index 000000000..c1a2d3cec
--- /dev/null
+++ b/examples/host/audio_host/only.txt
@@ -0,0 +1,27 @@
+family:at32f402_405
+family:at32f415
+family:at32f423
+family:at32f425
+family:at32f435_437
+family:at32f45x
+family:efm32
+family:kinetis_kl
+family:rx
+board:nrf54h20dk
+board:nrf54lm20dk
+board:stm32l476disco
+board:stm32l496nucleo
+board:stm32l4p5nucleo
+board:stm32l4r5nucleo
+mcu:MAX3421
+mcu:MSP432E4
+mcu:RAXXX
+mcu:RP2040
+mcu:STM32F2
+mcu:STM32F4
+mcu:STM32F7
+mcu:STM32H7
+mcu:STM32H7RS
+mcu:STM32N6
+mcu:STM32U5
+mcu:STM32WBA
diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h
new file mode 100644
index 000000000..45ba52dd2
--- /dev/null
+++ b/examples/host/audio_host/src/app.h
@@ -0,0 +1,27 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 TinyUSB contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ */
+
+#ifndef TUSB_TINYUSB_EXAMPLES_APP_H
+#define TUSB_TINYUSB_EXAMPLES_APP_H
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+void led_blinking_task(void);
+void audio_app_task(void);
+void defer_queue_task(void);
+#endif
diff --git a/examples/host/audio_host/src/audio_app.c b/examples/host/audio_host/src/audio_app.c
new file mode 100644
index 000000000..9f84290b9
--- /dev/null
+++ b/examples/host/audio_host/src/audio_app.c
@@ -0,0 +1,548 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 TinyUSB contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include "bsp/board_api.h"
+#include "tusb.h"
+#include "app.h"
+
+//--------------------------------------------------------------------+
+// MACRO TYPEDEF CONSTANT ENUM DECLARATION
+//--------------------------------------------------------------------+
+
+// Default configuration of this example, adjust to the target device:
+// - 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_CHANNELS 2
+#define SAMPLE_RATES {44100, 48000}
+#define FEATURE_UNIT_VOLUME_DB (-20 * 256)
+#define AUDIO_BUFFER_SAMPLE_COUNT (CFG_TUH_AUDIO_STREAM_BUFSIZE / 2 / sizeof(int16_t))
+static uint8_t audio_idx = TUSB_INDEX_INVALID_8;
+static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8;
+static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8;
+
+// True after a stream start request is accepted and until it is stopped or fails.
+static bool mic_enabled;
+static bool spk_enabled;
+
+static int16_t audio_samples[AUDIO_BUFFER_SAMPLE_COUNT];
+static tuh_audio_stream_config_t mic_config;
+static tuh_audio_stream_config_t spk_config;
+
+// Diagnostic counters printed and cleared once per second.
+static uint32_t spk_cb_count;
+static uint32_t mic_cb_count;
+static uint32_t fail_event_count;
+static uint32_t stats_start_ms;
+
+//--------------------------------------------------------------------+
+// Async Deferred Call Queue
+//--------------------------------------------------------------------+
+// Schedules one-shot callbacks to be invoked after a given delay in ms.
+// Processed by defer_queue_task() in the main loop, no dynamic allocation.
+
+#define APP_DEFER_QUEUE_SZ 4
+
+typedef void (*app_defer_func_t)(uintptr_t param);
+
+typedef struct {
+ app_defer_func_t func;
+ uintptr_t arg;
+ uint32_t at_ms;
+} app_defer_t;
+
+static app_defer_t _defer_q[APP_DEFER_QUEUE_SZ];
+
+// Clear all pending deferred callbacks.
+static void app_defer_queue_clear(void) {
+ memset(_defer_q, 0, sizeof(_defer_q));
+}
+
+// Schedule func to be called after 'ms' milliseconds. Four slots cover the
+// phase timer, one restart per stream, and the single supported Audio device.
+static void app_defer_ms_async(uint32_t ms, app_defer_func_t func, uintptr_t arg) {
+ for (uint8_t i = 0; i < APP_DEFER_QUEUE_SZ; i++) {
+ if (_defer_q[i].func == NULL) {
+ _defer_q[i].func = func;
+ _defer_q[i].arg = arg;
+ // add one to ensure we wait at least 'ms' milliseconds
+ _defer_q[i].at_ms = tusb_time_millis_api() + ms + 1;
+ return;
+ }
+ }
+ TU_ASSERT(false, ); // Queue sizing above is an application invariant.
+}
+
+// Invoke all callbacks whose delay has expired, must be called periodically from main loop
+void defer_queue_task(void) {
+ const uint32_t now_ms = tusb_time_millis_api();
+ for (uint8_t i = 0; i < APP_DEFER_QUEUE_SZ; i++) {
+ if (_defer_q[i].func != NULL && (int32_t)(_defer_q[i].at_ms - now_ms) <= 0) {
+ const app_defer_func_t func = _defer_q[i].func;
+ const uintptr_t arg = _defer_q[i].arg;
+ _defer_q[i].func = NULL; // free slot before invoking, callback may re-schedule
+ func(arg);
+ }
+ }
+}
+
+// Expand backward so mono capture can be converted to stereo in place.
+static void mono_to_stereo(int16_t *samples, uint32_t frames) {
+ for (uint32_t i = frames; i-- > 0;) {
+ const int16_t sample = samples[i];
+ samples[i * 2] = sample;
+ samples[i * 2 + 1] = sample;
+ }
+}
+
+// Contract forward so stereo capture can be converted to mono in place.
+static void stereo_to_mono(int16_t *samples, uint32_t frames) {
+ for (uint32_t i = 0; i < frames; i++) {
+ samples[i] = (int16_t)(((int32_t)samples[i * 2] + samples[i * 2 + 1]) / 2);
+ }
+}
+
+#define SINE_TONE_HZ 1000u
+#define SINE_LUT_BITS 6u
+#define SINE_LUT_SIZE (1u << SINE_LUT_BITS)
+
+// One sine period with a peak amplitude of 4096 (-18 dBFS).
+static const int16_t sine_lut[SINE_LUT_SIZE] = {
+ 0, 401, 799, 1189, 1567, 1931, 2276, 2598, 2896, 3166, 3406, 3612, 3784, 3920, 4017, 4076,
+ 4096, 4076, 4017, 3920, 3784, 3612, 3406, 3166, 2896, 2598, 2276, 1931, 1567, 1189, 799, 401,
+ 0, -401, -799, -1189, -1567, -1931, -2276, -2598, -2896, -3166, -3406, -3612, -3784, -3920, -4017, -4076,
+ -4096, -4076, -4017, -3920, -3784, -3612, -3406, -3166, -2896, -2598, -2276, -1931, -1567, -1189, -799, -401,
+};
+
+static uint32_t sine_phase;
+static uint32_t sine_phase_step;
+
+// Generate a continuous tone at the selected sample rate and pack each frame
+// according to the actual playback channel count.
+static void spk_fill_sine(uint32_t frames) {
+ for (uint32_t i = 0; i < frames; i++) {
+ const int16_t sample = sine_lut[sine_phase >> (32u - SINE_LUT_BITS)];
+ sine_phase += sine_phase_step;
+ for (uint8_t ch = 0; ch < spk_config.channels; ch++) {
+ audio_samples[i * spk_config.channels + ch] = sample;
+ }
+ }
+}
+
+// One application buffer is half of the driver's stream FIFO. Servicing the
+// FIFO at this watermark leaves the other half available to absorb scheduling
+// jitter between main-loop iterations.
+static uint32_t audio_half_fifo_frames(const tuh_audio_stream_config_t *config) {
+ return TU_ARRAY_SIZE(audio_samples) / config->channels;
+}
+
+//--------------------------------------------------------------------+
+// Periodic Stream Switching
+//--------------------------------------------------------------------+
+// Cycles through three phases with tuh_audio_start()/stop(). The driver
+// activates/deactivates the stream's interface (SET_INTERFACE alt setting)
+// on each switch.
+// 1. mic only (5 s): capture runs, captured data is dropped
+// 2. spk only (5 s): playback plays the sine test tone
+// 3. echo (5 s): both streams run, captured audio is echoed back
+#define APP_PHASE_MIC_ONLY_MS 5000
+#define APP_PHASE_SPK_ONLY_MS 5000
+#define APP_PHASE_ECHO_MS 5000
+
+enum {
+ APP_PHASE_MIC_ONLY = 0,
+ APP_PHASE_SPK_ONLY,
+ APP_PHASE_ECHO,
+ APP_PHASE_COUNT
+};
+
+static uint8_t app_audio_phase = APP_PHASE_MIC_ONLY;
+static const uint32_t app_phase_ms[APP_PHASE_COUNT] = {APP_PHASE_MIC_ONLY_MS, APP_PHASE_SPK_ONLY_MS, APP_PHASE_ECHO_MS};
+
+// Start or stop the capture/playback streams according to the current phase.
+// The app task discards capture in mic-only mode, generates sine in speaker-only
+// mode, and echoes capture when both streams run.
+static void app_audio_phase_apply(void) {
+ switch (app_audio_phase) {
+ case APP_PHASE_MIC_ONLY:
+ if (!mic_enabled) {
+ mic_enabled = tuh_audio_start(audio_idx, cap_stream_idx);
+ }
+ if (spk_enabled) {
+ spk_enabled = !tuh_audio_stop(audio_idx, spk_stream_idx);
+ }
+ printf(" Phase %u: mic on, spk off (data dropped)\r\n", app_audio_phase);
+ break;
+ case APP_PHASE_SPK_ONLY:
+ if (mic_enabled) {
+ mic_enabled = !tuh_audio_stop(audio_idx, cap_stream_idx);
+ }
+ if (!spk_enabled) {
+ spk_enabled = tuh_audio_start(audio_idx, spk_stream_idx);
+ }
+ printf(" Phase %u: mic off, spk on (sine)\r\n", app_audio_phase);
+ break;
+ case APP_PHASE_ECHO:
+ if (!mic_enabled) {
+ mic_enabled = tuh_audio_start(audio_idx, cap_stream_idx);
+ }
+ if (!spk_enabled) {
+ spk_enabled = tuh_audio_start(audio_idx, spk_stream_idx);
+ }
+ printf(" Phase %u: mic + spk on (echo)\r\n", app_audio_phase);
+ break;
+ default:
+ break;
+ }
+}
+
+static void audio_stats_reset(void) {
+ stats_start_ms = tusb_time_millis_api();
+ mic_cb_count = 0;
+ spk_cb_count = 0;
+ fail_event_count = 0;
+}
+
+// Enter a phase, then schedule the next switch after this phase's duration
+static void app_audio_phase_enter(uintptr_t phase) {
+ app_audio_phase = (uint8_t)phase;
+ // Cancel stale deferred callbacks (e.g. a stream restart scheduled on a
+ // transfer error) so they cannot re-start a stream this phase stops.
+ app_defer_queue_clear();
+ app_audio_phase_apply();
+ audio_stats_reset();
+ const uint8_t next_phase = (uint8_t)((app_audio_phase + 1) % APP_PHASE_COUNT);
+ app_defer_ms_async(app_phase_ms[app_audio_phase], app_audio_phase_enter, next_phase);
+}
+
+//--------------------------------------------------------------------+
+// Blinking Task
+//--------------------------------------------------------------------+
+void led_blinking_task(void) {
+ const uint32_t interval_ms = 1000;
+
+ static bool led_state = false;
+
+ // Blink every interval ms
+ if (tusb_time_millis_api() - stats_start_ms < interval_ms) {
+ return; // not enough time
+ }
+ stats_start_ms += interval_ms;
+
+ board_led_write(led_state);
+ led_state = 1 - led_state; // toggle
+ 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;
+ fail_event_count = 0;
+}
+
+//--------------------------------------------------------------------+
+// Application Task
+//--------------------------------------------------------------------+
+// Echo one chunk after capture is at least half full and playback is at least
+// half drained. This runs from the main loop, independently of USB callbacks.
+static void audio_echo_task(void) {
+ const uint32_t mic_frames = audio_half_fifo_frames(&mic_config);
+ const uint32_t spk_frames = audio_half_fifo_frames(&spk_config);
+ if (tuh_audio_read_available(audio_idx, cap_stream_idx) < mic_frames ||
+ tuh_audio_write_available(audio_idx, spk_stream_idx) < spk_frames) {
+ return;
+ }
+
+ const uint32_t frames = TU_MIN(mic_frames, spk_frames);
+ (void)tuh_audio_read(audio_idx, cap_stream_idx, audio_samples, frames);
+ if (mic_config.channels == 1 && spk_config.channels == 2) {
+ mono_to_stereo(audio_samples, frames);
+ } else if (mic_config.channels == 2 && spk_config.channels == 1) {
+ stereo_to_mono(audio_samples, frames);
+ }
+
+ (void)tuh_audio_write(audio_idx, spk_stream_idx, audio_samples, frames);
+}
+
+void audio_app_task(void) {
+ if (mic_enabled && spk_enabled) {
+ audio_echo_task();
+ } else if (mic_enabled) {
+ const uint32_t frames = audio_half_fifo_frames(&mic_config);
+ if (tuh_audio_read_available(audio_idx, cap_stream_idx) >= frames) {
+ (void)tuh_audio_read(audio_idx, cap_stream_idx, audio_samples, frames);
+ }
+ } else if (spk_enabled) {
+ const uint32_t frames = audio_half_fifo_frames(&spk_config);
+ if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) {
+ spk_fill_sine(frames);
+ (void)tuh_audio_write(audio_idx, spk_stream_idx, audio_samples, frames);
+ }
+ }
+}
+
+// Transfer callbacks are intentionally not used to service the FIFOs. The
+// main-loop audio_app_task() reads and writes independently at half-FIFO
+// watermarks; these callbacks only collect diagnostic counts.
+void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) {
+ (void)idx;
+ (void)stream_idx;
+ (void)xferred_bytes;
+ mic_cb_count++;
+}
+
+void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) {
+ (void)idx;
+ (void)stream_idx;
+ (void)xferred_bytes;
+ spk_cb_count++;
+}
+
+// Re-open a stream stopped by a transfer error: the driver keeps the stream
+// configured, so tuh_audio_start() resumes it. Invoked deferred so repeated
+// errors cannot stall the main loop.
+static void audio_app_restart_stream(uintptr_t param) {
+ const uint8_t idx = (uint8_t)(param >> 8);
+ const uint8_t stream_idx = (uint8_t)param;
+ if (!tuh_audio_mounted(idx)) {
+ return; // device is gone
+ }
+ if (stream_idx == cap_stream_idx) {
+ printf(" Restarting capture stream %u\r\n", stream_idx);
+ mic_enabled = tuh_audio_start(idx, stream_idx);
+ } else if (stream_idx == spk_stream_idx) {
+ printf(" Restarting playback stream %u\r\n", stream_idx);
+ spk_enabled = tuh_audio_start(idx, stream_idx);
+ }
+}
+
+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);
+ if (result != XFER_RESULT_SUCCESS) {
+ fail_event_count++;
+ }
+ return;
+ } else {
+ printf(" %s transfer failed: result=%u\r\n", stream_name, result);
+ }
+
+ fail_event_count++;
+ if (stream_idx == cap_stream_idx) {
+ mic_enabled = false;
+ } else if (stream_idx == spk_stream_idx) {
+ spk_enabled = false;
+ }
+ app_defer_ms_async(100, audio_app_restart_stream, ((uintptr_t)idx << 8) | stream_idx);
+}
+
+//--------------------------------------------------------------------+
+// TinyUSB Callbacks
+//--------------------------------------------------------------------+
+
+// Print all supported stream configurations
+static void print_stream_configs(uint8_t idx, uint8_t stream_idx) {
+ const tuh_audio_direction_t dir = tuh_audio_stream_direction(idx, stream_idx);
+ const char *dir_name = (dir == TUH_AUDIO_STREAM_CAPTURE) ? "capture" : "playback";
+ printf(" %s stream %u, configurations: %u\r\n", dir_name, stream_idx, tuh_audio_config_count(idx, stream_idx));
+ 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(" 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)) {
+ printf(" [%u] format=%u rate=%lu channels=%u\r\n", i, (unsigned)config.format,
+ (unsigned long)config.sample_rate, (unsigned)config.channels);
+ }
+ }
+}
+
+static void configure_stream_controls(uint8_t idx, uint8_t stream_idx, const char *stream_name) {
+ 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 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) {
+ 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)) {
+ has_control = true;
+ int16_t volume;
+ tusb_xfer_result_t result = tuh_audio_volume_get_sync(idx, stream_idx, TUH_AUDIO_CHANNEL_MASTER, &volume);
+ if (result == XFER_RESULT_SUCCESS) {
+ printf(" %s master volume: %d (1/256 dB)\r\n", stream_name, volume);
+ }
+ int32_t target = FEATURE_UNIT_VOLUME_DB;
+ target = TU_MAX(target, range.min);
+ target = TU_MIN(target, range.max);
+ result = tuh_audio_volume_set_sync(idx, stream_idx, TUH_AUDIO_CHANNEL_MASTER, (int16_t)target);
+ if (result == XFER_RESULT_SUCCESS) {
+ printf(" %s volume set: %d (1/256 dB)\r\n", stream_name, (int)target);
+ }
+ if (result != XFER_RESULT_SUCCESS) {
+ printf(" Setting %s volume failed: result=%u\r\n", stream_name, result);
+ }
+ }
+
+ if (!has_control) {
+ printf(" %s stream has no mute/volume control\r\n", stream_name);
+ }
+}
+
+// Invoked when device with Audio interface is un-mounted
+void tuh_audio_umount_cb(uint8_t idx) {
+ printf("Audio device unmounted: idx=%u\r\n", idx);
+ if (idx == audio_idx) {
+ app_defer_queue_clear();
+ audio_idx = TUSB_INDEX_INVALID_8;
+ cap_stream_idx = TUSB_INDEX_INVALID_8;
+ spk_stream_idx = TUSB_INDEX_INVALID_8;
+ mic_enabled = false;
+ spk_enabled = false;
+ }
+}
+
+static void tuh_audio_mount_async(uintptr_t param) {
+ uint8_t idx = (uint8_t)param;
+ printf("Audio device mounted: idx=%u addr=%u\r\n", idx, tuh_audio_get_dev_addr(idx));
+
+ // Inspect every stream and print its supported configurations
+ for (uint8_t stream_idx = 0; stream_idx < tuh_audio_stream_count(idx); stream_idx++) {
+ if (!tuh_audio_stream_exists(idx, stream_idx)) {
+ continue;
+ }
+ print_stream_configs(idx, stream_idx);
+ }
+
+ // Select a preferred S16_LE capture configuration without
+ // accessing USB interfaces, alternate settings, or endpoint addresses.
+ // Sample rates are tried in SAMPLE_RATES order (44.1 kHz first), stereo is
+ // preferred, mono is accepted.
+ static const uint32_t sample_rates[] = SAMPLE_RATES;
+ bool capture_found = false;
+ for (uint8_t r = 0; r < TU_ARRAY_SIZE(sample_rates) && !capture_found; r++) {
+ const uint32_t sample_rate = sample_rates[r];
+ for (uint8_t stream_idx = 0; stream_idx < tuh_audio_stream_count(idx) && !capture_found; stream_idx++) {
+ // Only consider capture streams, ignore playback streams
+ if (tuh_audio_stream_direction(idx, stream_idx) != TUH_AUDIO_STREAM_CAPTURE) {
+ continue;
+ }
+ for (uint8_t ch = AUDIO_MAX_CHANNELS; ch >= 1 && !capture_found; ch--) {
+ for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) {
+ tuh_audio_stream_config_t config;
+ // Check for a matching sample rate S16_LE configuration with the desired channel count
+ if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE &&
+ config.sample_rate == sample_rate && config.channels == ch) {
+ 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;
+ printf(" Microphone configured\r\n");
+ configure_stream_controls(idx, stream_idx, "Microphone");
+ mic_enabled = tuh_audio_start(idx, stream_idx);
+ capture_found = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!capture_found) {
+ printf(" No supported 44.1/48 kHz S16_LE capture configuration found\r\n");
+ }
+
+ // The echo needs a playback stream at the capture sample rate (or at any
+ // preferred rate when no capture stream exists, for the sine fallback).
+ // Prefer the same channel count as the capture stream (direct echo), then
+ // the other one (converted).
+ uint8_t playback_config_idx = TUSB_INDEX_INVALID_8;
+ for (uint8_t r = 0; r < TU_ARRAY_SIZE(sample_rates) && playback_config_idx == TUSB_INDEX_INVALID_8; r++) {
+ const uint32_t sample_rate = capture_found ? mic_config.sample_rate : sample_rates[r];
+ for (uint8_t stream_idx = 0;
+ stream_idx < tuh_audio_stream_count(idx) && playback_config_idx == TUSB_INDEX_INVALID_8; stream_idx++) {
+ // Only consider playback streams, ignore capture streams
+ if (tuh_audio_stream_direction(idx, stream_idx) != TUH_AUDIO_STREAM_PLAYBACK) {
+ continue;
+ }
+ const uint8_t preferred_channels = capture_found ? mic_config.channels : 2;
+ for (uint8_t n = 0; n < 2 && playback_config_idx == TUSB_INDEX_INVALID_8; n++) {
+ const uint8_t ch = (n == 0) ? preferred_channels : (uint8_t)(preferred_channels == 1 ? 2 : 1);
+ for (uint8_t i = 0; i < tuh_audio_config_count(idx, stream_idx); i++) {
+ tuh_audio_stream_config_t config;
+ if (tuh_audio_config_get(idx, stream_idx, i, &config) && config.format == TUH_AUDIO_FORMAT_S16_LE &&
+ config.sample_rate == sample_rate && config.channels == ch) {
+ spk_stream_idx = stream_idx;
+ spk_config = config;
+ playback_config_idx = i;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (playback_config_idx == TUSB_INDEX_INVALID_8) {
+ printf(" No supported %u S16_LE playback configuration, echo disabled\r\n",
+ (unsigned)(capture_found ? mic_config.sample_rate : sample_rates[0]));
+ return;
+ }
+ printf(" Configuring %u S16_LE playback (%u channels)\r\n", (unsigned)spk_config.sample_rate, spk_config.channels);
+ 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");
+ sine_phase = 0;
+ sine_phase_step = (uint32_t)(((uint64_t)SINE_TONE_HZ << 32) / spk_config.sample_rate);
+ configure_stream_controls(idx, spk_stream_idx, "Speaker");
+
+ if (capture_found) {
+ // Start in the mic-only phase without briefly activating playback first.
+ app_audio_phase_enter(APP_PHASE_MIC_ONLY);
+ } else {
+ // Playback-only device: start the sine test tone immediately.
+ spk_enabled = tuh_audio_start(idx, spk_stream_idx);
+ }
+}
+
+// Invoked when device with Audio interface is mounted
+void tuh_audio_mount_cb(uint8_t idx) {
+ app_defer_ms_async(100, tuh_audio_mount_async, idx);
+}
diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c
new file mode 100644
index 000000000..c7800958b
--- /dev/null
+++ b/examples/host/audio_host/src/main.c
@@ -0,0 +1,48 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 TinyUSB contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "bsp/board_api.h"
+#include "tusb.h"
+#include "app.h"
+
+//--------------------------------------------------------------------+
+// MACRO CONSTANT TYPEDEF PROTYPES
+//--------------------------------------------------------------------+
+
+/*------------- MAIN -------------*/
+int main(void) {
+ board_init();
+
+ printf("TinyUSB Host USB Audio Example\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};
+ tusb_init(BOARD_TUH_RHPORT, &host_init);
+
+ board_init_after_tusb();
+ while (1) {
+ // tinyusb host task
+ tuh_task();
+ led_blinking_task();
+ audio_app_task();
+ defer_queue_task();
+ }
+}
diff --git a/examples/host/audio_host/src/tusb_config.h b/examples/host/audio_host/src/tusb_config.h
new file mode 100644
index 000000000..60d0fee65
--- /dev/null
+++ b/examples/host/audio_host/src/tusb_config.h
@@ -0,0 +1,106 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 TinyUSB contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef TUSB_CONFIG_H_
+#define TUSB_CONFIG_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//--------------------------------------------------------------------
+// Common Configuration
+//--------------------------------------------------------------------
+
+#ifndef CFG_TUSB_MCU
+ #error CFG_TUSB_MCU must be defined
+#endif
+
+#ifndef CFG_TUSB_OS
+ #define CFG_TUSB_OS OPT_OS_NONE
+#endif
+
+#ifndef CFG_TUSB_DEBUG
+ #define CFG_TUSB_DEBUG 0
+#endif
+
+#ifndef CFG_TUH_MEM_SECTION
+ #define CFG_TUH_MEM_SECTION
+#endif
+
+#ifndef CFG_TUH_MEM_ALIGN
+ #define CFG_TUH_MEM_ALIGN __attribute__((aligned(4)))
+#endif
+
+//--------------------------------------------------------------------
+// Host Configuration
+//--------------------------------------------------------------------
+
+#define CFG_TUH_ENABLED 1
+
+#if CFG_TUSB_MCU == OPT_MCU_RP2040
+ #if (defined(CFG_TUH_RPI_PIO_USB) && CFG_TUH_RPI_PIO_USB) || (defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421)
+ #define BOARD_TUH_RHPORT 1
+ #endif
+#endif
+
+#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED
+
+#ifndef BOARD_TUH_RHPORT
+ #define BOARD_TUH_RHPORT 0
+#endif
+
+#ifndef BOARD_TUH_MAX_SPEED
+ #define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED
+#endif
+
+//--------------------------------------------------------------------
+// Driver Configuration
+//--------------------------------------------------------------------
+
+#define CFG_TUH_ENUMERATION_BUFSIZE 512
+
+#define CFG_TUH_HUB 0
+#define CFG_TUH_CDC 0
+#define CFG_TUH_HID 0
+#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)
+
+//------------- Audio Host Config -------------//
+#define CFG_TUH_AUDIO_MAX 1
+#define CFG_TUH_AUDIO_EPIN_BUFSIZE 256 // max capture transfer the application submits
+#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256 // max playback transfer the application submits
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TUSB_CONFIG_H_ */
diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake
index 57be416a2..ba63cf44b 100644
--- a/hw/bsp/rp2040/family.cmake
+++ b/hw/bsp/rp2040/family.cmake
@@ -122,6 +122,7 @@ target_sources(tinyusb_host_base INTERFACE
${TOP}/src/host/usbh.c
${TOP}/src/host/hub.c
${TOP}/src/class/cdc/cdc_host.c
+ ${TOP}/src/class/audio/audio_host.c
${TOP}/src/class/hid/hid_host.c
${TOP}/src/class/midi/midi_host.c
${TOP}/src/class/midi/midi2_host.c
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/CMakeLists.txt b/src/CMakeLists.txt
index e113f2d88..fb82e48e3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -27,6 +27,7 @@ function(tinyusb_sources_get OUTPUT_VAR)
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/host/usbh.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/host/hub.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/cdc/cdc_host.c
+ ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/audio/audio_host.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/hid/hid_host.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi_host.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi2_host.c
diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c
new file mode 100644
index 000000000..d93ff535d
--- /dev/null
+++ b/src/class/audio/audio_host.c
@@ -0,0 +1,2724 @@
+/*
+ * 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.
+ */
+
+// clang-format off
+/*
+ * USB Audio Host driver architecture
+ * ==================================
+ *
+ * One audioh_interface_t represents an Audio Control (AC) interface and owns
+ * at most one logical stream in each direction. A capture stream receives
+ * isochronous IN data from the device; a playback stream sends isochronous OUT
+ * data to the device. Audio topology remains private, while applications see
+ * each stream as a flat list of format, sample-rate, and channel-count tuples.
+ * Internally, tuples using the same Audio Streaming (AS) alternate setting
+ * share one audioh_as_config_t and refer to a rate source by index.
+ * Only Type-I PCM configurations are exposed. UAC1 requires a discrete
+ * sampling-frequency list; UAC2 Clock Source ranges are expanded into the
+ * bounded public list.
+ *
+ * Mounting discovers the topology and completes any control requests needed
+ * to describe the public configurations:
+ *
+ * USB enumeration
+ * audioh_open()
+ * +-- validate and retain the AC descriptor range
+ * +-- audioh_parse_as() for each consecutive AS interface
+ * | +-- parse the protocol-specific AS and format descriptors
+ * | +-- associate the data and optional feedback endpoints
+ * | `-- store UAC1 rates or a UAC2 Clock Source reference
+ * +-- audioh_link_feature_units()
+ * `-- tuh_audio_descriptor_cb()
+ *
+ * audioh_set_config()
+ * +-- UAC2: audioh_mount_clock_next()
+ * | `-- RANGE/CUR completion -> next Clock Source
+ * | -> rebuild public configurations
+ * `-- audioh_mount_feature_unit_next()
+ * `-- volume RANGE completion -> next logical stream
+ * -> tuh_audio_mount_cb()
+ * -> usbh_driver_set_config_complete()
+ *
+ * UAC1 rates come from each Format Type descriptor, whereas UAC2 rates are
+ * queried from the Clock Sources referenced by the parsed topology. Feature
+ * Unit parsing records master mute 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:
+ *
+ * tuh_audio_configure(stream, configuration)
+ * +-- resolve the public tuple to AS and rate-source indices
+ * +-- close endpoints from the previous configuration
+ * +-- initialize frame size, FIFO, and playback scheduler state
+ * `-- audioh_stream_open_ep() (data and optional feedback EP)
+ *
+ * tuh_audio_start(stream)
+ * +-- UAC1: SET_INTERFACE(non-zero alt)
+ * | `-- optional endpoint SET_CUR(sample rate)
+ * +-- UAC2: optional Clock Source CUR(sample rate)
+ * | `-- SET_INTERFACE(non-zero alt)
+ * `-- audioh_stream_start_xfer()
+ * `-- tuh_audio_event_cb(START_COMPLETE)
+ *
+ * tuh_audio_stop(stream)
+ * +-- SET_INTERFACE(alt 0) and stop local transfer resubmission
+ * `-- completion -> tuh_audio_event_cb(STOP_COMPLETE)
+ *
+ * A successful start/stop API return means that the first control request was
+ * submitted. The corresponding event reports completion of the entire chain.
+ * UAC1 sets the rate after activating the endpoint because its control targets
+ * that endpoint; UAC2 sets the Clock Source before activating the AS interface.
+ *
+ * Once started, each endpoint completion prepares and submits its successor:
+ *
+ * host controller -> audioh_xfer_cb()
+ * +-- capture data
+ * | +-- copy whole audio frames to the overwrite FIFO
+ * | +-- tuh_audio_capture_cb()
+ * | `-- audioh_stream_capture_xfer()
+ * +-- playback data
+ * | +-- tuh_audio_playback_cb()
+ * | `-- audioh_stream_playback_xfer()
+ * | +-- calculate the next fractional packet size
+ * | +-- read a complete packet from the FIFO, or send silence
+ * | `-- submit the next OUT transfer
+ * `-- explicit feedback
+ * +-- validate and stage the Q10.14 or Q16.16 rate
+ * `-- audioh_stream_feedback_xfer()
+ *
+ * tuh_audio_read() and tuh_audio_write() access only the stream FIFOs and do
+ * not need to run from transfer callbacks. The FIFOs decouple application I/O
+ * from USB polling cadence and never expose partial interleaved audio frames.
+ * A transfer failure stops resubmission and is reported through
+ * tuh_audio_event_cb(XFER_FAILED).
+ */
+// clang-format on
+
+#include "tusb_option.h"
+
+#if (CFG_TUH_ENABLED && CFG_TUH_AUDIO)
+
+ #include "host/usbh.h"
+ #include "host/usbh_pvt.h"
+ #include "audio_host.h"
+
+ // Driver-specific log level; defaults to the host-stack log level.
+ #ifndef CFG_TUH_AUDIO_LOG_LEVEL
+ #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL
+ #endif
+
+ #define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__)
+
+
+//--------------------------------------------------------------------+
+// MACROS, CONSTANTS, AND TYPES
+//--------------------------------------------------------------------+
+
+enum {
+ STREAM_STATE_IDLE = 0, // No active configuration.
+ 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,
+ 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
+
+// UAC1 stores one rate source per alternate setting. UAC2 alternate settings
+// that reference the same Clock Source share one rate source.
+typedef struct {
+ uint32_t sample_rate[CFG_TUH_AUDIO_MAX_SAM_FREQ];
+ uint8_t control_id; // UAC1 endpoint address or UAC2 Clock Source ID.
+ uint8_t sample_rate_count;
+ uint8_t frequency_access;
+} audioh_rate_source_t;
+
+// Properties shared by every sampling frequency of one AS alternate setting.
+typedef struct {
+ uint16_t ep_size;
+ uint8_t itf_num;
+ uint8_t alt_setting;
+ uint8_t ep_addr;
+ uint8_t ep_interval;
+ uint8_t ep_attr; // Synchronization and usage fields from bmAttributes.
+ uint8_t format;
+ uint8_t channels;
+ uint8_t terminal_id;
+ uint8_t rate_source_idx;
+ uint8_t rate_count;
+} audioh_as_config_t;
+
+// Explicit-feedback endpoint associated with a playback alternate setting.
+typedef struct {
+ uint8_t ep_addr;
+ uint8_t ep_size;
+ uint8_t ep_interval;
+ uint8_t ep_attr;
+} audioh_feedback_ep_t;
+
+typedef struct {
+ audioh_feedback_ep_t feedback[CFG_TUH_AUDIO_MAX_AS];
+
+ // 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;
+ uint16_t feedback_min_frames;
+ uint16_t feedback_max_frames;
+ uint16_t rem_acc;
+ bool feedback_opened;
+} audioh_playback_t;
+
+// Control-transfer bookkeeping; transfer payloads are stored in audioh_epbuf_t.
+typedef struct {
+ tuh_xfer_cb_t complete_cb;
+ uintptr_t user_data;
+ void *value;
+ union {
+ struct {
+ uint8_t width;
+ uint8_t value_type;
+ uint8_t channel;
+ uint8_t last_channel;
+ } control;
+ struct {
+ uint8_t stream_idx;
+ uint8_t range_step;
+ } mount;
+ } fu;
+ #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2
+ struct {
+ uint8_t rate_source_idx;
+ bool read_cur;
+ } clock;
+ #endif
+ bool fu_busy;
+} audioh_ctrl_state_t;
+
+// One logical capture or playback stream.
+typedef struct {
+ // Identity is initialized once and preserved when the stream is reset.
+ uint8_t idx;
+ uint8_t stream_idx;
+ tusb_dir_t dir; // TUSB_DIR_IN is capture; TUSB_DIR_OUT is playback.
+
+ // Device address, or zero while this stream slot is unused.
+ uint8_t daddr;
+
+ // Configurations discovered during enumeration.
+ uint8_t as_count;
+ uint8_t config_count;
+ audioh_as_config_t as[CFG_TUH_AUDIO_MAX_AS];
+
+ // Selected configuration and runtime state.
+ uint8_t active_config; // Index in the flattened public configuration list.
+ uint8_t active_as;
+ uint8_t active_rate;
+ uint8_t state;
+ uint8_t operation;
+ bool running;
+
+ // Directly associated Feature Unit, or zero when none is usable.
+ uint8_t feature_unit_id;
+ uint8_t mute_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.
+ uint16_t frame_bytes;
+
+ // The FIFO decouples application I/O from isochronous transfers. ep_buf is
+ // assigned during driver initialization and the endpoint during configure.
+ tu_edpt_stream_t edpt;
+ uint8_t ff_buf[CFG_TUH_AUDIO_STREAM_BUFSIZE];
+} tuh_audio_stream_t;
+
+// State owned by one Audio Control interface.
+typedef struct {
+ uint8_t daddr; // Device address, or zero for a free instance.
+ uint8_t ac_itf_num;
+ uint8_t protocol;
+ uint8_t stream_count;
+ uint8_t rate_source_count;
+ bool mounted;
+
+ audioh_rate_source_t rate_source[AUDIOH_MAX_RATE_SOURCES];
+
+ // Public stream indices are assigned in playback-then-capture order.
+ tuh_audio_stream_t out_stream;
+ tuh_audio_stream_t in_stream;
+ audioh_playback_t playback;
+ audioh_ctrl_state_t ctrl;
+} 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 {
+ // Clock discovery finishes before mount, so its buffer can be reused by
+ // runtime sampling-frequency and Feature Unit requests.
+ union {
+ #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2
+ TUH_EPBUF_DEF(clock_range, AUDIOH_CLOCK_RANGE_BUFSIZE);
+ #endif
+ struct {
+ TUH_EPBUF_DEF(rate_ctrl, 4);
+ TUH_EPBUF_DEF(fu_ctrl, 8);
+ } runtime;
+ } control;
+ // Feedback transfers may overlap runtime control transfers, so the feedback buffer is separate.
+ TUH_EPBUF_DEF(feedback, 4);
+ TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE);
+ TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE);
+} audioh_epbuf_t;
+
+static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX];
+
+CFG_TUH_MEM_SECTION static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX];
+
+//--------------------------------------------------------------------+
+// WEAK APPLICATION CALLBACKS
+//--------------------------------------------------------------------+
+
+TU_ATTR_WEAK void tuh_audio_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;
+}
+
+TU_ATTR_WEAK void tuh_audio_umount_cb(uint8_t idx) {
+ (void)idx;
+}
+
+TU_ATTR_WEAK void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) {
+ (void)idx;
+ (void)stream_idx;
+ (void)xferred_bytes;
+}
+
+TU_ATTR_WEAK void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) {
+ (void)idx;
+ (void)stream_idx;
+ (void)xferred_bytes;
+}
+
+TU_ATTR_WEAK void tuh_audio_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)event;
+ (void)result;
+}
+
+//--------------------------------------------------------------------+
+// HELPERS
+//--------------------------------------------------------------------+
+
+TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_rate_ctrl(audioh_epbuf_t *epbuf) {
+ return epbuf->control.runtime.rate_ctrl;
+}
+
+TU_ATTR_ALWAYS_INLINE static inline uint8_t *audioh_fu_ctrl(audioh_epbuf_t *epbuf) {
+ return epbuf->control.runtime.fu_ctrl;
+}
+
+TU_ATTR_ALWAYS_INLINE static inline uint8_t find_new_audio_index(void) {
+ for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) {
+ if (_audioh_itf[idx].daddr == 0) {
+ return idx;
+ }
+ }
+ return TUSB_INDEX_INVALID_8;
+}
+
+static 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 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) {
+ return (direction == TUSB_DIR_IN) ? &p_audio->in_stream : &p_audio->out_stream;
+}
+
+static tuh_audio_stream_t *audioh_get_stream_by_idx(audioh_interface_t *p_audio, uint8_t stream_idx) {
+ 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->as_count > 0 && s->stream_idx == stream_idx) {
+ return s;
+ }
+ }
+ return NULL;
+}
+
+TU_ATTR_ALWAYS_INLINE static inline tuh_audio_stream_t *audioh_get_stream_by_idx_unchecked(
+ audioh_interface_t *p_audio, uint8_t stream_idx) {
+ return (p_audio->out_stream.stream_idx == stream_idx) ? &p_audio->out_stream : &p_audio->in_stream;
+}
+
+TU_ATTR_ALWAYS_INLINE static inline audioh_playback_t *audioh_get_playback(const tuh_audio_stream_t *s) {
+ return &_audioh_itf[s->idx].playback;
+}
+
+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];
+}
+
+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) {
+ for (uint8_t i = 0; i < s->as_count; i++) {
+ if (config_idx < s->as[i].rate_count) {
+ const audioh_as_config_t *as = &s->as[i];
+ 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].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];
+ 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,
+ 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 mute_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) {
+ if (subslot_size == 1 && bit_resolution == 8) {
+ *format = TUH_AUDIO_FORMAT_S8;
+ } else if (subslot_size == 2 && bit_resolution == 16) {
+ *format = TUH_AUDIO_FORMAT_S16_LE;
+ } else if (subslot_size == 3 && bit_resolution == 24) {
+ *format = TUH_AUDIO_FORMAT_S24_3LE;
+ } else if (subslot_size == 4 && bit_resolution == 24) {
+ *format = TUH_AUDIO_FORMAT_S24_LE;
+ } else if (subslot_size == 4 && bit_resolution == 32) {
+ *format = TUH_AUDIO_FORMAT_S32_LE;
+ } else {
+ return false;
+ }
+ return true;
+}
+
+// bInterval encodes 2^(bInterval-1) full-speed frames or high-speed microframes.
+static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) {
+ const uint32_t unit_us = (tuh_speed_get(daddr) == TUSB_SPEED_HIGH) ? 125u : 1000u;
+ return ((uint32_t)1u << (ep_interval - 1)) * unit_us;
+}
+
+// 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);
+}
+
+// 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_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);
+}
+
+static void audioh_playback_reset(audioh_playback_t *playback) {
+ tu_memclr(playback, sizeof(*playback));
+}
+
+static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) {
+ for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) {
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ 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_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;
+ }
+ }
+ }
+ }
+ return NULL;
+}
+
+//--------------------------------------------------------------------+
+// PACKET SCHEDULER
+//--------------------------------------------------------------------+
+
+static void audioh_stream_xfer_failed(tuh_audio_stream_t *s, tusb_xfer_result_t result);
+
+static bool audioh_stream_feedback_xfer(tuh_audio_stream_t *s) {
+ const audioh_feedback_ep_t *feedback = &audioh_get_playback(s)->feedback[s->active_as];
+ TU_VERIFY(usbh_edpt_claim(s->daddr, feedback->ep_addr), false);
+ return usbh_edpt_xfer(s->daddr, feedback->ep_addr, _audioh_epbuf[s->idx].feedback, feedback->ep_size);
+}
+
+static bool audioh_stream_capture_xfer(tuh_audio_stream_t *s) {
+ const audioh_as_config_t *as = audioh_stream_active_as(s);
+ TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), false);
+ return usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, as->ep_size);
+}
+
+static bool audioh_stream_playback_xfer(tuh_audio_stream_t *s) {
+ const audioh_as_config_t *as = audioh_stream_active_as(s);
+ audioh_playback_t *playback = audioh_get_playback(s);
+ TU_VERIFY(usbh_edpt_claim(s->daddr, as->ep_addr), false);
+
+ // 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;
+ if (next_rem_acc >= 65536u) {
+ next_rem_acc -= 65536u;
+ frames++;
+ }
+
+ const uint16_t bytes = (uint16_t)(frames * s->frame_bytes);
+ if (tu_fifo_count(&s->edpt.ff) < bytes) {
+ // Isochronous OUT must continue at every interval. Send silence until a
+ // complete packet is queued, leaving any partial packet in the FIFO.
+ tu_memclr(s->edpt.ep_buf, bytes);
+ } else {
+ tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes);
+ }
+
+ if (!usbh_edpt_xfer(s->daddr, as->ep_addr, s->edpt.ep_buf, bytes)) {
+ return false;
+ }
+ playback->rem_acc = (uint16_t)next_rem_acc;
+ return true;
+}
+
+//--------------------------------------------------------------------+
+// STREAM CONFIGURATION
+//--------------------------------------------------------------------+
+
+static bool audioh_stream_close_ep(tuh_audio_stream_t *s) {
+ 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;
+ }
+ playback->feedback_opened = false;
+ }
+
+ if (!tu_edpt_stream_is_opened(&s->edpt)) {
+ return true;
+ }
+
+ const uint8_t ep_addr = s->edpt.ep_addr;
+ if (!tuh_edpt_close(s->daddr, ep_addr)) {
+ TU_LOG_DRV(" AUDIO close endpoint failed: addr=%u ep=%02x\r\n", s->daddr, ep_addr);
+ return false;
+ }
+
+ tu_edpt_stream_close(&s->edpt);
+ return true;
+}
+
+static void audioh_stream_fail(tuh_audio_stream_t *s) {
+ (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->operation = AUDIOH_STREAM_OP_NONE;
+ s->running = false;
+}
+
+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);
+ playback->target_frames_q16 = playback->nominal_frames_q16;
+ playback->rem_acc = 0;
+ }
+ tu_edpt_stream_clear(&s->edpt);
+}
+
+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) {
+ 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_rate_ctrl(&_audioh_epbuf[s->idx]);
+ 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);
+ 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,
+ .setup = &request,
+ .buffer = ctrl,
+ .complete_cb = complete_cb,
+ .user_data = (uintptr_t)s};
+ return tuh_control_xfer(&xfer);
+}
+
+static bool audioh_stream_open_ep(tuh_audio_stream_t *s) {
+ 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 = as->ep_addr,
+ .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS,
+ .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, as->ep_addr);
+ audioh_stream_fail(s);
+ return false;
+ }
+
+ // Bind the transfer helper to the selected endpoint and empty its FIFO.
+ const uint16_t xfer_len = (s->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE;
+ tu_edpt_stream_open(&s->edpt, s->daddr, &desc_ep, xfer_len);
+ tu_edpt_stream_clear(&s->edpt);
+
+ 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->state = STREAM_STATE_READY;
+ return true;
+}
+
+//--------------------------------------------------------------------+
+// USB HOST CLASS DRIVER
+//--------------------------------------------------------------------+
+bool audioh_init(void) {
+ tu_memclr(&_audioh_itf, sizeof(_audioh_itf));
+
+ for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) {
+ tuh_audio_stream_t *in = &_audioh_itf[idx].in_stream;
+ tuh_audio_stream_t *out = &_audioh_itf[idx].out_stream;
+
+ in->idx = idx;
+ in->dir = TUSB_DIR_IN;
+ out->idx = idx;
+ out->dir = TUSB_DIR_OUT;
+
+ 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));
+
+ audioh_stream_reset(in);
+ audioh_stream_reset(out);
+ audioh_playback_reset(&_audioh_itf[idx].playback);
+ }
+ return true;
+}
+
+bool audioh_deinit(void) {
+ for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) {
+ tu_edpt_stream_deinit(&_audioh_itf[idx].in_stream.edpt);
+ tu_edpt_stream_deinit(&_audioh_itf[idx].out_stream.edpt);
+ }
+ return true;
+}
+
+void audioh_close(uint8_t daddr) {
+ for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) {
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ if (p_audio->daddr != daddr) {
+ continue;
+ }
+
+ TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx);
+ if (p_audio->mounted) {
+ tuh_audio_umount_cb(idx);
+ }
+
+ for (uint8_t s = 0; s < 2; s++) {
+ 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);
+
+ // A disconnected device cannot complete its pending control request.
+ tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl));
+
+ p_audio->stream_count = 0;
+ p_audio->daddr = 0;
+ p_audio->protocol = 0;
+ p_audio->rate_source_count = 0;
+ p_audio->mounted = false;
+ }
+}
+
+static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_bytes) {
+ const uint8_t *fb = _audioh_epbuf[s->idx].feedback;
+ audioh_playback_t *playback = audioh_get_playback(s);
+ uint32_t feedback_q16;
+ if (xferred_bytes == 3) {
+ // Three-byte feedback is Q10.14; the scheduler uses Q16.16 throughout.
+ feedback_q16 = ((uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16)) << 2;
+ } else if (xferred_bytes == 4) {
+ feedback_q16 = (uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16) | ((uint32_t)fb[3] << 24);
+ } else {
+ TU_LOG_DRV(" AUDIO invalid feedback length: %lu\r\n", (unsigned long)xferred_bytes);
+ return;
+ }
+
+ 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 measured per USB frame or microframe. Scale it to the data
+ // endpoint's polling interval.
+ const audioh_as_config_t *as = audioh_stream_active_as(s);
+ const uint64_t target_q16_64 = (uint64_t)feedback_q16 << (as->ep_interval - 1u);
+ if (target_q16_64 > UINT32_MAX) {
+ 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 > 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;
+ }
+
+ // 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) {
+ tuh_audio_stream_t *s = audioh_find_stream(dev_addr, ep_addr);
+ if (s == NULL) {
+ return false;
+ }
+
+ // 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;
+ }
+
+ // 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);
+ if (!audioh_stream_feedback_xfer(s)) {
+ audioh_stream_xfer_failed(s, XFER_RESULT_FAILED);
+ }
+ return true;
+ }
+
+ if (s->dir == TUSB_DIR_IN) {
+ // Queue whole capture frames, notify the application, then re-arm.
+ const uint16_t bytes = (uint16_t)(xferred_bytes - (xferred_bytes % s->frame_bytes));
+ if (bytes > 0) {
+ tu_fifo_write_n(&s->edpt.ff, s->edpt.ep_buf, bytes);
+ }
+ tuh_audio_capture_cb(s->idx, s->stream_idx, (uint16_t)xferred_bytes);
+ 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);
+ if (s->running && !audioh_stream_playback_xfer(s)) {
+ audioh_stream_xfer_failed(s, XFER_RESULT_FAILED);
+ }
+ }
+ return true;
+}
+
+//--------------------------------------------------------------------+
+// ENUMERATION
+//--------------------------------------------------------------------+
+
+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_master_access;
+ uint8_t channels;
+ uint8_t volume_range_channel;
+ bool volume_all_channels_writable;
+} audioh_fu_info_t;
+
+typedef struct {
+ uint8_t id;
+ uint8_t frequency_access;
+} audioh_clock_info_t;
+
+typedef struct {
+ 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) {
+ 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 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];
+ 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;
+ }
+ }
+ #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) &&
+ 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:
+ return true;
+ }
+ #endif
+ default:
+ return false;
+ }
+}
+
+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
+ 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 false;
+}
+
+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;
+ }
+
+ 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) {
+ 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) {
+ 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:
+ 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
+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;
+ *info =
+ (audioh_clock_info_t){.id = id,
+ .frequency_access =
+ audioh_uac2_control_access(clock->bmControls, AUDIO20_CLOCK_SOURCE_CTRL_CLK_FRQ_POS)};
+ return true;
+ }
+ }
+ return false;
+}
+ #endif
+
+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->as_count == 0) {
+ continue;
+ }
+ 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 (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_master_access, fu.channels,
+ fu.volume_range_channel, fu.volume_all_channels_writable);
+ 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_desc_range_t *range,
+ const audioh_terminal_info_t *terminal) {
+ if (terminal->clock_id == 0) {
+ return -1;
+ }
+ 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) {
+ 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 AS alternate setting and return the next interface descriptor.
+// Supported configurations are appended to the stream matching its endpoint.
+static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const audioh_ac_desc_range_t *ac_desc,
+ const tusb_desc_interface_t *desc_itf, const uint8_t *p_desc,
+ const uint8_t *desc_end) {
+ const uint8_t itf_num = desc_itf->bInterfaceNumber;
+ const uint8_t alt = desc_itf->bAlternateSetting;
+
+ p_desc = tu_desc_next(p_desc);
+
+ // Alternate setting zero is the zero-bandwidth setting, not a configuration.
+ if (alt == 0 || desc_itf->bNumEndpoints == 0) {
+ while (p_desc < desc_end) {
+ TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL);
+ if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) {
+ break;
+ }
+ p_desc = tu_desc_next(p_desc);
+ }
+ return p_desc;
+ }
+
+ audioh_as_class_info_t class_info = {0};
+
+ // Retain one data endpoint and, for playback, one explicit-feedback endpoint.
+ // An implicit-feedback IN endpoint remains the data endpoint of its own AS
+ // interface and is therefore exposed as a capture stream.
+ typedef struct {
+ uint8_t ep_addr;
+ uint16_t ep_size;
+ uint8_t ep_interval;
+ uint8_t ep_attr;
+ bool sam_freq_ctrl;
+ } audioh_ep_info_t;
+ 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);
+ 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);
+ 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 (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;
+ }
+ 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;
+ }
+
+ bool is_data_ep = false;
+ bool is_explicit_feedback = false;
+ // UAC1 distinguishes feedback by synchronization type; UAC2 uses the
+ // endpoint usage field.
+ switch (p_audio->protocol) {
+ #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC1
+ case AUDIO_INT_PROTOCOL_CODE_V1:
+ 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,
+ 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_attr =
+ (uint8_t)((desc_endpoint->bmAttributes.sync << 2) | (desc_endpoint->bmAttributes.usage << 4));
+ has_feedback_ep = true;
+ break;
+ }
+
+ 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);
+ 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;
+ // Isochronous bInterval is an exponent in the inclusive range 1..16.
+ if (ep_info.ep_interval == 0 || ep_info.ep_interval > 16) {
+ ep_info.ep_interval = 1;
+ }
+ ep_info.ep_attr =
+ (uint8_t)((desc_endpoint->bmAttributes.sync << 2) | (desc_endpoint->bmAttributes.usage << 4));
+ has_data_ep = true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ p_desc = tu_desc_next(p_desc);
+ }
+
+ if (!has_data_ep) {
+ return p_desc;
+ }
+
+ 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_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 (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)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;
+ }
+ // Store the alternate setting once; the public API expands its sampling
+ // frequencies into separate configurations.
+ const audioh_ep_info_t *ep = &ep_info;
+ tuh_audio_stream_t *stream = audioh_get_stream(p_audio, tu_edpt_dir(ep->ep_addr));
+ 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;
+ }
+
+ 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;
+ }
+
+ // Capture always requests the endpoint's maximum packet size, so both the
+ // transfer buffer and FIFO must hold it.
+ if (stream->dir == TUSB_DIR_IN && (ep->ep_size > epbuf_size || ep->ep_size > CFG_TUH_AUDIO_STREAM_BUFSIZE)) {
+ TU_LOG_DRV(" AUDIO AS itf %u alt %u: capture ep size %u exceeds buffer capacity\r\n", itf_num, alt, ep->ep_size);
+ return p_desc;
+ }
+
+ 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_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;
+ }
+ as_config.rate_source_idx = (uint8_t)rate_source_idx;
+ break;
+ }
+ #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;
+ #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;
+ 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.rate_count;
+
+ return p_desc;
+}
+
+uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) {
+ (void)rhport;
+
+ 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(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));
+ tu_memclr(&p_audio->ctrl, sizeof(p_audio->ctrl));
+ audioh_stream_reset(&p_audio->in_stream);
+ audioh_stream_reset(&p_audio->out_stream);
+ audioh_playback_reset(&p_audio->playback);
+ p_audio->in_stream.daddr = dev_addr;
+ p_audio->out_stream.daddr = dev_addr;
+
+ TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr);
+
+ 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;
+ }
+ 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;
+ }
+ 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;
+
+ // Audio Streaming interfaces belonging to this function immediately follow
+ // its Audio Control descriptor block.
+ while (p_desc < desc_end) {
+ if (!audioh_desc_valid(p_desc, desc_end, 2)) {
+ goto open_failed;
+ }
+ 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 ||
+ 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, &ac_desc, desc_interface, p_desc, desc_end);
+ if (p_desc == NULL) {
+ goto open_failed;
+ }
+ }
+
+ audioh_link_feature_units(p_audio, &ac_desc);
+
+ if (p_audio->in_stream.as_count == 0 && p_audio->out_stream.as_count == 0) {
+ goto open_failed;
+ }
+
+ // Assign contiguous public indices in playback-then-capture order.
+ uint8_t stream_idx = 0;
+ if (p_audio->out_stream.as_count > 0) {
+ p_audio->out_stream.stream_idx = stream_idx++;
+ }
+ if (p_audio->in_stream.as_count > 0) {
+ p_audio->in_stream.stream_idx = stream_idx++;
+ }
+ 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:
+ 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->stream_count = 0;
+ p_audio->rate_source_count = 0;
+ p_audio->mounted = false;
+ return 0;
+}
+
+//--------------------------------------------------------------------+
+// SET CONFIGURATION
+//--------------------------------------------------------------------+
+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);
+ 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_ctrl_state_t *ctrl = &p_audio->ctrl;
+ audioh_rate_source_t *rate_source = &p_audio->rate_source[ctrl->clock.rate_source_idx];
+ const uint16_t length = ctrl->clock.read_cur ? 4u : (uint16_t)sizeof(epbuf->control.clock_range);
+
+ const tusb_control_request_t request = {
+ .bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN},
+ .bRequest = ctrl->clock.read_cur ? AUDIO20_CS_REQ_CUR : AUDIO20_CS_REQ_RANGE,
+ .wValue = tu_htole16(tu_u16(AUDIO20_CS_CTRL_SAM_FREQ, 0)),
+ .wIndex = tu_htole16(tu_u16(rate_source->control_id, p_audio->ac_itf_num)),
+ .wLength = tu_htole16(length),
+ };
+ tuh_xfer_t xfer = {.daddr = p_audio->daddr,
+ .ep_addr = 0,
+ .setup = &request,
+ .buffer = epbuf->control.clock_range,
+ .complete_cb = audioh_mount_clock_complete,
+ .user_data = (uintptr_t)idx};
+ return tuh_control_xfer(&xfer);
+}
+
+static void audioh_mount_clock_finish(uint8_t idx) {
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+ ctrl->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;
+ }
+
+ ctrl->fu.mount.stream_idx = 0;
+ audioh_mount_feature_unit_next(idx);
+}
+
+static void audioh_mount_clock_next(uint8_t idx) {
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+
+ while (ctrl->clock.rate_source_idx < p_audio->rate_source_count) {
+ ctrl->clock.read_cur = false;
+ ctrl->fu_busy = true;
+ if (audioh_mount_clock_submit(idx)) {
+ return;
+ }
+ p_audio->rate_source[ctrl->clock.rate_source_idx].sample_rate_count = 0;
+ ctrl->clock.rate_source_idx++;
+ }
+ audioh_mount_clock_finish(idx);
+}
+
+static void audioh_mount_clock_complete(tuh_xfer_t *xfer) {
+ const uint8_t idx = (uint8_t)xfer->user_data;
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ if (!ctrl->fu_busy) {
+ return;
+ }
+ audioh_rate_source_t *rate_source = &p_audio->rate_source[ctrl->clock.rate_source_idx];
+
+ bool success = xfer->result == XFER_RESULT_SUCCESS;
+ if (success && ctrl->clock.read_cur) {
+ success = xfer->actual_len == 4;
+ if (success) {
+ const uint32_t current = tu_le32toh(tu_unaligned_read32(epbuf->control.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->control.clock_range, (uint16_t)xfer->actual_len);
+ if (success && rate_source->frequency_access == AUDIOH_CTRL_READ) {
+ ctrl->clock.read_cur = true;
+ if (audioh_mount_clock_submit(idx)) {
+ return;
+ }
+ success = false;
+ }
+ }
+
+ if (!success) {
+ rate_source->sample_rate_count = 0;
+ }
+ ctrl->fu_busy = false;
+ ctrl->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++) {
+ if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].ac_itf_num == itf_num) {
+ idx = i;
+ break;
+ }
+ }
+
+ if (idx == TUSB_INDEX_INVALID_8) {
+ // Only the Audio Control interface drives mounting. Streaming alternate
+ // settings are selected later by tuh_audio_start().
+ usbh_driver_set_config_complete(dev_addr, itf_num);
+ return true;
+ }
+
+ audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl;
+ #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2
+ if (_audioh_itf[idx].protocol == AUDIO_INT_PROTOCOL_CODE_V2) {
+ ctrl->clock.rate_source_idx = 0;
+ audioh_mount_clock_next(idx);
+ } else
+ #endif
+ {
+ ctrl->fu.mount.stream_idx = 0;
+ audioh_mount_feature_unit_next(idx);
+ }
+ return true;
+}
+
+//--------------------------------------------------------------------+
+// APPLICATION API
+//--------------------------------------------------------------------+
+bool tuh_audio_mounted(uint8_t idx) {
+ TU_VERIFY(idx < CFG_TUH_AUDIO_MAX);
+ return _audioh_itf[idx].mounted;
+}
+
+uint8_t tuh_audio_get_dev_addr(uint8_t idx) {
+ TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0);
+ return _audioh_itf[idx].daddr;
+}
+
+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_access != AUDIOH_CTRL_NONE;
+}
+
+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_range_channel != TUSB_INDEX_INVALID_8, 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];
+ TU_VERIFY(p_audio->daddr != 0, 0);
+ return p_audio->stream_count;
+}
+
+bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->daddr != 0, false);
+ return audioh_get_stream_by_idx(p_audio, stream_idx) != NULL;
+}
+
+tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUH_AUDIO_STREAM_DIRECTION_COUNT);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->daddr != 0, TUH_AUDIO_STREAM_DIRECTION_COUNT);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s, TUH_AUDIO_STREAM_DIRECTION_COUNT);
+ return (s->dir == TUSB_DIR_IN) ? TUH_AUDIO_STREAM_CAPTURE : TUH_AUDIO_STREAM_PLAYBACK;
+}
+
+uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->daddr != 0, 0);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s, 0);
+ return s->config_count;
+}
+uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, TUSB_INDEX_INVALID_8);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->daddr != 0, TUSB_INDEX_INVALID_8);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s, TUSB_INDEX_INVALID_8);
+ return s->active_config;
+}
+bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->daddr != 0, false);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s && config, false);
+
+ 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) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->mounted, false);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s, false);
+ 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);
+ TU_VERIFY(!s->running, false);
+ if (s->state == STREAM_STATE_READY) {
+ // Configuration cannot close an endpoint while its final transfer drains.
+ TU_VERIFY(!usbh_edpt_busy(s->daddr, s->edpt.ep_addr), false);
+ if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) {
+ TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false);
+ }
+ }
+
+ // Reopen even when the address is unchanged: packet size and interval belong
+ // to the alternate setting and may differ.
+ TU_VERIFY(audioh_stream_close_ep(s), false);
+
+ const audioh_as_config_t *as = &s->as[as_idx];
+ 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->feedback_min_frames = (uint16_t)((cfg.sample_rate - 1u) / frame_div);
+ playback->feedback_max_frames = (uint16_t)(cfg.sample_rate / frame_div + 1u);
+ playback->rem_acc = 0;
+ }
+ if (s->dir == TUSB_DIR_IN) {
+ // Overwrite mode is frame-safe only when FIFO depth is a whole-frame multiple.
+ const uint16_t fifo_depth = CFG_TUH_AUDIO_STREAM_BUFSIZE - (CFG_TUH_AUDIO_STREAM_BUFSIZE % s->frame_bytes);
+ if (!tu_fifo_config(&s->edpt.ff, s->ff_buf, fifo_depth, true)) {
+ audioh_stream_fail(s);
+ 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, as->itf_num, as->alt_setting,
+ as->ep_addr);
+
+ return audioh_stream_open_ep(s);
+}
+
+// Start endpoint transfers after the alternate setting and sampling frequency
+// are both active.
+static bool audioh_stream_start_xfer(tuh_audio_stream_t *s) {
+ if (s->dir == TUSB_DIR_IN) {
+ return audioh_stream_capture_xfer(s);
+ } else {
+ if (audioh_get_playback(s)->feedback[s->active_as].ep_addr != 0) {
+ TU_VERIFY(audioh_stream_feedback_xfer(s), false);
+ }
+ 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);
+
+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) {
+ // Ignore a completion delivered after disconnect or stop.
+ return;
+ }
+ if (xfer->result != XFER_RESULT_SUCCESS) {
+ TU_LOG_DRV(" AUDIO set sampling frequency failed: result=%u\r\n", xfer->result);
+ 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_start_done(s, XFER_RESULT_FAILED);
+ }
+ } else
+ #endif
+ {
+ audioh_stream_start_xfers(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_start_done(s, XFER_RESULT_FAILED);
+ }
+ return;
+ } else
+ #endif
+ {
+ audioh_stream_start_xfers(s);
+ }
+}
+
+static void audioh_stream_start_complete(tuh_xfer_t *xfer) {
+ tuh_audio_stream_t *s = (tuh_audio_stream_t *)xfer->user_data;
+ if (s->daddr != xfer->daddr || s->state != STREAM_STATE_READY || !s->running) {
+ // Ignore a completion delivered after disconnect or stop.
+ return;
+ }
+ if (xfer->result != XFER_RESULT_SUCCESS) {
+ TU_LOG_DRV(" AUDIO SET_INTERFACE activate failed: result=%u\r\n", xfer->result);
+ audioh_stream_start_done(s, xfer->result);
+ return;
+ }
+ audioh_stream_start_active(s);
+}
+
+bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, false);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->mounted, false);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s, false);
+ TU_VERIFY(s->state == STREAM_STATE_READY && s->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);
+ 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);
+ }
+
+ // Capture and playback must use the same rate while both are running.
+ tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream;
+ if (other->running) {
+ const audioh_as_config_t *other_as = audioh_stream_active_as(other);
+ 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.rem_acc = 0;
+ }
+ 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;
+ #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2
+ if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2 && rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) {
+ submitted = audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete);
+ } else
+ #endif
+ {
+ submitted = audioh_stream_activate(s);
+ }
+ if (!submitted) {
+ s->operation = AUDIOH_STREAM_OP_NONE;
+ s->running = false;
+ return false;
+ }
+ return true;
+}
+
+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 || 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) {
+ 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 && 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.
+ TU_VERIFY(tuh_interface_set(s->daddr, as->itf_num, 0, audioh_stream_stop_complete, (uintptr_t)s), false);
+
+ // SET_INTERFACE stops future traffic. The current transfer drains, while its
+ // data and all queued frames are discarded.
+ s->operation = AUDIOH_STREAM_OP_STOP;
+ audioh_stream_stop_xfers(s);
+ return true;
+}
+
+uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->mounted && buffer, 0);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0);
+ TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0);
+
+ // Never split an audio frame at the FIFO boundary.
+ const uint32_t frames = TU_MIN(frame_count, tu_fifo_remaining(&s->edpt.ff) / s->frame_bytes);
+ if (frames == 0) {
+ return 0;
+ }
+ tu_fifo_write_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes));
+
+ return frames;
+}
+
+uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->mounted && buffer, 0);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0);
+ TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0);
+
+ // Never return a partial audio frame.
+ const uint32_t frames = TU_MIN(frame_count, tu_fifo_count(&s->edpt.ff) / s->frame_bytes);
+ if (frames > 0) {
+ tu_fifo_read_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes));
+ }
+ return frames;
+}
+
+uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->daddr != 0, 0);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s && s->dir == TUSB_DIR_OUT, 0);
+ TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0);
+ return tu_edpt_stream_write_available(&s->edpt) / s->frame_bytes;
+}
+
+uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx) {
+ TU_VERIFY(dev_idx < CFG_TUH_AUDIO_MAX, 0);
+ audioh_interface_t *p_audio = &_audioh_itf[dev_idx];
+ TU_VERIFY(p_audio->daddr != 0, 0);
+
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx(p_audio, stream_idx);
+ TU_VERIFY(s && s->dir == TUSB_DIR_IN, 0);
+ TU_VERIFY(s->state == STREAM_STATE_READY && s->running, 0);
+ return tu_edpt_stream_read_available(&s->edpt) / s->frame_bytes;
+}
+
+//--------------------------------------------------------------------+
+// AUDIO CONTROL REQUESTS
+//--------------------------------------------------------------------+
+
+static void audioh_fu_set_complete(tuh_xfer_t *xfer);
+
+enum {
+ AUDIOH_FU_VALUE_BOOL,
+ AUDIOH_FU_VALUE_I16
+};
+
+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
+ 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_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;
+ }
+
+ 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;
+}
+
+// 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;
+ } else {
+ const uint16_t value = tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf)));
+ *((int16_t *)ctrl->value) = (int16_t)value;
+ }
+}
+
+// Convert the driver-owned response before releasing the request state and
+// invoking the application callback.
+static void audioh_fu_get_complete(tuh_xfer_t *xfer) {
+ const uint8_t idx = (uint8_t)xfer->user_data;
+ audioh_ctrl_state_t *ctrl = &_audioh_itf[idx].ctrl;
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ tuh_xfer_cb_t app_cb = ctrl->complete_cb;
+ uintptr_t user_data = ctrl->user_data;
+ ctrl->complete_cb = NULL;
+ ctrl->fu_busy = false;
+
+ if (ctrl->value != NULL && xfer->result == XFER_RESULT_SUCCESS) {
+ if (xfer->actual_len == ctrl->fu.control.width) {
+ audioh_fu_value_store(ctrl, epbuf);
+ } else {
+ xfer->result = XFER_RESULT_FAILED;
+ }
+ }
+
+ xfer->user_data = user_data;
+ if (app_cb != NULL) {
+ app_cb(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(tuh_audio_stream_t *s, audioh_ctrl_state_t *ctrl, audioh_epbuf_t *epbuf) {
+ const uint16_t value = tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf)));
+ switch (ctrl->fu.mount.range_step) {
+ case AUDIOH_VOLUME_RANGE_MIN:
+ s->volume_range.min = (int16_t)value;
+ break;
+ case AUDIOH_VOLUME_RANGE_MAX:
+ s->volume_range.max = (int16_t)value;
+ break;
+ case AUDIOH_VOLUME_RANGE_RES:
+ s->volume_range.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_ctrl_state_t *ctrl = &p_audio->ctrl;
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx);
+
+ const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2;
+ const uint8_t selector = uac2 ? AUDIO20_FU_CTRL_VOLUME : AUDIO10_FU_CTRL_VOLUME;
+ 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, s->volume_range_channel)),
+ .wIndex = tu_htole16(tu_u16(s->feature_unit_id, p_audio->ac_itf_num)),
+ .wLength = tu_htole16(uac2 ? 8u : 2u),
+ };
+ tuh_xfer_t xfer = {.daddr = p_audio->daddr,
+ .ep_addr = 0,
+ .setup = &request,
+ .buffer = audioh_fu_ctrl(epbuf),
+ .complete_cb = audioh_mount_feature_unit_complete,
+ .user_data = (uintptr_t)idx};
+ return tuh_control_xfer(&xfer);
+}
+
+static void audioh_mount_feature_unit_next(uint8_t idx) {
+ audioh_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+
+ while (ctrl->fu.mount.stream_idx < p_audio->stream_count) {
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx);
+ 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_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;
+ }
+ ctrl->fu_busy = false;
+ }
+ ctrl->fu.mount.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_interface_t *p_audio = &_audioh_itf[idx];
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ if (!ctrl->fu_busy) {
+ return;
+ }
+ tuh_audio_stream_t *s = audioh_get_stream_by_idx_unchecked(p_audio, ctrl->fu.mount.stream_idx);
+
+ const bool uac2 = p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2;
+ if (uac2 && xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 8 &&
+ tu_le16toh(tu_unaligned_read16(audioh_fu_ctrl(epbuf))) == 1) {
+ uint8_t *fu_ctrl = audioh_fu_ctrl(epbuf);
+ s->volume_range.min = (int16_t)tu_le16toh(tu_unaligned_read16(&fu_ctrl[2]));
+ s->volume_range.max = (int16_t)tu_le16toh(tu_unaligned_read16(&fu_ctrl[4]));
+ s->volume_range.res = tu_le16toh(tu_unaligned_read16(&fu_ctrl[6]));
+ if (s->volume_range.min > s->volume_range.max || s->volume_range.res == 0) {
+ xfer->result = XFER_RESULT_FAILED;
+ }
+ } else if (!uac2 && xfer->result == XFER_RESULT_SUCCESS && xfer->actual_len == 2) {
+ audioh_fu_volume_range_store(s, ctrl, epbuf);
+ ctrl->fu.mount.range_step++;
+
+ if (ctrl->fu.mount.range_step < AUDIOH_VOLUME_RANGE_COUNT) {
+ if (audioh_mount_feature_unit_submit(idx)) {
+ return;
+ }
+ xfer->result = XFER_RESULT_FAILED;
+ } else if (s->volume_range.min > s->volume_range.max || s->volume_range.res == 0) {
+ xfer->result = XFER_RESULT_FAILED;
+ }
+ }
+
+ const uint32_t expected_len = uac2 ? 8u : 2u;
+ if (xfer->result != XFER_RESULT_SUCCESS || xfer->actual_len != expected_len) {
+ 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;
+ }
+ }
+ ctrl->fu_busy = false;
+ ctrl->fu.mount.stream_idx++;
+ audioh_mount_feature_unit_next(idx);
+}
+
+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(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_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);
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ TU_VERIFY(!ctrl->fu_busy, false);
+ // Reserve both bookkeeping and payload storage before populating the request.
+ ctrl->fu_busy = true;
+
+ 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);
+ }
+
+ if (complete_cb == NULL) {
+ 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;
+ 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, 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, 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(channel == 0 && s->mute_access != AUDIOH_CTRL_NONE, false);
+ } else if (control_selector == AUDIO10_FU_CTRL_VOLUME) {
+ 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);
+ audioh_ctrl_state_t *ctrl = &p_audio->ctrl;
+ audioh_epbuf_t *epbuf = &_audioh_epbuf[idx];
+ TU_VERIFY(!ctrl->fu_busy, false);
+ ctrl->fu_busy = true;
+ ctrl->value = value;
+ ctrl->fu.control.width = width;
+ ctrl->fu.control.value_type = value_type;
+
+ if (complete_cb == NULL) {
+ // 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, channel,
+ audioh_fu_ctrl(epbuf), width, &xfer)) {
+ ctrl->fu_busy = false;
+ return false;
+ }
+ if (xfer.result == XFER_RESULT_SUCCESS && xfer.actual_len == width) {
+ audioh_fu_value_store(ctrl, epbuf);
+ } else if (xfer.result == XFER_RESULT_SUCCESS && user_data != 0) {
+ *((tusb_xfer_result_t *)user_data) = XFER_RESULT_FAILED;
+ }
+ ctrl->fu_busy = false;
+ return true;
+ }
+
+ // The asynchronous wrapper converts the response before calling the application.
+ ctrl->complete_cb = complete_cb;
+ 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, channel,
+ audioh_fu_ctrl(epbuf), width, &xfer)) {
+ ctrl->complete_cb = NULL;
+ ctrl->fu_busy = false;
+ return false;
+ }
+ return true;
+}
+
+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, 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, 0, mute, 1, AUDIOH_FU_VALUE_BOOL, complete_cb, 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);
+ 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 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);
+ }
+ 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, 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, 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
new file mode 100644
index 000000000..95911ab3b
--- /dev/null
+++ b/src/class/audio/audio_host.h
@@ -0,0 +1,361 @@
+/*
+ * 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.
+ */
+
+#ifndef TUSB_AUDIO_HOST_H_
+#define TUSB_AUDIO_HOST_H_
+
+#include "audio.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//--------------------------------------------------------------------+
+// 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
+#endif
+// Maximum discrete sampling frequencies retained per rate source. UAC1 uses
+// one rate source per alternate setting; UAC2 alternate settings may share a
+// Clock Source.
+#ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ
+ #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5
+#endif
+// Maximum supported nonzero-bandwidth Audio Streaming alternate settings per
+// logical stream.
+#ifndef CFG_TUH_AUDIO_MAX_AS
+ #define CFG_TUH_AUDIO_MAX_AS 4
+#endif
+
+// Maximum size of one capture (IN) isochronous transfer the driver submits.
+// Configurations needing a larger per-poll-interval packet are rejected.
+// 256 covers 2-ch 48 kHz S16_LE (192 B) and common endpoint padding (208 B).
+#ifndef CFG_TUH_AUDIO_EPIN_BUFSIZE
+ #define CFG_TUH_AUDIO_EPIN_BUFSIZE 256
+#endif
+
+// Maximum size of one playback (OUT) isochronous transfer the driver submits.
+// Configurations needing a larger per-poll-interval packet are rejected.
+#ifndef CFG_TUH_AUDIO_EPOUT_BUFSIZE
+ #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 256
+#endif
+
+// 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. 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
+
+//--------------------------------------------------------------------+
+// Types
+//--------------------------------------------------------------------+
+
+// Fixed transfer direction of a logical stream.
+typedef enum {
+ TUH_AUDIO_STREAM_PLAYBACK = 0, // Host -> Device (OUT)
+ TUH_AUDIO_STREAM_CAPTURE = 1, // Device -> Host (IN)
+ TUH_AUDIO_STREAM_DIRECTION_COUNT
+} tuh_audio_direction_t;
+
+// 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 {
+ TUH_AUDIO_FORMAT_S8 = 0, // signed 8-bit
+ TUH_AUDIO_FORMAT_S16_LE, // signed 16-bit little-endian
+ TUH_AUDIO_FORMAT_S24_3LE, // signed 24-bit packed in 3 bytes, LE
+ TUH_AUDIO_FORMAT_S24_LE, // signed 24-bit in 32-bit container, LE
+ TUH_AUDIO_FORMAT_S32_LE, // signed 32-bit little-endian
+ TUH_AUDIO_FORMAT_COUNT
+} tuh_audio_format_t;
+
+// One complete supported discrete configuration tuple.
+// Each entry is a full (format, sample_rate, channels) combination,
+// avoiding invalid mixes between independent format/rate/channel lists.
+// dir is constant for all configs of a given (dev_idx, stream_idx) and
+// equals the result of tuh_audio_stream_direction().
+typedef struct {
+ uint32_t sample_rate;
+ tuh_audio_direction_t dir;
+ tuh_audio_format_t format;
+ uint8_t channels;
+} tuh_audio_stream_config_t;
+
+// Feature Unit channel zero selects the master channel.
+#define TUH_AUDIO_CHANNEL_MASTER 0
+
+// Volume values are signed 1/256 dB. INT16_MIN represents silence.
+#define TUH_AUDIO_VOLUME_SILENCE INT16_MIN
+
+// 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;
+
+// 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
+//--------------------------------------------------------------------+
+
+// Number of logical audio streams exposed by one mounted device. The
+// application iterates stream indices [0, tuh_audio_stream_count()) and
+// inspects each with tuh_audio_stream_exists()/tuh_audio_stream_direction().
+uint8_t tuh_audio_stream_count(uint8_t dev_idx);
+
+// True if (dev_idx, stream_idx) identifies an existing stream.
+bool tuh_audio_stream_exists(uint8_t dev_idx, uint8_t stream_idx);
+
+// Fixed transfer direction of the stream.
+tuh_audio_direction_t tuh_audio_stream_direction(uint8_t dev_idx, uint8_t stream_idx);
+
+//--------------------------------------------------------------------+
+// Configuration Enumeration
+//--------------------------------------------------------------------+
+
+// Number of supported discrete configurations of the stream.
+uint8_t tuh_audio_config_count(uint8_t dev_idx, uint8_t stream_idx);
+
+// Active configuration index of the stream, or TUSB_INDEX_INVALID_8 if none.
+uint8_t tuh_audio_active_config(uint8_t dev_idx, uint8_t stream_idx);
+
+// Retrieve one discrete configuration tuple into *config.
+bool tuh_audio_config_get(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx, tuh_audio_stream_config_t *config);
+
+//--------------------------------------------------------------------+
+// Configuration (ALSA hw_params analogue)
+//--------------------------------------------------------------------+
+
+// Synchronously configure the stream with the discrete configuration identified by
+// config_idx. The driver:
+// 1. resolves the AS interface and alternate setting,
+// 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 transferring data with the configuration selected by configure().
+// UAC1 activates the alternate setting before setting an endpoint frequency;
+// UAC2 sets a writable Clock Source before activating the alternate setting.
+// Startup is asynchronous: true means that the first request was submitted.
+// 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.
+// tuh_audio_write() is valid only for TUH_AUDIO_STREAM_PLAYBACK streams,
+// tuh_audio_read() only for TUH_AUDIO_STREAM_CAPTURE streams.
+// Both functions are non-blocking and return immediately.
+// Returns the number of frames actually written/read (0 on any error,
+// including wrong direction, unconfigured/stopped stream, or full/empty FIFO).
+uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer, uint32_t frame_count);
+uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint32_t frame_count);
+
+// Number of frames that can be queued immediately for playback.
+uint32_t tuh_audio_write_available(uint8_t dev_idx, uint8_t stream_idx);
+// Number of captured frames that can be read immediately.
+uint32_t tuh_audio_read_available(uint8_t dev_idx, uint8_t stream_idx);
+
+//--------------------------------------------------------------------+
+// Helpers
+//--------------------------------------------------------------------+
+
+// Container size in bytes of one sample for a given format.
+static inline uint8_t tuh_audio_format_bytes(tuh_audio_format_t format) {
+ switch (format) {
+ case TUH_AUDIO_FORMAT_S8:
+ return 1;
+ case TUH_AUDIO_FORMAT_S16_LE:
+ return 2;
+ case TUH_AUDIO_FORMAT_S24_3LE:
+ return 3;
+ case TUH_AUDIO_FORMAT_S24_LE:
+ case TUH_AUDIO_FORMAT_S32_LE:
+ return 4;
+ default:
+ return 0;
+ }
+}
+
+// Size in bytes of one frame (all channels) for a configuration.
+static inline uint32_t tuh_audio_config_frame_size(const tuh_audio_stream_config_t *config) {
+ TU_ASSERT(config != NULL);
+ return (uint32_t)tuh_audio_format_bytes(config->format) * config->channels;
+}
+
+//--------------------------------------------------------------------+
+// Device Info
+//--------------------------------------------------------------------+
+
+// Check if Audio device is mounted
+bool tuh_audio_mounted(uint8_t idx);
+// Get device address of Audio device
+uint8_t tuh_audio_get_dev_addr(uint8_t idx);
+// 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 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);
+
+//--------------------------------------------------------------------+
+// Control Request API
+//--------------------------------------------------------------------+
+
+// 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 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, 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, 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,
+ 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) {
+ 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,
+ 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,
+ uint8_t channel, int16_t *volume) {
+ TU_API_SYNC(tuh_audio_volume_get, idx, stream_idx, channel, volume);
+}
+
+//--------------------------------------------------------------------+
+// 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);
+
+// Invoked when device with Audio interface is un-mounted
+void tuh_audio_umount_cb(uint8_t idx);
+
+// Invoked when an isochronous IN transfer completes successfully: the
+// received data is already queued into the stream's capture FIFO.
+void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes);
+
+// Invoked after a successful isochronous OUT transfer, before the next packet
+// is prepared. After this callback returns, the driver submits queued audio
+// from the stream FIFO, or silence when a complete packet is unavailable.
+void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes);
+
+// 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
+//--------------------------------------------------------------------+
+bool audioh_init(void);
+bool audioh_deinit(void);
+uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len);
+bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num);
+bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes);
+void audioh_close(uint8_t daddr);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TUSB_AUDIO_HOST_H_ */
diff --git a/src/host/usbh.c b/src/host/usbh.c
index 94d075813..fd0c9b7b5 100644
--- a/src/host/usbh.c
+++ b/src/host/usbh.c
@@ -258,6 +258,18 @@ static usbh_class_driver_t const usbh_class_drivers[] = {
},
#endif
+ #if CFG_TUH_AUDIO
+ {
+ .name = DRIVER_NAME("AUDIO"),
+ .init = audioh_init,
+ .deinit = audioh_deinit,
+ .open = audioh_open,
+ .set_config = audioh_set_config,
+ .xfer_cb = audioh_xfer_cb,
+ .close = audioh_close
+ },
+ #endif
+
#if CFG_TUH_HID
{
.name = DRIVER_NAME("HID"),
diff --git a/src/tinyusb.mk b/src/tinyusb.mk
index 941791670..c8e943a5a 100644
--- a/src/tinyusb.mk
+++ b/src/tinyusb.mk
@@ -19,6 +19,7 @@ TINYUSB_SRC_C += \
src/class/usbtmc/usbtmc_device.c \
src/class/video/video_device.c \
src/class/vendor/vendor_device.c \
+ src/class/audio/audio_host.c \
src/host/usbh.c \
src/host/hub.c \
src/class/cdc/cdc_host.c \
diff --git a/src/tusb.h b/src/tusb.h
index cdf6f8171..2e98dc137 100644
--- a/src/tusb.h
+++ b/src/tusb.h
@@ -28,6 +28,10 @@
#if CFG_TUH_ENABLED
#include "host/usbh.h"
+ #if CFG_TUH_AUDIO
+ #include "class/audio/audio_host.h"
+ #endif
+
#if CFG_TUH_HID
#include "class/hid/hid_host.h"
#endif
diff --git a/src/tusb_option.h b/src/tusb_option.h
index 1eb23fb00..2aa0c0acf 100644
--- a/src/tusb_option.h
+++ b/src/tusb_option.h
@@ -866,6 +866,10 @@
{ 0x067b, 0x23f3 } /* GS */
#endif
+#ifndef CFG_TUH_AUDIO
+ #define CFG_TUH_AUDIO 0
+#endif
+
#ifndef CFG_TUH_HID
#define CFG_TUH_HID 0
#endif
diff --git a/test/hil/test/test_ci_metrics.py b/test/hil/test/test_ci_metrics.py
index 6f1511913..a502feb70 100644
--- a/test/hil/test/test_ci_metrics.py
+++ b/test/hil/test/test_ci_metrics.py
@@ -569,7 +569,7 @@ class TestWorkflowSelectionHandOff(unittest.TestCase):
break
finally:
os.chdir(cwd)
- self.assertEqual(diverging, {'imxrt', 'lpc11', 'lpc18', 'lpc54', 'mcx', 'rx',
+ self.assertEqual(diverging, {'imxrt', 'lpc11', 'lpc18', 'lpc54', 'mcx', 'nrf', 'rx',
'samd11', 'samd2x_l2x', 'samd5x_e5x', 'stm32l0',
'stm32l4', 'tm4c'},
'the set of families whose membrowse upload can land on an '
diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt
index a33af4563..06cff5b09 100644
--- a/test/unit-test/CMakeLists.txt
+++ b/test/unit-test/CMakeLists.txt
@@ -128,4 +128,19 @@ 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_TUH_AUDIO_PROTOCOLS=3
+ 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..599dabc21 100644
--- a/test/unit-test/project.yml
+++ b/test/unit-test/project.yml
@@ -127,10 +127,18 @@
# - 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_TUH_AUDIO_PROTOCOLS=3
+ - 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..101a0b58b
--- /dev/null
+++ b/test/unit-test/test/host/audio/test_audio_host.c
@@ -0,0 +1,2035 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2026 TinyUSB contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+#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")
+
+enum {
+ AUDIO_DEV_ADDR = 1,
+ AUDIO_AC_ITF = 0,
+ AUDIO_AS_ITF = 1,
+ AUDIO_AS_IN_ITF = 2,
+
+ PLAYBACK_INPUT_TERM = 1,
+ PLAYBACK_FU = 2,
+ PLAYBACK_OUTPUT_TERM = 3,
+
+ CAPTURE_INPUT_TERM = 11,
+ 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,
+};
+
+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;
+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];
+static uint8_t edpt_open_count;
+
+static bool edpt_close_result;
+static uint8_t closed_ep[4];
+static uint8_t edpt_close_count;
+
+static uint32_t edpt_busy_mask;
+static bool edpt_xfer_result;
+#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 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;
+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;
+}
+
+static void complete_edpt(uint8_t ep_addr) {
+ edpt_busy_mask &= ~edpt_mask(ep_addr);
+}
+
+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) {
+ 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;
+}
+
+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;
+ xfer->actual_len = control_sync_actual_len;
+ 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) {
+ complete_edpt(ep_addr);
+ }
+ 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;
+ interface_set_count++;
+ 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)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) {
+ 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;
+ const uint32_t mask = edpt_mask(ep_addr);
+ if (edpt_busy_mask & mask) {
+ return false;
+ }
+ edpt_busy_mask |= mask;
+ return true;
+}
+
+bool usbh_edpt_release(uint8_t dev_addr, uint8_t ep_addr) {
+ (void)dev_addr;
+ complete_edpt(ep_addr);
+ return true;
+}
+
+bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) {
+ (void)dev_addr;
+ return (edpt_busy_mask & edpt_mask(ep_addr)) != 0;
+}
+
+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_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
+
+#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_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
+
+#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_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, \
+ 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_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, \
+ 0, 0
+
+#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_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, \
+ 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_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)
+
+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 midi1_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 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,
+};
+
+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_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),
+ 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_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),
+ 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 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),
+ 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_ASYNCHRONOUS | 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),
+ 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 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),
+ 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 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),
+ 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, 3, 24, 32000),
+ TEST_UAC1_CS_DATA_EP,
+ 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_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),
+ TEST_UAC1_AS_ALT0,
+ TEST_UAC1_AS_INTERFACE(1, 1),
+ TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM),
+ 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,
+};
+
+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 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),
+ 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, 48000, 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 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),
+ 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;
+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) {
+ 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);
+}
+
+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);
+}
+
+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++) {
+ 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) {
+ test_speed = TUSB_SPEED_FULL;
+
+ interface_set_result = true;
+ memset(&interface_xfer, 0, sizeof(interface_xfer));
+ interface_alt = 0;
+ interface_set_count = 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;
+ control_sync_actual_len = 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_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));
+ edpt_xfer_count = 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;
+ 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;
+ fu_cb_result = XFER_RESULT_INVALID;
+
+ 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 complete_mount(void) {
+ 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;
+}
+
+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)));
+ 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_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));
+
+ 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) {
+ 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_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));
+ 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_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, 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_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));
+ 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, 0, feature_unit_complete, 0));
+
+ int16_t volume = 0;
+ 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));
+ TEST_ASSERT_EQUAL_INT16(-12 * 256, volume);
+ 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));
+ 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_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));
+ 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));
+ 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));
+
+ 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_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));
+
+ 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));
+
+ 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_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);
+ 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_TRUE(tuh_audio_mute_supported(0, 0));
+
+ 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
+ // 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);
+ 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);
+ }
+
+ 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_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_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) {
+ 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));
+
+ 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);
+ 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));
+ 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_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, 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, 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, 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) {
+ 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, 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_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, 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));
+ 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));
+
+ 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);
+}
+
+void test_audio_host_parses_discrete_frequencies_with_interval_greater_than_one(void) {
+ tuh_audio_stream_config_t config;
+ mount_descriptors(playback_with_two_frequencies, sizeof(playback_with_two_frequencies));
+
+ 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);
+
+ // 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));
+
+ 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_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));
+
+ 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_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));
+
+ 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);
+}
+
+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));
+ 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));
+ TEST_ASSERT_EQUAL_UINT8(1, edpt_close_count);
+ TEST_ASSERT_EQUAL_HEX8(0x01, closed_ep[0]);
+ 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]));
+
+ 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]);
+
+ TEST_ASSERT_EQUAL_UINT8(3, edpt_open_count);
+ TEST_ASSERT_EQUAL_UINT8(TUSB_INDEX_INVALID_8, tuh_audio_active_config(0, 0));
+}
+
+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));
+
+ 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_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) {
+ 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));
+}
+
+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_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_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, 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, 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));
+ 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, 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, 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, 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, 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));
+
+ 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);
+ TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count);
+
+ 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_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]));
+ }
+
+ 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_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));
+ 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));
+
+ 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) {
+ 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);
+ 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;
+ }
+}
+
+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));
+
+ 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));
+
+ 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) {
+ 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) {
+ 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++) {
+ 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];
+ 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));
+ 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));
+ 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);
+ 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));
+ 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);
+ TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[2]);
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(sample, edpt_xfer_data[2], sizeof(sample));
+}
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