mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-11-12 23:07:21 +00:00
Scope the legacy latency offset to classic SPICE and use the LGMP poll bound as its fixed arrival reserve. Keep learned jitter additive. Select provider clock feedback only when the current remote endpoint advertises it, avoiding a failed startup attempt on broker v1.
994 lines
26 KiB
C
994 lines
26 KiB
C
/**
|
|
* 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.h"
|
|
|
|
#include "common/debug.h"
|
|
#include "common/event.h"
|
|
#include "common/locking.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS 2000
|
|
|
|
typedef struct SpiceAudioStream
|
|
{
|
|
bool active;
|
|
uint32_t generation;
|
|
LG_AudioFormat format;
|
|
|
|
bool volumeValid;
|
|
uint8_t volumeChannels;
|
|
uint16_t volume[LG_AUDIO_MAX_CHANNELS];
|
|
bool muteValid;
|
|
bool mute;
|
|
}
|
|
SpiceAudioStream;
|
|
|
|
typedef struct SpiceAudioEventTarget
|
|
{
|
|
const LG_AudioEventOps * events;
|
|
void * opaque;
|
|
uint32_t generation;
|
|
}
|
|
SpiceAudioEventTarget;
|
|
|
|
typedef struct SpiceAudioCallbackWaiter
|
|
{
|
|
struct SpiceAudioCallbackWaiter * next;
|
|
LGEvent * event;
|
|
unsigned int depth;
|
|
}
|
|
SpiceAudioCallbackWaiter;
|
|
|
|
typedef struct SpiceAudioCallbackWaitQueue
|
|
{
|
|
LG_Lock lock;
|
|
atomic_uint count;
|
|
SpiceAudioCallbackWaiter * waiters;
|
|
}
|
|
SpiceAudioCallbackWaitQueue;
|
|
|
|
struct SpiceAudio
|
|
{
|
|
LG_RWLock lock;
|
|
|
|
bool available;
|
|
uint32_t statusGeneration;
|
|
LG_AudioStatusFn statusCallback;
|
|
void * statusOpaque;
|
|
atomic_uint statusInFlight;
|
|
SpiceAudioCallbackWaitQueue statusWait;
|
|
|
|
const LG_AudioEventOps * events;
|
|
void * eventOpaque;
|
|
uint32_t eventGeneration;
|
|
atomic_uint inFlight;
|
|
SpiceAudioCallbackWaitQueue eventWait;
|
|
|
|
SpiceAudioStream playback;
|
|
SpiceAudioStream record;
|
|
|
|
bool playbackClockValid;
|
|
uint32_t playbackMediaTime;
|
|
int64_t playbackTime;
|
|
uint64_t playbackPosition;
|
|
LG_AudioClock playbackClock;
|
|
};
|
|
|
|
static _Thread_local unsigned int l_eventDepth;
|
|
static _Thread_local unsigned int l_statusDepth;
|
|
|
|
static uint32_t nextGeneration(uint32_t generation)
|
|
{
|
|
if (++generation == 0)
|
|
++generation;
|
|
return generation;
|
|
}
|
|
|
|
static LGEvent * createWaitEvent(void)
|
|
{
|
|
LGEvent * event = lgCreateEvent(true, 0);
|
|
if (!event)
|
|
DEBUG_FATAL("Failed to create SPICE audio wait event");
|
|
return event;
|
|
}
|
|
|
|
static void waitEvent(LGEvent * event)
|
|
{
|
|
if (!lgWaitEvent(event, TIMEOUT_INFINITE))
|
|
DEBUG_FATAL("Failed to wait for SPICE audio event");
|
|
}
|
|
|
|
static void signalWaitEvent(LGEvent * event)
|
|
{
|
|
if (!lgSignalEvent(event))
|
|
DEBUG_FATAL("Failed to signal SPICE audio event");
|
|
}
|
|
|
|
static void signalCallbackWaiters(
|
|
SpiceAudioCallbackWaitQueue * queue, unsigned int remaining)
|
|
{
|
|
if (!atomic_load_explicit(&queue->count, memory_order_acquire))
|
|
return;
|
|
|
|
LG_LOCK(queue->lock);
|
|
SpiceAudioCallbackWaiter ** link = &queue->waiters;
|
|
while (*link)
|
|
{
|
|
SpiceAudioCallbackWaiter * waiter = *link;
|
|
if (remaining > waiter->depth)
|
|
{
|
|
link = &waiter->next;
|
|
continue;
|
|
}
|
|
|
|
*link = waiter->next;
|
|
atomic_fetch_sub_explicit(
|
|
&queue->count, 1, memory_order_release);
|
|
signalWaitEvent(waiter->event);
|
|
}
|
|
LG_UNLOCK(queue->lock);
|
|
}
|
|
|
|
static void endCallback(atomic_uint * inFlight,
|
|
SpiceAudioCallbackWaitQueue * waitQueue)
|
|
{
|
|
const unsigned int previous = atomic_fetch_sub_explicit(
|
|
inFlight, 1, memory_order_release);
|
|
DEBUG_ASSERT(previous > 0);
|
|
signalCallbackWaiters(waitQueue, previous - 1);
|
|
}
|
|
|
|
static void waitCallbacks(atomic_uint * inFlight,
|
|
SpiceAudioCallbackWaitQueue * waitQueue, unsigned int depth)
|
|
{
|
|
if (atomic_load_explicit(inFlight, memory_order_acquire) <= depth)
|
|
return;
|
|
|
|
SpiceAudioCallbackWaiter waiter =
|
|
{
|
|
.event = createWaitEvent(),
|
|
.depth = depth,
|
|
};
|
|
|
|
bool queued = false;
|
|
LG_LOCK(waitQueue->lock);
|
|
if (atomic_load_explicit(inFlight, memory_order_acquire) > depth)
|
|
{
|
|
waiter.next = waitQueue->waiters;
|
|
waitQueue->waiters = &waiter;
|
|
atomic_fetch_add_explicit(
|
|
&waitQueue->count, 1, memory_order_release);
|
|
queued = true;
|
|
}
|
|
LG_UNLOCK(waitQueue->lock);
|
|
|
|
if (queued)
|
|
{
|
|
waitEvent(waiter.event);
|
|
|
|
/* The signaler owns the waiter until it releases the queue lock. */
|
|
LG_LOCK(waitQueue->lock);
|
|
LG_UNLOCK(waitQueue->lock);
|
|
}
|
|
|
|
lgFreeEvent(waiter.event);
|
|
}
|
|
|
|
/* The instance lock must be held exclusively while admitting a callback so
|
|
* detach cannot invalidate the target between the snapshot and in-flight
|
|
* increment. */
|
|
static bool beginEventNL(SpiceAudio * audio,
|
|
SpiceAudioEventTarget * target)
|
|
{
|
|
if (!audio->events)
|
|
return false;
|
|
|
|
target->events = audio->events;
|
|
target->opaque = audio->eventOpaque;
|
|
target->generation = audio->eventGeneration;
|
|
atomic_fetch_add_explicit(&audio->inFlight, 1, memory_order_relaxed);
|
|
++l_eventDepth;
|
|
return true;
|
|
}
|
|
|
|
static bool playbackEventCurrent(SpiceAudio * audio,
|
|
const SpiceAudioEventTarget * target, uint32_t generation, bool active)
|
|
{
|
|
LG_LOCK_SHARED(audio->lock);
|
|
const bool result =
|
|
target->events == audio->events &&
|
|
target->opaque == audio->eventOpaque &&
|
|
target->generation == audio->eventGeneration &&
|
|
audio->playback.generation == generation &&
|
|
audio->playback.active == active;
|
|
LG_UNLOCK_SHARED(audio->lock);
|
|
return result;
|
|
}
|
|
|
|
static bool recordEventCurrent(SpiceAudio * audio,
|
|
const SpiceAudioEventTarget * target, uint32_t generation, bool active)
|
|
{
|
|
LG_LOCK_SHARED(audio->lock);
|
|
const bool result =
|
|
target->events == audio->events &&
|
|
target->opaque == audio->eventOpaque &&
|
|
target->generation == audio->eventGeneration &&
|
|
audio->record.generation == generation &&
|
|
audio->record.active == active;
|
|
LG_UNLOCK_SHARED(audio->lock);
|
|
return result;
|
|
}
|
|
|
|
static void endEvent(SpiceAudio * audio)
|
|
{
|
|
--l_eventDepth;
|
|
endCallback(&audio->inFlight, &audio->eventWait);
|
|
}
|
|
|
|
static bool sampleFormat(PSAudioFormat source,
|
|
LG_AudioSampleFormat * format)
|
|
{
|
|
switch (source)
|
|
{
|
|
case PS_AUDIO_FMT_S16:
|
|
*format = LG_AUDIO_FMT_S16_LE;
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void channelLayout(LG_AudioFormat * format)
|
|
{
|
|
memset(format->channels, 0, sizeof(format->channels));
|
|
|
|
switch (format->channelCount)
|
|
{
|
|
case 1:
|
|
format->channels[0] = LG_AUDIO_CH_MONO;
|
|
break;
|
|
|
|
case 2:
|
|
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
|
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
|
break;
|
|
|
|
case 3:
|
|
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
|
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
|
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
|
break;
|
|
|
|
case 4:
|
|
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
|
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
|
format->channels[2] = LG_AUDIO_CH_REAR_LEFT;
|
|
format->channels[3] = LG_AUDIO_CH_REAR_RIGHT;
|
|
break;
|
|
|
|
case 5:
|
|
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
|
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
|
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
|
format->channels[3] = LG_AUDIO_CH_REAR_LEFT;
|
|
format->channels[4] = LG_AUDIO_CH_REAR_RIGHT;
|
|
break;
|
|
|
|
case 6:
|
|
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
|
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
|
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
|
format->channels[3] = LG_AUDIO_CH_LFE;
|
|
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
|
|
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
|
|
break;
|
|
|
|
case 7:
|
|
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
|
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
|
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
|
format->channels[3] = LG_AUDIO_CH_LFE;
|
|
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
|
|
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
|
|
format->channels[6] = LG_AUDIO_CH_REAR_CENTER;
|
|
break;
|
|
|
|
case 8:
|
|
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
|
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
|
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
|
format->channels[3] = LG_AUDIO_CH_LFE;
|
|
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
|
|
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
|
|
format->channels[6] = LG_AUDIO_CH_SIDE_LEFT;
|
|
format->channels[7] = LG_AUDIO_CH_SIDE_RIGHT;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool makeFormat(int channels, int sampleRate, PSAudioFormat source,
|
|
LG_AudioFormat * format)
|
|
{
|
|
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || sampleRate < 1 ||
|
|
!sampleFormat(source, &format->sampleFormat))
|
|
return false;
|
|
|
|
format->sampleRate = sampleRate;
|
|
format->channelCount = channels;
|
|
channelLayout(format);
|
|
return true;
|
|
}
|
|
|
|
static size_t sampleSize(LG_AudioSampleFormat format)
|
|
{
|
|
switch (format)
|
|
{
|
|
case LG_AUDIO_FMT_U8: return 1;
|
|
case LG_AUDIO_FMT_S16_LE: return 2;
|
|
case LG_AUDIO_FMT_S24_LE: return 3;
|
|
case LG_AUDIO_FMT_S32_LE: return 4;
|
|
case LG_AUDIO_FMT_F32_LE: return 4;
|
|
case LG_AUDIO_FMT_F32_NE: return 4;
|
|
case LG_AUDIO_FMT_F64_LE: return 8;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static LG_AudioClock playbackClockNL(SpiceAudio * audio, uint32_t time)
|
|
{
|
|
bool discontinuity = false;
|
|
|
|
if (!audio->playbackClockValid)
|
|
{
|
|
audio->playbackClockValid = true;
|
|
audio->playbackMediaTime = time;
|
|
audio->playbackTime = 0;
|
|
}
|
|
else
|
|
{
|
|
const int32_t delta = (int32_t)(time - audio->playbackMediaTime);
|
|
audio->playbackMediaTime = time;
|
|
if (delta < 0 || delta > SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS)
|
|
{
|
|
audio->playbackTime = 0;
|
|
discontinuity = true;
|
|
}
|
|
else
|
|
audio->playbackTime += (int64_t)delta * 1000000;
|
|
}
|
|
|
|
audio->playbackClock = (LG_AudioClock)
|
|
{
|
|
.position = audio->playbackPosition,
|
|
.time = audio->playbackTime,
|
|
.rate = 0.0,
|
|
.stable = !discontinuity,
|
|
.discontinuity = discontinuity,
|
|
};
|
|
return audio->playbackClock;
|
|
}
|
|
|
|
static void spiceSetStatusListener(void * opaque,
|
|
LG_AudioStatusFn callback, void * callbackOpaque)
|
|
{
|
|
SpiceAudio * audio = opaque;
|
|
LG_AudioStatus status;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
audio->statusCallback = callback;
|
|
audio->statusOpaque = callbackOpaque;
|
|
status = (LG_AudioStatus)
|
|
{
|
|
.available = audio->available,
|
|
.generation = audio->statusGeneration,
|
|
};
|
|
if (callback)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&audio->statusInFlight, 1, memory_order_relaxed);
|
|
++l_statusDepth;
|
|
}
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (callback)
|
|
{
|
|
callback(callbackOpaque, &status);
|
|
--l_statusDepth;
|
|
endCallback(&audio->statusInFlight, &audio->statusWait);
|
|
}
|
|
else
|
|
waitCallbacks(&audio->statusInFlight,
|
|
&audio->statusWait, l_statusDepth);
|
|
}
|
|
|
|
static bool spiceAttach(void * opaque, const LG_AudioEventOps * events,
|
|
void * eventOpaque)
|
|
{
|
|
SpiceAudio * audio = opaque;
|
|
if (!events)
|
|
return false;
|
|
|
|
SpiceAudioEventTarget target;
|
|
SpiceAudioStream playback;
|
|
SpiceAudioStream record;
|
|
LG_AudioClock playbackClock;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return false;
|
|
}
|
|
|
|
audio->events = events;
|
|
audio->eventOpaque = eventOpaque;
|
|
audio->eventGeneration =
|
|
nextGeneration(audio->eventGeneration);
|
|
playback = audio->playback;
|
|
record = audio->record;
|
|
playbackClock = audio->playbackClock;
|
|
dispatch = beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return true;
|
|
|
|
if (playback.active)
|
|
{
|
|
if (playbackEventCurrent(audio, &target, playback.generation, true) &&
|
|
target.events->playbackStart)
|
|
{
|
|
target.events->playbackStart(target.opaque,
|
|
playback.generation, &playback.format, &playbackClock);
|
|
if (playback.volumeValid &&
|
|
playbackEventCurrent(audio, &target, playback.generation, true) &&
|
|
target.events->playbackVolume)
|
|
target.events->playbackVolume(target.opaque,
|
|
playback.generation,
|
|
playback.volumeChannels, playback.volume);
|
|
if (playback.muteValid &&
|
|
playbackEventCurrent(audio, &target, playback.generation, true) &&
|
|
target.events->playbackMute)
|
|
target.events->playbackMute(target.opaque,
|
|
playback.generation, playback.mute);
|
|
}
|
|
}
|
|
|
|
if (record.active)
|
|
{
|
|
if (recordEventCurrent(audio, &target, record.generation, true) &&
|
|
target.events->recordStart)
|
|
{
|
|
target.events->recordStart(target.opaque,
|
|
record.generation, &record.format);
|
|
if (record.volumeValid &&
|
|
recordEventCurrent(audio, &target, record.generation, true) &&
|
|
target.events->recordVolume)
|
|
target.events->recordVolume(target.opaque, record.generation,
|
|
record.volumeChannels, record.volume);
|
|
if (record.muteValid &&
|
|
recordEventCurrent(audio, &target, record.generation, true) &&
|
|
target.events->recordMute)
|
|
target.events->recordMute(target.opaque,
|
|
record.generation, record.mute);
|
|
}
|
|
}
|
|
|
|
endEvent(audio);
|
|
return true;
|
|
}
|
|
|
|
static void spiceDetach(void * opaque)
|
|
{
|
|
SpiceAudio * audio = opaque;
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
audio->events = NULL;
|
|
audio->eventOpaque = NULL;
|
|
audio->eventGeneration =
|
|
nextGeneration(audio->eventGeneration);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
waitCallbacks(&audio->inFlight, &audio->eventWait, l_eventDepth);
|
|
}
|
|
|
|
static bool spiceRecordData(void * opaque, uint32_t generation,
|
|
const void * data, size_t frames, const LG_AudioClock * sourceClock)
|
|
{
|
|
SpiceAudio * audio = opaque;
|
|
size_t size = 0;
|
|
bool valid = false;
|
|
|
|
LG_LOCK_SHARED(audio->lock);
|
|
if (audio->available && audio->events && audio->record.active &&
|
|
generation == audio->record.generation)
|
|
{
|
|
const size_t bytesPerSample =
|
|
sampleSize(audio->record.format.sampleFormat);
|
|
const size_t channels = audio->record.format.channelCount;
|
|
if (bytesPerSample && frames <= SIZE_MAX / bytesPerSample / channels &&
|
|
(frames == 0 || data))
|
|
{
|
|
size = frames * bytesPerSample * channels;
|
|
valid = true;
|
|
}
|
|
}
|
|
LG_UNLOCK_SHARED(audio->lock);
|
|
|
|
return valid && purespice_writeAudio((void *)data, size, 0);
|
|
}
|
|
|
|
static const LG_AudioOps l_spiceAudioOps =
|
|
{
|
|
.name = "SPICE",
|
|
.playbackFlags = LG_AUDIO_PLAYBACK_LEGACY_LATENCY_OFFSET,
|
|
.setStatusListener = spiceSetStatusListener,
|
|
.attach = spiceAttach,
|
|
.detach = spiceDetach,
|
|
.recordData = spiceRecordData,
|
|
.clockFeedback = NULL,
|
|
};
|
|
|
|
bool spiceAudio_init(SpiceAudio ** result)
|
|
{
|
|
if (!result)
|
|
return false;
|
|
|
|
SpiceAudio * audio = calloc(1, sizeof(*audio));
|
|
if (!audio)
|
|
return false;
|
|
|
|
LG_RWLOCK_INIT(audio->lock);
|
|
atomic_init(&audio->statusInFlight, 0);
|
|
LG_LOCK_INIT(audio->statusWait.lock);
|
|
atomic_init(&audio->statusWait.count, 0);
|
|
atomic_init(&audio->inFlight, 0);
|
|
LG_LOCK_INIT(audio->eventWait.lock);
|
|
atomic_init(&audio->eventWait.count, 0);
|
|
|
|
*result = audio;
|
|
return true;
|
|
}
|
|
|
|
void spiceAudio_free(SpiceAudio ** audio)
|
|
{
|
|
if (!audio || !*audio)
|
|
return;
|
|
|
|
SpiceAudio * instance = *audio;
|
|
spiceAudio_setAvailable(instance, false);
|
|
spiceSetStatusListener(instance, NULL, NULL);
|
|
spiceDetach(instance);
|
|
|
|
LG_LOCK_FREE(instance->statusWait.lock);
|
|
LG_LOCK_FREE(instance->eventWait.lock);
|
|
LG_RWLOCK_FREE(instance->lock);
|
|
free(instance);
|
|
*audio = NULL;
|
|
}
|
|
|
|
const LG_AudioOps * spiceAudio_getOps(void)
|
|
{
|
|
return &l_spiceAudioOps;
|
|
}
|
|
|
|
void spiceAudio_setAvailable(SpiceAudio * audio, bool available)
|
|
{
|
|
LG_AudioStatusFn callback;
|
|
void * callbackOpaque;
|
|
LG_AudioStatus status;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (audio->available == available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
audio->available = available;
|
|
audio->statusGeneration =
|
|
nextGeneration(audio->statusGeneration);
|
|
if (!available)
|
|
{
|
|
audio->playback.active = false;
|
|
audio->record.active = false;
|
|
|
|
audio->playbackClockValid = false;
|
|
audio->playbackPosition = 0;
|
|
audio->playbackClock = (LG_AudioClock) { 0 };
|
|
|
|
audio->playback.volumeValid = false;
|
|
audio->playback.muteValid = false;
|
|
|
|
audio->record.volumeValid = false;
|
|
audio->record.muteValid = false;
|
|
}
|
|
|
|
callback = audio->statusCallback;
|
|
callbackOpaque = audio->statusOpaque;
|
|
status = (LG_AudioStatus)
|
|
{
|
|
.available = available,
|
|
.generation = audio->statusGeneration,
|
|
};
|
|
if (callback)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&audio->statusInFlight, 1, memory_order_relaxed);
|
|
++l_statusDepth;
|
|
}
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (callback)
|
|
{
|
|
callback(callbackOpaque, &status);
|
|
--l_statusDepth;
|
|
endCallback(&audio->statusInFlight, &audio->statusWait);
|
|
}
|
|
}
|
|
|
|
void spiceAudio_playbackStart(SpiceAudio * audio,
|
|
int channels, int sampleRate, PSAudioFormat sourceFormat, uint32_t time)
|
|
{
|
|
LG_AudioFormat format;
|
|
if (!makeFormat(channels, sampleRate, sourceFormat, &format))
|
|
{
|
|
DEBUG_ERROR("Invalid SPICE playback format: %d channels, %d Hz, %d",
|
|
channels, sampleRate, sourceFormat);
|
|
return;
|
|
}
|
|
|
|
SpiceAudioEventTarget target;
|
|
SpiceAudioStream playback;
|
|
LG_AudioClock clock;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
audio->playback.active = true;
|
|
audio->playback.generation =
|
|
nextGeneration(audio->playback.generation);
|
|
audio->playback.format = format;
|
|
audio->playbackClockValid = false;
|
|
audio->playbackPosition = 0;
|
|
clock = playbackClockNL(audio, time);
|
|
clock.stable = false;
|
|
clock.discontinuity = true;
|
|
audio->playbackClock = clock;
|
|
playback = audio->playback;
|
|
dispatch = beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (playbackEventCurrent(audio, &target, playback.generation, true) &&
|
|
target.events->playbackStart)
|
|
{
|
|
target.events->playbackStart(target.opaque,
|
|
playback.generation, &playback.format, &clock);
|
|
if (playback.volumeValid &&
|
|
playbackEventCurrent(audio, &target, playback.generation, true) &&
|
|
target.events->playbackVolume)
|
|
target.events->playbackVolume(target.opaque,
|
|
playback.generation, playback.volumeChannels, playback.volume);
|
|
if (playback.muteValid &&
|
|
playbackEventCurrent(audio, &target, playback.generation, true) &&
|
|
target.events->playbackMute)
|
|
target.events->playbackMute(target.opaque,
|
|
playback.generation, playback.mute);
|
|
}
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_playbackStop(SpiceAudio * audio)
|
|
{
|
|
SpiceAudioEventTarget target;
|
|
uint32_t generation;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->playback.active)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
generation = audio->playback.generation;
|
|
audio->playback.active = false;
|
|
audio->playbackClockValid = false;
|
|
dispatch = beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (target.events->playbackStop &&
|
|
playbackEventCurrent(audio, &target, generation, false))
|
|
target.events->playbackStop(target.opaque, generation);
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_playbackVolume(SpiceAudio * audio,
|
|
int channels, const uint16_t volume[])
|
|
{
|
|
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume)
|
|
return;
|
|
|
|
SpiceAudioEventTarget target;
|
|
uint32_t generation;
|
|
uint16_t snapshot[LG_AUDIO_MAX_CHANNELS];
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
memcpy(audio->playback.volume, volume,
|
|
(size_t)channels * sizeof(*volume));
|
|
audio->playback.volumeChannels = channels;
|
|
audio->playback.volumeValid = true;
|
|
generation = audio->playback.generation;
|
|
memcpy(snapshot, volume, (size_t)channels * sizeof(*volume));
|
|
dispatch = audio->playback.active && beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (target.events->playbackVolume &&
|
|
playbackEventCurrent(audio, &target, generation, true))
|
|
target.events->playbackVolume(
|
|
target.opaque, generation, channels, snapshot);
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_playbackMute(SpiceAudio * audio, bool mute)
|
|
{
|
|
SpiceAudioEventTarget target;
|
|
uint32_t generation;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
audio->playback.mute = mute;
|
|
audio->playback.muteValid = true;
|
|
generation = audio->playback.generation;
|
|
dispatch = audio->playback.active && beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (target.events->playbackMute &&
|
|
playbackEventCurrent(audio, &target, generation, true))
|
|
target.events->playbackMute(target.opaque, generation, mute);
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_playbackData(SpiceAudio * audio,
|
|
uint8_t * data, size_t size, uint32_t time)
|
|
{
|
|
SpiceAudioEventTarget target;
|
|
uint32_t generation;
|
|
size_t frames;
|
|
LG_AudioClock clock;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available || !audio->playback.active)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
const size_t bytesPerSample =
|
|
sampleSize(audio->playback.format.sampleFormat);
|
|
const size_t stride =
|
|
bytesPerSample * audio->playback.format.channelCount;
|
|
if (!stride || !size || !data || size % stride)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
DEBUG_ERROR("Invalid SPICE playback packet size: %zu", size);
|
|
return;
|
|
}
|
|
|
|
frames = size / stride;
|
|
generation = audio->playback.generation;
|
|
clock = playbackClockNL(audio, time);
|
|
audio->playbackPosition += frames;
|
|
dispatch = beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (target.events->playbackData &&
|
|
playbackEventCurrent(audio, &target, generation, true))
|
|
target.events->playbackData(target.opaque, generation,
|
|
data, frames, &clock);
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_recordStart(SpiceAudio * audio,
|
|
int channels, int sampleRate, PSAudioFormat sourceFormat)
|
|
{
|
|
LG_AudioFormat format;
|
|
if (!makeFormat(channels, sampleRate, sourceFormat, &format))
|
|
{
|
|
DEBUG_ERROR("Invalid SPICE record format: %d channels, %d Hz, %d",
|
|
channels, sampleRate, sourceFormat);
|
|
return;
|
|
}
|
|
|
|
SpiceAudioEventTarget target;
|
|
SpiceAudioStream record;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
audio->record.active = true;
|
|
audio->record.generation = nextGeneration(audio->record.generation);
|
|
audio->record.format = format;
|
|
record = audio->record;
|
|
dispatch = beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (recordEventCurrent(audio, &target, record.generation, true) &&
|
|
target.events->recordStart)
|
|
{
|
|
target.events->recordStart(
|
|
target.opaque, record.generation, &record.format);
|
|
if (record.volumeValid &&
|
|
recordEventCurrent(audio, &target, record.generation, true) &&
|
|
target.events->recordVolume)
|
|
target.events->recordVolume(target.opaque, record.generation,
|
|
record.volumeChannels, record.volume);
|
|
if (record.muteValid &&
|
|
recordEventCurrent(audio, &target, record.generation, true) &&
|
|
target.events->recordMute)
|
|
target.events->recordMute(
|
|
target.opaque, record.generation, record.mute);
|
|
}
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_recordStop(SpiceAudio * audio)
|
|
{
|
|
SpiceAudioEventTarget target;
|
|
uint32_t generation;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->record.active)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
generation = audio->record.generation;
|
|
audio->record.active = false;
|
|
dispatch = beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (target.events->recordStop &&
|
|
recordEventCurrent(audio, &target, generation, false))
|
|
target.events->recordStop(target.opaque, generation);
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_recordVolume(SpiceAudio * audio,
|
|
int channels, const uint16_t volume[])
|
|
{
|
|
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume)
|
|
return;
|
|
|
|
SpiceAudioEventTarget target;
|
|
uint32_t generation;
|
|
uint16_t snapshot[LG_AUDIO_MAX_CHANNELS];
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
memcpy(audio->record.volume, volume,
|
|
(size_t)channels * sizeof(*volume));
|
|
audio->record.volumeChannels = channels;
|
|
audio->record.volumeValid = true;
|
|
generation = audio->record.generation;
|
|
memcpy(snapshot, volume, (size_t)channels * sizeof(*volume));
|
|
dispatch = audio->record.active && beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (target.events->recordVolume &&
|
|
recordEventCurrent(audio, &target, generation, true))
|
|
target.events->recordVolume(
|
|
target.opaque, generation, channels, snapshot);
|
|
endEvent(audio);
|
|
}
|
|
|
|
void spiceAudio_recordMute(SpiceAudio * audio, bool mute)
|
|
{
|
|
SpiceAudioEventTarget target;
|
|
uint32_t generation;
|
|
bool dispatch;
|
|
|
|
LG_LOCK_EXCLUSIVE(audio->lock);
|
|
if (!audio->available)
|
|
{
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
return;
|
|
}
|
|
|
|
audio->record.mute = mute;
|
|
audio->record.muteValid = true;
|
|
generation = audio->record.generation;
|
|
dispatch = audio->record.active && beginEventNL(audio, &target);
|
|
LG_UNLOCK_EXCLUSIVE(audio->lock);
|
|
|
|
if (!dispatch)
|
|
return;
|
|
|
|
if (target.events->recordMute &&
|
|
recordEventCurrent(audio, &target, generation, true))
|
|
target.events->recordMute(target.opaque, generation, mute);
|
|
endEvent(audio);
|
|
}
|