summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiFiPHile <[email protected]>2026-08-26 16:56:42 +0200
committerHiFiPHile <[email protected]>2026-08-27 10:34:33 +0200
commit0782fb796d7981c36a3d2a11485596f288dd6bd5 (patch)
tree1a97754f4a6f73bc80497e9993078da5176f7be9
parent4893a14be173e9ab3fadb606222b440ab94e55b4 (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.md2
-rw-r--r--src/class/audio/audio_host.c30
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c30
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));