From a43227cba60ec58ec7617d58c74d6375876203d6 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 30 Sep 2026 23:22:53 +1000 Subject: [PATCH] [client/idd] network: add bidirectional audio --- client/transports/Network/CMakeLists.txt | 1 + client/transports/Network/audio.c | 2248 ++++++++++++++ client/transports/Network/audio.h | 46 + client/transports/Network/network.c | 40 +- idd/LGIdd/LGIdd.vcxproj | 2 + idd/LGIdd/LGIdd.vcxproj.filters | 6 + idd/LGIdd/transport/network/CNetworkAudio.cpp | 2701 +++++++++++++++++ idd/LGIdd/transport/network/CNetworkAudio.h | 102 + .../transport/network/CNetworkServer.cpp | 201 +- idd/LGIdd/transport/network/CNetworkServer.h | 14 +- .../transport/network/CNetworkTransport.cpp | 15 +- .../transport/network/CNetworkTransport.h | 3 + 12 files changed, 5344 insertions(+), 35 deletions(-) create mode 100644 client/transports/Network/audio.c create mode 100644 client/transports/Network/audio.h create mode 100644 idd/LGIdd/transport/network/CNetworkAudio.cpp create mode 100644 idd/LGIdd/transport/network/CNetworkAudio.h diff --git a/client/transports/Network/CMakeLists.txt b/client/transports/Network/CMakeLists.txt index 2d004be5..d3f67016 100644 --- a/client/transports/Network/CMakeLists.txt +++ b/client/transports/Network/CMakeLists.txt @@ -6,6 +6,7 @@ if(NOT TARGET msquic::msquic) endif() set(TRANSPORT_NETWORK_SOURCES + audio.c connection.c control.c cursor.c diff --git a/client/transports/Network/audio.c b/client/transports/Network/audio.c new file mode 100644 index 00000000..9365a2e2 --- /dev/null +++ b/client/transports/Network/audio.c @@ -0,0 +1,2248 @@ +/** + * 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/event.h" +#include "common/locking.h" +#include "common/time.h" + +#include + +#include +#include +#include +#include +#include + +#define NETWORK_AUDIO_TIMER_MS 10U +#define NETWORK_AUDIO_SUBSCRIBE_RETRY_MS 500U +#define NETWORK_AUDIO_KEEPALIVE_MS 500U +#define NETWORK_AUDIO_RENEWAL_MS 1000U +#define NETWORK_AUDIO_RENEWAL_RETRY_MS 100U +#define NETWORK_AUDIO_RENEWAL_TIMEOUT_MS 3000U +#define NETWORK_AUDIO_TARGET_LATENCY_US 2000U +#define NETWORK_AUDIO_MAX_PACKET_FRAMES 2048U +#define NETWORK_AUDIO_MAX_PCM_BYTES 65536U +#define NETWORK_AUDIO_MAX_SAMPLE_RATE 384000U +#define NETWORK_AUDIO_MAX_CHANNELS 8U +#define NETWORK_AUDIO_CHANNEL_MASK UINT32_C(0x0003FFFF) +#define NETWORK_AUDIO_PLAYBACK_STREAM_ID 1U +#define NETWORK_AUDIO_CAPTURE_STREAM_ID 2U +#define NETWORK_AUDIO_CLOCK_CACHE_SIZE 16U +#define NETWORK_AUDIO_DATAGRAM_PAYLOAD 1136U +#define NETWORK_AUDIO_DATAGRAM_PCM_BYTES \ + (NETWORK_AUDIO_DATAGRAM_PAYLOAD - LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE) + +#define NETWORK_AUDIO_SUBSCRIBE_FLAGS \ + (LG_NET_AUDIO_SUBSCRIBE_LOW_LATENCY | \ + LG_NET_AUDIO_SUBSCRIBE_EXCLUSIVE_CAPTURE | \ + LG_NET_AUDIO_SUBSCRIBE_CLOCK_FEEDBACK | \ + LG_NET_AUDIO_SUBSCRIBE_DATAGRAM_PCM | \ + LG_NET_AUDIO_SUBSCRIBE_RELIABLE_PCM) + +#define NETWORK_AUDIO_DIRECTIONS \ + (LG_NET_AUDIO_DIRECTIONS_PLAYBACK | LG_NET_AUDIO_DIRECTIONS_CAPTURE) + +#define NETWORK_AUDIO_SEMANTIC_FLAGS \ + (LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL | \ + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_ERROR | \ + LG_NET_ENVELOPE_EARLY_DATA) + +enum NetworkAudioPhase +{ + NETWORK_AUDIO_DISCONNECTED, + NETWORK_AUDIO_SUBSCRIBING, + NETWORK_AUDIO_GRANTED, + NETWORK_AUDIO_WAIT_BARRIER, + NETWORK_AUDIO_READY, + NETWORK_AUDIO_RELEASING, +}; + +struct NetworkAudioClockEntry +{ + bool valid; + LGNetAudioClockState state; +}; + +struct NetworkAudioStream +{ + uint32_t streamID; + LGNetAudioDirection direction; + + LGNetAudioFormat wireFormat; + LG_AudioFormat format; + uint32_t generation; + bool formatValid; + bool active; + + uint64_t stateSequence; + bool volumeValid; + uint8_t volumeChannels; + uint16_t volume[LG_AUDIO_MAX_CHANNELS]; + bool muteValid; + bool mute; + bool masterMute; + uint32_t mutedChannels; + + bool barrierPending; + bool barrierReady; + uint64_t barrierRequestID; + LGNetAudioBarrier barrier; + + bool packetKnown; + uint64_t lastPacketID; + bool discontinuity; + unsigned clockInsert; + struct NetworkAudioClockEntry clocks[NETWORK_AUDIO_CLOCK_CACHE_SIZE]; +}; + +struct NetworkAudio +{ + NetworkConnection * connection; + LGTimer * timer; + LGEvent * statusIdle; + LGEvent * eventIdle; + LG_Lock stateLock; + LG_Lock eventLock; + + bool destroying; + bool connected; + bool serviceAvailable; + bool wanted; + uint16_t serviceVersion; + enum NetworkAudioPhase phase; + + uint64_t subscriberID; + uint64_t requestSerial; + uint64_t subscribeRequestID; + uint64_t grantRequestID; + uint64_t subscriptionEpoch; + uint64_t lastSubscriptionEpoch; + uint64_t controlSequence; + uint64_t capturePacketID; + uint64_t captureFramePosition; + uint64_t nextSubscribeNs; + uint64_t nextKeepaliveNs; + uint64_t nextRenewalNs; + uint64_t renewalDeadlineNs; + + LGNetAudioRoles requestedRoles; + LGNetAudioRoles grantedRoles; + uint32_t targetLatencyUs; + uint32_t maxPacketFrames; + uint32_t generationSerial; + uint32_t endpointGeneration; + bool statusAvailable; + bool statusNotifyPending; + bool statusDispatching; + bool eventDispatching; + + struct NetworkAudioStream playback; + struct NetworkAudioStream capture; + atomic_bool feedbackAvailable; + + const LG_AudioEventOps * events; + void * eventOpaque; + bool attached; + + LG_AudioStatusFn statusCallback; + void * statusOpaque; +}; + +static _Thread_local NetworkAudio * l_networkAudioStatusCallback; +static const uint8_t l_networkAudioSilence + [NETWORK_AUDIO_MAX_PCM_BYTES]; + +static uint64_t networkAudioNext64(uint64_t value) +{ + ++value; + return value ? value : 1U; +} + +static uint32_t networkAudioNext32(uint32_t value) +{ + ++value; + return value ? value : 1U; +} + +static bool networkAudioVersionSupported(uint16_t version) +{ + return version >= LG_NET_AUDIO_OWNERSHIP_INTRODUCED_SERVICE_VERSION && + version <= LG_NET_AUDIO_VERSION_CURRENT; +} + +static uint64_t networkAudioDeadline(uint32_t milliseconds) +{ + return nanotime() + (uint64_t)milliseconds * UINT64_C(1000000); +} + +static unsigned networkAudioBitCount(uint32_t value) +{ + unsigned count = 0; + while (value) + { + count += value & 1U; + value >>= 1; + } + return count; +} + +static unsigned networkAudioSampleSize(LGNetAudioSampleFormat format) +{ + switch (format) + { + case LG_NET_AUDIO_SAMPLE_S16_LE: + return 2U; + + case LG_NET_AUDIO_SAMPLE_S24_LE: + return 3U; + + case LG_NET_AUDIO_SAMPLE_S32_LE: + case LG_NET_AUDIO_SAMPLE_F32_LE: + return 4U; + } + + return 0; +} + +static LG_AudioChannel networkAudioChannel(unsigned bit) +{ + static const LG_AudioChannel 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 < sizeof(channels) / sizeof(channels[0]) ? + channels[bit] : LG_AUDIO_CH_UNKNOWN; +} + +static bool networkAudioFormat(const LGNetAudioFormat * wire, + uint32_t grantedFrames, LG_AudioFormat * format, size_t * frameSize) +{ + if (!wire || !format || !frameSize || !lgNetAudioFormatValid(wire) || + wire->sampleRate > NETWORK_AUDIO_MAX_SAMPLE_RATE || + wire->channels > NETWORK_AUDIO_MAX_CHANNELS || + !wire->channelMask || + (wire->channelMask & ~NETWORK_AUDIO_CHANNEL_MASK) || + networkAudioBitCount(wire->channelMask) != wire->channels || + wire->framesPerPacket > NETWORK_AUDIO_MAX_PACKET_FRAMES || + wire->maxPacketFrames > NETWORK_AUDIO_MAX_PACKET_FRAMES || + wire->maxPacketFrames > grantedFrames) + return false; + + LG_AudioSampleFormat sampleFormat; + const unsigned sampleSize = networkAudioSampleSize( + wire->sampleFormat); + switch (wire->sampleFormat) + { + case LG_NET_AUDIO_SAMPLE_S16_LE: + sampleFormat = LG_AUDIO_FMT_S16_LE; + break; + + case LG_NET_AUDIO_SAMPLE_S24_LE: + sampleFormat = LG_AUDIO_FMT_S24_LE; + break; + + case LG_NET_AUDIO_SAMPLE_S32_LE: + sampleFormat = LG_AUDIO_FMT_S32_LE; + break; + + case LG_NET_AUDIO_SAMPLE_F32_LE: + sampleFormat = LG_AUDIO_FMT_F32_LE; + break; + + default: + return false; + } + + const size_t bytes = (size_t)sampleSize * wire->channels; + if (!sampleSize || !bytes || + wire->maxPacketFrames > NETWORK_AUDIO_MAX_PCM_BYTES / bytes) + return false; + + *format = (LG_AudioFormat) + { + .sampleFormat = sampleFormat, + .sampleRate = wire->sampleRate, + .channelCount = (uint8_t)wire->channels, + }; + unsigned channel = 0; + for (unsigned bit = 0; bit < 18U; ++bit) + if (wire->channelMask & (UINT32_C(1) << bit)) + format->channels[channel++] = networkAudioChannel(bit); + + *frameSize = bytes; + return true; +} + +static uint16_t networkAudioVolumeLevel(int32_t millibels) +{ + if (millibels <= LG_NET_AUDIO_VOLUME_MIN_MILLIBELS) + return 0; + if (millibels >= 0) + return UINT16_MAX; + + const double linear = pow(10.0, (double)millibels / 2000.0); + const double raw = log((linear + 0.000172787) / 9.3234e-7) / + log(1.000211902); + if (!isfinite(raw) || raw <= 0.0) + return 0; + if (raw >= UINT16_MAX) + return UINT16_MAX; + return (uint16_t)(raw + 0.5); +} + +static bool networkAudioRateQ32(double rate, bool allowZero, + uint64_t * result) +{ + if (!result || !isfinite(rate) || rate < 0.0 || + (!allowZero && rate == 0.0) || + rate > NETWORK_AUDIO_MAX_SAMPLE_RATE) + return false; + + const long double scaled = (long double)rate * 4294967296.0L; + if (scaled > (long double)LG_NET_AUDIO_MAX_RATE_Q32) + return false; + + *result = (uint64_t)(scaled + 0.5L); + return allowZero || *result != 0; +} + +static double networkAudioRate(uint64_t rateQ32) +{ + return (double)rateQ32 / 4294967296.0; +} + +static LG_AudioStatus networkAudioStatusNL(const NetworkAudio * audio) +{ + return (LG_AudioStatus) + { + .available = audio->statusAvailable, + .generation = audio->endpointGeneration, + }; +} + +static void networkAudioSetAvailableNL(NetworkAudio * audio, + bool available, bool restart) +{ + if (!restart && audio->statusAvailable == available) + return; + + audio->statusAvailable = available; + audio->endpointGeneration = + networkAudioNext32(audio->endpointGeneration); + audio->statusNotifyPending = true; +} + +static void networkAudioDispatchStatus(NetworkAudio * audio, + LG_AudioStatusFn callback, void * opaque, LG_AudioStatus status) +{ + while (callback) + { + NetworkAudio * previous = l_networkAudioStatusCallback; + l_networkAudioStatusCallback = audio; + callback(opaque, &status); + l_networkAudioStatusCallback = previous; + + LG_LOCK(audio->stateLock); + if (audio->statusNotifyPending && audio->statusCallback) + { + audio->statusNotifyPending = false; + callback = audio->statusCallback; + opaque = audio->statusOpaque; + status = networkAudioStatusNL(audio); + LG_UNLOCK(audio->stateLock); + continue; + } + + audio->statusDispatching = false; + lgSignalEvent(audio->statusIdle); + LG_UNLOCK(audio->stateLock); + break; + } +} + +static void networkAudioNotifyStatus(NetworkAudio * audio) +{ + LG_AudioStatusFn callback; + void * opaque; + LG_AudioStatus status; + + LG_LOCK(audio->stateLock); + if (!audio->statusNotifyPending || audio->statusDispatching) + { + LG_UNLOCK(audio->stateLock); + return; + } + + audio->statusNotifyPending = false; + callback = audio->statusCallback; + opaque = audio->statusOpaque; + status = networkAudioStatusNL(audio); + if (!callback) + { + LG_UNLOCK(audio->stateLock); + return; + } + + audio->statusDispatching = true; + lgResetEvent(audio->statusIdle); + LG_UNLOCK(audio->stateLock); + networkAudioDispatchStatus(audio, callback, opaque, status); +} + +static void networkAudioBeginEvents(NetworkAudio * audio) +{ + for (;;) + { + LG_LOCK(audio->eventLock); + if (!audio->eventDispatching) + { + audio->eventDispatching = true; + lgResetEvent(audio->eventIdle); + return; + } + + LG_UNLOCK(audio->eventLock); + lgWaitEvent(audio->eventIdle, TIMEOUT_INFINITE); + } +} + +static void networkAudioAbortEventsNL(NetworkAudio * audio) +{ + audio->eventDispatching = false; + lgSignalEvent(audio->eventIdle); + LG_UNLOCK(audio->eventLock); +} + +static void networkAudioCallEventsNL(NetworkAudio * audio) +{ + LG_UNLOCK(audio->eventLock); +} + +static void networkAudioEndEvents(NetworkAudio * audio) +{ + LG_LOCK(audio->eventLock); + audio->eventDispatching = false; + lgSignalEvent(audio->eventIdle); + LG_UNLOCK(audio->eventLock); +} + +static void networkAudioClearClockNL(struct NetworkAudioStream * stream) +{ + stream->clockInsert = 0; + memset(stream->clocks, 0, sizeof(stream->clocks)); +} + +static void networkAudioClearStreamNL(struct NetworkAudioStream * stream) +{ + const uint32_t streamID = stream->streamID; + const LGNetAudioDirection direction = stream->direction; + memset(stream, 0, sizeof(*stream)); + stream->streamID = streamID; + stream->direction = direction; +} + +static bool networkAudioStreamRequiredNL(const NetworkAudio * audio, + const struct NetworkAudioStream * stream) +{ + const LGNetAudioRoles role = stream->direction == + LG_NET_AUDIO_DIRECTION_PLAYBACK ? + LG_NET_AUDIO_ROLE_PLAYBACK : LG_NET_AUDIO_ROLE_CAPTURE; + return (audio->grantedRoles & role) != 0; +} + +static void networkAudioUpdateReadyNL(NetworkAudio * audio) +{ + const bool granted = audio->phase == NETWORK_AUDIO_GRANTED || + audio->phase == NETWORK_AUDIO_WAIT_BARRIER || + audio->phase == NETWORK_AUDIO_READY; + const bool playbackReady = + !networkAudioStreamRequiredNL(audio, &audio->playback) || + audio->playback.barrierReady; + const bool captureReady = + !networkAudioStreamRequiredNL(audio, &audio->capture) || + audio->capture.barrierReady; + + if (granted) + audio->phase = playbackReady && captureReady ? + NETWORK_AUDIO_READY : NETWORK_AUDIO_WAIT_BARRIER; + + const bool feedback = audio->phase == NETWORK_AUDIO_READY && + audio->playback.barrierReady && audio->playback.active && + audio->playback.packetKnown && + (audio->grantedRoles & LG_NET_AUDIO_ROLE_CLOCK_FEEDBACK); + atomic_store_explicit( + &audio->feedbackAvailable, feedback, memory_order_release); +} + +static uint32_t networkAudioMapGenerationNL(NetworkAudio * audio, + struct NetworkAudioStream * stream, uint64_t formatEpoch) +{ + if (stream->formatValid && + stream->wireFormat.formatEpoch == formatEpoch) + return stream->generation; + + audio->generationSerial = networkAudioNext32(audio->generationSerial); + return audio->generationSerial; +} + +static void networkAudioInvalidateBarrierNL(NetworkAudio * audio, + struct NetworkAudioStream * stream) +{ + stream->barrierPending = false; + stream->barrierReady = false; + stream->barrierRequestID = 0; + memset(&stream->barrier, 0, sizeof(stream->barrier)); + networkAudioUpdateReadyNL(audio); +} + +static bool networkAudioSubscriptionCurrentNL(const NetworkAudio * audio, + const LGNetPacketView * packet) +{ + return audio->connected && audio->serviceAvailable && + audio->subscriptionEpoch && + lgNetAudioEnvelopeMatchesSubscription( + &packet->envelope, audio->subscriptionEpoch); +} + +static void networkAudioStopStreams(NetworkAudio * audio) +{ + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + const uint32_t playbackGeneration = audio->playback.active ? + audio->playback.generation : 0; + const uint32_t captureGeneration = audio->capture.active ? + audio->capture.generation : 0; + networkAudioClearStreamNL(&audio->playback); + networkAudioClearStreamNL(&audio->capture); + atomic_store_explicit( + &audio->feedbackAvailable, false, memory_order_release); + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events && playbackGeneration && events->playbackStop) + events->playbackStop(opaque, playbackGeneration); + if (events && captureGeneration && events->recordStop) + events->recordStop(opaque, captureGeneration); + networkAudioEndEvents(audio); +} + +static bool networkAudioReliableFlags(const LGNetPacketView * packet, + LGNetEnvelopeFlags semantic) +{ + return packet && + (packet->envelope.flags & + (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM)) == + LG_NET_ENVELOPE_RELIABLE && + (packet->envelope.flags & NETWORK_AUDIO_SEMANTIC_FLAGS) == semantic; +} + +static bool networkAudioDatagramFlags(const LGNetPacketView * packet) +{ + return packet && + (packet->envelope.flags & + (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM)) == + LG_NET_ENVELOPE_DATAGRAM && + (packet->envelope.flags & NETWORK_AUDIO_SEMANTIC_FLAGS) == + LG_NET_ENVELOPE_FINAL; +} + +static LG_TransportStatus networkAudioSendReliableNL(NetworkAudio * audio, + uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t requestID, + const void * payload, uint32_t payloadLength) +{ + return networkConnectionSend(audio->connection, LG_NET_SERVICE_AUDIO, + messageType, audio->serviceVersion, flags | LG_NET_ENVELOPE_FINAL, + audio->subscriptionEpoch, requestID, payload, payloadLength); +} + +static LG_TransportStatus networkAudioSendDatagramNL(NetworkAudio * audio, + uint16_t messageType, uint64_t requestID, + const void * payload, uint32_t payloadLength) +{ + return networkConnectionSendDatagram(audio->connection, + LG_NET_SERVICE_AUDIO, messageType, audio->serviceVersion, 0, + audio->subscriptionEpoch, requestID, payload, payloadLength, false); +} + +static bool networkAudioSendSubscribeNL(NetworkAudio * audio) +{ + if (!audio->connected || !audio->serviceAvailable || !audio->wanted || + audio->destroying) + return false; + + const LGNetAudioSubscribe subscribe = + { + .subscriberID = audio->subscriberID, + .directions = NETWORK_AUDIO_DIRECTIONS, + .targetLatencyUs = NETWORK_AUDIO_TARGET_LATENCY_US, + .maxPacketFrames = NETWORK_AUDIO_MAX_PACKET_FRAMES, + .flags = NETWORK_AUDIO_SUBSCRIBE_FLAGS, + }; + uint8_t payload[LG_NET_AUDIO_SUBSCRIBE_WIRE_SIZE]; + if (!lgNetAudioSubscribeEncodeForVersion(payload, sizeof(payload), + &subscribe, audio->serviceVersion)) + return false; + + if (!audio->subscribeRequestID) + { + audio->requestSerial = networkAudioNext64(audio->requestSerial); + audio->subscribeRequestID = audio->requestSerial; + } + + const LG_TransportStatus status = networkConnectionSend( + audio->connection, LG_NET_SERVICE_AUDIO, + LG_NET_AUDIO_MESSAGE_SUBSCRIBE, audio->serviceVersion, + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_FINAL, + 0, audio->subscribeRequestID, payload, sizeof(payload)); + audio->phase = NETWORK_AUDIO_SUBSCRIBING; + audio->nextSubscribeNs = networkAudioDeadline( + NETWORK_AUDIO_SUBSCRIBE_RETRY_MS); + return status == LG_TRANSPORT_OK; +} + +static bool networkAudioSendRenewalNL(NetworkAudio * audio) +{ + if (!audio->subscriptionEpoch || !audio->grantRequestID) + return false; + + const LGNetAudioSubscribe subscribe = + { + .subscriberID = audio->subscriberID, + .directions = NETWORK_AUDIO_DIRECTIONS, + .targetLatencyUs = NETWORK_AUDIO_TARGET_LATENCY_US, + .maxPacketFrames = NETWORK_AUDIO_MAX_PACKET_FRAMES, + .flags = NETWORK_AUDIO_SUBSCRIBE_FLAGS, + }; + uint8_t payload[LG_NET_AUDIO_SUBSCRIBE_WIRE_SIZE]; + if (!lgNetAudioSubscribeEncodeForVersion(payload, sizeof(payload), + &subscribe, audio->serviceVersion)) + return false; + + const LG_TransportStatus status = networkConnectionSend( + audio->connection, LG_NET_SERVICE_AUDIO, + LG_NET_AUDIO_MESSAGE_SUBSCRIBE, audio->serviceVersion, + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_FINAL, + 0, audio->grantRequestID, payload, sizeof(payload)); + audio->nextRenewalNs = networkAudioDeadline( + status == LG_TRANSPORT_OK ? NETWORK_AUDIO_RENEWAL_MS : + NETWORK_AUDIO_RENEWAL_RETRY_MS); + return status == LG_TRANSPORT_OK; +} + +static bool networkAudioSendBarrierAckNL(NetworkAudio * audio, + struct NetworkAudioStream * stream) +{ + if (!stream->barrierPending || !audio->subscriptionEpoch) + return true; + + uint8_t payload[LG_NET_AUDIO_BARRIER_WIRE_SIZE]; + if (!lgNetAudioBarrierEncode(payload, sizeof(payload), &stream->barrier)) + return false; + + const LG_TransportStatus status = networkAudioSendReliableNL(audio, + LG_NET_AUDIO_MESSAGE_STATE_ACK, LG_NET_ENVELOPE_RESPONSE, + stream->barrierRequestID, payload, sizeof(payload)); + if (status != LG_TRANSPORT_OK) + return false; + + stream->barrierPending = false; + stream->barrierReady = true; + stream->barrierRequestID = 0; + networkAudioUpdateReadyNL(audio); + return true; +} + +static bool networkAudioSendControlNL(NetworkAudio * audio, + uint16_t messageType, LGNetAudioControlFlags flags) +{ + const bool playback = + (audio->grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK) != 0; + const struct NetworkAudioStream * stream = playback ? + &audio->playback : &audio->capture; + audio->controlSequence = networkAudioNext64(audio->controlSequence); + const LGNetAudioControl control = + { + .streamID = stream->streamID, + .direction = stream->direction, + .flags = flags, + .sequence = audio->controlSequence, + .formatEpoch = stream->formatValid ? + stream->wireFormat.formatEpoch : 0, + .detail = 0, + }; + uint8_t payload[LG_NET_AUDIO_CONTROL_WIRE_SIZE]; + if (!lgNetAudioControlEncode(payload, sizeof(payload), &control)) + return false; + + return networkAudioSendReliableNL(audio, messageType, 0, 0, + payload, sizeof(payload)) == LG_TRANSPORT_OK; +} + +static void networkAudioClearSubscriptionNL(NetworkAudio * audio, + bool disconnected) +{ + audio->phase = NETWORK_AUDIO_DISCONNECTED; + audio->subscribeRequestID = 0; + audio->grantRequestID = 0; + audio->subscriptionEpoch = 0; + audio->controlSequence = 0; + audio->capturePacketID = 0; + audio->captureFramePosition = 0; + audio->nextSubscribeNs = 0; + audio->nextKeepaliveNs = 0; + audio->nextRenewalNs = 0; + audio->renewalDeadlineNs = 0; + audio->grantedRoles = 0; + audio->targetLatencyUs = 0; + audio->maxPacketFrames = 0; + if (disconnected) + audio->lastSubscriptionEpoch = 0; + networkAudioSetAvailableNL(audio, false, false); + atomic_store_explicit( + &audio->feedbackAvailable, false, memory_order_release); +} + +void networkAudioProcess(NetworkAudio * audio) +{ + if (!audio) + return; + + bool stopStreams = false; + LG_LOCK(audio->stateLock); + if (audio->destroying) + { + LG_UNLOCK(audio->stateLock); + return; + } + + const uint64_t now = nanotime(); + if (!audio->connected || !audio->serviceAvailable) + { + LG_UNLOCK(audio->stateLock); + return; + } + + if (!audio->wanted) + { + if (audio->phase == NETWORK_AUDIO_SUBSCRIBING) + networkAudioClearSubscriptionNL(audio, false); + else if (audio->subscriptionEpoch && + audio->phase != NETWORK_AUDIO_RELEASING) + audio->phase = NETWORK_AUDIO_RELEASING; + + if (audio->phase == NETWORK_AUDIO_RELEASING && + networkAudioSendControlNL(audio, LG_NET_AUDIO_MESSAGE_RELEASE, + LG_NET_AUDIO_CONTROL_GRACEFUL | LG_NET_AUDIO_CONTROL_FLUSH)) + { + networkAudioClearSubscriptionNL(audio, false); + stopStreams = true; + } + LG_UNLOCK(audio->stateLock); + if (stopStreams) + networkAudioStopStreams(audio); + return; + } + + if (audio->subscriptionEpoch && audio->renewalDeadlineNs && + now >= audio->renewalDeadlineNs) + { + networkAudioClearSubscriptionNL(audio, false); + LG_UNLOCK(audio->stateLock); + networkAudioStopStreams(audio); + return; + } + + if (audio->phase == NETWORK_AUDIO_DISCONNECTED || + (audio->phase == NETWORK_AUDIO_SUBSCRIBING && + now >= audio->nextSubscribeNs)) + networkAudioSendSubscribeNL(audio); + + if (audio->subscriptionEpoch && + (audio->phase == NETWORK_AUDIO_GRANTED || + audio->phase == NETWORK_AUDIO_WAIT_BARRIER || + audio->phase == NETWORK_AUDIO_READY)) + { + networkAudioSendBarrierAckNL(audio, &audio->playback); + networkAudioSendBarrierAckNL(audio, &audio->capture); + if (now >= audio->nextRenewalNs) + networkAudioSendRenewalNL(audio); + if (now >= audio->nextKeepaliveNs) + { + if (networkAudioSendControlNL(audio, + LG_NET_AUDIO_MESSAGE_KEEPALIVE, 0)) + audio->nextKeepaliveNs = networkAudioDeadline( + NETWORK_AUDIO_KEEPALIVE_MS); + } + } + LG_UNLOCK(audio->stateLock); +} + +void networkAudioSendReady(NetworkAudio * audio, bool datagram) +{ + (void)datagram; + networkAudioProcess(audio); +} + +static bool networkAudioTimer(void * opaque) +{ + NetworkAudio * audio = opaque; + LG_LOCK(audio->stateLock); + const bool destroying = audio->destroying; + LG_UNLOCK(audio->stateLock); + if (destroying) + return false; + + networkAudioProcess(audio); + networkAudioNotifyStatus(audio); + return true; +} + +static void networkAudioSetStatusListener(void * opaque, + LG_AudioStatusFn callback, void * callbackOpaque) +{ + NetworkAudio * audio = opaque; + const bool reentrant = l_networkAudioStatusCallback == audio; + + LG_LOCK(audio->stateLock); + audio->statusCallback = NULL; + audio->statusOpaque = NULL; + audio->statusNotifyPending = false; + + bool dispatching = audio->statusDispatching; + LG_UNLOCK(audio->stateLock); + + while (dispatching && !reentrant) + { + lgWaitEvent(audio->statusIdle, TIMEOUT_INFINITE); + LG_LOCK(audio->stateLock); + dispatching = audio->statusDispatching; + LG_UNLOCK(audio->stateLock); + } + + if (!callback) + { + LG_LOCK(audio->stateLock); + audio->wanted = false; + LG_UNLOCK(audio->stateLock); + networkAudioProcess(audio); + return; + } + + LG_LOCK(audio->stateLock); + audio->statusCallback = callback; + audio->statusOpaque = callbackOpaque; + audio->statusNotifyPending = false; + audio->wanted = true; + + const LG_AudioStatus status = networkAudioStatusNL(audio); + const bool startDispatch = !audio->statusDispatching; + if (startDispatch) + { + audio->statusDispatching = true; + lgResetEvent(audio->statusIdle); + } + LG_UNLOCK(audio->stateLock); + + if (startDispatch) + networkAudioDispatchStatus(audio, callback, callbackOpaque, status); + else + { + NetworkAudio * previous = l_networkAudioStatusCallback; + l_networkAudioStatusCallback = audio; + callback(callbackOpaque, &status); + l_networkAudioStatusCallback = previous; + } + networkAudioProcess(audio); +} + +static bool networkAudioAttach(void * opaque, + const LG_AudioEventOps * events, void * eventOpaque) +{ + NetworkAudio * audio = opaque; + if (!events) + return false; + + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + const bool admitted = audio->statusAvailable && !audio->attached && + audio->subscriptionEpoch && + audio->phase != NETWORK_AUDIO_RELEASING; + if (admitted) + { + audio->events = events; + audio->eventOpaque = eventOpaque; + audio->attached = true; + } + const struct NetworkAudioStream playback = audio->playback; + const struct NetworkAudioStream capture = audio->capture; + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (admitted && playback.active && events->playbackStart) + { + events->playbackStart(eventOpaque, playback.generation, + &playback.format, NULL); + if (playback.volumeValid && events->playbackVolume) + events->playbackVolume(eventOpaque, playback.generation, + playback.volumeChannels, playback.volume); + if (playback.muteValid && events->playbackMute) + events->playbackMute(eventOpaque, playback.generation, + playback.mute); + } + if (admitted && capture.active && events->recordStart) + { + events->recordStart(eventOpaque, capture.generation, + &capture.format); + if (capture.volumeValid && events->recordVolume) + events->recordVolume(eventOpaque, capture.generation, + capture.volumeChannels, capture.volume); + if (capture.muteValid && events->recordMute) + events->recordMute(eventOpaque, capture.generation, capture.mute); + } + networkAudioEndEvents(audio); + return admitted; +} + +static void networkAudioDetach(void * opaque) +{ + NetworkAudio * audio = opaque; + LG_LOCK(audio->eventLock); + LG_LOCK(audio->stateLock); + audio->events = NULL; + audio->eventOpaque = NULL; + audio->attached = false; + LG_UNLOCK(audio->stateLock); + + while (audio->eventDispatching) + { + LG_UNLOCK(audio->eventLock); + lgWaitEvent(audio->eventIdle, TIMEOUT_INFINITE); + LG_LOCK(audio->eventLock); + } + LG_UNLOCK(audio->eventLock); +} + +static bool networkAudioSendClockAndDataNL(NetworkAudio * audio, + const uint8_t * clockPayload, const uint8_t * dataPayload, + uint32_t dataPayloadLength) +{ + /* CLOCK_STATE and PCM are an ordered pair. Keep both on the same reliable + * stream until a future protocol version provides atomic datagram framing. */ + if (!(audio->grantedRoles & LG_NET_AUDIO_ROLE_RELIABLE_PCM)) + return false; + + return networkAudioSendReliableNL(audio, + LG_NET_AUDIO_MESSAGE_CLOCK_STATE, 0, 0, clockPayload, + LG_NET_AUDIO_CLOCK_STATE_WIRE_SIZE) == LG_TRANSPORT_OK && + networkAudioSendReliableNL(audio, + LG_NET_AUDIO_MESSAGE_CAPTURE_DATA, 0, 0, dataPayload, + dataPayloadLength) == LG_TRANSPORT_OK; +} + +static bool networkAudioRecordData(void * opaque, uint32_t generation, + const void * data, size_t frames, const LG_AudioClock * sourceClock) +{ + NetworkAudio * audio = opaque; + if (!generation || !data || !frames) + return false; + + uint64_t rateQ32 = 0; + if (sourceClock && + !networkAudioRateQ32(sourceClock->rate, true, &rateQ32)) + return false; + + LG_LOCK(audio->stateLock); + struct NetworkAudioStream * stream = &audio->capture; + if (audio->phase != NETWORK_AUDIO_READY || + !stream->active || !stream->formatValid || + !stream->barrierReady || stream->generation != generation || + !(audio->grantedRoles & LG_NET_AUDIO_ROLE_CAPTURE) || + !(audio->grantedRoles & LG_NET_AUDIO_ROLE_EXCLUSIVE_CAPTURE)) + { + LG_UNLOCK(audio->stateLock); + return false; + } + + const size_t frameSize = (size_t)networkAudioSampleSize( + stream->wireFormat.sampleFormat) * stream->wireFormat.channels; + if (!frameSize || frames > SIZE_MAX / frameSize) + { + LG_UNLOCK(audio->stateLock); + return false; + } + const uint64_t firstPosition = sourceClock ? + sourceClock->position : audio->captureFramePosition; + if (frames > UINT64_MAX - firstPosition) + { + LG_UNLOCK(audio->stateLock); + return false; + } + + size_t maxFrames = stream->wireFormat.maxPacketFrames; + if (maxFrames > audio->maxPacketFrames) + maxFrames = audio->maxPacketFrames; + if (maxFrames > NETWORK_AUDIO_MAX_PCM_BYTES / frameSize) + maxFrames = NETWORK_AUDIO_MAX_PCM_BYTES / frameSize; + if (audio->grantedRoles & LG_NET_AUDIO_ROLE_DATAGRAM_PCM) + { + const size_t datagramFrames = + NETWORK_AUDIO_DATAGRAM_PCM_BYTES / frameSize; + if (datagramFrames && maxFrames > datagramFrames) + maxFrames = datagramFrames; + } + if (!maxFrames) + { + LG_UNLOCK(audio->stateLock); + return false; + } + + uint8_t * payload = malloc(LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE + + maxFrames * frameSize); + if (!payload) + { + LG_UNLOCK(audio->stateLock); + return false; + } + + bool result = true; + bool discontinuity = stream->discontinuity || + (sourceClock && sourceClock->discontinuity); + size_t offset = 0; + const uint8_t * pcm = data; + while (offset < frames) + { + size_t chunkFrames = frames - offset; + if (chunkFrames > maxFrames) + chunkFrames = maxFrames; + const size_t chunkBytes = chunkFrames * frameSize; + + audio->capturePacketID = networkAudioNext64(audio->capturePacketID); + + const uint64_t packetID = audio->capturePacketID; + uint64_t position = firstPosition + offset; + int64_t timeNs = sourceClock ? sourceClock->time : 0; + if (sourceClock && offset) + { + const long double elapsed = + (long double)offset * 1000000000.0L / + stream->wireFormat.sampleRate; + const long double adjusted = (long double)timeNs + elapsed; + if (adjusted >= (long double)INT64_MIN && + adjusted <= (long double)INT64_MAX) + timeNs = (int64_t)adjusted; + } + uint64_t timestampNs = timeNs > 0 ? (uint64_t)timeNs : nanotime(); + if (!timestampNs) + timestampNs = 1; + + const LGNetAudioClockState clock = + { + .streamID = stream->streamID, + .direction = stream->direction, + .flags = sourceClock ? + LG_NET_AUDIO_CLOCK_VALID | + (sourceClock->stable ? LG_NET_AUDIO_CLOCK_STABLE : 0) : 0, + .formatEpoch = stream->wireFormat.formatEpoch, + .subscriptionEpoch = audio->subscriptionEpoch, + .packetID = packetID, + .framePosition = position, + .timeNs = timeNs, + .rateQ32 = rateQ32, + .targetRateQ32 = 0, + .queuedFrames = 0, + .driftPpm = 0, + }; + const LGNetAudioData packet = + { + .streamID = stream->streamID, + .direction = stream->direction, + .flags = discontinuity ? + LG_NET_AUDIO_DATA_DISCONTINUITY : 0, + .formatEpoch = stream->wireFormat.formatEpoch, + .packetID = packetID, + .timestampNs = timestampNs, + .startFrame = position, + .frameCount = (uint32_t)chunkFrames, + .dataLength = (uint32_t)chunkBytes, + .data = pcm + offset * frameSize, + }; + uint8_t clockPayload[LG_NET_AUDIO_CLOCK_STATE_WIRE_SIZE]; + const size_t dataPayloadLength = + LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE + chunkBytes; + if (!lgNetAudioClockStateEncode(clockPayload, + sizeof(clockPayload), &clock) || + !lgNetAudioDataEncode(payload, dataPayloadLength, &packet) || + !networkAudioSendClockAndDataNL(audio, clockPayload, payload, + (uint32_t)dataPayloadLength)) + { + result = false; + break; + } + + discontinuity = false; + offset += chunkFrames; + } + + audio->captureFramePosition = firstPosition + offset; + stream->discontinuity = !result; + free(payload); + LG_UNLOCK(audio->stateLock); + return result; +} + +static bool networkAudioClockFeedback(void * opaque, uint32_t generation, + const LG_AudioClock * playbackClock, double targetRate) +{ + NetworkAudio * audio = opaque; + uint64_t rateQ32; + uint64_t targetRateQ32; + if (!generation || !playbackClock || + !networkAudioRateQ32(playbackClock->rate, true, &rateQ32) || + !networkAudioRateQ32(targetRate, false, &targetRateQ32)) + return false; + + LG_LOCK(audio->stateLock); + const struct NetworkAudioStream * stream = &audio->playback; + if (audio->phase != NETWORK_AUDIO_READY || + !stream->active || !stream->formatValid || + !stream->barrierReady || !stream->packetKnown || + stream->generation != generation || + !(audio->grantedRoles & LG_NET_AUDIO_ROLE_CLOCK_FEEDBACK)) + { + LG_UNLOCK(audio->stateLock); + return false; + } + + const LGNetAudioClockState feedback = + { + .streamID = stream->streamID, + .direction = stream->direction, + .flags = LG_NET_AUDIO_CLOCK_VALID | + (playbackClock->stable ? LG_NET_AUDIO_CLOCK_STABLE : 0), + .formatEpoch = stream->wireFormat.formatEpoch, + .subscriptionEpoch = audio->subscriptionEpoch, + .packetID = stream->lastPacketID, + .framePosition = playbackClock->position, + .timeNs = playbackClock->time, + .rateQ32 = rateQ32, + .targetRateQ32 = targetRateQ32, + .queuedFrames = 0, + .driftPpm = 0, + }; + uint8_t payload[LG_NET_AUDIO_CLOCK_STATE_WIRE_SIZE]; + const bool encoded = lgNetAudioClockStateEncode( + payload, sizeof(payload), &feedback); + bool result = false; + if (encoded && + (audio->grantedRoles & LG_NET_AUDIO_ROLE_DATAGRAM_PCM)) + result = networkAudioSendDatagramNL(audio, + LG_NET_AUDIO_MESSAGE_CLOCK_STATE, 0, + payload, sizeof(payload)) == LG_TRANSPORT_OK; + if (encoded && !result && + (audio->grantedRoles & LG_NET_AUDIO_ROLE_RELIABLE_PCM)) + result = networkAudioSendReliableNL(audio, + LG_NET_AUDIO_MESSAGE_CLOCK_STATE, 0, 0, + payload, sizeof(payload)) == LG_TRANSPORT_OK; + LG_UNLOCK(audio->stateLock); + return result; +} + +static bool networkAudioClockFeedbackAvailable(void * opaque) +{ + NetworkAudio * audio = opaque; + return atomic_load_explicit( + &audio->feedbackAvailable, memory_order_acquire); +} + +static const LG_AudioOps NETWORK_AUDIO_OPS = +{ + .name = "Network", + .playbackFlags = 0, + .playbackArrivalReserveUs = 500U, + .setStatusListener = networkAudioSetStatusListener, + .attach = networkAudioAttach, + .detach = networkAudioDetach, + .recordData = networkAudioRecordData, + .clockFeedback = networkAudioClockFeedback, + .clockFeedbackAvailable = networkAudioClockFeedbackAvailable, +}; + +const LG_AudioOps * networkAudioGetOps(void) +{ + return &NETWORK_AUDIO_OPS; +} + +static struct NetworkAudioStream * networkAudioStreamNL( + NetworkAudio * audio, uint32_t streamID, + LGNetAudioDirection direction) +{ + if (streamID == NETWORK_AUDIO_PLAYBACK_STREAM_ID && + direction == LG_NET_AUDIO_DIRECTION_PLAYBACK) + return &audio->playback; + if (streamID == NETWORK_AUDIO_CAPTURE_STREAM_ID && + direction == LG_NET_AUDIO_DIRECTION_CAPTURE) + return &audio->capture; + return NULL; +} + +static bool networkAudioHandleGrant(NetworkAudio * audio, + const LGNetPacketView * packet) +{ + if (!networkAudioReliableFlags(packet, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL)) + return false; + + LGNetAudioSubscriptionGrant grant; + if (lgNetAudioSubscriptionGrantDecode(&grant, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + packet->envelope.componentEpoch != grant.subscriptionEpoch) + return false; + + const bool datagramAvailable = networkConnectionServiceAvailable( + audio->connection, LG_NET_SERVICE_AUDIO, + LG_NET_CAPABILITY_DATAGRAM); + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + const bool awaitingGrant = + audio->phase == NETWORK_AUDIO_SUBSCRIBING; + const uint64_t expectedRequestID = awaitingGrant ? + audio->subscribeRequestID : audio->grantRequestID; + if (!audio->connected || !audio->serviceAvailable || + grant.subscriberID != audio->subscriberID || + grant.requestedRoles != audio->requestedRoles) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + if (!expectedRequestID || + packet->envelope.requestID != expectedRequestID) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + + const bool accepted = grant.status == LG_NET_AUDIO_GRANT_GRANTED || + grant.status == LG_NET_AUDIO_GRANT_PARTIAL; + if (audio->subscriptionEpoch == grant.subscriptionEpoch && + audio->grantedRoles == grant.grantedRoles && + audio->phase != NETWORK_AUDIO_SUBSCRIBING) + { + if (!accepted || grant.targetLatencyUs != audio->targetLatencyUs || + grant.maxPacketFrames != audio->maxPacketFrames) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + audio->nextRenewalNs = networkAudioDeadline( + NETWORK_AUDIO_RENEWAL_MS); + audio->renewalDeadlineNs = networkAudioDeadline( + NETWORK_AUDIO_RENEWAL_TIMEOUT_MS); + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + + if (grant.subscriptionEpoch == audio->lastSubscriptionEpoch) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + + if (!accepted) + { + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + const uint32_t playbackGeneration = audio->playback.active ? + audio->playback.generation : 0; + const uint32_t captureGeneration = audio->capture.active ? + audio->capture.generation : 0; + networkAudioClearSubscriptionNL(audio, false); + networkAudioClearStreamNL(&audio->playback); + networkAudioClearStreamNL(&audio->capture); + audio->lastSubscriptionEpoch = grant.subscriptionEpoch; + audio->phase = NETWORK_AUDIO_SUBSCRIBING; + audio->nextSubscribeNs = networkAudioDeadline( + NETWORK_AUDIO_SUBSCRIBE_RETRY_MS); + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events && playbackGeneration && events->playbackStop) + events->playbackStop(opaque, playbackGeneration); + if (events && captureGeneration && events->recordStop) + events->recordStop(opaque, captureGeneration); + networkAudioEndEvents(audio); + return true; + } + + if (!grant.maxPacketFrames || + (grant.grantedRoles & ~audio->requestedRoles) || + grant.maxPacketFrames > NETWORK_AUDIO_MAX_PACKET_FRAMES || + (grant.grantedRoles & LG_NET_AUDIO_ROLE_DATAGRAM_PCM && + !datagramAvailable) || + (grant.grantedRoles & LG_NET_AUDIO_ROLE_CAPTURE && + !(grant.grantedRoles & LG_NET_AUDIO_ROLE_EXCLUSIVE_CAPTURE)) || + (grant.grantedRoles & LG_NET_AUDIO_ROLE_CLOCK_FEEDBACK && + !(grant.grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK))) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + const uint32_t playbackGeneration = audio->playback.active ? + audio->playback.generation : 0; + const uint32_t captureGeneration = audio->capture.active ? + audio->capture.generation : 0; + networkAudioClearStreamNL(&audio->playback); + networkAudioClearStreamNL(&audio->capture); + + audio->subscribeRequestID = 0; + audio->grantRequestID = packet->envelope.requestID; + audio->subscriptionEpoch = grant.subscriptionEpoch; + audio->lastSubscriptionEpoch = grant.subscriptionEpoch; + audio->grantedRoles = grant.grantedRoles; + audio->targetLatencyUs = grant.targetLatencyUs; + audio->maxPacketFrames = grant.maxPacketFrames; + audio->controlSequence = 0; + audio->capturePacketID = 0; + audio->captureFramePosition = 0; + audio->phase = NETWORK_AUDIO_GRANTED; + audio->nextKeepaliveNs = networkAudioDeadline( + NETWORK_AUDIO_KEEPALIVE_MS); + audio->nextRenewalNs = networkAudioDeadline( + NETWORK_AUDIO_RENEWAL_MS); + audio->renewalDeadlineNs = networkAudioDeadline( + NETWORK_AUDIO_RENEWAL_TIMEOUT_MS); + networkAudioSetAvailableNL(audio, true, true); + networkAudioUpdateReadyNL(audio); + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events && playbackGeneration && events->playbackStop) + events->playbackStop(opaque, playbackGeneration); + if (events && captureGeneration && events->recordStop) + events->recordStop(opaque, captureGeneration); + networkAudioEndEvents(audio); + return true; +} + +static bool networkAudioHandleStart(NetworkAudio * audio, + const LGNetPacketView * packet, struct NetworkAudioStream * stream) +{ + LGNetAudioFormat wire; + if (lgNetAudioFormatDecode(&wire, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + wire.streamID != stream->streamID || + wire.direction != stream->direction) + return false; + + LG_AudioFormat format; + size_t frameSize; + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + if (!networkAudioSubscriptionCurrentNL(audio, packet) || + !networkAudioStreamRequiredNL(audio, stream) || + !networkAudioFormat(&wire, audio->maxPacketFrames, + &format, &frameSize)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + (void)frameSize; + + if (stream->formatValid && + stream->wireFormat.formatEpoch == wire.formatEpoch) + { + const bool duplicate = + memcmp(&stream->wireFormat, &wire, sizeof(wire)) == 0; + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return duplicate; + } + + const uint32_t oldGeneration = stream->active ? + stream->generation : 0; + const uint32_t generation = networkAudioMapGenerationNL( + audio, stream, wire.formatEpoch); + const bool replace = + oldGeneration && generation != oldGeneration; + stream->wireFormat = wire; + stream->format = format; + stream->generation = generation; + stream->formatValid = true; + stream->active = true; + stream->volumeValid = false; + stream->volumeChannels = 0; + memset(stream->volume, 0, sizeof(stream->volume)); + stream->muteValid = false; + stream->mute = false; + stream->masterMute = false; + stream->mutedChannels = 0; + stream->packetKnown = false; + stream->lastPacketID = 0; + stream->discontinuity = true; + networkAudioClearClockNL(stream); + networkAudioInvalidateBarrierNL(audio, stream); + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events && replace) + { + if (stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackStop) + events->playbackStop(opaque, oldGeneration); + if (stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordStop) + events->recordStop(opaque, oldGeneration); + } + if (events) + { + if (stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackStart) + events->playbackStart(opaque, generation, &format, NULL); + if (stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordStart) + events->recordStart(opaque, generation, &format); + } + networkAudioEndEvents(audio); + return true; +} + +static bool networkAudioHandleStop(NetworkAudio * audio, + const LGNetPacketView * packet, struct NetworkAudioStream * stream) +{ + LGNetAudioControl control; + if (lgNetAudioControlDecode(&control, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + control.streamID != stream->streamID || + control.direction != stream->direction) + return false; + + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + if (!networkAudioSubscriptionCurrentNL(audio, packet) || + !networkAudioStreamRequiredNL(audio, stream)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (control.sequence <= stream->stateSequence) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (stream->formatValid && control.formatEpoch && + control.formatEpoch != stream->wireFormat.formatEpoch) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + + const uint32_t generation = stream->active ? + stream->generation : 0; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + stream->stateSequence = control.sequence; + stream->active = false; + stream->formatValid = false; + stream->volumeValid = false; + stream->muteValid = false; + stream->discontinuity = true; + networkAudioClearClockNL(stream); + networkAudioInvalidateBarrierNL(audio, stream); + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events && generation) + { + if (stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackStop) + events->playbackStop(opaque, generation); + if (stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordStop) + events->recordStop(opaque, generation); + } + networkAudioEndEvents(audio); + return true; +} + +static bool networkAudioApplyVolumeNL(struct NetworkAudioStream * stream, + const LGNetAudioVolume * volume) +{ + if (!stream->active || !stream->formatValid || + volume->channelCount > stream->wireFormat.channels) + return false; + + if (volume->channelMask && + (volume->channelMask & ~stream->wireFormat.channelMask)) + return false; + + if (!stream->volumeValid) + for (unsigned i = 0; i < stream->wireFormat.channels; ++i) + stream->volume[i] = UINT16_MAX; + + if (!volume->channelMask) + { + for (unsigned i = 0; i < volume->channelCount; ++i) + stream->volume[i] = networkAudioVolumeLevel( + volume->volumeMillibels[i]); + } + else + { + unsigned valueIndex = 0; + unsigned channelIndex = 0; + for (unsigned bit = 0; bit < 18U; ++bit) + { + const uint32_t mask = UINT32_C(1) << bit; + if (!(stream->wireFormat.channelMask & mask)) + continue; + if (volume->channelMask & mask) + stream->volume[channelIndex] = networkAudioVolumeLevel( + volume->volumeMillibels[valueIndex++]); + ++channelIndex; + } + } + + stream->volumeChannels = (uint8_t)stream->wireFormat.channels; + stream->volumeValid = true; + return true; +} + +static bool networkAudioHandleVolume(NetworkAudio * audio, + const LGNetPacketView * packet, struct NetworkAudioStream * stream) +{ + LGNetAudioVolume volume; + if (lgNetAudioVolumeDecode(&volume, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + volume.streamID != stream->streamID || + volume.direction != stream->direction) + return false; + + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + if (!networkAudioSubscriptionCurrentNL(audio, packet) || + !networkAudioStreamRequiredNL(audio, stream)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (volume.sequence <= stream->stateSequence) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (!networkAudioApplyVolumeNL(stream, &volume)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + + stream->stateSequence = volume.sequence; + networkAudioInvalidateBarrierNL(audio, stream); + const uint32_t generation = stream->generation; + const uint8_t channels = stream->volumeChannels; + uint16_t levels[LG_AUDIO_MAX_CHANNELS]; + memcpy(levels, stream->volume, sizeof(levels)); + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events) + { + if (stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackVolume) + events->playbackVolume(opaque, generation, channels, levels); + if (stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordVolume) + events->recordVolume(opaque, generation, channels, levels); + } + networkAudioEndEvents(audio); + return true; +} + +static bool networkAudioApplyMuteNL(struct NetworkAudioStream * stream, + const LGNetAudioMute * mute) +{ + if (!stream->active || !stream->formatValid || + (mute->channelMask & ~stream->wireFormat.channelMask)) + return false; + + if (!mute->channelMask) + stream->masterMute = mute->muted != 0; + else if (mute->muted) + stream->mutedChannels |= mute->channelMask; + else + stream->mutedChannels &= ~mute->channelMask; + + stream->mute = stream->masterMute || + (stream->mutedChannels & stream->wireFormat.channelMask) == + stream->wireFormat.channelMask; + stream->muteValid = true; + return true; +} + +static bool networkAudioHandleMute(NetworkAudio * audio, + const LGNetPacketView * packet, struct NetworkAudioStream * stream) +{ + LGNetAudioMute mute; + if (lgNetAudioMuteDecode(&mute, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + mute.streamID != stream->streamID || + mute.direction != stream->direction) + return false; + + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + if (!networkAudioSubscriptionCurrentNL(audio, packet) || + !networkAudioStreamRequiredNL(audio, stream)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (mute.sequence <= stream->stateSequence) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (!networkAudioApplyMuteNL(stream, &mute)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + + stream->stateSequence = mute.sequence; + networkAudioInvalidateBarrierNL(audio, stream); + const uint32_t generation = stream->generation; + const bool muted = stream->mute; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events) + { + if (stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackMute) + events->playbackMute(opaque, generation, muted); + if (stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordMute) + events->recordMute(opaque, generation, muted); + } + networkAudioEndEvents(audio); + return true; +} + +static bool networkAudioHandleStatus(NetworkAudio * audio, + const LGNetPacketView * packet) +{ + LGNetAudioState state; + if (lgNetAudioStateDecode(&state, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK) + return false; + + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + struct NetworkAudioStream * stream = networkAudioStreamNL( + audio, state.streamID, state.direction); + if (!stream) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + if (!networkAudioSubscriptionCurrentNL(audio, packet) || + !networkAudioStreamRequiredNL(audio, stream)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (state.stateSequence <= stream->stateSequence) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + + const bool running = state.state == LG_NET_AUDIO_STATE_RUNNING || + state.state == LG_NET_AUDIO_STATE_PAUSED; + if (running && (!stream->active || !stream->formatValid || + !(state.flags & LG_NET_AUDIO_STATE_FORMAT_ACTIVE) || + state.formatEpoch != stream->wireFormat.formatEpoch)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + if (!running && (state.flags & LG_NET_AUDIO_STATE_FORMAT_ACTIVE)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + + const bool wasActive = stream->active; + const uint32_t generation = stream->generation; + bool volumeChanged = false; + bool muteChanged = false; + if (running) + { + const uint16_t level = networkAudioVolumeLevel( + state.volumeMillibels); + for (unsigned i = 0; i < stream->wireFormat.channels; ++i) + stream->volume[i] = level; + stream->volumeChannels = (uint8_t)stream->wireFormat.channels; + stream->volumeValid = true; + volumeChanged = true; + + stream->masterMute = + (state.flags & LG_NET_AUDIO_STATE_MUTED) != 0; + stream->mutedChannels = 0; + stream->mute = stream->masterMute; + stream->muteValid = true; + muteChanged = true; + } + else + { + stream->active = false; + stream->formatValid = false; + stream->volumeValid = false; + stream->muteValid = false; + stream->discontinuity = true; + networkAudioClearClockNL(stream); + } + stream->stateSequence = state.stateSequence; + networkAudioInvalidateBarrierNL(audio, stream); + + const uint8_t channels = stream->volumeChannels; + uint16_t levels[LG_AUDIO_MAX_CHANNELS]; + memcpy(levels, stream->volume, sizeof(levels)); + const bool mute = stream->mute; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events && running) + { + if (volumeChanged && + stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackVolume) + events->playbackVolume(opaque, generation, channels, levels); + if (volumeChanged && + stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordVolume) + events->recordVolume(opaque, generation, channels, levels); + if (muteChanged && + stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackMute) + events->playbackMute(opaque, generation, mute); + if (muteChanged && + stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordMute) + events->recordMute(opaque, generation, mute); + } + else if (events && wasActive) + { + if (stream->direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + events->playbackStop) + events->playbackStop(opaque, generation); + if (stream->direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + events->recordStop) + events->recordStop(opaque, generation); + } + networkAudioEndEvents(audio); + return true; +} + +static bool networkAudioHandleBarrier(NetworkAudio * audio, + const LGNetPacketView * packet) +{ + LGNetAudioBarrier barrier; + if (lgNetAudioBarrierDecode(&barrier, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + packet->envelope.requestID != barrier.barrierID) + return false; + + LG_LOCK(audio->stateLock); + struct NetworkAudioStream * stream = networkAudioStreamNL( + audio, barrier.streamID, barrier.direction); + if (!stream) + { + LG_UNLOCK(audio->stateLock); + return false; + } + if (!networkAudioSubscriptionCurrentNL(audio, packet)) + { + LG_UNLOCK(audio->stateLock); + return true; + } + if (!networkAudioStreamRequiredNL(audio, stream) || + !stream->stateSequence || + barrier.stateSequence != stream->stateSequence) + { + LG_UNLOCK(audio->stateLock); + return false; + } + + stream->barrier = barrier; + stream->barrierRequestID = packet->envelope.requestID; + stream->barrierPending = true; + stream->barrierReady = false; + networkAudioUpdateReadyNL(audio); + networkAudioSendBarrierAckNL(audio, stream); + LG_UNLOCK(audio->stateLock); + return true; +} + +static struct NetworkAudioClockEntry * networkAudioFindClockNL( + struct NetworkAudioStream * stream, uint64_t packetID) +{ + for (unsigned i = 0; i < NETWORK_AUDIO_CLOCK_CACHE_SIZE; ++i) + if (stream->clocks[i].valid && + stream->clocks[i].state.packetID == packetID) + return &stream->clocks[i]; + return NULL; +} + +static bool networkAudioHandleClock(NetworkAudio * audio, + const LGNetPacketView * packet) +{ + LGNetAudioClockState state; + if (lgNetAudioClockStateDecode(&state, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK) + return false; + + LG_LOCK(audio->stateLock); + struct NetworkAudioStream * stream = networkAudioStreamNL( + audio, state.streamID, state.direction); + if (!stream || stream != &audio->playback) + { + LG_UNLOCK(audio->stateLock); + return false; + } + if (!networkAudioSubscriptionCurrentNL(audio, packet) || + !networkAudioStreamRequiredNL(audio, stream) || + state.subscriptionEpoch != audio->subscriptionEpoch) + { + LG_UNLOCK(audio->stateLock); + return true; + } + if (!stream->active || !stream->formatValid || + state.formatEpoch != stream->wireFormat.formatEpoch || + (stream->packetKnown && state.packetID <= stream->lastPacketID)) + { + LG_UNLOCK(audio->stateLock); + return true; + } + + struct NetworkAudioClockEntry * entry = networkAudioFindClockNL( + stream, state.packetID); + if (!entry) + { + entry = &stream->clocks[ + stream->clockInsert++ % NETWORK_AUDIO_CLOCK_CACHE_SIZE]; + if (entry->valid) + stream->discontinuity = true; + } + entry->valid = true; + entry->state = state; + LG_UNLOCK(audio->stateLock); + return true; +} + +static bool networkAudioHandlePlaybackData(NetworkAudio * audio, + const LGNetPacketView * packet) +{ + LGNetAudioData data; + if (lgNetAudioDataDecode(&data, + packet->payload, packet->payloadLength) != LG_NET_PARSE_OK || + data.streamID != NETWORK_AUDIO_PLAYBACK_STREAM_ID || + data.direction != LG_NET_AUDIO_DIRECTION_PLAYBACK) + return false; + + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + struct NetworkAudioStream * stream = &audio->playback; + if (!networkAudioSubscriptionCurrentNL(audio, packet) || + !networkAudioStreamRequiredNL(audio, stream)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (!stream->active || !stream->formatValid || + data.formatEpoch != stream->wireFormat.formatEpoch) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + if (!lgNetAudioDataMatchesFormat(&data, &stream->wireFormat) || + data.frameCount > audio->maxPacketFrames) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + if (stream->packetKnown && data.packetID <= stream->lastPacketID) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + + const bool sequenceGap = stream->packetKnown && + data.packetID != stream->lastPacketID + 1U; + stream->packetKnown = true; + stream->lastPacketID = data.packetID; + networkAudioUpdateReadyNL(audio); + if (!stream->barrierReady || audio->phase != NETWORK_AUDIO_READY) + { + stream->discontinuity = true; + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return true; + } + + struct NetworkAudioClockEntry * entry = networkAudioFindClockNL( + stream, data.packetID); + LG_AudioClock clock; + const LG_AudioClock * clockPtr = NULL; + if (entry && (entry->state.flags & LG_NET_AUDIO_CLOCK_VALID)) + { + clock = (LG_AudioClock) + { + .position = entry->state.framePosition, + .time = entry->state.timeNs, + .rate = networkAudioRate(entry->state.rateQ32), + .stable = + (entry->state.flags & LG_NET_AUDIO_CLOCK_STABLE) != 0, + .discontinuity = stream->discontinuity || sequenceGap || + (data.flags & LG_NET_AUDIO_DATA_DISCONTINUITY), + }; + clockPtr = &clock; + stream->discontinuity = false; + } + else + stream->discontinuity = true; + if (entry) + entry->valid = false; + if (data.flags & LG_NET_AUDIO_DATA_END_OF_STREAM) + stream->discontinuity = true; + + const size_t frameSize = (size_t)networkAudioSampleSize( + stream->wireFormat.sampleFormat) * stream->wireFormat.channels; + const size_t bytes = frameSize * data.frameCount; + const void * pcm = data.flags & LG_NET_AUDIO_DATA_SILENT ? + l_networkAudioSilence : data.data; + if (!frameSize || bytes > sizeof(l_networkAudioSilence)) + { + LG_UNLOCK(audio->stateLock); + networkAudioAbortEventsNL(audio); + return false; + } + + const uint32_t generation = stream->generation; + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + if (events && events->playbackData) + events->playbackData(opaque, generation, pcm, + data.frameCount, clockPtr); + networkAudioEndEvents(audio); + return true; +} + +static bool networkAudioHandlePacket(NetworkAudio * audio, + const LGNetPacketView * packet, bool datagram) +{ + if (!audio || !packet || + packet->envelope.service != LG_NET_SERVICE_AUDIO || + !networkAudioVersionSupported(packet->envelope.serviceVersion)) + return false; + + if (packet->envelope.messageType == + LG_NET_AUDIO_MESSAGE_SUBSCRIPTION_GRANT) + { + if (datagram) + return false; + return networkAudioHandleGrant(audio, packet); + } + + LG_LOCK(audio->stateLock); + const bool current = networkAudioSubscriptionCurrentNL(audio, packet); + LG_UNLOCK(audio->stateLock); + if (!current) + return true; + + if (datagram) + { + if (!networkAudioDatagramFlags(packet) || + packet->envelope.requestID) + return false; + } + else + { + LGNetEnvelopeFlags semantic = LG_NET_ENVELOPE_FINAL; + if (packet->envelope.messageType == + LG_NET_AUDIO_MESSAGE_STATE_BARRIER) + semantic |= LG_NET_ENVELOPE_ACK_REQUIRED; + if (!networkAudioReliableFlags(packet, semantic)) + return false; + if (packet->envelope.messageType != + LG_NET_AUDIO_MESSAGE_STATE_BARRIER && + packet->envelope.requestID) + return false; + } + + switch (packet->envelope.messageType) + { + case LG_NET_AUDIO_MESSAGE_STATUS: + return !datagram && networkAudioHandleStatus(audio, packet); + + case LG_NET_AUDIO_MESSAGE_PLAYBACK_START: + return !datagram && networkAudioHandleStart( + audio, packet, &audio->playback); + + case LG_NET_AUDIO_MESSAGE_PLAYBACK_STOP: + return !datagram && networkAudioHandleStop( + audio, packet, &audio->playback); + + case LG_NET_AUDIO_MESSAGE_PLAYBACK_DATA: + return networkAudioHandlePlaybackData(audio, packet); + + case LG_NET_AUDIO_MESSAGE_PLAYBACK_VOLUME: + return !datagram && networkAudioHandleVolume( + audio, packet, &audio->playback); + + case LG_NET_AUDIO_MESSAGE_PLAYBACK_MUTE: + return !datagram && networkAudioHandleMute( + audio, packet, &audio->playback); + + case LG_NET_AUDIO_MESSAGE_CAPTURE_START: + return !datagram && networkAudioHandleStart( + audio, packet, &audio->capture); + + case LG_NET_AUDIO_MESSAGE_CAPTURE_STOP: + return !datagram && networkAudioHandleStop( + audio, packet, &audio->capture); + + case LG_NET_AUDIO_MESSAGE_CAPTURE_VOLUME: + return !datagram && networkAudioHandleVolume( + audio, packet, &audio->capture); + + case LG_NET_AUDIO_MESSAGE_CAPTURE_MUTE: + return !datagram && networkAudioHandleMute( + audio, packet, &audio->capture); + + case LG_NET_AUDIO_MESSAGE_STATE_BARRIER: + return !datagram && networkAudioHandleBarrier(audio, packet); + + case LG_NET_AUDIO_MESSAGE_CLOCK_STATE: + return networkAudioHandleClock(audio, packet); + + default: + return false; + } +} + +bool networkAudioPacket(NetworkAudio * audio, + const LGNetPacketView * packet) +{ + return networkAudioHandlePacket(audio, packet, false); +} + +bool networkAudioDatagram(NetworkAudio * audio, + const LGNetPacketView * packet) +{ + return networkAudioHandlePacket(audio, packet, true); +} + +bool networkAudioCreate(NetworkAudio ** result, + NetworkConnection * connection) +{ + if (!result || !connection) + return false; + + NetworkAudio * audio = calloc(1, sizeof(*audio)); + if (!audio) + return false; + + LG_LOCK_INIT(audio->stateLock); + LG_LOCK_INIT(audio->eventLock); + atomic_init(&audio->feedbackAvailable, false); + audio->connection = connection; + audio->phase = NETWORK_AUDIO_DISCONNECTED; + audio->subscriberID = nanotime() ^ + ((uint64_t)(uintptr_t)audio << 19); + audio->requestedRoles = + LG_NET_AUDIO_ROLE_PLAYBACK | + LG_NET_AUDIO_ROLE_CAPTURE | + LG_NET_AUDIO_ROLE_CLOCK_FEEDBACK | + LG_NET_AUDIO_ROLE_DATAGRAM_PCM | + LG_NET_AUDIO_ROLE_RELIABLE_PCM | + LG_NET_AUDIO_ROLE_EXCLUSIVE_CAPTURE; + audio->endpointGeneration = 1; + audio->generationSerial = 1; + audio->playback.streamID = NETWORK_AUDIO_PLAYBACK_STREAM_ID; + audio->playback.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + audio->capture.streamID = NETWORK_AUDIO_CAPTURE_STREAM_ID; + audio->capture.direction = LG_NET_AUDIO_DIRECTION_CAPTURE; + if (!audio->subscriberID) + audio->subscriberID = 1; + + audio->statusIdle = lgCreateEvent(false, 0); + audio->eventIdle = lgCreateEvent(false, 0); + if (!audio->statusIdle || !audio->eventIdle || + !lgSignalEvent(audio->statusIdle) || + !lgSignalEvent(audio->eventIdle) || + !lgCreateTimer(NETWORK_AUDIO_TIMER_MS, + networkAudioTimer, audio, &audio->timer)) + { + if (audio->eventIdle) + lgFreeEvent(audio->eventIdle); + if (audio->statusIdle) + lgFreeEvent(audio->statusIdle); + LG_LOCK_FREE(audio->eventLock); + LG_LOCK_FREE(audio->stateLock); + free(audio); + return false; + } + + *result = audio; + return true; +} + +void networkAudioConnectionState(NetworkAudio * audio, bool connected) +{ + if (!audio) + return; + + uint16_t serviceVersion = 0; + const bool serviceAvailable = connected && + networkConnectionServiceAvailable(audio->connection, + LG_NET_SERVICE_AUDIO, LG_NET_CAPABILITY_RELIABLE) && + networkConnectionServiceVersion(audio->connection, + LG_NET_SERVICE_AUDIO, &serviceVersion) && + networkAudioVersionSupported(serviceVersion); + + networkAudioBeginEvents(audio); + LG_LOCK(audio->stateLock); + const LG_AudioEventOps * events = + audio->attached ? audio->events : NULL; + void * opaque = audio->eventOpaque; + const uint32_t playbackGeneration = audio->playback.active ? + audio->playback.generation : 0; + const uint32_t captureGeneration = audio->capture.active ? + audio->capture.generation : 0; + + audio->connected = connected; + audio->serviceAvailable = serviceAvailable; + audio->serviceVersion = serviceAvailable ? serviceVersion : 0; + networkAudioClearSubscriptionNL(audio, true); + networkAudioClearStreamNL(&audio->playback); + networkAudioClearStreamNL(&audio->capture); + LG_UNLOCK(audio->stateLock); + networkAudioCallEventsNL(audio); + + if (events && playbackGeneration && events->playbackStop) + events->playbackStop(opaque, playbackGeneration); + if (events && captureGeneration && events->recordStop) + events->recordStop(opaque, captureGeneration); + networkAudioEndEvents(audio); + + if (serviceAvailable) + networkAudioProcess(audio); +} + +void networkAudioDestroy(NetworkAudio ** result) +{ + if (!result || !*result) + return; + + NetworkAudio * audio = *result; + networkAudioSetStatusListener(audio, NULL, NULL); + networkAudioDetach(audio); + + LG_LOCK(audio->stateLock); + audio->destroying = true; + LG_UNLOCK(audio->stateLock); + lgTimerDestroy(audio->timer); + + networkAudioConnectionState(audio, false); + lgFreeEvent(audio->eventIdle); + lgFreeEvent(audio->statusIdle); + LG_LOCK_FREE(audio->eventLock); + LG_LOCK_FREE(audio->stateLock); + free(audio); + *result = NULL; +} diff --git a/client/transports/Network/audio.h b/client/transports/Network/audio.h new file mode 100644 index 00000000..1b8d982f --- /dev/null +++ b/client/transports/Network/audio.h @@ -0,0 +1,46 @@ +/** + * 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 + */ + +#pragma once + +#include "connection.h" + +#include "interface/audio.h" + +#include + +#include + +typedef struct NetworkAudio NetworkAudio; + +bool networkAudioCreate(NetworkAudio ** audio, + NetworkConnection * connection); +void networkAudioDestroy(NetworkAudio ** audio); + +void networkAudioConnectionState(NetworkAudio * audio, bool connected); +void networkAudioProcess(NetworkAudio * audio); +void networkAudioSendReady(NetworkAudio * audio, bool datagram); + +bool networkAudioPacket(NetworkAudio * audio, + const LGNetPacketView * packet); +bool networkAudioDatagram(NetworkAudio * audio, + const LGNetPacketView * packet); + +const LG_AudioOps * networkAudioGetOps(void); diff --git a/client/transports/Network/network.c b/client/transports/Network/network.c index c53faa8d..c9a3027b 100644 --- a/client/transports/Network/network.c +++ b/client/transports/Network/network.c @@ -19,6 +19,7 @@ */ #include "network.h" +#include "audio.h" #include "connection.h" #include "control.h" #include "cursor.h" @@ -47,6 +48,8 @@ #define NETWORK_MAX_PAYLOAD \ (LG_NET_CURSOR_SHAPE_HEADER_WIRE_SIZE + LG_NET_CURSOR_MAX_SHAPE_LENGTH) #define NETWORK_MAX_DATAGRAM 1136U +#define NETWORK_AUDIO_MAX_PAYLOAD \ + (LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE + 65536U) static const LGNetCapability NETWORK_CAPABILITIES[] = { @@ -85,6 +88,19 @@ static const LGNetCapability NETWORK_CAPABILITIES[] = .maxBidirectionalStreams = 0, .maxUnidirectionalStreams = 0, }, + { + .service = LG_NET_SERVICE_AUDIO, + .versionMin = + LG_NET_AUDIO_OWNERSHIP_INTRODUCED_SERVICE_VERSION, + .versionMax = LG_NET_AUDIO_VERSION_CURRENT, + .flags = LG_NET_CAPABILITY_RELIABLE | + LG_NET_CAPABILITY_DATAGRAM | LG_NET_CAPABILITY_MULTI_CLIENT, + .features = 0, + .maxPayloadLength = NETWORK_AUDIO_MAX_PAYLOAD, + .maxDatagramPayload = NETWORK_MAX_DATAGRAM, + .maxBidirectionalStreams = 0, + .maxUnidirectionalStreams = 0, + }, { .service = LG_NET_SERVICE_CONTROL, .versionMin = LG_NET_CONTROL_VERSION_MIN, @@ -108,6 +124,7 @@ struct NetworkTrustPrompt struct LG_Transport { NetworkConnection * connection; + NetworkAudio * audio; NetworkControl * control; NetworkCursor * cursor; NetworkInput * input; @@ -323,6 +340,7 @@ static void networkConnectionState(void * opaque, bool connected, LG_TRANSPORT_UNAVAILABLE : reason; networkInputConnectionState(this->input, connected); + networkAudioConnectionState(this->audio, connected); networkControlConnectionState(this->control, connected); networkRecoveryConnectionState(this->recovery, connected); const bool cursorAvailable = @@ -356,6 +374,11 @@ static void networkPacket(void * opaque, const LGNetPacketView * packet, case LG_NET_SERVICE_INPUT: valid = networkInputPacket(this->input, packet, datagram); break; + case LG_NET_SERVICE_AUDIO: + valid = datagram ? + networkAudioDatagram(this->audio, packet) : + networkAudioPacket(this->audio, packet); + break; case LG_NET_SERVICE_CONTROL: valid = networkControlPacket(this->control, packet, datagram); break; @@ -377,8 +400,8 @@ static void networkSendReady(void * opaque, LGNetService service, bool datagram) { LG_Transport * this = opaque; - (void)service; - (void)datagram; + if (service == LG_NET_SERVICE_AUDIO) + networkAudioSendReady(this->audio, datagram); lgSignalEvent(this->frameEvent); } @@ -439,7 +462,8 @@ static bool networkCreate(LG_Transport ** result) }; if (!networkConnectionCreate(&this->connection, ¶ms)) goto fail; - if (!networkControlCreate(&this->control, this->connection) || + if (!networkAudioCreate(&this->audio, this->connection) || + !networkControlCreate(&this->control, this->connection) || !networkCursorCreate(&this->cursor, this->connection) || !networkInputCreate(&this->input, this->connection) || !networkRecoveryCreate(&this->recovery, this->connection)) @@ -453,6 +477,7 @@ fail: networkInputDestroy(&this->input); networkCursorDestroy(&this->cursor); networkControlDestroy(&this->control); + networkAudioDestroy(&this->audio); networkConnectionDestroy(&this->connection); if (this->frameEvent) lgFreeEvent(this->frameEvent); @@ -487,6 +512,7 @@ static void networkDestroy(LG_Transport ** transport) networkInputDestroy(&this->input); networkCursorDestroy(&this->cursor); networkControlDestroy(&this->control); + networkAudioDestroy(&this->audio); networkConnectionDestroy(&this->connection); lgFreeEvent(this->frameEvent); lgFreeEvent(this->promptIdle); @@ -515,6 +541,7 @@ static LG_TransportStatus networkConnect(LG_Transport * this, static void networkDisconnect(LG_Transport * this) { networkInputConnectionState(this->input, false); + networkAudioConnectionState(this->audio, false); networkControlConnectionState(this->control, false); networkRecoveryConnectionState(this->recovery, false); networkCursorConnectionState(this->cursor, false); @@ -690,9 +717,12 @@ static const LG_InputOps * networkGetInputOps(LG_Transport * this, static const LG_AudioOps * networkGetAudioOps(LG_Transport * this, void ** opaque) { - (void)this; *opaque = NULL; - return NULL; + if (!networkConnectionServiceAvailable(this->connection, + LG_NET_SERVICE_AUDIO, LG_NET_CAPABILITY_RELIABLE)) + return NULL; + *opaque = this->audio; + return networkAudioGetOps(); } static const LG_ClipboardOps * networkGetClipboardOps(LG_Transport * this, diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj index 4a9f122c..3f221154 100644 --- a/idd/LGIdd/LGIdd.vcxproj +++ b/idd/LGIdd/LGIdd.vcxproj @@ -88,6 +88,7 @@ + @@ -173,6 +174,7 @@ + diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters index 1a681649..26acc6fa 100644 --- a/idd/LGIdd/LGIdd.vcxproj.filters +++ b/idd/LGIdd/LGIdd.vcxproj.filters @@ -286,6 +286,9 @@ Transport\LGMP + + Transport\Network + Transport\Network @@ -477,6 +480,9 @@ Transport\LGMP + + Transport\Network + Transport\Network diff --git a/idd/LGIdd/transport/network/CNetworkAudio.cpp b/idd/LGIdd/transport/network/CNetworkAudio.cpp new file mode 100644 index 00000000..7527467d --- /dev/null +++ b/idd/LGIdd/transport/network/CNetworkAudio.cpp @@ -0,0 +1,2701 @@ +/** + * 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 "transport/network/CNetworkAudio.h" + +#include "CDebug.h" +#include "CSRWLock.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr uint32_t PLAYBACK_STREAM_ID = 1; + constexpr uint32_t CAPTURE_STREAM_ID = 2; + constexpr uint64_t AUDIO_LEASE_MS = 2000; + constexpr uint64_t AUTHORITY_STALL_MS = 20; + constexpr size_t CLOCK_CACHE_SIZE = 8; + constexpr size_t RECENT_PACKET_SIZE = 16; + + constexpr LGNetAudioRoles DIRECTION_ROLES = + LG_NET_AUDIO_ROLE_PLAYBACK | LG_NET_AUDIO_ROLE_CAPTURE; + constexpr LGNetAudioRoles DELIVERY_ROLES = + LG_NET_AUDIO_ROLE_DATAGRAM_PCM | LG_NET_AUDIO_ROLE_RELIABLE_PCM; + + bool SamePeer(const NetworkAudioPeer& left, + const NetworkAudioPeer& right) + { + return left.client == right.client && + left.generation == right.generation; + } + + bool SameSubscription(const LGNetAudioSubscribe& left, + const LGNetAudioSubscribe& right) + { + return left.subscriberID == right.subscriberID && + left.directions == right.directions && + left.targetLatencyUs == right.targetLatencyUs && + left.maxPacketFrames == right.maxPacketFrames && + left.flags == right.flags; + } + + uint64_t NextSerial(uint64_t serial) + { + ++serial; + return serial ? serial : 1; + } + + uint64_t MonotonicNs() + { + const uint64_t result = GetTickCount64() * UINT64_C(1000000); + return result ? result : 1; + } + + uint32_t ChannelCount(uint32_t mask) + { + uint32_t count = 0; + while (mask) + { + count += mask & 1U; + mask >>= 1; + } + return count; + } + + size_t SampleBytes(AudioSampleFormat format) + { + switch (format) + { + case AudioSampleFormat::S16_LE: + return 2; + + case AudioSampleFormat::S24_LE: + return 3; + + case AudioSampleFormat::S32_LE: + case AudioSampleFormat::F32_LE: + return 4; + + default: + return 0; + } + } + + bool ValidFormat(const AudioFormat& format) + { + const size_t bytes = SampleBytes(format.sampleFormat); + return bytes && format.sampleRate && format.sampleRate <= 384000U && + format.channels && format.channels <= AUDIO_STREAM_MAX_CHANNELS && + format.channelMask && + !(format.channelMask & ~AUDIO_CHANNEL_POSITION_MASK) && + ChannelCount(format.channelMask) == format.channels && + format.containerBits == bytes * 8U && + format.validBits == format.containerBits && + format.blockAlign == bytes * format.channels; + } + + bool SameFormat(const AudioFormat& left, const AudioFormat& right) + { + return left.sampleFormat == right.sampleFormat && + left.sampleRate == right.sampleRate && + left.channelMask == right.channelMask && + left.channels == right.channels && + left.containerBits == right.containerBits && + left.validBits == right.validBits && + left.blockAlign == right.blockAlign; + } + + bool SamePacket(const AudioPacket& left, const AudioPacket& right) + { + return left.endpointGeneration == right.endpointGeneration && + left.streamGeneration == right.streamGeneration && + left.flags == right.flags && left.frames == right.frames && + left.position == right.position && left.timeNs == right.timeNs && + left.rateQ32 == right.rateQ32 && SameFormat(left.format, right.format); + } + + LGNetAudioSampleFormat WireSampleFormat(AudioSampleFormat format) + { + switch (format) + { + case AudioSampleFormat::S16_LE: + return LG_NET_AUDIO_SAMPLE_S16_LE; + + case AudioSampleFormat::S24_LE: + return LG_NET_AUDIO_SAMPLE_S24_LE; + + case AudioSampleFormat::S32_LE: + return LG_NET_AUDIO_SAMPLE_S32_LE; + + case AudioSampleFormat::F32_LE: + return LG_NET_AUDIO_SAMPLE_F32_LE; + + default: + return 0; + } + } + + LGNetAudioStateCode WireState(AudioStreamState state) + { + switch (state) + { + case AudioStreamState::RUNNING: + return LG_NET_AUDIO_STATE_RUNNING; + + case AudioStreamState::ACQUIRED: + case AudioStreamState::PAUSED: + return LG_NET_AUDIO_STATE_PAUSED; + + case AudioStreamState::UNAVAILABLE: + case AudioStreamState::STOPPED: + default: + return LG_NET_AUDIO_STATE_STOPPED; + } + } + + bool DeadlineReached(uint64_t now, uint64_t deadline) + { + return static_cast(now - deadline) >= 0; + } + + bool AudioVersionSupported(uint16_t version) + { + return version >= + LG_NET_AUDIO_OWNERSHIP_INTRODUCED_SERVICE_VERSION && + version <= LG_NET_AUDIO_VERSION_CURRENT; + } + + bool ReliableFlags(const LGNetEnvelope& envelope, + LGNetEnvelopeFlags expected) + { + constexpr LGNetEnvelopeFlags MASK = + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL | + LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM | + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_ERROR | + LG_NET_ENVELOPE_EARLY_DATA; + return (envelope.flags & MASK) == + (expected | LG_NET_ENVELOPE_RELIABLE); + } + + bool DeliveryFlags(const LGNetEnvelope& envelope, bool datagram, + LGNetEnvelopeFlags expected) + { + constexpr LGNetEnvelopeFlags MASK = + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL | + LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_DATAGRAM | + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_ERROR | + LG_NET_ENVELOPE_EARLY_DATA; + const LGNetEnvelopeFlags delivery = datagram ? + LG_NET_ENVELOPE_DATAGRAM : LG_NET_ENVELOPE_RELIABLE; + return (envelope.flags & MASK) == (expected | delivery); + } +} + +struct CNetworkAudio::Impl +{ + enum class PumpStage + { + GRANT, + RENEWAL_GRANT, + PLAYBACK_STOP, + CAPTURE_STOP, + PLAYBACK_START, + PLAYBACK_VOLUME, + PLAYBACK_MUTE, + CAPTURE_START, + CAPTURE_VOLUME, + CAPTURE_MUTE, + PLAYBACK_STATUS, + CAPTURE_STATUS, + PLAYBACK_BARRIER, + CAPTURE_BARRIER, + DONE, + }; + + enum class PumpEffect + { + NONE, + RENEWAL, + EXPIRY, + PLAYBACK_STOPPED, + CAPTURE_STOPPED, + PLAYBACK_STARTED, + CAPTURE_STARTED, + STATE_SEQUENCE, + BARRIER, + }; + + enum class BlockDisposition + { + STALE, + RETRY, + WAIT, + }; + + struct ClockRecord + { + bool valid = false; + LGNetAudioClockState state = {}; + }; + + struct PacketRecord + { + bool valid = false; + uint64_t formatEpoch = 0; + uint64_t packetID = 0; + }; + + struct PeerState + { + NetworkAudioPeer peer; + NetworkAudioPeerInfo info; + uint64_t serial = 0; + bool hasRequest = false; + bool subscribed = false; + bool expiring = false; + bool feedbackAuthority = false; + bool feedbackRevoked = false; + bool captureOwner = false; + bool captureRevoked = false; + bool ready = false; + bool awaitingAck = false; + bool playbackGap = true; + bool captureGap = true; + bool resetPlayback = false; + bool resetCapture = false; + bool capturePacketKnown = false; + uint64_t deadline = 0; + uint64_t requestID = 0; + uint64_t subscriptionEpoch = 0; + uint64_t sendReadySerial = 0; + LGNetAudioSubscribe request = {}; + LGNetAudioRoles requestedRoles = 0; + LGNetAudioRoles grantedRoles = 0; + LGNetAudioGrantStatus grantStatus = LG_NET_AUDIO_GRANT_BUSY; + + uint64_t playbackSequence = 0; + uint64_t captureSequence = 0; + uint64_t playbackPacketID = 0; + uint64_t capturePacketID = 0; + uint64_t barrierID = 0; + uint64_t playbackBarrierID = 0; + uint64_t captureBarrierID = 0; + uint8_t pendingBarriers = 0; + uint64_t announcedPlayback = 0; + uint64_t announcedCapture = 0; + uint64_t pumpToken = 0; + PumpStage pumpStage = PumpStage::DONE; + bool pumpGrant = false; + bool pumpInFlight = false; + + std::array captureClock = {}; + size_t captureClockNext = 0; + std::array playbackPacket = {}; + size_t playbackPacketNext = 0; + }; + + struct PumpAction + { + NetworkAudioPeer peer; + uint64_t peerSerial = 0; + uint64_t pumpToken = 0; + uint64_t componentEpoch = 0; + uint64_t requestID = 0; + uint64_t value = 0; + bool playback = false; + uint16_t messageType = 0; + LGNetEnvelopeFlags flags = LG_NET_ENVELOPE_FINAL; + PumpEffect effect = PumpEffect::NONE; + std::vector payload; + }; + + struct PlaybackTarget + { + NetworkAudioPeer peer; + NetworkAudioPeerInfo info; + uint64_t peerSerial = 0; + uint64_t subscriptionEpoch = 0; + uint64_t formatEpoch = 0; + uint32_t maxPacketFrames = 0; + LGNetAudioRoles roles = 0; + bool authority = false; + bool discontinuity = false; + }; + + struct PendingPlayback + { + bool valid = false; + AudioPacket packet; + size_t size = 0; + std::vector targets; + size_t target = 0; + uint32_t frameOffset = 0; + uint32_t chunkFrames = 0; + uint64_t packetID = 0; + bool clockSent = false; + uint64_t blockedSince = 0; + }; + + struct PendingCapture + { + bool valid = false; + bool callback = false; + NetworkAudioPeer peer; + uint64_t peerSerial = 0; + AudioPacket packet; + size_t size = 0; + std::array data = {}; + }; + + CSRWLock m_lifecycleLock; + CSRWLock m_processLock; + CSRWLock m_senderCallbackLock; + CSRWLock m_targetCallbackLock; + CSRWLock m_lock; + + INetworkAudioSender * m_sender = nullptr; + IAudioTarget * m_target = nullptr; + bool m_started = false; + bool m_stateKnown = false; + bool m_activationCommanded = false; + bool m_failed = false; + bool m_failureNotified = false; + bool m_playbackReadyPending = false; + bool m_playbackWaiting = false; + NetworkAudioPeer m_playbackWaitingPeer; + AudioEndpointStatus m_state; + std::vector m_peers; + uint64_t m_nextPeerSerial = 0; + uint64_t m_nextEpoch = 0; + PendingPlayback m_playback; + PendingCapture m_capture; + + PeerState * FindPeerNL(const NetworkAudioPeer& peer) + { + for (PeerState& state : m_peers) + if (SamePeer(state.peer, peer)) + return &state; + return nullptr; + } + + const PeerState * FindPeerNL(const NetworkAudioPeer& peer) const + { + for (const PeerState& state : m_peers) + if (SamePeer(state.peer, peer)) + return &state; + return nullptr; + } + + bool PlaybackCapableNL() const + { + return !m_stateKnown || + (m_state.capabilities & AUDIO_CAP_PLAYBACK) != 0; + } + + bool CaptureCapableNL() const + { + return !m_stateKnown || + (m_state.capabilities & AUDIO_CAP_CAPTURE) != 0; + } + + bool FeedbackCapableNL() const + { + return !m_stateKnown || + (m_state.capabilities & AUDIO_CAP_CLOCK_FEEDBACK) != 0; + } + + static bool PeerCanDeliver(const PeerState& peer) + { + const bool reliable = + (peer.requestedRoles & LG_NET_AUDIO_ROLE_RELIABLE_PCM) && + (peer.info.capabilityFlags & LG_NET_CAPABILITY_RELIABLE) && + peer.info.maxReliablePayload >= LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE; + const bool datagram = + (peer.requestedRoles & LG_NET_AUDIO_ROLE_DATAGRAM_PCM) && + (peer.info.capabilityFlags & LG_NET_CAPABILITY_DATAGRAM) && + peer.info.maxDatagramPayload >= LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE; + return reliable || datagram; + } + + bool CaptureCandidateNL(const PeerState& peer) const + { + return peer.hasRequest && !peer.expiring && !peer.captureRevoked && + CaptureCapableNL() && PeerCanDeliver(peer) && + (peer.requestedRoles & LG_NET_AUDIO_ROLE_CAPTURE) && + (peer.requestedRoles & LG_NET_AUDIO_ROLE_EXCLUSIVE_CAPTURE); + } + + bool FeedbackCandidateNL(const PeerState& peer) const + { + return peer.hasRequest && !peer.expiring && PlaybackCapableNL() && + FeedbackCapableNL() && PeerCanDeliver(peer) && + !peer.feedbackRevoked && + (peer.requestedRoles & LG_NET_AUDIO_ROLE_PLAYBACK) && + (peer.requestedRoles & LG_NET_AUDIO_ROLE_CLOCK_FEEDBACK); + } + + LGNetAudioRoles DeliveryRolesNL(const PeerState& peer) const + { + LGNetAudioRoles result = 0; + if ((peer.requestedRoles & LG_NET_AUDIO_ROLE_RELIABLE_PCM) && + (peer.info.capabilityFlags & LG_NET_CAPABILITY_RELIABLE) && + peer.info.maxReliablePayload >= LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE) + result |= LG_NET_AUDIO_ROLE_RELIABLE_PCM; + if ((peer.requestedRoles & LG_NET_AUDIO_ROLE_DATAGRAM_PCM) && + (peer.info.capabilityFlags & LG_NET_CAPABILITY_DATAGRAM) && + peer.info.maxDatagramPayload >= LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE) + result |= LG_NET_AUDIO_ROLE_DATAGRAM_PCM; + return result; + } + + LGNetAudioRoles DesiredRolesNL(const PeerState& peer) const + { + if (peer.expiring) + return 0; + + LGNetAudioRoles result = DeliveryRolesNL(peer); + if ((peer.requestedRoles & LG_NET_AUDIO_ROLE_PLAYBACK) && + PlaybackCapableNL()) + result |= LG_NET_AUDIO_ROLE_PLAYBACK; + if (peer.captureOwner) + result |= LG_NET_AUDIO_ROLE_CAPTURE | + LG_NET_AUDIO_ROLE_EXCLUSIVE_CAPTURE; + if (peer.feedbackAuthority) + result |= LG_NET_AUDIO_ROLE_CLOCK_FEEDBACK; + if (!(result & DIRECTION_ROLES) || !(result & DELIVERY_ROLES)) + return 0; + return result; + } + + void SchedulePumpNL(PeerState& peer, bool grant) + { + peer.ready = false; + peer.awaitingAck = false; + peer.pendingBarriers = 0; + peer.playbackBarrierID = 0; + peer.captureBarrierID = 0; + peer.pumpGrant = peer.pumpGrant || grant; + peer.pumpStage = peer.pumpGrant ? + PumpStage::GRANT : PumpStage::PLAYBACK_STOP; + peer.pumpToken = NextSerial(peer.pumpToken); + } + + void ClearSubscriptionNL(PeerState& peer) + { + if (m_capture.valid && SamePeer(m_capture.peer, peer.peer)) + m_capture.valid = false; + if (m_playbackWaiting && SamePeer(m_playbackWaitingPeer, peer.peer)) + { + m_playbackWaiting = false; + m_playbackReadyPending = true; + } + + peer.hasRequest = false; + peer.subscribed = false; + peer.expiring = false; + peer.feedbackAuthority = false; + peer.feedbackRevoked = false; + peer.captureOwner = false; + peer.captureRevoked = false; + peer.ready = false; + peer.awaitingAck = false; + peer.pendingBarriers = 0; + peer.deadline = 0; + peer.requestID = 0; + peer.request = {}; + peer.requestedRoles = 0; + peer.grantedRoles = 0; + peer.pumpGrant = false; + peer.pumpStage = PumpStage::DONE; + peer.pumpToken = NextSerial(peer.pumpToken); + peer.playbackGap = true; + peer.captureGap = true; + peer.resetPlayback = false; + peer.resetCapture = false; + peer.capturePacketKnown = false; + peer.capturePacketID = 0; + peer.captureClock = {}; + peer.captureClockNext = 0; + peer.playbackPacket = {}; + peer.playbackPacketNext = 0; + } + + void RebalanceRolesNL(uint64_t forcedSerial = 0); + bool BuildPumpActionNL(PeerState& peer, PumpAction& action); + void PumpPeer(const NetworkAudioPeer& peer); + void PumpAll(); + void PublishBarrierNL(PeerState& peer, const PumpAction& action); + void RollbackBarrierNL(PeerState& peer, const PumpAction& action); + void ApplyPumpResultNL(PeerState& peer, const PumpAction& action); + + bool PlaybackCurrentNL(const PeerState& peer) const; + bool CaptureCurrentNL(const PeerState& peer) const; + bool WireFormatNL(const PeerState& peer, bool playback, + LGNetAudioFormat& format) const; + + NetworkAudioSendResult Send(const NetworkAudioPeer& peer, + bool datagram, uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength); + NetworkAudioSendResult SendPCM(const PlaybackTarget& target, + uint16_t messageType, const void * payload, uint32_t payloadLength); + + NetworkAudioPacketResult HandleSubscribe(PeerState& peer, + const LGNetPacketView& packet, uint64_t now); + NetworkAudioPacketResult HandleCurrentPacket(const NetworkAudioPeer& peer, + const LGNetPacketView& packet, bool datagram, uint64_t now); + NetworkAudioPacketResult HandleClock(const NetworkAudioPeer& peer, + const LGNetPacketView& packet); + NetworkAudioPacketResult HandleCapture(const NetworkAudioPeer& peer, + const LGNetPacketView& packet); + + void ProcessActivation(); + void NotifyPlaybackReady(); + void NotifyFailure(); + void MarkFailed(const char * reason); + void RetryCapture(); + void RevokeStalledAuthority(uint64_t now); + uint64_t PlaybackReadySerial(const PlaybackTarget& target); + BlockDisposition HandlePlaybackBlock( + const PlaybackTarget& target, uint64_t readySerial); + + AudioResult BeginPlayback(const AudioPacket& packet, size_t size); + AudioResult ContinuePlayback(const uint8_t * data); +}; + +void CNetworkAudio::Impl::RebalanceRolesNL(uint64_t forcedSerial) +{ + PeerState * capture = nullptr; + PeerState * feedback = nullptr; + + for (PeerState& peer : m_peers) + { + if (peer.captureOwner && CaptureCandidateNL(peer)) + capture = &peer; + if (peer.feedbackAuthority && FeedbackCandidateNL(peer)) + feedback = &peer; + } + + if (!capture) + for (PeerState& peer : m_peers) + if (CaptureCandidateNL(peer)) + { + capture = &peer; + break; + } + if (!feedback) + for (PeerState& peer : m_peers) + if (FeedbackCandidateNL(peer)) + { + feedback = &peer; + break; + } + + for (PeerState& peer : m_peers) + { + peer.captureOwner = &peer == capture; + peer.feedbackAuthority = &peer == feedback; + } + + for (PeerState& peer : m_peers) + { + if (!peer.hasRequest) + continue; + + const LGNetAudioRoles desired = DesiredRolesNL(peer); + LGNetAudioGrantStatus status; + if (peer.expiring) + status = LG_NET_AUDIO_GRANT_DENIED; + else if (!desired) + { + const bool captureBusy = + (peer.requestedRoles & LG_NET_AUDIO_ROLE_CAPTURE) && + (peer.requestedRoles & LG_NET_AUDIO_ROLE_EXCLUSIVE_CAPTURE) && + capture && capture != &peer; + status = captureBusy ? + LG_NET_AUDIO_GRANT_BUSY : LG_NET_AUDIO_GRANT_UNSUPPORTED; + } + else + status = desired == peer.requestedRoles ? + LG_NET_AUDIO_GRANT_GRANTED : LG_NET_AUDIO_GRANT_PARTIAL; + + const bool changed = peer.serial == forcedSerial || + desired != peer.grantedRoles || status != peer.grantStatus; + peer.subscribed = status == LG_NET_AUDIO_GRANT_GRANTED || + status == LG_NET_AUDIO_GRANT_PARTIAL; + if (!changed) + continue; + + if (m_capture.valid && m_capture.peerSerial == peer.serial && + SamePeer(m_capture.peer, peer.peer)) + m_capture.valid = false; + peer.subscriptionEpoch = NextSerial(m_nextEpoch); + m_nextEpoch = peer.subscriptionEpoch; + peer.grantedRoles = desired; + peer.grantStatus = status; + peer.playbackGap = true; + peer.captureGap = true; + peer.capturePacketKnown = false; + peer.capturePacketID = 0; + peer.captureClock = {}; + peer.captureClockNext = 0; + peer.playbackPacket = {}; + peer.playbackPacketNext = 0; + SchedulePumpNL(peer, true); + } +} + +bool CNetworkAudio::Impl::WireFormatNL(const PeerState& peer, + bool playback, LGNetAudioFormat& format) const +{ + const AudioStreamStatus& stream = playback ? + m_state.playback : m_state.capture; + if (!stream.generation || !stream.formatValid || + !ValidFormat(stream.format)) + return false; + + const uint32_t maxFrames = (std::min)( + peer.request.maxPacketFrames, AUDIO_MAX_FRAMES_PER_PACKET); + if (!maxFrames) + return false; + + uint64_t preferred = + static_cast(stream.format.sampleRate) * + peer.request.targetLatencyUs; + preferred = (preferred + UINT64_C(999999)) / UINT64_C(1000000); + preferred = (std::max)(preferred, UINT64_C(1)); + preferred = (std::min)(preferred, static_cast(maxFrames)); + + format.streamID = playback ? + PLAYBACK_STREAM_ID : CAPTURE_STREAM_ID; + format.direction = playback ? + LG_NET_AUDIO_DIRECTION_PLAYBACK : LG_NET_AUDIO_DIRECTION_CAPTURE; + format.sampleFormat = WireSampleFormat(stream.format.sampleFormat); + format.formatEpoch = stream.generation; + format.sampleRate = stream.format.sampleRate; + format.channels = stream.format.channels; + format.framesPerPacket = static_cast(preferred); + format.channelMask = stream.format.channelMask; + format.flags = LG_NET_AUDIO_FORMAT_INTERLEAVED; + format.maxPacketFrames = maxFrames; + return lgNetAudioFormatValid(&format); +} + +bool CNetworkAudio::Impl::PlaybackCurrentNL(const PeerState& peer) const +{ + LGNetAudioFormat format = {}; + return m_stateKnown && m_state.available && m_state.active && + peer.subscribed && + (peer.grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK) && + m_state.playback.state != AudioStreamState::UNAVAILABLE && + m_state.playback.state != AudioStreamState::STOPPED && + WireFormatNL(peer, true, format); +} + +bool CNetworkAudio::Impl::CaptureCurrentNL(const PeerState& peer) const +{ + LGNetAudioFormat format = {}; + return m_stateKnown && m_state.available && m_state.active && + peer.subscribed && peer.captureOwner && + (peer.grantedRoles & LG_NET_AUDIO_ROLE_CAPTURE) && + m_state.capture.state != AudioStreamState::UNAVAILABLE && + m_state.capture.state != AudioStreamState::STOPPED && + WireFormatNL(peer, false, format); +} + +bool CNetworkAudio::Impl::BuildPumpActionNL( + PeerState& peer, PumpAction& action) +{ + action.peer = peer.peer; + action.peerSerial = peer.serial; + action.pumpToken = peer.pumpToken; + action.componentEpoch = peer.subscriptionEpoch; + + for (;;) + { + switch (peer.pumpStage) + { + case PumpStage::GRANT: + case PumpStage::RENEWAL_GRANT: + { + const bool renewal = + peer.pumpStage == PumpStage::RENEWAL_GRANT; + LGNetAudioSubscriptionGrant grant = {}; + grant.subscriberID = peer.request.subscriberID; + grant.subscriptionEpoch = peer.subscriptionEpoch; + grant.requestedRoles = peer.requestedRoles; + grant.grantedRoles = peer.grantedRoles; + grant.status = peer.grantStatus; + if (peer.subscribed) + { + grant.targetLatencyUs = peer.request.targetLatencyUs; + grant.maxPacketFrames = (std::min)( + peer.request.maxPacketFrames, AUDIO_MAX_FRAMES_PER_PACKET); + } + + action.payload.resize( + LG_NET_AUDIO_SUBSCRIPTION_GRANT_WIRE_SIZE); + if (!lgNetAudioSubscriptionGrantEncode(action.payload.data(), + action.payload.size(), &grant)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_SUBSCRIPTION_GRANT; + action.flags = LG_NET_ENVELOPE_RESPONSE | + LG_NET_ENVELOPE_FINAL; + action.effect = peer.expiring ? PumpEffect::EXPIRY : + (renewal ? PumpEffect::RENEWAL : PumpEffect::NONE); + action.requestID = peer.requestID; + return true; + } + + case PumpStage::PLAYBACK_STOP: + if (peer.announcedPlayback && + (peer.resetPlayback || !PlaybackCurrentNL(peer) || + peer.announcedPlayback != m_state.playback.generation)) + { + LGNetAudioControl control = {}; + control.streamID = PLAYBACK_STREAM_ID; + control.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + control.flags = LG_NET_AUDIO_CONTROL_FLUSH; + control.sequence = NextSerial(peer.playbackSequence); + control.formatEpoch = peer.announcedPlayback; + action.payload.resize(LG_NET_AUDIO_CONTROL_WIRE_SIZE); + if (!lgNetAudioControlEncode(action.payload.data(), + action.payload.size(), &control)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_PLAYBACK_STOP; + action.effect = PumpEffect::PLAYBACK_STOPPED; + action.value = control.sequence; + return true; + } + peer.resetPlayback = false; + peer.pumpStage = PumpStage::CAPTURE_STOP; + break; + + case PumpStage::CAPTURE_STOP: + if (peer.announcedCapture && + (peer.resetCapture || !CaptureCurrentNL(peer) || + peer.announcedCapture != m_state.capture.generation)) + { + LGNetAudioControl control = {}; + control.streamID = CAPTURE_STREAM_ID; + control.direction = LG_NET_AUDIO_DIRECTION_CAPTURE; + control.flags = LG_NET_AUDIO_CONTROL_FLUSH; + control.sequence = NextSerial(peer.captureSequence); + control.formatEpoch = peer.announcedCapture; + action.payload.resize(LG_NET_AUDIO_CONTROL_WIRE_SIZE); + if (!lgNetAudioControlEncode(action.payload.data(), + action.payload.size(), &control)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_CAPTURE_STOP; + action.effect = PumpEffect::CAPTURE_STOPPED; + action.value = control.sequence; + return true; + } + peer.resetCapture = false; + peer.pumpStage = PumpStage::PLAYBACK_START; + break; + + case PumpStage::PLAYBACK_START: + if (PlaybackCurrentNL(peer) && + peer.announcedPlayback != m_state.playback.generation) + { + LGNetAudioFormat format = {}; + if (!WireFormatNL(peer, true, format)) + return false; + action.payload.resize(LG_NET_AUDIO_FORMAT_WIRE_SIZE); + if (!lgNetAudioFormatEncode(action.payload.data(), + action.payload.size(), &format)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_PLAYBACK_START; + action.effect = PumpEffect::PLAYBACK_STARTED; + action.value = format.formatEpoch; + return true; + } + peer.pumpStage = PumpStage::CAPTURE_START; + break; + + case PumpStage::PLAYBACK_VOLUME: + { + LGNetAudioFormat format = {}; + if (!WireFormatNL(peer, true, format)) + return false; + LGNetAudioVolume volume = {}; + volume.streamID = PLAYBACK_STREAM_ID; + volume.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + volume.channelCount = format.channels; + volume.sequence = NextSerial(peer.playbackSequence); + volume.channelMask = format.channelMask; + const size_t wireSize = lgNetAudioVolumeSize(&volume); + action.payload.resize(wireSize); + if (!wireSize || !lgNetAudioVolumeEncode(action.payload.data(), + action.payload.size(), &volume)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_PLAYBACK_VOLUME; + action.effect = PumpEffect::STATE_SEQUENCE; + action.value = volume.sequence; + action.playback = true; + return true; + } + + case PumpStage::PLAYBACK_MUTE: + { + LGNetAudioMute mute = {}; + mute.streamID = PLAYBACK_STREAM_ID; + mute.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + mute.sequence = NextSerial(peer.playbackSequence); + mute.channelMask = m_state.playback.format.channelMask; + action.payload.resize(LG_NET_AUDIO_MUTE_WIRE_SIZE); + if (!lgNetAudioMuteEncode(action.payload.data(), + action.payload.size(), &mute)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_PLAYBACK_MUTE; + action.effect = PumpEffect::STATE_SEQUENCE; + action.value = mute.sequence; + action.playback = true; + return true; + } + + case PumpStage::CAPTURE_START: + if (CaptureCurrentNL(peer) && + peer.announcedCapture != m_state.capture.generation) + { + LGNetAudioFormat format = {}; + if (!WireFormatNL(peer, false, format)) + return false; + action.payload.resize(LG_NET_AUDIO_FORMAT_WIRE_SIZE); + if (!lgNetAudioFormatEncode(action.payload.data(), + action.payload.size(), &format)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_CAPTURE_START; + action.effect = PumpEffect::CAPTURE_STARTED; + action.value = format.formatEpoch; + return true; + } + peer.pumpStage = PumpStage::PLAYBACK_STATUS; + break; + + case PumpStage::CAPTURE_VOLUME: + { + LGNetAudioFormat format = {}; + if (!WireFormatNL(peer, false, format)) + return false; + LGNetAudioVolume volume = {}; + volume.streamID = CAPTURE_STREAM_ID; + volume.direction = LG_NET_AUDIO_DIRECTION_CAPTURE; + volume.channelCount = format.channels; + volume.sequence = NextSerial(peer.captureSequence); + volume.channelMask = format.channelMask; + const size_t wireSize = lgNetAudioVolumeSize(&volume); + action.payload.resize(wireSize); + if (!wireSize || !lgNetAudioVolumeEncode(action.payload.data(), + action.payload.size(), &volume)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_CAPTURE_VOLUME; + action.effect = PumpEffect::STATE_SEQUENCE; + action.value = volume.sequence; + return true; + } + + case PumpStage::CAPTURE_MUTE: + { + LGNetAudioMute mute = {}; + mute.streamID = CAPTURE_STREAM_ID; + mute.direction = LG_NET_AUDIO_DIRECTION_CAPTURE; + mute.sequence = NextSerial(peer.captureSequence); + mute.channelMask = m_state.capture.format.channelMask; + action.payload.resize(LG_NET_AUDIO_MUTE_WIRE_SIZE); + if (!lgNetAudioMuteEncode(action.payload.data(), + action.payload.size(), &mute)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_CAPTURE_MUTE; + action.effect = PumpEffect::STATE_SEQUENCE; + action.value = mute.sequence; + return true; + } + + case PumpStage::PLAYBACK_STATUS: + if (peer.subscribed && + (peer.grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK)) + { + const bool current = PlaybackCurrentNL(peer); + LGNetAudioState state = {}; + state.streamID = PLAYBACK_STREAM_ID; + state.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + state.state = current ? + WireState(m_state.playback.state) : + static_cast( + LG_NET_AUDIO_STATE_STOPPED); + state.stateSequence = NextSerial(peer.playbackSequence); + state.formatEpoch = current ? m_state.playback.generation : 0; + state.volumeMillibels = 0; + state.flags = current ? + LG_NET_AUDIO_STATE_FORMAT_ACTIVE : 0; + action.payload.resize(LG_NET_AUDIO_STATE_WIRE_SIZE); + if (!lgNetAudioStateEncode(action.payload.data(), + action.payload.size(), &state)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_STATUS; + action.effect = PumpEffect::STATE_SEQUENCE; + action.value = state.stateSequence; + action.playback = true; + return true; + } + peer.pumpStage = PumpStage::CAPTURE_STATUS; + break; + + case PumpStage::CAPTURE_STATUS: + if (peer.subscribed && + (peer.grantedRoles & LG_NET_AUDIO_ROLE_CAPTURE)) + { + const bool current = CaptureCurrentNL(peer); + LGNetAudioState state = {}; + state.streamID = CAPTURE_STREAM_ID; + state.direction = LG_NET_AUDIO_DIRECTION_CAPTURE; + state.state = current ? + WireState(m_state.capture.state) : + static_cast( + LG_NET_AUDIO_STATE_STOPPED); + state.stateSequence = NextSerial(peer.captureSequence); + state.formatEpoch = current ? m_state.capture.generation : 0; + state.volumeMillibels = 0; + state.flags = current ? + LG_NET_AUDIO_STATE_FORMAT_ACTIVE : 0; + action.payload.resize(LG_NET_AUDIO_STATE_WIRE_SIZE); + if (!lgNetAudioStateEncode(action.payload.data(), + action.payload.size(), &state)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_STATUS; + action.effect = PumpEffect::STATE_SEQUENCE; + action.value = state.stateSequence; + return true; + } + peer.pumpStage = PumpStage::PLAYBACK_BARRIER; + break; + + case PumpStage::PLAYBACK_BARRIER: + if (peer.subscribed && + (peer.grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK)) + { + LGNetAudioBarrier barrier = {}; + barrier.streamID = PLAYBACK_STREAM_ID; + barrier.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + barrier.barrierID = NextSerial(peer.barrierID); + barrier.stateSequence = peer.playbackSequence; + action.requestID = barrier.barrierID; + action.payload.resize(LG_NET_AUDIO_BARRIER_WIRE_SIZE); + if (!lgNetAudioBarrierEncode(action.payload.data(), + action.payload.size(), &barrier)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_STATE_BARRIER; + action.flags = LG_NET_ENVELOPE_ACK_REQUIRED | + LG_NET_ENVELOPE_FINAL; + action.effect = PumpEffect::BARRIER; + action.value = barrier.barrierID; + action.playback = true; + return true; + } + peer.pumpStage = PumpStage::CAPTURE_BARRIER; + break; + + case PumpStage::CAPTURE_BARRIER: + if (peer.subscribed && + (peer.grantedRoles & LG_NET_AUDIO_ROLE_CAPTURE)) + { + LGNetAudioBarrier barrier = {}; + barrier.streamID = CAPTURE_STREAM_ID; + barrier.direction = LG_NET_AUDIO_DIRECTION_CAPTURE; + barrier.barrierID = NextSerial(peer.barrierID); + barrier.stateSequence = peer.captureSequence; + action.requestID = barrier.barrierID; + action.payload.resize(LG_NET_AUDIO_BARRIER_WIRE_SIZE); + if (!lgNetAudioBarrierEncode(action.payload.data(), + action.payload.size(), &barrier)) + return false; + action.messageType = LG_NET_AUDIO_MESSAGE_STATE_BARRIER; + action.flags = LG_NET_ENVELOPE_ACK_REQUIRED | + LG_NET_ENVELOPE_FINAL; + action.effect = PumpEffect::BARRIER; + action.value = barrier.barrierID; + action.playback = false; + return true; + } + peer.pumpStage = PumpStage::DONE; + break; + + case PumpStage::DONE: + return false; + } + } +} + +NetworkAudioSendResult CNetworkAudio::Impl::Send( + const NetworkAudioPeer& peer, bool datagram, uint16_t messageType, + LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) +{ + CSRWSharedLock callbackLock(m_senderCallbackLock); + INetworkAudioSender * sender; + { + CSRWSharedLock lock(m_lock); + sender = m_sender; + } + if (!sender) + return NetworkAudioSendResult::FAILED; + + return sender->SendAudio(peer, datagram, LG_NET_SERVICE_AUDIO, + messageType, flags, componentEpoch, requestID, payload, payloadLength); +} + +void CNetworkAudio::Impl::ApplyPumpResultNL( + PeerState& peer, const PumpAction& action) +{ + switch (action.effect) + { + case PumpEffect::NONE: + peer.pumpGrant = false; + peer.resetPlayback = peer.announcedPlayback != 0; + peer.resetCapture = peer.announcedCapture != 0; + peer.pumpStage = PumpStage::PLAYBACK_STOP; + break; + + case PumpEffect::RENEWAL: + peer.pumpStage = PumpStage::DONE; + peer.ready = peer.subscribed && !peer.pendingBarriers; + break; + + case PumpEffect::EXPIRY: + ClearSubscriptionNL(peer); + break; + + case PumpEffect::PLAYBACK_STOPPED: + peer.playbackSequence = action.value; + peer.announcedPlayback = 0; + peer.resetPlayback = false; + peer.pumpStage = PumpStage::CAPTURE_STOP; + break; + + case PumpEffect::CAPTURE_STOPPED: + peer.captureSequence = action.value; + peer.announcedCapture = 0; + peer.resetCapture = false; + peer.pumpStage = PumpStage::PLAYBACK_START; + break; + + case PumpEffect::PLAYBACK_STARTED: + peer.announcedPlayback = action.value; + peer.pumpStage = PumpStage::PLAYBACK_VOLUME; + break; + + case PumpEffect::CAPTURE_STARTED: + peer.announcedCapture = action.value; + peer.pumpStage = PumpStage::CAPTURE_VOLUME; + break; + + case PumpEffect::STATE_SEQUENCE: + if (action.playback) + peer.playbackSequence = action.value; + else + peer.captureSequence = action.value; + switch (peer.pumpStage) + { + case PumpStage::PLAYBACK_VOLUME: + peer.pumpStage = PumpStage::PLAYBACK_MUTE; + break; + + case PumpStage::PLAYBACK_MUTE: + peer.pumpStage = PumpStage::CAPTURE_START; + break; + + case PumpStage::CAPTURE_VOLUME: + peer.pumpStage = PumpStage::CAPTURE_MUTE; + break; + + case PumpStage::CAPTURE_MUTE: + peer.pumpStage = PumpStage::PLAYBACK_STATUS; + break; + + case PumpStage::PLAYBACK_STATUS: + peer.pumpStage = PumpStage::CAPTURE_STATUS; + break; + + case PumpStage::CAPTURE_STATUS: + peer.pumpStage = PumpStage::PLAYBACK_BARRIER; + break; + + default: + peer.pumpStage = PumpStage::DONE; + break; + } + break; + + case PumpEffect::BARRIER: + peer.barrierID = action.value; + if (action.playback) + peer.pumpStage = PumpStage::CAPTURE_BARRIER; + else + peer.pumpStage = PumpStage::DONE; + peer.awaitingAck = peer.pendingBarriers != 0; + peer.ready = !peer.awaitingAck && + peer.pumpStage == PumpStage::DONE; + break; + } +} + +void CNetworkAudio::Impl::PublishBarrierNL( + PeerState& peer, const PumpAction& action) +{ + const uint8_t barrierBit = action.playback ? 1U : 2U; + uint64_t& barrierID = action.playback ? + peer.playbackBarrierID : peer.captureBarrierID; + + barrierID = action.value; + peer.pendingBarriers |= barrierBit; + peer.awaitingAck = true; + peer.ready = false; +} + +void CNetworkAudio::Impl::RollbackBarrierNL( + PeerState& peer, const PumpAction& action) +{ + const uint8_t barrierBit = action.playback ? 1U : 2U; + uint64_t& barrierID = action.playback ? + peer.playbackBarrierID : peer.captureBarrierID; + if (barrierID != action.value) + return; + + barrierID = 0; + peer.pendingBarriers &= static_cast(~barrierBit); + peer.awaitingAck = peer.pendingBarriers != 0; + peer.ready = false; +} + +void CNetworkAudio::Impl::PumpPeer(const NetworkAudioPeer& key) +{ + for (unsigned iteration = 0; iteration < 16U; ++iteration) + { + PumpAction action; + bool built = false; + try + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (!peer || peer->pumpInFlight) + return; + if (peer->pumpStage == PumpStage::DONE) + { + if (peer->subscribed && !peer->pendingBarriers) + peer->ready = true; + if (m_playbackWaiting && + SamePeer(m_playbackWaitingPeer, peer->peer) && + (peer->ready || !peer->subscribed)) + { + m_playbackWaiting = false; + m_playbackReadyPending = true; + } + return; + } + peer->pumpInFlight = true; + built = BuildPumpActionNL(*peer, action); + if (!built) + { + peer->pumpInFlight = false; + if (peer->pumpStage != PumpStage::DONE) + { + DEBUG_ERROR("Failed to encode network audio state"); + peer->pumpStage = PumpStage::DONE; + MarkFailed("network audio state encoding"); + } + else + { + if (peer->subscribed && !peer->pendingBarriers) + peer->ready = true; + if (m_playbackWaiting && + SamePeer(m_playbackWaitingPeer, peer->peer) && + (peer->ready || !peer->subscribed)) + { + m_playbackWaiting = false; + m_playbackReadyPending = true; + } + } + return; + } + if (action.effect == PumpEffect::BARRIER) + PublishBarrierNL(*peer, action); + } + catch (...) + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (peer) + { + peer->pumpInFlight = false; + peer->pumpStage = PumpStage::DONE; + } + MarkFailed("network audio state allocation"); + return; + } + + const NetworkAudioSendResult result = Send(action.peer, false, + action.messageType, action.flags, action.componentEpoch, + action.requestID, action.payload.data(), + static_cast(action.payload.size())); + + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(action.peer); + if (!peer || peer->serial != action.peerSerial) + return; + peer->pumpInFlight = false; + if (result == NetworkAudioSendResult::FAILED) + { + ClearSubscriptionNL(*peer); + RebalanceRolesNL(); + return; + } + if (result == NetworkAudioSendResult::BLOCKED) + { + if (peer->pumpToken == action.pumpToken && + peer->subscriptionEpoch == action.componentEpoch && + action.effect == PumpEffect::BARRIER) + RollbackBarrierNL(*peer, action); + return; + } + if (peer->pumpToken != action.pumpToken || + peer->subscriptionEpoch != action.componentEpoch) + { + if (peer->subscriptionEpoch == action.componentEpoch) + { + switch (action.effect) + { + case PumpEffect::PLAYBACK_STOPPED: + peer->playbackSequence = action.value; + peer->announcedPlayback = 0; + break; + + case PumpEffect::CAPTURE_STOPPED: + peer->captureSequence = action.value; + peer->announcedCapture = 0; + break; + + case PumpEffect::PLAYBACK_STARTED: + peer->announcedPlayback = action.value; + break; + + case PumpEffect::CAPTURE_STARTED: + peer->announcedCapture = action.value; + break; + + case PumpEffect::STATE_SEQUENCE: + if (action.playback) + peer->playbackSequence = action.value; + else + peer->captureSequence = action.value; + break; + + case PumpEffect::NONE: + case PumpEffect::RENEWAL: + case PumpEffect::EXPIRY: + case PumpEffect::BARRIER: + break; + } + } + continue; + } + ApplyPumpResultNL(*peer, action); + } +} + +void CNetworkAudio::Impl::PumpAll() +{ + std::vector peers; + try + { + CSRWSharedLock lock(m_lock); + peers.reserve(m_peers.size()); + for (const PeerState& peer : m_peers) + if (peer.pumpStage != PumpStage::DONE) + peers.push_back(peer.peer); + } + catch (...) + { + CSRWExclusiveLock lock(m_lock); + MarkFailed("network audio pump allocation"); + return; + } + + for (const NetworkAudioPeer& peer : peers) + PumpPeer(peer); +} + +NetworkAudioPacketResult CNetworkAudio::Impl::HandleSubscribe( + PeerState& peer, const LGNetPacketView& packet, uint64_t now) +{ + LGNetAudioSubscribe subscribe = {}; + if (lgNetAudioSubscribeDecodeForVersion(&subscribe, + packet.envelope.serviceVersion, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK) + return NetworkAudioPacketResult::INVALID; + + const LGNetAudioRoles requestedRoles = + lgNetAudioSubscribeRoles(&subscribe); + if (peer.hasRequest && peer.subscribed && !peer.expiring && + peer.requestID == packet.envelope.requestID && + peer.requestedRoles == requestedRoles && + SameSubscription(peer.request, subscribe)) + { + peer.deadline = now + AUDIO_LEASE_MS; + if (!peer.pumpInFlight && peer.pumpStage == PumpStage::DONE) + { + peer.pumpStage = PumpStage::RENEWAL_GRANT; + peer.pumpToken = NextSerial(peer.pumpToken); + } + return NetworkAudioPacketResult::HANDLED; + } + + peer.hasRequest = true; + peer.expiring = false; + peer.feedbackRevoked = false; + peer.captureRevoked = false; + peer.request = subscribe; + peer.requestID = packet.envelope.requestID; + peer.requestedRoles = requestedRoles; + peer.deadline = now + AUDIO_LEASE_MS; + peer.captureClock = {}; + peer.playbackPacket = {}; + RebalanceRolesNL(peer.serial); + return NetworkAudioPacketResult::HANDLED; +} + +NetworkAudioPacketResult CNetworkAudio::Impl::HandleClock( + const NetworkAudioPeer& key, const LGNetPacketView& packet) +{ + LGNetAudioClockState clock = {}; + if (lgNetAudioClockStateDecode(&clock, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK) + return NetworkAudioPacketResult::INVALID; + + if (clock.direction == LG_NET_AUDIO_DIRECTION_CAPTURE) + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (!peer || !peer->hasRequest || + peer->subscriptionEpoch != packet.envelope.componentEpoch) + return NetworkAudioPacketResult::IGNORED; + if (clock.subscriptionEpoch != peer->subscriptionEpoch || + clock.streamID != CAPTURE_STREAM_ID) + return NetworkAudioPacketResult::INVALID; + + peer->deadline = GetTickCount64() + AUDIO_LEASE_MS; + LGNetAudioFormat format = {}; + if (!peer->captureOwner || + !peer->ready || !CaptureCurrentNL(*peer) || + !WireFormatNL(*peer, false, format) || + clock.formatEpoch != format.formatEpoch) + return NetworkAudioPacketResult::IGNORED; + if (peer->capturePacketKnown && + clock.packetID <= peer->capturePacketID) + return NetworkAudioPacketResult::IGNORED; + + ClockRecord * record = nullptr; + for (ClockRecord& candidate : peer->captureClock) + if (candidate.valid && candidate.state.packetID == clock.packetID && + candidate.state.formatEpoch == clock.formatEpoch) + { + record = &candidate; + break; + } + if (!record) + record = &peer->captureClock[ + peer->captureClockNext++ % CLOCK_CACHE_SIZE]; + record->valid = true; + record->state = clock; + return NetworkAudioPacketResult::HANDLED; + } + + if (clock.direction != LG_NET_AUDIO_DIRECTION_PLAYBACK || + clock.streamID != PLAYBACK_STREAM_ID) + return NetworkAudioPacketResult::INVALID; + + CSRWSharedLock callbackLock(m_targetCallbackLock); + IAudioTarget * target = nullptr; + AudioClockFeedback feedback = {}; + uint64_t peerSerial = 0; + uint64_t subscriptionEpoch = 0; + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (!peer || !peer->hasRequest || + peer->subscriptionEpoch != packet.envelope.componentEpoch) + return NetworkAudioPacketResult::IGNORED; + if (clock.subscriptionEpoch != peer->subscriptionEpoch) + return NetworkAudioPacketResult::INVALID; + peer->deadline = GetTickCount64() + AUDIO_LEASE_MS; + + if (!peer->feedbackAuthority || !peer->ready || + !PlaybackCurrentNL(*peer) || + clock.formatEpoch != m_state.playback.generation) + return NetworkAudioPacketResult::IGNORED; + + bool knownPacket = false; + for (const PacketRecord& record : peer->playbackPacket) + if (record.valid && record.packetID == clock.packetID && + record.formatEpoch == clock.formatEpoch) + { + knownPacket = true; + break; + } + if (!knownPacket) + return NetworkAudioPacketResult::IGNORED; + if (!(clock.flags & LG_NET_AUDIO_CLOCK_VALID) || + !clock.targetRateQ32) + return NetworkAudioPacketResult::HANDLED; + + feedback.endpointGeneration = m_state.generation; + feedback.playbackGeneration = m_state.playback.generation; + feedback.position = clock.framePosition; + feedback.timeNs = clock.timeNs; + feedback.rateQ32 = clock.rateQ32; + feedback.targetRateQ32 = clock.targetRateQ32; + if (clock.flags & LG_NET_AUDIO_CLOCK_VALID) + feedback.flags |= AUDIO_CLOCK_VALID; + if (clock.flags & LG_NET_AUDIO_CLOCK_STABLE) + feedback.flags |= AUDIO_CLOCK_STABLE; + target = m_started ? m_target : nullptr; + peerSerial = peer->serial; + subscriptionEpoch = peer->subscriptionEpoch; + } + + if (!target) + return NetworkAudioPacketResult::IGNORED; + const AudioResult result = target->SetClockFeedback(feedback); + if (result == AudioResult::FAILED || result == AudioResult::REJECTED) + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (peer && peer->serial == peerSerial && + peer->subscriptionEpoch == subscriptionEpoch && + peer->feedbackAuthority && + m_state.playback.generation == feedback.playbackGeneration) + { + peer->feedbackAuthority = false; + peer->feedbackRevoked = true; + RebalanceRolesNL(); + } + } + return NetworkAudioPacketResult::HANDLED; +} + +NetworkAudioPacketResult CNetworkAudio::Impl::HandleCapture( + const NetworkAudioPeer& key, const LGNetPacketView& packet) +{ + LGNetAudioData audio = {}; + if (lgNetAudioDataDecode(&audio, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK) + return NetworkAudioPacketResult::INVALID; + if (audio.direction != LG_NET_AUDIO_DIRECTION_CAPTURE || + audio.streamID != CAPTURE_STREAM_ID || + audio.frameCount > AUDIO_MAX_FRAMES_PER_PACKET || + audio.dataLength > AUDIO_MAX_PCM_BYTES) + return NetworkAudioPacketResult::INVALID; + + CSRWSharedLock callbackLock(m_targetCallbackLock); + IAudioTarget * target = nullptr; + AudioPacket capture = {}; + uint64_t peerSerial = 0; + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (!peer || !peer->hasRequest || + peer->subscriptionEpoch != packet.envelope.componentEpoch) + return NetworkAudioPacketResult::IGNORED; + + LGNetAudioFormat format = {}; + if (!peer->captureOwner || !peer->ready || !CaptureCurrentNL(*peer) || + !WireFormatNL(*peer, false, format)) + { + peer->captureGap = true; + return NetworkAudioPacketResult::IGNORED; + } + if (!lgNetAudioDataMatchesFormat(&audio, &format)) + return NetworkAudioPacketResult::INVALID; + peer->deadline = GetTickCount64() + AUDIO_LEASE_MS; + + if (peer->capturePacketKnown && + audio.packetID <= peer->capturePacketID) + return NetworkAudioPacketResult::IGNORED; + const bool sequenceGap = peer->capturePacketKnown && + audio.packetID != peer->capturePacketID + 1U; + peer->capturePacketKnown = true; + peer->capturePacketID = audio.packetID; + if (m_capture.valid || m_capture.callback) + { + peer->captureGap = true; + return NetworkAudioPacketResult::IGNORED; + } + + capture.endpointGeneration = m_state.generation; + capture.streamGeneration = m_state.capture.generation; + capture.frames = audio.frameCount; + capture.format = m_state.capture.format; + if (audio.flags & LG_NET_AUDIO_DATA_SILENT) + capture.flags |= AUDIO_PACKET_SILENT; + if ((audio.flags & (LG_NET_AUDIO_DATA_DISCONTINUITY | + LG_NET_AUDIO_DATA_END_OF_STREAM)) || peer->captureGap || + sequenceGap) + capture.flags |= AUDIO_PACKET_DISCONTINUITY; + + bool clockFound = false; + for (ClockRecord& record : peer->captureClock) + if (record.valid && record.state.packetID == audio.packetID && + record.state.formatEpoch == audio.formatEpoch) + { + const LGNetAudioClockState& clock = record.state; + if (clock.flags & LG_NET_AUDIO_CLOCK_VALID) + { + capture.position = clock.framePosition; + capture.timeNs = clock.timeNs; + capture.rateQ32 = clock.rateQ32; + capture.flags |= AUDIO_PACKET_CLOCK_VALID; + } + if (clock.flags & LG_NET_AUDIO_CLOCK_STABLE) + capture.flags |= AUDIO_PACKET_CLOCK_STABLE; + record.valid = false; + clockFound = true; + break; + } + if (!clockFound) + capture.flags |= AUDIO_PACKET_DISCONTINUITY; + + target = m_started ? m_target : nullptr; + peerSerial = peer->serial; + if (!target) + { + peer->captureGap = true; + return NetworkAudioPacketResult::IGNORED; + } + peer->captureGap = false; + m_capture.callback = true; + } + + const AudioResult result = target->SendCapture( + capture, audio.data, audio.dataLength); + + CSRWExclusiveLock lock(m_lock); + m_capture.callback = false; + PeerState * peer = FindPeerNL(key); + if (!peer || peer->serial != peerSerial || !peer->captureOwner || + peer->subscriptionEpoch != packet.envelope.componentEpoch) + return NetworkAudioPacketResult::IGNORED; + if (result == AudioResult::BUSY) + { + m_capture.valid = true; + m_capture.peer = key; + m_capture.peerSerial = peerSerial; + m_capture.packet = capture; + m_capture.size = audio.dataLength; + if (audio.dataLength) + memcpy(m_capture.data.data(), audio.data, audio.dataLength); + return NetworkAudioPacketResult::HANDLED; + } + + if (result == AudioResult::FAILED || result == AudioResult::REJECTED) + { + peer->captureRevoked = true; + peer->captureGap = true; + RebalanceRolesNL(); + } + else if (result != AudioResult::ACCEPTED) + peer->captureGap = true; + return NetworkAudioPacketResult::HANDLED; +} + +NetworkAudioPacketResult CNetworkAudio::Impl::HandleCurrentPacket( + const NetworkAudioPeer& key, const LGNetPacketView& packet, + bool datagram, uint64_t now) +{ + switch (packet.envelope.messageType) + { + case LG_NET_AUDIO_MESSAGE_CLOCK_STATE: + if (packet.envelope.requestID || + !DeliveryFlags(packet.envelope, datagram, + LG_NET_ENVELOPE_FINAL)) + return NetworkAudioPacketResult::INVALID; + return HandleClock(key, packet); + + case LG_NET_AUDIO_MESSAGE_CAPTURE_DATA: + if (packet.envelope.requestID || + !DeliveryFlags(packet.envelope, datagram, + LG_NET_ENVELOPE_FINAL)) + return NetworkAudioPacketResult::INVALID; + return HandleCapture(key, packet); + + case LG_NET_AUDIO_MESSAGE_STATE_ACK: + { + if (datagram || !packet.envelope.requestID || + !ReliableFlags(packet.envelope, + LG_NET_ENVELOPE_RESPONSE | LG_NET_ENVELOPE_FINAL)) + return NetworkAudioPacketResult::INVALID; + LGNetAudioBarrier barrier = {}; + if (lgNetAudioBarrierDecode(&barrier, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK) + return NetworkAudioPacketResult::INVALID; + + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (!peer || !peer->hasRequest || + peer->subscriptionEpoch != packet.envelope.componentEpoch) + return NetworkAudioPacketResult::IGNORED; + peer->deadline = now + AUDIO_LEASE_MS; + uint8_t barrierBit; + uint64_t barrierID; + uint64_t stateSequence; + if (barrier.streamID == PLAYBACK_STREAM_ID && + barrier.direction == LG_NET_AUDIO_DIRECTION_PLAYBACK && + (peer->grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK)) + { + barrierBit = 1U; + barrierID = peer->playbackBarrierID; + stateSequence = peer->playbackSequence; + } + else if (barrier.streamID == CAPTURE_STREAM_ID && + barrier.direction == LG_NET_AUDIO_DIRECTION_CAPTURE && + (peer->grantedRoles & LG_NET_AUDIO_ROLE_CAPTURE)) + { + barrierBit = 2U; + barrierID = peer->captureBarrierID; + stateSequence = peer->captureSequence; + } + else + return NetworkAudioPacketResult::INVALID; + if (!peer->awaitingAck || !(peer->pendingBarriers & barrierBit) || + packet.envelope.requestID != barrier.barrierID || + barrier.barrierID != barrierID || + barrier.stateSequence != stateSequence) + return NetworkAudioPacketResult::IGNORED; + peer->pendingBarriers &= static_cast(~barrierBit); + peer->awaitingAck = peer->pendingBarriers != 0; + peer->ready = !peer->awaitingAck && + peer->pumpStage == PumpStage::DONE; + if (m_playbackWaiting && SamePeer(m_playbackWaitingPeer, key)) + { + m_playbackWaiting = false; + m_playbackReadyPending = true; + } + return NetworkAudioPacketResult::HANDLED; + } + + case LG_NET_AUDIO_MESSAGE_KEEPALIVE: + case LG_NET_AUDIO_MESSAGE_RELEASE: + { + if ((packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_RELEASE && + datagram) || + packet.envelope.requestID || + !DeliveryFlags(packet.envelope, datagram, + LG_NET_ENVELOPE_FINAL)) + return NetworkAudioPacketResult::INVALID; + LGNetAudioControl control = {}; + if (lgNetAudioControlDecode(&control, packet.payload, + packet.payloadLength) != LG_NET_PARSE_OK) + return NetworkAudioPacketResult::INVALID; + + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(key); + if (!peer || !peer->hasRequest || + peer->subscriptionEpoch != packet.envelope.componentEpoch) + return NetworkAudioPacketResult::IGNORED; + const bool playback = control.streamID == PLAYBACK_STREAM_ID && + control.direction == LG_NET_AUDIO_DIRECTION_PLAYBACK; + const bool capture = control.streamID == CAPTURE_STREAM_ID && + control.direction == LG_NET_AUDIO_DIRECTION_CAPTURE; + if ((!playback && !capture) || control.detail || + (playback && + !(peer->grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK)) || + (capture && + !(peer->grantedRoles & LG_NET_AUDIO_ROLE_CAPTURE)) || + (packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_KEEPALIVE && + control.flags)) + return NetworkAudioPacketResult::INVALID; + peer->deadline = now + AUDIO_LEASE_MS; + if (packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_RELEASE) + { + if (m_capture.valid && SamePeer(m_capture.peer, key)) + m_capture.valid = false; + ClearSubscriptionNL(*peer); + RebalanceRolesNL(); + } + return NetworkAudioPacketResult::HANDLED; + } + + default: + return NetworkAudioPacketResult::INVALID; + } +} + +NetworkAudioSendResult CNetworkAudio::Impl::SendPCM( + const PlaybackTarget& target, uint16_t messageType, + const void * payload, uint32_t payloadLength) +{ + /* CLOCK_STATE and PCM are an ordered pair. Keep both on the same reliable + * stream until a future protocol version provides atomic datagram framing. */ + if (!(target.roles & LG_NET_AUDIO_ROLE_RELIABLE_PCM) || + payloadLength > target.info.maxReliablePayload) + return NetworkAudioSendResult::FAILED; + + return Send(target.peer, false, messageType, LG_NET_ENVELOPE_FINAL, + target.subscriptionEpoch, 0, payload, payloadLength); +} + +AudioResult CNetworkAudio::Impl::BeginPlayback( + const AudioPacket& packet, size_t size) +{ + if (!m_started) + return AudioResult::UNAVAILABLE; + if (m_failed) + return AudioResult::FAILED; + if (!m_stateKnown || !m_state.available || !m_state.active || + !(m_state.capabilities & AUDIO_CAP_PLAYBACK) || + !m_state.playback.formatValid || + m_state.playback.state != AudioStreamState::RUNNING) + return AudioResult::UNAVAILABLE; + if (packet.endpointGeneration != m_state.generation || + packet.streamGeneration != m_state.playback.generation) + return AudioResult::STALE; + if (!SameFormat(packet.format, m_state.playback.format)) + return AudioResult::UNAVAILABLE; + + PendingPlayback pending; + pending.valid = true; + pending.packet = packet; + pending.size = size; + try + { + pending.targets.reserve(m_peers.size()); + for (const PeerState& peer : m_peers) + { + if (!peer.subscribed || + !(peer.grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK) || + !PlaybackCurrentNL(peer)) + continue; + + LGNetAudioFormat format = {}; + if (!WireFormatNL(peer, true, format)) + continue; + PlaybackTarget target; + target.peer = peer.peer; + target.info = peer.info; + target.peerSerial = peer.serial; + target.subscriptionEpoch = peer.subscriptionEpoch; + target.formatEpoch = format.formatEpoch; + target.maxPacketFrames = format.maxPacketFrames; + target.roles = peer.grantedRoles; + target.authority = peer.feedbackAuthority; + target.discontinuity = peer.playbackGap; + pending.targets.push_back(target); + } + std::stable_sort(pending.targets.begin(), pending.targets.end(), + [](const PlaybackTarget& left, const PlaybackTarget& right) + { + return left.authority && !right.authority; + }); + } + catch (...) + { + return AudioResult::FAILED; + } + + if (pending.targets.empty()) + return AudioResult::ACCEPTED; + m_playback = std::move(pending); + return AudioResult::BUSY; +} + +uint64_t CNetworkAudio::Impl::PlaybackReadySerial( + const PlaybackTarget& target) +{ + CSRWSharedLock lock(m_lock); + const PeerState * peer = FindPeerNL(target.peer); + if (!peer || peer->serial != target.peerSerial || + peer->subscriptionEpoch != target.subscriptionEpoch) + return 0; + return peer->sendReadySerial; +} + +CNetworkAudio::Impl::BlockDisposition +CNetworkAudio::Impl::HandlePlaybackBlock( + const PlaybackTarget& target, uint64_t readySerial) +{ + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(target.peer); + if (!peer || peer->serial != target.peerSerial || + peer->subscriptionEpoch != target.subscriptionEpoch || + !peer->feedbackAuthority) + return BlockDisposition::STALE; + if (peer->sendReadySerial != readySerial) + return BlockDisposition::RETRY; + + m_playbackWaiting = true; + m_playbackWaitingPeer = target.peer; + return BlockDisposition::WAIT; +} + +AudioResult CNetworkAudio::Impl::ContinuePlayback(const uint8_t * data) +{ + while (m_playback.valid && + m_playback.target < m_playback.targets.size()) + { + PlaybackTarget& target = + m_playback.targets[m_playback.target]; + bool ready = false; + bool authority = false; + bool gap = target.discontinuity; + { + CSRWSharedLock lock(m_lock); + const PeerState * peer = FindPeerNL(target.peer); + if (!peer || peer->serial != target.peerSerial || + peer->subscriptionEpoch != target.subscriptionEpoch || + !peer->subscribed || + !(peer->grantedRoles & LG_NET_AUDIO_ROLE_PLAYBACK) || + !PlaybackCurrentNL(*peer)) + { + ++m_playback.target; + m_playback.frameOffset = 0; + m_playback.chunkFrames = 0; + m_playback.packetID = 0; + m_playback.clockSent = false; + continue; + } + ready = peer->ready && peer->pumpStage == PumpStage::DONE; + authority = target.authority && peer->feedbackAuthority; + gap = gap || peer->playbackGap; + } + + if (!ready) + { + if (authority) + { + CSRWExclusiveLock lock(m_lock); + m_playbackWaiting = true; + m_playbackWaitingPeer = target.peer; + return AudioResult::BUSY; + } + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(target.peer); + if (peer && peer->serial == target.peerSerial) + peer->playbackGap = true; + ++m_playback.target; + m_playback.frameOffset = 0; + m_playback.chunkFrames = 0; + m_playback.packetID = 0; + m_playback.clockSent = false; + continue; + } + + const uint32_t remaining = + m_playback.packet.frames - m_playback.frameOffset; + if (!m_playback.chunkFrames) + { + const uint32_t blockAlign = m_playback.packet.format.blockAlign; + uint32_t capacity = 0; + if (target.roles & LG_NET_AUDIO_ROLE_DATAGRAM_PCM) + { + const uint32_t bytes = target.info.maxDatagramPayload > + LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE ? + target.info.maxDatagramPayload - + LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE : 0; + capacity = (m_playback.packet.flags & AUDIO_PACKET_SILENT) ? + target.maxPacketFrames : bytes / blockAlign; + } + if (!capacity && + (target.roles & LG_NET_AUDIO_ROLE_RELIABLE_PCM)) + { + const uint32_t bytes = target.info.maxReliablePayload > + LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE ? + target.info.maxReliablePayload - + LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE : 0; + capacity = (m_playback.packet.flags & AUDIO_PACKET_SILENT) ? + target.maxPacketFrames : bytes / blockAlign; + } + capacity = (std::min)(capacity, target.maxPacketFrames); + capacity = (std::min)(capacity, AUDIO_MAX_FRAMES_PER_PACKET); + if (!capacity) + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(target.peer); + if (peer && peer->serial == target.peerSerial) + peer->playbackGap = true; + ++m_playback.target; + continue; + } + m_playback.chunkFrames = (std::min)(remaining, capacity); + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(target.peer); + if (!peer || peer->serial != target.peerSerial || + peer->subscriptionEpoch != target.subscriptionEpoch) + { + ++m_playback.target; + m_playback.frameOffset = 0; + m_playback.chunkFrames = 0; + continue; + } + peer->playbackPacketID = NextSerial(peer->playbackPacketID); + m_playback.packetID = peer->playbackPacketID; + } + } + + const uint32_t frameOffset = m_playback.frameOffset; + const uint32_t frames = m_playback.chunkFrames; + const uint64_t timeAdvance = + static_cast(frameOffset) * UINT64_C(1000000000) / + m_playback.packet.format.sampleRate; + const bool clockValid = + (m_playback.packet.flags & AUDIO_PACKET_CLOCK_VALID) != 0; + const bool clockStable = + (m_playback.packet.flags & AUDIO_PACKET_CLOCK_STABLE) != 0; + + LGNetAudioClockState clock = {}; + clock.streamID = PLAYBACK_STREAM_ID; + clock.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + clock.flags = clockValid ? LG_NET_AUDIO_CLOCK_VALID : 0; + if (clockStable) + clock.flags |= LG_NET_AUDIO_CLOCK_STABLE; + clock.formatEpoch = target.formatEpoch; + clock.subscriptionEpoch = target.subscriptionEpoch; + clock.packetID = m_playback.packetID; + clock.framePosition = m_playback.packet.position + frameOffset; + clock.timeNs = clockValid ? + m_playback.packet.timeNs + static_cast(timeAdvance) : 0; + clock.rateQ32 = clockValid ? m_playback.packet.rateQ32 : 0; + clock.targetRateQ32 = 0; + + std::array clockPayload; + if (!lgNetAudioClockStateEncode(clockPayload.data(), + clockPayload.size(), &clock)) + return AudioResult::FAILED; + if (!m_playback.clockSent) + { + const uint64_t readySerial = PlaybackReadySerial(target); + const NetworkAudioSendResult result = SendPCM(target, + LG_NET_AUDIO_MESSAGE_CLOCK_STATE, clockPayload.data(), + static_cast(clockPayload.size())); + if (result != NetworkAudioSendResult::ACCEPTED) + { + if (authority && result == NetworkAudioSendResult::BLOCKED) + { + const BlockDisposition disposition = + HandlePlaybackBlock(target, readySerial); + if (disposition == BlockDisposition::RETRY) + continue; + if (disposition == BlockDisposition::WAIT) + { + if (!m_playback.blockedSince) + m_playback.blockedSince = GetTickCount64(); + return AudioResult::BUSY; + } + } + + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(target.peer); + if (peer && peer->serial == target.peerSerial && + peer->subscriptionEpoch == target.subscriptionEpoch) + { + peer->playbackGap = true; + if (authority && peer->feedbackAuthority) + { + peer->feedbackAuthority = false; + peer->feedbackRevoked = true; + RebalanceRolesNL(); + } + } + ++m_playback.target; + m_playback.frameOffset = 0; + m_playback.chunkFrames = 0; + m_playback.packetID = 0; + m_playback.clockSent = false; + continue; + } + m_playback.clockSent = true; + } + + const int64_t packetTime = m_playback.packet.timeNs + + static_cast(timeAdvance); + LGNetAudioData audio = {}; + audio.streamID = PLAYBACK_STREAM_ID; + audio.direction = LG_NET_AUDIO_DIRECTION_PLAYBACK; + audio.formatEpoch = target.formatEpoch; + audio.packetID = m_playback.packetID; + audio.timestampNs = packetTime > 0 ? + static_cast(packetTime) : MonotonicNs(); + audio.startFrame = m_playback.packet.position + frameOffset; + audio.frameCount = frames; + if (!frameOffset && + ((m_playback.packet.flags & AUDIO_PACKET_DISCONTINUITY) || gap)) + audio.flags |= LG_NET_AUDIO_DATA_DISCONTINUITY; + if (m_playback.packet.flags & AUDIO_PACKET_SILENT) + audio.flags |= LG_NET_AUDIO_DATA_SILENT; + else + { + audio.dataLength = frames * m_playback.packet.format.blockAlign; + audio.data = data + + static_cast(frameOffset) * + m_playback.packet.format.blockAlign; + } + + const size_t wireSize = lgNetAudioDataSize(&audio); + std::vector payload; + try + { + payload.resize(wireSize); + } + catch (...) + { + return AudioResult::FAILED; + } + if (!wireSize || !lgNetAudioDataEncode( + payload.data(), payload.size(), &audio)) + return AudioResult::FAILED; + + const uint64_t readySerial = PlaybackReadySerial(target); + const NetworkAudioSendResult result = SendPCM(target, + LG_NET_AUDIO_MESSAGE_PLAYBACK_DATA, payload.data(), + static_cast(payload.size())); + if (result != NetworkAudioSendResult::ACCEPTED) + { + if (authority && result == NetworkAudioSendResult::BLOCKED) + { + m_playback.clockSent = false; + const BlockDisposition disposition = + HandlePlaybackBlock(target, readySerial); + if (disposition == BlockDisposition::RETRY) + continue; + if (disposition == BlockDisposition::WAIT) + { + if (!m_playback.blockedSince) + m_playback.blockedSince = GetTickCount64(); + return AudioResult::BUSY; + } + } + + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(target.peer); + if (peer && peer->serial == target.peerSerial && + peer->subscriptionEpoch == target.subscriptionEpoch) + { + peer->playbackGap = true; + if (authority && peer->feedbackAuthority) + { + peer->feedbackAuthority = false; + peer->feedbackRevoked = true; + RebalanceRolesNL(); + } + } + ++m_playback.target; + m_playback.frameOffset = 0; + m_playback.chunkFrames = 0; + m_playback.packetID = 0; + m_playback.clockSent = false; + continue; + } + + { + CSRWExclusiveLock lock(m_lock); + PeerState * peer = FindPeerNL(target.peer); + if (peer && peer->serial == target.peerSerial && + peer->subscriptionEpoch == target.subscriptionEpoch) + { + PacketRecord& record = peer->playbackPacket[ + peer->playbackPacketNext++ % RECENT_PACKET_SIZE]; + record.valid = true; + record.formatEpoch = target.formatEpoch; + record.packetID = m_playback.packetID; + peer->playbackGap = false; + } + } + + m_playback.blockedSince = 0; + m_playback.frameOffset += frames; + m_playback.chunkFrames = 0; + m_playback.packetID = 0; + m_playback.clockSent = false; + if (m_playback.frameOffset == m_playback.packet.frames) + { + ++m_playback.target; + m_playback.frameOffset = 0; + } + } + + m_playback = {}; + { + CSRWExclusiveLock lock(m_lock); + m_playbackWaiting = false; + } + return AudioResult::ACCEPTED; +} + +void CNetworkAudio::Impl::MarkFailed(const char * reason) +{ + if (!m_failed) + DEBUG_ERROR("%s failed", reason); + m_failed = true; + if (m_playback.valid) + m_playbackReadyPending = true; +} + +void CNetworkAudio::Impl::NotifyFailure() +{ + CSRWSharedLock callbackLock(m_targetCallbackLock); + IAudioTarget * target = nullptr; + { + CSRWExclusiveLock lock(m_lock); + if (!m_failed || m_failureNotified) + return; + m_failureNotified = true; + target = m_started ? m_target : nullptr; + } + if (target) + target->AudioFailed(); +} + +void CNetworkAudio::Impl::NotifyPlaybackReady() +{ + CSRWSharedLock callbackLock(m_targetCallbackLock); + IAudioTarget * target = nullptr; + { + CSRWExclusiveLock lock(m_lock); + if (!m_playbackReadyPending) + return; + m_playbackReadyPending = false; + target = m_started ? m_target : nullptr; + } + if (target) + target->PlaybackReceiveReady(); +} + +void CNetworkAudio::Impl::ProcessActivation() +{ + CSRWSharedLock callbackLock(m_targetCallbackLock); + IAudioTarget * target = nullptr; + bool active = false; + { + CSRWSharedLock lock(m_lock); + if (!m_started || !m_target || !m_stateKnown || m_failed) + return; + for (const PeerState& peer : m_peers) + if (peer.subscribed) + { + active = true; + break; + } + if (active == m_activationCommanded) + return; + target = m_target; + } + + const AudioResult result = target->SetAudioActive(active); + CSRWExclusiveLock lock(m_lock); + if (!m_started || m_target != target) + return; + if (result == AudioResult::ACCEPTED || + (!active && (result == AudioResult::UNAVAILABLE || + result == AudioResult::STALE))) + m_activationCommanded = active; + else if (result == AudioResult::FAILED || + result == AudioResult::REJECTED) + MarkFailed("network audio endpoint activation"); +} + +void CNetworkAudio::Impl::RetryCapture() +{ + CSRWSharedLock callbackLock(m_targetCallbackLock); + IAudioTarget * target = nullptr; + AudioPacket packet = {}; + const uint8_t * data = nullptr; + size_t size = 0; + NetworkAudioPeer peerKey; + uint64_t peerSerial = 0; + { + CSRWExclusiveLock lock(m_lock); + if (!m_capture.valid || m_capture.callback || + !m_started || !m_target) + return; + PeerState * peer = FindPeerNL(m_capture.peer); + if (!peer || peer->serial != m_capture.peerSerial || + !peer->captureOwner || !peer->ready || + !CaptureCurrentNL(*peer) || + m_capture.packet.endpointGeneration != m_state.generation || + m_capture.packet.streamGeneration != m_state.capture.generation || + !SameFormat(m_capture.packet.format, m_state.capture.format)) + { + if (peer) + peer->captureGap = true; + m_capture.valid = false; + return; + } + + m_capture.callback = true; + target = m_target; + packet = m_capture.packet; + data = m_capture.size ? m_capture.data.data() : nullptr; + size = m_capture.size; + peerKey = m_capture.peer; + peerSerial = m_capture.peerSerial; + } + + const AudioResult result = target->SendCapture(packet, data, size); + CSRWExclusiveLock lock(m_lock); + m_capture.callback = false; + PeerState * peer = FindPeerNL(peerKey); + if (!peer || peer->serial != peerSerial || + !m_capture.valid || m_capture.peerSerial != peerSerial) + { + m_capture.valid = false; + return; + } + if (result == AudioResult::BUSY) + return; + m_capture.valid = false; + if (result == AudioResult::FAILED || result == AudioResult::REJECTED) + { + peer->captureRevoked = true; + peer->captureGap = true; + RebalanceRolesNL(); + } + else if (result != AudioResult::ACCEPTED) + peer->captureGap = true; +} + +void CNetworkAudio::Impl::RevokeStalledAuthority(uint64_t now) +{ + CSRWExclusiveLock lock(m_lock); + if (!m_playback.valid || !m_playback.blockedSince || + !DeadlineReached(now, + m_playback.blockedSince + AUTHORITY_STALL_MS) || + m_playback.target >= m_playback.targets.size()) + return; + + const PlaybackTarget& target = + m_playback.targets[m_playback.target]; + PeerState * peer = FindPeerNL(target.peer); + if (peer && peer->serial == target.peerSerial && + peer->feedbackAuthority) + { + DEBUG_WARN("Network audio feedback authority %u:%u stalled", + peer->peer.client, peer->peer.generation); + peer->feedbackAuthority = false; + peer->feedbackRevoked = true; + peer->playbackGap = true; + RebalanceRolesNL(); + } + m_playback.blockedSince = 0; + m_playbackWaiting = false; + m_playbackReadyPending = true; +} + +CNetworkAudio::CNetworkAudio() : + m_impl(new (std::nothrow) Impl()) +{ +} + +CNetworkAudio::~CNetworkAudio() +{ + if (!m_impl) + return; + Stop(); + DetachSender(); +} + +bool CNetworkAudio::AttachSender(INetworkAudioSender& sender) +{ + if (!m_impl) + return false; + CSRWExclusiveLock callbackLock(m_impl->m_senderCallbackLock); + CSRWExclusiveLock lock(m_impl->m_lock); + if (m_impl->m_sender) + return m_impl->m_sender == &sender; + m_impl->m_sender = &sender; + return true; +} + +void CNetworkAudio::DetachSender() +{ + if (!m_impl) + return; + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock callbackLock(m_impl->m_senderCallbackLock); + CSRWExclusiveLock lock(m_impl->m_lock); + m_impl->m_sender = nullptr; + m_impl->m_peers.clear(); + m_impl->m_playback = {}; + m_impl->m_capture = {}; + m_impl->m_playbackWaiting = false; +} + +bool CNetworkAudio::PeerConnected(const NetworkAudioPeer& key) +{ + if (!m_impl || !key.client || !key.generation) + return false; + + NetworkAudioPeerInfo info; + { + CSRWSharedLock callbackLock(m_impl->m_senderCallbackLock); + INetworkAudioSender * sender; + { + CSRWSharedLock lock(m_impl->m_lock); + sender = m_impl->m_sender; + } + if (!sender || !sender->AudioPeerInfo(key, info)) + return false; + } + + if (!AudioVersionSupported(info.serviceVersion) || + !(info.capabilityFlags & LG_NET_CAPABILITY_RELIABLE) || + info.maxReliablePayload < + LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE + AUDIO_MAX_PCM_BYTES) + return false; + + CSRWExclusiveLock lock(m_impl->m_lock); + Impl::PeerState * existing = m_impl->FindPeerNL(key); + if (existing) + { + existing->info = info; + return true; + } + + try + { + Impl::PeerState peer; + peer.peer = key; + peer.info = info; + peer.serial = NextSerial(m_impl->m_nextPeerSerial); + + m_impl->m_nextPeerSerial = peer.serial; + m_impl->m_peers.push_back(peer); + } + catch (...) + { + return false; + } + return true; +} + +void CNetworkAudio::PeerDisconnected(const NetworkAudioPeer& key) +{ + if (!m_impl) + return; + CSRWExclusiveLock lock(m_impl->m_lock); + const auto found = std::find_if(m_impl->m_peers.begin(), + m_impl->m_peers.end(), [&key](const Impl::PeerState& peer) + { + return SamePeer(peer.peer, key); + }); + if (found == m_impl->m_peers.end()) + return; + if (m_impl->m_capture.valid && SamePeer(m_impl->m_capture.peer, key)) + m_impl->m_capture.valid = false; + if (m_impl->m_playbackWaiting && + SamePeer(m_impl->m_playbackWaitingPeer, key)) + { + m_impl->m_playbackWaiting = false; + m_impl->m_playbackReadyPending = true; + } + m_impl->m_peers.erase(found); + m_impl->RebalanceRolesNL(); +} + +NetworkAudioPacketResult CNetworkAudio::HandlePacket( + const NetworkAudioPeer& key, const LGNetPacketView& packet, + bool datagram) +{ + if (!m_impl || packet.envelope.service != LG_NET_SERVICE_AUDIO || + !AudioVersionSupported(packet.envelope.serviceVersion) || + packet.payloadLength != packet.envelope.payloadLength) + return NetworkAudioPacketResult::INVALID; + + const uint64_t now = GetTickCount64(); + if (packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_SUBSCRIBE) + { + if (datagram || packet.envelope.componentEpoch || + !packet.envelope.requestID || + !ReliableFlags(packet.envelope, + LG_NET_ENVELOPE_ACK_REQUIRED | LG_NET_ENVELOPE_FINAL)) + return NetworkAudioPacketResult::INVALID; + CSRWExclusiveLock lock(m_impl->m_lock); + Impl::PeerState * peer = m_impl->FindPeerNL(key); + if (!peer || peer->info.serviceVersion != + packet.envelope.serviceVersion) + return NetworkAudioPacketResult::IGNORED; + return m_impl->HandleSubscribe(*peer, packet, now); + } + + { + CSRWSharedLock lock(m_impl->m_lock); + const Impl::PeerState * peer = m_impl->FindPeerNL(key); + if (!peer || !peer->hasRequest || + !lgNetAudioEnvelopeMatchesSubscription( + &packet.envelope, peer->subscriptionEpoch)) + return NetworkAudioPacketResult::IGNORED; + } + + const NetworkAudioPacketResult result = + m_impl->HandleCurrentPacket(key, packet, datagram, now); + CSRWExclusiveLock processLock = + CSRWExclusiveLock::Try(m_impl->m_processLock); + if (processLock) + m_impl->NotifyPlaybackReady(); + return result; +} + +void CNetworkAudio::Process(uint64_t nowMs) +{ + if (!m_impl) + return; + CSRWExclusiveLock processLock(m_impl->m_processLock); + { + CSRWExclusiveLock lock(m_impl->m_lock); + bool rebalance = false; + for (Impl::PeerState& peer : m_impl->m_peers) + if (peer.hasRequest && !peer.expiring && + DeadlineReached(nowMs, peer.deadline)) + { + DEBUG_WARN("Network audio subscriber %u:%u expired", + peer.peer.client, peer.peer.generation); + if (m_impl->m_capture.valid && + SamePeer(m_impl->m_capture.peer, peer.peer)) + m_impl->m_capture.valid = false; + peer.expiring = true; + peer.deadline = 0; + rebalance = true; + } + if (rebalance) + m_impl->RebalanceRolesNL(); + } + + m_impl->RevokeStalledAuthority(nowMs); + m_impl->ProcessActivation(); + m_impl->PumpAll(); + m_impl->NotifyPlaybackReady(); + m_impl->NotifyFailure(); +} + +void CNetworkAudio::SendReady(const NetworkAudioPeer& key, + LGNetService service, bool datagram) +{ + if (!m_impl || service != LG_NET_SERVICE_AUDIO) + return; + { + CSRWExclusiveLock lock(m_impl->m_lock); + Impl::PeerState * peer = m_impl->FindPeerNL(key); + if (peer) + peer->sendReadySerial = NextSerial(peer->sendReadySerial); + if (m_impl->m_playbackWaiting && + SamePeer(m_impl->m_playbackWaitingPeer, key)) + { + m_impl->m_playbackWaiting = false; + m_impl->m_playbackReadyPending = true; + } + } + CSRWExclusiveLock processLock = + CSRWExclusiveLock::Try(m_impl->m_processLock); + if (processLock) + { + if (!datagram) + m_impl->PumpPeer(key); + m_impl->NotifyPlaybackReady(); + } +} + +bool CNetworkAudio::Start(IAudioTarget& target) +{ + if (!m_impl) + return false; + CSRWExclusiveLock lifecycleLock(m_impl->m_lifecycleLock); + CSRWExclusiveLock callbackLock(m_impl->m_targetCallbackLock); + CSRWExclusiveLock lock(m_impl->m_lock); + if (m_impl->m_started) + return m_impl->m_target == ⌖ + m_impl->m_target = ⌖ + m_impl->m_started = true; + m_impl->m_stateKnown = false; + m_impl->m_failed = false; + m_impl->m_failureNotified = false; + m_impl->m_activationCommanded = false; + return true; +} + +void CNetworkAudio::Stop() +{ + if (!m_impl) + return; + CSRWExclusiveLock lifecycleLock(m_impl->m_lifecycleLock); + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock callbackLock(m_impl->m_targetCallbackLock); + + IAudioTarget * target; + bool deactivate; + { + CSRWExclusiveLock lock(m_impl->m_lock); + if (!m_impl->m_started && !m_impl->m_target) + return; + target = m_impl->m_target; + deactivate = m_impl->m_activationCommanded; + for (Impl::PeerState& peer : m_impl->m_peers) + m_impl->ClearSubscriptionNL(peer); + m_impl->m_playback = {}; + m_impl->m_capture = {}; + m_impl->m_started = false; + m_impl->m_target = nullptr; + m_impl->m_stateKnown = false; + m_impl->m_activationCommanded = false; + m_impl->m_playbackReadyPending = false; + m_impl->m_playbackWaiting = false; + } + callbackLock.Unlock(); + processLock.Unlock(); + if (deactivate && target) + (void)target->SetAudioActive(false); +} + +void CNetworkAudio::AudioState(const AudioEndpointStatus& state) +{ + if (!m_impl) + return; + CSRWExclusiveLock processLock(m_impl->m_processLock); + CSRWExclusiveLock lock(m_impl->m_lock); + if (!m_impl->m_started) + return; + + const uint64_t previousEndpoint = m_impl->m_state.generation; + const uint64_t previousPlayback = m_impl->m_state.playback.generation; + const uint64_t previousCapture = m_impl->m_state.capture.generation; + m_impl->m_state = state; + m_impl->m_stateKnown = true; + if (state.generation != previousEndpoint || + state.playback.generation != previousPlayback) + for (Impl::PeerState& peer : m_impl->m_peers) + peer.feedbackRevoked = false; + if (state.generation != previousEndpoint || + state.capture.generation != previousCapture) + for (Impl::PeerState& peer : m_impl->m_peers) + peer.captureRevoked = false; + + if (m_impl->m_playback.valid && + (m_impl->m_playback.packet.endpointGeneration != state.generation || + m_impl->m_playback.packet.streamGeneration != + state.playback.generation || + !state.playback.formatValid || + !SameFormat(m_impl->m_playback.packet.format, + state.playback.format))) + { + m_impl->m_playback = {}; + m_impl->m_playbackReadyPending = true; + m_impl->m_playbackWaiting = false; + } + if (m_impl->m_capture.valid && + (m_impl->m_capture.packet.endpointGeneration != state.generation || + m_impl->m_capture.packet.streamGeneration != + state.capture.generation || + !state.capture.formatValid || + !SameFormat(m_impl->m_capture.packet.format, + state.capture.format))) + m_impl->m_capture.valid = false; + + m_impl->RebalanceRolesNL(); + for (Impl::PeerState& peer : m_impl->m_peers) + if (peer.hasRequest) + m_impl->SchedulePumpNL(peer, false); +} + +AudioResult CNetworkAudio::SendPlayback( + const AudioPacket& packet, const uint8_t * data, size_t size) +{ + constexpr uint32_t PACKET_FLAGS = + AUDIO_PACKET_DISCONTINUITY | AUDIO_PACKET_SILENT | + AUDIO_PACKET_CLOCK_VALID | AUDIO_PACKET_CLOCK_STABLE; + if (!m_impl || (packet.flags & ~PACKET_FLAGS) || + ((packet.flags & AUDIO_PACKET_CLOCK_STABLE) && + !(packet.flags & AUDIO_PACKET_CLOCK_VALID)) || + (!(packet.flags & AUDIO_PACKET_CLOCK_VALID) && + (packet.position || packet.timeNs || packet.rateQ32)) || + packet.rateQ32 > LG_NET_AUDIO_MAX_RATE_Q32 || + !packet.frames || packet.frames > AUDIO_MAX_FRAMES_PER_PACKET || + !ValidFormat(packet.format)) + return AudioResult::REJECTED; + + const uint64_t lastFrameOffset = packet.frames - 1U; + const uint64_t maxTimeAdvance = + lastFrameOffset * UINT64_C(1000000000) / packet.format.sampleRate; + if (packet.position > UINT64_MAX - lastFrameOffset || + ((packet.flags & AUDIO_PACKET_CLOCK_VALID) && + packet.timeNs > INT64_MAX - static_cast(maxTimeAdvance))) + return AudioResult::REJECTED; + + const size_t pcmBytes = static_cast(packet.frames) * + packet.format.blockAlign; + const bool silent = (packet.flags & AUDIO_PACKET_SILENT) != 0; + if (pcmBytes > AUDIO_MAX_PCM_BYTES || + (silent ? size != 0 : (size != pcmBytes || !data))) + return AudioResult::REJECTED; + + CSRWExclusiveLock processLock(m_impl->m_processLock); + { + CSRWExclusiveLock lock(m_impl->m_lock); + if (m_impl->m_playback.valid) + { + if (m_impl->m_playback.size != size || + !SamePacket(m_impl->m_playback.packet, packet)) + { + DEBUG_WARN("Network audio playback retry did not match"); + m_impl->m_playback = {}; + return AudioResult::REJECTED; + } + } + else + { + const AudioResult begin = m_impl->BeginPlayback(packet, size); + if (begin != AudioResult::BUSY) + return begin; + } + m_impl->m_playbackWaiting = false; + } + + const AudioResult result = m_impl->ContinuePlayback(data); + if (result == AudioResult::FAILED || result == AudioResult::REJECTED) + { + CSRWExclusiveLock lock(m_impl->m_lock); + m_impl->m_playback = {}; + if (result == AudioResult::FAILED) + m_impl->MarkFailed("network audio playback"); + } + return result; +} + +void CNetworkAudio::CaptureReceiveReady() +{ + if (m_impl) + m_impl->RetryCapture(); +} diff --git a/idd/LGIdd/transport/network/CNetworkAudio.h b/idd/LGIdd/transport/network/CNetworkAudio.h new file mode 100644 index 00000000..d7bbf5af --- /dev/null +++ b/idd/LGIdd/transport/network/CNetworkAudio.h @@ -0,0 +1,102 @@ +/** + * 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 + */ + +#pragma once + +#include "transport/IAudioSource.h" + +#include + +#include +#include + +struct NetworkAudioPeer +{ + uint32_t client = 0; + uint32_t generation = 0; +}; + +struct NetworkAudioPeerInfo +{ + uint16_t serviceVersion = 0; + LGNetCapabilityFlags capabilityFlags = 0; + uint32_t maxReliablePayload = 0; + uint32_t maxDatagramPayload = 0; +}; + +enum class NetworkAudioSendResult +{ + ACCEPTED, + BLOCKED, + FAILED, +}; + +enum class NetworkAudioPacketResult +{ + HANDLED, + IGNORED, + INVALID, +}; + +class INetworkAudioSender +{ +public: + virtual ~INetworkAudioSender() = default; + + virtual bool AudioPeerInfo(const NetworkAudioPeer& peer, + NetworkAudioPeerInfo& info) = 0; + virtual NetworkAudioSendResult SendAudio( + const NetworkAudioPeer& peer, bool datagram, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) = 0; +}; + +class CNetworkAudio final : public IAudioSource +{ +private: + struct Impl; + std::unique_ptr m_impl; + +public: + CNetworkAudio(); + ~CNetworkAudio() override; + + CNetworkAudio(const CNetworkAudio&) = delete; + CNetworkAudio& operator=(const CNetworkAudio&) = delete; + + bool AttachSender(INetworkAudioSender& sender); + void DetachSender(); + + bool PeerConnected(const NetworkAudioPeer& peer); + void PeerDisconnected(const NetworkAudioPeer& peer); + NetworkAudioPacketResult HandlePacket(const NetworkAudioPeer& peer, + const LGNetPacketView& packet, bool datagram); + void Process(uint64_t nowMs); + void SendReady(const NetworkAudioPeer& peer, + LGNetService service, bool datagram); + + bool Start(IAudioTarget& target) override; + void Stop() override; + void AudioState(const AudioEndpointStatus& state) override; + AudioResult SendPlayback(const AudioPacket& packet, + const uint8_t * data, size_t size) override; + void CaptureReceiveReady() override; +}; diff --git a/idd/LGIdd/transport/network/CNetworkServer.cpp b/idd/LGIdd/transport/network/CNetworkServer.cpp index 098d1651..9512b223 100644 --- a/idd/LGIdd/transport/network/CNetworkServer.cpp +++ b/idd/LGIdd/transport/network/CNetworkServer.cpp @@ -394,8 +394,11 @@ struct CNetworkServer::Impl Impl& owner; }; - Impl(CNetworkTransport& transport, const NetworkSettings& settings) : - transport(transport), settings(settings) + Impl(CNetworkTransport& transport, CNetworkAudio& audio, + const NetworkSettings& settings) : + transport(transport), + audio(audio), + settings(settings) { } @@ -413,6 +416,7 @@ struct CNetworkServer::Impl } CNetworkTransport& transport; + CNetworkAudio& audio; NetworkSettings settings; const QUIC_API_TABLE * api = nullptr; HQUIC registration = nullptr; @@ -468,6 +472,80 @@ struct CNetworkServer::Impl return 0; } + static NetworkAudioPeer AudioPeer(const Connection& connection) + { + return + { + connection.client, + connection.generation, + }; + } + + bool AudioPeerInfo(const NetworkAudioPeer& peer, + NetworkAudioPeerInfo& info) + { + CSRWSharedLock guard(lock); + for (Connection * connection : connections) + { + if (connection->client != peer.client || + connection->generation != peer.generation) + continue; + + CSRWSharedLock connectionGuard(connection->lock); + const uint16_t serviceMask = + UINT16_C(1) << LG_NET_SERVICE_AUDIO; + if (connection->closing || + connection->handshake != HandshakeState::READY || + !(connection->selectedServices & serviceMask)) + return false; + + const LGNetCapability& capability = + connection->selectedCapabilities[LG_NET_SERVICE_AUDIO]; + info.serviceVersion = capability.versionMin; + info.capabilityFlags = capability.flags; + info.maxReliablePayload = capability.maxPayloadLength; + info.maxDatagramPayload = capability.maxDatagramPayload; + return true; + } + return false; + } + + NetworkAudioSendResult SendAudio( + const NetworkAudioPeer& peer, bool datagram, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) + { + if (service != LG_NET_SERVICE_AUDIO) + return NetworkAudioSendResult::FAILED; + + CSRWSharedLock guard(lock); + for (Connection * connection : connections) + { + if (connection->client != peer.client || + connection->generation != peer.generation) + continue; + + const SendResult result = datagram ? + TrySendDatagram(*connection, service, messageType, flags, + componentEpoch, requestID, payload, payloadLength, true) : + TrySendPacket(*connection, service, messageType, flags, + componentEpoch, requestID, payload, payloadLength); + switch (result) + { + case SendResult::SENT: + return NetworkAudioSendResult::ACCEPTED; + + case SendResult::BLOCKED: + return NetworkAudioSendResult::BLOCKED; + + case SendResult::FAILED: + return NetworkAudioSendResult::FAILED; + } + } + return NetworkAudioSendResult::FAILED; + } + void Shutdown(Connection& connection, QUIC_UINT62 error) { HQUIC handle = nullptr; @@ -708,6 +786,14 @@ struct CNetworkServer::Impl return ready; } + void NotifyAudioSendReady(Connection& connection, bool datagram) + { + if (ConsumeSendReady( + connection, LG_NET_SERVICE_AUDIO, datagram)) + audio.SendReady( + AudioPeer(connection), LG_NET_SERVICE_AUDIO, datagram); + } + SendResult TrySendPacket(Connection& connection, LGNetService service, uint16_t messageType, LGNetEnvelopeFlags flags, uint64_t componentEpoch, uint64_t requestID, const void * payload, @@ -1081,6 +1167,11 @@ struct CNetworkServer::Impl minimumPayload = LG_NET_INPUT_ABSOLUTE_WIRE_SIZE; break; + case LG_NET_SERVICE_AUDIO: + minimumPayload = LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE + + AUDIO_MAX_PCM_BYTES; + break; + case LG_NET_SERVICE_CONTROL: minimumPayload = LG_NET_CONTROL_FRAME_SCHEDULE_WIRE_SIZE; break; @@ -1094,6 +1185,9 @@ struct CNetworkServer::Impl !(offered.flags & LG_NET_CAPABILITY_RELIABLE) || offered.versionMin > version->current || offered.versionMax < version->minimum || + (offered.service == LG_NET_SERVICE_AUDIO && + offered.versionMax < + LG_NET_AUDIO_OWNERSHIP_INTRODUCED_SERVICE_VERSION) || offered.maxPayloadLength < minimumPayload || connection.maxPayload < minimumPayload) return false; @@ -1109,6 +1203,9 @@ struct CNetworkServer::Impl selected.versionMax = selectedVersion; selected.flags = LG_NET_CAPABILITY_SELECTED | LG_NET_CAPABILITY_RELIABLE; + if (offered.service == LG_NET_SERVICE_AUDIO && + (offered.flags & LG_NET_CAPABILITY_MULTI_CLIENT)) + selected.flags |= LG_NET_CAPABILITY_MULTI_CLIENT; selected.features = 0; selected.maxPayloadLength = offered.maxPayloadLength < connection.maxPayload ? offered.maxPayloadLength : @@ -1119,7 +1216,9 @@ struct CNetworkServer::Impl const uint32_t minimumDatagram = offered.service == LG_NET_SERVICE_INPUT ? LG_NET_INPUT_ABSOLUTE_WIRE_SIZE : offered.service == LG_NET_SERVICE_CURSOR ? - LG_NET_CURSOR_POSITION_WIRE_SIZE : 0; + LG_NET_CURSOR_POSITION_WIRE_SIZE : + offered.service == LG_NET_SERVICE_AUDIO ? + LG_NET_AUDIO_DATA_HEADER_WIRE_SIZE + 2U : 0; if (minimumDatagram && (offered.flags & LG_NET_CAPABILITY_DATAGRAM) && offered.maxDatagramPayload >= minimumDatagram && @@ -1259,7 +1358,8 @@ struct CNetworkServer::Impl if (!session) return false; - bool activated; + bool activated = false; + bool audioSelected = false; { CSRWExclusiveLock guard(connection.lock); activated = !connection.closing && @@ -1276,6 +1376,8 @@ struct CNetworkServer::Impl NetworkAuthCrypto::SecureZero( connection.serverNonce, sizeof(connection.serverNonce)); connection.handshake = HandshakeState::READY; + audioSelected = (connection.selectedServices & + (UINT16_C(1) << LG_NET_SERVICE_AUDIO)) != 0; } } if (!activated) @@ -1283,6 +1385,8 @@ struct CNetworkServer::Impl transport.RemoveSession(connection.client, connection.generation); return false; } + if (audioSelected && !audio.PeerConnected(AudioPeer(connection))) + return false; DEBUG_INFO("Network client %u completed the control handshake", connection.client); return true; @@ -2757,18 +2861,24 @@ struct CNetworkServer::Impl bool HandlePacket(Connection& connection, const LGNetPacketView& packet) { - const bool cursorStatus = + const bool cursorStatus = packet.envelope.service == LG_NET_SERVICE_CURSOR && packet.envelope.messageType == LG_NET_CURSOR_MESSAGE_STATUS; + const bool audioSubscribe = + packet.envelope.service == LG_NET_SERVICE_AUDIO && + packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_SUBSCRIBE; + const bool audioResponse = + packet.envelope.service == LG_NET_SERVICE_AUDIO && + packet.envelope.messageType == LG_NET_AUDIO_MESSAGE_STATE_ACK; + LGNetEnvelopeFlags expectedFlags = + LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL; + if (audioSubscribe) + expectedFlags |= LG_NET_ENVELOPE_ACK_REQUIRED; + if (cursorStatus || audioResponse) + expectedFlags |= LG_NET_ENVELOPE_RESPONSE; if (packet.envelope.headerSize != LG_NET_ENVELOPE_WIRE_SIZE || packet.envelope.payloadLength > NETWORK_MAX_CONTROL_PAYLOAD || - packet.envelope.flags & (LG_NET_ENVELOPE_DATAGRAM | - LG_NET_ENVELOPE_ERROR | LG_NET_ENVELOPE_EARLY_DATA) || - !!(packet.envelope.flags & LG_NET_ENVELOPE_RESPONSE) != - cursorStatus || - (packet.envelope.flags & - (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL)) != - (LG_NET_ENVELOPE_RELIABLE | LG_NET_ENVELOPE_FINAL) || + packet.envelope.flags != expectedFlags || !lgNetEnvelopeRouteValid(&packet.envelope, LG_NET_ROLE_CLIENT, LG_NET_ROUTE_DELIVERY_RELIABLE)) return false; @@ -2866,6 +2976,10 @@ struct CNetworkServer::Impl case LG_NET_SERVICE_INPUT: return HandleInput(connection, packet, false); + case LG_NET_SERVICE_AUDIO: + return audio.HandlePacket(AudioPeer(connection), packet, false) != + NetworkAudioPacketResult::INVALID; + case LG_NET_SERVICE_CONTROL: return HandleControl(connection, packet); @@ -2932,8 +3046,12 @@ struct CNetworkServer::Impl 0, packet.envelope.requestID, packet.payload, static_cast(packet.payloadLength), true); - return packet.envelope.service == LG_NET_SERVICE_INPUT && - HandleInput(connection, packet, true); + if (packet.envelope.service == LG_NET_SERVICE_INPUT) + return HandleInput(connection, packet, true); + if (packet.envelope.service == LG_NET_SERVICE_AUDIO) + return audio.HandlePacket(AudioPeer(connection), packet, true) != + NetworkAudioPacketResult::INVALID; + return false; } bool DrainReceive(Connection& connection) @@ -3037,6 +3155,7 @@ struct CNetworkServer::Impl connection.client); session = std::move(connection.session); } + audio.PeerDisconnected(AudioPeer(connection)); if (session) transport.RemoveSession(client, generation); session.reset(); @@ -3171,13 +3290,16 @@ struct CNetworkServer::Impl case QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED: { - CSRWExclusiveLock guard(connection.lock); - connection.datagramSendEnabled = - event->DATAGRAM_STATE_CHANGED.SendEnabled != FALSE; - connection.maxDatagramSendLength = - event->DATAGRAM_STATE_CHANGED.MaxSendLength; - connection.datagramSend.writableServices |= - ReadyServicesLocked(connection, true); + { + CSRWExclusiveLock guard(connection.lock); + connection.datagramSendEnabled = + event->DATAGRAM_STATE_CHANGED.SendEnabled != FALSE; + connection.maxDatagramSendLength = + event->DATAGRAM_STATE_CHANGED.MaxSendLength; + connection.datagramSend.writableServices |= + ReadyServicesLocked(connection, true); + } + owner.NotifyAudioSendReady(connection, true); return QUIC_STATUS_SUCCESS; } @@ -3187,8 +3309,11 @@ struct CNetworkServer::Impl { std::unique_ptr send(static_cast( event->DATAGRAM_SEND_STATE_CHANGED.ClientContext)); - CSRWExclusiveLock guard(connection.lock); - ReleaseSendLocked(connection, *send); + { + CSRWExclusiveLock guard(connection.lock); + ReleaseSendLocked(connection, *send); + } + owner.NotifyAudioSendReady(connection, true); } return QUIC_STATUS_SUCCESS; @@ -3229,8 +3354,11 @@ struct CNetworkServer::Impl { std::unique_ptr send( static_cast(event->SEND_COMPLETE.ClientContext)); - CSRWExclusiveLock guard(connection.lock); - ReleaseSendLocked(connection, *send); + { + CSRWExclusiveLock guard(connection.lock); + ReleaseSendLocked(connection, *send); + } + owner.NotifyAudioSendReady(connection, false); return QUIC_STATUS_SUCCESS; } @@ -3269,6 +3397,7 @@ struct CNetworkServer::Impl void Process() { const uint64_t now = GetTickCount64(); + audio.Process(now); CSRWSharedLock guard(lock); if (stopping || !api) return; @@ -3495,8 +3624,8 @@ struct CNetworkServer::Impl }; CNetworkServer::CNetworkServer(CNetworkTransport& transport, - const NetworkSettings& settings) : - m_impl(new Impl(transport, settings)) + CNetworkAudio& audio, const NetworkSettings& settings) : + m_impl(new Impl(transport, audio, settings)) { } @@ -3518,3 +3647,21 @@ void CNetworkServer::Process() if (m_impl) m_impl->Process(); } + +bool CNetworkServer::AudioPeerInfo(const NetworkAudioPeer& peer, + NetworkAudioPeerInfo& info) +{ + return m_impl && m_impl->AudioPeerInfo(peer, info); +} + +NetworkAudioSendResult CNetworkServer::SendAudio( + const NetworkAudioPeer& peer, bool datagram, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) +{ + return m_impl ? + m_impl->SendAudio(peer, datagram, service, messageType, flags, + componentEpoch, requestID, payload, payloadLength) : + NetworkAudioSendResult::FAILED; +} diff --git a/idd/LGIdd/transport/network/CNetworkServer.h b/idd/LGIdd/transport/network/CNetworkServer.h index 10b97960..94a83fcd 100644 --- a/idd/LGIdd/transport/network/CNetworkServer.h +++ b/idd/LGIdd/transport/network/CNetworkServer.h @@ -20,19 +20,21 @@ #pragma once +#include "transport/network/CNetworkAudio.h" + #include class CNetworkTransport; struct NetworkSettings; -class CNetworkServer final +class CNetworkServer final : public INetworkAudioSender { private: struct Impl; std::unique_ptr m_impl; public: - CNetworkServer(CNetworkTransport& transport, + CNetworkServer(CNetworkTransport& transport, CNetworkAudio& audio, const NetworkSettings& settings); ~CNetworkServer(); @@ -42,4 +44,12 @@ public: bool Start(); void Stop(); void Process(); + + bool AudioPeerInfo(const NetworkAudioPeer& peer, + NetworkAudioPeerInfo& info) override; + NetworkAudioSendResult SendAudio( + const NetworkAudioPeer& peer, bool datagram, LGNetService service, + uint16_t messageType, LGNetEnvelopeFlags flags, + uint64_t componentEpoch, uint64_t requestID, + const void * payload, uint32_t payloadLength) override; }; diff --git a/idd/LGIdd/transport/network/CNetworkTransport.cpp b/idd/LGIdd/transport/network/CNetworkTransport.cpp index 96bc1758..227f1375 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.cpp +++ b/idd/LGIdd/transport/network/CNetworkTransport.cpp @@ -76,13 +76,19 @@ bool CNetworkTransport::Initialize() try { - m_server.reset(new CNetworkServer(*this, m_settings)); + m_server.reset(new CNetworkServer(*this, m_audio, m_settings)); } catch (...) { DEBUG_ERROR("Failed to allocate the network server"); return false; } + if (!m_audio.AttachSender(*m_server)) + { + DEBUG_ERROR("Failed to attach the network audio sender"); + m_server.reset(); + return false; + } m_state = State::INITIALIZED; if (m_config.services & TRANSPORT_SERVICE_FRAME) @@ -157,6 +163,7 @@ void CNetworkTransport::Stop() if (server) server->Stop(); + m_audio.DetachSender(); std::vector sessions; { @@ -167,6 +174,7 @@ void CNetworkTransport::Stop() session->Close(); m_input.Stop(); + m_audio.Stop(); m_control.SetControlEvents(nullptr, {}); m_events.Reset(); } @@ -224,6 +232,11 @@ DirectFrameBufferMemory CNetworkTransport::GetDirectMemory() const return DirectFrameBufferMemory {}; } +IAudioSource * CNetworkTransport::Audio() +{ + return (m_config.services & TRANSPORT_SERVICE_AUDIO) ? &m_audio : nullptr; +} + IControlSink * CNetworkTransport::Control() { return (m_config.services & TRANSPORT_SERVICE_CONTROL) ? diff --git a/idd/LGIdd/transport/network/CNetworkTransport.h b/idd/LGIdd/transport/network/CNetworkTransport.h index 357f2c9b..d73d39db 100644 --- a/idd/LGIdd/transport/network/CNetworkTransport.h +++ b/idd/LGIdd/transport/network/CNetworkTransport.h @@ -22,6 +22,7 @@ #include "CSRWLock.h" #include "transport/ITransport.h" +#include "transport/network/CNetworkAudio.h" #include "transport/network/CNetworkSession.h" #include "transport/network/NetworkSettings.h" @@ -49,6 +50,7 @@ private: TransportInstance m_config; NetworkSettings m_settings; + CNetworkAudio m_audio; std::unique_ptr m_server; mutable CSRWLock m_lifecycleLock; @@ -89,6 +91,7 @@ public: std::shared_ptr GetFrameCaps() const override; DirectFrameBufferMemory GetDirectMemory() const override; + IAudioSource * Audio() override; IControlSink * Control() override; IInputSource * Input() override;