mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-11-12 14:57:20 +00:00
2353 lines
70 KiB
C
2353 lines
70 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 <LGProtocol/KVMFR.h>
|
|
#include "common/debug.h"
|
|
#include "common/locking.h"
|
|
#include "common/thread.h"
|
|
#include "common/time.h"
|
|
|
|
#include <lgmp/stream.h>
|
|
|
|
#include <errno.h>
|
|
#include <math.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/random.h>
|
|
|
|
#define AUDIO_IDLE_POLL_MS 20U
|
|
#define AUDIO_ACTIVE_POLL_US 125U
|
|
#define AUDIO_CLAIM_RETRY_US 200000U
|
|
#define AUDIO_OWNER_RETRY_US 1200000U
|
|
#define AUDIO_DRAIN_MAX 32U
|
|
|
|
#define AUDIO_CAPABILITIES \
|
|
(KVMFR_AUDIO_CAP_PLAYBACK | KVMFR_AUDIO_CAP_CAPTURE | \
|
|
KVMFR_AUDIO_CAP_CLOCK_FEEDBACK)
|
|
|
|
#define AUDIO_STATUS_FLAGS \
|
|
(KVMFR_AUDIO_STATUS_AVAILABLE | KVMFR_AUDIO_STATUS_HAS_OWNER | \
|
|
KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE | \
|
|
KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE | \
|
|
KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE | \
|
|
KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE)
|
|
|
|
#define AUDIO_CLOCK_FLAGS \
|
|
(KVMFR_AUDIO_MESSAGE_CLOCK_VALID | \
|
|
KVMFR_AUDIO_MESSAGE_CLOCK_STABLE | \
|
|
KVMFR_AUDIO_MESSAGE_DISCONTINUITY)
|
|
|
|
#define AUDIO_DATA_FLAGS \
|
|
(AUDIO_CLOCK_FLAGS | KVMFR_AUDIO_MESSAGE_SILENT)
|
|
|
|
enum AudioPhase
|
|
{
|
|
AUDIO_PHASE_IDLE,
|
|
AUDIO_PHASE_CLAIM_PENDING,
|
|
AUDIO_PHASE_CLAIM_SENT,
|
|
AUDIO_PHASE_BOUND,
|
|
AUDIO_PHASE_RELEASING,
|
|
AUDIO_PHASE_COOLDOWN,
|
|
};
|
|
|
|
struct AudioGeneration
|
|
{
|
|
uint64_t wire;
|
|
uint32_t local;
|
|
};
|
|
|
|
struct AudioStreamState
|
|
{
|
|
struct AudioGeneration generation;
|
|
KVMFRAudioFormat wireFormat;
|
|
LG_AudioFormat format;
|
|
bool active;
|
|
bool formatValid;
|
|
bool volumeValid;
|
|
bool muteValid;
|
|
bool mute;
|
|
bool clockValid;
|
|
LG_AudioClock clock;
|
|
uint8_t volumeChannels;
|
|
uint16_t volume[KVMFR_AUDIO_MAX_CHANNELS];
|
|
};
|
|
|
|
struct AudioStatusCallbackFrame
|
|
{
|
|
const LGMPAudio * audio;
|
|
struct AudioStatusCallbackFrame * previous;
|
|
};
|
|
|
|
struct AudioCallbackWaiter
|
|
{
|
|
struct AudioCallbackWaiter * next;
|
|
LGEvent * event;
|
|
unsigned depth;
|
|
};
|
|
|
|
struct AudioCallbackWaitQueue
|
|
{
|
|
LG_Lock lock;
|
|
atomic_uint count;
|
|
struct AudioCallbackWaiter * waiters;
|
|
};
|
|
|
|
struct LGMPAudio
|
|
{
|
|
PLGMPClient client;
|
|
LGEvent * pointerWake;
|
|
LGEvent * event;
|
|
LGThread * thread;
|
|
LG_Lock lock;
|
|
LG_Lock eventLock;
|
|
atomic_flag writeGate;
|
|
atomic_bool stop;
|
|
atomic_bool dataPlaneLost;
|
|
atomic_bool captureDiscontinuity;
|
|
atomic_bool clockFeedbackAvailable;
|
|
atomic_uint statusInFlight;
|
|
struct AudioCallbackWaitQueue statusWait;
|
|
|
|
bool connected;
|
|
bool discover;
|
|
bool attached;
|
|
bool statusAvailable;
|
|
bool statusDirty;
|
|
bool responsePending;
|
|
bool claimPosting;
|
|
bool sequenceKnown;
|
|
bool playbackDiscontinuity;
|
|
bool releaseRequested;
|
|
bool releaseSent;
|
|
bool hostReadActive;
|
|
bool clientWriteActive;
|
|
enum AudioPhase phase;
|
|
uint32_t clientID;
|
|
uint32_t requestSerial;
|
|
uint32_t requestGeneration;
|
|
unsigned claimSends;
|
|
uint64_t claimToken;
|
|
uint64_t nextClaimAt;
|
|
uint64_t releaseDeadline;
|
|
uint64_t nextKeepalive;
|
|
uint64_t nextAttachAt;
|
|
uint64_t expectedSequence;
|
|
uint32_t generationSerial;
|
|
struct AudioGeneration endpointGeneration;
|
|
KVMFRAudioClaim claim;
|
|
KVMFRAudioStreamResponse response;
|
|
KVMFRAudioStatus remoteStatus;
|
|
struct AudioStreamState playback;
|
|
struct AudioStreamState capture;
|
|
|
|
PLGMPClientStream hostToClientStream;
|
|
PLGMPClientStream clientToHostStream;
|
|
LGMPStreamPollState streamPoll;
|
|
|
|
/* Protected by writeGate. */
|
|
uint64_t clientSequence;
|
|
bool writeBound;
|
|
bool publicDataAllowed;
|
|
uint64_t writeCaptureGeneration;
|
|
uint32_t writeCaptureLocalGeneration;
|
|
KVMFRAudioFormat writeCaptureFormat;
|
|
uint64_t writePlaybackGeneration;
|
|
uint32_t writePlaybackLocalGeneration;
|
|
|
|
const LG_AudioEventOps * events;
|
|
void * eventOpaque;
|
|
LG_AudioStatusFn statusCallback;
|
|
void * statusOpaque;
|
|
};
|
|
|
|
static _Thread_local struct AudioStatusCallbackFrame * l_statusFrames;
|
|
|
|
_Static_assert(sizeof(struct LGMPStreamDescriptor) ==
|
|
sizeof(KVMFRStreamDescriptor),
|
|
"LGMP and KVMFR stream descriptor sizes differ");
|
|
_Static_assert(offsetof(struct LGMPStreamDescriptor, offset) ==
|
|
offsetof(KVMFRStreamDescriptor, offset),
|
|
"LGMP and KVMFR stream descriptor offsets differ");
|
|
_Static_assert(offsetof(struct LGMPStreamDescriptor, slotSize) ==
|
|
offsetof(KVMFRStreamDescriptor, slotSize),
|
|
"LGMP and KVMFR stream descriptor geometry differs");
|
|
_Static_assert(KVMFR_AUDIO_MAX_CHANNELS <= LG_AUDIO_MAX_CHANNELS,
|
|
"client audio cannot represent every KVMFR speaker position");
|
|
|
|
static uint32_t nextNonzero(uint32_t * value)
|
|
{
|
|
if (++*value == 0)
|
|
++*value;
|
|
return *value;
|
|
}
|
|
|
|
static bool randomBytes(void * buffer, size_t size)
|
|
{
|
|
uint8_t * output = buffer;
|
|
while (size)
|
|
{
|
|
const ssize_t received = getrandom(output, size, 0);
|
|
if (received < 0 && errno == EINTR)
|
|
continue;
|
|
if (received <= 0)
|
|
return false;
|
|
output += (size_t)received;
|
|
size -= (size_t)received;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool tryWriteGate(LGMPAudio * audio)
|
|
{
|
|
return !atomic_flag_test_and_set_explicit(
|
|
&audio->writeGate, memory_order_acquire);
|
|
}
|
|
|
|
static void lockWriteGate(LGMPAudio * audio)
|
|
{
|
|
while (!tryWriteGate(audio))
|
|
{
|
|
}
|
|
}
|
|
|
|
static void unlockWriteGate(LGMPAudio * audio)
|
|
{
|
|
atomic_flag_clear_explicit(&audio->writeGate, memory_order_release);
|
|
}
|
|
|
|
/* writeGate must be held. */
|
|
static void closePublicDataLocked(LGMPAudio * audio)
|
|
{
|
|
audio->publicDataAllowed = false;
|
|
audio->writeCaptureGeneration = 0;
|
|
audio->writeCaptureLocalGeneration = 0;
|
|
audio->writePlaybackGeneration = 0;
|
|
audio->writePlaybackLocalGeneration = 0;
|
|
memset(&audio->writeCaptureFormat, 0,
|
|
sizeof(audio->writeCaptureFormat));
|
|
}
|
|
|
|
static void signalWorker(LGMPAudio * audio)
|
|
{
|
|
lgSignalEvent(audio->event);
|
|
}
|
|
|
|
static unsigned statusCallbackDepth(const LGMPAudio * audio)
|
|
{
|
|
unsigned depth = 0;
|
|
for (const struct AudioStatusCallbackFrame * frame = l_statusFrames;
|
|
frame; frame = frame->previous)
|
|
if (frame->audio == audio)
|
|
++depth;
|
|
return depth;
|
|
}
|
|
|
|
static void signalCallbackWaiters(
|
|
struct AudioCallbackWaitQueue * queue, unsigned remaining)
|
|
{
|
|
if (!atomic_load_explicit(&queue->count, memory_order_acquire))
|
|
return;
|
|
|
|
LG_LOCK(queue->lock);
|
|
struct AudioCallbackWaiter ** link = &queue->waiters;
|
|
while (*link)
|
|
{
|
|
struct AudioCallbackWaiter * waiter = *link;
|
|
if (remaining > waiter->depth)
|
|
{
|
|
link = &waiter->next;
|
|
continue;
|
|
}
|
|
|
|
*link = waiter->next;
|
|
atomic_fetch_sub_explicit(
|
|
&queue->count, 1, memory_order_release);
|
|
lgSignalEvent(waiter->event);
|
|
}
|
|
LG_UNLOCK(queue->lock);
|
|
}
|
|
|
|
/* audio->lock must protect callback admission. */
|
|
static void beginStatusCallback(LGMPAudio * audio,
|
|
struct AudioStatusCallbackFrame * frame)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&audio->statusInFlight, 1, memory_order_relaxed);
|
|
frame->audio = audio;
|
|
frame->previous = l_statusFrames;
|
|
l_statusFrames = frame;
|
|
}
|
|
|
|
static void endStatusCallback(LGMPAudio * audio,
|
|
const struct AudioStatusCallbackFrame * frame)
|
|
{
|
|
l_statusFrames = frame->previous;
|
|
const unsigned previous = atomic_fetch_sub_explicit(
|
|
&audio->statusInFlight, 1, memory_order_release);
|
|
DEBUG_ASSERT(previous > 0);
|
|
signalCallbackWaiters(&audio->statusWait, previous - 1);
|
|
}
|
|
|
|
static void waitStatusCallbacks(LGMPAudio * audio)
|
|
{
|
|
const unsigned depth = statusCallbackDepth(audio);
|
|
if (atomic_load_explicit(
|
|
&audio->statusInFlight, memory_order_acquire) <= depth)
|
|
return;
|
|
|
|
struct AudioCallbackWaiter waiter =
|
|
{
|
|
.event = lgCreateEvent(true, 0),
|
|
.depth = depth,
|
|
};
|
|
if (!waiter.event)
|
|
DEBUG_FATAL("Failed to create LGMP audio callback wait event");
|
|
|
|
bool queued = false;
|
|
LG_LOCK(audio->statusWait.lock);
|
|
atomic_fetch_add_explicit(
|
|
&audio->statusWait.count, 1, memory_order_release);
|
|
if (atomic_load_explicit(
|
|
&audio->statusInFlight, memory_order_acquire) > depth)
|
|
{
|
|
waiter.next = audio->statusWait.waiters;
|
|
audio->statusWait.waiters = &waiter;
|
|
queued = true;
|
|
}
|
|
else
|
|
atomic_fetch_sub_explicit(
|
|
&audio->statusWait.count, 1, memory_order_release);
|
|
LG_UNLOCK(audio->statusWait.lock);
|
|
|
|
if (queued)
|
|
{
|
|
if (!lgWaitEvent(waiter.event, TIMEOUT_INFINITE))
|
|
DEBUG_FATAL("Failed to wait for LGMP audio status callbacks");
|
|
LG_LOCK(audio->statusWait.lock);
|
|
LG_UNLOCK(audio->statusWait.lock);
|
|
}
|
|
lgFreeEvent(waiter.event);
|
|
}
|
|
|
|
static unsigned channelCount(uint32_t mask)
|
|
{
|
|
unsigned count = 0;
|
|
while (mask)
|
|
{
|
|
count += mask & 1U;
|
|
mask >>= 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static LG_AudioChannel channelFromBit(unsigned bit)
|
|
{
|
|
static const LG_AudioChannel channels[KVMFR_AUDIO_MAX_CHANNELS] =
|
|
{
|
|
LG_AUDIO_CH_FRONT_LEFT,
|
|
LG_AUDIO_CH_FRONT_RIGHT,
|
|
LG_AUDIO_CH_FRONT_CENTER,
|
|
LG_AUDIO_CH_LFE,
|
|
LG_AUDIO_CH_REAR_LEFT,
|
|
LG_AUDIO_CH_REAR_RIGHT,
|
|
LG_AUDIO_CH_FRONT_LEFT_CENTER,
|
|
LG_AUDIO_CH_FRONT_RIGHT_CENTER,
|
|
LG_AUDIO_CH_REAR_CENTER,
|
|
LG_AUDIO_CH_SIDE_LEFT,
|
|
LG_AUDIO_CH_SIDE_RIGHT,
|
|
LG_AUDIO_CH_TOP_CENTER,
|
|
LG_AUDIO_CH_TOP_FRONT_LEFT,
|
|
LG_AUDIO_CH_TOP_FRONT_CENTER,
|
|
LG_AUDIO_CH_TOP_FRONT_RIGHT,
|
|
LG_AUDIO_CH_TOP_REAR_LEFT,
|
|
LG_AUDIO_CH_TOP_REAR_CENTER,
|
|
LG_AUDIO_CH_TOP_REAR_RIGHT,
|
|
};
|
|
return bit < KVMFR_AUDIO_MAX_CHANNELS ?
|
|
channels[bit] : LG_AUDIO_CH_UNKNOWN;
|
|
}
|
|
|
|
static bool validFormat(const KVMFRAudioFormat * wire,
|
|
LG_AudioFormat * result)
|
|
{
|
|
if (!wire || wire->version != KVMFR_AUDIO_FORMAT_VERSION ||
|
|
wire->size != sizeof(*wire) || !wire->sampleRate ||
|
|
wire->sampleRate > KVMFR_AUDIO_MAX_SAMPLE_RATE ||
|
|
!wire->channelMask ||
|
|
(wire->channelMask & ~KVMFR_AUDIO_CHANNEL_POSITION_MASK) ||
|
|
!wire->channelCount ||
|
|
wire->channelCount > KVMFR_AUDIO_STREAM_MAX_CHANNELS ||
|
|
channelCount(wire->channelMask) != wire->channelCount ||
|
|
wire->reserved[0] || wire->reserved[1])
|
|
return false;
|
|
|
|
LG_AudioSampleFormat format;
|
|
unsigned bytesPerSample;
|
|
switch (wire->sampleFormat)
|
|
{
|
|
case KVMFR_AUDIO_SAMPLE_S16_LE:
|
|
format = LG_AUDIO_FMT_S16_LE;
|
|
bytesPerSample = 2;
|
|
if (wire->validBits != 16 || wire->containerBits != 16)
|
|
return false;
|
|
break;
|
|
|
|
case KVMFR_AUDIO_SAMPLE_S24_LE:
|
|
format = LG_AUDIO_FMT_S24_LE;
|
|
bytesPerSample = 3;
|
|
if (wire->validBits != 24 || wire->containerBits != 24)
|
|
return false;
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (wire->blockAlign != wire->channelCount * bytesPerSample)
|
|
return false;
|
|
|
|
*result = (LG_AudioFormat)
|
|
{
|
|
.sampleFormat = format,
|
|
.sampleRate = wire->sampleRate,
|
|
.channelCount = (uint8_t)wire->channelCount,
|
|
};
|
|
unsigned channel = 0;
|
|
for (unsigned bit = 0; bit < KVMFR_AUDIO_MAX_CHANNELS; ++bit)
|
|
if (wire->channelMask & (UINT32_C(1) << bit))
|
|
result->channels[channel++] = channelFromBit(bit);
|
|
return true;
|
|
}
|
|
|
|
static bool sameFormat(const KVMFRAudioFormat * left,
|
|
const KVMFRAudioFormat * right)
|
|
{
|
|
return !memcmp(left, right, sizeof(*left));
|
|
}
|
|
|
|
static bool emptyFormat(const KVMFRAudioFormat * format)
|
|
{
|
|
const KVMFRAudioFormat empty = { 0 };
|
|
return !memcmp(format, &empty, sizeof(empty));
|
|
}
|
|
|
|
static bool validClockFlags(uint32_t flags)
|
|
{
|
|
return !(flags & ~AUDIO_CLOCK_FLAGS) &&
|
|
(!(flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE) ||
|
|
(flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID));
|
|
}
|
|
|
|
static LG_AudioClock clockFromMessage(const KVMFRAudioMessage * message,
|
|
bool discontinuity)
|
|
{
|
|
return (LG_AudioClock)
|
|
{
|
|
.position = message->position,
|
|
.time = message->timeNs,
|
|
.rate = (double)message->rateQ32 / 4294967296.0,
|
|
.stable =
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_STABLE) != 0,
|
|
.discontinuity = discontinuity ||
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY) != 0,
|
|
};
|
|
}
|
|
|
|
static uint32_t mapGeneration(LGMPAudio * audio,
|
|
struct AudioGeneration * generation, uint64_t wire)
|
|
{
|
|
if (!wire)
|
|
return 0;
|
|
if (generation->wire != wire || !generation->local)
|
|
{
|
|
generation->wire = wire;
|
|
generation->local = nextNonzero(&audio->generationSerial);
|
|
}
|
|
return generation->local;
|
|
}
|
|
|
|
static struct LGMPStreamDescriptor descriptorFromWire(
|
|
const KVMFRStreamDescriptor * wire)
|
|
{
|
|
return (struct LGMPStreamDescriptor)
|
|
{
|
|
.magic = wire->magic,
|
|
.version = wire->version,
|
|
.size = wire->size,
|
|
.offset = wire->offset,
|
|
.regionSize = wire->regionSize,
|
|
.direction = wire->direction,
|
|
.policy = wire->policy,
|
|
.slotCount = wire->slotCount,
|
|
.slotSize = wire->slotSize,
|
|
};
|
|
}
|
|
|
|
static bool validDescriptor(const KVMFRStreamDescriptor * descriptor,
|
|
enum LGMPStreamDirection direction)
|
|
{
|
|
return descriptor->magic == LGMP_STREAM_DESCRIPTOR_MAGIC &&
|
|
descriptor->version == LGMP_STREAM_DESCRIPTOR_VERSION &&
|
|
descriptor->size == sizeof(*descriptor) && descriptor->offset &&
|
|
descriptor->regionSize &&
|
|
descriptor->direction == (uint32_t)direction &&
|
|
descriptor->policy == LGMP_STREAM_RELIABLE_FIFO &&
|
|
descriptor->slotCount == KVMFR_AUDIO_STREAM_SLOT_COUNT &&
|
|
descriptor->slotSize == KVMFR_AUDIO_STREAM_SLOT_BYTES;
|
|
}
|
|
|
|
static void markStatusNL(LGMPAudio * audio, bool available,
|
|
uint64_t endpointGeneration)
|
|
{
|
|
const uint32_t oldGeneration = audio->endpointGeneration.local;
|
|
if (endpointGeneration)
|
|
mapGeneration(audio, &audio->endpointGeneration, endpointGeneration);
|
|
if (audio->statusAvailable != available ||
|
|
oldGeneration != audio->endpointGeneration.local)
|
|
audio->statusDirty = true;
|
|
audio->statusAvailable = available;
|
|
atomic_store_explicit(&audio->clockFeedbackAvailable,
|
|
available && (audio->remoteStatus.capabilities &
|
|
KVMFR_AUDIO_CAP_CLOCK_FEEDBACK), memory_order_release);
|
|
}
|
|
|
|
static void publishStatus(LGMPAudio * audio)
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
if (!audio->statusDirty)
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
return;
|
|
}
|
|
audio->statusDirty = false;
|
|
const LG_AudioStatusFn callback = audio->statusCallback;
|
|
void * opaque = audio->statusOpaque;
|
|
const LG_AudioStatus status =
|
|
{
|
|
.available = audio->connected && audio->statusAvailable,
|
|
.generation = audio->endpointGeneration.local,
|
|
};
|
|
struct AudioStatusCallbackFrame frame;
|
|
if (callback)
|
|
beginStatusCallback(audio, &frame);
|
|
LG_UNLOCK(audio->lock);
|
|
if (callback)
|
|
{
|
|
callback(opaque, &status);
|
|
endStatusCallback(audio, &frame);
|
|
}
|
|
}
|
|
|
|
static void updateWriteStreamState(LGMPAudio * audio)
|
|
{
|
|
lockWriteGate(audio);
|
|
LG_LOCK(audio->lock);
|
|
const bool admitted = audio->writeBound &&
|
|
audio->phase == AUDIO_PHASE_BOUND && !audio->releaseRequested;
|
|
const uint64_t captureGeneration = admitted && audio->capture.active ?
|
|
audio->capture.generation.wire : 0;
|
|
const uint32_t captureLocalGeneration =
|
|
admitted && audio->capture.active ?
|
|
audio->capture.generation.local : 0;
|
|
const bool captureChanged =
|
|
audio->writeCaptureGeneration != captureGeneration ||
|
|
audio->writeCaptureLocalGeneration != captureLocalGeneration;
|
|
audio->writeCaptureGeneration = captureGeneration;
|
|
audio->writeCaptureLocalGeneration = captureLocalGeneration;
|
|
audio->writeCaptureFormat = audio->capture.wireFormat;
|
|
audio->writePlaybackGeneration = admitted && audio->playback.active ?
|
|
audio->playback.generation.wire : 0;
|
|
audio->writePlaybackLocalGeneration =
|
|
admitted && audio->playback.active ?
|
|
audio->playback.generation.local : 0;
|
|
audio->publicDataAllowed = admitted &&
|
|
(audio->writeCaptureGeneration || audio->writePlaybackGeneration);
|
|
LG_UNLOCK(audio->lock);
|
|
if (captureChanged)
|
|
atomic_store_explicit(
|
|
&audio->captureDiscontinuity, false, memory_order_release);
|
|
unlockWriteGate(audio);
|
|
}
|
|
|
|
static void stopActiveStreams(LGMPAudio * audio)
|
|
{
|
|
const LG_AudioEventOps * events;
|
|
void * opaque;
|
|
uint32_t playbackGeneration;
|
|
uint32_t captureGeneration;
|
|
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
events = audio->attached ? audio->events : NULL;
|
|
opaque = audio->eventOpaque;
|
|
playbackGeneration = audio->playback.active ?
|
|
audio->playback.generation.local : 0;
|
|
captureGeneration = audio->capture.active ?
|
|
audio->capture.generation.local : 0;
|
|
memset(&audio->playback, 0, sizeof(audio->playback));
|
|
memset(&audio->capture, 0, sizeof(audio->capture));
|
|
audio->playbackDiscontinuity = false;
|
|
LG_UNLOCK(audio->lock);
|
|
updateWriteStreamState(audio);
|
|
|
|
if (events && playbackGeneration && events->playbackStop)
|
|
events->playbackStop(opaque, playbackGeneration);
|
|
if (events && captureGeneration && events->recordStop)
|
|
events->recordStop(opaque, captureGeneration);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void detachStreams(LGMPAudio * audio)
|
|
{
|
|
lockWriteGate(audio);
|
|
audio->publicDataAllowed = false;
|
|
audio->writeBound = false;
|
|
audio->writeCaptureGeneration = 0;
|
|
audio->writeCaptureLocalGeneration = 0;
|
|
audio->writePlaybackGeneration = 0;
|
|
audio->writePlaybackLocalGeneration = 0;
|
|
memset(&audio->writeCaptureFormat, 0,
|
|
sizeof(audio->writeCaptureFormat));
|
|
|
|
lgmpClientStreamDetach(&audio->clientToHostStream);
|
|
lgmpClientStreamDetach(&audio->hostToClientStream);
|
|
audio->hostReadActive = false;
|
|
audio->clientWriteActive = false;
|
|
audio->clientSequence = 0;
|
|
atomic_store_explicit(
|
|
&audio->captureDiscontinuity, false, memory_order_release);
|
|
unlockWriteGate(audio);
|
|
}
|
|
|
|
static void scheduleRetryNL(LGMPAudio * audio, uint64_t delay)
|
|
{
|
|
audio->phase = AUDIO_PHASE_IDLE;
|
|
audio->requestGeneration = 0;
|
|
audio->claimToken = 0;
|
|
audio->claimSends = 0;
|
|
audio->claimPosting = false;
|
|
audio->releaseRequested = false;
|
|
audio->releaseSent = false;
|
|
memset(&audio->claim, 0, sizeof(audio->claim));
|
|
audio->nextClaimAt = microtime() + delay;
|
|
}
|
|
|
|
static void finishBinding(LGMPAudio * audio, uint64_t delay)
|
|
{
|
|
stopActiveStreams(audio);
|
|
detachStreams(audio);
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, false, memory_order_release);
|
|
|
|
LG_LOCK(audio->lock);
|
|
memset(&audio->remoteStatus, 0, sizeof(audio->remoteStatus));
|
|
audio->sequenceKnown = false;
|
|
audio->expectedSequence = 0;
|
|
audio->releaseRequested = false;
|
|
audio->releaseSent = false;
|
|
audio->responsePending = false;
|
|
markStatusNL(audio, false, 0);
|
|
audio->endpointGeneration.wire = 0;
|
|
if (audio->connected && audio->discover)
|
|
scheduleRetryNL(audio, delay);
|
|
else
|
|
{
|
|
audio->phase = AUDIO_PHASE_IDLE;
|
|
audio->requestGeneration = 0;
|
|
audio->claimToken = 0;
|
|
audio->claimSends = 0;
|
|
audio->claimPosting = false;
|
|
}
|
|
LG_UNLOCK(audio->lock);
|
|
if (audio->pointerWake)
|
|
lgSignalEvent(audio->pointerWake);
|
|
}
|
|
|
|
static bool attachStreams(LGMPAudio * audio,
|
|
const KVMFRAudioStreamResponse * response)
|
|
{
|
|
if (!validDescriptor(&response->hostToClient,
|
|
LGMP_STREAM_HOST_TO_CLIENT) ||
|
|
!validDescriptor(&response->clientToHost,
|
|
LGMP_STREAM_CLIENT_TO_HOST))
|
|
{
|
|
DEBUG_WARN("Invalid LGMP audio stream descriptors");
|
|
return false;
|
|
}
|
|
|
|
LGMP_STATUS hostStatus = LGMP_OK;
|
|
if (!audio->hostToClientStream)
|
|
{
|
|
const struct LGMPStreamDescriptor hostToClient =
|
|
descriptorFromWire(&response->hostToClient);
|
|
hostStatus = lgmpClientStreamAttach(audio->client,
|
|
&hostToClient, &audio->hostToClientStream);
|
|
}
|
|
if (hostStatus == LGMP_OK)
|
|
{
|
|
uint32_t epoch = 0;
|
|
hostStatus = lgmpClientStreamActivate(
|
|
audio->hostToClientStream, &epoch);
|
|
audio->hostReadActive = hostStatus == LGMP_OK;
|
|
}
|
|
if (hostStatus != LGMP_OK)
|
|
DEBUG_WARN("Failed to attach/activate host-to-client audio stream: %s",
|
|
lgmpStatusString(hostStatus));
|
|
|
|
bool hostOwner = audio->hostReadActive;
|
|
if (hostOwner)
|
|
{
|
|
uint32_t clientID = 0;
|
|
uint32_t epoch = 0;
|
|
hostOwner = lgmpClientStreamGetBinding(
|
|
audio->hostToClientStream, &clientID, &epoch) == LGMP_OK &&
|
|
clientID == audio->clientID && epoch;
|
|
}
|
|
|
|
lockWriteGate(audio);
|
|
LGMP_STATUS clientStatus = LGMP_OK;
|
|
if (!audio->clientToHostStream)
|
|
{
|
|
const struct LGMPStreamDescriptor clientToHost =
|
|
descriptorFromWire(&response->clientToHost);
|
|
clientStatus = lgmpClientStreamAttach(audio->client,
|
|
&clientToHost, &audio->clientToHostStream);
|
|
}
|
|
if (clientStatus == LGMP_OK)
|
|
{
|
|
uint32_t epoch = 0;
|
|
clientStatus = lgmpClientStreamActivate(
|
|
audio->clientToHostStream, &epoch);
|
|
audio->clientWriteActive = clientStatus == LGMP_OK;
|
|
}
|
|
bool clientOwner = audio->clientWriteActive;
|
|
if (clientOwner)
|
|
{
|
|
uint32_t clientID = 0;
|
|
uint32_t epoch = 0;
|
|
clientOwner = lgmpClientStreamGetBinding(
|
|
audio->clientToHostStream, &clientID, &epoch) == LGMP_OK &&
|
|
clientID == audio->clientID && epoch;
|
|
}
|
|
audio->clientWriteActive = clientOwner;
|
|
const bool wasWriteBound = audio->writeBound;
|
|
audio->writeBound = clientOwner;
|
|
if (audio->writeBound && !wasWriteBound)
|
|
audio->clientSequence = 0;
|
|
unlockWriteGate(audio);
|
|
|
|
if (clientStatus != LGMP_OK)
|
|
DEBUG_WARN("Failed to attach/activate client-to-host audio stream: %s",
|
|
lgmpStatusString(clientStatus));
|
|
audio->hostReadActive = hostOwner;
|
|
lgmpStreamPollActivity(&audio->streamPoll);
|
|
return hostOwner && clientOwner;
|
|
}
|
|
|
|
static bool parseMessage(const LGMPStreamBuffer * buffer,
|
|
KVMFRAudioMessage * message, const uint8_t ** payload)
|
|
{
|
|
if (!buffer->data || buffer->size < sizeof(*message) ||
|
|
buffer->size > buffer->capacity)
|
|
return false;
|
|
memcpy(message, buffer->data, sizeof(*message));
|
|
if (message->version != KVMFR_AUDIO_MESSAGE_VERSION ||
|
|
message->size != sizeof(*message) || message->reserved0 ||
|
|
message->length > KVMFR_AUDIO_PCM_BYTES ||
|
|
buffer->size != sizeof(*message) + message->length)
|
|
return false;
|
|
*payload = (const uint8_t *)buffer->data + sizeof(*message);
|
|
return true;
|
|
}
|
|
|
|
static void requestRelease(LGMPAudio * audio, const char * reason)
|
|
{
|
|
bool log = false;
|
|
lockWriteGate(audio);
|
|
LG_LOCK(audio->lock);
|
|
closePublicDataLocked(audio);
|
|
if (!audio->releaseRequested)
|
|
{
|
|
audio->releaseRequested = true;
|
|
audio->releaseDeadline =
|
|
microtime() + AUDIO_OWNER_RETRY_US;
|
|
markStatusNL(audio, false, 0);
|
|
log = true;
|
|
}
|
|
if (audio->phase == AUDIO_PHASE_BOUND)
|
|
audio->phase = AUDIO_PHASE_RELEASING;
|
|
LG_UNLOCK(audio->lock);
|
|
unlockWriteGate(audio);
|
|
if (log)
|
|
DEBUG_INFO("LGMP audio ownership releasing: %s", reason);
|
|
signalWorker(audio);
|
|
}
|
|
|
|
static bool validEmptyMessage(const KVMFRAudioMessage * message,
|
|
uint32_t allowedFlags)
|
|
{
|
|
return !(message->flags & ~allowedFlags) &&
|
|
!message->position && !message->timeNs && !message->rateQ32 &&
|
|
!message->frames && !message->length;
|
|
}
|
|
|
|
static bool validVolume(const KVMFRAudioVolume * volume,
|
|
uint8_t channels)
|
|
{
|
|
if (volume->version != KVMFR_AUDIO_VOLUME_VERSION ||
|
|
volume->size != sizeof(*volume) || volume->reserved ||
|
|
volume->channelCount != channels)
|
|
return false;
|
|
for (unsigned i = channels; i < KVMFR_AUDIO_MAX_CHANNELS; ++i)
|
|
if (volume->values[i])
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static bool validStatus(const LGMPAudio * audio,
|
|
const KVMFRAudioStatus * status)
|
|
{
|
|
if (status->version != KVMFR_AUDIO_STATUS_VERSION ||
|
|
status->size != sizeof(*status) ||
|
|
(status->flags & ~AUDIO_STATUS_FLAGS) ||
|
|
status->protocolVersion != KVMFR_AUDIO_PROTOCOL_MAX ||
|
|
status->requestGeneration != audio->requestGeneration ||
|
|
(status->capabilities & ~AUDIO_CAPABILITIES) ||
|
|
status->ownerClientID != audio->clientID ||
|
|
status->ownerGeneration != audio->requestGeneration ||
|
|
!(status->flags & KVMFR_AUDIO_STATUS_HAS_OWNER) ||
|
|
status->reserved0 || !status->endpointGeneration ||
|
|
status->playbackState > KVMFR_AUDIO_STREAM_RUNNING ||
|
|
status->captureState > KVMFR_AUDIO_STREAM_RUNNING)
|
|
return false;
|
|
|
|
const bool playbackAvailable =
|
|
(status->flags & KVMFR_AUDIO_STATUS_PLAYBACK_AVAILABLE) != 0;
|
|
const bool captureAvailable =
|
|
(status->flags & KVMFR_AUDIO_STATUS_CAPTURE_AVAILABLE) != 0;
|
|
if (playbackAvailable !=
|
|
((status->capabilities & KVMFR_AUDIO_CAP_PLAYBACK) != 0) ||
|
|
captureAvailable !=
|
|
((status->capabilities & KVMFR_AUDIO_CAP_CAPTURE) != 0))
|
|
return false;
|
|
|
|
const bool playbackActive =
|
|
(status->flags & KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE) != 0;
|
|
const bool captureActive =
|
|
(status->flags & KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE) != 0;
|
|
const bool available =
|
|
(status->flags & KVMFR_AUDIO_STATUS_AVAILABLE) != 0;
|
|
LG_AudioFormat ignored;
|
|
if (playbackActive &&
|
|
(!available || !playbackAvailable || !status->playbackGeneration ||
|
|
status->playbackState == KVMFR_AUDIO_STREAM_STOPPED ||
|
|
!validFormat(&status->playbackFormat, &ignored)))
|
|
return false;
|
|
if (captureActive &&
|
|
(!available || !captureAvailable || !status->captureGeneration ||
|
|
status->captureState == KVMFR_AUDIO_STREAM_STOPPED ||
|
|
!validFormat(&status->captureFormat, &ignored)))
|
|
return false;
|
|
if ((!emptyFormat(&status->playbackFormat) &&
|
|
!validFormat(&status->playbackFormat, &ignored)) ||
|
|
(!emptyFormat(&status->captureFormat) &&
|
|
!validFormat(&status->captureFormat, &ignored)))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static bool currentPlaybackNL(const LGMPAudio * audio,
|
|
uint64_t wireGeneration)
|
|
{
|
|
return audio->playback.active && wireGeneration &&
|
|
audio->playback.generation.wire == wireGeneration;
|
|
}
|
|
|
|
static bool currentCaptureNL(const LGMPAudio * audio,
|
|
uint64_t wireGeneration)
|
|
{
|
|
return audio->capture.active && wireGeneration &&
|
|
audio->capture.generation.wire == wireGeneration;
|
|
}
|
|
|
|
static void dispatchPlaybackStop(LGMPAudio * audio,
|
|
uint64_t wireGeneration)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
if (!currentPlaybackNL(audio, wireGeneration))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
return;
|
|
}
|
|
const uint32_t generation = audio->playback.generation.local;
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
memset(&audio->playback, 0, sizeof(audio->playback));
|
|
audio->playbackDiscontinuity = false;
|
|
LG_UNLOCK(audio->lock);
|
|
updateWriteStreamState(audio);
|
|
if (events && events->playbackStop)
|
|
events->playbackStop(opaque, generation);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void dispatchCaptureStop(LGMPAudio * audio,
|
|
uint64_t wireGeneration)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
if (!currentCaptureNL(audio, wireGeneration))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
return;
|
|
}
|
|
const uint32_t generation = audio->capture.generation.local;
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
memset(&audio->capture, 0, sizeof(audio->capture));
|
|
LG_UNLOCK(audio->lock);
|
|
updateWriteStreamState(audio);
|
|
if (events && events->recordStop)
|
|
events->recordStop(opaque, generation);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void dispatchPlaybackStart(LGMPAudio * audio,
|
|
const KVMFRAudioMessage * message, const KVMFRAudioFormat * wire,
|
|
const LG_AudioFormat * format, bool sequenceGap)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
const uint32_t oldGeneration = audio->playback.active ?
|
|
audio->playback.generation.local : 0;
|
|
const uint32_t generation = mapGeneration(audio,
|
|
&audio->playback.generation, message->streamGeneration);
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
audio->playback.wireFormat = *wire;
|
|
audio->playback.format = *format;
|
|
audio->playback.active = true;
|
|
audio->playback.formatValid = true;
|
|
audio->playback.volumeValid = false;
|
|
audio->playback.muteValid = false;
|
|
audio->playback.clockValid =
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0;
|
|
audio->playback.clock = clockFromMessage(message, sequenceGap);
|
|
audio->playbackDiscontinuity = sequenceGap ||
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_DISCONTINUITY);
|
|
LG_UNLOCK(audio->lock);
|
|
updateWriteStreamState(audio);
|
|
|
|
if (events && oldGeneration && oldGeneration != generation &&
|
|
events->playbackStop)
|
|
events->playbackStop(opaque, oldGeneration);
|
|
if (events && events->playbackStart)
|
|
{
|
|
const bool clockValid =
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0;
|
|
const LG_AudioClock clock =
|
|
clockFromMessage(message, sequenceGap);
|
|
events->playbackStart(opaque, generation, format,
|
|
clockValid ? &clock : NULL);
|
|
}
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void dispatchCaptureStart(LGMPAudio * audio,
|
|
const KVMFRAudioMessage * message, const KVMFRAudioFormat * wire,
|
|
const LG_AudioFormat * format)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
const uint32_t oldGeneration = audio->capture.active ?
|
|
audio->capture.generation.local : 0;
|
|
const uint32_t generation = mapGeneration(audio,
|
|
&audio->capture.generation, message->streamGeneration);
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
audio->capture.wireFormat = *wire;
|
|
audio->capture.format = *format;
|
|
audio->capture.active = true;
|
|
audio->capture.formatValid = true;
|
|
audio->capture.volumeValid = false;
|
|
audio->capture.muteValid = false;
|
|
LG_UNLOCK(audio->lock);
|
|
updateWriteStreamState(audio);
|
|
|
|
if (events && oldGeneration && oldGeneration != generation &&
|
|
events->recordStop)
|
|
events->recordStop(opaque, oldGeneration);
|
|
if (events && events->recordStart)
|
|
events->recordStart(opaque, generation, format);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void dispatchPlaybackVolume(LGMPAudio * audio,
|
|
uint64_t wireGeneration, const KVMFRAudioVolume * volume)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
if (!currentPlaybackNL(audio, wireGeneration))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
return;
|
|
}
|
|
const uint32_t generation = audio->playback.generation.local;
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
audio->playback.volumeValid = true;
|
|
audio->playback.volumeChannels = (uint8_t)volume->channelCount;
|
|
memcpy(audio->playback.volume, volume->values,
|
|
sizeof(audio->playback.volume));
|
|
LG_UNLOCK(audio->lock);
|
|
if (events && events->playbackVolume)
|
|
events->playbackVolume(opaque, generation,
|
|
(uint8_t)volume->channelCount, volume->values);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void dispatchCaptureVolume(LGMPAudio * audio,
|
|
uint64_t wireGeneration, const KVMFRAudioVolume * volume)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
if (!currentCaptureNL(audio, wireGeneration))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
return;
|
|
}
|
|
const uint32_t generation = audio->capture.generation.local;
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
audio->capture.volumeValid = true;
|
|
audio->capture.volumeChannels = (uint8_t)volume->channelCount;
|
|
memcpy(audio->capture.volume, volume->values,
|
|
sizeof(audio->capture.volume));
|
|
LG_UNLOCK(audio->lock);
|
|
if (events && events->recordVolume)
|
|
events->recordVolume(opaque, generation,
|
|
(uint8_t)volume->channelCount, volume->values);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void dispatchPlaybackMute(LGMPAudio * audio,
|
|
uint64_t wireGeneration, bool mute)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
if (!currentPlaybackNL(audio, wireGeneration))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
return;
|
|
}
|
|
const uint32_t generation = audio->playback.generation.local;
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
audio->playback.muteValid = true;
|
|
audio->playback.mute = mute;
|
|
LG_UNLOCK(audio->lock);
|
|
if (events && events->playbackMute)
|
|
events->playbackMute(opaque, generation, mute);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static void dispatchCaptureMute(LGMPAudio * audio,
|
|
uint64_t wireGeneration, bool mute)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
if (!currentCaptureNL(audio, wireGeneration))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
return;
|
|
}
|
|
const uint32_t generation = audio->capture.generation.local;
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
audio->capture.muteValid = true;
|
|
audio->capture.mute = mute;
|
|
LG_UNLOCK(audio->lock);
|
|
if (events && events->recordMute)
|
|
events->recordMute(opaque, generation, mute);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static const uint8_t l_silence[KVMFR_AUDIO_PCM_BYTES];
|
|
|
|
static void dispatchPlaybackData(LGMPAudio * audio,
|
|
const KVMFRAudioMessage * message, const uint8_t * payload,
|
|
bool sequenceGap)
|
|
{
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
if (!currentPlaybackNL(audio, message->streamGeneration))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
return;
|
|
}
|
|
|
|
const bool silent =
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_SILENT) != 0;
|
|
const size_t bytes =
|
|
(size_t)message->frames * audio->playback.wireFormat.blockAlign;
|
|
if (bytes > KVMFR_AUDIO_PCM_BYTES ||
|
|
(silent ? message->length != 0 : message->length != bytes))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
requestRelease(audio, "invalid playback audio length");
|
|
return;
|
|
}
|
|
|
|
const uint32_t generation = audio->playback.generation.local;
|
|
const LG_AudioEventOps * events =
|
|
audio->attached ? audio->events : NULL;
|
|
void * opaque = audio->eventOpaque;
|
|
const bool discontinuity = sequenceGap ||
|
|
audio->playbackDiscontinuity;
|
|
const bool clockValid =
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) != 0;
|
|
if (!events || !events->playbackData)
|
|
audio->playbackDiscontinuity = true;
|
|
else
|
|
audio->playbackDiscontinuity = false;
|
|
LG_UNLOCK(audio->lock);
|
|
|
|
if (events && events->playbackData)
|
|
{
|
|
const LG_AudioClock clock =
|
|
clockFromMessage(message, discontinuity);
|
|
events->playbackData(opaque, generation,
|
|
silent ? l_silence : payload, message->frames,
|
|
clockValid ? &clock : NULL);
|
|
}
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static bool processStatus(LGMPAudio * audio,
|
|
const KVMFRAudioMessage * message, const uint8_t * payload)
|
|
{
|
|
if (message->streamGeneration || message->flags ||
|
|
message->position || message->timeNs || message->rateQ32 ||
|
|
message->frames || message->length != sizeof(KVMFRAudioStatus))
|
|
return false;
|
|
|
|
KVMFRAudioStatus status;
|
|
memcpy(&status, payload, sizeof(status));
|
|
LG_LOCK(audio->lock);
|
|
const bool valid = validStatus(audio, &status);
|
|
if (valid)
|
|
{
|
|
const bool playbackActive =
|
|
(status.flags & KVMFR_AUDIO_STATUS_PLAYBACK_ACTIVE) != 0;
|
|
const bool captureActive =
|
|
(status.flags & KVMFR_AUDIO_STATUS_CAPTURE_ACTIVE) != 0;
|
|
if (playbackActive &&
|
|
(!currentPlaybackNL(audio, status.playbackGeneration) ||
|
|
!sameFormat(&audio->playback.wireFormat,
|
|
&status.playbackFormat)))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
return false;
|
|
}
|
|
if (captureActive &&
|
|
(!currentCaptureNL(audio, status.captureGeneration) ||
|
|
!sameFormat(&audio->capture.wireFormat,
|
|
&status.captureFormat)))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
return false;
|
|
}
|
|
if ((!playbackActive && audio->playback.active) ||
|
|
(!captureActive && audio->capture.active))
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
return false;
|
|
}
|
|
|
|
audio->remoteStatus = status;
|
|
const bool available =
|
|
(status.flags & KVMFR_AUDIO_STATUS_AVAILABLE) != 0;
|
|
markStatusNL(audio, available, status.endpointGeneration);
|
|
}
|
|
LG_UNLOCK(audio->lock);
|
|
return valid;
|
|
}
|
|
|
|
static bool processInboundMessage(LGMPAudio * audio,
|
|
const KVMFRAudioMessage * message, const uint8_t * payload,
|
|
bool sequenceGap)
|
|
{
|
|
switch (message->type)
|
|
{
|
|
case KVMFR_AUDIO_MESSAGE_STATUS:
|
|
return processStatus(audio, message, payload);
|
|
|
|
case KVMFR_AUDIO_MESSAGE_PLAYBACK_START:
|
|
case KVMFR_AUDIO_MESSAGE_CAPTURE_START:
|
|
{
|
|
if (!message->streamGeneration || message->frames ||
|
|
message->length != sizeof(KVMFRAudioFormat) ||
|
|
!validClockFlags(message->flags) ||
|
|
(!(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) &&
|
|
(message->position || message->timeNs || message->rateQ32)))
|
|
return false;
|
|
KVMFRAudioFormat wire;
|
|
LG_AudioFormat format;
|
|
memcpy(&wire, payload, sizeof(wire));
|
|
if (!validFormat(&wire, &format))
|
|
return false;
|
|
if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_START)
|
|
dispatchPlaybackStart(
|
|
audio, message, &wire, &format, sequenceGap);
|
|
else
|
|
dispatchCaptureStart(audio, message, &wire, &format);
|
|
return true;
|
|
}
|
|
|
|
case KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP:
|
|
case KVMFR_AUDIO_MESSAGE_CAPTURE_STOP:
|
|
if (!message->streamGeneration ||
|
|
!validEmptyMessage(message, 0))
|
|
return false;
|
|
if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_STOP)
|
|
dispatchPlaybackStop(audio, message->streamGeneration);
|
|
else
|
|
dispatchCaptureStop(audio, message->streamGeneration);
|
|
return true;
|
|
|
|
case KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME:
|
|
case KVMFR_AUDIO_MESSAGE_CAPTURE_VOLUME:
|
|
{
|
|
if (!message->streamGeneration || message->flags ||
|
|
message->position || message->timeNs || message->rateQ32 ||
|
|
message->frames ||
|
|
message->length != sizeof(KVMFRAudioVolume))
|
|
return false;
|
|
KVMFRAudioVolume volume;
|
|
memcpy(&volume, payload, sizeof(volume));
|
|
LG_LOCK(audio->lock);
|
|
const bool current =
|
|
message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME ?
|
|
currentPlaybackNL(audio, message->streamGeneration) :
|
|
currentCaptureNL(audio, message->streamGeneration);
|
|
const uint8_t channels =
|
|
message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME ?
|
|
audio->playback.format.channelCount :
|
|
audio->capture.format.channelCount;
|
|
LG_UNLOCK(audio->lock);
|
|
if (!current)
|
|
return true;
|
|
if (!validVolume(&volume, channels))
|
|
return false;
|
|
if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_VOLUME)
|
|
dispatchPlaybackVolume(
|
|
audio, message->streamGeneration, &volume);
|
|
else
|
|
dispatchCaptureVolume(
|
|
audio, message->streamGeneration, &volume);
|
|
return true;
|
|
}
|
|
|
|
case KVMFR_AUDIO_MESSAGE_PLAYBACK_MUTE:
|
|
case KVMFR_AUDIO_MESSAGE_CAPTURE_MUTE:
|
|
if (!message->streamGeneration ||
|
|
!validEmptyMessage(message, KVMFR_AUDIO_MESSAGE_MUTED))
|
|
return false;
|
|
if (message->type == KVMFR_AUDIO_MESSAGE_PLAYBACK_MUTE)
|
|
dispatchPlaybackMute(audio, message->streamGeneration,
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_MUTED) != 0);
|
|
else
|
|
dispatchCaptureMute(audio, message->streamGeneration,
|
|
(message->flags & KVMFR_AUDIO_MESSAGE_MUTED) != 0);
|
|
return true;
|
|
|
|
case KVMFR_AUDIO_MESSAGE_PLAYBACK_DATA:
|
|
if (!message->streamGeneration || !message->frames ||
|
|
message->frames > KVMFR_AUDIO_MAX_FRAMES_PER_PACKET ||
|
|
(message->flags & ~AUDIO_DATA_FLAGS) ||
|
|
!validClockFlags(message->flags & AUDIO_CLOCK_FLAGS) ||
|
|
(!(message->flags & KVMFR_AUDIO_MESSAGE_CLOCK_VALID) &&
|
|
(message->position || message->timeNs || message->rateQ32)))
|
|
return false;
|
|
dispatchPlaybackData(audio, message, payload, sequenceGap);
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
enum WriteResult
|
|
{
|
|
WRITE_OK,
|
|
WRITE_FULL,
|
|
WRITE_LOST,
|
|
};
|
|
|
|
static enum WriteResult writeMessageLocked(LGMPAudio * audio,
|
|
KVMFRAudioMessageType type, uint64_t streamGeneration,
|
|
KVMFRAudioMessageFlags flags, uint64_t position, int64_t timeNs,
|
|
uint64_t rateQ32, uint32_t frames, const void * payload,
|
|
uint32_t length)
|
|
{
|
|
if (!audio->writeBound || !audio->clientToHostStream)
|
|
return WRITE_LOST;
|
|
|
|
LGMPStreamBuffer buffer = { 0 };
|
|
LGMP_STATUS status = lgmpClientStreamWriteAcquire(
|
|
audio->clientToHostStream, &buffer);
|
|
if (status == LGMP_ERR_STREAM_FULL)
|
|
return WRITE_FULL;
|
|
if (status != LGMP_OK)
|
|
{
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, true, memory_order_release);
|
|
return WRITE_LOST;
|
|
}
|
|
|
|
const uint32_t bytes = (uint32_t)sizeof(KVMFRAudioMessage) + length;
|
|
if (!buffer.data || buffer.capacity < bytes)
|
|
{
|
|
(void)lgmpClientStreamWriteCancel(
|
|
audio->clientToHostStream, &buffer);
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, true, memory_order_release);
|
|
return WRITE_LOST;
|
|
}
|
|
|
|
const KVMFRAudioMessage message =
|
|
{
|
|
.version = KVMFR_AUDIO_MESSAGE_VERSION,
|
|
.size = sizeof(KVMFRAudioMessage),
|
|
.type = type,
|
|
.streamGeneration = streamGeneration,
|
|
.flags = flags,
|
|
.sequence = audio->clientSequence,
|
|
.position = position,
|
|
.timeNs = timeNs,
|
|
.rateQ32 = rateQ32,
|
|
.frames = frames,
|
|
.length = length,
|
|
};
|
|
memcpy(buffer.data, &message, sizeof(message));
|
|
if (length)
|
|
memcpy((uint8_t *)buffer.data + sizeof(message), payload, length);
|
|
|
|
status = lgmpClientStreamWriteCommit(
|
|
audio->clientToHostStream, &buffer, bytes);
|
|
if (status == LGMP_OK)
|
|
{
|
|
++audio->clientSequence;
|
|
return WRITE_OK;
|
|
}
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, true, memory_order_release);
|
|
return WRITE_LOST;
|
|
}
|
|
|
|
static bool validResponse(const LGMPAudio * audio,
|
|
const KVMFRAudioStreamResponse * response)
|
|
{
|
|
if (response->magic != KVMFR_AUDIO_RESPONSE_MAGIC ||
|
|
response->version != KVMFR_AUDIO_CONTROL_VERSION ||
|
|
response->size != sizeof(*response) ||
|
|
response->requestGeneration != audio->requestGeneration ||
|
|
response->boundClientID != audio->clientID ||
|
|
response->claimToken != audio->claimToken ||
|
|
response->result < KVMFR_AUDIO_RESPONSE_BOUND ||
|
|
response->result > KVMFR_AUDIO_RESPONSE_ERROR)
|
|
return false;
|
|
for (unsigned i = 0; i < sizeof(response->cursorCompatibility); ++i)
|
|
if (response->cursorCompatibility[i])
|
|
return false;
|
|
|
|
if (response->result == KVMFR_AUDIO_RESPONSE_BOUND)
|
|
return response->protocolVersion == KVMFR_AUDIO_PROTOCOL_MAX &&
|
|
response->endpointGeneration &&
|
|
validDescriptor(&response->hostToClient,
|
|
LGMP_STREAM_HOST_TO_CLIENT) &&
|
|
validDescriptor(&response->clientToHost,
|
|
LGMP_STREAM_CLIENT_TO_HOST);
|
|
|
|
const KVMFRStreamDescriptor empty = { 0 };
|
|
return !response->protocolVersion &&
|
|
!memcmp(&response->hostToClient, &empty, sizeof(empty)) &&
|
|
!memcmp(&response->clientToHost, &empty, sizeof(empty));
|
|
}
|
|
|
|
static void processResponse(LGMPAudio * audio)
|
|
{
|
|
KVMFRAudioStreamResponse response;
|
|
bool pending = false;
|
|
LG_LOCK(audio->lock);
|
|
if (audio->responsePending)
|
|
{
|
|
response = audio->response;
|
|
audio->responsePending = false;
|
|
pending = true;
|
|
}
|
|
LG_UNLOCK(audio->lock);
|
|
if (!pending)
|
|
return;
|
|
|
|
LG_LOCK(audio->lock);
|
|
const bool current = audio->connected &&
|
|
(audio->phase == AUDIO_PHASE_CLAIM_SENT ||
|
|
audio->phase == AUDIO_PHASE_CLAIM_PENDING ||
|
|
audio->phase == AUDIO_PHASE_COOLDOWN) &&
|
|
response.requestGeneration == audio->requestGeneration &&
|
|
response.claimToken == audio->claimToken;
|
|
const bool valid = current && validResponse(audio, &response);
|
|
LG_UNLOCK(audio->lock);
|
|
if (!current)
|
|
return;
|
|
if (!valid)
|
|
{
|
|
DEBUG_WARN("Ignoring malformed matching LGMP audio response");
|
|
LG_LOCK(audio->lock);
|
|
audio->phase = AUDIO_PHASE_COOLDOWN;
|
|
audio->releaseDeadline =
|
|
microtime() + AUDIO_OWNER_RETRY_US;
|
|
LG_UNLOCK(audio->lock);
|
|
return;
|
|
}
|
|
|
|
if (response.result != KVMFR_AUDIO_RESPONSE_BOUND)
|
|
{
|
|
if (response.result == KVMFR_AUDIO_RESPONSE_INCOMPATIBLE)
|
|
DEBUG_WARN("The host LGMP audio protocol is incompatible");
|
|
LG_LOCK(audio->lock);
|
|
scheduleRetryNL(audio, response.result ==
|
|
KVMFR_AUDIO_RESPONSE_INCOMPATIBLE ?
|
|
AUDIO_OWNER_RETRY_US : AUDIO_CLAIM_RETRY_US);
|
|
LG_UNLOCK(audio->lock);
|
|
if (audio->pointerWake)
|
|
lgSignalEvent(audio->pointerWake);
|
|
return;
|
|
}
|
|
|
|
if (!attachStreams(audio, &response))
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
audio->phase = AUDIO_PHASE_RELEASING;
|
|
audio->releaseRequested = true;
|
|
audio->releaseSent = false;
|
|
audio->releaseDeadline =
|
|
microtime() + AUDIO_OWNER_RETRY_US;
|
|
audio->nextAttachAt = microtime() + AUDIO_CLAIM_RETRY_US;
|
|
LG_UNLOCK(audio->lock);
|
|
return;
|
|
}
|
|
|
|
LG_LOCK(audio->lock);
|
|
if (!audio->connected ||
|
|
response.requestGeneration != audio->requestGeneration ||
|
|
response.claimToken != audio->claimToken)
|
|
{
|
|
audio->releaseRequested = true;
|
|
audio->phase = AUDIO_PHASE_RELEASING;
|
|
}
|
|
else
|
|
{
|
|
mapGeneration(audio, &audio->endpointGeneration,
|
|
response.endpointGeneration);
|
|
audio->phase = audio->releaseRequested || !audio->discover ?
|
|
AUDIO_PHASE_RELEASING : AUDIO_PHASE_BOUND;
|
|
}
|
|
audio->sequenceKnown = false;
|
|
audio->expectedSequence = 0;
|
|
audio->releaseSent = false;
|
|
audio->nextKeepalive = microtime();
|
|
LG_UNLOCK(audio->lock);
|
|
DEBUG_INFO("LGMP audio streams attached for request %u",
|
|
response.requestGeneration);
|
|
}
|
|
|
|
static unsigned drainInbound(LGMPAudio * audio)
|
|
{
|
|
unsigned drained = 0;
|
|
while (audio->hostToClientStream && audio->hostReadActive &&
|
|
drained < AUDIO_DRAIN_MAX)
|
|
{
|
|
LGMPStreamBuffer buffer = { 0 };
|
|
LGMP_STATUS status = lgmpClientStreamReadPeek(
|
|
audio->hostToClientStream, &buffer);
|
|
if (status == LGMP_ERR_STREAM_EMPTY)
|
|
break;
|
|
if (status == LGMP_ERR_STREAM_UNBOUND ||
|
|
status == LGMP_ERR_STREAM_STALE)
|
|
{
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, true, memory_order_release);
|
|
break;
|
|
}
|
|
if (status != LGMP_OK)
|
|
{
|
|
DEBUG_WARN("Failed to read LGMP audio stream: %s",
|
|
lgmpStatusString(status));
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, true, memory_order_release);
|
|
break;
|
|
}
|
|
|
|
++drained;
|
|
LG_LOCK(audio->lock);
|
|
const bool releasing = audio->phase == AUDIO_PHASE_RELEASING;
|
|
LG_UNLOCK(audio->lock);
|
|
bool valid = true;
|
|
if (!releasing)
|
|
{
|
|
KVMFRAudioMessage message;
|
|
const uint8_t * payload;
|
|
valid = parseMessage(&buffer, &message, &payload);
|
|
bool sequenceGap = false;
|
|
if (valid)
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
sequenceGap = audio->sequenceKnown ?
|
|
message.sequence != audio->expectedSequence :
|
|
message.sequence != 0;
|
|
audio->sequenceKnown = true;
|
|
audio->expectedSequence = message.sequence + 1;
|
|
LG_UNLOCK(audio->lock);
|
|
valid = processInboundMessage(
|
|
audio, &message, payload, sequenceGap);
|
|
}
|
|
}
|
|
|
|
status = lgmpClientStreamReadRelease(
|
|
audio->hostToClientStream, &buffer);
|
|
if (status != LGMP_OK)
|
|
{
|
|
if (status != LGMP_ERR_STREAM_UNBOUND &&
|
|
status != LGMP_ERR_STREAM_STALE)
|
|
DEBUG_WARN("Failed to release LGMP audio record: %s",
|
|
lgmpStatusString(status));
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, true, memory_order_release);
|
|
break;
|
|
}
|
|
if (!valid)
|
|
{
|
|
requestRelease(audio, "malformed host-to-client record");
|
|
break;
|
|
}
|
|
}
|
|
return drained;
|
|
}
|
|
|
|
static bool bindingGone(LGMPAudio * audio)
|
|
{
|
|
bool any = false;
|
|
const PLGMPClientStream streams[2] =
|
|
{
|
|
audio->hostToClientStream,
|
|
audio->clientToHostStream,
|
|
};
|
|
for (unsigned i = 0; i < 2; ++i)
|
|
{
|
|
if (!streams[i])
|
|
continue;
|
|
any = true;
|
|
uint32_t clientID = 0;
|
|
uint32_t epoch = 0;
|
|
if (i == 1)
|
|
lockWriteGate(audio);
|
|
const LGMP_STATUS status = lgmpClientStreamGetBinding(
|
|
streams[i], &clientID, &epoch);
|
|
if (i == 1)
|
|
unlockWriteGate(audio);
|
|
if (status == LGMP_OK && clientID == audio->clientID && epoch)
|
|
return false;
|
|
if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND &&
|
|
status != LGMP_ERR_STREAM_STALE)
|
|
{
|
|
DEBUG_WARN("Failed to query LGMP audio binding: %s",
|
|
lgmpStatusString(status));
|
|
return false;
|
|
}
|
|
}
|
|
return any;
|
|
}
|
|
|
|
static void maintainBinding(LGMPAudio * audio)
|
|
{
|
|
const uint64_t now = microtime();
|
|
LG_LOCK(audio->lock);
|
|
const enum AudioPhase phase = audio->phase;
|
|
const bool releaseRequested = audio->releaseRequested;
|
|
const bool releaseSent = audio->releaseSent;
|
|
const uint64_t deadline = audio->releaseDeadline;
|
|
const uint64_t nextAttachAt = audio->nextAttachAt;
|
|
const bool keepalive = phase == AUDIO_PHASE_BOUND &&
|
|
!releaseRequested && now >= audio->nextKeepalive;
|
|
LG_UNLOCK(audio->lock);
|
|
|
|
if ((phase == AUDIO_PHASE_CLAIM_PENDING ||
|
|
phase == AUDIO_PHASE_CLAIM_SENT ||
|
|
phase == AUDIO_PHASE_COOLDOWN) &&
|
|
releaseRequested && now >= deadline)
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
if ((audio->phase == AUDIO_PHASE_CLAIM_PENDING ||
|
|
audio->phase == AUDIO_PHASE_CLAIM_SENT ||
|
|
audio->phase == AUDIO_PHASE_COOLDOWN) &&
|
|
audio->releaseRequested &&
|
|
!audio->claimPosting)
|
|
{
|
|
audio->responsePending = false;
|
|
scheduleRetryNL(audio, AUDIO_CLAIM_RETRY_US);
|
|
}
|
|
LG_UNLOCK(audio->lock);
|
|
return;
|
|
}
|
|
|
|
if (phase == AUDIO_PHASE_COOLDOWN && now >= deadline)
|
|
{
|
|
bool wakePointer = false;
|
|
LG_LOCK(audio->lock);
|
|
if (audio->phase == AUDIO_PHASE_COOLDOWN &&
|
|
now >= audio->releaseDeadline &&
|
|
!audio->releaseRequested && audio->discover)
|
|
{
|
|
audio->phase = audio->claimSends ?
|
|
AUDIO_PHASE_CLAIM_SENT : AUDIO_PHASE_CLAIM_PENDING;
|
|
audio->claimSends = 0;
|
|
audio->nextClaimAt = 0;
|
|
wakePointer = true;
|
|
}
|
|
else if (audio->phase == AUDIO_PHASE_COOLDOWN &&
|
|
!audio->claimSends && !audio->claimPosting)
|
|
scheduleRetryNL(audio, AUDIO_CLAIM_RETRY_US);
|
|
LG_UNLOCK(audio->lock);
|
|
if (wakePointer && audio->pointerWake)
|
|
lgSignalEvent(audio->pointerWake);
|
|
return;
|
|
}
|
|
|
|
if (phase != AUDIO_PHASE_BOUND &&
|
|
phase != AUDIO_PHASE_RELEASING)
|
|
return;
|
|
|
|
if (phase == AUDIO_PHASE_RELEASING &&
|
|
(!audio->hostReadActive || !audio->clientWriteActive) &&
|
|
now >= nextAttachAt)
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
const KVMFRAudioStreamResponse response = audio->response;
|
|
audio->nextAttachAt = now + AUDIO_CLAIM_RETRY_US;
|
|
LG_UNLOCK(audio->lock);
|
|
attachStreams(audio, &response);
|
|
}
|
|
|
|
if (phase == AUDIO_PHASE_RELEASING && !releaseSent &&
|
|
audio->clientWriteActive)
|
|
{
|
|
lockWriteGate(audio);
|
|
const enum WriteResult result = writeMessageLocked(audio,
|
|
KVMFR_AUDIO_MESSAGE_RELEASE, 0, 0, 0, 0, 0, 0, NULL, 0);
|
|
unlockWriteGate(audio);
|
|
if (result == WRITE_OK)
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
if (audio->phase == AUDIO_PHASE_RELEASING)
|
|
audio->releaseSent = true;
|
|
LG_UNLOCK(audio->lock);
|
|
}
|
|
}
|
|
else if (keepalive)
|
|
{
|
|
lockWriteGate(audio);
|
|
const enum WriteResult result = writeMessageLocked(audio,
|
|
KVMFR_AUDIO_MESSAGE_KEEPALIVE, 0, 0, 0, 0, 0, 0, NULL, 0);
|
|
unlockWriteGate(audio);
|
|
if (result == WRITE_OK)
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
if (audio->phase == AUDIO_PHASE_BOUND)
|
|
audio->nextKeepalive =
|
|
now + (uint64_t)KVMFR_AUDIO_KEEPALIVE_MS * 1000U;
|
|
LG_UNLOCK(audio->lock);
|
|
}
|
|
}
|
|
|
|
if (phase == AUDIO_PHASE_RELEASING && bindingGone(audio))
|
|
finishBinding(audio, AUDIO_CLAIM_RETRY_US);
|
|
}
|
|
|
|
static void stopWorkerBinding(LGMPAudio * audio)
|
|
{
|
|
LG_LOCK(audio->lock);
|
|
const bool active = audio->phase == AUDIO_PHASE_BOUND ||
|
|
audio->phase == AUDIO_PHASE_RELEASING;
|
|
audio->phase = active ? AUDIO_PHASE_RELEASING : audio->phase;
|
|
LG_UNLOCK(audio->lock);
|
|
if (!active)
|
|
{
|
|
stopActiveStreams(audio);
|
|
detachStreams(audio);
|
|
return;
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
drainInbound(audio);
|
|
maintainBinding(audio);
|
|
if (bindingGone(audio) || !lgmpClientSessionValid(audio->client))
|
|
break;
|
|
lgWaitEvent(audio->event, 1);
|
|
}
|
|
stopActiveStreams(audio);
|
|
detachStreams(audio);
|
|
}
|
|
|
|
static int audioThread(void * opaque)
|
|
{
|
|
LGMPAudio * audio = opaque;
|
|
const struct LGMPStreamPollConfig pollConfig =
|
|
{
|
|
.spinCount = 64,
|
|
.minWaitUs = 25,
|
|
.maxWaitUs = AUDIO_ACTIVE_POLL_US,
|
|
};
|
|
if (lgmpStreamPollInit(&audio->streamPoll, pollConfig) != LGMP_OK)
|
|
return -1;
|
|
|
|
while (!atomic_load_explicit(&audio->stop, memory_order_acquire))
|
|
{
|
|
processResponse(audio);
|
|
const unsigned drained = drainInbound(audio);
|
|
if (drained)
|
|
lgmpStreamPollActivity(&audio->streamPoll);
|
|
maintainBinding(audio);
|
|
if (atomic_exchange_explicit(
|
|
&audio->dataPlaneLost, false, memory_order_acq_rel))
|
|
{
|
|
requestRelease(audio, "audio stream operation failed");
|
|
if (bindingGone(audio))
|
|
finishBinding(audio, AUDIO_CLAIM_RETRY_US);
|
|
}
|
|
publishStatus(audio);
|
|
|
|
if (atomic_load_explicit(&audio->stop, memory_order_acquire))
|
|
break;
|
|
LG_LOCK(audio->lock);
|
|
const bool active = audio->phase == AUDIO_PHASE_BOUND ||
|
|
audio->phase == AUDIO_PHASE_RELEASING;
|
|
LG_UNLOCK(audio->lock);
|
|
if (active)
|
|
{
|
|
if (drained == AUDIO_DRAIN_MAX)
|
|
{
|
|
lgmpStreamPollActivity(&audio->streamPoll);
|
|
continue;
|
|
}
|
|
const uint32_t waitUs = lgmpStreamPollIdle(&audio->streamPoll);
|
|
if (!waitUs)
|
|
continue;
|
|
if (lgWaitEventNS(audio->event, waitUs * 1000U))
|
|
lgmpStreamPollActivity(&audio->streamPoll);
|
|
}
|
|
else
|
|
lgWaitEvent(audio->event, AUDIO_IDLE_POLL_MS);
|
|
}
|
|
|
|
stopWorkerBinding(audio);
|
|
publishStatus(audio);
|
|
return 0;
|
|
}
|
|
|
|
bool lgmpAudio_create(PLGMPClient client, LGEvent * pointerWake,
|
|
LGMPAudio ** result)
|
|
{
|
|
if (!client || !pointerWake || !result)
|
|
return false;
|
|
LGMPAudio * audio = calloc(1, sizeof(*audio));
|
|
if (!audio)
|
|
return false;
|
|
audio->client = client;
|
|
audio->pointerWake = pointerWake;
|
|
audio->event = lgCreateEvent(true, 0);
|
|
if (!audio->event)
|
|
{
|
|
free(audio);
|
|
return false;
|
|
}
|
|
audio->generationSerial = 1;
|
|
if (!randomBytes(&audio->requestSerial,
|
|
sizeof(audio->requestSerial)))
|
|
{
|
|
lgFreeEvent(audio->event);
|
|
free(audio);
|
|
return false;
|
|
}
|
|
if (!audio->requestSerial)
|
|
audio->requestSerial = 1;
|
|
LG_LOCK_INIT(audio->lock);
|
|
LG_LOCK_INIT(audio->eventLock);
|
|
LG_LOCK_INIT(audio->statusWait.lock);
|
|
atomic_flag_clear(&audio->writeGate);
|
|
atomic_init(&audio->stop, false);
|
|
atomic_init(&audio->dataPlaneLost, false);
|
|
atomic_init(&audio->captureDiscontinuity, false);
|
|
atomic_init(&audio->clockFeedbackAvailable, false);
|
|
atomic_init(&audio->statusInFlight, 0);
|
|
atomic_init(&audio->statusWait.count, 0);
|
|
*result = audio;
|
|
return true;
|
|
}
|
|
|
|
void lgmpAudio_destroy(LGMPAudio ** audio)
|
|
{
|
|
if (!audio || !*audio)
|
|
return;
|
|
lgmpAudio_disconnect(*audio);
|
|
lgFreeEvent((*audio)->event);
|
|
LG_LOCK_FREE((*audio)->statusWait.lock);
|
|
LG_LOCK_FREE((*audio)->eventLock);
|
|
LG_LOCK_FREE((*audio)->lock);
|
|
free(*audio);
|
|
*audio = NULL;
|
|
}
|
|
|
|
bool lgmpAudio_connect(LGMPAudio * audio, uint32_t clientID)
|
|
{
|
|
if (!audio || !clientID)
|
|
return false;
|
|
LG_LOCK(audio->lock);
|
|
if (audio->connected)
|
|
{
|
|
const bool same = audio->clientID == clientID;
|
|
LG_UNLOCK(audio->lock);
|
|
return same;
|
|
}
|
|
if (audio->thread)
|
|
{
|
|
LG_UNLOCK(audio->lock);
|
|
return false;
|
|
}
|
|
lgResetEvent(audio->event);
|
|
audio->connected = true;
|
|
audio->clientID = clientID;
|
|
audio->phase = AUDIO_PHASE_IDLE;
|
|
audio->statusAvailable = false;
|
|
audio->statusDirty = true;
|
|
audio->nextClaimAt = 0;
|
|
audio->releaseRequested = false;
|
|
audio->releaseSent = false;
|
|
memset(&audio->endpointGeneration, 0,
|
|
sizeof(audio->endpointGeneration));
|
|
atomic_store_explicit(&audio->stop, false, memory_order_release);
|
|
atomic_store_explicit(
|
|
&audio->dataPlaneLost, false, memory_order_release);
|
|
|
|
LGThread * thread;
|
|
if (lgCreateThread("lgmpAudio", audioThread, audio, &thread))
|
|
{
|
|
audio->thread = thread;
|
|
LG_UNLOCK(audio->lock);
|
|
return true;
|
|
}
|
|
|
|
audio->connected = false;
|
|
LG_UNLOCK(audio->lock);
|
|
return false;
|
|
}
|
|
|
|
void lgmpAudio_disconnect(LGMPAudio * audio)
|
|
{
|
|
if (!audio)
|
|
return;
|
|
|
|
LG_LOCK(audio->eventLock);
|
|
lockWriteGate(audio);
|
|
LG_LOCK(audio->lock);
|
|
closePublicDataLocked(audio);
|
|
audio->events = NULL;
|
|
audio->eventOpaque = NULL;
|
|
audio->attached = false;
|
|
audio->discover = false;
|
|
audio->releaseRequested = true;
|
|
audio->releaseDeadline =
|
|
microtime() + AUDIO_OWNER_RETRY_US;
|
|
if (audio->phase == AUDIO_PHASE_BOUND)
|
|
audio->phase = AUDIO_PHASE_RELEASING;
|
|
markStatusNL(audio, false, 0);
|
|
audio->connected = false;
|
|
LGThread * thread = audio->thread;
|
|
LGEvent * event = audio->event;
|
|
atomic_store_explicit(&audio->stop, true, memory_order_release);
|
|
LG_UNLOCK(audio->lock);
|
|
unlockWriteGate(audio);
|
|
LG_UNLOCK(audio->eventLock);
|
|
|
|
if (event)
|
|
lgSignalEvent(event);
|
|
if (thread)
|
|
lgJoinThread(thread, NULL);
|
|
|
|
LG_LOCK(audio->lock);
|
|
audio->thread = NULL;
|
|
audio->clientID = 0;
|
|
audio->phase = AUDIO_PHASE_IDLE;
|
|
audio->responsePending = false;
|
|
audio->claimPosting = false;
|
|
audio->requestGeneration = 0;
|
|
audio->claimToken = 0;
|
|
audio->claimSends = 0;
|
|
audio->releaseRequested = false;
|
|
audio->releaseSent = false;
|
|
audio->statusCallback = NULL;
|
|
audio->statusOpaque = NULL;
|
|
memset(&audio->claim, 0, sizeof(audio->claim));
|
|
memset(&audio->response, 0, sizeof(audio->response));
|
|
memset(&audio->remoteStatus, 0, sizeof(audio->remoteStatus));
|
|
LG_UNLOCK(audio->lock);
|
|
waitStatusCallbacks(audio);
|
|
}
|
|
|
|
static bool newClaimNL(LGMPAudio * audio)
|
|
{
|
|
uint64_t token = 0;
|
|
if (!randomBytes(&token, sizeof(token)))
|
|
{
|
|
token = microtime() ^ ((uint64_t)audio->clientID << 32) ^
|
|
audio->requestSerial;
|
|
}
|
|
if (!token)
|
|
token = 1;
|
|
|
|
audio->requestGeneration = nextNonzero(&audio->requestSerial);
|
|
audio->claimToken = token;
|
|
audio->claim = (KVMFRAudioClaim)
|
|
{
|
|
.messageType = KVMFR_MESSAGE_AUDIO_CLAIM,
|
|
.version = KVMFR_AUDIO_CONTROL_VERSION,
|
|
.size = sizeof(KVMFRAudioClaim),
|
|
.requestGeneration = audio->requestGeneration,
|
|
.flags = KVMFR_AUDIO_CLAIM_BIND,
|
|
.claimToken = audio->claimToken,
|
|
.protocolMin = KVMFR_AUDIO_PROTOCOL_MIN,
|
|
.protocolMax = KVMFR_AUDIO_PROTOCOL_MAX,
|
|
.clientCapabilities = AUDIO_CAPABILITIES,
|
|
};
|
|
audio->claimSends = 0;
|
|
audio->claimPosting = false;
|
|
audio->phase = AUDIO_PHASE_CLAIM_PENDING;
|
|
audio->nextClaimAt = 0;
|
|
return true;
|
|
}
|
|
|
|
void lgmpAudio_pumpClaim(LGMPAudio * audio,
|
|
PLGMPClientQueue pointerQueue)
|
|
{
|
|
if (!audio || !pointerQueue)
|
|
return;
|
|
|
|
KVMFRAudioClaim claim;
|
|
bool send = false;
|
|
const uint64_t now = microtime();
|
|
LG_LOCK(audio->lock);
|
|
if (audio->connected && audio->discover &&
|
|
audio->phase == AUDIO_PHASE_IDLE &&
|
|
now >= audio->nextClaimAt)
|
|
newClaimNL(audio);
|
|
|
|
if (audio->connected && !audio->releaseRequested &&
|
|
(audio->phase == AUDIO_PHASE_CLAIM_PENDING ||
|
|
audio->phase == AUDIO_PHASE_CLAIM_SENT) &&
|
|
now >= audio->nextClaimAt)
|
|
{
|
|
if (audio->claimSends >= 3)
|
|
{
|
|
audio->phase = AUDIO_PHASE_COOLDOWN;
|
|
audio->releaseDeadline = now + AUDIO_OWNER_RETRY_US;
|
|
}
|
|
else
|
|
{
|
|
claim = audio->claim;
|
|
send = true;
|
|
audio->claimPosting = true;
|
|
}
|
|
}
|
|
LG_UNLOCK(audio->lock);
|
|
if (!send)
|
|
return;
|
|
|
|
uint32_t serial = 0;
|
|
const LGMP_STATUS status = lgmpClientSendData(pointerQueue,
|
|
&claim, sizeof(claim), &serial);
|
|
LG_LOCK(audio->lock);
|
|
if (claim.requestGeneration == audio->requestGeneration &&
|
|
claim.claimToken == audio->claimToken)
|
|
{
|
|
audio->claimPosting = false;
|
|
if (audio->connected)
|
|
{
|
|
if (status == LGMP_OK)
|
|
{
|
|
audio->phase = AUDIO_PHASE_CLAIM_SENT;
|
|
++audio->claimSends;
|
|
audio->nextClaimAt = now + AUDIO_CLAIM_RETRY_US;
|
|
}
|
|
else if (status == LGMP_ERR_QUEUE_FULL ||
|
|
status == LGMP_ERR_QUEUE_BUSY)
|
|
audio->nextClaimAt = now + 1000U;
|
|
else
|
|
{
|
|
audio->nextClaimAt = now + AUDIO_CLAIM_RETRY_US;
|
|
DEBUG_WARN("Failed to send LGMP audio claim: %s",
|
|
lgmpStatusString(status));
|
|
}
|
|
}
|
|
}
|
|
LG_UNLOCK(audio->lock);
|
|
}
|
|
|
|
void lgmpAudio_handleResponse(LGMPAudio * audio,
|
|
const KVMFRAudioStreamResponse * response)
|
|
{
|
|
if (!audio || !response)
|
|
return;
|
|
LG_LOCK(audio->lock);
|
|
if (audio->connected &&
|
|
response->requestGeneration == audio->requestGeneration &&
|
|
response->claimToken == audio->claimToken &&
|
|
response->boundClientID == audio->clientID &&
|
|
(audio->phase == AUDIO_PHASE_CLAIM_PENDING ||
|
|
audio->phase == AUDIO_PHASE_CLAIM_SENT ||
|
|
audio->phase == AUDIO_PHASE_COOLDOWN) &&
|
|
!audio->responsePending)
|
|
{
|
|
audio->response = *response;
|
|
audio->responsePending = true;
|
|
}
|
|
LG_UNLOCK(audio->lock);
|
|
signalWorker(audio);
|
|
}
|
|
|
|
static void setStatusListener(void * opaque,
|
|
LG_AudioStatusFn callback, void * callbackOpaque)
|
|
{
|
|
LGMPAudio * audio = opaque;
|
|
if (!callback)
|
|
lockWriteGate(audio);
|
|
LG_LOCK(audio->lock);
|
|
if (!callback)
|
|
closePublicDataLocked(audio);
|
|
audio->statusCallback = callback;
|
|
audio->statusOpaque = callbackOpaque;
|
|
audio->discover = callback && audio->connected;
|
|
if (audio->discover && audio->phase == AUDIO_PHASE_IDLE)
|
|
{
|
|
audio->releaseRequested = false;
|
|
audio->nextClaimAt = 0;
|
|
}
|
|
if (!callback)
|
|
{
|
|
const bool ownsOrMayOwn =
|
|
audio->phase != AUDIO_PHASE_IDLE;
|
|
audio->releaseRequested = ownsOrMayOwn;
|
|
if (ownsOrMayOwn)
|
|
{
|
|
audio->releaseDeadline =
|
|
microtime() + AUDIO_OWNER_RETRY_US;
|
|
if (audio->phase == AUDIO_PHASE_BOUND)
|
|
audio->phase = AUDIO_PHASE_RELEASING;
|
|
}
|
|
markStatusNL(audio, false, 0);
|
|
}
|
|
const LG_AudioStatus status =
|
|
{
|
|
.available = audio->connected && audio->statusAvailable,
|
|
.generation = audio->endpointGeneration.local,
|
|
};
|
|
struct AudioStatusCallbackFrame frame;
|
|
if (callback)
|
|
beginStatusCallback(audio, &frame);
|
|
audio->statusDirty = false;
|
|
LG_UNLOCK(audio->lock);
|
|
if (!callback)
|
|
unlockWriteGate(audio);
|
|
if (callback)
|
|
{
|
|
callback(callbackOpaque, &status);
|
|
endStatusCallback(audio, &frame);
|
|
}
|
|
else
|
|
waitStatusCallbacks(audio);
|
|
if (callback && audio->pointerWake)
|
|
lgSignalEvent(audio->pointerWake);
|
|
signalWorker(audio);
|
|
}
|
|
|
|
static bool attach(void * opaque, const LG_AudioEventOps * events,
|
|
void * eventOpaque)
|
|
{
|
|
LGMPAudio * audio = opaque;
|
|
if (!events)
|
|
return false;
|
|
|
|
struct AudioStreamState playback;
|
|
struct AudioStreamState capture;
|
|
bool admitted;
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
admitted = audio->connected && audio->statusAvailable &&
|
|
audio->phase == AUDIO_PHASE_BOUND && !audio->releaseRequested &&
|
|
!audio->attached;
|
|
if (admitted)
|
|
{
|
|
audio->events = events;
|
|
audio->eventOpaque = eventOpaque;
|
|
audio->attached = true;
|
|
}
|
|
playback = audio->playback;
|
|
capture = audio->capture;
|
|
LG_UNLOCK(audio->lock);
|
|
|
|
if (admitted && playback.active && events->playbackStart)
|
|
{
|
|
events->playbackStart(eventOpaque, playback.generation.local,
|
|
&playback.format,
|
|
playback.clockValid ? &playback.clock : NULL);
|
|
if (playback.volumeValid && events->playbackVolume)
|
|
events->playbackVolume(eventOpaque, playback.generation.local,
|
|
playback.volumeChannels, playback.volume);
|
|
if (playback.muteValid && events->playbackMute)
|
|
events->playbackMute(eventOpaque, playback.generation.local,
|
|
playback.mute);
|
|
}
|
|
if (admitted && capture.active && events->recordStart)
|
|
{
|
|
events->recordStart(eventOpaque, capture.generation.local,
|
|
&capture.format);
|
|
if (capture.volumeValid && events->recordVolume)
|
|
events->recordVolume(eventOpaque, capture.generation.local,
|
|
capture.volumeChannels, capture.volume);
|
|
if (capture.muteValid && events->recordMute)
|
|
events->recordMute(eventOpaque, capture.generation.local,
|
|
capture.mute);
|
|
}
|
|
LG_UNLOCK(audio->eventLock);
|
|
if (admitted)
|
|
signalWorker(audio);
|
|
return admitted;
|
|
}
|
|
|
|
static void detach(void * opaque)
|
|
{
|
|
LGMPAudio * audio = opaque;
|
|
LG_LOCK(audio->eventLock);
|
|
LG_LOCK(audio->lock);
|
|
audio->attached = false;
|
|
audio->events = NULL;
|
|
audio->eventOpaque = NULL;
|
|
LG_UNLOCK(audio->lock);
|
|
LG_UNLOCK(audio->eventLock);
|
|
}
|
|
|
|
static bool rateToQ32(double rate, bool allowZero, uint64_t * result)
|
|
{
|
|
if (!isfinite(rate) || rate < 0.0 || (!allowZero && rate == 0.0) ||
|
|
rate > (double)KVMFR_AUDIO_MAX_SAMPLE_RATE * 2.0)
|
|
return false;
|
|
const long double scaled = (long double)rate * 4294967296.0L;
|
|
if (scaled > (long double)UINT64_MAX)
|
|
return false;
|
|
*result = (uint64_t)(scaled + 0.5L);
|
|
return allowZero || *result != 0;
|
|
}
|
|
|
|
static bool recordData(void * opaque, uint32_t generation,
|
|
const void * data, size_t frames, const LG_AudioClock * sourceClock)
|
|
{
|
|
LGMPAudio * audio = opaque;
|
|
if (!generation || !data || !frames ||
|
|
frames > KVMFR_AUDIO_MAX_FRAMES_PER_PACKET)
|
|
return false;
|
|
if (!tryWriteGate(audio))
|
|
{
|
|
atomic_store_explicit(
|
|
&audio->captureDiscontinuity, true, memory_order_release);
|
|
return false;
|
|
}
|
|
|
|
if (!audio->publicDataAllowed || !audio->writeBound ||
|
|
generation != audio->writeCaptureLocalGeneration ||
|
|
!audio->writeCaptureGeneration)
|
|
{
|
|
unlockWriteGate(audio);
|
|
return false;
|
|
}
|
|
const size_t bytes = frames * audio->writeCaptureFormat.blockAlign;
|
|
if (!audio->writeCaptureFormat.blockAlign ||
|
|
bytes > KVMFR_AUDIO_PCM_BYTES)
|
|
{
|
|
unlockWriteGate(audio);
|
|
return false;
|
|
}
|
|
|
|
uint32_t flags = 0;
|
|
uint64_t position = 0;
|
|
int64_t timeNs = 0;
|
|
uint64_t rateQ32 = 0;
|
|
if (sourceClock)
|
|
{
|
|
if (!rateToQ32(sourceClock->rate, true, &rateQ32))
|
|
{
|
|
unlockWriteGate(audio);
|
|
return false;
|
|
}
|
|
flags |= KVMFR_AUDIO_MESSAGE_CLOCK_VALID;
|
|
position = sourceClock->position;
|
|
timeNs = sourceClock->time;
|
|
if (sourceClock->stable)
|
|
flags |= KVMFR_AUDIO_MESSAGE_CLOCK_STABLE;
|
|
if (sourceClock->discontinuity)
|
|
flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY;
|
|
}
|
|
if (atomic_exchange_explicit(
|
|
&audio->captureDiscontinuity, false, memory_order_acq_rel))
|
|
flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY;
|
|
|
|
const enum WriteResult result = writeMessageLocked(audio,
|
|
KVMFR_AUDIO_MESSAGE_CAPTURE_DATA,
|
|
audio->writeCaptureGeneration, flags, position, timeNs, rateQ32,
|
|
(uint32_t)frames, data, (uint32_t)bytes);
|
|
if (result != WRITE_OK)
|
|
{
|
|
if (result == WRITE_FULL)
|
|
++audio->clientSequence;
|
|
atomic_store_explicit(
|
|
&audio->captureDiscontinuity, true, memory_order_release);
|
|
}
|
|
unlockWriteGate(audio);
|
|
return result == WRITE_OK;
|
|
}
|
|
|
|
static bool clockFeedback(void * opaque, uint32_t generation,
|
|
const LG_AudioClock * playbackClock, double targetRate)
|
|
{
|
|
LGMPAudio * audio = opaque;
|
|
uint64_t targetRateQ32;
|
|
uint64_t rateQ32;
|
|
if (!generation || !playbackClock ||
|
|
!rateToQ32(targetRate, false, &targetRateQ32) ||
|
|
!rateToQ32(playbackClock->rate, true, &rateQ32))
|
|
return false;
|
|
|
|
if (!tryWriteGate(audio))
|
|
{
|
|
atomic_store_explicit(
|
|
&audio->captureDiscontinuity, true, memory_order_release);
|
|
return false;
|
|
}
|
|
LG_LOCK(audio->lock);
|
|
const bool supported = audio->phase == AUDIO_PHASE_BOUND &&
|
|
!audio->releaseRequested && audio->statusAvailable &&
|
|
(audio->remoteStatus.capabilities &
|
|
KVMFR_AUDIO_CAP_CLOCK_FEEDBACK) != 0 &&
|
|
audio->playback.active &&
|
|
audio->playback.generation.local == generation;
|
|
LG_UNLOCK(audio->lock);
|
|
if (!supported)
|
|
{
|
|
unlockWriteGate(audio);
|
|
return false;
|
|
}
|
|
if (!audio->publicDataAllowed || !audio->writeBound ||
|
|
generation != audio->writePlaybackLocalGeneration ||
|
|
!audio->writePlaybackGeneration)
|
|
{
|
|
unlockWriteGate(audio);
|
|
return false;
|
|
}
|
|
|
|
uint32_t flags = KVMFR_AUDIO_MESSAGE_CLOCK_VALID;
|
|
if (playbackClock->stable)
|
|
flags |= KVMFR_AUDIO_MESSAGE_CLOCK_STABLE;
|
|
if (playbackClock->discontinuity)
|
|
flags |= KVMFR_AUDIO_MESSAGE_DISCONTINUITY;
|
|
const KVMFRAudioClockFeedback feedback =
|
|
{
|
|
.targetRateQ32 = targetRateQ32,
|
|
};
|
|
const enum WriteResult result = writeMessageLocked(audio,
|
|
KVMFR_AUDIO_MESSAGE_CLOCK_FEEDBACK,
|
|
audio->writePlaybackGeneration, flags,
|
|
playbackClock->position, playbackClock->time, rateQ32, 0,
|
|
&feedback, sizeof(feedback));
|
|
if (result != WRITE_OK)
|
|
{
|
|
if (result == WRITE_FULL)
|
|
++audio->clientSequence;
|
|
atomic_store_explicit(
|
|
&audio->captureDiscontinuity, true, memory_order_release);
|
|
}
|
|
unlockWriteGate(audio);
|
|
return result == WRITE_OK;
|
|
}
|
|
|
|
static bool clockFeedbackAvailable(void * opaque)
|
|
{
|
|
LGMPAudio * audio = opaque;
|
|
return atomic_load_explicit(
|
|
&audio->clockFeedbackAvailable, memory_order_acquire);
|
|
}
|
|
|
|
static const LG_AudioOps l_audioOps =
|
|
{
|
|
.name = "LGMP",
|
|
.playbackArrivalReserveUs = AUDIO_ACTIVE_POLL_US,
|
|
.setStatusListener = setStatusListener,
|
|
.attach = attach,
|
|
.detach = detach,
|
|
.recordData = recordData,
|
|
.clockFeedback = clockFeedback,
|
|
.clockFeedbackAvailable = clockFeedbackAvailable,
|
|
};
|
|
|
|
const LG_AudioOps * lgmpAudio_getOps(void)
|
|
{
|
|
return &l_audioOps;
|
|
}
|