diff --git a/examples/host/audio_host/CMakeLists.txt b/examples/host/audio_host/CMakeLists.txt new file mode 100644 index 0000000000..0891f58298 --- /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/Makefile b/examples/host/audio_host/Makefile new file mode 100644 index 0000000000..5c2e231847 --- /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 0000000000..072adbbf94 --- /dev/null +++ b/examples/host/audio_host/README.md @@ -0,0 +1,98 @@ +# USB Audio Host Example + +This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) to communicate with a UAC 1.0 compatible USB Audio Device. + +## Features + +- Enumerates and mounts USB Audio Class 1.0 devices +- Receives audio data from IN endpoint (e.g., microphone) +- Sends audio data to OUT endpoint (e.g., speaker) +- Sets sampling frequency via control requests +- Demonstrates isochronous transfer handling + +## Supported Devices + +This example supports any UAC 1.0 compliant USB Audio device, such as: +- USB microphones +- USB speakers/headphones +- USB audio interfaces + +## Building + +### Using CMake (recommended) + +```bash +cd examples/host/audio_host +mkdir -p build && cd build +cmake -DBOARD= -G Ninja .. +cmake --build . +``` + +Replace `` with your target board name (e.g., `raspberry_pi_pico`, `stm32f407disco`, etc.) + +### Using Make + +```bash +cd examples/host/audio_host +make BOARD= all +``` + +## Flashing + +```bash +# Using CMake +ninja flash + +# Using Make +make BOARD= flash +``` + +## Usage + +1. Build and flash the example to your board +2. Connect a USB Audio device (UAC 1.0) to the USB host port +3. Open a serial terminal to view output +4. The example will: + - Print device information when mounted + - Set sampling frequency based on the device's advertised capabilities + - Receive audio samples from the device (IN endpoint) + - Loop back received audio to the device (OUT endpoint) for testing + +## Serial Output Example + +``` +TinyUSB Host USB Audio Example +Connect a USB Audio Device (UAC 1.0) to test +Audio device mounted: idx=0, daddr=1 + --- Microphone --- + IN EP: 0x81 (max size: 192) + Input Terminal: ID=1, Type=0x0201, Channels=1 + Format Type: 1, Channels: 1, SubFrameSize: 2, BitResolution: 16 + Sampling Freq: Discrete, count=4 + Freq[0]: 44100 Hz + Freq[1]: 48000 Hz + Freq[2]: 96000 Hz + Freq[3]: 192000 Hz + --- Speaker --- + OUT EP: 0x02 (max size: 192) + Output Terminal: ID=2, Type=0x0301 + Format Type: 1, Channels: 2, SubFrameSize: 2, BitResolution: 16 + Sampling Freq: Continuous range 8000 Hz - 48000 Hz + Feature Unit: ID=3, SourceID=1 + Setting IN sampling frequency to 48000 Hz + Setting OUT sampling frequency to 48000 Hz + Sampling frequency set OK, ready for isochronous transfer +``` + +## Configuration + +Edit `src/tusb_config.h` to modify: +- `CFG_TUH_AUDIO_MAX`: Maximum number of audio devices supported +- `CFG_TUH_AUDIO_EPIN_BUFSIZE`: IN endpoint buffer size +- `CFG_TUH_AUDIO_EPOUT_BUFSIZE`: OUT endpoint buffer size + +## Notes + +- This example uses isochronous transfers which require precise timing +- For production applications, synchronize audio transfers with the device's audio clock +- The example sends a simple sine wave for testing; replace with actual audio data in real applications diff --git a/examples/host/audio_host/only.txt b/examples/host/audio_host/only.txt new file mode 100644 index 0000000000..a2ff93be53 --- /dev/null +++ b/examples/host/audio_host/only.txt @@ -0,0 +1,32 @@ +family:hpmicro +family:samd21 +family:samd5x_e5x +mcu:CH32V20X +mcu:KINETIS_KL +mcu:LPC175X_6X +mcu:LPC177X_8X +mcu:LPC18XX +mcu:LPC40XX +mcu:LPC43XX +mcu:LPC54 +mcu:LPC55 +mcu:MAX3421 +mcu:MIMXRT10XX +mcu:MIMXRT11XX +mcu:MIMXRT1XXX +mcu:MSP432E4 +mcu:RAXXX +mcu:RP2040 +mcu:RW61X +mcu:RX65X +mcu:STM32C0 +mcu:STM32C5 +mcu:STM32F4 +mcu:STM32F7 +mcu:STM32G0 +mcu:STM32H5 +mcu:STM32H7 +mcu:STM32H7RS +mcu:STM32N6 +mcu:STM32U3 +mcu:STM32U5 diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h new file mode 100644 index 0000000000..a807aeaa65 --- /dev/null +++ b/examples/host/audio_host/src/app.h @@ -0,0 +1,26 @@ +/* + * 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 +#include +#include + +void audio_app_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 0000000000..d9134f0896 --- /dev/null +++ b/examples/host/audio_host/src/audio_app.c @@ -0,0 +1,221 @@ +/* + * 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 +#include "bsp/board_api.h" +#include "tusb.h" +#include "app.h" + +//--------------------------------------------------------------------+ +// MACRO TYPEDEF CONSTANT ENUM DECLARATION +//--------------------------------------------------------------------+ + +static bool audio_mounted = false; +static uint8_t audio_dev_addr = 0xFF; +static volatile bool audio_ready = false; // Wait for sampling freq set before starting isochronous transfer +static volatile bool audio_rx_busy = false; // Track IN endpoint transfer state +static volatile bool audio_tx_busy = false; // Track OUT endpoint transfer state +static uint8_t audio_idx = 0xFF; +static uint8_t audiostream_in_idx = 0xFF; +static uint8_t audiostream_out_idx = 0xFF; +static uint32_t sampling_freq = 48000; // Default sampling frequency (Hz) +static uint8_t audio_mic_channels = 1; + +static uint8_t audio_rx_buffer[CFG_TUH_AUDIO_EPIN_BUFSIZE] __attribute__((aligned(4))); +static uint8_t audio_tx_buffer[CFG_TUH_AUDIO_EPOUT_BUFSIZE] __attribute__((aligned(4))); + +//--------------------------------------------------------------------+ +// Helper Functions +//--------------------------------------------------------------------+ + +// Mono (96 bytes, 48 samples) -> Stereo (192 bytes) +static void mono_to_stereo(const uint8_t *mono, uint8_t *stereo, uint16_t mono_samples) { + for (uint16_t i = 0; i < mono_samples; i++) { + // Copy 2 bytes (one int16 sample) to left channel + stereo[i * 4] = mono[i * 2]; + stereo[i * 4 + 1] = mono[i * 2 + 1]; + // Copy same 2 bytes to right channel + stereo[i * 4 + 2] = mono[i * 2]; + stereo[i * 4 + 3] = mono[i * 2 + 1]; + } +} + +// Print sampling frequency info for an AS interface +static void print_sampling_freq(const tuh_audio_as_info_t *as) { + if (as->sam_freq_type == 0) { + printf(" Sampling Freq: Continuous range %lu Hz - %lu Hz\r\n", (unsigned long)as->sam_freq_lower, + (unsigned long)as->sam_freq_upper); + } else { + printf(" Sampling Freq: Discrete, count=%u\r\n", as->sam_freq_type); + for (uint8_t j = 0; j < as->sam_freq_type && j < CFG_TUH_AUDIO_MAX_SAM_FREQ; j++) { + printf(" Freq[%u]: %lu Hz\r\n", j, (unsigned long)as->sam_freq[j]); + } + } +} + +// Print all AS interface info +static void print_as_interfaces(uint8_t idx) { + tuh_audio_as_info_t as = {}; + uint8_t as_count = tuh_audio_as_get_count(idx); + for (uint8_t i = 0; i < as_count; i++) { + tuh_audio_as_get_info(idx, i, &as); + if (as.ep_dir == TUSB_DIR_IN) { + // Save microphone channel count for mono-to-stereo conversion + audio_mic_channels = as.num_channels; + printf(" --- Microphone (AS %u) ---\r\n", i); + printf(" IN EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); + } else { + printf(" --- Speaker (AS %u) ---\r\n", i); + printf(" OUT EP: 0x%02x (max size: %u)\r\n", as.ep_addr, as.ep_size); + } + printf(" Interface: %u, Alt: %u\r\n", as.interface_num, as.alt_setting); + printf(" Format Type: %u, Channels: %u, SubFrameSize: %u, BitResolution: %u\r\n", as.format_type, + as.num_channels, as.sub_frame_size, as.bit_resolution); + print_sampling_freq(&as); + } +} + +//--------------------------------------------------------------------+ +// Application Task +//--------------------------------------------------------------------+ +void audio_app_task(void) { + if (!audio_mounted || !audio_ready) { + return; + } + + if (!audio_rx_busy) { + if (tuh_audio_receive(audio_idx, audiostream_in_idx, audio_rx_buffer, CFG_TUH_AUDIO_EPIN_BUFSIZE)) { + audio_rx_busy = true; + } + } +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + + +void tuh_audio_mount_cb(uint8_t idx) { + if (idx >= CFG_TUH_AUDIO_MAX) { + printf("Audio device mount failed: idx=%u exceeds max=%u\r\n", idx, CFG_TUH_AUDIO_MAX); + return; + } + + print_as_interfaces(idx); + + // Save device info + audio_dev_addr = tuh_audio_get_dev_addr(idx); + audio_idx = idx; + audio_mounted = true; + + // Find endpoints and IN sampling frequency + tuh_audio_as_info_t as; + for (uint8_t i = 0; i < tuh_audio_as_get_count(idx); i++) { + + tuh_audio_as_get_info(idx, i, &as); + if (as.ep_dir == TUSB_DIR_IN) { + audiostream_in_idx = i; + if (as.sam_freq_type > 0) { + sampling_freq = as.sam_freq[0]; + } + } else { + audiostream_out_idx = i; + } + } + + // Set IN sampling frequency before starting isochronous transfer + if (audiostream_in_idx != 0xFF && sampling_freq != 0) { + printf(" Setting IN sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); + // tuh_audio_set_sampling_freq(audio_idx, audiostream_in_idx, sampling_freq, in_sampling_freq_set_cb, 0); + + tusb_xfer_result_t result; + result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_in_idx, sampling_freq); + if (result == XFER_RESULT_SUCCESS) { + tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_in_idx, &sampling_freq); + printf(" IN sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); + if (audiostream_out_idx != 0xFF) { + printf(" Setting OUT sampling frequency to %lu Hz\r\n", (unsigned long)sampling_freq); + result = tuh_audio_set_sampling_freq_sync(audio_idx, audiostream_out_idx, sampling_freq); + if (result == XFER_RESULT_SUCCESS) { + tuh_audio_get_sampling_freq_sync(audio_idx, audiostream_out_idx, &sampling_freq); + printf(" OUT sampling frequency set to %lu Hz\r\n", (unsigned long)sampling_freq); + } else { + printf(" Setting OUT sampling frequency FAILED: result=%u\r\n", result); + } + } + } else { + printf(" Setting IN sampling frequency FAILED: result=%u\r\n", result); + } + uint16_t volume = 0x0600; + + result = tuh_audio_feature_unit_set_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, volume); + if (result == XFER_RESULT_SUCCESS) { + printf(" Feature Unit volume set:volume 0x%04x\r\n", (unsigned int)volume); + tuh_audio_feature_unit_get_sync(audio_idx, AUDIO10_FU_CTRL_VOLUME, 0, &volume); + printf(" Feature Unit volume get: 0x%04x\r\n", (unsigned int)volume); + } else { + printf(" Setting Feature Unit volume FAILED: result=%u\r\n", result); + } + } + audio_ready = true; +} + +// 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 (audio_mounted && audio_idx == idx) { + audio_mounted = false; + audio_ready = false; + audio_rx_busy = false; + audio_tx_busy = false; + audio_dev_addr = 0; + audio_idx = 0; + audiostream_in_idx = 0xFF; + audiostream_out_idx = 0xFF; + } +} + +// Invoked when an isochronous IN transfer is complete +void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)dev_addr; + (void)ep_addr; + audio_rx_busy = false; + + if (xferred_bytes > 0 && audiostream_out_idx != 0xFF && !audio_tx_busy) { + bool ok; + if (audio_mic_channels == 1) { + // Mono microphone, convert to stereo and send to OUT endpoint + uint16_t samples = xferred_bytes / 2; + mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); + ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_tx_buffer, xferred_bytes * 2); + } else { + // Stereo microphone, send directly to OUT endpoint + ok = tuh_audio_send(audio_idx, audiostream_out_idx, audio_rx_buffer, xferred_bytes); + } + + if (ok) { + audio_tx_busy = true; + } + } +} + +// Invoked when an isochronous OUT transfer is complete +void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)dev_addr; + (void)ep_addr; + (void)xferred_bytes; + audio_tx_busy = false; +} diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c new file mode 100644 index 0000000000..b80cd29386 --- /dev/null +++ b/examples/host/audio_host/src/main.c @@ -0,0 +1,73 @@ +/* + * 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 +#include +#include + +#include "bsp/board_api.h" +#include "tusb.h" +#include "app.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF PROTYPES +//--------------------------------------------------------------------+ +void led_blinking_task(void); + +/*------------- MAIN -------------*/ +int main(void) { + board_init(); + + printf("TinyUSB Host USB Audio Example\r\n"); + printf("Connect a USB Audio Device (UAC 1.0) to test\r\n"); + + // 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(); + } +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +//--------------------------------------------------------------------+ +// Blinking Task +//--------------------------------------------------------------------+ +void led_blinking_task(void) { + const uint32_t interval_ms = 1000; + static uint32_t start_ms = 0; + + static bool led_state = false; + + // Blink every interval ms + if ( tusb_time_millis_api() - start_ms < interval_ms) return; // not enough time + start_ms += interval_ms; + + board_led_write(led_state); + led_state = 1 - led_state; // toggle +} 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 0000000000..4a7a6ad565 --- /dev/null +++ b/examples/host/audio_host/src/tusb_config.h @@ -0,0 +1,103 @@ +/* + * 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 1 +#define CFG_TUH_CDC 0 +#define CFG_TUH_HID 1 +#define CFG_TUH_MSC 0 +#define CFG_TUH_VENDOR 0 +#define CFG_TUH_AUDIO 1 + +// 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 2 +#define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 +#define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_CONFIG_H_ */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b3e05f60fb..b5a5a0b3b4 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 0000000000..c74a95d48a --- /dev/null +++ b/src/class/audio/audio_host.c @@ -0,0 +1,736 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * SPDX-License-Identifier: MIT + * + * This file is part of the TinyUSB stack. + */ + +/* + * This driver implements a USB Audio Host (UAC 1.0) class driver. + * It supports multiple Audio Streaming (AS) interfaces with independent format storage. + * Each AS interface can have its own sample rate, channel count, bit resolution, + * and endpoint configuration. + * + * The driver handles: + * 1. Audio Control (AC) interface parsing — Input Terminal, Output Terminal, + * and Feature Unit descriptors. + * 2. Audio Streaming (AS) interface enumeration — multiple AS interfaces with + * alternate settings, each storing its own format information. + * 3. Isochronous IN/OUT endpoint management for audio data transfer. + * 4. Asynchronous control transfers for sample frequency get/set. + * + * In case you need to adjust the number of supported AS interfaces, change + * CFG_TUH_AUDIO_MAX_AS in your tusb_config.h. + * + * */ + +#include "tusb_option.h" + +#if (CFG_TUH_ENABLED && CFG_TUH_AUDIO) + + #include "host/usbh.h" + #include "host/usbh_pvt.h" + #include "audio_host.h" + + // Level where CFG_TUSB_DEBUG must be at least for this driver is logged + #ifndef CFG_TUH_AUDIO_LOG_LEVEL + #define CFG_TUH_AUDIO_LOG_LEVEL CFG_TUH_LOG_LEVEL + #endif + + #define TU_LOG_DRV(...) TU_LOG(CFG_TUH_AUDIO_LOG_LEVEL, __VA_ARGS__) + +//--------------------------------------------------------------------+ +// Weak stubs: invoked if no strong implementation is available +//--------------------------------------------------------------------+ + + +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_rx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)idx; + (void)ep_addr; + (void)xferred_bytes; +} + +TU_ATTR_WEAK void tuh_audio_tx_cb(uint8_t idx, uint8_t ep_addr, uint16_t xferred_bytes) { + (void)idx; + (void)ep_addr; + (void)xferred_bytes; +} + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF +//--------------------------------------------------------------------+ + + +// Per-interface storage +typedef struct { + uint8_t daddr; // device address + uint8_t ac_itf_num; // Audio Control interface number + uint8_t itf_count; // number of interfaces (AC + AS) + + // Terminal info (from Audio Control Interface) + uint16_t input_terminal_type; // wTerminalType of Input Terminal + uint8_t input_terminal_id; // bTerminalID of Input Terminal + uint8_t input_terminal_channels; // bNrChannels of Input Terminal + uint16_t output_terminal_type; // wTerminalType of Output Terminal + uint8_t output_terminal_id; // bTerminalID of Output Terminal + + // Feature Unit info + uint8_t feature_unit_id; // bUnitID of Feature Unit (0 = none) + uint8_t feature_unit_source_id; // bSourceID of Feature Unit + + // Multiple AS interfaces support + uint8_t as_count; + uint8_t as_set_idx; + + // Per-AS interface independent storage (new) + tuh_audio_as_info_t as[CFG_TUH_AUDIO_MAX_AS]; // Array of Audio Streaming interface info structures + + bool mounted; +} audioh_interface_t; + +typedef struct { + TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); + TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); + TUH_EPBUF_DEF(ctrl, 8); +} audioh_epbuf_t; + +static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; +static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; +//--------------------------------------------------------------------+ +// Helper +//--------------------------------------------------------------------+ +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 inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + const audioh_interface_t *p_audio = &_audioh_itf[idx]; + if (p_audio->daddr == daddr) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + if (p_audio->as[as_idx].ep_addr == ep_addr) { + return idx; + } + } + } + } + return TUSB_INDEX_INVALID_8; +} + +static uint8_t audioh_get_ep_addr_by_dir(const audioh_interface_t *p_audio, uint8_t dir) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + const tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + if (as->ep_addr != 0 && as->ep_dir == dir) { + return as->ep_addr; + } + } + + return 0; +} + +//--------------------------------------------------------------------+ +// USBH API +//--------------------------------------------------------------------+ +bool audioh_init(void) { + tu_memclr(&_audioh_itf, sizeof(_audioh_itf)); + return true; +} + +bool audioh_deinit(void) { + 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) { + TU_LOG_DRV(" AUDIO close addr = %u index = %u\r\n", daddr, idx); + tuh_audio_umount_cb(idx); + + p_audio->ac_itf_num = 0; + p_audio->daddr = 0; + p_audio->mounted = false; + p_audio->as_count = 0; + p_audio->as_set_idx = 0; + tu_memclr(p_audio->as, sizeof(p_audio->as)); + } + } +} + +bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + (void)result; + if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { + tuh_audio_rx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); + } else { + tuh_audio_tx_cb(dev_addr, ep_addr, (uint16_t)xferred_bytes); + } + + return true; +} + +//--------------------------------------------------------------------+ +// Enumeration +//--------------------------------------------------------------------+ +uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { + (void)rhport; + + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + TU_VERIFY(AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass, 0); + + 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; + + 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->itf_count = 0; + + // Parse Audio Control Interface + TU_LOG_DRV("AUDIO opening AC Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); + p_audio->ac_itf_num = desc_itf->bInterfaceNumber; + p_audio->itf_count = 1; + + // Parse Audio Control interface descriptors (Input Terminal, Output Terminal, Feature Unit, etc.) + p_desc = tu_desc_next(p_desc); + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AC_INTERFACE_INPUT_TERMINAL: { + const audio10_desc_input_terminal_t *desc_input_terminal = (const audio10_desc_input_terminal_t *)p_desc; + p_audio->input_terminal_type = tu_le16toh(desc_input_terminal->wTerminalType); + p_audio->input_terminal_id = desc_input_terminal->bTerminalID; + p_audio->input_terminal_channels = desc_input_terminal->bNrChannels; + TU_LOG_DRV(" Input Terminal: ID=%u, Type=0x%04x, Channels=%u\r\n", desc_input_terminal->bTerminalID, + tu_le16toh(desc_input_terminal->wTerminalType), desc_input_terminal->bNrChannels); + break; + } + case AUDIO10_CS_AC_INTERFACE_OUTPUT_TERMINAL: { + const audio10_desc_output_terminal_t *desc_output_terminal = (const audio10_desc_output_terminal_t *)p_desc; + p_audio->output_terminal_type = tu_le16toh(desc_output_terminal->wTerminalType); + p_audio->output_terminal_id = desc_output_terminal->bTerminalID; + TU_LOG_DRV(" Output Terminal: ID=%u, Type=0x%04x\r\n", desc_output_terminal->bTerminalID, + tu_le16toh(desc_output_terminal->wTerminalType)); + break; + } + case AUDIO10_CS_AC_INTERFACE_FEATURE_UNIT: { + const uint8_t *desc_feature_unit = p_desc; + p_audio->feature_unit_id = desc_feature_unit[3]; // bUnitID + p_audio->feature_unit_source_id = desc_feature_unit[4]; // bSourceID + TU_LOG_DRV(" Feature Unit: ID=%u, SourceID=%u\r\n", desc_feature_unit[3], desc_feature_unit[4]); + break; + } + default: + break; + } + } + p_desc = tu_desc_next(p_desc); + } + + // Parse all remaining descriptors in this configuration looking for Audio Streaming interfaces + while (tu_desc_in_bounds(p_desc, desc_end)) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + const tusb_desc_interface_t *desc_interface = (const tusb_desc_interface_t *)p_desc; + // Stop at the first non-Audio interface so we don't claim the rest of the configuration + if (desc_interface->bInterfaceClass != TUSB_CLASS_AUDIO) { + break; + } + if (desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { + // Found Audio Streaming Interface + TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", desc_interface->bInterfaceNumber, + desc_interface->bAlternateSetting); + + if (desc_interface->bAlternateSetting == 0) { + // Interface descriptor with alt setting 0 (no endpoints) + // Add to AS entries + if (p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { + p_audio->as[p_audio->as_count].interface_num = desc_interface->bInterfaceNumber; + p_audio->as[p_audio->as_count].alt_setting = 0; + p_audio->as_count++; + } else { + TU_LOG_DRV(" Skip AS Interface %u: reach CFG_TUH_AUDIO_MAX_AS=%u\r\n", desc_interface->bInterfaceNumber, + CFG_TUH_AUDIO_MAX_AS); + } + } else if (desc_interface->bNumEndpoints > 0) { + // Interface descriptor with alt setting > 0 (has endpoints) + // Find matching AS entry and set alt_setting + uint8_t as_entry_idx = CFG_TUH_AUDIO_MAX_AS; + for (uint8_t i = 0; i < p_audio->as_count; i++) { + if (p_audio->as[i].interface_num == desc_interface->bInterfaceNumber) { + as_entry_idx = i; + break; + } + } + if (as_entry_idx >= CFG_TUH_AUDIO_MAX_AS && p_audio->as_count < CFG_TUH_AUDIO_MAX_AS) { + as_entry_idx = p_audio->as_count; + p_audio->as[as_entry_idx].interface_num = desc_interface->bInterfaceNumber; + p_audio->as_count++; + } + if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { + p_audio->as[as_entry_idx].alt_setting = desc_interface->bAlternateSetting; + } + + // Parse the interface's descriptors + p_desc = tu_desc_next(p_desc); + // Temporary variables to hold format info until endpoint direction is known + uint8_t tmp_format_type = 0; + uint8_t tmp_num_channels = 0; + uint8_t tmp_sub_frame_size = 0; + uint8_t tmp_bit_resolution = 0; + uint8_t tmp_sam_freq_type = 0; + uint32_t tmp_sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ] = {0}; + uint32_t tmp_sam_freq_lower = 0; + uint32_t tmp_sam_freq_upper = 0; + while (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) != TUSB_DESC_INTERFACE) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_CS_INTERFACE: { + switch (tu_desc_subtype(p_desc)) { + case AUDIO10_CS_AS_INTERFACE_AS_GENERAL: { + TU_LOG_DRV(" AS General descriptor\r\n"); + break; + } + case AUDIO10_CS_AS_INTERFACE_FORMAT_TYPE: { + TU_LOG_DRV(" Format Type descriptor\r\n"); + TU_ASSERT(p_desc[0] >= 8, 0); + // Parse UAC 1.0 Format Type I descriptor fields into temporary variables + tmp_format_type = p_desc[3]; // bFormatType + tmp_num_channels = p_desc[4]; // bNrChannels + tmp_sub_frame_size = p_desc[5]; // bSubFrameSize + tmp_bit_resolution = p_desc[6]; // bBitResolution + + // Parse sampling frequencies + uint8_t bLength = p_desc[0]; + if (bLength >= 8) { + tmp_sam_freq_type = p_desc[7]; // bSamFreqType + if (tmp_sam_freq_type == 0) { + // Continuous range: tLowerSamFreq, tUpperSamFreq (3 bytes each) + if (bLength >= 14) { + tmp_sam_freq_lower = + ((uint32_t)p_desc[8] | ((uint32_t)p_desc[9] << 8) | ((uint32_t)p_desc[10] << 16)); + tmp_sam_freq_upper = + ((uint32_t)p_desc[11] | ((uint32_t)p_desc[12] << 8) | ((uint32_t)p_desc[13] << 16)); + } + } else { + // Discrete sampling frequencies + uint8_t max_freqs = tmp_sam_freq_type < CFG_TUH_AUDIO_MAX_SAM_FREQ ? tmp_sam_freq_type + : CFG_TUH_AUDIO_MAX_SAM_FREQ; + for (uint8_t i = 0; i < max_freqs && (8 + i * 3 + 2) < bLength; i++) { + tmp_sam_freq[i] = ((uint32_t)p_desc[8 + i * 3] | ((uint32_t)p_desc[9 + i * 3] << 8) | + ((uint32_t)p_desc[10 + i * 3] << 16)); + } + } + } + break; + } + default: + break; + } + break; + } + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; + if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { + TU_LOG_DRV(" Isochronous EP %02x\r\n", desc_endpoint->bEndpointAddress); + if (tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN) { + // Save to per-AS storage + if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { + tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); + as->ep_dir = TUSB_DIR_IN; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; + as->sub_frame_size = tmp_sub_frame_size; + as->bit_resolution = tmp_bit_resolution; + as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_lower = tmp_sam_freq_lower; + as->sam_freq_upper = tmp_sam_freq_upper; + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { + as->sam_freq[i] = tmp_sam_freq[i]; + } + } + } else { + // Save to per-AS storage + if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { + tuh_audio_as_info_t *as = &p_audio->as[as_entry_idx]; + as->ep_addr = desc_endpoint->bEndpointAddress; + as->ep_size = tu_edpt_packet_size(desc_endpoint); + as->ep_dir = TUSB_DIR_OUT; + as->format_type = tmp_format_type; + as->num_channels = tmp_num_channels; + as->sub_frame_size = tmp_sub_frame_size; + as->bit_resolution = tmp_bit_resolution; + as->sam_freq_type = tmp_sam_freq_type; + as->sam_freq_lower = tmp_sam_freq_lower; + as->sam_freq_upper = tmp_sam_freq_upper; + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { + as->sam_freq[i] = tmp_sam_freq[i]; + } + } + } + TU_ASSERT(tuh_edpt_open(dev_addr, desc_endpoint), 0); + } + break; + } + default: + break; + } + p_desc = tu_desc_next(p_desc); + } + // Continue to parse other AS interfaces (don't break, device may have both IN and OUT) + // break; // Removed: allow parsing multiple AS interfaces (e.g. mic + speaker) + continue; + } + p_audio->itf_count++; + } else if (desc_interface->bInterfaceClass == TUSB_CLASS_AUDIO && + desc_interface->bInterfaceSubClass == AUDIO_SUBCLASS_CONTROL) { + // Another Audio Control interface (shouldn't happen in normal UAC 1.0) + p_audio->itf_count++; + } + } + p_desc = tu_desc_next(p_desc); + } + + p_audio->daddr = dev_addr; + + return (uint16_t)((uintptr_t)p_desc - (uintptr_t)desc_start); +} + +static void _audioh_mount(uint8_t dev_addr, uint8_t idx); + +static void audioh_set_interface_complete(tuh_xfer_t *xfer) { + uint8_t idx = (uint8_t)xfer->user_data; + audioh_interface_t *p_audio = &_audioh_itf[idx]; + + // Send SET_INTERFACE for next AS interface if any + p_audio->as_set_idx++; + if (p_audio->as_set_idx < p_audio->as_count) { + uint8_t as_idx = p_audio->as_set_idx; + uint8_t itf = p_audio->as[as_idx].interface_num; + uint8_t alt = p_audio->as[as_idx].alt_setting; + if (alt > 0) { + TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, xfer->daddr); + tuh_interface_set(xfer->daddr, itf, alt, audioh_set_interface_complete, idx); + return; + } + } + + // All SET_INTERFACE done, mount the device + _audioh_mount(xfer->daddr, idx); +} + +static void _audioh_mount(uint8_t dev_addr, uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + p_audio->mounted = true; + + + tuh_audio_mount_cb(idx); + + usbh_driver_set_config_complete(dev_addr, p_audio->ac_itf_num); +} + +bool audioh_set_config(uint8_t dev_addr, uint8_t itf_num) { + uint8_t idx = tuh_audio_itf_get_index(dev_addr, itf_num); + + // If not found, check if this is an AS interface that belongs to a known AC interface + if (idx >= CFG_TUH_AUDIO_MAX) { + for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { + const audioh_interface_t *p_audio = &_audioh_itf[i]; + if (p_audio->daddr == dev_addr) { + for (uint8_t as_idx = 0; as_idx < p_audio->as_count; as_idx++) { + if (p_audio->as[as_idx].interface_num == itf_num) { + // AS interface: configuration is driven by the AC interface, so just pass through + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + } + } + } + // Not an Audio interface we own; pass through so enumeration can continue + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio->as_count <= CFG_TUH_AUDIO_MAX_AS, false); + + // Send SET_INTERFACE for all AS interfaces with alt_setting > 0 + if (p_audio->as_count > 0) { + p_audio->as_set_idx = 0; + uint8_t itf = p_audio->as[0].interface_num; + uint8_t alt = p_audio->as[0].alt_setting; + if (alt > 0) { + TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, dev_addr); + tuh_interface_set(dev_addr, itf, alt, audioh_set_interface_complete, idx); + return true; + } + } + + _audioh_mount(dev_addr, idx); + return true; +} + +//--------------------------------------------------------------------+ +// Application API +//--------------------------------------------------------------------+ +bool tuh_audio_mounted(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->mounted; +} + +uint8_t tuh_audio_get_dev_addr(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->daddr; +} + + +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + return p_audio->feature_unit_id; +} + +uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num) { + for (uint8_t idx = 0; idx < CFG_TUH_AUDIO_MAX; idx++) { + const audioh_interface_t *p_audio = &_audioh_itf[idx]; + if (p_audio->daddr == daddr && p_audio->ac_itf_num == itf_num) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { + audioh_interface_t *p_audio = &_audioh_itf[idx]; + TU_VERIFY(p_audio && info); + + info->daddr = p_audio->daddr; + + // re-construct descriptor + tusb_desc_interface_t *desc_interface = &info->desc; + desc_interface->bLength = sizeof(tusb_desc_interface_t); + desc_interface->bDescriptorType = TUSB_DESC_INTERFACE; + + uint8_t ep_in = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_IN); + uint8_t ep_out = audioh_get_ep_addr_by_dir(p_audio, TUSB_DIR_OUT); + + desc_interface->bInterfaceNumber = p_audio->ac_itf_num; + desc_interface->bAlternateSetting = 0; + desc_interface->bNumEndpoints = (uint8_t)((ep_in ? 1u : 0u) + (ep_out ? 1u : 0u)); + desc_interface->bInterfaceClass = TUSB_CLASS_AUDIO; + desc_interface->bInterfaceSubClass = AUDIO_SUBCLASS_CONTROL; + desc_interface->bInterfaceProtocol = 0; + desc_interface->iInterface = 0; + + return true; +} + +//--------------------------------------------------------------------+ +// Control Endpoint API +//--------------------------------------------------------------------+ +bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, 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 && as_idx < p_audio->as_count, false); + + uint8_t ep_addr = p_audio->as[as_idx].ep_addr; + uint8_t daddr = p_audio->daddr; + uint8_t *freq_buf = _audioh_epbuf[idx].ctrl; + + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t)ep_addr), + .wLength = 3}; + + // UAC 1.0 sampling frequency is 3 bytes little-endian + // uint8_t freq_buf[3] = { + // (uint8_t)(sampling_freq & 0xFF), + // (uint8_t)((sampling_freq >> 8) & 0xFF), + // (uint8_t)((sampling_freq >> 16) & 0xFF) + // }; + freq_buf[0] = (uint8_t)(sampling_freq & 0xFF); + freq_buf[1] = (uint8_t)((sampling_freq >> 8) & 0xFF); + freq_buf[2] = (uint8_t)((sampling_freq >> 16) & 0xFF); + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = freq_buf, + .complete_cb = complete_cb, + .user_data = user_data}; + + return tuh_control_xfer(&xfer); +} + +bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, 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 && as_idx < p_audio->as_count && sampling_freq, false); + uint8_t ep_addr = p_audio->as[as_idx].ep_addr; + uint8_t daddr = p_audio->daddr; + + *sampling_freq = 0; + + const tusb_control_request_t request = + {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_ENDPOINT, .type = TUSB_REQ_TYPE_CLASS, .direction = TUSB_DIR_IN}, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 + .wIndex = tu_htole16((uint16_t)ep_addr), + .wLength = 3}; + + // Application needs to parse 3-byte little-endian sampling frequency from buffer + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)sampling_freq, + .complete_cb = complete_cb, + .user_data = user_data}; + + return tuh_control_xfer(&xfer); +} + +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + uint8_t daddr = _audioh_itf[idx].daddr; + uint8_t itf_num = _audioh_itf[idx].ac_itf_num; + uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_OUT}, + .bRequest = AUDIO10_CS_REQ_SET_CUR, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), + .wLength = 2}; + + uint8_t *val_buf = _audioh_epbuf[idx].ctrl; + val_buf[0] = (uint8_t)(value & 0xFF); + val_buf[1] = (uint8_t)((value >> 8) & 0xFF); + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = val_buf, + .complete_cb = complete_cb, + .user_data = user_data}; + + return tuh_control_xfer(&xfer); +} + +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *buffer, + tuh_xfer_cb_t complete_cb, uintptr_t user_data) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + uint8_t daddr = _audioh_itf[idx].daddr; + uint8_t itf_num = _audioh_itf[idx].ac_itf_num; + uint8_t unit_id = _audioh_itf[idx].feature_unit_id; + + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_CLASS, + .direction = TUSB_DIR_IN}, + .bRequest = AUDIO10_CS_REQ_GET_CUR, + .wValue = tu_htole16(tu_u16(control_selector, channel)), + .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), + .wLength = 2}; + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = (uint8_t *)buffer, + .complete_cb = complete_cb, + .user_data = user_data}; + + return tuh_control_xfer(&xfer); +} + +//--------------------------------------------------------------------+ +// Multi-AS interface API +//--------------------------------------------------------------------+ +uint8_t tuh_audio_as_get_count(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, 0); + return _audioh_itf[idx].as_count; +} + +bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); + TU_VERIFY(as_idx < _audioh_itf[idx].as_count, false); + TU_VERIFY(info, false); + + tuh_audio_as_info_t *as = &_audioh_itf[idx].as[as_idx]; + info->interface_num = as->interface_num; + info->alt_setting = as->alt_setting; + info->ep_addr = as->ep_addr; + info->ep_size = as->ep_size; + info->ep_dir = as->ep_dir; + info->format_type = as->format_type; + info->num_channels = as->num_channels; + info->sub_frame_size = as->sub_frame_size; + info->bit_resolution = as->bit_resolution; + info->sam_freq_type = as->sam_freq_type; + info->sam_freq_lower = as->sam_freq_lower; + info->sam_freq_upper = as->sam_freq_upper; + memcpy(info->sam_freq, as->sam_freq, sizeof(info->sam_freq)); + return true; +} + +//--------------------------------------------------------------------+ +// Isochronous Endpoint API +//--------------------------------------------------------------------+ +bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + TU_VERIFY(as->ep_addr != 0); + + return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, buffer, len); +} + +bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len) { + TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); + audioh_interface_t *p_audio = &_audioh_itf[idx]; + tuh_audio_as_info_t *as = &p_audio->as[as_idx]; + TU_VERIFY(as->ep_addr != 0); + + return usbh_edpt_xfer(p_audio->daddr, as->ep_addr, (uint8_t *)buffer, len); +} + +//--------------------------------------------------------------------+ +// Set Interface +//--------------------------------------------------------------------+ +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, + uintptr_t user_data) { + const tusb_control_request_t request = {.bmRequestType_bit = {.recipient = TUSB_REQ_RCPT_INTERFACE, + .type = TUSB_REQ_TYPE_STANDARD, + .direction = TUSB_DIR_OUT}, + .bRequest = TUSB_REQ_SET_INTERFACE, + .wValue = alt_setting, + .wIndex = itf_num, + .wLength = 0}; + + tuh_xfer_t xfer = {.daddr = daddr, + .ep_addr = 0, + .setup = &request, + .buffer = NULL, + .complete_cb = complete_cb, + .user_data = user_data}; + + return tuh_control_xfer(&xfer); +} + +#endif diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h new file mode 100644 index 0000000000..aafd58f43b --- /dev/null +++ b/src/class/audio/audio_host.h @@ -0,0 +1,183 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Zhenjiang Zhang + * 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 +//--------------------------------------------------------------------+ +// Maximum number of Audio interfaces per Audio device +#ifndef CFG_TUH_AUDIO_MAX + #define CFG_TUH_AUDIO_MAX 1 +#endif +// Maximum number of Audio Streaming interfaces per Audio device +#ifndef CFG_TUH_AUDIO_MAX_SAM_FREQ + #define CFG_TUH_AUDIO_MAX_SAM_FREQ 5 +#endif +// Maximum number of Audio Streaming interfaces per Audio device +#ifndef CFG_TUH_AUDIO_MAX_AS + #define CFG_TUH_AUDIO_MAX_AS 4 +#endif + +//--------------------------------------------------------------------+ +// AS Interface Info (per-interface independent storage) +//--------------------------------------------------------------------+ +typedef struct { + uint8_t interface_num; // AS interface number + uint8_t alt_setting; // Current alt setting + uint8_t ep_addr; // Endpoint address + uint16_t ep_size; // Max packet size + uint8_t ep_dir; // TUSB_DIR_IN or TUSB_DIR_OUT + + // Format info + uint8_t format_type; + uint8_t num_channels; + uint8_t sub_frame_size; + uint8_t bit_resolution; + uint8_t sam_freq_type; + uint32_t sam_freq[CFG_TUH_AUDIO_MAX_SAM_FREQ]; + uint32_t sam_freq_lower; + uint32_t sam_freq_upper; +} tuh_audio_as_info_t; + +#ifndef CFG_TUH_AUDIO_EPIN_BUFSIZE + #define CFG_TUH_AUDIO_EPIN_BUFSIZE 192 +#endif + +#ifndef CFG_TUH_AUDIO_EPOUT_BUFSIZE + #define CFG_TUH_AUDIO_EPOUT_BUFSIZE 192 +#endif + +//--------------------------------------------------------------------+ +// Application API +//--------------------------------------------------------------------+ + +// Check if Audio interface is mounted +bool tuh_audio_mounted(uint8_t idx); +// Get device address of Audio interface +uint8_t tuh_audio_get_dev_addr(uint8_t idx); +// Get Feature Unit ID +uint8_t tuh_audio_get_feature_unit_id(uint8_t idx); +// Get Interface index from device address + interface number +// return TUSB_INDEX_INVALID_8 (0xFF) if not found +uint8_t tuh_audio_itf_get_index(uint8_t daddr, uint8_t itf_num); + +// Get Interface information +// return true if index is correct and interface is currently mounted +bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info); + +// Get number of AS interfaces for an audio device +uint8_t tuh_audio_as_get_count(uint8_t idx); + +// Get AS interface info by index +// as_idx: 0 to (as_count - 1) +bool tuh_audio_as_get_info(uint8_t idx, uint8_t as_idx, tuh_audio_as_info_t *info); + +// Set Audio Streaming interface alternate setting (to enable/disable endpoints) +bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Control Endpoint API +//--------------------------------------------------------------------+ + +// Set current sampling frequency on an isochronous endpoint (UAC 1.0) +// Sampling frequency is 3 bytes little-endian +// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). +bool tuh_audio_set_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); + +// Get current sampling frequency from an isochronous endpoint (UAC 1.0) +// In multi-AS scenarios, pass the endpoint address from tuh_audio_as_get_info(). +bool tuh_audio_get_sampling_freq(uint8_t idx, uint8_t as_idx, uint32_t *sampling_freq, tuh_xfer_cb_t complete_cb, + uintptr_t user_data); + +// Set current/mute/volume etc. for a feature unit (UAC 1.0) +bool tuh_audio_feature_unit_set(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +// Get current/mute/volume etc. from a feature unit (UAC 1.0) +bool tuh_audio_feature_unit_get(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + +//--------------------------------------------------------------------+ +// Control Request Sync API +// Each Function will make a USB control transfer request to/from device the function will block until request is +// complete. The function will return the transfer request result +//--------------------------------------------------------------------+ +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_get_sampling_freq_sync(uint8_t idx, uint8_t as_idx, + uint32_t *sampling_freq) { + TU_API_SYNC(tuh_audio_get_sampling_freq, idx, as_idx, sampling_freq); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t tuh_audio_set_sampling_freq_sync(uint8_t idx, uint8_t as_idx, + uint32_t sampling_freq) { + TU_API_SYNC(tuh_audio_set_sampling_freq, idx, as_idx, sampling_freq); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t +tuh_audio_feature_unit_set_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t value) { + TU_API_SYNC(tuh_audio_feature_unit_set, idx, control_selector, channel, value); +} + +TU_ATTR_ALWAYS_INLINE static inline tusb_xfer_result_t +tuh_audio_feature_unit_get_sync(uint8_t idx, uint8_t control_selector, uint8_t channel, uint16_t *value) { + TU_API_SYNC(tuh_audio_feature_unit_get, idx, control_selector, channel, value); +} + +//--------------------------------------------------------------------+ +// Interrupt/Isochronous Endpoint API +//--------------------------------------------------------------------+ + +// Submit an isochronous transfer to receive audio data from a default IN endpoint. +// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. +// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. +bool tuh_audio_receive(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); + +// Submit an isochronous transfer to send audio data to a default OUT endpoint. +// In multi-AS scenarios, endpoint selection is implementation-defined default behavior. +// Use tuh_audio_as_get_info() when application needs explicit per-AS endpoint control. +bool tuh_audio_send(uint8_t idx, uint8_t as_idx, uint8_t *buffer, uint16_t len); + +//--------------------------------------------------------------------+ +// Callbacks (Weak is optional) +//--------------------------------------------------------------------+ + +// 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 is complete +void tuh_audio_rx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); + +// Invoked when an isochronous OUT transfer is complete +void tuh_audio_tx_cb(uint8_t dev_addr, uint8_t ep_addr, uint16_t xferred_bytes); + +//--------------------------------------------------------------------+ +// 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 e307bb5e5c..6e16c0b72b 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 3650439270..ab8549bc8a 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 6a30f7c133..2199516924 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 e19ee1629d..7a636657f2 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -807,6 +807,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