diff options
| -rw-r--r-- | examples/host/audio_host/README.md | 18 | ||||
| -rw-r--r-- | examples/host/audio_host/src/app.h | 3 | ||||
| -rw-r--r-- | examples/host/audio_host/src/audio_app.c | 152 | ||||
| -rw-r--r-- | examples/host/audio_host/src/main.c | 3 |
4 files changed, 78 insertions, 98 deletions
diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index e7140bf67..706752a05 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -7,9 +7,9 @@ This example demonstrates how to use TinyUSB's USB Audio Host driver (TUH_AUDIO) - Enumerates and mounts USB Audio Class 1.0 and 2.0 devices - Discovers the device's logical streams (capture/playback) and their supported configurations (discrete tuples only) - Reports each stream's master mute/volume capabilities and cached volume range -- Configures and starts an S16_LE capture stream (48 kHz preferred, 44.1 kHz fallback; stereo preferred, mono accepted) +- 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: `tuh_audio_read()` / `tuh_audio_write()` queue frames; the driver schedules transfers at the endpoint's polling interval +- 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 @@ -20,7 +20,7 @@ This example supports UAC1 devices whose Type I Format descriptor lists discrete - 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` (48 kHz stereo by default). Non-PCM formats are rejected by the driver. +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 @@ -69,10 +69,10 @@ make BOARD=<your_board> flash 3. Open a serial terminal to view output 4. The example will: - Print each stream's master mute/volume capabilities, cached volume range, and supported configurations when mounted - - Look for an S16_LE capture configuration at a preferred sample rate (48 kHz first, 44.1 kHz fallback; stereo preferred, mono accepted) and configure it + - Look for an S16_LE capture configuration at a preferred sample rate (44.1 kHz first, 48 kHz fallback; stereo preferred, mono accepted) and configure it - Echo captured audio to an S16_LE playback configuration at the same sample rate (same channel count preferred, converted otherwise) - Read/unmute the microphone and speaker Feature Units and set supported master volumes near -6 dB - - Drain the capture FIFO in `audio_app_task_read()` and queue the frames into the playback FIFO; a sine test tone plays on the playback stream when no capture stream is echoing + - Service both FIFOs from `audio_app_task()` at their half-full/half-drained watermarks; a sine test tone plays on the playback stream when no capture stream is echoing - Cycle through the three phases (mic-only / spk-only / echo, 5 s each) with `tuh_audio_start()` / `tuh_audio_stop()`; a failed stream is restarted automatically 100 ms after the error callback ## Serial Output Example @@ -91,12 +91,12 @@ Audio device mounted: idx=0 addr=1 master volume range: min=-23040 max=1536 res=256 (1/256 dB) [0] format=1 rate=44100 channels=2 [1] format=1 rate=48000 channels=2 - Configuring 48 kHz S16_LE capture (2 channels) + Configuring 44100 S16_LE capture (2 channels) Microphone configured Microphone master mute: off Microphone master volume: 0 (1/256 dB) Microphone master volume set: -1536 (1/256 dB) - Configuring 48 kHz S16_LE playback (2 channels) + Configuring 44100 S16_LE playback (2 channels) Speaker configured Speaker master mute: off Speaker master volume: 0 (1/256 dB) @@ -116,7 +116,7 @@ Edit `src/tusb_config.h` to modify: ## Notes - `tuh_audio_descriptor_cb()` exposes the validated Audio Control descriptor block during enumeration. Applications that need raw entity controls must copy the required entity IDs or descriptor fields before the callback returns, then use `tuh_audio_control_xfer()` after the device mounts. -- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers follow the endpoint's `bInterval`. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` report each completed transfer; `tuh_audio_err_cb()` reports failures. The example restarts the failed stream automatically 100 ms after the error callback. +- While a stream is running, the driver keeps one isochronous transfer in flight and re-submits on completion, so transfers follow the endpoint's `bInterval`. `tuh_audio_capture_cb()` / `tuh_audio_playback_cb()` only count completed transfers; `audio_app_task()` services the FIFOs independently from the main loop. `tuh_audio_err_cb()` reports failures, and the example restarts the failed stream automatically 100 ms later. - Capture and playback streams running concurrently in the same Audio Control instance must use the same sample rate. -- `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually queued/read (0 when the FIFO is empty/full or the stream is not running), and `tuh_audio_read_available()` / `tuh_audio_write_available()` report the FIFO occupancy in frames. `tuh_audio_write()` only queues data; the playback transfer-completion chain sends it, or sends silence when the FIFO does not contain a complete polling interval without consuming the partial data. +- `tuh_audio_read()` / `tuh_audio_write()` are non-blocking FIFO operations: they return the number of whole frames actually read/queued. `tuh_audio_read_available()` reports captured frames ready to read; `tuh_audio_write_available()` reports free playback capacity. `tuh_audio_write()` only queues data; the playback transfer-completion chain sends it, or sends silence when the FIFO does not contain a complete polling interval without consuming the partial data. - Isochronous transfers require the host to poll `tuh_task()` continuously; the capture FIFO absorbs short scheduling gaps and overwrites the oldest frames when full. diff --git a/examples/host/audio_host/src/app.h b/examples/host/audio_host/src/app.h index 474544a19..45ba52dd2 100644 --- a/examples/host/audio_host/src/app.h +++ b/examples/host/audio_host/src/app.h @@ -22,7 +22,6 @@ #include <stdint.h> void led_blinking_task(void); -void audio_app_task_read(void); -void audio_app_task_write(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 index 8852ca58c..d3cc79c41 100644 --- a/examples/host/audio_host/src/audio_app.c +++ b/examples/host/audio_host/src/audio_app.c @@ -25,26 +25,23 @@ //--------------------------------------------------------------------+ // Default configuration of this example, adjust to the target device: -// - AUDIO_MAX_FRAME_COUNT: buffer holds up to 48 frames (1 ms of 48 kHz) // - AUDIO_MAX_CHANNELS: maximum channels of the capture/playback stream // - SAMPLE_RATES: sample rates tried in order, first match wins (44.1 kHz stereo by default) -#define AUDIO_MAX_FRAME_COUNT 48 -#define AUDIO_MAX_CHANNELS 2 -#define SAMPLE_RATES {44100, 48000} -#define FEATURE_UNIT_VOLUME_DB (-6 * 256) -static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device -static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index -static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index -static bool mic_ready = false; // capture stream is running -static bool spk_ready = false; // playback stream is running -static int16_t mic_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // capture FIFO read buffer -static int16_t spk_samples[AUDIO_MAX_FRAME_COUNT * AUDIO_MAX_CHANNELS]; // playback FIFO write buffer -static tuh_audio_stream_config_t mic_config; // selected capture configuration -static tuh_audio_stream_config_t spk_config; // selected playback configuration -static uint32_t audio_frame_count = AUDIO_MAX_FRAME_COUNT; // frames per ms of the selected rate -static uint32_t spk_cb_count = 0; // count of playback callbacks (for debug) -static uint32_t mic_cb_count = 0; // count of capture callbacks (for debug) -static uint32_t err_cb_count = 0; // count of error callbacks (for debug) +#define AUDIO_MAX_CHANNELS 2 +#define SAMPLE_RATES {44100, 48000} +#define FEATURE_UNIT_VOLUME_DB (-6 * 256) +#define AUDIO_BUFFER_SAMPLE_COUNT (CFG_TUH_AUDIO_STREAM_BUFSIZE / 2 / sizeof(int16_t)) +static uint8_t audio_idx = TUSB_INDEX_INVALID_8; // index of the selected audio device +static uint8_t cap_stream_idx = TUSB_INDEX_INVALID_8; // capture stream index +static uint8_t spk_stream_idx = TUSB_INDEX_INVALID_8; // playback stream index +static bool mic_ready = false; // capture stream is running +static bool spk_ready = false; // playback stream is running +static int16_t audio_samples[AUDIO_BUFFER_SAMPLE_COUNT]; // shared capture/playback buffer +static tuh_audio_stream_config_t mic_config; // selected capture configuration +static tuh_audio_stream_config_t spk_config; // selected playback configuration +static uint32_t spk_cb_count = 0; // playback callbacks (for debug) +static uint32_t mic_cb_count = 0; // capture callbacks (for debug) +static uint32_t err_cb_count = 0; // error callbacks (for debug) //--------------------------------------------------------------------+ @@ -101,18 +98,19 @@ void defer_queue_task(void) { } } -// Duplicate each mono sample to both channels (mono mic -> stereo speaker) -static void mono_to_stereo(const int16_t *mono, int16_t *stereo, uint32_t frames) { - for (uint32_t i = 0; i < frames; i++) { - stereo[i * 2] = mono[i]; - stereo[i * 2 + 1] = mono[i]; +// 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; } } -// Average both channels into one sample (stereo mic -> mono speaker) -static void stereo_to_mono(const int16_t *stereo, int16_t *mono, uint32_t frames) { +// 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++) { - mono[i] = (int16_t)(((int32_t)stereo[i * 2] + stereo[i * 2 + 1]) / 2); + samples[i] = (int16_t)(((int32_t)samples[i * 2] + samples[i * 2 + 1]) / 2); } } @@ -138,23 +136,16 @@ static void spk_fill_sine(uint32_t frames) { const int16_t sample = sine_lut[sine_phase >> (32u - SINE_LUT_BITS)]; sine_phase += phase_step; for (uint8_t ch = 0; ch < spk_config.channels; ch++) { - spk_samples[i * spk_config.channels + ch] = sample; + audio_samples[i * spk_config.channels + ch] = sample; } } } -// Frames to queue this millisecond at the given sample rate: rate / 1000, -// with the fractional remainder (0.1 frame per ms at 44.1 kHz) accumulated -// and paid back as one extra frame, matching the driver's playback pacing. -static uint32_t frame_rem_acc = 0; -static uint32_t audio_frames_this_ms(uint32_t sample_rate) { - uint32_t frames = sample_rate / 1000; - frame_rem_acc += sample_rate % 1000; - if (frame_rem_acc >= 1000) { - frame_rem_acc -= 1000; - frames++; - } - return frames; +// 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; } //--------------------------------------------------------------------+ @@ -163,7 +154,7 @@ static uint32_t audio_frames_this_ms(uint32_t sample_rate) { // 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 (3 s): capture runs, captured data is dropped +// 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 @@ -181,8 +172,8 @@ 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 tasks already behave per phase: with mic_ready false the sine tone -// plays, with the playback stream stopped the echo write returns 0 (dropped). +// 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: @@ -261,50 +252,47 @@ void led_blinking_task(void) { //--------------------------------------------------------------------+ -// Echo the captured audio back to the playback stream: drain the capture -// FIFO into mic_samples, convert, and queue the frames into the playback -// FIFO. The driver schedules the actual isochronous transfers. - -void audio_app_task_read(void) { - if (!mic_ready) { +// 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 = - tuh_audio_read(audio_idx, cap_stream_idx, mic_samples, audio_frames_this_ms(mic_config.sample_rate)); - if (frames == 0) { - return; - } - if (!spk_ready) { - return; // capture-only device or playback intentionally stopped + 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); } - if (spk_config.channels == mic_config.channels) { - memcpy(spk_samples, mic_samples, frames * mic_config.channels * sizeof(int16_t)); - } else if (mic_config.channels == 1 && spk_config.channels == 2) { - mono_to_stereo(mic_samples, spk_samples, frames); - } else { - stereo_to_mono(mic_samples, spk_samples, frames); - } - - (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); + (void)tuh_audio_write(audio_idx, spk_stream_idx, audio_samples, frames); } -void audio_app_task_write(void) { - // Fallback: the sine test tone when no capture stream is echoing - if (mic_ready || !spk_ready) { - return; - } - - const uint32_t frames = audio_frames_this_ms(spk_config.sample_rate); - if (tuh_audio_write_available(audio_idx, spk_stream_idx) >= frames) { - spk_fill_sine(frames); - (void)tuh_audio_write(audio_idx, spk_stream_idx, spk_samples, frames); +void audio_app_task(void) { + if (mic_ready && spk_ready) { + audio_echo_task(); + } else if (mic_ready) { + 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_ready) { + 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); + } } } -// Invoked when an isochronous IN transfer completes: the captured data is -// already queued into the capture FIFO and drained by audio_app_task_read(). +// 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; @@ -312,8 +300,6 @@ void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_byte mic_cb_count++; } -// Invoked when an isochronous OUT transfer completes: the next queued packet -// is submitted from the playback FIFO by the driver. void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) { (void)idx; (void)stream_idx; @@ -453,7 +439,7 @@ static void tuh_audio_mount_async(uintptr_t param) { print_stream_configs(idx, stream_idx); } - // Select a supported 48 kHz S16_LE capture configuration without + // 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. @@ -480,8 +466,6 @@ static void tuh_audio_mount_async(uintptr_t param) { audio_idx = idx; cap_stream_idx = stream_idx; mic_config = config; - // one ms of audio at the selected rate, rounded down to whole frames - audio_frame_count = sample_rate / 1000; printf(" Microphone configured\r\n"); configure_stream_controls(idx, stream_idx, "Microphone"); mic_ready = tuh_audio_start(idx, stream_idx); @@ -493,7 +477,7 @@ static void tuh_audio_mount_async(uintptr_t param) { } } if (!capture_found) { - printf(" No supported 48/44.1 kHz S16_LE capture configuration found\r\n"); + 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 @@ -537,9 +521,7 @@ static void tuh_audio_mount_async(uintptr_t param) { } audio_idx = idx; printf(" Speaker configured\r\n"); - // playback-only device: set the frame cadence from the selected rate - audio_frame_count = spk_config.sample_rate / 1000; - sine_phase = 0; + sine_phase = 0; configure_stream_controls(idx, spk_stream_idx, "Speaker"); if (capture_found) { diff --git a/examples/host/audio_host/src/main.c b/examples/host/audio_host/src/main.c index 45c468f23..c7800958b 100644 --- a/examples/host/audio_host/src/main.c +++ b/examples/host/audio_host/src/main.c @@ -42,8 +42,7 @@ int main(void) { // tinyusb host task tuh_task(); led_blinking_task(); - audio_app_task_read(); - audio_app_task_write(); + audio_app_task(); defer_queue_task(); } } |
