From 06f111dc900136f8c683c97e414a030959989b3f Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sun, 9 Aug 2026 22:12:39 +1000 Subject: [PATCH] [client] usb audio: add audio provider Adapt the emulated UAC2 playback stream to LG_AudioOps. Preserve borrowed packet delivery, publish reconstructed timing, and synchronize provider availability and attachment transitions. --- client/CMakeLists.txt | 1 + client/src/audio_usb.c | 569 +++++++++++++++++++++++++++++++++++++++++ client/src/audio_usb.h | 37 +++ client/src/usbredir.c | 19 +- client/src/usbredir.h | 6 +- 5 files changed, 628 insertions(+), 4 deletions(-) create mode 100644 client/src/audio_usb.c create mode 100644 client/src/audio_usb.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 63900f7f..fe179468 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -216,6 +216,7 @@ endif() if(ENABLE_USB_AUDIO) list(APPEND SOURCES + src/audio_usb.c src/usb_audio.c src/usbredir.c ) diff --git a/client/src/audio_usb.c b/client/src/audio_usb.c new file mode 100644 index 00000000..e3265488 --- /dev/null +++ b/client/src/audio_usb.c @@ -0,0 +1,569 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "audio_usb.h" + +#include "usb_audio.h" + +#include "common/locking.h" +#include "common/time.h" + +#include +#include + +#define USB_AUDIO_NS_PER_SECOND INT64_C(1000000000) + +typedef struct USBAudioCallbackFrame +{ + const struct LGA_USBState * state; + struct USBAudioCallbackFrame * previous; +} +USBAudioCallbackFrame; + +typedef struct USBAudioEventTarget +{ + const LG_AudioEventOps * events; + void * opaque; + uint32_t attachmentGeneration; + USBAudioCallbackFrame frame; +} +USBAudioEventTarget; + +struct LGA_USBState +{ + LG_USBAudio * device; + LG_USBRedir * redir; + + LG_Lock statusLock; + atomic_bool available; + uint32_t statusGeneration; + LG_AudioStatusFn statusCallback; + void * statusOpaque; + atomic_uint statusInFlight; + atomic_uint_fast64_t statusNextTicket; + atomic_uint_fast64_t statusServingTicket; + + LG_Lock stateLock; + bool attached; + bool detaching; + uint32_t attachmentGeneration; + const LG_AudioEventOps * events; + void * eventOpaque; + + uint32_t streamGeneration; + uint32_t generationSerial; + int64_t clockOrigin; + atomic_uint_fast64_t position; + atomic_uint deliveryGeneration; + atomic_uint inFlight; +}; + +static _Thread_local USBAudioCallbackFrame * l_eventFrames; +static _Thread_local USBAudioCallbackFrame * l_statusFrames; + +static const LG_AudioFormat l_format = +{ + .sampleFormat = LG_AUDIO_FMT_F32_LE, + .sampleRate = LG_USB_AUDIO_SAMPLE_RATE, + .channelCount = LG_USB_AUDIO_CHANNELS, + .channels = + { + LG_AUDIO_CH_FRONT_LEFT, + LG_AUDIO_CH_FRONT_RIGHT, + }, +}; + +static uint32_t nextGeneration(uint32_t generation) +{ + if (++generation == 0) + ++generation; + return generation; +} + +static unsigned int callbackDepth( + const USBAudioCallbackFrame * frame, const LGA_USBState * state) +{ + unsigned int depth = 0; + while (frame) + { + if (frame->state == state) + ++depth; + frame = frame->previous; + } + return depth; +} + +static void beginCallback(LGA_USBState * state, + USBAudioCallbackFrame * frame, USBAudioCallbackFrame ** frames, + atomic_uint * inFlight) +{ + atomic_fetch_add_explicit(inFlight, 1, memory_order_seq_cst); + frame->state = state; + frame->previous = *frames; + *frames = frame; +} + +static void endCallback(USBAudioCallbackFrame * frame, + USBAudioCallbackFrame ** frames, atomic_uint * inFlight) +{ + *frames = frame->previous; + atomic_fetch_sub_explicit(inFlight, 1, memory_order_seq_cst); +} + +static LG_AudioClock makeClock( + 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 int64_t elapsed = + position / LG_USB_AUDIO_SAMPLE_RATE * USB_AUDIO_NS_PER_SECOND + + position % LG_USB_AUDIO_SAMPLE_RATE * USB_AUDIO_NS_PER_SECOND / + LG_USB_AUDIO_SAMPLE_RATE; + + return (LG_AudioClock) + { + .position = position, + .time = state->clockOrigin + elapsed, + .rate = 0.0, + .stable = false, + }; +} + +/* stateLock must be held while admitting a control event. The data event + * path performs the equivalent admission using deliveryGeneration. */ +static bool beginEventNL( + LGA_USBState * state, USBAudioEventTarget * target) +{ + if (!state->attached || !state->events) + return false; + + target->events = state->events; + target->opaque = state->eventOpaque; + target->attachmentGeneration = state->attachmentGeneration; + beginCallback( + state, &target->frame, &l_eventFrames, &state->inFlight); + return true; +} + +static bool attachmentCurrentNL(const LGA_USBState * state, + const USBAudioEventTarget * target) +{ + return state->attached && + state->attachmentGeneration == target->attachmentGeneration && + state->events == target->events && + state->eventOpaque == target->opaque; +} + +static bool eventCurrentNL(const LGA_USBState * state, + const USBAudioEventTarget * target, uint32_t streamGeneration) +{ + return attachmentCurrentNL(state, target) && + state->streamGeneration == streamGeneration; +} + +static void endEvent(LGA_USBState * state, USBAudioEventTarget * target) +{ + endCallback(&target->frame, &l_eventFrames, &state->inFlight); +} + +static void waitEvents(const LGA_USBState * state) +{ + const unsigned int depth = callbackDepth(l_eventFrames, state); + while (atomic_load_explicit( + &state->inFlight, memory_order_seq_cst) > depth) + ; +} + +static bool beginStatusOperation(LGA_USBState * state) +{ + if (callbackDepth(l_statusFrames, state)) + return false; + + const uint_fast64_t ticket = atomic_fetch_add_explicit( + &state->statusNextTicket, 1, memory_order_relaxed); + while (atomic_load_explicit( + &state->statusServingTicket, memory_order_acquire) != ticket) + ; + return true; +} + +static void endStatusOperation(LGA_USBState * state, bool owner) +{ + if (owner) + atomic_fetch_add_explicit( + &state->statusServingTicket, 1, memory_order_release); +} + +static void beginStatusCallback( + LGA_USBState * state, USBAudioCallbackFrame * frame) +{ + beginCallback( + state, frame, &l_statusFrames, &state->statusInFlight); +} + +static void endStatusCallback( + LGA_USBState * state, USBAudioCallbackFrame * frame) +{ + endCallback(frame, &l_statusFrames, &state->statusInFlight); +} + +static void waitStatusCallbacks(const LGA_USBState * state) +{ + const unsigned int depth = callbackDepth(l_statusFrames, state); + while (atomic_load_explicit( + &state->statusInFlight, memory_order_acquire) > depth) + ; +} + +static void usbAudioStart(void * opaque) +{ + LGA_USBState * state = opaque; + USBAudioEventTarget target; + bool dispatch; + uint32_t generation; + LG_AudioClock clock; + + LG_LOCK(state->stateLock); + if (state->streamGeneration) + { + LG_UNLOCK(state->stateLock); + return; + } + + generation = state->generationSerial = + nextGeneration(state->generationSerial); + state->streamGeneration = generation; + state->clockOrigin = (int64_t)nanotime(); + atomic_store_explicit(&state->position, 0, memory_order_relaxed); + atomic_store_explicit( + &state->deliveryGeneration, 0, memory_order_seq_cst); + clock = makeClock(state, 0); + const bool admitted = beginEventNL(state, &target); + dispatch = admitted && target.events->playbackStart; + if (admitted && !dispatch) + endEvent(state, &target); + LG_UNLOCK(state->stateLock); + + if (!dispatch) + return; + + target.events->playbackStart( + target.opaque, generation, &l_format, &clock); + + LG_LOCK(state->stateLock); + if (eventCurrentNL(state, &target, generation)) + atomic_store_explicit(&state->deliveryGeneration, + generation, memory_order_seq_cst); + LG_UNLOCK(state->stateLock); + endEvent(state, &target); +} + +static void usbAudioStop(void * opaque) +{ + LGA_USBState * state = opaque; + USBAudioEventTarget target; + + LG_LOCK(state->stateLock); + const uint32_t generation = state->streamGeneration; + if (!generation) + { + LG_UNLOCK(state->stateLock); + return; + } + + state->streamGeneration = 0; + atomic_store_explicit( + &state->deliveryGeneration, 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->playbackStop; + LG_UNLOCK(state->stateLock); + + if (dispatch) + target.events->playbackStop(target.opaque, generation); + endEvent(state, &target); +} + +static void usbAudioData(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); + const uint32_t generation = atomic_load_explicit( + &state->deliveryGeneration, 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); + events->playbackData( + target, generation, data, frames, &clock); + } + } + endCallback(&frame, &l_eventFrames, &state->inFlight); +} + +static const LG_USBAudioEventOps l_usbAudioEvents = +{ + .start = usbAudioStart, + .stop = usbAudioStop, + .data = usbAudioData, +}; + +static void usbSetAvailable(void * opaque, bool available) +{ + LGA_USBState * state = opaque; + const bool statusOwner = beginStatusOperation(state); + + LG_LOCK(state->statusLock); + const bool changed = atomic_load_explicit( + &state->available, memory_order_relaxed) != available; + if (changed) + { + atomic_store_explicit( + &state->available, available, memory_order_release); + state->statusGeneration = + nextGeneration(state->statusGeneration); + } + + const LG_AudioStatusFn callback = state->statusCallback; + void * callbackOpaque = state->statusOpaque; + const LG_AudioStatus status = + { + .available = available, + .generation = state->statusGeneration, + }; + USBAudioCallbackFrame frame; + if (changed && callback) + beginStatusCallback(state, &frame); + LG_UNLOCK(state->statusLock); + + if (changed && callback) + { + callback(callbackOpaque, &status); + endStatusCallback(state, &frame); + } + endStatusOperation(state, statusOwner); +} + +static void usbSetStatusListener(void * opaque, + LG_AudioStatusFn callback, void * callbackOpaque) +{ + LGA_USBState * state = opaque; + const bool statusOwner = beginStatusOperation(state); + + LG_LOCK(state->statusLock); + state->statusCallback = callback; + state->statusOpaque = callbackOpaque; + const LG_AudioStatus status = + { + .available = atomic_load_explicit( + &state->available, memory_order_acquire), + .generation = state->statusGeneration, + }; + USBAudioCallbackFrame frame; + if (callback) + beginStatusCallback(state, &frame); + LG_UNLOCK(state->statusLock); + + if (callback) + { + callback(callbackOpaque, &status); + endStatusCallback(state, &frame); + } + else + waitStatusCallbacks(state); + endStatusOperation(state, statusOwner); +} + +static bool usbAttach(void * opaque, const LG_AudioEventOps * events, + void * eventOpaque) +{ + LGA_USBState * state = opaque; + if (!events || !atomic_load_explicit( + &state->available, memory_order_acquire)) + return false; + + USBAudioEventTarget target; + LG_AudioClock clock; + uint32_t generation; + bool dispatch; + + for (;;) + { + LG_LOCK(state->stateLock); + if (!state->detaching) + break; + LG_UNLOCK(state->stateLock); + if (callbackDepth(l_eventFrames, state)) + return false; + } + + if (state->attached || !atomic_load_explicit( + &state->available, memory_order_acquire)) + { + LG_UNLOCK(state->stateLock); + return false; + } + + state->attached = true; + state->attachmentGeneration = + nextGeneration(state->attachmentGeneration); + state->events = events; + state->eventOpaque = eventOpaque; + generation = state->streamGeneration; + dispatch = generation && events->playbackStart && + beginEventNL(state, &target); + if (dispatch) + clock = makeClock(state, atomic_load_explicit( + &state->position, memory_order_relaxed)); + lgUsbRedir_setPlugged(state->redir, true); + LG_UNLOCK(state->stateLock); + + if (dispatch) + { + target.events->playbackStart( + target.opaque, generation, &l_format, &clock); + + LG_LOCK(state->stateLock); + if (eventCurrentNL(state, &target, generation)) + atomic_store_explicit(&state->deliveryGeneration, + generation, memory_order_seq_cst); + LG_UNLOCK(state->stateLock); + endEvent(state, &target); + } + + return true; +} + +static void usbDetach(void * opaque) +{ + LGA_USBState * state = opaque; + + for (;;) + { + LG_LOCK(state->stateLock); + if (!state->detaching) + break; + LG_UNLOCK(state->stateLock); + if (callbackDepth(l_eventFrames, state)) + return; + } + + state->detaching = true; + state->attached = false; + state->attachmentGeneration = + nextGeneration(state->attachmentGeneration); + const uint32_t attachmentGeneration = + state->attachmentGeneration; + atomic_store_explicit( + &state->deliveryGeneration, 0, memory_order_seq_cst); + lgUsbRedir_setPlugged(state->redir, false); + LG_UNLOCK(state->stateLock); + + waitEvents(state); + + LG_LOCK(state->stateLock); + if (!state->attached && + state->attachmentGeneration == attachmentGeneration) + { + state->events = NULL; + state->eventOpaque = NULL; + state->detaching = false; + } + LG_UNLOCK(state->stateLock); +} + +const LG_AudioOps LGA_USB = +{ + .name = "USB Audio", + .setStatusListener = usbSetStatusListener, + .attach = usbAttach, + .detach = usbDetach, + .recordData = NULL, + .clockFeedback = NULL, +}; + +LGA_USBState * lgaUsb_create(void) +{ + LGA_USBState * state = calloc(1, sizeof(*state)); + if (!state) + return NULL; + + 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->inFlight, 0); + atomic_init(&state->statusInFlight, 0); + atomic_init(&state->statusNextTicket, 0); + atomic_init(&state->statusServingTicket, 0); + + state->device = lgUsbAudio_create(&l_usbAudioEvents, state); + if (!state->device) + { + free(state); + return NULL; + } + + state->redir = lgUsbRedir_create( + lgUsbAudio_deviceOps(), state->device, usbSetAvailable, state); + if (!state->redir) + { + lgUsbAudio_destroy(state->device); + free(state); + return NULL; + } + + return state; +} + +void lgaUsb_destroy(LGA_USBState * state) +{ + if (!state) + return; + + usbSetStatusListener(state, NULL, NULL); + usbDetach(state); + lgUsbRedir_destroy(state->redir); + lgUsbAudio_destroy(state->device); + LG_LOCK_FREE(state->stateLock); + LG_LOCK_FREE(state->statusLock); + free(state); +} + +LG_USBRedir * lgaUsb_redir(LGA_USBState * state) +{ + return state ? state->redir : NULL; +} diff --git a/client/src/audio_usb.h b/client/src/audio_usb.h new file mode 100644 index 00000000..3ebd30b5 --- /dev/null +++ b/client/src/audio_usb.h @@ -0,0 +1,37 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_CLIENT_AUDIO_USB_ +#define _H_LG_CLIENT_AUDIO_USB_ + +#include "interface/audio.h" +#include "usbredir.h" + +typedef struct LGA_USBState LGA_USBState; + +LGA_USBState * lgaUsb_create(void); +/* Detach this provider and stop PureSpice before destroying its state. */ +void lgaUsb_destroy(LGA_USBState * state); + +LG_USBRedir * lgaUsb_redir(LGA_USBState * state); + +extern const LG_AudioOps LGA_USB; + +#endif diff --git a/client/src/usbredir.c b/client/src/usbredir.c index c1ab3b45..7a677a89 100644 --- a/client/src/usbredir.c +++ b/client/src/usbredir.c @@ -34,6 +34,8 @@ struct LG_USBRedir { const LG_USBRedirDeviceOps * deviceOps; void * deviceOpaque; + LG_USBRedirStatusFn status; + void * statusOpaque; PSUSBRedirChannel * channels[USB_REDIR_CHANNEL_COUNT]; PSUSBRedirChannel * channel; @@ -48,6 +50,14 @@ struct LG_USBRedir bool plugged; }; +static void setAvailable(LG_USBRedir * usbredir, bool available) +{ + const bool previous = atomic_exchange_explicit( + &usbredir->available, available, memory_order_acq_rel); + if (previous != available && usbredir->status) + usbredir->status(usbredir->statusOpaque, available); +} + static int readUSBRedir(void * opaque, uint8_t * data, int count) { LG_USBRedir * usbredir = opaque; @@ -105,7 +115,7 @@ static void helloUSBRedir(void * opaque, { (void)hello; LG_USBRedir * usbredir = opaque; - atomic_store_explicit(&usbredir->available, true, memory_order_release); + setAvailable(usbredir, true); } static void unplugDevice(LG_USBRedir * usbredir) @@ -119,7 +129,7 @@ static void unplugDevice(LG_USBRedir * usbredir) static void destroyParser(LG_USBRedir * usbredir) { - atomic_store_explicit(&usbredir->available, false, memory_order_release); + setAvailable(usbredir, false); unplugDevice(usbredir); if (!usbredir->parser) @@ -202,7 +212,8 @@ static bool createParser(LG_USBRedir * usbredir) } LG_USBRedir * lgUsbRedir_create( - const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque) + const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque, + LG_USBRedirStatusFn status, void * statusOpaque) { if (!deviceOps || !deviceOps->setup || !deviceOps->plug || !deviceOps->unplug) @@ -214,6 +225,8 @@ LG_USBRedir * lgUsbRedir_create( usbredir->deviceOps = deviceOps; usbredir->deviceOpaque = deviceOpaque; + usbredir->status = status; + usbredir->statusOpaque = statusOpaque; atomic_init(&usbredir->desiredPlugged, false); atomic_init(&usbredir->available, false); return usbredir; diff --git a/client/src/usbredir.h b/client/src/usbredir.h index b9f1ad4c..84d2e9a4 100644 --- a/client/src/usbredir.h +++ b/client/src/usbredir.h @@ -31,6 +31,9 @@ struct usbredirparser; typedef struct LG_USBRedir LG_USBRedir; +/* Availability transitions are delivered on the PureSpice process thread. */ +typedef void (*LG_USBRedirStatusFn)(void * opaque, bool available); + typedef struct LG_USBRedirDeviceOps { /* Install the device packet callbacks on a newly-created parser. */ @@ -45,7 +48,8 @@ typedef struct LG_USBRedirDeviceOps LG_USBRedirDeviceOps; LG_USBRedir * lgUsbRedir_create( - const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque); + const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque, + LG_USBRedirStatusFn status, void * statusOpaque); /* The PureSpice session must be stopped before destroying the bridge. */ void lgUsbRedir_destroy(LG_USBRedir * usbredir);