From 4434985aa33d81fba75aaa69fa804c0b7caa1f3c Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Mon, 10 Aug 2026 06:32:05 +1000 Subject: [PATCH] [client] usb audio: add microphone recording Add a composite UAC2 microphone function with its own clock so recording and playback can use independent sample rates. Packetize asynchronous capture through usbredir, follow the host capture clock, and preserve the newest frames across stalls. Reconfigure active recording without another permission prompt and fix lost PipeWire capture wakeups. --- client/audiodevs/PipeWire/pipewire.c | 16 +- client/include/interface/audio.h | 1 + client/src/audio.c | 37 +- client/src/audio_usb.c | 259 ++++++++-- client/src/audio_usb.h | 1 + client/src/main.c | 2 + client/src/usb_audio.c | 719 ++++++++++++++++++++++----- client/src/usb_audio.h | 20 +- client/src/usbredir.c | 3 + client/src/usbredir.h | 4 + 10 files changed, 862 insertions(+), 200 deletions(-) diff --git a/client/audiodevs/PipeWire/pipewire.c b/client/audiodevs/PipeWire/pipewire.c index 5ce064a1..ceaa895f 100644 --- a/client/audiodevs/PipeWire/pipewire.c +++ b/client/audiodevs/PipeWire/pipewire.c @@ -915,7 +915,10 @@ static void pipewire_recordQueueFrames(const void * data, int frames) atomic_fetch_add_explicit( &pw.record.droppedFrames, frames - append, memory_order_relaxed); - if (occupancy == 0 && append > 0 && sem_post(&pw.record.sendWake) < 0) + /* Posting only when the producer observes an empty queue can lose a wake + * while the consumer drains the previous batch. Always notify for appended + * data; the sender coalesces all queued frames when it wakes. */ + if (append > 0 && sem_post(&pw.record.sendWake) < 0) atomic_fetch_add_explicit( &pw.record.signalErrors, 1, memory_order_relaxed); } @@ -961,9 +964,13 @@ static void pipewire_onRecordProcess(void * userdata) static void pipewire_recordStart(const LG_AudioFormat * format, LG_AudioPushFn pushFn) { - const int channels = format->channelCount; - const int sampleRate = format->sampleRate; - const int sampleSize = pipewire_sampleSize(format->sampleFormat); + const int channels = format->channelCount; + const int sampleRate = format->sampleRate; + const int sampleSize = pipewire_sampleSize(format->sampleFormat); + const int periodFrames = max(sampleRate / 1000, 1); + char requestedNodeLatency[32]; + snprintf(requestedNodeLatency, sizeof(requestedNodeLatency), "%d/%d", + periodFrames, sampleRate); const struct spa_pod * params[1]; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); @@ -1000,6 +1007,7 @@ static void pipewire_recordStart(const LG_AudioFormat * format, PW_KEY_MEDIA_TYPE , "Audio", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE , "Music", + PW_KEY_NODE_LATENCY , requestedNodeLatency, NULL ); if (!props) diff --git a/client/include/interface/audio.h b/client/include/interface/audio.h index 9aae7aa8..d1564736 100644 --- a/client/include/interface/audio.h +++ b/client/include/interface/audio.h @@ -120,6 +120,7 @@ typedef struct LG_AudioEventOps const void * data, size_t frames, const LG_AudioClock * sourceClock); + /* May repeat with the same generation to update an active stream format. */ void (*recordStart)(void * opaque, uint32_t generation, const LG_AudioFormat * format); void (*recordStop)(void * opaque, uint32_t generation); diff --git a/client/src/audio.c b/client/src/audio.c index bedd4ae4..26c578cb 100644 --- a/client/src/audio.c +++ b/client/src/audio.c @@ -2156,7 +2156,7 @@ static void realRecordStartLocked(const LG_AudioFormat * format) audio.audioDev->record.start(format, recordPushFrames); // if a volume level was stored, set it before we return - if (audio.record.volumeChannels) + if (audio.record.volumeChannels && audio.audioDev->record.volume) audio.audioDev->record.volume( audio.record.volumeChannels, audio.record.volume); @@ -2201,7 +2201,8 @@ static void recordConfirm(bool yes, void * opaque) static void recordStart(const LG_AudioFormat * format) { LG_LOCK(audio.record.lock); - if (!audio.audioDev || audio.record.shuttingDown || + if (!audio.audioDev || !audio.audioDev->record.start || + audio.record.shuttingDown || !audioFormatValid(format)) { if (format && !audioFormatValid(format)) @@ -2271,6 +2272,29 @@ static void recordStart(const LG_AudioFormat * format) app_msgBoxClose(oldConfirm); } +static void recordReconfigure(const LG_AudioFormat * format) +{ + LG_LOCK(audio.record.lock); + if (!audio.audioDev || !audio.audioDev->record.start || + audio.record.shuttingDown || !audioFormatValid(format)) + { + LG_UNLOCK(audio.record.lock); + return; + } + + audio.record.lastFormat = *format; + if (audio.record.started && + !audioFormatEqual(format, &audio.record.format)) + { + realRecordStopLocked(); + realRecordStartLocked(format); + } + else if (audio.record.confirmPending) + audio.record.confirmFormat = *format; + + LG_UNLOCK(audio.record.lock); +} + static void realRecordStopLocked(void) { audio.audioDev->record.stop(); @@ -2522,9 +2546,12 @@ static void eventRecordStart(void * opaque, uint32_t generation, binding->ops->recordData; if (active) { - atomic_store_explicit(&audio.record.streamGeneration, - generation, memory_order_release); - recordStart(format); + const uint32_t previous = atomic_exchange_explicit( + &audio.record.streamGeneration, generation, memory_order_acq_rel); + if (previous == generation) + recordReconfigure(format); + else + recordStart(format); } LG_UNLOCK_SHARED(audio.activeLock); } diff --git a/client/src/audio_usb.c b/client/src/audio_usb.c index 007313f1..bb4855ff 100644 --- a/client/src/audio_usb.c +++ b/client/src/audio_usb.c @@ -67,12 +67,15 @@ struct LGA_USBState const LG_AudioEventOps * events; void * eventOpaque; - LG_AudioFormat streamFormat; - uint32_t streamGeneration; + LG_AudioFormat playbackFormat; + uint32_t playbackGeneration; + LG_AudioFormat recordFormat; + uint32_t recordGeneration; uint32_t generationSerial; - int64_t clockOrigin; - atomic_uint_fast64_t position; - atomic_uint deliveryGeneration; + int64_t playbackClockOrigin; + atomic_uint_fast64_t playbackPosition; + atomic_uint playbackDeliveryGeneration; + atomic_uint recordDeliveryGeneration; atomic_uint inFlight; }; @@ -153,12 +156,12 @@ static void endCallback(USBAudioCallbackFrame * frame, atomic_fetch_sub_explicit(inFlight, 1, memory_order_seq_cst); } -static LG_AudioClock makeClock( +static LG_AudioClock makePlaybackClock( const LGA_USBState * state, uint64_t position) { /* USB redirection carries no publisher timestamp or USB frame number. * Preserve the sample timeline, but do not claim a measured source rate. */ - const uint32_t sampleRate = state->streamFormat.sampleRate; + const uint32_t sampleRate = state->playbackFormat.sampleRate; const int64_t elapsed = position / sampleRate * USB_AUDIO_NS_PER_SECOND + position % sampleRate * USB_AUDIO_NS_PER_SECOND / sampleRate; @@ -166,7 +169,7 @@ static LG_AudioClock makeClock( return (LG_AudioClock) { .position = position, - .time = state->clockOrigin + elapsed, + .time = state->playbackClockOrigin + elapsed, .rate = 0.0, .stable = false, }; @@ -197,11 +200,18 @@ static bool attachmentCurrentNL(const LGA_USBState * state, state->eventOpaque == target->opaque; } -static bool eventCurrentNL(const LGA_USBState * state, +static bool playbackEventCurrentNL(const LGA_USBState * state, const USBAudioEventTarget * target, uint32_t streamGeneration) { return attachmentCurrentNL(state, target) && - state->streamGeneration == streamGeneration; + state->playbackGeneration == streamGeneration; +} + +static bool recordEventCurrentNL(const LGA_USBState * state, + const USBAudioEventTarget * target, uint32_t streamGeneration) +{ + return attachmentCurrentNL(state, target) && + state->recordGeneration == streamGeneration; } static void endEvent(LGA_USBState * state, USBAudioEventTarget * target) @@ -258,7 +268,7 @@ static void waitStatusCallbacks(const LGA_USBState * state) ; } -static void usbAudioStart( +static void usbPlaybackStart( void * opaque, uint32_t sampleRate, uint32_t channelMask) { LGA_USBState * state = opaque; @@ -268,21 +278,22 @@ static void usbAudioStart( LG_AudioClock clock; LG_LOCK(state->stateLock); - if (state->streamGeneration) + if (state->playbackGeneration) { LG_UNLOCK(state->stateLock); return; } - setStreamFormat(&state->streamFormat, sampleRate, channelMask); + setStreamFormat(&state->playbackFormat, sampleRate, channelMask); generation = state->generationSerial = nextGeneration(state->generationSerial); - state->streamGeneration = generation; - state->clockOrigin = (int64_t)nanotime(); - atomic_store_explicit(&state->position, 0, memory_order_relaxed); + state->playbackGeneration = generation; + state->playbackClockOrigin = (int64_t)nanotime(); atomic_store_explicit( - &state->deliveryGeneration, 0, memory_order_seq_cst); - clock = makeClock(state, 0); + &state->playbackPosition, 0, memory_order_relaxed); + atomic_store_explicit( + &state->playbackDeliveryGeneration, 0, memory_order_seq_cst); + clock = makePlaybackClock(state, 0); const bool admitted = beginEventNL(state, &target); dispatch = admitted && target.events->playbackStart; if (admitted && !dispatch) @@ -293,32 +304,32 @@ static void usbAudioStart( return; target.events->playbackStart( - target.opaque, generation, &state->streamFormat, &clock); + target.opaque, generation, &state->playbackFormat, &clock); LG_LOCK(state->stateLock); - if (eventCurrentNL(state, &target, generation)) - atomic_store_explicit(&state->deliveryGeneration, + if (playbackEventCurrentNL(state, &target, generation)) + atomic_store_explicit(&state->playbackDeliveryGeneration, generation, memory_order_seq_cst); LG_UNLOCK(state->stateLock); endEvent(state, &target); } -static void usbAudioStop(void * opaque) +static void usbPlaybackStop(void * opaque) { LGA_USBState * state = opaque; USBAudioEventTarget target; LG_LOCK(state->stateLock); - const uint32_t generation = state->streamGeneration; + const uint32_t generation = state->playbackGeneration; if (!generation) { LG_UNLOCK(state->stateLock); return; } - state->streamGeneration = 0; + state->playbackGeneration = 0; atomic_store_explicit( - &state->deliveryGeneration, 0, memory_order_seq_cst); + &state->playbackDeliveryGeneration, 0, memory_order_seq_cst); const bool admitted = beginEventNL(state, &target); LG_UNLOCK(state->stateLock); @@ -337,23 +348,24 @@ static void usbAudioStop(void * opaque) endEvent(state, &target); } -static void usbAudioData(void * opaque, const void * data, size_t frames) +static void usbPlaybackData( + void * opaque, const void * data, size_t frames) { LGA_USBState * state = opaque; USBAudioCallbackFrame frame; beginCallback(state, &frame, &l_eventFrames, &state->inFlight); const uint64_t position = atomic_fetch_add_explicit( - &state->position, frames, memory_order_relaxed); + &state->playbackPosition, frames, memory_order_relaxed); const uint32_t generation = atomic_load_explicit( - &state->deliveryGeneration, memory_order_seq_cst); + &state->playbackDeliveryGeneration, memory_order_seq_cst); if (generation) { const LG_AudioEventOps * events = state->events; void * target = state->eventOpaque; if (events && events->playbackData) { - const LG_AudioClock clock = makeClock(state, position); + const LG_AudioClock clock = makePlaybackClock(state, position); events->playbackData( target, generation, data, frames, &clock); } @@ -361,11 +373,86 @@ static void usbAudioData(void * opaque, const void * data, size_t frames) endCallback(&frame, &l_eventFrames, &state->inFlight); } +static void usbRecordStart( + void * opaque, uint32_t sampleRate, uint32_t channelMask) +{ + LGA_USBState * state = opaque; + USBAudioEventTarget target; + bool dispatch; + uint32_t generation; + + LG_LOCK(state->stateLock); + setStreamFormat(&state->recordFormat, sampleRate, channelMask); + generation = state->recordGeneration; + if (!generation) + { + generation = state->generationSerial = + nextGeneration(state->generationSerial); + state->recordGeneration = generation; + } + atomic_store_explicit( + &state->recordDeliveryGeneration, 0, memory_order_seq_cst); + const bool admitted = beginEventNL(state, &target); + dispatch = admitted && target.events->recordStart; + if (admitted && !dispatch) + endEvent(state, &target); + LG_UNLOCK(state->stateLock); + + if (!dispatch) + return; + + target.events->recordStart( + target.opaque, generation, &state->recordFormat); + + LG_LOCK(state->stateLock); + if (recordEventCurrentNL(state, &target, generation)) + atomic_store_explicit(&state->recordDeliveryGeneration, + generation, memory_order_seq_cst); + LG_UNLOCK(state->stateLock); + endEvent(state, &target); +} + +static void usbRecordStop(void * opaque) +{ + LGA_USBState * state = opaque; + USBAudioEventTarget target; + + LG_LOCK(state->stateLock); + const uint32_t generation = state->recordGeneration; + if (!generation) + { + LG_UNLOCK(state->stateLock); + return; + } + + state->recordGeneration = 0; + atomic_store_explicit( + &state->recordDeliveryGeneration, 0, memory_order_seq_cst); + const bool admitted = beginEventNL(state, &target); + LG_UNLOCK(state->stateLock); + + waitEvents(state); + + if (!admitted) + return; + + LG_LOCK(state->stateLock); + const bool dispatch = attachmentCurrentNL(state, &target) && + target.events->recordStop; + LG_UNLOCK(state->stateLock); + + if (dispatch) + target.events->recordStop(target.opaque, generation); + endEvent(state, &target); +} + static const LG_USBAudioEventOps l_usbAudioEvents = { - .start = usbAudioStart, - .stop = usbAudioStop, - .data = usbAudioData, + .playbackStart = usbPlaybackStart, + .playbackStop = usbPlaybackStop, + .playbackData = usbPlaybackData, + .recordStart = usbRecordStart, + .recordStop = usbRecordStop, }; static void usbSetAvailable(void * opaque, bool available) @@ -443,9 +530,14 @@ static bool usbAttach(void * opaque, const LG_AudioEventOps * events, return false; USBAudioEventTarget target; - LG_AudioClock clock; - uint32_t generation; - bool dispatch; + LG_AudioFormat playbackFormat; + LG_AudioFormat recordFormat; + LG_AudioClock playbackClock; + uint32_t playbackGeneration; + uint32_t recordGeneration; + bool playbackDispatch; + bool recordDispatch; + bool admitted; for (;;) { @@ -469,28 +561,61 @@ static bool usbAttach(void * opaque, const LG_AudioEventOps * events, nextGeneration(state->attachmentGeneration); state->events = events; state->eventOpaque = eventOpaque; - generation = state->streamGeneration; - dispatch = generation && events->playbackStart && + playbackGeneration = state->playbackGeneration; + recordGeneration = state->recordGeneration; + playbackDispatch = playbackGeneration && events->playbackStart; + recordDispatch = recordGeneration && events->recordStart; + admitted = (playbackDispatch || recordDispatch) && beginEventNL(state, &target); - if (dispatch) - clock = makeClock(state, atomic_load_explicit( - &state->position, memory_order_relaxed)); + playbackDispatch = playbackDispatch && admitted; + recordDispatch = recordDispatch && admitted; + if (playbackDispatch) + { + playbackFormat = state->playbackFormat; + playbackClock = makePlaybackClock(state, atomic_load_explicit( + &state->playbackPosition, memory_order_relaxed)); + } + if (recordDispatch) + recordFormat = state->recordFormat; lgUsbRedir_setPlugged(state->redir, true); LG_UNLOCK(state->stateLock); - if (dispatch) + if (playbackDispatch) { target.events->playbackStart( - target.opaque, generation, &state->streamFormat, &clock); + target.opaque, playbackGeneration, + &playbackFormat, &playbackClock); LG_LOCK(state->stateLock); - if (eventCurrentNL(state, &target, generation)) - atomic_store_explicit(&state->deliveryGeneration, - generation, memory_order_seq_cst); + if (playbackEventCurrentNL( + state, &target, playbackGeneration)) + atomic_store_explicit(&state->playbackDeliveryGeneration, + playbackGeneration, memory_order_seq_cst); LG_UNLOCK(state->stateLock); - endEvent(state, &target); } + if (recordDispatch) + { + LG_LOCK(state->stateLock); + recordDispatch = recordEventCurrentNL( + state, &target, recordGeneration); + LG_UNLOCK(state->stateLock); + + if (recordDispatch) + target.events->recordStart( + target.opaque, recordGeneration, &recordFormat); + + LG_LOCK(state->stateLock); + if (recordDispatch && + recordEventCurrentNL(state, &target, recordGeneration)) + atomic_store_explicit(&state->recordDeliveryGeneration, + recordGeneration, memory_order_seq_cst); + LG_UNLOCK(state->stateLock); + } + + if (admitted) + endEvent(state, &target); + return true; } @@ -515,7 +640,9 @@ static void usbDetach(void * opaque) const uint32_t attachmentGeneration = state->attachmentGeneration; atomic_store_explicit( - &state->deliveryGeneration, 0, memory_order_seq_cst); + &state->playbackDeliveryGeneration, 0, memory_order_seq_cst); + atomic_store_explicit( + &state->recordDeliveryGeneration, 0, memory_order_seq_cst); lgUsbRedir_setPlugged(state->redir, false); LG_UNLOCK(state->stateLock); @@ -538,13 +665,14 @@ static bool usbClockFeedback(void * opaque, uint32_t generation, LGA_USBState * state = opaque; LG_LOCK(state->stateLock); - if (!state->attached || state->streamGeneration != generation) + if (!state->attached || + state->playbackGeneration != generation) { LG_UNLOCK(state->stateLock); return false; } - const double nominalRate = state->streamFormat.sampleRate; + const double nominalRate = state->playbackFormat.sampleRate; const double rate = playbackClock && targetRate >= nominalRate * 0.995 && targetRate <= nominalRate * 1.005 ? @@ -554,13 +682,31 @@ static bool usbClockFeedback(void * opaque, uint32_t generation, return true; } +static bool usbRecordData(void * opaque, uint32_t generation, + const void * data, size_t frames, + const LG_AudioClock * sourceClock) +{ + (void)sourceClock; + LGA_USBState * state = opaque; + USBAudioCallbackFrame frame; + beginCallback(state, &frame, &l_eventFrames, &state->inFlight); + + const bool valid = generation && generation == atomic_load_explicit( + &state->recordDeliveryGeneration, memory_order_seq_cst); + const bool result = valid && + lgUsbAudio_recordData(state->device, data, frames); + + endCallback(&frame, &l_eventFrames, &state->inFlight); + return result; +} + const LG_AudioOps LGA_USB = { .name = "USB Audio", .setStatusListener = usbSetStatusListener, .attach = usbAttach, .detach = usbDetach, - .recordData = NULL, + .recordData = usbRecordData, .clockFeedback = usbClockFeedback, }; @@ -573,13 +719,15 @@ LGA_USBState * lgaUsb_create(void) LG_LOCK_INIT(state->statusLock); LG_LOCK_INIT(state->stateLock); atomic_init(&state->available, false); - atomic_init(&state->position, 0); - atomic_init(&state->deliveryGeneration, 0); + atomic_init(&state->playbackPosition, 0); + atomic_init(&state->playbackDeliveryGeneration, 0); + atomic_init(&state->recordDeliveryGeneration, 0); atomic_init(&state->inFlight, 0); atomic_init(&state->statusInFlight, 0); atomic_init(&state->statusNextTicket, 0); atomic_init(&state->statusServingTicket, 0); - state->streamFormat = l_formatTemplate; + state->playbackFormat = l_formatTemplate; + state->recordFormat = l_formatTemplate; state->device = lgUsbAudio_create(&l_usbAudioEvents, state); if (!state->device) @@ -618,3 +766,8 @@ LG_USBRedir * lgaUsb_redir(LGA_USBState * state) { return state ? state->redir : NULL; } + +bool lgaUsb_recording(const LGA_USBState * state) +{ + return state && lgUsbAudio_recording(state->device); +} diff --git a/client/src/audio_usb.h b/client/src/audio_usb.h index 3ebd30b5..b704a491 100644 --- a/client/src/audio_usb.h +++ b/client/src/audio_usb.h @@ -31,6 +31,7 @@ LGA_USBState * lgaUsb_create(void); void lgaUsb_destroy(LGA_USBState * state); LG_USBRedir * lgaUsb_redir(LGA_USBState * state); +bool lgaUsb_recording(const LGA_USBState * state); extern const LG_AudioOps LGA_USB; diff --git a/client/src/main.c b/client/src/main.c index 988044a8..6aec5bc6 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -1838,6 +1838,8 @@ int spiceThread(void * arg) #if ENABLE_USB_AUDIO if (usbRedir && !lgUsbRedir_process(usbRedir)) DEBUG_WARN("Failed to process USB audio redirection"); + if (usbAudio) + processTimeout = lgaUsb_recording(usbAudio) ? 1 : 10; #endif if ((status = purespice_process(processTimeout)) != PS_STATUS_RUN) diff --git a/client/src/usb_audio.c b/client/src/usb_audio.c index 7680d790..2c65d8b3 100644 --- a/client/src/usb_audio.c +++ b/client/src/usb_audio.c @@ -21,9 +21,13 @@ #include "usb_audio.h" #include "common/debug.h" +#include "common/locking.h" +#include "common/ringbuffer.h" +#include "common/time.h" #include +#include #include #include #include @@ -32,29 +36,50 @@ enum { - USB_AUDIO_VENDOR_ID = 0x043e, - USB_AUDIO_PRODUCT_ID = 0x0001, - USB_AUDIO_DEVICE_VERSION = 0x010b, - USB_AUDIO_CONFIGURATION = 1, - USB_AUDIO_CONTROL_IFACE = 0, - USB_AUDIO_STREAM_IFACE = 1, - USB_AUDIO_CLOCK_ID = 1, - USB_AUDIO_DATA_ENDPOINT = 0x01, - USB_AUDIO_FEEDBACK_ENDPOINT = 0x81, - USB_AUDIO_DATA_EP_INDEX = 1, - USB_AUDIO_FEEDBACK_EP_INDEX = 17, - USB_AUDIO_DATA_INTERVAL = 1, - USB_AUDIO_FEEDBACK_INTERVAL = 8, - USB_AUDIO_FEEDBACK_BINTERVAL = 4, - USB_AUDIO_FEEDBACK_SIZE = 4, - USB_AUDIO_FEEDBACK_MAX_QUEUE = 512, - USB_AUDIO_SAMPLE_SIZE = 3, - USB_AUDIO_HS_PACKET_FRAMES = 25, - USB_AUDIO_FS_PACKET_FRAMES = 97, - USB_AUDIO_DESCRIPTOR_BASE = 81, - USB_AUDIO_STREAM_DESC_SIZE = 53, - USB_AUDIO_OTHER_SPEED_SIZE = - USB_AUDIO_DESCRIPTOR_BASE + USB_AUDIO_STREAM_DESC_SIZE, + USB_AUDIO_VENDOR_ID = 0x043e, + USB_AUDIO_PRODUCT_ID = 0x0001, + USB_AUDIO_DEVICE_VERSION = 0x010d, + USB_AUDIO_CONFIGURATION = 1, + USB_AUDIO_PLAYBACK_CONTROL_IFACE = 0, + USB_AUDIO_PLAYBACK_IFACE = 1, + USB_AUDIO_RECORD_CONTROL_IFACE = 2, + USB_AUDIO_RECORD_IFACE = 3, + USB_AUDIO_PLAYBACK_CLOCK_ID = 1, + USB_AUDIO_RECORD_CLOCK_ID = 1, + USB_AUDIO_PLAYBACK_DATA_ENDPOINT = 0x01, + USB_AUDIO_FEEDBACK_ENDPOINT = 0x81, + USB_AUDIO_RECORD_DATA_ENDPOINT = 0x82, + USB_AUDIO_PLAYBACK_DATA_EP_INDEX = 1, + USB_AUDIO_FEEDBACK_EP_INDEX = 17, + USB_AUDIO_RECORD_DATA_EP_INDEX = 18, + USB_AUDIO_PLAYBACK_DATA_INTERVAL = 1, + USB_AUDIO_FEEDBACK_INTERVAL = 8, + USB_AUDIO_RECORD_DATA_INTERVAL = 4, + USB_AUDIO_FEEDBACK_BINTERVAL = 4, + USB_AUDIO_RECORD_DATA_BINTERVAL = 3, + USB_AUDIO_FEEDBACK_SIZE = 4, + USB_AUDIO_FEEDBACK_MAX_QUEUE = 512, + USB_AUDIO_SAMPLE_SIZE = 3, + USB_AUDIO_PLAYBACK_HS_PACKET_FRAMES = 25, + USB_AUDIO_FS_PACKET_FRAMES = 97, + USB_AUDIO_RECORD_PACKET_FRAMES = 97, + USB_AUDIO_RECORD_PACKETS_PER_SECOND = 2000, + USB_AUDIO_RECORD_PACKET_INTERVAL_NS = 500000, + USB_AUDIO_RECORD_RATE_SCALE = 65536, + USB_AUDIO_RECORD_RATE_SETTLE_NS = 2000000000, + USB_AUDIO_RECORD_RATE_GAP_NS = 100000000, + USB_AUDIO_RECORD_RATE_DENOMINATOR = + USB_AUDIO_RECORD_PACKETS_PER_SECOND * USB_AUDIO_RECORD_RATE_SCALE, + USB_AUDIO_RECORD_MAX_BATCH = 64, + USB_AUDIO_RECORD_QUEUE_FRAMES = 3840, + USB_AUDIO_RECORD_FRAME_SIZE = + 2 * USB_AUDIO_SAMPLE_SIZE, + USB_AUDIO_RECORD_PACKET_SIZE = + USB_AUDIO_RECORD_PACKET_FRAMES * USB_AUDIO_RECORD_FRAME_SIZE, + USB_AUDIO_CONFIGURATION_DESC_SIZE = 9, + USB_AUDIO_FUNCTION_CONTROL_SIZE = 63, + USB_AUDIO_PLAYBACK_STREAM_DESC_SIZE = 53, + USB_AUDIO_RECORD_STREAM_DESC_SIZE = 46, }; enum @@ -74,11 +99,22 @@ enum #define USB_AUDIO_COUNT_LAYOUT(alt, channels, mask) + 1 enum { - USB_AUDIO_LAYOUT_COUNT = + USB_AUDIO_LAYOUT_COUNT = 0 USB_AUDIO_LAYOUTS(USB_AUDIO_COUNT_LAYOUT), + USB_AUDIO_RECORD_FUNCTION_OFFSET = + USB_AUDIO_CONFIGURATION_DESC_SIZE + + USB_AUDIO_FUNCTION_CONTROL_SIZE + 9 + + USB_AUDIO_LAYOUT_COUNT * USB_AUDIO_PLAYBACK_STREAM_DESC_SIZE, USB_AUDIO_CONFIGURATION_SIZE = - USB_AUDIO_DESCRIPTOR_BASE + - USB_AUDIO_LAYOUT_COUNT * USB_AUDIO_STREAM_DESC_SIZE, + USB_AUDIO_RECORD_FUNCTION_OFFSET + + USB_AUDIO_FUNCTION_CONTROL_SIZE + + 9 + USB_AUDIO_RECORD_STREAM_DESC_SIZE, + USB_AUDIO_OTHER_SPEED_SIZE = + USB_AUDIO_CONFIGURATION_DESC_SIZE + + USB_AUDIO_FUNCTION_CONTROL_SIZE + + 9 + USB_AUDIO_PLAYBACK_STREAM_DESC_SIZE + + USB_AUDIO_FUNCTION_CONTROL_SIZE + + 9 + USB_AUDIO_RECORD_STREAM_DESC_SIZE, }; #undef USB_AUDIO_COUNT_LAYOUT @@ -142,9 +178,10 @@ static const uint8_t l_deviceDescriptor[] = 0x03, 0x01, }; -#define USB_AUDIO_STREAM_DESCRIPTOR(alt, channels, mask, packetFrames, \ - feedbackSize, feedbackInterval) \ - 0x09, 0x04, 0x01, alt, 0x02, 0x01, 0x02, 0x20, 0x00, \ +#define USB_AUDIO_PLAYBACK_STREAM_DESCRIPTOR(alt, channels, mask, \ + packetFrames, feedbackSize, feedbackInterval) \ + 0x09, 0x04, USB_AUDIO_PLAYBACK_IFACE, alt, \ + 0x02, 0x01, 0x02, 0x20, 0x00, \ 0x10, 0x24, 0x01, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, \ channels, \ (mask) & 0xff, \ @@ -153,54 +190,101 @@ static const uint8_t l_deviceDescriptor[] = (mask) >> 24 & 0xff, \ 0x00, \ 0x06, 0x24, 0x02, 0x01, 0x03, 0x18, \ - 0x07, 0x05, USB_AUDIO_DATA_ENDPOINT, 0x05, \ + 0x07, 0x05, USB_AUDIO_PLAYBACK_DATA_ENDPOINT, 0x05, \ USB_AUDIO_PACKET_SIZE(packetFrames, channels) & 0xff, \ USB_AUDIO_PACKET_SIZE(packetFrames, channels) >> 8 & 0xff, \ - USB_AUDIO_DATA_INTERVAL, \ + USB_AUDIO_PLAYBACK_DATA_INTERVAL, \ 0x08, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0x07, 0x05, USB_AUDIO_FEEDBACK_ENDPOINT, 0x11, \ feedbackSize, 0x00, feedbackInterval, -#define USB_AUDIO_HS_STREAM_DESCRIPTOR(alt, channels, mask) \ - USB_AUDIO_STREAM_DESCRIPTOR( \ - alt, channels, mask, USB_AUDIO_HS_PACKET_FRAMES, \ +#define USB_AUDIO_PLAYBACK_HS_STREAM_DESCRIPTOR(alt, channels, mask) \ + USB_AUDIO_PLAYBACK_STREAM_DESCRIPTOR( \ + alt, channels, mask, USB_AUDIO_PLAYBACK_HS_PACKET_FRAMES, \ USB_AUDIO_FEEDBACK_SIZE, USB_AUDIO_FEEDBACK_BINTERVAL) +#define USB_AUDIO_RECORD_STREAM_DESCRIPTOR(packetFrames, interval) \ + 0x09, 0x04, USB_AUDIO_RECORD_IFACE, 0x01, \ + 0x01, 0x01, 0x02, 0x20, 0x00, \ + 0x10, 0x24, 0x01, 0x03, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, \ + 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, \ + 0x06, 0x24, 0x02, 0x01, 0x03, 0x18, \ + 0x07, 0x05, USB_AUDIO_RECORD_DATA_ENDPOINT, 0x05, \ + USB_AUDIO_PACKET_SIZE(packetFrames, 2) & 0xff, \ + USB_AUDIO_PACKET_SIZE(packetFrames, 2) >> 8 & 0xff, \ + interval, \ + 0x08, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + static const uint8_t l_configurationDescriptor[] = { /* Configuration */ 0x09, 0x02, USB_AUDIO_CONFIGURATION_SIZE & 0xff, USB_AUDIO_CONFIGURATION_SIZE >> 8 & 0xff, - 0x02, 0x01, 0x00, 0x80, 0x32, + 0x04, 0x01, 0x00, 0x80, 0x32, - /* Audio function interface association */ - 0x08, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, + /* Playback audio function */ + 0x08, 0x0b, USB_AUDIO_PLAYBACK_CONTROL_IFACE, 0x02, + 0x01, 0x00, 0x20, 0x02, - /* Audio control interface */ - 0x09, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x20, 0x00, + 0x09, 0x04, USB_AUDIO_PLAYBACK_CONTROL_IFACE, 0x00, + 0x00, 0x01, 0x01, 0x20, 0x00, 0x09, 0x24, 0x01, 0x00, 0x02, 0x02, 0x2e, 0x00, 0x00, - 0x08, 0x24, 0x0a, 0x01, 0x03, 0x07, 0x00, 0x00, + 0x08, 0x24, 0x0a, USB_AUDIO_PLAYBACK_CLOCK_ID, + 0x03, 0x07, 0x00, 0x00, 0x11, 0x24, 0x02, 0x02, 0x01, 0x01, 0x00, 0x01, 0x08, 0x3f, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x24, 0x03, 0x03, 0x01, 0x03, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, - /* Audio streaming interface, idle alternate setting */ - 0x09, 0x04, 0x01, 0x00, 0x00, 0x01, 0x02, 0x20, 0x00, + /* Playback interface, idle alternate setting */ + 0x09, 0x04, USB_AUDIO_PLAYBACK_IFACE, 0x00, + 0x00, 0x01, 0x02, 0x20, 0x00, /* Publish the maximum topology first for the Windows endpoint model. */ - USB_AUDIO_LAYOUTS(USB_AUDIO_HS_STREAM_DESCRIPTOR) + USB_AUDIO_LAYOUTS(USB_AUDIO_PLAYBACK_HS_STREAM_DESCRIPTOR) + + /* Recording audio function */ + 0x08, 0x0b, USB_AUDIO_RECORD_CONTROL_IFACE, 0x02, + 0x01, 0x00, 0x20, 0x04, + + 0x09, 0x04, USB_AUDIO_RECORD_CONTROL_IFACE, 0x00, + 0x00, 0x01, 0x01, 0x20, 0x00, + 0x09, 0x24, 0x01, 0x00, 0x02, 0x03, 0x2e, 0x00, 0x00, + 0x08, 0x24, 0x0a, USB_AUDIO_RECORD_CLOCK_ID, + 0x03, 0x07, 0x00, 0x00, + 0x11, 0x24, 0x02, 0x02, 0x01, 0x02, 0x00, 0x01, 0x02, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x0c, 0x24, 0x03, 0x03, 0x01, 0x01, 0x00, 0x02, 0x01, + 0x00, 0x00, 0x00, + + /* Recording interface, idle alternate setting */ + 0x09, 0x04, USB_AUDIO_RECORD_IFACE, 0x00, + 0x00, 0x01, 0x02, 0x20, 0x00, + USB_AUDIO_RECORD_STREAM_DESCRIPTOR( + USB_AUDIO_RECORD_PACKET_FRAMES, + USB_AUDIO_RECORD_DATA_BINTERVAL) }; -#undef USB_AUDIO_HS_STREAM_DESCRIPTOR +#undef USB_AUDIO_PLAYBACK_HS_STREAM_DESCRIPTOR #undef USB_AUDIO_LAYOUTS -static const uint8_t l_fullSpeedStreamDescriptor[] = +static const uint8_t l_fullSpeedPlaybackDescriptor[] = { - USB_AUDIO_STREAM_DESCRIPTOR(1, 2, USB_AUDIO_LAYOUT_STEREO, + 0x09, 0x04, USB_AUDIO_PLAYBACK_IFACE, 0x00, + 0x00, 0x01, 0x02, 0x20, 0x00, + USB_AUDIO_PLAYBACK_STREAM_DESCRIPTOR(1, 2, USB_AUDIO_LAYOUT_STEREO, USB_AUDIO_FS_PACKET_FRAMES, 3, 1) }; -#undef USB_AUDIO_STREAM_DESCRIPTOR + +static const uint8_t l_fullSpeedRecordDescriptor[] = +{ + 0x09, 0x04, USB_AUDIO_RECORD_IFACE, 0x00, + 0x00, 0x01, 0x02, 0x20, 0x00, + USB_AUDIO_RECORD_STREAM_DESCRIPTOR( + USB_AUDIO_FS_PACKET_FRAMES, 1) +}; +#undef USB_AUDIO_RECORD_STREAM_DESCRIPTOR +#undef USB_AUDIO_PLAYBACK_STREAM_DESCRIPTOR #undef USB_AUDIO_PACKET_SIZE static const uint8_t l_deviceQualifierDescriptor[] = @@ -228,15 +312,20 @@ _Static_assert(sizeof(l_channelLayouts) / sizeof(*l_channelLayouts) == _Static_assert(sizeof(l_configurationDescriptor) == USB_AUDIO_CONFIGURATION_SIZE, "invalid USB configuration descriptor size"); -_Static_assert(sizeof(l_fullSpeedStreamDescriptor) == - USB_AUDIO_STREAM_DESC_SIZE, - "invalid full-speed stream descriptor size"); +_Static_assert(sizeof(l_fullSpeedPlaybackDescriptor) == + 9 + USB_AUDIO_PLAYBACK_STREAM_DESC_SIZE, + "invalid full-speed playback descriptor size"); +_Static_assert(sizeof(l_fullSpeedRecordDescriptor) == + 9 + USB_AUDIO_RECORD_STREAM_DESC_SIZE, + "invalid full-speed recording descriptor size"); _Static_assert(sizeof(l_deviceQualifierDescriptor) == 10, "invalid USB device qualifier descriptor size"); _Static_assert(USB_AUDIO_OTHER_SPEED_SIZE <= sizeof(l_configurationDescriptor), "invalid other-speed descriptor size"); _Static_assert(USB_AUDIO_FS_PACKET_FRAMES * 2 * USB_AUDIO_SAMPLE_SIZE <= 1023, "full-speed stream packet is too large"); +_Static_assert(USB_AUDIO_RECORD_PACKET_SIZE <= 1024, + "high-speed recording packet is too large"); struct LG_USBAudio { @@ -244,14 +333,27 @@ struct LG_USBAudio void * eventOpaque; struct usbredirparser * parser; - uint8_t configuration; - uint8_t streamAlt; - uint32_t sampleRate; - bool dataStreaming; - bool feedbackStreaming; - uint64_t feedbackPacketId; - uint8_t feedbackDataPackets; + uint8_t configuration; + uint8_t playbackAlt; + uint8_t recordAlt; + uint32_t playbackSampleRate; + uint32_t recordSampleRate; + bool playbackStreaming; + bool feedbackStreaming; + uint64_t feedbackPacketId; + uint8_t feedbackDataPackets; atomic_uint feedbackValue; + + atomic_bool recordStreaming; + uint64_t recordPacketId; + uint64_t recordPacketPhase; + uint64_t recordNextPacketTime; + LG_Lock recordLock; + RingBuffer recordBuffer; + uint64_t recordRateQ16; + uint64_t recordRateStartTime; + uint64_t recordRateLastTime; + uint64_t recordRateFrames; }; static LG_USBAudio * getAudio(void * opaque) @@ -259,6 +361,13 @@ static LG_USBAudio * getAudio(void * opaque) return lgUsbRedir_device(opaque); } +static uint64_t recordTime(void) +{ + /* PipeWire timestamps use CLOCK_MONOTONIC. Keep USB capture scheduling in + * that domain instead of introducing MONOTONIC_RAW clock drift. */ + return microtime() * UINT64_C(1000); +} + static uint32_t readLE32(const uint8_t * data) { return @@ -290,7 +399,7 @@ static uint32_t encodeFeedbackRate(double sampleRate) static void resetFeedbackRate(LG_USBAudio * audio) { atomic_store_explicit(&audio->feedbackValue, - encodeFeedbackRate(audio->sampleRate), memory_order_release); + encodeFeedbackRate(audio->playbackSampleRate), memory_order_release); } static uint32_t nearestSampleRate(uint32_t sampleRate) @@ -325,7 +434,7 @@ static const USBAudioLayout * getLayout(uint8_t alt) static uint16_t layoutPacketSize(const USBAudioLayout * layout) { - return USB_AUDIO_HS_PACKET_FRAMES * layout->channelCount * + return USB_AUDIO_PLAYBACK_HS_PACKET_FRAMES * layout->channelCount * USB_AUDIO_SAMPLE_SIZE; } @@ -347,13 +456,13 @@ static size_t writeSampleRateRange(uint8_t * buffer) static void stopPlayback(LG_USBAudio * audio) { - if (!audio->dataStreaming) + if (!audio->playbackStreaming) return; - audio->dataStreaming = false; + audio->playbackStreaming = false; resetFeedbackRate(audio); - if (audio->events && audio->events->stop) - audio->events->stop(audio->eventOpaque); + if (audio->events && audio->events->playbackStop) + audio->events->playbackStop(audio->eventOpaque); } static void stopFeedback(LG_USBAudio * audio) @@ -362,7 +471,7 @@ static void stopFeedback(LG_USBAudio * audio) audio->feedbackDataPackets = 0; } -static void stopStreams(LG_USBAudio * audio) +static void stopPlaybackStreams(LG_USBAudio * audio) { stopPlayback(audio); stopFeedback(audio); @@ -370,37 +479,127 @@ static void stopStreams(LG_USBAudio * audio) static void startPlayback(LG_USBAudio * audio) { - const USBAudioLayout * layout = getLayout(audio->streamAlt); - if (audio->dataStreaming || !layout) + const USBAudioLayout * layout = getLayout(audio->playbackAlt); + if (audio->playbackStreaming || !layout) return; resetFeedbackRate(audio); - audio->dataStreaming = true; - if (audio->events && audio->events->start) - audio->events->start( - audio->eventOpaque, audio->sampleRate, layout->channelMask); + audio->playbackStreaming = true; + if (audio->events && audio->events->playbackStart) + audio->events->playbackStart( + audio->eventOpaque, audio->playbackSampleRate, + layout->channelMask); } -static void setSampleRate(LG_USBAudio * audio, uint32_t sampleRate) +static void stopRecord(LG_USBAudio * audio) { - if (audio->sampleRate == sampleRate) + if (!atomic_exchange_explicit( + &audio->recordStreaming, false, memory_order_acq_rel)) return; - const bool restart = audio->dataStreaming; + if (audio->events && audio->events->recordStop) + audio->events->recordStop(audio->eventOpaque); + LG_LOCK(audio->recordLock); + ringbuffer_reset(audio->recordBuffer); + LG_UNLOCK(audio->recordLock); +} + +static void resetRecordData(LG_USBAudio * audio) +{ + LG_LOCK(audio->recordLock); + ringbuffer_reset(audio->recordBuffer); + audio->recordRateQ16 = + (uint64_t)audio->recordSampleRate * USB_AUDIO_RECORD_RATE_SCALE; + audio->recordRateStartTime = 0; + audio->recordRateLastTime = 0; + audio->recordRateFrames = 0; + LG_UNLOCK(audio->recordLock); + audio->recordPacketPhase = 0; +} + +static void startRecord(LG_USBAudio * audio) +{ + if (audio->recordAlt != 1 || atomic_load_explicit( + &audio->recordStreaming, memory_order_acquire)) + return; + + resetRecordData(audio); + audio->recordPacketId = 0; + atomic_store_explicit( + &audio->recordStreaming, true, memory_order_release); + + if (audio->events && audio->events->recordStart) + audio->events->recordStart(audio->eventOpaque, + audio->recordSampleRate, USB_AUDIO_LAYOUT_STEREO); + + /* Backend startup may block while the capture graph is configured. Do not + * turn that setup time into a burst of stale USB packets. */ + audio->recordNextPacketTime = recordTime(); +} + +static void reconfigureRecord(LG_USBAudio * audio) +{ + if (!atomic_load_explicit( + &audio->recordStreaming, memory_order_acquire)) + return; + + if (audio->events && audio->events->recordStart) + audio->events->recordStart(audio->eventOpaque, + audio->recordSampleRate, USB_AUDIO_LAYOUT_STEREO); + + /* The provider restart has quiesced the old producer. Drop any frames + * queued around the format transition and restart the USB sample phase. */ + resetRecordData(audio); + audio->recordNextPacketTime = recordTime(); +} + +static void stopStreams(LG_USBAudio * audio) +{ + stopPlaybackStreams(audio); + stopRecord(audio); +} + +static void setPlaybackSampleRate( + LG_USBAudio * audio, uint32_t sampleRate) +{ + if (audio->playbackSampleRate == sampleRate) + return; + + const bool restartPlayback = audio->playbackStreaming; stopPlayback(audio); - audio->sampleRate = sampleRate; + audio->playbackSampleRate = sampleRate; resetFeedbackRate(audio); - if (restart) + if (restartPlayback) startPlayback(audio); } +static void setRecordSampleRate( + LG_USBAudio * audio, uint32_t sampleRate) +{ + LG_LOCK(audio->recordLock); + const bool changed = audio->recordSampleRate != sampleRate; + audio->recordSampleRate = sampleRate; + LG_UNLOCK(audio->recordLock); + if (!changed) + return; + + const bool restartRecord = atomic_load_explicit( + &audio->recordStreaming, memory_order_acquire); + if (restartRecord) + reconfigureRecord(audio); +} + static void resetDevice(LG_USBAudio * audio) { stopStreams(audio); - audio->configuration = 0; - audio->streamAlt = 0; - audio->sampleRate = LG_USB_AUDIO_DEFAULT_SAMPLE_RATE; + audio->configuration = 0; + audio->playbackAlt = 0; + audio->recordAlt = 0; + audio->playbackSampleRate = LG_USB_AUDIO_DEFAULT_SAMPLE_RATE; + LG_LOCK(audio->recordLock); + audio->recordSampleRate = LG_USB_AUDIO_DEFAULT_SAMPLE_RATE; + LG_UNLOCK(audio->recordLock); resetFeedbackRate(audio); } @@ -408,11 +607,17 @@ static void sendInterfaceInfo(LG_USBAudio * audio) { struct usb_redir_interface_info_header info = { - .interface_count = 2, - .interface = { USB_AUDIO_CONTROL_IFACE, USB_AUDIO_STREAM_IFACE }, - .interface_class = { 0x01, 0x01 }, - .interface_subclass = { 0x01, 0x02 }, - .interface_protocol = { 0x20, 0x20 }, + .interface_count = 4, + .interface = + { + USB_AUDIO_PLAYBACK_CONTROL_IFACE, + USB_AUDIO_PLAYBACK_IFACE, + USB_AUDIO_RECORD_CONTROL_IFACE, + USB_AUDIO_RECORD_IFACE, + }, + .interface_class = { 0x01, 0x01, 0x01, 0x01 }, + .interface_subclass = { 0x01, 0x02, 0x01, 0x02 }, + .interface_protocol = { 0x20, 0x20, 0x20, 0x20 }, }; usbredirparser_send_interface_info(audio->parser, &info); } @@ -420,7 +625,7 @@ static void sendInterfaceInfo(LG_USBAudio * audio) static void sendEndpointInfo(LG_USBAudio * audio) { struct usb_redir_ep_info_header info = { 0 }; - const USBAudioLayout * layout = getLayout(audio->streamAlt); + const USBAudioLayout * layout = getLayout(audio->playbackAlt); memset(info.type, usb_redir_type_invalid, sizeof(info.type)); info.type[0] = usb_redir_type_control; @@ -430,21 +635,35 @@ static void sendEndpointInfo(LG_USBAudio * audio) if (audio->configuration == USB_AUDIO_CONFIGURATION && layout) { - info.type[USB_AUDIO_DATA_EP_INDEX] = usb_redir_type_iso; - info.interval[USB_AUDIO_DATA_EP_INDEX] = USB_AUDIO_DATA_INTERVAL; - info.interface[USB_AUDIO_DATA_EP_INDEX] = USB_AUDIO_STREAM_IFACE; - info.max_packet_size[USB_AUDIO_DATA_EP_INDEX] = + info.type[USB_AUDIO_PLAYBACK_DATA_EP_INDEX] = usb_redir_type_iso; + info.interval[USB_AUDIO_PLAYBACK_DATA_EP_INDEX] = + USB_AUDIO_PLAYBACK_DATA_INTERVAL; + info.interface[USB_AUDIO_PLAYBACK_DATA_EP_INDEX] = + USB_AUDIO_PLAYBACK_IFACE; + info.max_packet_size[USB_AUDIO_PLAYBACK_DATA_EP_INDEX] = layoutPacketSize(layout); info.type[USB_AUDIO_FEEDBACK_EP_INDEX] = usb_redir_type_iso; info.interval[USB_AUDIO_FEEDBACK_EP_INDEX] = USB_AUDIO_FEEDBACK_INTERVAL; info.interface[USB_AUDIO_FEEDBACK_EP_INDEX] = - USB_AUDIO_STREAM_IFACE; + USB_AUDIO_PLAYBACK_IFACE; info.max_packet_size[USB_AUDIO_FEEDBACK_EP_INDEX] = USB_AUDIO_FEEDBACK_SIZE; } + if (audio->configuration == USB_AUDIO_CONFIGURATION && + audio->recordAlt == 1) + { + info.type[USB_AUDIO_RECORD_DATA_EP_INDEX] = usb_redir_type_iso; + info.interval[USB_AUDIO_RECORD_DATA_EP_INDEX] = + USB_AUDIO_RECORD_DATA_INTERVAL; + info.interface[USB_AUDIO_RECORD_DATA_EP_INDEX] = + USB_AUDIO_RECORD_IFACE; + info.max_packet_size[USB_AUDIO_RECORD_DATA_EP_INDEX] = + USB_AUDIO_RECORD_PACKET_SIZE; + } + usbredirparser_send_ep_info(audio->parser, &info); } @@ -507,14 +726,24 @@ static void setAltSetting(void * opaque, uint64_t id, }; if (audio->configuration == USB_AUDIO_CONFIGURATION && - request->interface == USB_AUDIO_CONTROL_IFACE && request->alt == 0) + (request->interface == USB_AUDIO_PLAYBACK_CONTROL_IFACE || + request->interface == USB_AUDIO_RECORD_CONTROL_IFACE) && + request->alt == 0) status.status = usb_redir_success; else if (audio->configuration == USB_AUDIO_CONFIGURATION && - request->interface == USB_AUDIO_STREAM_IFACE && + request->interface == USB_AUDIO_PLAYBACK_IFACE && (request->alt == 0 || getLayout(request->alt))) { - stopStreams(audio); - audio->streamAlt = request->alt; + stopPlaybackStreams(audio); + audio->playbackAlt = request->alt; + status.status = usb_redir_success; + } + else if (audio->configuration == USB_AUDIO_CONFIGURATION && + request->interface == USB_AUDIO_RECORD_IFACE && + request->alt <= 1) + { + stopRecord(audio); + audio->recordAlt = request->alt; status.status = usb_redir_success; } @@ -535,13 +764,20 @@ static void getAltSetting(void * opaque, uint64_t id, }; if (audio->configuration == USB_AUDIO_CONFIGURATION && - request->interface == USB_AUDIO_CONTROL_IFACE) + (request->interface == USB_AUDIO_PLAYBACK_CONTROL_IFACE || + request->interface == USB_AUDIO_RECORD_CONTROL_IFACE)) status.status = usb_redir_success; else if (audio->configuration == USB_AUDIO_CONFIGURATION && - request->interface == USB_AUDIO_STREAM_IFACE) + request->interface == USB_AUDIO_PLAYBACK_IFACE) { status.status = usb_redir_success; - status.alt = audio->streamAlt; + status.alt = audio->playbackAlt; + } + else if (audio->configuration == USB_AUDIO_CONFIGURATION && + request->interface == USB_AUDIO_RECORD_IFACE) + { + status.status = usb_redir_success; + status.alt = audio->recordAlt; } usbredirparser_send_alt_setting_status(audio->parser, id, &status); @@ -576,6 +812,65 @@ static void sendFeedbackPackets(LG_USBAudio * audio, uint32_t count) &packet, data, USB_AUDIO_FEEDBACK_SIZE); } +static void sendRecordPackets(LG_USBAudio * audio) +{ + if (!atomic_load_explicit( + &audio->recordStreaming, memory_order_acquire)) + return; + + const uint64_t now = recordTime(); + if (audio->recordNextPacketTime > now) + return; + + uint64_t count = + (now - audio->recordNextPacketTime) / + USB_AUDIO_RECORD_PACKET_INTERVAL_NS + 1; + if (count > USB_AUDIO_RECORD_MAX_BATCH) + { + count = USB_AUDIO_RECORD_MAX_BATCH; + audio->recordNextPacketTime = now - + (count - 1) * USB_AUDIO_RECORD_PACKET_INTERVAL_NS; + } + + uint8_t data[USB_AUDIO_RECORD_PACKET_SIZE]; + const int target = max( + (int)(audio->recordSampleRate / 50), + USB_AUDIO_RECORD_PACKET_FRAMES); + LG_LOCK(audio->recordLock); + const uint64_t rateQ16 = audio->recordRateQ16; + LG_UNLOCK(audio->recordLock); + struct usb_redir_iso_packet_header packet = + { + .endpoint = USB_AUDIO_RECORD_DATA_ENDPOINT, + .status = usb_redir_success, + }; + + for (uint64_t i = 0; i < count; ++i) + { + audio->recordPacketPhase += rateQ16; + const uint32_t frames = (uint32_t)(audio->recordPacketPhase / + USB_AUDIO_RECORD_RATE_DENOMINATOR); + audio->recordPacketPhase %= USB_AUDIO_RECORD_RATE_DENOMINATOR; + DEBUG_ASSERT(frames <= USB_AUDIO_RECORD_PACKET_FRAMES); + + const size_t size = frames * USB_AUDIO_RECORD_FRAME_SIZE; + memset(data, 0, size); + + LG_LOCK(audio->recordLock); + const int queued = ringbuffer_getCount(audio->recordBuffer); + if (queued > target) + ringbuffer_consume(audio->recordBuffer, NULL, queued - target); + ringbuffer_consume(audio->recordBuffer, data, frames); + LG_UNLOCK(audio->recordLock); + + packet.length = (uint16_t)size; + usbredirparser_send_iso_packet(audio->parser, + audio->recordPacketId++, &packet, data, (int)size); + audio->recordNextPacketTime += + USB_AUDIO_RECORD_PACKET_INTERVAL_NS; + } +} + static void startISOStream(void * opaque, uint64_t id, struct usb_redir_start_iso_stream_header * request) { @@ -583,17 +878,20 @@ static void startISOStream(void * opaque, uint64_t id, uint8_t result = usb_redir_stall; uint32_t feedbackPrefill = 0; - if (audio->configuration == USB_AUDIO_CONFIGURATION && - getLayout(audio->streamAlt)) + if (audio->configuration == USB_AUDIO_CONFIGURATION) { switch (request->endpoint) { - case USB_AUDIO_DATA_ENDPOINT: + case USB_AUDIO_PLAYBACK_DATA_ENDPOINT: + if (!getLayout(audio->playbackAlt)) + break; result = usb_redir_success; startPlayback(audio); break; case USB_AUDIO_FEEDBACK_ENDPOINT: + if (!getLayout(audio->playbackAlt)) + break; resetFeedbackRate(audio); audio->feedbackStreaming = true; audio->feedbackDataPackets = 0; @@ -607,6 +905,13 @@ static void startISOStream(void * opaque, uint64_t id, feedbackPrefill = USB_AUDIO_FEEDBACK_MAX_QUEUE; result = usb_redir_success; break; + + case USB_AUDIO_RECORD_DATA_ENDPOINT: + if (audio->recordAlt != 1) + break; + result = usb_redir_success; + startRecord(audio); + break; } } @@ -622,7 +927,7 @@ static void stopISOStream(void * opaque, uint64_t id, uint8_t result = usb_redir_stall; switch (request->endpoint) { - case USB_AUDIO_DATA_ENDPOINT: + case USB_AUDIO_PLAYBACK_DATA_ENDPOINT: stopPlayback(audio); result = usb_redir_success; break; @@ -631,6 +936,11 @@ static void stopISOStream(void * opaque, uint64_t id, stopFeedback(audio); result = usb_redir_success; break; + + case USB_AUDIO_RECORD_DATA_ENDPOINT: + stopRecord(audio); + result = usb_redir_success; + break; } sendISOStatus(audio, id, request->endpoint, result); } @@ -654,9 +964,10 @@ static size_t stringDescriptor(uint8_t index, uint8_t * buffer, const char * text; switch (index) { - case 1: text = "Looking Glass" ; break; - case 2: text = "Looking Glass USB Audio"; break; - case 3: text = "LG-UAC2-0004" ; break; + case 1: text = "Looking Glass" ; break; + case 2: text = "Looking Glass USB Audio" ; break; + case 3: text = "LG-UAC2-0006" ; break; + case 4: text = "Looking Glass Microphone" ; break; default: return 0; } @@ -758,14 +1069,25 @@ static void controlPacket(void * opaque, uint64_t id, { /* Full-speed USB cannot carry the maximum PCM24 stream. Expose a * functional stereo alternate sized for rates through 96 kHz. */ + const size_t playbackControlSize = + USB_AUDIO_CONFIGURATION_DESC_SIZE + + USB_AUDIO_FUNCTION_CONTROL_SIZE; memcpy(buffer, l_configurationDescriptor, - USB_AUDIO_DESCRIPTOR_BASE); - memcpy(buffer + USB_AUDIO_DESCRIPTOR_BASE, - l_fullSpeedStreamDescriptor, - sizeof(l_fullSpeedStreamDescriptor)); + playbackControlSize); + size_t offset = playbackControlSize; + memcpy(buffer + offset, l_fullSpeedPlaybackDescriptor, + sizeof(l_fullSpeedPlaybackDescriptor)); + offset += sizeof(l_fullSpeedPlaybackDescriptor); + memcpy(buffer + offset, + l_configurationDescriptor + + USB_AUDIO_RECORD_FUNCTION_OFFSET, + USB_AUDIO_FUNCTION_CONTROL_SIZE); + offset += USB_AUDIO_FUNCTION_CONTROL_SIZE; + memcpy(buffer + offset, l_fullSpeedRecordDescriptor, + sizeof(l_fullSpeedRecordDescriptor)); buffer[1] = USB_DESCRIPTOR_OTHER_SPEED; - buffer[2] = USB_AUDIO_OTHER_SPEED_SIZE; - buffer[3] = 0; + buffer[2] = USB_AUDIO_OTHER_SPEED_SIZE & 0xff; + buffer[3] = USB_AUDIO_OTHER_SPEED_SIZE >> 8; response = buffer; responseSize = USB_AUDIO_OTHER_SPEED_SIZE; } @@ -787,16 +1109,20 @@ static void controlPacket(void * opaque, uint64_t id, case USB_REQUEST_TYPE_IN_STANDARD_INTERFACE: valid = audio->configuration == USB_AUDIO_CONFIGURATION && - (request->index == USB_AUDIO_CONTROL_IFACE || - request->index == USB_AUDIO_STREAM_IFACE); + (request->index == USB_AUDIO_PLAYBACK_CONTROL_IFACE || + request->index == USB_AUDIO_PLAYBACK_IFACE || + request->index == USB_AUDIO_RECORD_CONTROL_IFACE || + request->index == USB_AUDIO_RECORD_IFACE); break; case USB_REQUEST_TYPE_IN_STANDARD_ENDPOINT: valid = request->index == 0x00 || request->index == 0x80 || (audio->configuration == USB_AUDIO_CONFIGURATION && - getLayout(audio->streamAlt) && - (request->index == USB_AUDIO_DATA_ENDPOINT || - request->index == USB_AUDIO_FEEDBACK_ENDPOINT)); + ((getLayout(audio->playbackAlt) && + (request->index == USB_AUDIO_PLAYBACK_DATA_ENDPOINT || + request->index == USB_AUDIO_FEEDBACK_ENDPOINT)) || + (audio->recordAlt == 1 && + request->index == USB_AUDIO_RECORD_DATA_ENDPOINT))); break; } @@ -809,15 +1135,23 @@ static void controlPacket(void * opaque, uint64_t id, } else if (audio->configuration == USB_AUDIO_CONFIGURATION && request->requesttype == USB_REQUEST_TYPE_IN_CLASS_INTERFACE && - request->index == - ((USB_AUDIO_CLOCK_ID << 8) | USB_AUDIO_CONTROL_IFACE) && + (request->index == + ((USB_AUDIO_PLAYBACK_CLOCK_ID << 8) | + USB_AUDIO_PLAYBACK_CONTROL_IFACE) || + request->index == + ((USB_AUDIO_RECORD_CLOCK_ID << 8) | + USB_AUDIO_RECORD_CONTROL_IFACE)) && (uint8_t)request->value == 0) { + const bool playbackClock = request->index == + ((USB_AUDIO_PLAYBACK_CLOCK_ID << 8) | + USB_AUDIO_PLAYBACK_CONTROL_IFACE); const uint8_t control = request->value >> 8; if (request->request == USB_REQUEST_CUR && control == USB_AUDIO_CONTROL_FREQUENCY) { - writeLE32(buffer, audio->sampleRate); + writeLE32(buffer, playbackClock ? + audio->playbackSampleRate : audio->recordSampleRate); response = buffer; responseSize = 4; status = usb_redir_success; @@ -840,12 +1174,22 @@ static void controlPacket(void * opaque, uint64_t id, else if (audio->configuration == USB_AUDIO_CONFIGURATION && request->requesttype == USB_REQUEST_TYPE_OUT_CLASS_INTERFACE && request->request == USB_REQUEST_CUR && - request->index == - ((USB_AUDIO_CLOCK_ID << 8) | USB_AUDIO_CONTROL_IFACE) && + (request->index == + ((USB_AUDIO_PLAYBACK_CLOCK_ID << 8) | + USB_AUDIO_PLAYBACK_CONTROL_IFACE) || + request->index == + ((USB_AUDIO_RECORD_CLOCK_ID << 8) | + USB_AUDIO_RECORD_CONTROL_IFACE)) && request->value == (USB_AUDIO_CONTROL_FREQUENCY << 8) && request->length == 4 && dataLength == 4 && data) { - setSampleRate(audio, nearestSampleRate(readLE32(data))); + const uint32_t sampleRate = nearestSampleRate(readLE32(data)); + if (request->index == + ((USB_AUDIO_PLAYBACK_CLOCK_ID << 8) | + USB_AUDIO_PLAYBACK_CONTROL_IFACE)) + setPlaybackSampleRate(audio, sampleRate); + else + setRecordSampleRate(audio, sampleRate); status = usb_redir_success; } @@ -854,10 +1198,11 @@ static void controlPacket(void * opaque, uint64_t id, usbredirparser_free_packet_data(audio->parser, data); } -static void stallStream(LG_USBAudio * audio) +static void stallPlayback(LG_USBAudio * audio) { stopPlayback(audio); - sendISOStatus(audio, 0, USB_AUDIO_DATA_ENDPOINT, usb_redir_stall); + sendISOStatus(audio, 0, + USB_AUDIO_PLAYBACK_DATA_ENDPOINT, usb_redir_stall); } static void isoPacket(void * opaque, uint64_t id, @@ -866,25 +1211,25 @@ static void isoPacket(void * opaque, uint64_t id, { (void)id; LG_USBAudio * audio = getAudio(opaque); - const USBAudioLayout * layout = getLayout(audio->streamAlt); + const USBAudioLayout * layout = getLayout(audio->playbackAlt); - if (audio->dataStreaming) + if (audio->playbackStreaming) { const uint16_t frameSize = layout ? layout->channelCount * USB_AUDIO_SAMPLE_SIZE : 0; - if (packet->endpoint != USB_AUDIO_DATA_ENDPOINT || + if (packet->endpoint != USB_AUDIO_PLAYBACK_DATA_ENDPOINT || packet->status != usb_redir_success || dataLength < 0 || packet->length != dataLength || !layout || dataLength > layoutPacketSize(layout) || dataLength % frameSize != 0) { DEBUG_WARN("Invalid USB audio isochronous packet"); - stallStream(audio); + stallPlayback(audio); } else { - if (dataLength && audio->events && audio->events->data) - audio->events->data(audio->eventOpaque, data, + if (dataLength && audio->events && audio->events->playbackData) + audio->events->playbackData(audio->eventOpaque, data, dataLength / frameSize); if (audio->feedbackStreaming && @@ -942,11 +1287,17 @@ static void unplugDevice(void * opaque) resetDevice(opaque); } +static void processDevice(void * opaque) +{ + sendRecordPackets(opaque); +} + static const LG_USBRedirDeviceOps l_deviceOps = { - .setup = setupDevice, - .plug = plugDevice, - .unplug = unplugDevice, + .setup = setupDevice, + .plug = plugDevice, + .process = processDevice, + .unplug = unplugDevice, }; LG_USBAudio * lgUsbAudio_create( @@ -956,11 +1307,22 @@ LG_USBAudio * lgUsbAudio_create( if (!audio) return NULL; - audio->events = events; - audio->eventOpaque = eventOpaque; - audio->sampleRate = LG_USB_AUDIO_DEFAULT_SAMPLE_RATE; + audio->events = events; + audio->eventOpaque = eventOpaque; + audio->playbackSampleRate = LG_USB_AUDIO_DEFAULT_SAMPLE_RATE; + audio->recordSampleRate = LG_USB_AUDIO_DEFAULT_SAMPLE_RATE; + LG_LOCK_INIT(audio->recordLock); + audio->recordBuffer = ringbuffer_new( + USB_AUDIO_RECORD_QUEUE_FRAMES, USB_AUDIO_RECORD_FRAME_SIZE); + if (!audio->recordBuffer) + { + free(audio); + return NULL; + } + atomic_init(&audio->feedbackValue, - encodeFeedbackRate(audio->sampleRate)); + encodeFeedbackRate(audio->playbackSampleRate)); + atomic_init(&audio->recordStreaming, false); return audio; } @@ -973,11 +1335,98 @@ void lgUsbAudio_setFeedbackRate(LG_USBAudio * audio, double sampleRate) encodeFeedbackRate(sampleRate), memory_order_release); } +bool lgUsbAudio_recordData( + LG_USBAudio * audio, const void * data, size_t frames) +{ + if (!audio || frames > INT_MAX || (frames && !data) || + !atomic_load_explicit( + &audio->recordStreaming, memory_order_acquire)) + return false; + + LG_LOCK(audio->recordLock); + if (!atomic_load_explicit( + &audio->recordStreaming, memory_order_acquire)) + { + LG_UNLOCK(audio->recordLock); + return false; + } + + const size_t receivedFrames = frames; + const uint64_t now = recordTime(); + /* Average complete capture batches over time so callback jitter does not + * become USB packet jitter. recordRateQ16 is frames/second in Q16. */ + const bool resetRate = + !audio->recordRateLastTime || + now - audio->recordRateLastTime > USB_AUDIO_RECORD_RATE_GAP_NS; + if (resetRate) + { + audio->recordRateQ16 = + (uint64_t)audio->recordSampleRate * USB_AUDIO_RECORD_RATE_SCALE; + audio->recordRateStartTime = now; + audio->recordRateFrames = 0; + } + else + audio->recordRateFrames += receivedFrames; + audio->recordRateLastTime = now; + + const uint64_t elapsed = now - audio->recordRateStartTime; + if (elapsed >= USB_AUDIO_RECORD_RATE_SETTLE_NS) + { + const double measured = + (double)audio->recordRateFrames * 1000000000.0 / elapsed; + if (measured >= audio->recordSampleRate * 0.995 && + measured <= audio->recordSampleRate * 1.005) + { + const uint64_t measuredQ16 = (uint64_t)( + measured * USB_AUDIO_RECORD_RATE_SCALE + 0.5); + audio->recordRateQ16 = + (audio->recordRateQ16 * 3 + measuredQ16) / 4; + } + + audio->recordRateStartTime = now; + audio->recordRateFrames = 0; + } + + const int length = ringbuffer_getLength(audio->recordBuffer); + const uint8_t * input = data; + bool dropped = false; + if (frames > (size_t)length) + { + input += (frames - length) * USB_AUDIO_RECORD_FRAME_SIZE; + frames = length; + ringbuffer_reset(audio->recordBuffer); + dropped = true; + } + else + { + const int overflow = + ringbuffer_getCount(audio->recordBuffer) + (int)frames - length; + if (overflow > 0) + { + ringbuffer_consume(audio->recordBuffer, NULL, overflow); + dropped = true; + } + } + + const int appended = ringbuffer_append( + audio->recordBuffer, input, (int)frames); + LG_UNLOCK(audio->recordLock); + return !dropped && appended == (int)frames; +} + +bool lgUsbAudio_recording(const LG_USBAudio * audio) +{ + return audio && atomic_load_explicit( + &audio->recordStreaming, memory_order_acquire); +} + void lgUsbAudio_destroy(LG_USBAudio * audio) { if (!audio) return; + ringbuffer_free(&audio->recordBuffer); + LG_LOCK_FREE(audio->recordLock); free(audio); } diff --git a/client/src/usb_audio.h b/client/src/usb_audio.h index 7280b394..71d435b7 100644 --- a/client/src/usb_audio.h +++ b/client/src/usb_audio.h @@ -32,13 +32,19 @@ typedef struct LG_USBAudio LG_USBAudio; typedef struct LG_USBAudioEventOps { - void (*start)(void * opaque, uint32_t sampleRate, uint32_t channelMask); - void (*stop)(void * opaque); + void (*playbackStart)( + void * opaque, uint32_t sampleRate, uint32_t channelMask); + void (*playbackStop)(void * opaque); /* Data is borrowed interleaved packed signed 24-bit PCM, little endian. * Channels are ordered by ascending set bits in the selected UAC channel * mask. */ - void (*data)(void * opaque, const void * data, size_t frames); + void (*playbackData)(void * opaque, const void * data, size_t frames); + + /* Repeated while active when the recording clock changes rate. */ + void (*recordStart)( + void * opaque, uint32_t sampleRate, uint32_t channelMask); + void (*recordStop)(void * opaque); } LG_USBAudioEventOps; @@ -51,6 +57,14 @@ void lgUsbAudio_destroy(LG_USBAudio * audio); * feedback thread. */ void lgUsbAudio_setFeedbackRate(LG_USBAudio * audio, double sampleRate); +/* Queue interleaved packed signed 24-bit microphone frames. This may be + * called from the audio recording thread. */ +bool lgUsbAudio_recordData( + LG_USBAudio * audio, const void * data, size_t frames); + +/* This must be queried on the PureSpice processing thread. */ +bool lgUsbAudio_recording(const LG_USBAudio * audio); + const LG_USBRedirDeviceOps * lgUsbAudio_deviceOps(void); #endif diff --git a/client/src/usbredir.c b/client/src/usbredir.c index 435bd9dd..7925a4a3 100644 --- a/client/src/usbredir.c +++ b/client/src/usbredir.c @@ -296,6 +296,9 @@ bool lgUsbRedir_process(LG_USBRedir * usbredir) } } + if (usbredir->plugged && usbredir->deviceOps->process) + usbredir->deviceOps->process(usbredir->deviceOpaque); + return flushUSBRedir(usbredir); } diff --git a/client/src/usbredir.h b/client/src/usbredir.h index aa67fbb7..e3503698 100644 --- a/client/src/usbredir.h +++ b/client/src/usbredir.h @@ -42,6 +42,10 @@ typedef struct LG_USBRedirDeviceOps /* Queue the device descriptors and connection announcement. */ void (*plug)(void * opaque, struct usbredirparser * parser); + /* Queue time-sensitive device data. This runs on the PureSpice processing + * thread immediately before parser output is flushed. */ + void (*process)(void * opaque); + /* Stop all device activity. The bridge sends the disconnect packet. */ void (*unplug)(void * opaque); }