diff --git a/README.md b/README.md index 9a1947d..aa85007 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,11 @@ in C by another native module, the interpreter need not carry it at all: `UacHostOutput.c_sink()` hands that module a sink it writes from its own task, described in [src/pcm_c_sink.h](src/pcm_c_sink.h) ([#43](https://github.com/PyDevices/usbif/issues/43)). Not yet run on -hardware. +hardware. A hosted device can play and record at once: open +`uac_audio.output(dev)` and `uac_audio.input(dev)` side by side, on one +device or two. On an ESP32-P4 hosting a USB-C headset dongle with its output +looped into its mic, both ran 30 s with not one packet lost either way +([tests/hardware/uac_duplex_loopback.py](tests/hardware/uac_duplex_loopback.py)). **Not yet working, said precisely:** macOS desktop support, which sits at the [community-verified diff --git a/lib/usbif/uac_audio.py b/lib/usbif/uac_audio.py index 3c613bd..d6bbca5 100644 --- a/lib/usbif/uac_audio.py +++ b/lib/usbif/uac_audio.py @@ -136,8 +136,27 @@ def _ring_bytes(stream, rate, ring_ms): return max(1, frames) * frame +def _per_stream(call, endpoint): + """``call(endpoint)``: the driver keeps one stream per direction. + + Playback and capture can run at once, on one device or two, so the calls + that could mean either name the stream by its endpoint. Firmware from + before that kept one stream and takes no argument; there the call means + the only stream there is. + """ + try: + return call(endpoint) + except TypeError: + return call() + + class _UacHostMixin: - """Shared open/close over the native UAC host driver.""" + """Shared open/close over the native UAC host driver. + + One `output` and one `input` can be open together, on the same device + (a headset, or a sound card's output looped into its input) or on two. + Closing one leaves the other running. + """ def _uac_open(self, stream): _usbif.host_uac_open(self._dev_id, stream.interface, stream.alt, @@ -147,7 +166,7 @@ def _uac_open(self, stream): _ring_bytes(stream, self._rate, self._ring_ms)) def _close(self): - _usbif.host_uac_close() + _per_stream(_usbif.host_uac_close, self._stream.endpoint) @property def stream(self): @@ -156,7 +175,7 @@ def stream(self): def capacity(self): """The ring's size in bytes, as opened; 0 while closed.""" - return max(0, _usbif.host_uac_capacity()) + return max(0, _per_stream(_usbif.host_uac_capacity, self._stream.endpoint)) def stats(self): """``(packets, bytes, dropped, starved, errors, empty)`` from the driver. @@ -165,9 +184,10 @@ def stats(self): of three things, and lumping them together loses the answer: the ring overflowed because Python was late (``dropped``), the ring was empty when the bus asked (``starved``), or the bus itself reported a bad - packet (``errors``). + packet (``errors``). A capture stream also counts ``empty``: packets + that completed carrying nothing. Each direction has its own counters. """ - return _usbif.host_uac_stats() + return _per_stream(_usbif.host_uac_stats, self._stream.endpoint) class UacHostOutput(_UacHostMixin, PCMOutput): @@ -213,13 +233,13 @@ def try_write(self, buf): def space(self): """Bytes a write can take without coming up short, in whole frames.""" - room = _usbif.host_uac_space() + room = _per_stream(_usbif.host_uac_space, self._stream.endpoint) if room <= 0: return 0 return room - room % self.format.frame_size def queued_size(self): - queued = _usbif.host_uac_queued() + queued = _per_stream(_usbif.host_uac_queued, self._stream.endpoint) return max(0, queued) def c_sink(self): @@ -240,7 +260,11 @@ def c_sink(self): class UacHostInput(_UacHostMixin, PCMInput): - """A hosted USB microphone, as a ``PCMInput``.""" + """A hosted USB microphone, as a ``PCMInput``. + + It can run beside a `UacHostOutput`, on the same device or another. + `available` says how many captured bytes are waiting. + """ def __init__(self, dev_id, stream, rate, *, ring_ms=None, **kwargs): PCMInput.__init__(self, AudioFormat(rate, stream.channels, stream.bits), @@ -256,6 +280,10 @@ def _open(self): def _readinto(self, buf): return _usbif.host_uac_read(buf) + def available(self): + """Captured bytes waiting in the ring, ready for a read.""" + return max(0, _per_stream(_usbif.host_uac_queued, self._stream.endpoint)) + def output(dev_id, *, rate=None, channels=None, bits=None, ring_ms=None, **kwargs): """Open a hosted USB audio device for playback. diff --git a/src/mod_usbif.c b/src/mod_usbif.c index 52f7751..961efe4 100644 --- a/src/mod_usbif.c +++ b/src/mod_usbif.c @@ -92,13 +92,13 @@ extern int usbif_host_uac_open(uint32_t dev_id, uint8_t itf, uint8_t alt, uint8_ uint32_t ring_bytes); extern int usbif_host_uac_read(uint8_t *out, size_t max); extern int usbif_host_uac_write(const uint8_t *data, size_t len); -extern int usbif_host_uac_queued(void); -extern int usbif_host_uac_space(void); -extern int usbif_host_uac_capacity(void); +extern int usbif_host_uac_queued(int dir); +extern int usbif_host_uac_space(int dir); +extern int usbif_host_uac_capacity(int dir); extern int usbif_host_uac_c_sink(pcm_c_sink_t *out, uint32_t channels, uint32_t bits); -extern void usbif_host_uac_stats(uint32_t *packets, uint32_t *bytes, uint32_t *dropped, +extern void usbif_host_uac_stats(int dir, uint32_t *packets, uint32_t *bytes, uint32_t *dropped, uint32_t *starved, uint32_t *errors, uint32_t *empty); -extern void usbif_host_uac_close(void); +extern void usbif_host_uac_close(int dir); extern int usbif_host_uvc_negotiate(uint32_t dev_id, uint8_t itf, uint8_t format_index, uint8_t frame_index, uint32_t interval, uint32_t *payload_out, uint32_t *frame_out); extern int usbif_host_uvc_open(uint32_t dev_id, uint8_t itf, uint8_t alt, uint8_t ep, @@ -904,10 +904,12 @@ static mp_obj_t usbif_host_uac_open_py(size_t n_args, const mp_obj_t *args) { n_args > 8 ? (uint16_t)mp_obj_get_int(args[8]) : 0, // bytes per audio frame n_args > 9 ? (uint32_t)mp_obj_get_int(args[9]) : 0); // ring bytes, 0 = 8 KB if (rc != 0) { - // Carry the driver's own code: -1 already open, -2 bad packet size, - // -3 no such device, -4 alt 0 carries no endpoint, -5 claim refused, - // -6 transfer alloc failed, -7 nothing submitted, -8 no memory for - // the ring, -9 ring size out of range (two packets to 4 MB). + // Carry the driver's own code: -1 a stream in this direction is + // already open, -2 bad packet size, -3 no such device, -4 alt 0 + // carries no endpoint, -5 claim refused, -6 transfer alloc failed, + // -7 nothing submitted, -8 no memory for the ring, -9 ring size out + // of range (two packets to 4 MB), -10 the other direction's stream + // already holds this interface. if (rc == -5) { // IDF answers ESP_ERR_NOT_SUPPORTED to a claim whose endpoint // packet exceeds the FIFO the host was installed with, and says @@ -928,6 +930,20 @@ static mp_obj_t usbif_host_uac_open_py(size_t n_args, const mp_obj_t *args) { } static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usbif_host_uac_open_obj, 5, 10, usbif_host_uac_open_py); +// The calls below act on one of the two streams the driver keeps, one per +// direction. Each takes the stream's endpoint address, optionally: bit 7 +// picks it, as on the wire. Without one they mean what they meant when there +// was a single stream: whichever is open, playback first; close() closes both. +// read() and write() need no address: only capture reads, only playback writes. +#if USBIF_HAVE_HOST +static int usbif_host_uac_dir(size_t n_args, const mp_obj_t *args) { + if (n_args == 0 || args[0] == mp_const_none) { + return USBIF_HOST_UAC_ANY; + } + return (mp_obj_get_int(args[0]) & 0x80) ? USBIF_HOST_UAC_IN : USBIF_HOST_UAC_OUT; +} +#endif + static mp_obj_t usbif_host_uac_read_py(mp_obj_t buf_in) { #if USBIF_HAVE_HOST mp_buffer_info_t buf; @@ -960,35 +976,38 @@ static mp_obj_t usbif_host_uac_write_py(mp_obj_t buf_in) { } static MP_DEFINE_CONST_FUN_OBJ_1(usbif_host_uac_write_obj, usbif_host_uac_write_py); -static mp_obj_t usbif_host_uac_queued_py(void) { +static mp_obj_t usbif_host_uac_queued_py(size_t n_args, const mp_obj_t *args) { #if USBIF_HAVE_HOST - return MP_OBJ_NEW_SMALL_INT(usbif_host_uac_queued()); + return MP_OBJ_NEW_SMALL_INT(usbif_host_uac_queued(usbif_host_uac_dir(n_args, args))); #else + (void)n_args; (void)args; mp_raise_OSError(MP_EOPNOTSUPP); #endif } -static MP_DEFINE_CONST_FUN_OBJ_0(usbif_host_uac_queued_obj, usbif_host_uac_queued_py); +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usbif_host_uac_queued_obj, 0, 1, usbif_host_uac_queued_py); // Free room and total size of the stream's ring, in bytes; -1 when no stream // is open. A caller that must not block asks space() before it writes // (usbif#36), rather than hardcoding a size the ring no longer has. -static mp_obj_t usbif_host_uac_space_py(void) { +static mp_obj_t usbif_host_uac_space_py(size_t n_args, const mp_obj_t *args) { #if USBIF_HAVE_HOST - return MP_OBJ_NEW_SMALL_INT(usbif_host_uac_space()); + return MP_OBJ_NEW_SMALL_INT(usbif_host_uac_space(usbif_host_uac_dir(n_args, args))); #else + (void)n_args; (void)args; mp_raise_OSError(MP_EOPNOTSUPP); #endif } -static MP_DEFINE_CONST_FUN_OBJ_0(usbif_host_uac_space_obj, usbif_host_uac_space_py); +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usbif_host_uac_space_obj, 0, 1, usbif_host_uac_space_py); -static mp_obj_t usbif_host_uac_capacity_py(void) { +static mp_obj_t usbif_host_uac_capacity_py(size_t n_args, const mp_obj_t *args) { #if USBIF_HAVE_HOST - return MP_OBJ_NEW_SMALL_INT(usbif_host_uac_capacity()); + return MP_OBJ_NEW_SMALL_INT(usbif_host_uac_capacity(usbif_host_uac_dir(n_args, args))); #else + (void)n_args; (void)args; mp_raise_OSError(MP_EOPNOTSUPP); #endif } -static MP_DEFINE_CONST_FUN_OBJ_0(usbif_host_uac_capacity_obj, usbif_host_uac_capacity_py); +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usbif_host_uac_capacity_obj, 0, 1, usbif_host_uac_capacity_py); // host_uac_c_sink(channels, bits) -> bytes: the open playback stream's // pcm_c_sink_t, by value (usbif#43). Another native module copies it with @@ -1016,10 +1035,11 @@ static MP_DEFINE_CONST_FUN_OBJ_2(usbif_host_uac_c_sink_obj, usbif_host_uac_c_sin // separate the three answers: the ring overflowed because Python was late // (dropped), the ring was empty when the bus asked (starved), or the bus // itself reported a bad packet (errors). -static mp_obj_t usbif_host_uac_stats_py(void) { +static mp_obj_t usbif_host_uac_stats_py(size_t n_args, const mp_obj_t *args) { #if USBIF_HAVE_HOST uint32_t packets = 0, bytes = 0, dropped = 0, starved = 0, errors = 0, empty = 0; - usbif_host_uac_stats(&packets, &bytes, &dropped, &starved, &errors, &empty); + usbif_host_uac_stats(usbif_host_uac_dir(n_args, args), + &packets, &bytes, &dropped, &starved, &errors, &empty); mp_obj_t items[6] = { mp_obj_new_int_from_uint(packets), mp_obj_new_int_from_uint(bytes), @@ -1030,10 +1050,11 @@ static mp_obj_t usbif_host_uac_stats_py(void) { }; return mp_obj_new_tuple(6, items); #else + (void)n_args; (void)args; return mp_const_none; #endif } -static MP_DEFINE_CONST_FUN_OBJ_0(usbif_host_uac_stats_obj, usbif_host_uac_stats_py); +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usbif_host_uac_stats_obj, 0, 1, usbif_host_uac_stats_py); // (dev_id, control_itf, clock_id) -> a flat tuple of (min, max, res) triplets // from a USB Audio 2.0 clock source; usbif.uac.rates_from_ranges() reads it. @@ -1058,13 +1079,15 @@ static mp_obj_t usbif_host_uac_clock_ranges_py(mp_obj_t dev_in, mp_obj_t itf_in, } static MP_DEFINE_CONST_FUN_OBJ_3(usbif_host_uac_clock_ranges_obj, usbif_host_uac_clock_ranges_py); -static mp_obj_t usbif_host_uac_close_py(void) { +static mp_obj_t usbif_host_uac_close_py(size_t n_args, const mp_obj_t *args) { #if USBIF_HAVE_HOST - usbif_host_uac_close(); + usbif_host_uac_close(usbif_host_uac_dir(n_args, args)); + #else + (void)n_args; (void)args; #endif return mp_const_none; } -static MP_DEFINE_CONST_FUN_OBJ_0(usbif_host_uac_close_obj, usbif_host_uac_close_py); +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(usbif_host_uac_close_obj, 0, 1, usbif_host_uac_close_py); // --- UVC host ----------------------------------------------------------- diff --git a/src/usbif_classes.h b/src/usbif_classes.h index 5f1c49b..4ca4284 100644 --- a/src/usbif_classes.h +++ b/src/usbif_classes.h @@ -15,4 +15,11 @@ #define USBIF_CLASS_UAC (1u << 4) #define USBIF_CLASS_UVC (1u << 5) +// Which hosted UAC stream a call means (usbif_host_uac.c keeps one per +// direction). ANY is the meaning the calls had when there was one stream: +// whichever is open, playback first; for close, both. +#define USBIF_HOST_UAC_ANY (-1) +#define USBIF_HOST_UAC_OUT (0) +#define USBIF_HOST_UAC_IN (1) + #endif // USBIF_CLASSES_H diff --git a/src/usbif_host_uac.c b/src/usbif_host_uac.c index af85a59..14d75d7 100644 --- a/src/usbif_host_uac.c +++ b/src/usbif_host_uac.c @@ -47,6 +47,7 @@ #include "shared/usbif_byte_ring.h" #include "shared/usbif_pcm_sink.h" #include "pcm_c_sink.h" +#include "usbif_classes.h" extern usb_host_client_handle_t usbif_host_client_get(void); extern void usbif_host_lock(void); @@ -160,15 +161,29 @@ typedef struct { // stream sounds wrong the first question is always whether bytes are // being lost, and where. volatile uint32_t packets, bytes, dropped, starved, errors, empty; + uint32_t cb_count; // the first few callbacks are printed } usbif_uac_host_t; -static usbif_uac_host_t usbif_uach; +// Two streams, one per direction, so a board can play to a device and record +// from it at once -- a headset, or a DAC with its output looped back into its +// input -- or play to one device while recording from another. Each has its +// own interface, endpoint, transfers, ring and counters; a transfer carries +// its stream in `context`, so the callbacks never have to ask which one. +// One per direction rather than a pool because that is what the class needs: +// a second playback stream to another device is a mixer's job, not a bus's. +#define USBIF_UAC_OUT (0) +#define USBIF_UAC_IN (1) +static usbif_uac_host_t usbif_uach[2]; + +static inline usbif_uac_host_t *usbif_uac_slot(uint8_t ep) { + return &usbif_uach[(ep & 0x80) ? USBIF_UAC_IN : USBIF_UAC_OUT]; +} // The playback ring's writers go through this gate (usbif#43), the -// interpreter's and a usermod task's alike. It lives outside usbif_uach -// because open() zeroes that, and the gate's generation count must survive -// from one stream to the next: a sink issued for a stream that has closed -// stays closed. See shared/usbif_pcm_sink.h for the guarantee. +// interpreter's and a usermod task's alike. Only the OUT stream has one. It +// lives outside usbif_uach because open() zeroes that stream, and the gate's +// generation count must survive from one stream to the next: a sink issued +// for a stream that has closed stays closed. See shared/usbif_pcm_sink.h for the guarantee. static usbif_pcm_sink_t usbif_uac_sink; static bool usbif_uac_sink_ready; @@ -178,7 +193,7 @@ static void usbif_uac_sink_wait(void) { static void usbif_uac_sink_setup(void) { if (!usbif_uac_sink_ready) { - usbif_pcm_sink_init(&usbif_uac_sink, &usbif_uach.ring, usbif_uac_sink_wait); + usbif_pcm_sink_init(&usbif_uac_sink, &usbif_uach[USBIF_UAC_OUT].ring, usbif_uac_sink_wait); usbif_uac_sink_ready = true; } } @@ -189,25 +204,25 @@ static void usbif_uac_sink_setup(void) { // Python reads; on OUT the reverse. The ring itself is shared/usbif_byte_ring, // tested on the host; this file only sizes and places its storage. -static inline uint32_t usbif_uac_ring_used(void) { - return usbif_byte_ring_used(&usbif_uach.ring); +static inline uint32_t usbif_uac_ring_used(usbif_uac_host_t *s) { + return usbif_byte_ring_used(&s->ring); } -static inline uint32_t usbif_uac_ring_free(void) { - return usbif_byte_ring_free(&usbif_uach.ring); +static inline uint32_t usbif_uac_ring_free(usbif_uac_host_t *s) { + return usbif_byte_ring_free(&s->ring); } -static void usbif_uac_ring_push(const uint8_t *data, uint32_t len) { - if (usbif_byte_ring_push(&usbif_uach.ring, data, len) < len) { - usbif_uach.dropped++; // the tail of this packet, not the ring +static void usbif_uac_ring_push(usbif_uac_host_t *s, const uint8_t *data, uint32_t len) { + if (usbif_byte_ring_push(&s->ring, data, len) < len) { + s->dropped++; // the tail of this packet, not the ring } } -static uint32_t usbif_uac_ring_pop(uint8_t *out, uint32_t max) { - return usbif_byte_ring_pop(&usbif_uach.ring, out, max); +static uint32_t usbif_uac_ring_pop(usbif_uac_host_t *s, uint8_t *out, uint32_t max) { + return usbif_byte_ring_pop(&s->ring, out, max); } -static bool usbif_uac_ring_alloc(uint32_t size) { +static bool usbif_uac_ring_alloc(usbif_uac_host_t *s, uint32_t size) { const uint32_t internal = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; const uint32_t psram = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; const bool small = size <= USBIF_UAC_RING_INTERNAL; @@ -218,63 +233,63 @@ static bool usbif_uac_ring_alloc(uint32_t size) { if (mem == NULL) { return false; } - usbif_uach.ring_mem = mem; - usbif_byte_ring_init(&usbif_uach.ring, mem, size); + s->ring_mem = mem; + usbif_byte_ring_init(&s->ring, mem, size); return true; } -static void usbif_uac_ring_release(void) { - if (usbif_uach.ring_mem) { - heap_caps_free(usbif_uach.ring_mem); - usbif_uach.ring_mem = NULL; +static void usbif_uac_ring_release(usbif_uac_host_t *s) { + if (s->ring_mem) { + heap_caps_free(s->ring_mem); + s->ring_mem = NULL; } - usbif_byte_ring_init(&usbif_uach.ring, NULL, 0); + usbif_byte_ring_init(&s->ring, NULL, 0); } // --- transfer plumbing -------------------------------------------------- -static void usbif_uac_prepare(usb_transfer_t *xfer) { +static void usbif_uac_prepare(usbif_uac_host_t *s, usb_transfer_t *xfer) { // A capture transfer asks for a full packet in every interval. A playback // transfer carries whatever the ring has: a short packet is legal and is // how an underrun is expressed on the wire, rather than by stalling. - xfer->device_handle = usbif_uach.dev; - xfer->bEndpointAddress = usbif_uach.ep; + xfer->device_handle = s->dev; + xfer->bEndpointAddress = s->ep; // num_isoc_packets is const in usb_transfer_t: it is fixed by // usb_host_transfer_alloc() and describes how the buffer is carved up, so // it is a property of the allocation rather than of this submission. uint32_t total = 0; for (int i = 0; i < USBIF_UAC_PKTS_PER_XFER; i++) { - uint32_t want = usbif_uach.mps; - if (!usbif_uach.is_in && usbif_uach.frame && usbif_uach.rate) { + uint32_t want = s->mps; + if (!s->is_in && s->frame && s->rate) { // One millisecond of audio per packet, the remainder carried: // 44.1 kHz sends 44 frames then 45 in the right proportion. The // host is full-speed today (usbif#3 parks the P4's), so the // interval is a millisecond. - usbif_uach.acc += usbif_uach.rate; - const uint32_t frames = usbif_uach.acc / 1000; - usbif_uach.acc -= frames * 1000; - want = frames * usbif_uach.frame; - if (want > usbif_uach.mps) { - want = usbif_uach.mps; + s->acc += s->rate; + const uint32_t frames = s->acc / 1000; + s->acc -= frames * 1000; + want = frames * s->frame; + if (want > s->mps) { + want = s->mps; } } - if (!usbif_uach.is_in) { + if (!s->is_in) { // Playback always sends a full packet. Where the ring is short, // the remainder is silence -- an underrun in audio is silence, // not a stall, and the stream must keep its slot on the bus. - uint32_t have = usbif_uac_ring_used(); + uint32_t have = usbif_uac_ring_used(s); uint32_t take = have < want ? have : want; - if (usbif_uach.frame) { + if (s->frame) { // Whole frames only: popping half a frame into a packet // padded with silence would shift every sample after it. - take -= take % usbif_uach.frame; + take -= take % s->frame; } if (take) { - usbif_uac_ring_pop(xfer->data_buffer + total, take); + usbif_uac_ring_pop(s, xfer->data_buffer + total, take); } if (take < want) { memset(xfer->data_buffer + total + take, 0, want - take); - usbif_uach.starved++; + s->starved++; } } xfer->isoc_packet_desc[i].num_bytes = want; @@ -288,59 +303,58 @@ static void usbif_uac_prepare(usb_transfer_t *xfer) { xfer->num_bytes = total; } -static volatile uint32_t usbif_uac_cb_count; - static void usbif_uac_cb(usb_transfer_t *xfer) { - if (usbif_uac_cb_count < 4) { - printf("usbif_uac: cb#%u status=%d actual=%d pkts=%d pkt0=%d/%d\n", - (unsigned)usbif_uac_cb_count, (int)xfer->status, + usbif_uac_host_t *s = (usbif_uac_host_t *)xfer->context; + if (s->cb_count < 4) { + printf("usbif_uac: %s cb#%u status=%d actual=%d pkts=%d pkt0=%d/%d\n", + s->is_in ? "in" : "out", (unsigned)s->cb_count, (int)xfer->status, (int)xfer->actual_num_bytes, (int)xfer->num_isoc_packets, (int)xfer->isoc_packet_desc[0].status, (int)xfer->isoc_packet_desc[0].actual_num_bytes); } - usbif_uac_cb_count++; - if (!usbif_uach.open) { - usbif_uach.inflight--; + s->cb_count++; + if (!s->open) { + s->inflight--; return; } - if (usbif_uach.is_in) { + if (s->is_in) { uint32_t offset = 0; for (int i = 0; i < xfer->num_isoc_packets; i++) { usb_isoc_packet_desc_t *pkt = &xfer->isoc_packet_desc[i]; if (pkt->status == USB_TRANSFER_STATUS_COMPLETED && pkt->actual_num_bytes) { - usbif_uac_ring_push(xfer->data_buffer + offset, pkt->actual_num_bytes); - usbif_uach.bytes += pkt->actual_num_bytes; - usbif_uach.packets++; + usbif_uac_ring_push(s, xfer->data_buffer + offset, pkt->actual_num_bytes); + s->bytes += pkt->actual_num_bytes; + s->packets++; } else if (pkt->status != USB_TRANSFER_STATUS_COMPLETED) { - usbif_uach.errors++; + s->errors++; } else { // Completed carrying nothing. Counted separately because it // is neither success nor error, and leaving it uncounted made // every statistic read zero while the device streamed // silence -- indistinguishable from never having started. - usbif_uach.empty++; + s->empty++; } // Packets are laid out at their *requested* size, not their // actual one -- the next packet's data starts where this one's // buffer ended, however few bytes actually arrived. Advancing by // actual_num_bytes here is the classic isochronous read bug and // produces audio that is subtly, progressively wrong. - offset += usbif_uach.mps; + offset += s->mps; } } else { for (int i = 0; i < xfer->num_isoc_packets; i++) { if (xfer->isoc_packet_desc[i].status == USB_TRANSFER_STATUS_COMPLETED) { - usbif_uach.bytes += xfer->isoc_packet_desc[i].actual_num_bytes; - usbif_uach.packets++; + s->bytes += xfer->isoc_packet_desc[i].actual_num_bytes; + s->packets++; } else { - usbif_uach.errors++; + s->errors++; } } } - usbif_uac_prepare(xfer); + usbif_uac_prepare(s, xfer); if (usb_host_transfer_submit(xfer) != ESP_OK) { - usbif_uach.inflight--; - usbif_uach.errors++; + s->inflight--; + s->errors++; } } @@ -464,9 +478,9 @@ static int usbif_uac_control_on(usb_device_handle_t dev, uint8_t req_type, uint8 return rc; } -static int usbif_uac_control(uint8_t req_type, uint8_t request, uint16_t value, - uint16_t index, const uint8_t *payload, uint16_t len) { - return usbif_uac_control_on(usbif_uach.dev, req_type, request, value, index, +static int usbif_uac_control(usbif_uac_host_t *s, uint8_t req_type, uint8_t request, + uint16_t value, uint16_t index, const uint8_t *payload, uint16_t len) { + return usbif_uac_control_on(s->dev, req_type, request, value, index, payload, len, NULL, NULL); } @@ -480,32 +494,32 @@ static int usbif_uac_control(uint8_t req_type, uint8_t request, uint16_t value, // polls and every isochronous packet completes with zero bytes. Measured // exactly that way before this call existed: submits fine, callbacks fire, // status COMPLETED, actual_num_bytes 0, forever. -static int usbif_uac_set_interface(uint8_t itf, uint8_t alt) { - return usbif_uac_control(0x01, 0x0B, alt, itf, NULL, 0); +static int usbif_uac_set_interface(usbif_uac_host_t *s, uint8_t itf, uint8_t alt) { + return usbif_uac_control(s, 0x01, 0x0B, alt, itf, NULL, 0); } // Ask the device to run at `rate`. UAC 1.0 puts sampling frequency in an // *endpoint* control, three bytes little-endian. A device with a single fixed // rate may STALL this, which is not fatal -- it is already running at the only // rate it has -- so the result is reported and not treated as failure. -static int usbif_uac_set_rate(uint32_t rate) { - if (usbif_uach.clock) { +static int usbif_uac_set_rate(usbif_uac_host_t *s, uint32_t rate) { + if (s->clock) { // 2.0: CUR on the clock source, four bytes. uint8_t cur[4] = { (uint8_t)(rate & 0xFF), (uint8_t)((rate >> 8) & 0xFF), (uint8_t)((rate >> 16) & 0xFF), (uint8_t)((rate >> 24) & 0xFF), }; - return usbif_uac_control(UAC2_REQTYPE_SET_ITF, UAC2_CUR, + return usbif_uac_control(s, UAC2_REQTYPE_SET_ITF, UAC2_CUR, (uint16_t)(UAC2_CS_SAM_FREQ_CONTROL << 8), - (uint16_t)((usbif_uach.clock << 8) | usbif_uach.control), cur, 4); + (uint16_t)((s->clock << 8) | s->control), cur, 4); } uint8_t payload[3] = { (uint8_t)(rate & 0xFF), (uint8_t)((rate >> 8) & 0xFF), (uint8_t)((rate >> 16) & 0xFF), }; - return usbif_uac_control(UAC_REQTYPE_SET_EP, UAC_SET_CUR, - (uint16_t)(UAC_SAMPLING_FREQ_CONTROL << 8), usbif_uach.ep, payload, 3); + return usbif_uac_control(s, UAC_REQTYPE_SET_EP, UAC_SET_CUR, + (uint16_t)(UAC_SAMPLING_FREQ_CONTROL << 8), s->ep, payload, 3); } // --- public API --------------------------------------------------------- @@ -560,9 +574,13 @@ int usbif_host_uac_clock_ranges(uint32_t dev_id, uint8_t control_itf, uint8_t cl static int usbif_host_uac_open_locked(uint32_t dev_id, uint8_t itf, uint8_t alt, uint8_t ep, uint16_t mps, uint32_t rate, uint8_t clock, uint8_t control, uint16_t frame, uint32_t ring_bytes) { - if (usbif_uach.open) { + // The endpoint's direction picks the stream: one playback and one capture + // may run at once, on one device or on two. + usbif_uac_host_t *s = usbif_uac_slot(ep); + if (s->open) { return -1; } + const usbif_uac_host_t *other = &usbif_uach[(ep & 0x80) ? USBIF_UAC_OUT : USBIF_UAC_IN]; if (mps == 0 || mps > USBIF_UAC_MAX_MPS) { return -2; } @@ -580,88 +598,97 @@ static int usbif_host_uac_open_locked(uint32_t dev_id, uint8_t itf, uint8_t alt, if (usbif_host_dev_lookup(dev_id, &dev) != 0) { return -3; } - memset(&usbif_uach, 0, sizeof(usbif_uach)); - usbif_uach.dev = dev; - usbif_uach.itf = itf; - usbif_uach.alt = alt; - usbif_uach.ep = ep; - usbif_uach.mps = mps; - usbif_uach.rate = rate; - usbif_uach.clock = clock; - usbif_uach.control = control; - usbif_uach.frame = frame; - usbif_uach.acc = 0; - usbif_uach.is_in = (ep & 0x80) != 0; - - // Claiming with the chosen alternate setting is what starts the device - // reserving isochronous bandwidth. Alt 0 carries no endpoint by - // specification, so claiming it and then submitting would fail in a way - // that looks like a driver bug rather than a wrong argument. + // Alt 0 carries no endpoint by specification, so claiming it and then + // submitting would fail in a way that looks like a driver bug rather than + // a wrong argument. if (alt == 0) { return -4; } - if (!usbif_uac_ring_alloc(ring_bytes)) { + // The two streams of one device are two interfaces. A device that put + // both directions on one interface would have them share an alternate + // setting, and claiming it twice is not something either stream can own. + if (other->open && other->dev == dev && other->itf == itf) { + return -10; + } + memset(s, 0, sizeof(*s)); + s->dev = dev; + s->itf = itf; + s->alt = alt; + s->ep = ep; + s->mps = mps; + s->rate = rate; + s->clock = clock; + s->control = control; + s->frame = frame; + s->acc = 0; + s->is_in = (ep & 0x80) != 0; + + // Claiming with the chosen alternate setting is what starts the device + // reserving isochronous bandwidth. + if (!usbif_uac_ring_alloc(s, ring_bytes)) { return -8; } esp_err_t cerr = usb_host_interface_claim(usbif_host_client_get(), dev, itf, alt); printf("usbif_uac: interface_claim(itf=%u alt=%u) -> 0x%x\n", (unsigned)itf, (unsigned)alt, (unsigned)cerr); if (cerr != ESP_OK) { - usbif_uac_ring_release(); + usbif_uac_ring_release(s); return -5; } - int sif = usbif_uac_set_interface(itf, alt); + int sif = usbif_uac_set_interface(s, itf, alt); printf("usbif_uac: SET_INTERFACE(%u, %u) -> %d\n", (unsigned)itf, (unsigned)alt, sif); if (rate) { - usbif_uac_set_rate(rate); // advisory: a fixed-rate device may STALL + usbif_uac_set_rate(s, rate); // advisory: a fixed-rate device may STALL } const size_t buf = (size_t)mps * USBIF_UAC_PKTS_PER_XFER; for (int i = 0; i < USBIF_UAC_NUM_XFER; i++) { - if (usb_host_transfer_alloc(buf, USBIF_UAC_PKTS_PER_XFER, &usbif_uach.xfer[i]) != ESP_OK) { + if (usb_host_transfer_alloc(buf, USBIF_UAC_PKTS_PER_XFER, &s->xfer[i]) != ESP_OK) { for (int j = 0; j < i; j++) { - usb_host_transfer_free(usbif_uach.xfer[j]); - usbif_uach.xfer[j] = NULL; + usb_host_transfer_free(s->xfer[j]); + s->xfer[j] = NULL; } usb_host_interface_release(usbif_host_client_get(), dev, itf); - usbif_uac_ring_release(); + usbif_uac_ring_release(s); return -6; } - usbif_uach.xfer[i]->callback = usbif_uac_cb; + s->xfer[i]->callback = usbif_uac_cb; + s->xfer[i]->context = s; } - usbif_uach.open = true; - if (!usbif_uach.is_in) { + s->open = true; + if (!s->is_in) { usbif_uac_sink_setup(); usbif_pcm_sink_open(&usbif_uac_sink, frame); } - printf("usbif_uac: claimed itf %u alt %u ep 0x%02x mps %u rate %u ring %u (%s)\n", + printf("usbif_uac: claimed itf %u alt %u ep 0x%02x mps %u rate %u ring %u (%s)%s\n", (unsigned)itf, (unsigned)alt, (unsigned)ep, (unsigned)mps, (unsigned)rate, (unsigned)ring_bytes, - esp_ptr_external_ram(usbif_uach.ring_mem) ? "psram" : "internal"); + esp_ptr_external_ram(s->ring_mem) ? "psram" : "internal", + other->open ? ", beside the other direction" : ""); for (int i = 0; i < USBIF_UAC_NUM_XFER; i++) { - usbif_uac_prepare(usbif_uach.xfer[i]); - esp_err_t serr = usb_host_transfer_submit(usbif_uach.xfer[i]); + usbif_uac_prepare(s, s->xfer[i]); + esp_err_t serr = usb_host_transfer_submit(s->xfer[i]); printf("usbif_uac: submit[%d] num_bytes=%d pkts=%d -> 0x%x\n", - i, (int)usbif_uach.xfer[i]->num_bytes, - (int)usbif_uach.xfer[i]->num_isoc_packets, (unsigned)serr); + i, (int)s->xfer[i]->num_bytes, + (int)s->xfer[i]->num_isoc_packets, (unsigned)serr); if (serr == ESP_OK) { - usbif_uach.inflight++; + s->inflight++; } else { - usbif_uach.errors++; + s->errors++; } } - if (usbif_uach.inflight == 0) { - usbif_uach.open = false; - if (usbif_uac_sink_ready) { + if (s->inflight == 0) { + s->open = false; + if (!s->is_in && usbif_uac_sink_ready) { usbif_pcm_sink_close(&usbif_uac_sink); } for (int i = 0; i < USBIF_UAC_NUM_XFER; i++) { - usb_host_transfer_free(usbif_uach.xfer[i]); - usbif_uach.xfer[i] = NULL; + usb_host_transfer_free(s->xfer[i]); + s->xfer[i] = NULL; } usb_host_interface_release(usbif_host_client_get(), dev, itf); - usbif_uac_ring_release(); + usbif_uac_ring_release(s); return -7; } return 0; @@ -677,15 +704,30 @@ int usbif_host_uac_open(uint32_t dev_id, uint8_t itf, uint8_t alt, uint8_t ep, return r; } +// Which stream a query means. `dir` is USBIF_HOST_UAC_OUT or _IN, or +// USBIF_HOST_UAC_ANY for the calls that predate two streams: then the one +// that is open, playback first when both are. NULL when that is none. +static usbif_uac_host_t *usbif_uac_pick(int dir) { + if (dir == USBIF_HOST_UAC_OUT || dir == USBIF_HOST_UAC_IN) { + usbif_uac_host_t *s = &usbif_uach[dir == USBIF_HOST_UAC_IN ? USBIF_UAC_IN : USBIF_UAC_OUT]; + return s->open ? s : NULL; + } + if (usbif_uach[USBIF_UAC_OUT].open) { + return &usbif_uach[USBIF_UAC_OUT]; + } + return usbif_uach[USBIF_UAC_IN].open ? &usbif_uach[USBIF_UAC_IN] : NULL; +} + int usbif_host_uac_read(uint8_t *out, size_t max) { - if (!usbif_uach.open || !usbif_uach.is_in) { + usbif_uac_host_t *s = &usbif_uach[USBIF_UAC_IN]; + if (!s->open) { return -1; } - return (int)usbif_uac_ring_pop(out, (uint32_t)max); + return (int)usbif_uac_ring_pop(s, out, (uint32_t)max); } int usbif_host_uac_write(const uint8_t *data, size_t len) { - if (!usbif_uach.open || usbif_uach.is_in) { + if (!usbif_uach[USBIF_UAC_OUT].open) { return -1; } // Through the gate, like a C sink's writes: a usermod's task may be @@ -712,8 +754,9 @@ static int usbif_uac_c_space(void *ctx) { // the caller (Python chose the format and knows them); rate and frame size // are the driver's own. 0, or -1 when no playback stream is open. int usbif_host_uac_c_sink(pcm_c_sink_t *out, uint32_t channels, uint32_t bits) { + const usbif_uac_host_t *s = &usbif_uach[USBIF_UAC_OUT]; uint32_t gen = usbif_uac_sink_ready ? usbif_pcm_sink_current(&usbif_uac_sink) : 0; - if (!usbif_uach.open || usbif_uach.is_in || gen == 0) { + if (!s->open || gen == 0) { return -1; } memset(out, 0, sizeof(*out)); @@ -722,91 +765,130 @@ int usbif_host_uac_c_sink(pcm_c_sink_t *out, uint32_t channels, uint32_t bits) { out->ctx = (void *)(uintptr_t)gen; out->write = usbif_uac_c_write; out->space = usbif_uac_c_space; - out->rate = usbif_uach.rate; + out->rate = s->rate; out->channels = channels; out->bits = bits; - out->frame_bytes = usbif_uach.frame; + out->frame_bytes = s->frame; return 0; } -int usbif_host_uac_queued(void) { - return usbif_uach.open ? (int)usbif_uac_ring_used() : -1; +int usbif_host_uac_queued(int dir) { + usbif_uac_host_t *s = usbif_uac_pick(dir); + return s ? (int)usbif_uac_ring_used(s) : -1; } -// Room for a write that will not be short, in bytes; -1 when closed. -int usbif_host_uac_space(void) { - if (usbif_uach.open && !usbif_uach.is_in) { +// Room for a write that will not be short, in bytes; -1 when closed. For a +// capture stream, the room left before the ring drops packets. +int usbif_host_uac_space(int dir) { + usbif_uac_host_t *s = usbif_uac_pick(dir); + if (s == NULL) { + return -1; + } + if (!s->is_in) { return usbif_pcm_sink_space(&usbif_uac_sink, usbif_pcm_sink_current(&usbif_uac_sink)); } - return usbif_uach.open ? (int)usbif_uac_ring_free() : -1; + return (int)usbif_uac_ring_free(s); } // The ring's size in bytes, as opened; -1 when closed. -int usbif_host_uac_capacity(void) { - return usbif_uach.open ? (int)usbif_uach.ring.size : -1; +int usbif_host_uac_capacity(int dir) { + usbif_uac_host_t *s = usbif_uac_pick(dir); + return s ? (int)s->ring.size : -1; } -void usbif_host_uac_stats(uint32_t *packets, uint32_t *bytes, uint32_t *dropped, +// The counters of a stream. They outlive its close, so a caller can read +// how a run went after stopping it; a direction never opened reads zeros. +// USBIF_HOST_UAC_ANY reads the open stream, or the playback one when +// neither is. +void usbif_host_uac_stats(int dir, uint32_t *packets, uint32_t *bytes, uint32_t *dropped, uint32_t *starved, uint32_t *errors, uint32_t *empty) { - *packets = usbif_uach.packets; - *bytes = usbif_uach.bytes; - *dropped = usbif_uach.dropped; - *starved = usbif_uach.starved; - *errors = usbif_uach.errors; - *empty = usbif_uach.empty; + const usbif_uac_host_t *s = usbif_uac_pick(dir); + if (s == NULL) { + s = &usbif_uach[dir == USBIF_HOST_UAC_IN ? USBIF_UAC_IN : USBIF_UAC_OUT]; + if (dir == USBIF_HOST_UAC_ANY && !s->packets && usbif_uach[USBIF_UAC_IN].packets) { + s = &usbif_uach[USBIF_UAC_IN]; + } + } + *packets = s->packets; + *bytes = s->bytes; + *dropped = s->dropped; + *starved = s->starved; + *errors = s->errors; + *empty = s->empty; } -static void usbif_host_uac_close_locked(void) { - if (!usbif_uach.open) { +// `tell_device`: send SET_INTERFACE alt 0 before releasing. Not from +// host_stop()'s teardown, where the host task that delivers control +// completions has already exited and the request could only time out. +static void usbif_host_uac_close_locked(usbif_uac_host_t *s, bool tell_device) { + if (!s->open) { return; } - usbif_uach.open = false; + s->open = false; // Let the in-flight transfers retire before their buffers go away: an // isochronous transfer is scheduled bus time, and freeing underneath one // is how a host stack gets corrupted rather than merely stopped. // Suspended for the same reason as the control wait: `inflight` only // falls when the host task delivers the completion callbacks, and this - // lock is what keeps it out. + // lock is what keeps it out. The other direction's stream, if any, keeps + // running throughout: its transfers and ring are its own. int drain_held = usbif_host_lock_suspend(); // First the producers: a usermod's task may be inside a write right now, // on the other core. This marks the sink closed and waits it out, so // from here on nothing but the transfer callbacks can reach the ring // (usbif#43, and the guarantee in shared/usbif_pcm_sink.h). - if (usbif_uac_sink_ready) { + if (!s->is_in && usbif_uac_sink_ready) { usbif_pcm_sink_close(&usbif_uac_sink); } const TickType_t drain_limit = USBIF_DELAY_TICKS(200); - for (TickType_t i = 0; i < drain_limit && usbif_uach.inflight; i++) { + for (TickType_t i = 0; i < drain_limit && s->inflight; i++) { vTaskDelay(1); } usbif_host_lock_resume(drain_held); for (int i = 0; i < USBIF_UAC_NUM_XFER; i++) { - if (usbif_uach.xfer[i]) { - usb_host_transfer_free(usbif_uach.xfer[i]); - usbif_uach.xfer[i] = NULL; + if (s->xfer[i]) { + usb_host_transfer_free(s->xfer[i]); + s->xfer[i] = NULL; } } - // Drop back to alt 0: the device stops reserving isochronous bandwidth, - // which matters on a full-speed bus where that reservation is the scarce - // resource every other device is competing for. - usb_host_interface_release(usbif_host_client_get(), usbif_uach.dev, usbif_uach.itf); + // Drop back to alt 0: the device stops streaming and stops reserving + // isochronous bandwidth, which matters on a full-speed bus where that + // reservation is the scarce resource every other device is competing + // for. Releasing the interface does not do this -- IDF sends no + // SET_INTERFACE at all, as the note on usbif_uac_set_interface() says -- + // so without it a closed stream left the device on its streaming + // setting, and the next open's SET_INTERFACE was no change to it. + if (tell_device) { + usbif_uac_set_interface(s, s->itf, 0); + } + usb_host_interface_release(usbif_host_client_get(), s->dev, s->itf); // Only once the transfers have retired: a callback still in flight pops // from this storage. One that outlived the drain above returns early on // `open` and never reaches the ring. - usbif_uac_ring_release(); + usbif_uac_ring_release(s); } -void usbif_host_uac_close(void) { +static void usbif_host_uac_close_dir(int dir, bool tell_device) { usbif_host_lock(); - usbif_host_uac_close_locked(); + if (dir != USBIF_HOST_UAC_IN) { + usbif_host_uac_close_locked(&usbif_uach[USBIF_UAC_OUT], tell_device); + } + if (dir != USBIF_HOST_UAC_OUT) { + usbif_host_uac_close_locked(&usbif_uach[USBIF_UAC_IN], tell_device); + } usbif_host_unlock(); } +// Close one direction's stream, or both with USBIF_HOST_UAC_ANY. +void usbif_host_uac_close(int dir) { + usbif_host_uac_close_dir(dir, true); +} + // Called from host_stop()'s teardown, with the event pump explicit because // the host task's own loop has already exited by then -- the same reason the // other class drivers have a _for_host_stop variant. void usbif_host_uac_close_for_host_stop(void) { - usbif_host_uac_close(); + usbif_host_uac_close_dir(USBIF_HOST_UAC_ANY, false); for (int i = 0; i < 10; i++) { usb_host_client_handle_events(usbif_host_client_get(), pdMS_TO_TICKS(10)); } diff --git a/tests/hardware/uac_duplex_loopback.py b/tests/hardware/uac_duplex_loopback.py new file mode 100644 index 0000000..c3223be --- /dev/null +++ b/tests/hardware/uac_duplex_loopback.py @@ -0,0 +1,357 @@ +"""UAC host duplex check: play to a USB sound card and record from it at once. + +Run on a board that hosts a USB audio device whose output is looped back into +its input: a USB-C DAC dongle with a headset splitter, its headphone plug in +its own mic jack. Nothing else should be connected to that output, and the +level stays moderate (``TONE_DBFS``). + + mpftp run --follow -d COM4 tests/hardware/uac_duplex_loopback.py + +Both streams stay open for the whole run, through four phases: + +1. **tone**: a quiet window, then 1 kHz and 1.5 kHz at ``TONE_DBFS``. For + each, the returned waveform's period (by autocorrelation) gives its + fundamental; the FFT peak, the zero-crossing rate and the level are + reported beside it. The quiet window is the control: it must show no + period at all. +2. **latency**: ``BURSTS`` short bursts, each written into a stream of + silence, timed from the write to the first captured sample above the + noise, in milliseconds. ``ring`` says how much of that was the output + ring ahead of the burst. +3. **soak**: ``SOAK_S`` seconds with the tone playing and capture read, + then each direction's counters over that window. The gate is zero + dropped, starved, errored and empty packets either way, and a packet + every millisecond. +4. **planted stall**: the same feed with a ``STALL_MS`` stall, which must + starve the output. It is the proof that phase 3's zero can fail. + +A headset dongle's capture path is its own business, and the one this was +written on processes its mic heavily: a 1 kHz sine loops back with a 1 ms +period but its 3rd harmonic on top, 1.5 kHz comes back repeating every 2 ms, +quiet steady tones are suppressed, and it sends no capture at all unless +playback is running. So the tone gate asks for the period and for spectral +peaks on harmonics of the tone, not for a clean sine. + +Logs stay in RAM until the end: a flash write stalls USB interrupts and costs +isochronous packets (usbif#49). Prints ``RESULT`` lines and a final +``GATE PASS`` or ``GATE FAIL``. +""" + +import array +import gc +import math +import time + +import usbif.auto +from usbif import uac_audio +from ulab import numpy as np +from ulab import utils + +RATE = 48000 +FRAME = 4 # stereo 16-bit +TONES = (1000, 1500) # each a whole number of samples per period +TONE_DBFS = -20 +BURST_DBFS = -12 +BURSTS = 10 +BURST_GAP_MS = 400 +SOAK_S = 30 +STALL_MS = 300 +OUT_RING_MS = 200 +IN_RING_MS = 2000 +DEEP = 150 * 192 # bytes kept queued when only continuity matters +SHALLOW = 40 * 192 # ... and when the ring's delay is being measured + +out_log = [] +_beat = [0] + + +def say(*args): + out_log.append(" ".join(str(a) for a in args)) + + +def beat(): + """A dot every 2 s: ``mpftp run --follow`` gives up on a quiet board. + + The UART carries it, never flash, so it costs the USB streams nothing. + """ + now = time.ticks_ms() + if time.ticks_diff(now, _beat[0]) >= 2000: + _beat[0] = now + print(".", end="") + + +def tone(hz, dbfs, frames): + """``frames`` of a stereo sine at ``hz``, as bytes.""" + amp = int(32767 * 10 ** (dbfs / 20)) + a = array.array("h", bytes(frames * FRAME)) + for i in range(frames): + v = int(amp * math.sin(2 * math.pi * hz * i / RATE)) + a[2 * i] = v + a[2 * i + 1] = v + return bytes(a) + + +class Rig: + def __init__(self, dev): + self.out = uac_audio.output(dev, rate=RATE, channels=2, bits=16, ring_ms=OUT_RING_MS) + self.mic = uac_audio.input(dev, rate=RATE, channels=2, bits=16, ring_ms=IN_RING_MS) + self.chunk = bytearray(8192) + self.read_frames = 0 + self.src = None + self.pos = 0 + + def play(self, src): + self.src = memoryview(src) + self.pos = 0 + + def feed(self, keep): + """Keep ``keep`` bytes queued from the current source, cyclically.""" + beat() + room = keep - self.out.queued_size() + while room >= FRAME: + take = min(room, len(self.src) - self.pos) + take -= take % FRAME + n = self.out.try_write(self.src[self.pos:self.pos + take]) + if n <= 0: + break + self.pos = (self.pos + n) % len(self.src) + room -= n + + def capture(self, into, got): + """Read into ``into`` from ``got``; past its end, read and discard.""" + if got < len(into): + n = self.mic.readinto(memoryview(into)[got:]) + self.read_frames += n // FRAME + return got + n + n = self.mic.readinto(self.chunk) + while n: + self.read_frames += n // FRAME + n = self.mic.readinto(self.chunk) + return got + + def run(self, ms, keep, into=None, got=0): + t0 = time.ticks_ms() + scratch = into if into is not None else bytearray(0) + while time.ticks_diff(time.ticks_ms(), t0) < ms: + self.feed(keep) + got = self.capture(scratch, got) + time.sleep_ms(1) + return got + + +def left(buf, nbytes): + full = np.frombuffer(buf, dtype=np.int16, count=nbytes // 2) + x = np.array(full[0::2], dtype=np.float) + return x - np.mean(x) + + +def level(x): + rms = math.sqrt(np.sum(x * x) / len(x)) + return (20 * math.log10(rms / 32768) if rms else -200.0), float(np.max(abs(x))) + + +def period(x, lo=12, hi=200): + """(fundamental Hz, correlation) from the waveform's period. + + The smallest lag whose normalised autocorrelation comes within 0.02 of the + best one, refined by a parabola through its neighbours. A harmonic-rich + waveform still repeats at its fundamental's period; noise repeats at none. + """ + m = len(x) - hi + base = x[:m] + e0 = float(np.sum(base * base)) or 1.0 + r = [float(np.sum(base * x[lag:lag + m])) / e0 for lag in range(lo, hi)] + best = max(r) + k = next(i for i, v in enumerate(r) if v >= best - 0.02) + while k + 1 < len(r) and r[k + 1] > r[k]: + k += 1 + d = 0.0 + if 0 < k < len(r) - 1: + a, b, c = r[k - 1], r[k], r[k + 1] + if a - 2 * b + c: + d = (a - c) / (2 * (a - 2 * b + c)) + return RATE / (lo + k + d), r[k] + + +def zero_cross_hz(x): + n = 0 + prev = x[0] + for i in range(1, len(x)): + v = x[i] + if prev < 0 <= v: + n += 1 + prev = v + return n * RATE / len(x) + + +def fft_peak(x): + size = 16384 + seg = x[:size] + win = np.array([0.5 - 0.5 * math.cos(2 * math.pi * i / size) for i in range(size)]) + mag = utils.spectrogram(seg * win) + k = int(np.argmax(mag[1:size // 2])) + 1 + return k * RATE / size + + +def analyse(name, buf, nbytes): + x = left(buf, nbytes) + db, pk = level(x) + print(".", end="") + f0, corr = period(x[:9600]) + print(".", end="") + zc = zero_cross_hz(x[:9600]) + fp = fft_peak(x) + print(".", end="") + del x + gc.collect() + return db, pk, f0, corr, zc, fp + + +def phase_tone(rig): + cap = bytearray(RATE * FRAME // 2) # 500 ms + rig.play(bytes(48 * FRAME * 10)) + rig.run(1000, DEEP) # past the open's spike + got = rig.run(600, DEEP, cap) + db, pk, f0, corr, zc, fp = analyse("quiet", cap, got) + say("RESULT quiet: level %.1f dBFS, peak %d; period correlation %.2f" % (db, pk, corr)) + noise_peak = pk + control_found = corr > 0.9 + ok = not control_found + for hz in TONES: + rig.play(tone(hz, TONE_DBFS, RATE // 10)) # 100 ms, whole periods + rig.run(500, DEEP) + o0 = rig.out.stats()[3] + got = rig.run(600, DEEP, cap) + starved = rig.out.stats()[3] - o0 + db, pk, f0, corr, zc, fp = analyse(str(hz), cap, got) + harmonic = fp / hz + say("RESULT tone %d Hz at %d dBFS: back at %.1f Hz fundamental (period correlation %.2f)," + " level %.1f dBFS, peak %d; FFT peak %.1f Hz = %.2f x sent; zero crossings %.0f Hz;" + " starved %d" % (hz, TONE_DBFS, f0, corr, db, pk, fp, harmonic, zc, starved)) + # The period found is the tone's own or a whole multiple of it (the + # dongle's processing can add a slower repeat), and the spectrum's + # peak sits on a harmonic of what was sent: a stream at the wrong + # rate moves both. + ratio = hz / f0 + ok = ok and corr > 0.9 and abs(ratio - round(ratio)) < 0.005 * ratio and starved == 0 \ + and abs(harmonic - round(harmonic)) < 0.01 and db > -40 + say("RESULT tone gate", "PASS" if ok else "FAIL", "| control (silence) shows a period:", + control_found) + return ok, noise_peak + + +def phase_latency(rig, noise_peak): + """Each burst: write it, then find its onset in what comes back.""" + silence = bytes(48 * FRAME * 10) + burst = tone(1000, BURST_DBFS, 96) # 2 ms + threshold = int(max(1000, 20 * noise_peak)) + window = bytearray(RATE * FRAME // 5) # 200 ms after each burst + lat, ring = [], [] + rig.play(silence) + s0 = rig.out.stats()[3] + for _ in range(BURSTS): + rig.run(BURST_GAP_MS, SHALLOW) + rig.capture(bytearray(0), 0) # empty the capture ring + # The capture position at the moment of writing: frames read so far + # plus those captured and waiting. + mark = rig.read_frames + rig.mic.available() // FRAME + q = rig.out.queued_size() + n = rig.out.try_write(burst) + first = rig.read_frames + got = rig.run(200, SHALLOW, window) + if n != len(burst): + say("RESULT latency: burst write short", n) + continue + a = array.array("h", window[:got]) + for i in range(0, len(a), 2): + v = a[i] + if v > threshold or v < -threshold: + lat.append((first + i // 2 - mark) * 1000 / RATE) + ring.append(q * 1000 / (FRAME * RATE)) + break + starved = rig.out.stats()[3] - s0 + if not lat: + say("RESULT latency: no burst came back over", threshold) + return False + beyond = sorted(a - b for a, b in zip(lat, ring)) + lat.sort() + ring.sort() + say("RESULT latency, write to read-back: %d of %d bursts, min %.1f, median %.1f, max %.1f ms;" + " output ring ahead of the burst %.1f ms median; threshold %d; starved %d" + % (len(lat), BURSTS, lat[0], lat[len(lat) // 2], lat[-1], ring[len(ring) // 2], + threshold, starved)) + # Beyond the ring: the driver's scheduled OUT transfers (3 x 8 ms), the + # device's DAC, analogue loop, ADC and processing, and up to one 8 ms + # capture transfer before the bytes are readable. + say("RESULT latency beyond the output ring: min %.1f, median %.1f, max %.1f ms" + % (beyond[0], beyond[len(beyond) // 2], beyond[-1])) + return len(lat) == BURSTS and starved == 0 + + +def phase_soak(rig, seconds, stall_ms=0): + rig.play(tone(1000, TONE_DBFS, RATE // 10)) + rig.run(300, DEEP) # prime + o0, i0 = rig.out.stats(), rig.mic.stats() + t0 = time.ticks_ms() + stalled = False + while time.ticks_diff(time.ticks_ms(), t0) < seconds * 1000: + rig.feed(DEEP) + rig.capture(bytearray(0), 0) + if stall_ms and not stalled and time.ticks_diff(time.ticks_ms(), t0) > 1000: + time.sleep_ms(stall_ms) # the planted fault: the feeder stops + stalled = True + time.sleep_ms(5) + o1, i1 = rig.out.stats(), rig.mic.stats() + d_out = [b - a for a, b in zip(o0, o1)] + d_in = [b - a for a, b in zip(i0, i1)] + names = ("packets", "bytes", "dropped", "starved", "errors", "empty") + tag = " (planted %d ms stall)" % stall_ms if stall_ms else "" + say("RESULT soak%s %d s out:" % (tag, seconds), + ", ".join("%s %d" % (k, v) for k, v in zip(names, d_out))) + say("RESULT soak%s %d s in: " % (tag, seconds), + ", ".join("%s %d" % (k, v) for k, v in zip(names, d_in))) + clean = (d_out[2] == 0 and d_out[3] == 0 and d_out[4] == 0 + and d_in[2] == 0 and d_in[4] == 0 and d_in[5] == 0 + and d_out[0] >= seconds * 990 and d_in[0] >= seconds * 990) + return clean, d_out + + +def main(): + host = usbif.auto.host(classes=("uac",)) + host.start() + devs = () + for _ in range(60): + devs = uac_audio.audio_devices() + if devs: + break + time.sleep_ms(250) + if not devs: + print("no USB audio device") + return + rig = Rig(devs[0][0]) + say("streams: %d B out on ep 0x%02x, %d B in on ep 0x%02x" % ( + rig.out.stream.max_packet, rig.out.stream.endpoint, + rig.mic.stream.max_packet, rig.mic.stream.endpoint)) + gates = {} + rig.out.open() + rig.mic.open() + try: + gates["tone"], noise_peak = phase_tone(rig) + gates["latency"] = phase_latency(rig, noise_peak) + gates["soak"], _ = phase_soak(rig, SOAK_S) + stall_clean, d_out = phase_soak(rig, 5, STALL_MS) + gates["planted stall is caught"] = (not stall_clean) and d_out[3] > 0 + say("RESULT totals out", rig.out.stats(), "in", rig.mic.stats()) + finally: + rig.out.close() + rig.mic.close() + host.stop() + print() + for line in out_log: + print(line) + for k, v in gates.items(): + print("GATE", k, "PASS" if v else "FAIL") + print("GATE", "PASS" if all(gates.values()) else "FAIL") + + +main() diff --git a/tests/test_usbif.py b/tests/test_usbif.py index d55c293..f75880b 100644 --- a/tests/test_usbif.py +++ b/tests/test_usbif.py @@ -1479,6 +1479,10 @@ def __init__(self, blob, dev_id=4, classes=("uac",)): self.closed = 0 self.written = bytearray() self.incoming = bytearray() + # The driver keeps one stream per direction: endpoint -> True while + # open, and every endpoint a close was asked for (None = both). + self.open_eps = {} + self.closed_eps = [] def host_devices(self): return ((self.dev_id, 0x08bb, 0x2900, "USB Audio CODEC", None, @@ -1491,15 +1495,18 @@ def host_desc(self, dev_id): def host_uac_open(self, dev_id, itf, alt, ep, mps, rate=0, clock=0, control=0, frame=0, ring=0): + if any((e & 0x80) == (ep & 0x80) for e in self.open_eps): + raise OSError("host_uac_open failed (-1)") self.opened = (dev_id, itf, alt, ep, mps, rate) # The 2.0 arguments, kept apart so the 1.0 assertions above read as before. self.opened_2_0 = (clock, control, frame) self.ring = ring or 8192 + self.open_eps[ep] = True - def host_uac_space(self): + def host_uac_space(self, ep=None): return self.ring - len(self.written) - def host_uac_capacity(self): + def host_uac_capacity(self, ep=None): return self.ring # A 2.0 clock source's answer: one discrete rate unless a test sets @@ -1522,10 +1529,13 @@ def host_uac_read(self, buf): del self.incoming[:n] return n - def host_uac_queued(self): + def host_uac_queued(self, ep=None): + if ep is not None and ep & 0x80: + return len(self.incoming) return len(self.written) - def host_uac_stats(self): + def host_uac_stats(self, ep=None): + self.stats_asked = getattr(self, "stats_asked", []) + [ep] return (0, len(self.written), 0, 0, 0) def host_uac_c_sink(self, channels, bits): @@ -1535,8 +1545,12 @@ def host_uac_c_sink(self, channels, bits): self.c_sink_asked = (channels, bits) return b"PCMS" + bytes(52) - def host_uac_close(self): + def host_uac_close(self, ep=None): self.closed += 1 + self.closed_eps.append(ep) + for e in list(self.open_eps): + if ep is None or (e & 0x80) == (ep & 0x80): + del self.open_eps[e] class TestUacAudioSelection(unittest.TestCase): @@ -1634,6 +1648,50 @@ def test_input_selects_an_in_endpoint(self): self.assertEqual(fake.opened[3] & 0x80, 0x80) device.close() + def test_output_and_input_run_side_by_side(self): + # One stream per direction: playing to a device while recording from + # it (a headset, a DAC looped back into its input) opens both, and + # each object's calls name its own endpoint. + fake = self._install() + out = self.mod.output(4) + mic = self.mod.input(4) + out.open() + mic.open() + self.assertEqual(len(fake.open_eps), 2) + out_ep, in_ep = out.stream.endpoint, mic.stream.endpoint + self.assertEqual(out_ep & 0x80, 0) + self.assertEqual(in_ep & 0x80, 0x80) + out.stats() + mic.stats() + self.assertEqual(fake.stats_asked, [out_ep, in_ep]) + fake.incoming.extend(bytes(12)) + self.assertEqual(mic.available(), 12) + + def test_closing_one_direction_leaves_the_other_running(self): + fake = self._install() + out = self.mod.output(4) + mic = self.mod.input(4) + out.open() + mic.open() + out.close() + self.assertEqual(fake.closed_eps, [out.stream.endpoint]) + self.assertEqual(list(fake.open_eps), [mic.stream.endpoint]) + mic.close() + self.assertEqual(fake.open_eps, {}) + + def test_firmware_with_one_stream_still_closes_it(self): + # Older firmware's calls take no endpoint; the library falls back to + # the call that means "the stream". + fake = self._install() + closed = [] + fake.host_uac_close = lambda: closed.append(True) + fake.host_uac_capacity = lambda: 8192 + out = self.mod.output(4) + out.open() + self.assertEqual(out.capacity(), 8192) + out.close() + self.assertEqual(closed, [True]) + def test_asking_for_an_unavailable_format_raises_and_says_what_is_offered(self): # Substituting the nearest rate is how a pitch bug ships. The error # has to name the alternatives so the caller can pick one.