diff options
| author | HiFiPHile <[email protected]> | 2026-08-26 16:56:42 +0200 |
|---|---|---|
| committer | HiFiPHile <[email protected]> | 2026-08-27 10:34:33 +0200 |
| commit | 0782fb796d7981c36a3d2a11485596f288dd6bd5 (patch) | |
| tree | 1a97754f4a6f73bc80497e9993078da5176f7be9 | |
| parent | 4893a14be173e9ab3fadb606222b440ab94e55b4 (diff) | |
audio: allow stopped duplex streams to change rate
Enforce a shared capture/playback sample rate only while the opposite stream is running. This lets both stopped streams be configured in either order while preventing a live shared clock from changing underneath a transfer.
Signed-off-by: HiFiPHile <[email protected]>
| -rw-r--r-- | examples/host/audio_host/README.md | 2 | ||||
| -rw-r--r-- | src/class/audio/audio_host.c | 30 | ||||
| -rw-r--r-- | test/unit-test/test/host/audio/test_audio_host.c | 30 |
3 files changed, 48 insertions, 14 deletions
diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 2b5df7785..326b469e8 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -116,6 +116,6 @@ Edit `src/tusb_config.h` to modify: ## Notes - 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. -- Capture and playback streams in the same Audio Control instance must use the same sample rate. +- 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. - 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/src/class/audio/audio_host.c b/src/class/audio/audio_host.c index 9ca075925..82d725329 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -1921,17 +1921,6 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx } } - tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; - if (other->active_config != TUSB_INDEX_INVALID_8) { - tuh_audio_stream_config_t other_cfg; - audioh_stream_config_fill(other, other->active_as, other->active_rate, &other_cfg); - if (cfg.sample_rate != other_cfg.sample_rate) { - TU_LOG_DRV(" AUDIO configure failed: capture/playback sample rates must match (%lu != %lu)\r\n", - (unsigned long)cfg.sample_rate, (unsigned long)other_cfg.sample_rate); - return false; - } - } - // The HCD endpoint must be reopened even when the new configuration uses // the same address, since its packet size and interval may have changed. TU_VERIFY(audioh_stream_close_ep(s), false); @@ -2056,12 +2045,28 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { TU_VERIFY(s, false); TU_VERIFY(s->state == STREAM_STATE_READY && !s->running, false); // Wait for any in-flight transfer to complete and be discarded - const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_as_config_t *as = audioh_stream_active_as(s); + const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); TU_VERIFY(!usbh_edpt_busy(s->daddr, as->ep_addr), false); if (s->dir == TUSB_DIR_OUT && p_audio->playback.feedback_opened) { TU_VERIFY(!usbh_edpt_busy(s->daddr, p_audio->playback.feedback[s->active_as].ep_addr), false); } + // Do not change a device-wide/shared clock while the other direction is + // running. Stopped streams may be reconfigured independently in either order. + tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream; + if (other->running) { + const audioh_as_config_t *other_as = audioh_stream_active_as(other); + const audioh_rate_source_t *other_rate_source = audioh_as_rate_source(other, other_as); + const uint32_t sample_rate = rate_source->sample_rate[s->active_rate]; + const uint32_t other_sample_rate = other_rate_source->sample_rate[other->active_rate]; + if (sample_rate != other_sample_rate) { + TU_LOG_DRV(" AUDIO start failed: capture/playback sample rates must match (%lu != %lu)\r\n", + (unsigned long)sample_rate, (unsigned long)other_sample_rate); + return false; + } + } + if (s->dir == TUSB_DIR_OUT) { p_audio->playback.target_frames_q16 = p_audio->playback.nominal_frames_q16; p_audio->playback.feedback_pending = false; @@ -2072,7 +2077,6 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) { // UAC1 selects the alternate first because its control targets the endpoint. bool submitted = false; #if CFG_TUH_AUDIO_PROTOCOLS & TUH_AUDIO_PROTOCOL_UAC2 - const audioh_rate_source_t *rate_source = audioh_as_rate_source(s, as); if (p_audio->protocol == AUDIO_INT_PROTOCOL_CODE_V2 && rate_source->frequency_access == AUDIOH_CTRL_READ_WRITE) { submitted = audioh_stream_set_freq(s, audioh_stream_start_set_freq_complete); } else diff --git a/test/unit-test/test/host/audio/test_audio_host.c b/test/unit-test/test/host/audio/test_audio_host.c index 4c8d21245..2f92b232f 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -976,6 +976,36 @@ void test_audio_host_uac2_shared_clock_is_discovered_once_for_both_streams(void) TEST_ASSERT_EQUAL_UINT8(2, tuh_audio_config_count(0, 1)); } +void test_audio_host_reconfigures_stopped_duplex_streams_to_a_new_common_rate(void) { + open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 1)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 1, 1)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_active_config(0, 0)); + TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_active_config(0, 1)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 1, 0)); + TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_active_config(0, 1)); +} + +void test_audio_host_rejects_start_at_different_rate_from_running_peer(void) { + open_descriptors(uac2_duplex_shared_clock, sizeof(uac2_duplex_shared_clock)); + TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); + complete_uac2_clock_range(44100, 48000); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0)); + TEST_ASSERT_TRUE(tuh_audio_configure(0, 1, 1)); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_control_xfer(XFER_RESULT_SUCCESS); + complete_interface_set(XFER_RESULT_SUCCESS); + + TEST_ASSERT_FALSE(tuh_audio_start(0, 1)); + TEST_ASSERT_EQUAL_UINT8(2, control_xfer_count); + TEST_ASSERT_EQUAL_UINT8(1, interface_set_count); +} + void test_audio_host_rejects_malformed_uac2_clock_range_and_releases_instance(void) { open_descriptors(uac2_playback, sizeof(uac2_playback)); TEST_ASSERT_TRUE(audioh_set_config(AUDIO_DEV_ADDR, AUDIO_AC_ITF)); |
