diff options
Diffstat (limited to 'examples/host')
| -rw-r--r-- | examples/host/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | examples/host/audio_host/CMakeLists.txt | 30 | ||||
| -rw-r--r-- | examples/host/audio_host/CMakePresets.json | 6 | ||||
| -rw-r--r-- | examples/host/audio_host/Makefile | 14 | ||||
| -rw-r--r-- | examples/host/audio_host/README.md | 122 | ||||
| -rw-r--r-- | examples/host/audio_host/only.txt | 27 | ||||
| -rw-r--r-- | examples/host/audio_host/src/app.h | 27 | ||||
| -rw-r--r-- | examples/host/audio_host/src/audio_app.c | 548 | ||||
| -rw-r--r-- | examples/host/audio_host/src/main.c | 48 | ||||
| -rw-r--r-- | examples/host/audio_host/src/tusb_config.h | 106 |
10 files changed, 929 insertions, 0 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_ */ |
