[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.
This commit is contained in:
Geoffrey McRae
2026-08-09 22:12:39 +10:00
parent 6979b93da5
commit 06f111dc90
5 changed files with 628 additions and 4 deletions

View File

@@ -216,6 +216,7 @@ endif()
if(ENABLE_USB_AUDIO)
list(APPEND SOURCES
src/audio_usb.c
src/usb_audio.c
src/usbredir.c
)

569
client/src/audio_usb.c Normal file
View File

@@ -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 <stdatomic.h>
#include <stdlib.h>
#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;
}

37
client/src/audio_usb.h Normal file
View File

@@ -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

View File

@@ -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;

View File

@@ -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);