[client] spice: move protocol into transport

This commit is contained in:
Geoffrey McRae
2026-08-11 15:33:53 +10:00
parent 2bd0db1a26
commit 5b12685e3c
28 changed files with 2398 additions and 1058 deletions

View File

@@ -0,0 +1,992 @@
/**
* 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",
.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);
}