diff options
| author | HiFiPHile <[email protected]> | 2026-08-25 09:28:12 +0200 |
|---|---|---|
| committer | HiFiPHile <[email protected]> | 2026-08-25 09:28:12 +0200 |
| commit | ddf4bad89017dab131eddf2fa27d835218e7f72e (patch) | |
| tree | e0d3fe77802c9046a811227b15604644a8454728 | |
| parent | dfac26a272fa7bbbca2050fbe9f1ca09008e548e (diff) | |
fix(audio): drive streams from transfer completion
Keep capture and playback transfers continuously armed from their completion callbacks. Capture overwrites the oldest complete frames when full, while playback sends silence on underrun without consuming partial queued audio.
Signed-off-by: HiFiPHile <[email protected]>
| -rw-r--r-- | examples/host/audio_host/README.md | 6 | ||||
| -rw-r--r-- | src/class/audio/audio_host.c | 36 | ||||
| -rw-r--r-- | src/class/audio/audio_host.h | 6 | ||||
| -rw-r--r-- | test/unit-test/test/host/audio/test_audio_host.c | 76 |
4 files changed, 94 insertions, 30 deletions
diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md index 88c948ccd..bbb3a7684 100644 --- a/examples/host/audio_host/README.md +++ b/examples/host/audio_host/README.md @@ -98,10 +98,10 @@ Edit `src/tusb_config.h` to modify: - `CFG_TUH_AUDIO_MAX`: Maximum number of audio devices supported - `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) +- `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 - 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. -- `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. -- Isochronous transfers require the host to poll `tuh_task()` continuously; the capture FIFO absorbs short scheduling gaps, but frames are dropped when it overflows. +- `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 8918ca86e..b682d2d8a 100644 --- a/src/class/audio/audio_host.c +++ b/src/class/audio/audio_host.c @@ -311,15 +311,12 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr) //--------------------------------------------------------------------+ // Re-arm the capture endpoint: request one full packet (the device sends at -// most its max packet size per poll interval). Only submit while the whole -// packet fits into the FIFO — otherwise the frame is lost anyway and the -// transfer would be wasted; the stream resumes when tuh_audio_read() frees -// FIFO space. +// most its max packet size per poll interval). The overwritable FIFO retains +// the newest capture frames when the application cannot drain it in time. static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) { TU_VERIFY(s->state == STREAM_STATE_READY && s->running, ); const audioh_stream_map_t *map = &s->map[s->active_config]; - TU_VERIFY(tu_fifo_remaining(&s->edpt.ff) >= map->ep_size, ); TU_VERIFY(usbh_edpt_claim(s->daddr, map->ep_addr), ); // one transfer in flight // ep_size is guaranteed <= CFG_TUH_AUDIO_EPIN_BUFSIZE by enumeration @@ -347,13 +344,13 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) { bytes_64 <= CFG_TUH_AUDIO_STREAM_BUFSIZE, ); const uint16_t bytes = (uint16_t)bytes_64; if (tu_fifo_count(&s->edpt.ff) < bytes) { - // Wait until one complete poll interval is queued. This is required when - // bInterval is greater than one frame and also avoids short audio packets. - usbh_edpt_release(s->daddr, map->ep_addr); - return; + // Keep the isochronous stream active without consuming a partial frame. + // The queued audio is sent once a complete poll interval is available. + tu_memclr(s->edpt.ep_buf, bytes); + } else { + tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes); } - tu_fifo_read_n(&s->edpt.ff, s->edpt.ep_buf, bytes); TU_ASSERT(usbh_edpt_xfer(s->daddr, map->ep_addr, s->edpt.ep_buf, bytes), ); s->rem_acc = next_rem_acc; } @@ -506,7 +503,7 @@ bool audioh_init(void) { out->dir = TUSB_DIR_OUT; // Bind FIFO buffer and transfer buffer (see tu_edpt_stream_init) - TU_VERIFY(tu_edpt_stream_init(&in->edpt, true, false, false, in->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, + TU_VERIFY(tu_edpt_stream_init(&in->edpt, true, false, true, in->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, _audioh_epbuf[idx].epin)); TU_VERIFY(tu_edpt_stream_init(&out->edpt, true, true, false, out->ff_buf, CFG_TUH_AUDIO_STREAM_BUFSIZE, _audioh_epbuf[idx].epout)); @@ -883,8 +880,8 @@ uint16_t audioh_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface uint8_t usb_input_terminal_id = 0; uint8_t usb_output_source_id = 0; // A Feature Unit may precede the USB terminal that identifies its stream. - uint8_t pending_fu_id = 0; - uint8_t pending_fu_source_id = 0; + uint8_t pending_fu_id = 0; + uint8_t pending_fu_source_id = 0; bool have_header = false; p_desc = tu_desc_next(p_desc); @@ -1159,6 +1156,12 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx s->complete_cb = complete_cb; s->user_data = user_data; s->state = STREAM_STATE_CONFIG; + if (s->dir == TUSB_DIR_IN) { + // A byte FIFO can overwrite only complete audio frames when its depth is + // an exact multiple of the configured frame size. + const uint16_t fifo_depth = CFG_TUH_AUDIO_STREAM_BUFSIZE - (CFG_TUH_AUDIO_STREAM_BUFSIZE % s->frame_bytes); + TU_VERIFY(tu_fifo_config(&s->edpt.ff, s->ff_buf, fifo_depth, true), false); + } TU_LOG_DRV(" AUDIO configure %s stream %u: itf %u alt %u ep %02x\r\n", (s->dir == TUSB_DIR_IN) ? "capture" : "playback", s->stream_idx, map->itf_num, map->alt_setting, @@ -1187,7 +1190,7 @@ static void audioh_stream_start_complete(tuh_xfer_t *xfer) { if (s->dir == TUSB_DIR_IN) { audioh_stream_capture_xfer(s); // feed the capture endpoint } else { - audioh_stream_playback_xfer(s); // flush queued frames, if any + audioh_stream_playback_xfer(s); // start the continuous playback transfer chain } } @@ -1261,10 +1264,6 @@ uint32_t tuh_audio_write(uint8_t dev_idx, uint8_t stream_idx, const void *buffer } tu_fifo_write_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); - // Flush a packet when the FIFO holds at least one; the scheduler drains - // the rest on completion - audioh_stream_playback_xfer(s); - return frames; } @@ -1283,7 +1282,6 @@ uint32_t tuh_audio_read(uint8_t dev_idx, uint8_t stream_idx, void *buffer, uint3 const uint32_t frames = TU_MIN(frame_count, tu_fifo_count(&s->edpt.ff) / s->frame_bytes); if (frames > 0) { tu_fifo_read_n(&s->edpt.ff, buffer, (uint16_t)(frames * s->frame_bytes)); - audioh_stream_capture_xfer(s); // re-arm: the FIFO has room again } return frames; } diff --git a/src/class/audio/audio_host.h b/src/class/audio/audio_host.h index 74dfdcdb6..9fcbe2577 100644 --- a/src/class/audio/audio_host.h +++ b/src/class/audio/audio_host.h @@ -45,7 +45,8 @@ extern "C" { // Depth in bytes of the per-stream data FIFO. The FIFO decouples the // application's read/write calls from the endpoint's isochronous polling cadence -// and absorbs rate differences. 1024 bytes hold 4 default (256 B) packets. +// and absorbs rate differences. Capture overwrites the oldest frames when full. +// 1024 bytes hold 4 default (256 B) packets. #ifndef CFG_TUH_AUDIO_STREAM_BUFSIZE #define CFG_TUH_AUDIO_STREAM_BUFSIZE 1024 #endif @@ -240,7 +241,8 @@ void tuh_audio_umount_cb(uint8_t idx); void tuh_audio_capture_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); // Invoked when an isochronous OUT transfer completes successfully: the -// next queued packet is submitted from the stream's playback FIFO. +// next playback packet is submitted from the stream FIFO, or as silence when +// a complete packet is not queued. void tuh_audio_playback_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes); // Invoked when an isochronous transfer fails. The stream is stopped 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 728bcdf29..b87f94bc9 100644 --- a/test/unit-test/test/host/audio/test_audio_host.c +++ b/test/unit-test/test/host/audio/test_audio_host.c @@ -44,6 +44,8 @@ static uint8_t edpt_close_count; static bool edpt_busy; static bool edpt_xfer_result; static uint16_t edpt_xfer_bytes[16]; +static uint8_t edpt_xfer_data[16][8]; +static uint8_t *edpt_xfer_buffer[16]; static uint8_t edpt_xfer_count; tusb_speed_t tuh_speed_get(uint8_t daddr) { @@ -98,11 +100,13 @@ bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t *bu tuh_xfer_cb_t complete_cb, uintptr_t user_data) { (void)dev_addr; (void)ep_addr; - (void)buffer; (void)complete_cb; (void)user_data; if (edpt_xfer_count < TU_ARRAY_SIZE(edpt_xfer_bytes)) { - edpt_xfer_bytes[edpt_xfer_count++] = total_bytes; + edpt_xfer_bytes[edpt_xfer_count] = total_bytes; + edpt_xfer_buffer[edpt_xfer_count] = buffer; + memcpy(edpt_xfer_data[edpt_xfer_count], buffer, TU_MIN(sizeof(edpt_xfer_data[0]), total_bytes)); + edpt_xfer_count++; } return edpt_xfer_result; } @@ -254,7 +258,7 @@ static const uint8_t capture_fu_before_usb_output[] = { TEST_UAC1_AS_ALT0, TEST_UAC1_AS_INTERFACE(1, 1), TEST_UAC1_AS_GENERAL(CAPTURE_OUTPUT_TERM), - TEST_UAC1_FORMAT(1, 2, 16, 48000), + TEST_UAC1_FORMAT(1, 3, 24, 32000), TEST_UAC1_CS_DATA_EP, TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_ASYNCHRONOUS, 96, 1), }; @@ -396,6 +400,8 @@ void setUp(void) { edpt_busy = false; edpt_xfer_result = true; memset(edpt_xfer_bytes, 0, sizeof(edpt_xfer_bytes)); + memset(edpt_xfer_data, 0, sizeof(edpt_xfer_data)); + memset(edpt_xfer_buffer, 0, sizeof(edpt_xfer_buffer)); edpt_xfer_count = 0; configure_cb_count = 0; @@ -443,10 +449,35 @@ void test_audio_host_maps_playback_fu_declared_before_usb_input_terminal(void) { } void test_audio_host_maps_capture_fu_declared_before_usb_output_terminal(void) { - open_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); + uint8_t captured[CFG_TUH_AUDIO_STREAM_BUFSIZE]; + const uint16_t fifo_depth = CFG_TUH_AUDIO_STREAM_BUFSIZE - (CFG_TUH_AUDIO_STREAM_BUFSIZE % 3); + mount_descriptors(capture_fu_before_usb_output, sizeof(capture_fu_before_usb_output)); TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_CAPTURE, tuh_audio_stream_direction(0, 0)); TEST_ASSERT_EQUAL_UINT8(CAPTURE_FU, tuh_audio_get_feature_unit_id(0, 0)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + complete_control_xfer(XFER_RESULT_SUCCESS); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + + // Keep polling without application reads. Once full, the capture FIFO must + // overwrite its oldest frames while retaining the newest complete packets. + for (uint8_t packet = 0; packet < 11; packet++) { + TEST_ASSERT_NOT_NULL(edpt_xfer_buffer[packet]); + memset(edpt_xfer_buffer[packet], packet + 1, 96); + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96)); + TEST_ASSERT_EQUAL_UINT8(packet + 2, edpt_xfer_count); + } + + TEST_ASSERT_EQUAL_UINT32(fifo_depth / 3, tuh_audio_read_available(0, 0)); + TEST_ASSERT_EQUAL_UINT32(fifo_depth / 3, tuh_audio_read(0, 0, captured, fifo_depth / 3)); + TEST_ASSERT_EACH_EQUAL_UINT8(1, captured, 63); + TEST_ASSERT_EACH_EQUAL_UINT8(2, captured + 63, 96); + TEST_ASSERT_EACH_EQUAL_UINT8(11, captured + fifo_depth - 96, 96); } void test_audio_host_parses_discrete_frequencies_with_interval_greater_than_one(void) { @@ -566,10 +597,11 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); TEST_ASSERT_EQUAL_UINT8(1, interface_alt); - complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT32(256, tuh_audio_write(0, 0, samples, 256)); TEST_ASSERT_EQUAL_UINT8(0, edpt_xfer_count); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); - TEST_ASSERT_EQUAL_UINT32(256, tuh_audio_write(0, 0, samples, 256)); while (edpt_xfer_count < 5) { edpt_busy = false; TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1])); @@ -585,3 +617,35 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only } TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes[9]); } + +void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) { + uint8_t sample[4] = {1, 2, 3, 4}; + uint8_t more_samples[43 * 4]; + memset(more_samples, 0x55, sizeof(more_samples)); + mount_descriptors(playback_44100_max_packets_only, sizeof(playback_44100_max_packets_only)); + + TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0, configure_complete, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + TEST_ASSERT_TRUE(tuh_audio_start(0, 0)); + complete_interface_set(XFER_RESULT_SUCCESS); + + TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[0]); + TEST_ASSERT_EACH_EQUAL_HEX8(0, edpt_xfer_data[0], sizeof(edpt_xfer_data[0])); + + TEST_ASSERT_EQUAL_UINT32(1, tuh_audio_write(0, 0, sample, 1)); + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[0])); + + TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[1]); + TEST_ASSERT_EACH_EQUAL_HEX8(0, edpt_xfer_data[1], sizeof(edpt_xfer_data[1])); + + TEST_ASSERT_EQUAL_UINT32(43, tuh_audio_write(0, 0, more_samples, 43)); + edpt_busy = false; + TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[1])); + + TEST_ASSERT_EQUAL_UINT8(3, edpt_xfer_count); + TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes[2]); + TEST_ASSERT_EQUAL_UINT8_ARRAY(sample, edpt_xfer_data[2], sizeof(sample)); +} |
