summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/host/audio_host/README.md6
-rw-r--r--src/class/audio/audio_host.c36
-rw-r--r--src/class/audio/audio_host.h6
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c76
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));
+}