summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/host/audio_host/README.md2
-rw-r--r--src/class/audio/audio_host.c260
-rw-r--r--test/unit-test/test/host/audio/test_audio_host.c196
3 files changed, 383 insertions, 75 deletions
diff --git a/examples/host/audio_host/README.md b/examples/host/audio_host/README.md
index 27e6425b8..06cc4f197 100644
--- a/examples/host/audio_host/README.md
+++ b/examples/host/audio_host/README.md
@@ -24,7 +24,7 @@ The echo needs a matching S16_LE playback stream at the capture sample rate; dev
## Limitations and trade-offs
-- Explicit feedback endpoint data is ignored. Asynchronous playback still uses the nominal sample rate, but device/host clock drift is not corrected and may cause underruns, overruns, or audible pops and clicks. An implicit-feedback IN endpoint is treated as an ordinary audio-data endpoint and is not used to pace playback.
+- Explicit feedback endpoints are supported with both 10.14 and 16.16 feedback values. An implicit-feedback IN endpoint is treated as an ordinary audio-data endpoint and is not used to pace playback.
- UAC1 Type I Format descriptors with `bSamFreqType == 0` are unsupported; the driver requires a list of discrete sampling frequencies.
- Master mute and volume controls are discovered before the mount callback, including the volume MIN/MAX/RES range. Feature Units without master mute or volume are ignored. The typed API controls the master channel; the lower-level Feature Unit API remains available for fixed-width UAC1 controls on the associated unit.
- The UAC1 `MaxPacketsOnly` endpoint attribute is not supported. OUT transfers are not padded to `wMaxPacketSize`, and padding in IN transfers is not removed from the reported audio data.
diff --git a/src/class/audio/audio_host.c b/src/class/audio/audio_host.c
index 98822b9c0..b34b7534b 100644
--- a/src/class/audio/audio_host.c
+++ b/src/class/audio/audio_host.c
@@ -112,6 +112,10 @@ typedef struct {
uint8_t ep_sync; // bmAttributes sync type
uint8_t ep_usage; // bmAttributes usage type
bool sam_freq_ctrl; // endpoint supports sampling-frequency control
+ uint8_t fb_ep_addr; // explicit feedback endpoint address (0 = none)
+ uint8_t fb_ep_size; // feedback endpoint max packet size (3 or 4)
+ uint8_t fb_ep_interval;
+ uint8_t fb_ep_attr;
} audioh_stream_map_t;
// One logical stream (capture or playback)
@@ -143,11 +147,17 @@ typedef struct {
// Size in bytes of one frame (all channels) of the active configuration
uint16_t frame_bytes;
- // Playback pacing per endpoint poll interval. Fractional frames are
- // accumulated in millionths because the interval is expressed in us.
- uint16_t frames_per_interval;
- uint32_t frames_rem;
- uint32_t rem_acc;
+ // Playback pacing in Q16.16 frames per data-endpoint poll interval.
+ // Feedback is latched only when the current fractional scheduling cycle
+ // wraps, so one cycle is never generated from two different rates.
+ uint32_t nominal_frames_q16;
+ uint32_t target_frames_q16;
+ uint32_t pending_frames_q16;
+ uint32_t feedback_min_q16;
+ uint32_t feedback_max_q16;
+ uint16_t rem_acc;
+ bool feedback_pending;
+ bool feedback_opened;
// FIFO + endpoint transfer helper (see tu_edpt_stream, used by the MIDI
// host driver): the FIFO decouples the application's frame-based read/write
@@ -174,6 +184,7 @@ typedef struct {
typedef struct {
TUH_EPBUF_DEF(sam_freq, 4); // shared sampling-frequency SET data
TUH_EPBUF_DEF(fu_ctrl, 8); // feature-unit SET data
+ TUH_EPBUF_DEF(feedback, 4); // explicit-feedback transfer buffer
TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); // capture transfer buffer
TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); // playback transfer buffer
// Feature-unit request state: only one operation in flight per device
@@ -267,6 +278,14 @@ static uint32_t audioh_interval_us(uint8_t ep_interval, uint8_t daddr) {
return ((uint32_t)1u << (ep_interval - 1)) * unit_us;
}
+// Convert a nominal sample rate to Q16.16 frames per data endpoint poll
+// interval. Round to the nearest representable value to preserve common
+// fractional rates such as 44.1 frames/ms.
+static uint32_t audioh_nominal_frames_q16(uint32_t sample_rate, uint8_t ep_interval, uint8_t daddr) {
+ const uint64_t numerator = (uint64_t)sample_rate * audioh_interval_us(ep_interval, daddr) * 65536u;
+ return (uint32_t)((numerator + 500000u) / 1000000u);
+}
+
// UAC 1.0 feature-unit control value width (0 = unsupported variable/unknown width)
static uint8_t audioh_fu_control_width(uint8_t control_selector) {
switch (control_selector) {
@@ -288,20 +307,25 @@ static uint8_t audioh_fu_control_width(uint8_t control_selector) {
// Reset a stream to its unconfigured state (keeps idx, dir, and FIFO configuration)
static void audioh_stream_reset(tuh_audio_stream_t *s) {
- s->daddr = 0;
- s->stream_idx = TUSB_INDEX_INVALID_8;
- s->config_count = 0;
- s->active_config = TUSB_INDEX_INVALID_8;
- s->state = STREAM_STATE_IDLE;
- s->running = false;
- s->feature_unit_id = 0;
- s->mute_supported = false;
- s->volume_supported = false;
- s->volume_range = (tuh_audio_volume_range_t){0};
- s->frame_bytes = 0;
- s->frames_per_interval = 0;
- s->frames_rem = 0;
- s->rem_acc = 0;
+ s->daddr = 0;
+ s->stream_idx = TUSB_INDEX_INVALID_8;
+ s->config_count = 0;
+ s->active_config = TUSB_INDEX_INVALID_8;
+ s->state = STREAM_STATE_IDLE;
+ s->running = false;
+ s->feature_unit_id = 0;
+ s->mute_supported = false;
+ s->volume_supported = false;
+ s->volume_range = (tuh_audio_volume_range_t){0};
+ s->frame_bytes = 0;
+ s->nominal_frames_q16 = 0;
+ s->target_frames_q16 = 0;
+ s->pending_frames_q16 = 0;
+ s->feedback_min_q16 = 0;
+ s->feedback_max_q16 = 0;
+ s->rem_acc = 0;
+ s->feedback_pending = false;
+ s->feedback_opened = false;
tu_edpt_stream_close(&s->edpt);
tu_edpt_stream_clear(&s->edpt);
}
@@ -312,9 +336,11 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr)
audioh_interface_t *p_audio = &_audioh_itf[idx];
for (uint8_t s = 0; s < 2; s++) {
tuh_audio_stream_t *stream = (s == 0) ? &p_audio->in_stream : &p_audio->out_stream;
- if (stream->daddr == dev_addr && stream->active_config != TUSB_INDEX_INVALID_8 &&
- stream->map[stream->active_config].ep_addr == ep_addr) {
- return stream;
+ if (stream->daddr == dev_addr && stream->active_config != TUSB_INDEX_INVALID_8) {
+ const audioh_stream_map_t *map = &stream->map[stream->active_config];
+ if (map->ep_addr == ep_addr || (map->fb_ep_addr != 0 && map->fb_ep_addr == ep_addr)) {
+ return stream;
+ }
}
}
}
@@ -327,6 +353,17 @@ static tuh_audio_stream_t *audioh_find_stream(uint8_t dev_addr, uint8_t ep_addr)
static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes);
+static void audioh_stream_feedback_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(map->fb_ep_addr != 0, );
+ TU_VERIFY(usbh_edpt_claim(s->daddr, map->fb_ep_addr), );
+ if (!usbh_edpt_xfer(s->daddr, map->fb_ep_addr, _audioh_epbuf[s->idx].feedback, map->fb_ep_size)) {
+ audioh_stream_error(s, 0);
+ }
+}
+
// Re-arm the capture endpoint: request one full packet (the device sends at
// most its max packet size per poll interval). The overwritable FIFO retains
// the newest capture frames when the application cannot drain it in time.
@@ -344,18 +381,21 @@ static void audioh_stream_capture_xfer(tuh_audio_stream_t *s) {
// Submit the next queued playback packet. Fractional frames per endpoint poll
// interval are accumulated on each successful submission, keeping the average
-// data rate exactly at the sample rate.
+// data rate at the active nominal or feedback target.
static void audioh_stream_playback_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(usbh_edpt_claim(s->daddr, map->ep_addr), ); // one transfer in flight
- uint32_t frames = s->frames_per_interval;
- uint32_t next_rem_acc = s->rem_acc + s->frames_rem;
- if (next_rem_acc >= 1000000) {
- next_rem_acc -= 1000000;
+ uint32_t frames = s->target_frames_q16 >> 16;
+ const uint32_t fraction = s->target_frames_q16 & 0xFFFFu;
+ uint32_t next_rem_acc = s->rem_acc + fraction;
+ bool loop_done = (fraction == 0);
+ if (next_rem_acc >= 65536u) {
+ next_rem_acc -= 65536u;
frames++;
+ loop_done = true;
}
const uint64_t bytes_64 = (uint64_t)frames * s->frame_bytes;
@@ -374,7 +414,12 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) {
audioh_stream_error(s, 0);
return;
}
- s->rem_acc = next_rem_acc;
+ s->rem_acc = (uint16_t)next_rem_acc;
+ if (loop_done && s->feedback_pending) {
+ s->target_frames_q16 = s->pending_frames_q16;
+ s->feedback_pending = false;
+ s->rem_acc = 0;
+ }
}
//--------------------------------------------------------------------+
@@ -382,6 +427,15 @@ static void audioh_stream_playback_xfer(tuh_audio_stream_t *s) {
//--------------------------------------------------------------------+
static bool audioh_stream_close_ep(tuh_audio_stream_t *s) {
+ if (s->feedback_opened) {
+ const uint8_t fb_ep_addr = s->map[s->active_config].fb_ep_addr;
+ if (!tuh_edpt_close(s->daddr, fb_ep_addr)) {
+ TU_LOG_DRV(" AUDIO close feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, fb_ep_addr);
+ return false;
+ }
+ s->feedback_opened = false;
+ }
+
if (!tu_edpt_stream_is_opened(&s->edpt)) {
return true;
}
@@ -404,7 +458,10 @@ static void audioh_stream_fail(tuh_audio_stream_t *s) {
}
static void audioh_stream_error(tuh_audio_stream_t *s, uint16_t xferred_bytes) {
- s->running = false;
+ s->running = false;
+ s->target_frames_q16 = s->nominal_frames_q16;
+ s->feedback_pending = false;
+ s->rem_acc = 0;
tu_edpt_stream_clear(&s->edpt);
tuh_audio_err_cb(s->idx, s->stream_idx, xferred_bytes);
}
@@ -458,6 +515,24 @@ static bool audioh_stream_open_ep(tuh_audio_stream_t *s) {
const uint16_t xfer_len = (s->dir == TUSB_DIR_IN) ? CFG_TUH_AUDIO_EPIN_BUFSIZE : CFG_TUH_AUDIO_EPOUT_BUFSIZE;
tu_edpt_stream_open(&s->edpt, s->daddr, &desc_ep, xfer_len);
tu_edpt_stream_clear(&s->edpt);
+
+ if (map->fb_ep_addr != 0) {
+ const tusb_desc_endpoint_t desc_fb = {.bLength = sizeof(tusb_desc_endpoint_t),
+ .bDescriptorType = TUSB_DESC_ENDPOINT,
+ .bEndpointAddress = map->fb_ep_addr,
+ .bmAttributes = {.xfer = TUSB_XFER_ISOCHRONOUS,
+ .sync = (map->fb_ep_attr >> 2) & 0x03u,
+ .usage = (map->fb_ep_attr >> 4) & 0x03u},
+ .wMaxPacketSize = tu_htole16(map->fb_ep_size),
+ .bInterval = map->fb_ep_interval};
+ if (!tuh_edpt_open(s->daddr, &desc_fb)) {
+ TU_LOG_DRV(" AUDIO open feedback endpoint failed: addr=%u ep=%02x\r\n", s->daddr, map->fb_ep_addr);
+ audioh_stream_fail(s);
+ return false;
+ }
+ s->feedback_opened = true;
+ }
+
s->state = STREAM_STATE_READY;
return true;
}
@@ -523,6 +598,46 @@ void audioh_close(uint8_t daddr) {
}
}
+static void audioh_feedback_received(tuh_audio_stream_t *s, uint32_t xferred_bytes) {
+ const uint8_t *fb = _audioh_epbuf[s->idx].feedback;
+ uint32_t feedback_q16;
+ if (xferred_bytes == 3) {
+ // Full-speed feedback is normally Q10.14. Keep the scheduler in Q16.16.
+ feedback_q16 = ((uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16)) << 2;
+ } else if (xferred_bytes == 4) {
+ feedback_q16 = (uint32_t)fb[0] | ((uint32_t)fb[1] << 8) | ((uint32_t)fb[2] << 16) | ((uint32_t)fb[3] << 24);
+ } else {
+ TU_LOG_DRV(" AUDIO invalid feedback length: %lu\r\n", (unsigned long)xferred_bytes);
+ return;
+ }
+
+ const audioh_stream_map_t *map = &s->map[s->active_config];
+ if (feedback_q16 < s->feedback_min_q16 || feedback_q16 > s->feedback_max_q16) {
+ TU_LOG_DRV(" AUDIO feedback out of range: 0x%08lx\r\n", (unsigned long)feedback_q16);
+ return;
+ }
+
+ // Feedback is expressed per USB frame/microframe. Scale it to the data
+ // endpoint's polling interval before handing it to the packet scheduler.
+ const uint64_t target_q16_64 = (uint64_t)feedback_q16 << (map->ep_interval - 1u);
+ if (target_q16_64 > UINT32_MAX) {
+ return;
+ }
+
+ const uint32_t target_q16 = (uint32_t)target_q16_64;
+ const uint64_t max_bytes = (((uint64_t)target_q16 + 0xFFFFu) >> 16) * s->frame_bytes;
+ if (max_bytes == 0 || max_bytes > map->ep_size || max_bytes > CFG_TUH_AUDIO_EPOUT_BUFSIZE ||
+ max_bytes > CFG_TUH_AUDIO_STREAM_BUFSIZE) {
+ TU_LOG_DRV(" AUDIO feedback exceeds playback packet capacity: 0x%08lx\r\n", (unsigned long)feedback_q16);
+ return;
+ }
+
+ // Keep only the newest feedback sample. The packet scheduler promotes it at
+ // the end of its current fractional cycle.
+ s->pending_frames_q16 = target_q16;
+ s->feedback_pending = true;
+}
+
bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) {
tuh_audio_stream_t *s = audioh_find_stream(dev_addr, ep_addr);
if (s == NULL) {
@@ -541,6 +656,13 @@ bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin
return true;
}
+ const audioh_stream_map_t *map = &s->map[s->active_config];
+ if (ep_addr == map->fb_ep_addr) {
+ audioh_feedback_received(s, xferred_bytes);
+ audioh_stream_feedback_xfer(s);
+ return true;
+ }
+
if (s->dir == TUSB_DIR_IN) {
// Capture: move the received bytes into the FIFO (whole frames only),
// notify, then re-arm for the next packet
@@ -605,8 +727,10 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de
uint8_t ep_usage;
bool sam_freq_ctrl;
} audioh_ep_info_t;
- audioh_ep_info_t ep_info = {0};
- bool has_data_ep = false;
+ audioh_ep_info_t ep_info = {0};
+ audioh_ep_info_t fb_info = {0};
+ bool has_data_ep = false;
+ bool has_feedback_ep = false;
while (p_desc < desc_end) {
TU_VERIFY(audioh_desc_valid(p_desc, desc_end, 2), NULL);
@@ -667,12 +791,27 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de
const bool implicit_feedback =
usage == (TUSB_ISO_EP_ATT_IMPLICIT_FB >> 4) && tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN;
const bool explicit_feedback =
- usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4) ||
- (usage == (TUSB_ISO_EP_ATT_DATA >> 4) && desc_endpoint->bmAttributes.sync == TUSB_ISO_EP_ATT_NO_SYNC);
+ tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN &&
+ (usage == (TUSB_ISO_EP_ATT_EXPLICIT_FB >> 4) ||
+ (usage == (TUSB_ISO_EP_ATT_DATA >> 4) && desc_endpoint->bmAttributes.sync == TUSB_ISO_EP_ATT_NO_SYNC));
if (explicit_feedback) {
- TU_LOG_DRV(" AUDIO AS itf %u alt %u: explicit feedback ep %02x ignored\r\n", itf_num, alt,
- desc_endpoint->bEndpointAddress);
+ const uint16_t fb_ep_size = tu_edpt_packet_size(desc_endpoint);
+ if (has_feedback_ep || (fb_ep_size != 3 && fb_ep_size != 4)) {
+ TU_LOG_DRV(" AUDIO AS itf %u alt %u: invalid/extra feedback ep %02x ignored\r\n", itf_num, alt,
+ desc_endpoint->bEndpointAddress);
+ break;
+ }
+
+ fb_info.ep_addr = desc_endpoint->bEndpointAddress;
+ fb_info.ep_size = fb_ep_size;
+ fb_info.ep_interval = desc_endpoint->bInterval;
+ if (fb_info.ep_interval == 0 || fb_info.ep_interval > 16) {
+ fb_info.ep_interval = 1;
+ }
+ fb_info.ep_sync = desc_endpoint->bmAttributes.sync;
+ fb_info.ep_usage = desc_endpoint->bmAttributes.usage;
+ has_feedback_ep = true;
break;
}
@@ -796,6 +935,16 @@ static const uint8_t *audioh_parse_as(audioh_interface_t *p_audio, const tusb_de
stream->map[stream->config_count].ep_sync = ep->ep_sync;
stream->map[stream->config_count].ep_usage = ep->ep_usage;
stream->map[stream->config_count].sam_freq_ctrl = ep->sam_freq_ctrl;
+ stream->map[stream->config_count].fb_ep_addr = 0;
+ stream->map[stream->config_count].fb_ep_size = 0;
+ stream->map[stream->config_count].fb_ep_interval = 0;
+ stream->map[stream->config_count].fb_ep_attr = 0;
+ if (stream->dir == TUSB_DIR_OUT && has_feedback_ep) {
+ stream->map[stream->config_count].fb_ep_addr = fb_info.ep_addr;
+ stream->map[stream->config_count].fb_ep_size = (uint8_t)fb_info.ep_size;
+ stream->map[stream->config_count].fb_ep_interval = fb_info.ep_interval;
+ stream->map[stream->config_count].fb_ep_attr = (uint8_t)((fb_info.ep_sync << 2) | (fb_info.ep_usage << 4));
+ }
stream->config_count++;
}
@@ -1123,6 +1272,9 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx
if (s->state == STREAM_STATE_READY) {
// Wait for any in-flight transfer to complete and be discarded
TU_VERIFY(!usbh_edpt_busy(s->daddr, s->edpt.ep_addr), false);
+ if (s->feedback_opened) {
+ TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].fb_ep_addr), false);
+ }
}
tuh_audio_stream_t *other = (s == &p_audio->out_stream) ? &p_audio->in_stream : &p_audio->out_stream;
@@ -1139,14 +1291,18 @@ bool tuh_audio_configure(uint8_t dev_idx, uint8_t stream_idx, uint8_t config_idx
// the same address, since its packet size and interval may have changed.
TU_VERIFY(audioh_stream_close_ep(s), false);
- const audioh_stream_map_t *map = &s->map[config_idx];
- const uint64_t frames_numerator = (uint64_t)cfg->sample_rate * audioh_interval_us(map->ep_interval, s->daddr);
- s->active_config = config_idx;
- s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(cfg);
- s->frames_per_interval = (uint16_t)(frames_numerator / 1000000u);
- s->frames_rem = (uint32_t)(frames_numerator % 1000000u);
- s->rem_acc = 0;
- s->state = STREAM_STATE_IDLE;
+ const audioh_stream_map_t *map = &s->map[config_idx];
+ const uint32_t frame_div = (tuh_speed_get(s->daddr) == TUSB_SPEED_HIGH) ? 8000u : 1000u;
+ s->active_config = config_idx;
+ s->frame_bytes = (uint16_t)tuh_audio_config_frame_size(cfg);
+ s->nominal_frames_q16 = audioh_nominal_frames_q16(cfg->sample_rate, map->ep_interval, s->daddr);
+ s->target_frames_q16 = s->nominal_frames_q16;
+ s->pending_frames_q16 = 0;
+ s->feedback_min_q16 = ((cfg->sample_rate - 1u) / frame_div) << 16;
+ s->feedback_max_q16 = (cfg->sample_rate / frame_div + 1u) << 16;
+ s->feedback_pending = false;
+ s->rem_acc = 0;
+ s->state = STREAM_STATE_IDLE;
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.
@@ -1171,6 +1327,12 @@ static void audioh_stream_start_xfer(tuh_audio_stream_t *s) {
if (s->dir == TUSB_DIR_IN) {
audioh_stream_capture_xfer(s); // feed the capture endpoint
} else {
+ if (s->map[s->active_config].fb_ep_addr != 0) {
+ audioh_stream_feedback_xfer(s);
+ }
+ if (!s->running) {
+ return;
+ }
audioh_stream_playback_xfer(s); // start the continuous playback transfer chain
}
}
@@ -1224,8 +1386,14 @@ bool tuh_audio_start(uint8_t dev_idx, uint8_t stream_idx) {
TU_VERIFY(s->state == STREAM_STATE_READY && !s->running, false);
// Wait for any in-flight transfer to complete and be discarded
TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].ep_addr), false);
+ if (s->feedback_opened) {
+ TU_VERIFY(!usbh_edpt_busy(s->daddr, s->map[s->active_config].fb_ep_addr), false);
+ }
- s->running = true;
+ s->target_frames_q16 = s->nominal_frames_q16;
+ s->feedback_pending = false;
+ s->rem_acc = 0;
+ s->running = true;
// Activate the interface's alternate setting asynchronously: transfers
// begin once SET_INTERFACE and sampling-frequency control complete.
const audioh_stream_map_t *map = &s->map[s->active_config];
@@ -1262,7 +1430,9 @@ bool tuh_audio_stop(uint8_t dev_idx, uint8_t stream_idx) {
// The in-flight transfer (if any) completes and its data is discarded;
// queued frames are dropped as well. The interface is being deactivated so
// the device stops transferring.
- s->running = false;
+ s->running = false;
+ s->target_frames_q16 = s->nominal_frames_q16;
+ s->feedback_pending = false;
tu_edpt_stream_clear(&s->edpt);
s->rem_acc = 0; // restart the pacing accumulator on the next tuh_audio_start()
return true;
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 e5056bc2c..3e11e733c 100644
--- a/test/unit-test/test/host/audio/test_audio_host.c
+++ b/test/unit-test/test/host/audio/test_audio_host.c
@@ -43,8 +43,9 @@ static bool edpt_close_result;
static uint8_t closed_ep[4];
static uint8_t edpt_close_count;
-static bool edpt_busy;
+static uint32_t edpt_busy_mask;
static bool edpt_xfer_result;
+static uint8_t edpt_xfer_ep[16];
static uint16_t edpt_xfer_bytes[16];
static uint8_t edpt_xfer_data[16][8];
static uint8_t *edpt_xfer_buffer[16];
@@ -55,6 +56,15 @@ static uint8_t err_cb_idx;
static uint8_t err_cb_stream_idx;
static uint16_t err_cb_xferred_bytes;
+static uint32_t edpt_mask(uint8_t ep_addr) {
+ const uint8_t bit = tu_edpt_number(ep_addr) + (tu_edpt_dir(ep_addr) == TUSB_DIR_IN ? 16 : 0);
+ return 1u << bit;
+}
+
+static void complete_edpt(uint8_t ep_addr) {
+ edpt_busy_mask &= ~edpt_mask(ep_addr);
+}
+
void tuh_audio_err_cb(uint8_t idx, uint8_t stream_idx, uint16_t xferred_bytes) {
err_cb_count++;
err_cb_idx = idx;
@@ -93,7 +103,7 @@ bool tuh_edpt_close(uint8_t daddr, uint8_t ep_addr) {
closed_ep[edpt_close_count++] = ep_addr;
}
if (edpt_close_result) {
- edpt_busy = false;
+ complete_edpt(ep_addr);
}
return edpt_close_result;
}
@@ -114,42 +124,40 @@ bool tuh_interface_set(uint8_t daddr, uint8_t itf_num, uint8_t itf_alt, tuh_xfer
bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes,
tuh_xfer_cb_t complete_cb, uintptr_t user_data) {
(void)dev_addr;
- (void)ep_addr;
(void)complete_cb;
(void)user_data;
if (edpt_xfer_count < TU_ARRAY_SIZE(edpt_xfer_bytes)) {
+ edpt_xfer_ep[edpt_xfer_count] = ep_addr;
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++;
}
if (!edpt_xfer_result) {
- edpt_busy = false; // match usbh_edpt_xfer() cleanup after HCD rejection
+ complete_edpt(ep_addr); // match usbh_edpt_xfer() cleanup after HCD rejection
}
return edpt_xfer_result;
}
bool usbh_edpt_claim(uint8_t dev_addr, uint8_t ep_addr) {
(void)dev_addr;
- (void)ep_addr;
- if (edpt_busy) {
+ const uint32_t mask = edpt_mask(ep_addr);
+ if (edpt_busy_mask & mask) {
return false;
}
- edpt_busy = true;
+ edpt_busy_mask |= mask;
return true;
}
bool usbh_edpt_release(uint8_t dev_addr, uint8_t ep_addr) {
(void)dev_addr;
- (void)ep_addr;
- edpt_busy = false;
+ complete_edpt(ep_addr);
return true;
}
bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) {
(void)dev_addr;
- (void)ep_addr;
- return edpt_busy;
+ return (edpt_busy_mask & edpt_mask(ep_addr)) != 0;
}
void usbh_driver_set_config_complete(uint8_t dev_addr, uint8_t itf_num) {
@@ -487,6 +495,30 @@ static const uint8_t playback_44100_max_packets_only[] = {
TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_MAX_PACKETS_ONLY),
};
+static const uint8_t playback_44100_with_feedback_10_14[] = {
+ TEST_UAC1_AC_HEADER,
+ TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2),
+ TEST_UAC1_AS_ALT0,
+ TEST_UAC1_AS_INTERFACE(1, 2),
+ TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM),
+ TEST_UAC1_FORMAT(2, 2, 16, 44100),
+ TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 180, 1),
+ TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_MAX_PACKETS_ONLY),
+ TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_EXPLICIT_FB, 3, 1),
+};
+
+static const uint8_t playback_44100_with_feedback_16_16[] = {
+ TEST_UAC1_AC_HEADER,
+ TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2),
+ TEST_UAC1_AS_ALT0,
+ TEST_UAC1_AS_INTERFACE(1, 2),
+ TEST_UAC1_AS_GENERAL(PLAYBACK_INPUT_TERM),
+ TEST_UAC1_FORMAT(2, 2, 16, 44100),
+ TEST_UAC1_DATA_EP(0x01, TUSB_ISO_EP_ATT_ADAPTIVE, 180, 1),
+ TEST_UAC1_CS_DATA_EP_ATTR(AUDIO10_CS_AS_ISO_DATA_EP_ATT_MAX_PACKETS_ONLY),
+ TEST_UAC1_DATA_EP(0x81, TUSB_ISO_EP_ATT_EXPLICIT_FB, 4, 1),
+};
+
static const uint8_t playback_11025_interval4[] = {
TEST_UAC1_AC_HEADER,
TEST_UAC1_INPUT_TERM(PLAYBACK_INPUT_TERM, AUDIO_TERM_TYPE_USB_STREAMING, 2),
@@ -529,6 +561,50 @@ static void complete_control_xfer_with_u16(uint16_t value) {
complete_control_xfer(XFER_RESULT_SUCCESS);
}
+static uint16_t edpt_xfer_bytes_at(uint8_t ep_addr, uint8_t ep_xfer_idx) {
+ uint8_t found = 0;
+ for (uint8_t i = 0; i < edpt_xfer_count; i++) {
+ if (edpt_xfer_ep[i] == ep_addr && found++ == ep_xfer_idx) {
+ return edpt_xfer_bytes[i];
+ }
+ }
+ TEST_FAIL_MESSAGE("Endpoint transfer not found");
+ return 0;
+}
+
+static uint8_t *edpt_xfer_buffer_at(uint8_t ep_addr, uint8_t ep_xfer_idx) {
+ uint8_t found = 0;
+ for (uint8_t i = 0; i < edpt_xfer_count; i++) {
+ if (edpt_xfer_ep[i] == ep_addr && found++ == ep_xfer_idx) {
+ return edpt_xfer_buffer[i];
+ }
+ }
+ TEST_FAIL_MESSAGE("Endpoint transfer not found");
+ return NULL;
+}
+
+static void complete_playback_xfer(void) {
+ complete_edpt(0x01);
+ TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, 0));
+}
+
+static void complete_feedback_xfer(uint32_t feedback_q16, uint8_t length) {
+ uint8_t *buffer = edpt_xfer_buffer_at(0x81, 0);
+ if (length == 3) {
+ const uint32_t feedback_q14 = feedback_q16 >> 2;
+ buffer[0] = (uint8_t)feedback_q14;
+ buffer[1] = (uint8_t)(feedback_q14 >> 8);
+ buffer[2] = (uint8_t)(feedback_q14 >> 16);
+ } else {
+ buffer[0] = (uint8_t)feedback_q16;
+ buffer[1] = (uint8_t)(feedback_q16 >> 8);
+ buffer[2] = (uint8_t)(feedback_q16 >> 16);
+ buffer[3] = (uint8_t)(feedback_q16 >> 24);
+ }
+ complete_edpt(0x81);
+ TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, length));
+}
+
void setUp(void) {
interface_set_result = true;
memset(&interface_xfer, 0, sizeof(interface_xfer));
@@ -549,8 +625,9 @@ void setUp(void) {
memset(closed_ep, 0, sizeof(closed_ep));
edpt_close_count = 0;
- edpt_busy = false;
+ edpt_busy_mask = 0;
edpt_xfer_result = true;
+ memset(edpt_xfer_ep, 0, sizeof(edpt_xfer_ep));
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));
@@ -598,17 +675,22 @@ static void mount_descriptors(const uint8_t *desc, uint16_t desc_len) {
control_xfer_count = 0;
}
-void test_audio_host_ignores_explicit_feedback_endpoint(void) {
+void test_audio_host_saves_and_opens_explicit_feedback_endpoint(void) {
// A MIDI-only AC collection must not consume an Audio Host instance.
TEST_ASSERT_EQUAL_UINT16(0, audioh_open(0, AUDIO_DEV_ADDR, (const tusb_desc_interface_t *)midi_only_collection,
sizeof(midi_only_collection)));
TEST_ASSERT_EQUAL_UINT8(0, tuh_audio_get_dev_addr(0));
- open_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback));
+ mount_descriptors(playback_with_explicit_feedback, sizeof(playback_with_explicit_feedback));
TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_stream_count(0));
TEST_ASSERT_EQUAL(TUH_AUDIO_STREAM_PLAYBACK, tuh_audio_stream_direction(0, 0));
TEST_ASSERT_EQUAL_UINT8(1, tuh_audio_config_count(0, 0));
+ TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0));
+ TEST_ASSERT_EQUAL_UINT8(2, edpt_open_count);
+ TEST_ASSERT_EQUAL_HEX8(0x01, opened_ep[0].bEndpointAddress);
+ TEST_ASSERT_EQUAL_HEX8(0x81, opened_ep[1].bEndpointAddress);
+ TEST_ASSERT_EQUAL_UINT16(3, tu_edpt_packet_size(&opened_ep[1]));
}
void test_audio_host_rejects_uac2_interface_without_consuming_instance(void) {
@@ -660,7 +742,7 @@ void test_audio_host_maps_capture_fu_declared_before_usb_output_terminal(void) {
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;
+ complete_edpt(0x81);
TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96));
TEST_ASSERT_EQUAL_UINT8(packet + 2, edpt_xfer_count);
}
@@ -716,7 +798,7 @@ void test_audio_host_sets_sampling_frequency_after_each_stream_activation(void)
TEST_ASSERT_EQUAL_UINT8(0, interface_alt);
TEST_ASSERT_EQUAL_UINT8(2, interface_set_count);
complete_interface_set(XFER_RESULT_SUCCESS);
- edpt_busy = false;
+ complete_edpt(0x81);
TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96));
TEST_ASSERT_TRUE(tuh_audio_start(0, 0));
@@ -798,7 +880,7 @@ void test_audio_host_keeps_running_when_stop_cannot_be_submitted(void) {
interface_set_result = false;
TEST_ASSERT_FALSE(tuh_audio_stop(0, 0));
- edpt_busy = false;
+ complete_edpt(0x81);
TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x81, XFER_RESULT_SUCCESS, 96));
TEST_ASSERT_EQUAL_UINT8(2, edpt_xfer_count);
}
@@ -1050,13 +1132,15 @@ void test_audio_host_schedules_44100_hz_fractional_packets_with_max_packets_only
TEST_ASSERT_EQUAL_UINT8(1, edpt_xfer_count);
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]));
+ complete_edpt(0x01);
+ TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS,
+ edpt_xfer_bytes[edpt_xfer_count - 1]));
}
TEST_ASSERT_EQUAL_UINT32(185, tuh_audio_write(0, 0, samples, 185));
while (edpt_xfer_count < 10) {
- edpt_busy = false;
- TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1]));
+ complete_edpt(0x01);
+ TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS,
+ edpt_xfer_bytes[edpt_xfer_count - 1]));
}
for (uint8_t i = 0; i < 9; i++) {
@@ -1065,6 +1149,62 @@ 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_applies_10_14_feedback_after_fractional_scheduling_loop(void) {
+ mount_descriptors(playback_44100_with_feedback_10_14, sizeof(playback_44100_with_feedback_10_14));
+
+ TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0));
+ TEST_ASSERT_TRUE(tuh_audio_start(0, 0));
+ complete_interface_set(XFER_RESULT_SUCCESS);
+ TEST_ASSERT_EQUAL_UINT16(3, edpt_xfer_bytes_at(0x81, 0));
+ TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 0));
+
+ // Request 44 frames/ms after the first nominal 44.1-kHz packet. The old
+ // cycle must still emit its 45-frame correction packet before changing rate.
+ complete_feedback_xfer(44u << 16, 3);
+ for (uint8_t i = 0; i < 10; i++) {
+ complete_playback_xfer();
+ }
+
+ for (uint8_t i = 0; i < 9; i++) {
+ TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, i));
+ }
+ TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9));
+ TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 10));
+}
+
+void test_audio_host_accepts_16_16_feedback(void) {
+ mount_descriptors(playback_44100_with_feedback_16_16, sizeof(playback_44100_with_feedback_16_16));
+
+ TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0));
+ TEST_ASSERT_TRUE(tuh_audio_start(0, 0));
+ complete_interface_set(XFER_RESULT_SUCCESS);
+ TEST_ASSERT_EQUAL_UINT16(4, edpt_xfer_bytes_at(0x81, 0));
+
+ complete_feedback_xfer(45u << 16, 4);
+ for (uint8_t i = 0; i < 10; i++) {
+ complete_playback_xfer();
+ }
+
+ TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9));
+ TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 10));
+}
+
+void test_audio_host_ignores_out_of_range_feedback(void) {
+ mount_descriptors(playback_44100_with_feedback_16_16, sizeof(playback_44100_with_feedback_16_16));
+
+ TEST_ASSERT_TRUE(tuh_audio_configure(0, 0, 0));
+ TEST_ASSERT_TRUE(tuh_audio_start(0, 0));
+ complete_interface_set(XFER_RESULT_SUCCESS);
+
+ complete_feedback_xfer(50u << 16, 4);
+ for (uint8_t i = 0; i < 10; i++) {
+ complete_playback_xfer();
+ }
+
+ TEST_ASSERT_EQUAL_UINT16(180, edpt_xfer_bytes_at(0x01, 9));
+ TEST_ASSERT_EQUAL_UINT16(176, edpt_xfer_bytes_at(0x01, 10));
+}
+
void test_audio_host_uses_exponential_full_speed_iso_interval(void) {
uint8_t samples[441 * 4] = {0};
mount_descriptors(playback_11025_interval4, sizeof(playback_11025_interval4));
@@ -1075,15 +1215,13 @@ void test_audio_host_uses_exponential_full_speed_iso_interval(void) {
complete_interface_set(XFER_RESULT_SUCCESS);
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]));
+ complete_edpt(0x01);
+ TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1]));
}
TEST_ASSERT_EQUAL_UINT32(185, tuh_audio_write(0, 0, samples, 185));
while (edpt_xfer_count < 10) {
- edpt_busy = false;
- TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS,
- edpt_xfer_bytes[edpt_xfer_count - 1]));
+ complete_edpt(0x01);
+ TEST_ASSERT_TRUE(audioh_xfer_cb(AUDIO_DEV_ADDR, 0x01, XFER_RESULT_SUCCESS, edpt_xfer_bytes[edpt_xfer_count - 1]));
}
for (uint8_t i = 0; i < 9; i++) {
@@ -1107,7 +1245,7 @@ void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) {
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;
+ complete_edpt(0x01);
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);
@@ -1115,7 +1253,7 @@ void test_audio_host_sends_silence_when_playback_fifo_has_too_few_frames(void) {
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;
+ complete_edpt(0x01);
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);