diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj
index ab9cd486..2c3141de 100644
--- a/idd/LGIdd/LGIdd.vcxproj
+++ b/idd/LGIdd/LGIdd.vcxproj
@@ -66,6 +66,8 @@
+
+
@@ -127,6 +129,8 @@
+
+
@@ -139,6 +143,7 @@
+
diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters
index f3f650b8..5811185e 100644
--- a/idd/LGIdd/LGIdd.vcxproj.filters
+++ b/idd/LGIdd/LGIdd.vcxproj.filters
@@ -17,6 +17,9 @@
{202B79B2-5CCF-4AD4-B4C5-F5602419683D}
+
+ {86750913-8C21-4F76-8E51-9737D4963E50}
+
{4B3C58D3-5F40-49AB-894E-B83EB44E6CF5}
@@ -166,6 +169,15 @@
Post-processing\Effects
+
+ Audio
+
+
+ Transport
+
+
+ Transport
+
Transport
@@ -378,6 +390,12 @@
Post-processing\Effects
+
+ Audio
+
+
+ Transport
+
Transport
diff --git a/idd/LGIdd/audio/CLGAudioDevice.cpp b/idd/LGIdd/audio/CLGAudioDevice.cpp
new file mode 100644
index 00000000..16dd3abf
--- /dev/null
+++ b/idd/LGIdd/audio/CLGAudioDevice.cpp
@@ -0,0 +1,1432 @@
+/**
+ * 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/CLGAudioDevice.h"
+
+#include "CDebug.h"
+#include "CSRWLock.h"
+#include "LGAudioBroker.h"
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace
+{
+ static_assert(AUDIO_CHANNEL_POSITION_MASK ==
+ (1U << AUDIO_MAX_CHANNELS) - 1,
+ "Audio channel-position geometry changed");
+ static_assert(LGAUDIO_BROKER_MAX_PAYLOAD_BYTES >= AUDIO_MAX_PCM_BYTES,
+ "LGAudio broker cannot carry the largest transport audio packet");
+
+ static constexpr unsigned RENDER_SLOT_COUNT = 4;
+ static constexpr unsigned CAPTURE_SLOT_COUNT = 4;
+ static constexpr DWORD OPEN_RETRY_MS = 500;
+ static constexpr DWORD ACTIVE_POLL_MS = 10;
+ static constexpr DWORD IDLE_POLL_MS = 100;
+ static constexpr size_t BROKER_PACKET_BYTES =
+ sizeof(LGAudioBrokerPacketHeader) + AUDIO_MAX_PCM_BYTES;
+
+ uint64_t NextGeneration(uint64_t generation)
+ {
+ ++generation;
+ return generation ? generation : 1;
+ }
+
+ unsigned PopCount(uint32_t value)
+ {
+ unsigned count = 0;
+ while (value)
+ {
+ value &= value - 1;
+ ++count;
+ }
+ return count;
+ }
+
+ bool SameFormat(const AudioFormat& left, const AudioFormat& right)
+ {
+ return 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 ValidFormat(const AudioFormat& format)
+ {
+ return format.sampleRate && format.sampleRate <= 192000 &&
+ format.channels && format.channels <= AUDIO_STREAM_MAX_CHANNELS &&
+ format.channelMask &&
+ !(format.channelMask & ~AUDIO_CHANNEL_POSITION_MASK) &&
+ PopCount(format.channelMask) == format.channels &&
+ (format.containerBits == 16 || format.containerBits == 24) &&
+ format.validBits == format.containerBits &&
+ format.blockAlign ==
+ format.channels * (format.containerBits / 8);
+ }
+
+ AudioFormat BrokerFormat(const LGAudioBrokerFormat& source)
+ {
+ AudioFormat result;
+ result.sampleRate = source.sampleRate;
+ result.channelMask = source.channelMask;
+ result.channels = source.channels;
+ result.containerBits = source.containerBits;
+ result.validBits = source.validBits;
+ result.blockAlign = source.blockAlign;
+ return result;
+ }
+
+ LGAudioBrokerFormat DeviceFormat(const AudioFormat& source)
+ {
+ LGAudioBrokerFormat result = {};
+ result.sampleRate = source.sampleRate;
+ result.channelMask = source.channelMask;
+ result.channels = source.channels;
+ result.containerBits = source.containerBits;
+ result.validBits = source.validBits;
+ result.blockAlign = source.blockAlign;
+ return result;
+ }
+
+ AudioStreamState BrokerStreamState(uint32_t state)
+ {
+ switch (state)
+ {
+ case LGAUDIO_BROKER_STREAM_STOPPED:
+ return AudioStreamState::STOPPED;
+ case LGAUDIO_BROKER_STREAM_ACQUIRED:
+ return AudioStreamState::ACQUIRED;
+ case LGAUDIO_BROKER_STREAM_PAUSED:
+ return AudioStreamState::PAUSED;
+ case LGAUDIO_BROKER_STREAM_RUNNING:
+ return AudioStreamState::RUNNING;
+ default:
+ return AudioStreamState::UNAVAILABLE;
+ }
+ }
+
+ AudioStreamStatus BrokerStream(
+ const LGAudioBrokerStreamState& source)
+ {
+ AudioStreamStatus result;
+ result.generation = source.generation;
+ result.state = BrokerStreamState(source.state);
+ result.formatValid =
+ (source.flags & LGAUDIO_BROKER_STREAM_FORMAT_VALID) != 0;
+ if (result.formatValid)
+ {
+ result.format = BrokerFormat(source.format);
+ if (!ValidFormat(result.format))
+ {
+ result.format = {};
+ result.formatValid = false;
+ }
+ }
+ return result;
+ }
+
+ bool SameStream(const AudioStreamStatus& left,
+ const AudioStreamStatus& right)
+ {
+ return left.generation == right.generation &&
+ left.state == right.state &&
+ left.formatValid == right.formatValid &&
+ (!left.formatValid || SameFormat(left.format, right.format));
+ }
+
+ bool SameStatus(const AudioEndpointStatus& left,
+ const AudioEndpointStatus& right)
+ {
+ return left.generation == right.generation &&
+ left.capabilities == right.capabilities &&
+ left.available == right.available &&
+ left.active == right.active &&
+ SameStream(left.playback, right.playback) &&
+ SameStream(left.capture, right.capture);
+ }
+
+ bool RecoverableIoError(DWORD error)
+ {
+ return error == ERROR_OPERATION_ABORTED ||
+ error == ERROR_NOT_READY ||
+ error == ERROR_RETRY ||
+ error == ERROR_DEVICE_NOT_CONNECTED;
+ }
+}
+
+class CLGAudioDevice::CState
+{
+private:
+ enum class ActivationState
+ {
+ IDLE,
+ ACTIVATING,
+ ACTIVE,
+ DEACTIVATING,
+ };
+
+ enum class RenderState
+ {
+ IDLE,
+ PENDING,
+ READY,
+ };
+
+ struct RenderSlot
+ {
+ OVERLAPPED overlapped = {};
+ RenderState state = RenderState::IDLE;
+ DWORD transferred = 0;
+ uint8_t data[BROKER_PACKET_BYTES];
+ };
+
+ struct CaptureSlot
+ {
+ AudioPacket packet;
+ size_t size = 0;
+ uint8_t data[AUDIO_MAX_PCM_BYTES];
+ };
+
+ mutable CSRWLock m_lock;
+ ILGAudioDeviceEvents * m_events = nullptr;
+ HANDLE m_stopEvent = nullptr;
+ HANDLE m_wakeEvent = nullptr;
+ HANDLE m_controlEvent = nullptr;
+ HANDLE m_writeEvent = nullptr;
+ HANDLE m_thread = nullptr;
+ HANDLE m_handle = INVALID_HANDLE_VALUE;
+ bool m_started = false;
+ bool m_stopping = false;
+ bool m_bound = false;
+ ActivationState m_activation = ActivationState::IDLE;
+ bool m_controlDirty = true;
+
+ AudioEndpointStatus m_status;
+ uint64_t m_endpointGeneration = 1;
+ CaptureSlot m_capture[CAPTURE_SLOT_COUNT];
+ unsigned m_captureHead = 0;
+ unsigned m_captureCount = 0;
+
+ LGAudioBrokerCaps m_caps = {};
+ LGAudioBrokerState m_brokerState = {};
+ uint64_t m_lastAdapterGeneration = 0;
+ uint64_t m_lastBindingGeneration = 0;
+ uint64_t m_captureStreamGeneration = 0;
+ uint64_t m_nextCaptureSequence = 0;
+ uint64_t m_nextRenderSequence = 0;
+ bool m_captureDiscontinuity = false;
+ bool m_playbackBlocked = false;
+ uint64_t m_playbackReadySerial = 0;
+ bool m_renderSequenceValid = false;
+ bool m_renderDiscontinuity = false;
+ DWORD m_lastOpenError = ERROR_SUCCESS;
+ DWORD m_lastError = ERROR_SUCCESS;
+ uint64_t m_renderPackets = 0;
+ uint64_t m_renderBytes = 0;
+ uint64_t m_capturePackets = 0;
+ uint64_t m_captureBytes = 0;
+ uint64_t m_reconnects = 0;
+ bool m_everOpened = false;
+ uint64_t m_nextOpenAt = 0;
+ uint64_t m_nextPollAt = 0;
+
+ RenderSlot m_render[RENDER_SLOT_COUNT];
+ OVERLAPPED m_writeOverlapped = {};
+ bool m_writePending = false;
+ DWORD m_writeBytes = 0;
+ uint8_t m_writeData[BROKER_PACKET_BYTES];
+
+ bool Stopping() const
+ {
+ return WaitForSingleObject(m_stopEvent, 0) == 0;
+ }
+
+ void ClearPlaybackBlock()
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_playbackBlocked = false;
+ }
+
+ bool PlaybackBlocked() const
+ {
+ CSRWSharedLock lock(m_lock);
+ return m_playbackBlocked;
+ }
+
+ static DWORD CALLBACK ThreadProc(void * opaque)
+ {
+ static_cast(opaque)->Thread();
+ return 0;
+ }
+
+ bool Ioctl(DWORD code, const void * input, DWORD inputSize,
+ void * output, DWORD outputSize, DWORD& returned, DWORD& error)
+ {
+ returned = 0;
+ error = ERROR_SUCCESS;
+ OVERLAPPED overlapped = {};
+ overlapped.hEvent = m_controlEvent;
+ ResetEvent(m_controlEvent);
+
+ const BOOL immediate = DeviceIoControl(m_handle, code,
+ const_cast(input), inputSize, output, outputSize,
+ &returned, &overlapped);
+ if (immediate)
+ return true;
+
+ error = GetLastError();
+ if (error != ERROR_IO_PENDING)
+ return false;
+
+ const HANDLE waits[] = { m_stopEvent, m_controlEvent };
+ const DWORD wait = WaitForMultipleObjects(
+ ARRAYSIZE(waits), waits, FALSE, INFINITE);
+ if (wait == 0)
+ {
+ CancelIoEx(m_handle, &overlapped);
+ GetOverlappedResult(m_handle, &overlapped, &returned, TRUE);
+ error = ERROR_OPERATION_ABORTED;
+ return false;
+ }
+ if (wait != 1)
+ {
+ CancelIoEx(m_handle, &overlapped);
+ GetOverlappedResult(m_handle, &overlapped, &returned, TRUE);
+ error = wait == WAIT_FAILED ? GetLastError() : ERROR_GEN_FAILURE;
+ return false;
+ }
+
+ if (!GetOverlappedResult(m_handle, &overlapped, &returned, FALSE))
+ {
+ error = GetLastError();
+ return false;
+ }
+ return true;
+ }
+
+ void SetHandle(HANDLE handle)
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_handle = handle;
+ }
+
+ void Publish(AudioEndpointStatus next, bool advance)
+ {
+ ILGAudioDeviceEvents * events = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (advance)
+ m_endpointGeneration = NextGeneration(m_endpointGeneration);
+ next.generation = m_endpointGeneration;
+ if (SameStatus(next, m_status))
+ return;
+ m_status = next;
+ events = m_events;
+ }
+ if (events)
+ events->LGAudioState(next);
+ }
+
+ void PublishUnavailable(bool advance)
+ {
+ AudioEndpointStatus next;
+ next.capabilities = 0;
+ Publish(next, advance);
+ }
+
+ uint32_t Capabilities() const
+ {
+ uint32_t result = 0;
+ if (m_caps.capabilities & LGAUDIO_BROKER_CAP_RENDER_READ)
+ result |= AUDIO_CAP_PLAYBACK;
+ if (m_caps.capabilities & LGAUDIO_BROKER_CAP_CAPTURE_WRITE)
+ result |= AUDIO_CAP_CAPTURE;
+ if (m_caps.capabilities & LGAUDIO_BROKER_CAP_CLOCK_CONTROL)
+ result |= AUDIO_CAP_CLOCK_FEEDBACK;
+ return result;
+ }
+
+ void PublishBrokerState(bool forceAdvance = false)
+ {
+ AudioEndpointStatus previous = Status();
+ AudioEndpointStatus next;
+ next.capabilities = Capabilities();
+ next.available =
+ (m_brokerState.flags & LGAUDIO_BROKER_STATE_ADAPTER_PRESENT) != 0;
+ next.active =
+ (m_brokerState.flags & LGAUDIO_BROKER_STATE_BOUND) != 0 &&
+ (m_brokerState.flags &
+ LGAUDIO_BROKER_STATE_DATA_PLANE_ACTIVE) != 0;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_bound =
+ (m_brokerState.flags & LGAUDIO_BROKER_STATE_BOUND) != 0;
+ }
+ if (next.available)
+ {
+ next.playback = BrokerStream(m_brokerState.render);
+ next.capture = BrokerStream(m_brokerState.capture);
+ }
+
+ const bool bindingChanged =
+ m_lastBindingGeneration != m_brokerState.bindingGeneration;
+ const bool advance = forceAdvance ||
+ previous.available != next.available ||
+ previous.active != next.active ||
+ m_lastAdapterGeneration != m_brokerState.adapterGeneration ||
+ bindingChanged;
+ m_lastAdapterGeneration = m_brokerState.adapterGeneration;
+ m_lastBindingGeneration = m_brokerState.bindingGeneration;
+
+ if (m_captureStreamGeneration != next.capture.generation ||
+ previous.active != next.active || bindingChanged)
+ {
+ m_captureStreamGeneration = next.capture.generation;
+ m_nextCaptureSequence = 0;
+ m_captureDiscontinuity = false;
+ }
+ if (previous.playback.generation != next.playback.generation ||
+ previous.active != next.active || bindingChanged)
+ {
+ ClearPlaybackBlock();
+ m_renderSequenceValid = false;
+ m_renderDiscontinuity = true;
+ }
+ Publish(next, advance);
+ }
+
+ bool QueryCaps(DWORD& error)
+ {
+ LGAudioBrokerCaps caps = {};
+ DWORD returned = 0;
+ if (!Ioctl(IOCTL_LGAUDIO_BROKER_QUERY_CAPS,
+ nullptr, 0, &caps, sizeof(caps), returned, error))
+ return false;
+ if (returned != sizeof(caps) || caps.size != sizeof(caps) ||
+ caps.version != LGAUDIO_BROKER_ABI_VERSION ||
+ caps.minimumVersion > LGAUDIO_BROKER_ABI_VERSION ||
+ caps.maximumVersion < LGAUDIO_BROKER_ABI_VERSION ||
+ !(caps.capabilities & LGAUDIO_BROKER_CAP_CONTROL) ||
+ !(caps.capabilities & LGAUDIO_BROKER_CAP_RENDER_READ) ||
+ !(caps.capabilities & LGAUDIO_BROKER_CAP_CAPTURE_WRITE) ||
+ !caps.maximumRenderTransfer ||
+ !caps.maximumCaptureTransfer)
+ {
+ error = ERROR_REVISION_MISMATCH;
+ return false;
+ }
+ m_caps = caps;
+ return true;
+ }
+
+ bool QueryState(DWORD& error)
+ {
+ LGAudioBrokerState state = {};
+ DWORD returned = 0;
+ if (!Ioctl(IOCTL_LGAUDIO_BROKER_QUERY_STATE,
+ nullptr, 0, &state, sizeof(state), returned, error))
+ return false;
+ if (returned != sizeof(state) || state.size != sizeof(state) ||
+ state.version != LGAUDIO_BROKER_ABI_VERSION)
+ {
+ error = ERROR_INVALID_DATA;
+ return false;
+ }
+ m_brokerState = state;
+ return true;
+ }
+
+ bool Bind(DWORD& error)
+ {
+ LGAudioBrokerBind bind = {};
+ bind.size = sizeof(bind);
+ bind.version = LGAUDIO_BROKER_ABI_VERSION;
+ bind.adapterGeneration = m_brokerState.adapterGeneration;
+ DWORD returned = 0;
+ if (!Ioctl(IOCTL_LGAUDIO_BROKER_BIND,
+ &bind, sizeof(bind), &bind, sizeof(bind), returned, error))
+ return false;
+ if (returned != sizeof(bind) || !bind.bindingGeneration)
+ {
+ error = ERROR_INVALID_DATA;
+ return false;
+ }
+ return true;
+ }
+
+ bool Unbind(DWORD& error)
+ {
+ LGAudioBrokerHeader header = {};
+ header.size = sizeof(header);
+ header.version = LGAUDIO_BROKER_ABI_VERSION;
+ DWORD returned = 0;
+ return Ioctl(IOCTL_LGAUDIO_BROKER_UNBIND,
+ &header, sizeof(header), nullptr, 0, returned, error);
+ }
+
+ void CancelDataIo()
+ {
+ if (m_handle == INVALID_HANDLE_VALUE)
+ return;
+
+ for (RenderSlot& slot : m_render)
+ if (slot.state == RenderState::PENDING)
+ CancelIoEx(m_handle, &slot.overlapped);
+ if (m_writePending)
+ CancelIoEx(m_handle, &m_writeOverlapped);
+
+ for (RenderSlot& slot : m_render)
+ {
+ if (slot.state == RenderState::PENDING)
+ {
+ DWORD transferred = 0;
+ GetOverlappedResult(
+ m_handle, &slot.overlapped, &transferred, TRUE);
+ }
+ slot.state = RenderState::IDLE;
+ slot.transferred = 0;
+ ResetEvent(slot.overlapped.hEvent);
+ }
+ if (m_writePending)
+ {
+ DWORD transferred = 0;
+ GetOverlappedResult(
+ m_handle, &m_writeOverlapped, &transferred, TRUE);
+ }
+ m_writePending = false;
+ m_writeBytes = 0;
+ ResetEvent(m_writeOverlapped.hEvent);
+ m_captureDiscontinuity = false;
+ ClearPlaybackBlock();
+ m_renderSequenceValid = false;
+ m_renderDiscontinuity = true;
+ }
+
+ void ClearCaptureQueue()
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_captureHead = 0;
+ m_captureCount = 0;
+ }
+
+ void CloseBroker(bool publish = true)
+ {
+ if (m_handle == INVALID_HANDLE_VALUE)
+ return;
+
+ CancelIoEx(m_handle, nullptr);
+ CancelDataIo();
+ const HANDLE handle = m_handle;
+ SetHandle(INVALID_HANDLE_VALUE);
+ CloseHandle(handle);
+ bool inactiveBarrier = false;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_bound = false;
+ if (m_activation == ActivationState::DEACTIVATING)
+ {
+ m_activation = ActivationState::IDLE;
+ inactiveBarrier = true;
+ }
+ }
+ m_caps = {};
+ m_brokerState = {};
+ m_lastAdapterGeneration = 0;
+ m_lastBindingGeneration = 0;
+ m_captureStreamGeneration = 0;
+ m_nextCaptureSequence = 0;
+ m_captureDiscontinuity = false;
+ ClearCaptureQueue();
+ if (!Stopping())
+ m_nextOpenAt = GetTickCount64() + OPEN_RETRY_MS;
+ if (publish || inactiveBarrier)
+ PublishUnavailable(true);
+ }
+
+ bool FinishAbsentDeactivation()
+ {
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_handle != INVALID_HANDLE_VALUE ||
+ m_activation != ActivationState::DEACTIVATING)
+ return false;
+ m_activation = ActivationState::IDLE;
+ m_captureHead = 0;
+ m_captureCount = 0;
+ }
+ PublishUnavailable(true);
+ return true;
+ }
+
+ bool OpenBroker()
+ {
+ const uint64_t now = GetTickCount64();
+ if (now < m_nextOpenAt)
+ return false;
+
+ HANDLE handle = CreateFileW(LGAUDIO_BROKER_USER_DEVICE_PATH,
+ GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr);
+ if (handle == INVALID_HANDLE_VALUE)
+ {
+ const DWORD error = GetLastError();
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_lastError = error;
+ }
+ if (error != m_lastOpenError)
+ {
+ DEBUG_INFO("Looking Glass Audio broker unavailable: %lu", error);
+ m_lastOpenError = error;
+ }
+ m_nextOpenAt = now + OPEN_RETRY_MS;
+ return false;
+ }
+
+ SetHandle(handle);
+ DWORD error = ERROR_SUCCESS;
+ if (!QueryCaps(error) || !QueryState(error))
+ {
+ if (!Stopping())
+ DEBUG_WARN("Looking Glass Audio broker query failed: %lu", error);
+ CloseBroker(false);
+ m_nextOpenAt = now + OPEN_RETRY_MS;
+ PublishUnavailable(false);
+ return false;
+ }
+
+ m_lastOpenError = ERROR_SUCCESS;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_everOpened)
+ ++m_reconnects;
+ m_everOpened = true;
+ m_lastError = ERROR_SUCCESS;
+ }
+ m_nextPollAt = 0;
+ DEBUG_INFO("Looking Glass Audio broker connected");
+ PublishBrokerState();
+ return true;
+ }
+
+ bool DesiredActive(bool& dirty)
+ {
+ CSRWExclusiveLock lock(m_lock);
+ dirty = m_controlDirty;
+ m_controlDirty = false;
+ return m_activation == ActivationState::ACTIVATING ||
+ m_activation == ActivationState::ACTIVE;
+ }
+
+ bool FinishActivation(bool bound, bool dataPlaneActive)
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (bound && dataPlaneActive &&
+ m_activation == ActivationState::ACTIVATING)
+ m_activation = ActivationState::ACTIVE;
+ else if (!bound && m_activation == ActivationState::DEACTIVATING)
+ {
+ m_activation = ActivationState::IDLE;
+ return true;
+ }
+ return false;
+ }
+
+ void DropQueuedCapture()
+ {
+ bool ready = false;
+ ILGAudioDeviceEvents * events = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ ready = m_captureCount == CAPTURE_SLOT_COUNT;
+ m_captureHead = 0;
+ m_captureCount = 0;
+ events = m_events;
+ }
+ if (ready && events)
+ events->LGAudioCaptureReady();
+ }
+
+ bool Reconcile(bool forced)
+ {
+ bool dirty = false;
+ const bool desired = DesiredActive(dirty);
+ const uint64_t now = GetTickCount64();
+ if (!forced && !dirty && now < m_nextPollAt)
+ return true;
+
+ DWORD error = ERROR_SUCCESS;
+ if (!QueryState(error))
+ {
+ if (Stopping())
+ return false;
+ DEBUG_WARN("Looking Glass Audio state query failed: %lu", error);
+ CloseBroker();
+ return false;
+ }
+
+ const bool present =
+ (m_brokerState.flags & LGAUDIO_BROKER_STATE_ADAPTER_PRESENT) != 0;
+ const bool initiallyBound =
+ (m_brokerState.flags & LGAUDIO_BROKER_STATE_BOUND) != 0;
+ if (desired && present && !initiallyBound)
+ {
+ if (!Bind(error))
+ {
+ if (error != ERROR_RETRY && error != ERROR_NOT_READY &&
+ error != ERROR_DEVICE_NOT_CONNECTED)
+ DEBUG_WARN("Looking Glass Audio broker bind failed: %lu", error);
+ }
+ if (!QueryState(error))
+ {
+ CloseBroker();
+ return false;
+ }
+ }
+ else if (!desired && initiallyBound)
+ {
+ CancelDataIo();
+ DropQueuedCapture();
+ if (!Unbind(error) && !Stopping())
+ DEBUG_WARN("Looking Glass Audio broker unbind failed: %lu", error);
+ if (!QueryState(error))
+ {
+ CloseBroker();
+ return false;
+ }
+ }
+
+ const bool bound =
+ (m_brokerState.flags & LGAUDIO_BROKER_STATE_BOUND) != 0;
+ const bool dataPlaneActive =
+ (m_brokerState.flags &
+ LGAUDIO_BROKER_STATE_DATA_PLANE_ACTIVE) != 0;
+ const bool inactiveBarrier =
+ FinishActivation(bound, dataPlaneActive);
+ PublishBrokerState(inactiveBarrier);
+ m_nextPollAt = now +
+ (desired ? ACTIVE_POLL_MS : IDLE_POLL_MS);
+ return true;
+ }
+
+ bool ValidateRender(const LGAudioBrokerPacketHeader& header,
+ DWORD transferred, AudioPacket& packet) const
+ {
+ const AudioEndpointStatus state = Status();
+ const AudioFormat format = BrokerFormat(header.format);
+ const uint32_t allowedFlags = LGAUDIO_BROKER_PACKET_DISCONTINUITY;
+ if (!state.available || !state.active ||
+ state.playback.state != AudioStreamState::RUNNING ||
+ !state.playback.formatValid ||
+ header.size != sizeof(header) ||
+ header.version != LGAUDIO_BROKER_ABI_VERSION ||
+ (header.flags & ~allowedFlags) ||
+ header.direction != LGAUDIO_BROKER_PACKET_RENDER ||
+ header.adapterGeneration != m_brokerState.adapterGeneration ||
+ header.bindingGeneration != m_brokerState.bindingGeneration ||
+ header.streamGeneration != state.playback.generation ||
+ header.reserved[0] || header.reserved[1] ||
+ !ValidFormat(format) ||
+ !SameFormat(format, state.playback.format) ||
+ !header.frameCount ||
+ header.frameCount > AUDIO_MAX_FRAMES_PER_PACKET ||
+ !header.payloadBytes ||
+ header.payloadBytes > AUDIO_MAX_PCM_BYTES ||
+ header.payloadBytes != header.frameCount * format.blockAlign ||
+ transferred != sizeof(header) + header.payloadBytes)
+ return false;
+
+ packet.endpointGeneration = state.generation;
+ packet.streamGeneration = header.streamGeneration;
+ packet.flags = header.flags & LGAUDIO_BROKER_PACKET_DISCONTINUITY ?
+ AUDIO_PACKET_DISCONTINUITY : 0;
+ packet.frames = header.frameCount;
+ packet.format = format;
+ return true;
+ }
+
+ int NextReadyRender() const
+ {
+ int selected = -1;
+ uint64_t sequence = 0;
+ for (unsigned index = 0; index < RENDER_SLOT_COUNT; ++index)
+ {
+ const RenderSlot& slot = m_render[index];
+ if (slot.state != RenderState::READY ||
+ slot.transferred < sizeof(LGAudioBrokerPacketHeader))
+ continue;
+ LGAudioBrokerPacketHeader header = {};
+ memcpy(&header, slot.data, sizeof(header));
+ if (selected < 0 || header.sequence < sequence)
+ {
+ selected = static_cast(index);
+ sequence = header.sequence;
+ }
+ }
+ return selected;
+ }
+
+ void DeliverRender()
+ {
+ for (RenderSlot& slot : m_render)
+ {
+ if (slot.state != RenderState::READY ||
+ slot.transferred >= sizeof(LGAudioBrokerPacketHeader))
+ continue;
+ DEBUG_WARN("Rejected truncated Looking Glass Audio render packet");
+ slot.state = RenderState::IDLE;
+ slot.transferred = 0;
+ m_renderDiscontinuity = true;
+ }
+
+ if (PlaybackBlocked())
+ return;
+
+ for (;;)
+ {
+ const int index = NextReadyRender();
+ if (index < 0)
+ return;
+ RenderSlot& slot = m_render[index];
+ LGAudioBrokerPacketHeader header = {};
+ memcpy(&header, slot.data, sizeof(header));
+ AudioPacket packet;
+ if (!ValidateRender(header, slot.transferred, packet))
+ {
+ DEBUG_WARN("Rejected malformed Looking Glass Audio render packet");
+ slot.state = RenderState::IDLE;
+ m_renderDiscontinuity = true;
+ continue;
+ }
+
+ if (m_renderSequenceValid && header.sequence != m_nextRenderSequence)
+ packet.flags |= AUDIO_PACKET_DISCONTINUITY;
+ if (m_renderDiscontinuity)
+ packet.flags |= AUDIO_PACKET_DISCONTINUITY;
+
+ ILGAudioDeviceEvents * events = nullptr;
+ uint64_t readySerial = 0;
+ {
+ CSRWSharedLock lock(m_lock);
+ events = m_events;
+ readySerial = m_playbackReadySerial;
+ }
+ const uint8_t * payload = slot.data + sizeof(header);
+ const AudioResult result = events ?
+ events->LGAudioPlayback(packet, payload, header.payloadBytes) :
+ AudioResult::UNAVAILABLE;
+ if (result == AudioResult::BUSY)
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_playbackBlocked = m_playbackReadySerial == readySerial;
+ return;
+ }
+
+ slot.state = RenderState::IDLE;
+ m_nextRenderSequence = header.sequence + 1;
+ m_renderSequenceValid = true;
+ m_renderDiscontinuity = result != AudioResult::ACCEPTED;
+ if (result == AudioResult::ACCEPTED)
+ {
+ CSRWExclusiveLock lock(m_lock);
+ ++m_renderPackets;
+ m_renderBytes += header.payloadBytes;
+ }
+ }
+ }
+
+ bool CompleteRenderIo(bool& disconnected)
+ {
+ bool changed = false;
+ for (RenderSlot& slot : m_render)
+ {
+ if (slot.state != RenderState::PENDING)
+ continue;
+ DWORD transferred = 0;
+ if (GetOverlappedResult(
+ m_handle, &slot.overlapped, &transferred, FALSE))
+ {
+ ResetEvent(slot.overlapped.hEvent);
+ slot.transferred = transferred;
+ slot.state = RenderState::READY;
+ changed = true;
+ continue;
+ }
+ const DWORD error = GetLastError();
+ if (error == ERROR_IO_INCOMPLETE)
+ continue;
+ slot.state = RenderState::IDLE;
+ slot.transferred = 0;
+ ResetEvent(slot.overlapped.hEvent);
+ changed = true;
+ if (error == ERROR_DEVICE_NOT_CONNECTED || error == ERROR_INVALID_HANDLE)
+ disconnected = true;
+ else if (!RecoverableIoError(error))
+ DEBUG_WARN("Looking Glass Audio render read failed: %lu", error);
+ }
+ return changed;
+ }
+
+ void StartRenderIo()
+ {
+ const AudioEndpointStatus state = Status();
+ if (!state.active || !state.playback.formatValid ||
+ state.playback.state != AudioStreamState::RUNNING)
+ return;
+
+ const DWORD payload = static_cast(std::min(
+ AUDIO_MAX_FRAMES_PER_PACKET * state.playback.format.blockAlign,
+ m_caps.maximumRenderTransfer));
+ const DWORD aligned = payload -
+ payload % state.playback.format.blockAlign;
+ if (!aligned)
+ return;
+
+ for (RenderSlot& slot : m_render)
+ {
+ if (slot.state != RenderState::IDLE)
+ continue;
+ ResetEvent(slot.overlapped.hEvent);
+ DWORD transferred = 0;
+ const BOOL immediate = ReadFile(m_handle, slot.data,
+ sizeof(LGAudioBrokerPacketHeader) + aligned,
+ nullptr, &slot.overlapped);
+ if (!immediate)
+ {
+ const DWORD error = GetLastError();
+ if (error == ERROR_IO_PENDING)
+ {
+ slot.state = RenderState::PENDING;
+ continue;
+ }
+ if (!RecoverableIoError(error))
+ DEBUG_WARN("Looking Glass Audio render read failed: %lu", error);
+ ResetEvent(slot.overlapped.hEvent);
+ continue;
+ }
+ if (!GetOverlappedResult(
+ m_handle, &slot.overlapped, &transferred, FALSE))
+ {
+ const DWORD error = GetLastError();
+ if (error != ERROR_IO_INCOMPLETE && !RecoverableIoError(error))
+ DEBUG_WARN("Looking Glass Audio render completion failed: %lu",
+ error);
+ if (error == ERROR_IO_INCOMPLETE)
+ slot.state = RenderState::PENDING;
+ else
+ ResetEvent(slot.overlapped.hEvent);
+ continue;
+ }
+ slot.transferred = transferred;
+ slot.state = RenderState::READY;
+ ResetEvent(slot.overlapped.hEvent);
+ }
+ }
+
+ bool PopCapture(CaptureSlot& result, bool& becameReady)
+ {
+ ILGAudioDeviceEvents * events = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_captureCount)
+ return false;
+ becameReady = m_captureCount == CAPTURE_SLOT_COUNT;
+ CaptureSlot& source = m_capture[m_captureHead];
+ result.packet = source.packet;
+ result.size = source.size;
+ memcpy(result.data, source.data, source.size);
+ m_captureHead = (m_captureHead + 1) % CAPTURE_SLOT_COUNT;
+ --m_captureCount;
+ events = m_events;
+ }
+ if (becameReady && events)
+ events->LGAudioCaptureReady();
+ return true;
+ }
+
+ bool CompleteWriteIo(bool& disconnected)
+ {
+ if (!m_writePending)
+ return false;
+
+ DWORD transferred = 0;
+ if (!GetOverlappedResult(
+ m_handle, &m_writeOverlapped, &transferred, FALSE))
+ {
+ const DWORD error = GetLastError();
+ if (error == ERROR_IO_INCOMPLETE)
+ return false;
+ m_writePending = false;
+ m_writeBytes = 0;
+ m_captureDiscontinuity = true;
+ ResetEvent(m_writeOverlapped.hEvent);
+ if (error == ERROR_DEVICE_NOT_CONNECTED || error == ERROR_INVALID_HANDLE)
+ disconnected = true;
+ else if (!RecoverableIoError(error))
+ DEBUG_WARN("Looking Glass Audio capture write failed: %lu", error);
+ return true;
+ }
+
+ m_writePending = false;
+ ResetEvent(m_writeOverlapped.hEvent);
+ if (transferred == m_writeBytes)
+ {
+ ++m_nextCaptureSequence;
+ m_captureDiscontinuity = false;
+ CSRWExclusiveLock lock(m_lock);
+ ++m_capturePackets;
+ m_captureBytes += m_writeBytes - sizeof(LGAudioBrokerPacketHeader);
+ }
+ else
+ {
+ m_captureDiscontinuity = true;
+ DEBUG_WARN("Looking Glass Audio capture write was truncated");
+ }
+ m_writeBytes = 0;
+ return true;
+ }
+
+ void StartWriteIo()
+ {
+ if (m_writePending)
+ return;
+
+ const AudioEndpointStatus state = Status();
+ if (!state.active || !state.capture.formatValid ||
+ state.capture.state != AudioStreamState::RUNNING)
+ return;
+
+ CaptureSlot capture;
+ bool becameReady = false;
+ while (PopCapture(capture, becameReady))
+ {
+ if (capture.packet.endpointGeneration != state.generation ||
+ capture.packet.streamGeneration != state.capture.generation ||
+ !SameFormat(capture.packet.format, state.capture.format))
+ continue;
+
+ LGAudioBrokerPacketHeader header = {};
+ header.size = sizeof(header);
+ header.version = LGAUDIO_BROKER_ABI_VERSION;
+ header.flags = capture.packet.flags & AUDIO_PACKET_DISCONTINUITY ?
+ LGAUDIO_BROKER_PACKET_DISCONTINUITY : 0;
+ if (m_captureDiscontinuity)
+ header.flags |= LGAUDIO_BROKER_PACKET_DISCONTINUITY;
+ header.direction = LGAUDIO_BROKER_PACKET_CAPTURE;
+ header.adapterGeneration = m_brokerState.adapterGeneration;
+ header.bindingGeneration = m_brokerState.bindingGeneration;
+ header.streamGeneration = state.capture.generation;
+ header.sequence = m_nextCaptureSequence;
+ header.format = DeviceFormat(state.capture.format);
+ header.frameCount = capture.packet.frames;
+ header.payloadBytes = static_cast(capture.size);
+ memcpy(m_writeData, &header, sizeof(header));
+ memcpy(m_writeData + sizeof(header), capture.data, capture.size);
+ m_writeBytes = static_cast(sizeof(header) + capture.size);
+
+ ResetEvent(m_writeOverlapped.hEvent);
+ const BOOL immediate = WriteFile(m_handle, m_writeData,
+ m_writeBytes, nullptr, &m_writeOverlapped);
+ m_writePending = true;
+ if (!immediate && GetLastError() != ERROR_IO_PENDING)
+ {
+ const DWORD error = GetLastError();
+ m_writePending = false;
+ m_writeBytes = 0;
+ m_captureDiscontinuity = true;
+ ResetEvent(m_writeOverlapped.hEvent);
+ if (!RecoverableIoError(error))
+ DEBUG_WARN("Looking Glass Audio capture write failed: %lu", error);
+ }
+ return;
+ }
+ }
+
+ DWORD WaitTimeout() const
+ {
+ const uint64_t now = GetTickCount64();
+ const uint64_t deadline = m_handle == INVALID_HANDLE_VALUE ?
+ m_nextOpenAt : m_nextPollAt;
+ if (deadline <= now)
+ return 0;
+ return static_cast(std::min(
+ deadline - now, OPEN_RETRY_MS));
+ }
+
+ void Thread()
+ {
+ while (!Stopping())
+ {
+ if (m_handle == INVALID_HANDLE_VALUE)
+ FinishAbsentDeactivation();
+ if (m_handle == INVALID_HANDLE_VALUE && !OpenBroker())
+ {
+ const HANDLE waits[] = { m_stopEvent, m_wakeEvent };
+ WaitForMultipleObjects(
+ ARRAYSIZE(waits), waits, FALSE, WaitTimeout());
+ continue;
+ }
+
+ bool disconnected = false;
+ CompleteWriteIo(disconnected);
+ CompleteRenderIo(disconnected);
+ if (disconnected)
+ {
+ CloseBroker();
+ m_nextOpenAt = GetTickCount64() + OPEN_RETRY_MS;
+ continue;
+ }
+
+ if (!Reconcile(false))
+ continue;
+ DeliverRender();
+ StartWriteIo();
+ StartRenderIo();
+
+ HANDLE waits[2 + RENDER_SLOT_COUNT + 1] = {};
+ unsigned count = 0;
+ waits[count++] = m_stopEvent;
+ waits[count++] = m_wakeEvent;
+ for (RenderSlot& slot : m_render)
+ waits[count++] = slot.overlapped.hEvent;
+ waits[count++] = m_writeOverlapped.hEvent;
+ WaitForMultipleObjects(count, waits, FALSE, WaitTimeout());
+ }
+
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_activation = ActivationState::DEACTIVATING;
+ }
+ if (m_handle != INVALID_HANDLE_VALUE)
+ {
+ CancelDataIo();
+ if (m_brokerState.flags & LGAUDIO_BROKER_STATE_BOUND)
+ {
+ DWORD error = ERROR_SUCCESS;
+ Unbind(error);
+ }
+ CloseBroker(false);
+ }
+ FinishAbsentDeactivation();
+ }
+
+ void CloseEvents()
+ {
+ for (RenderSlot& slot : m_render)
+ {
+ if (slot.overlapped.hEvent)
+ CloseHandle(slot.overlapped.hEvent);
+ slot.overlapped = {};
+ slot.state = RenderState::IDLE;
+ }
+ if (m_writeEvent)
+ CloseHandle(m_writeEvent);
+ if (m_controlEvent)
+ CloseHandle(m_controlEvent);
+ if (m_wakeEvent)
+ CloseHandle(m_wakeEvent);
+ if (m_stopEvent)
+ CloseHandle(m_stopEvent);
+ m_writeEvent = nullptr;
+ m_controlEvent = nullptr;
+ m_wakeEvent = nullptr;
+ m_stopEvent = nullptr;
+ m_writeOverlapped = {};
+ }
+
+public:
+ CState()
+ {
+ m_status.generation = m_endpointGeneration;
+ }
+
+ ~CState()
+ {
+ Stop();
+ }
+
+ bool Start(ILGAudioDeviceEvents& events)
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_started)
+ return !m_stopping;
+
+ m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ m_wakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+ m_controlEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ m_writeEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ if (!m_stopEvent || !m_wakeEvent || !m_controlEvent || !m_writeEvent)
+ {
+ CloseEvents();
+ return false;
+ }
+ m_writeOverlapped.hEvent = m_writeEvent;
+ for (RenderSlot& slot : m_render)
+ {
+ slot.overlapped.hEvent =
+ CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ if (!slot.overlapped.hEvent)
+ {
+ CloseEvents();
+ return false;
+ }
+ }
+
+ m_events = &events;
+ m_started = true;
+ m_stopping = false;
+ m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
+ if (!m_thread)
+ {
+ m_started = false;
+ m_events = nullptr;
+ CloseEvents();
+ return false;
+ }
+ return true;
+ }
+
+ void Stop()
+ {
+ HANDLE thread = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_started)
+ return;
+ m_stopping = true;
+ thread = m_thread;
+ SetEvent(m_stopEvent);
+ SetEvent(m_wakeEvent);
+ }
+ if (thread)
+ WaitForSingleObject(thread, INFINITE);
+
+ CSRWExclusiveLock lock(m_lock);
+ if (m_thread)
+ CloseHandle(m_thread);
+ m_thread = nullptr;
+ m_events = nullptr;
+ m_started = false;
+ m_stopping = false;
+ m_handle = INVALID_HANDLE_VALUE;
+ m_bound = false;
+ m_captureHead = 0;
+ m_captureCount = 0;
+ m_activation = ActivationState::IDLE;
+ m_controlDirty = true;
+ m_status = {};
+ m_status.generation = m_endpointGeneration;
+ CloseEvents();
+ }
+
+ AudioEndpointStatus Status() const
+ {
+ CSRWSharedLock lock(m_lock);
+ return m_status;
+ }
+
+ LGAudioDeviceSnapshot Snapshot() const
+ {
+ CSRWSharedLock lock(m_lock);
+ LGAudioDeviceSnapshot result;
+ result.status = m_status;
+ result.open = m_handle != INVALID_HANDLE_VALUE;
+ result.bound = m_bound;
+ result.activeRequested =
+ m_activation == ActivationState::ACTIVATING ||
+ m_activation == ActivationState::ACTIVE;
+ result.deactivating =
+ m_activation == ActivationState::DEACTIVATING;
+ result.renderPackets = m_renderPackets;
+ result.renderBytes = m_renderBytes;
+ result.capturePackets = m_capturePackets;
+ result.captureBytes = m_captureBytes;
+ result.reconnects = m_reconnects;
+ result.lastError = m_lastError;
+ return result;
+ }
+
+ AudioResult SetActive(bool active)
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_started || m_stopping)
+ return AudioResult::UNAVAILABLE;
+ if (active)
+ {
+ if (m_activation == ActivationState::DEACTIVATING)
+ return AudioResult::BUSY;
+ if (m_activation == ActivationState::IDLE)
+ m_activation = ActivationState::ACTIVATING;
+ }
+ else if (m_activation == ActivationState::ACTIVATING ||
+ m_activation == ActivationState::ACTIVE)
+ {
+ m_activation = ActivationState::DEACTIVATING;
+ m_controlDirty = true;
+ SetEvent(m_wakeEvent);
+ }
+ if (active)
+ {
+ m_controlDirty = true;
+ SetEvent(m_wakeEvent);
+ }
+ return AudioResult::ACCEPTED;
+ }
+
+ AudioResult SendCapture(const AudioPacket& packet,
+ const uint8_t * data, size_t size)
+ {
+ const uint32_t allowedFlags = AUDIO_PACKET_DISCONTINUITY |
+ AUDIO_PACKET_SILENT | AUDIO_PACKET_CLOCK_VALID |
+ AUDIO_PACKET_CLOCK_STABLE;
+ if ((packet.flags & ~allowedFlags) ||
+ ((packet.flags & AUDIO_PACKET_CLOCK_STABLE) &&
+ !(packet.flags & AUDIO_PACKET_CLOCK_VALID)) ||
+ !packet.frames ||
+ packet.frames > AUDIO_MAX_FRAMES_PER_PACKET ||
+ !ValidFormat(packet.format))
+ 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 lock(m_lock);
+ if (!m_started || m_stopping ||
+ m_activation != ActivationState::ACTIVE ||
+ !m_status.available || !m_status.active)
+ return AudioResult::UNAVAILABLE;
+ if (packet.endpointGeneration != m_status.generation ||
+ packet.streamGeneration != m_status.capture.generation)
+ return AudioResult::STALE;
+ if (!m_status.capture.formatValid ||
+ m_status.capture.state != AudioStreamState::RUNNING ||
+ !SameFormat(packet.format, m_status.capture.format))
+ return AudioResult::UNAVAILABLE;
+ if (m_captureCount == CAPTURE_SLOT_COUNT)
+ return AudioResult::BUSY;
+
+ CaptureSlot& slot =
+ m_capture[(m_captureHead + m_captureCount) % CAPTURE_SLOT_COUNT];
+ slot.packet = packet;
+ slot.size = pcmBytes;
+ if (silent)
+ memset(slot.data, 0, pcmBytes);
+ else
+ memcpy(slot.data, data, pcmBytes);
+ ++m_captureCount;
+ SetEvent(m_wakeEvent);
+ return AudioResult::ACCEPTED;
+ }
+
+ AudioResult SetClockFeedback(const AudioClockFeedback& feedback)
+ {
+ const uint32_t allowedFlags =
+ AUDIO_CLOCK_VALID | AUDIO_CLOCK_STABLE;
+ if ((feedback.flags & ~allowedFlags) ||
+ !(feedback.flags & AUDIO_CLOCK_VALID))
+ return AudioResult::REJECTED;
+ CSRWSharedLock lock(m_lock);
+ if (!m_started || m_stopping || !m_status.active)
+ return AudioResult::UNAVAILABLE;
+ if (feedback.endpointGeneration != m_status.generation ||
+ feedback.playbackGeneration != m_status.playback.generation)
+ return AudioResult::STALE;
+ // The v1 broker advertises no clock-control capability or IOCTL yet.
+ return AudioResult::UNAVAILABLE;
+ }
+
+ void PlaybackReceiveReady()
+ {
+ bool wake = false;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_started && !m_stopping)
+ {
+ m_playbackReadySerial = NextGeneration(m_playbackReadySerial);
+ m_playbackBlocked = false;
+ wake = true;
+ }
+ }
+ if (wake)
+ SetEvent(m_wakeEvent);
+ }
+};
+
+CLGAudioDevice::CLGAudioDevice() :
+ m_state(new (std::nothrow) CState())
+{
+}
+
+CLGAudioDevice::~CLGAudioDevice()
+{
+ Stop();
+}
+
+bool CLGAudioDevice::Start(ILGAudioDeviceEvents& events)
+{
+ return m_state && m_state->Start(events);
+}
+
+void CLGAudioDevice::Stop()
+{
+ if (m_state)
+ m_state->Stop();
+}
+
+LGAudioDeviceSnapshot CLGAudioDevice::Snapshot() const
+{
+ return m_state ? m_state->Snapshot() : LGAudioDeviceSnapshot {};
+}
+
+AudioResult CLGAudioDevice::SetActive(bool active)
+{
+ return m_state ?
+ m_state->SetActive(active) : AudioResult::UNAVAILABLE;
+}
+
+AudioResult CLGAudioDevice::SendCapture(const AudioPacket& packet,
+ const uint8_t * data, size_t size)
+{
+ return m_state ? m_state->SendCapture(packet, data, size) :
+ AudioResult::UNAVAILABLE;
+}
+
+AudioResult CLGAudioDevice::SetClockFeedback(
+ const AudioClockFeedback& feedback)
+{
+ return m_state ? m_state->SetClockFeedback(feedback) :
+ AudioResult::UNAVAILABLE;
+}
+
+void CLGAudioDevice::PlaybackReceiveReady()
+{
+ if (m_state)
+ m_state->PlaybackReceiveReady();
+}
diff --git a/idd/LGIdd/audio/CLGAudioDevice.h b/idd/LGIdd/audio/CLGAudioDevice.h
new file mode 100644
index 00000000..062374a0
--- /dev/null
+++ b/idd/LGIdd/audio/CLGAudioDevice.h
@@ -0,0 +1,94 @@
+/**
+ * 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
+
+struct LGAudioDeviceSnapshot
+{
+ AudioEndpointStatus status;
+ bool open = false;
+ bool bound = false;
+ bool activeRequested = false;
+ bool deactivating = false;
+ uint64_t renderPackets = 0;
+ uint64_t renderBytes = 0;
+ uint64_t capturePackets = 0;
+ uint64_t captureBytes = 0;
+ uint64_t reconnects = 0;
+ uint32_t lastError = 0;
+};
+
+class ILGAudioDeviceEvents
+{
+public:
+ virtual ~ILGAudioDeviceEvents() = default;
+
+ virtual void LGAudioState(const AudioEndpointStatus& state) = 0;
+ virtual AudioResult LGAudioPlayback(const AudioPacket& packet,
+ const uint8_t * data, size_t size) = 0;
+ virtual void LGAudioCaptureReady() = 0;
+};
+
+class ILGAudioDevice
+{
+public:
+ virtual ~ILGAudioDevice() = default;
+
+ // Methods are thread-safe and nonblocking except Stop. Stop is a callback-
+ // quiescence barrier; after it returns no event callback can still run.
+ virtual bool Start(ILGAudioDeviceEvents& events) = 0;
+ virtual void Stop() = 0;
+ virtual LGAudioDeviceSnapshot Snapshot() const = 0;
+ virtual AudioResult SetActive(bool active) = 0;
+ virtual AudioResult SendCapture(const AudioPacket& packet,
+ const uint8_t * data, size_t size) = 0;
+ virtual AudioResult SetClockFeedback(
+ const AudioClockFeedback& feedback) = 0;
+ virtual void PlaybackReceiveReady() = 0;
+};
+
+class CLGAudioDevice final : public ILGAudioDevice
+{
+private:
+ class CState;
+ std::unique_ptr m_state;
+
+public:
+ CLGAudioDevice();
+ ~CLGAudioDevice();
+
+ CLGAudioDevice(const CLGAudioDevice&) = delete;
+ CLGAudioDevice& operator=(const CLGAudioDevice&) = delete;
+
+ bool Start(ILGAudioDeviceEvents& events) override;
+ void Stop() override;
+
+ LGAudioDeviceSnapshot Snapshot() const override;
+ AudioResult SetActive(bool active) override;
+ AudioResult SendCapture(const AudioPacket& packet,
+ const uint8_t * data, size_t size) override;
+ AudioResult SetClockFeedback(
+ const AudioClockFeedback& feedback) override;
+ void PlaybackReceiveReady() override;
+};
diff --git a/idd/LGIdd/transport/CAudioHub.cpp b/idd/LGIdd/transport/CAudioHub.cpp
new file mode 100644
index 00000000..34f8426e
--- /dev/null
+++ b/idd/LGIdd/transport/CAudioHub.cpp
@@ -0,0 +1,386 @@
+/**
+ * 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/CAudioHub.h"
+
+#include
+
+CAudioHub::CAudioHub() :
+ m_device(new (std::nothrow) CLGAudioDevice())
+{
+}
+
+CAudioHub::CAudioHub(std::unique_ptr device) :
+ m_device(std::move(device))
+{
+}
+
+CAudioHub::~CAudioHub()
+{
+ Stop();
+}
+
+bool CAudioHub::StartDevice()
+{
+ {
+ CSRWSharedLock lock(m_lock);
+ if (m_stopped || !m_device)
+ return false;
+ if (m_deviceStarted)
+ return true;
+ }
+
+ if (!m_device->Start(*this))
+ return false;
+
+ CSRWExclusiveLock lock(m_lock);
+ if (m_stopped)
+ {
+ lock.Unlock();
+ m_device->Stop();
+ return false;
+ }
+ m_deviceStarted = true;
+ return true;
+}
+
+bool CAudioHub::Start()
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ return StartDevice();
+}
+
+bool CAudioHub::MarkFailed(IAudioSource * source)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (!source || m_source != source || (!m_active && !m_reserved))
+ return false;
+ const bool changed = !m_failed;
+ m_failed = true;
+ m_failurePending = true;
+ return changed;
+}
+
+bool CAudioHub::Bind(
+ BackendId backend, uint32_t epoch, IAudioSource& source)
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ if (!backend || !epoch || !StartDevice())
+ return false;
+
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_stopped || m_source || m_active || m_reserved ||
+ m_deactivating)
+ return false;
+ m_source = &source;
+ m_backend = backend;
+ m_epoch = epoch;
+ m_active = true;
+ m_reserved = true;
+ m_failed = false;
+ m_failurePending = false;
+ }
+
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ const bool started = source.Start(*this);
+ bool attached = false;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (started && m_source == &source && m_backend == backend &&
+ m_epoch == epoch && m_active && m_reserved && !m_failed &&
+ !m_stopped)
+ {
+ m_running = true;
+ m_reserved = false;
+ attached = true;
+ }
+ else
+ {
+ m_active = false;
+ m_running = false;
+ }
+ }
+
+ if (!attached)
+ {
+ source.Stop();
+ CSRWExclusiveLock lock(m_lock);
+ if (m_source == &source && m_backend == backend && m_epoch == epoch)
+ {
+ m_source = nullptr;
+ m_backend = 0;
+ m_epoch = 0;
+ m_active = false;
+ m_running = false;
+ m_reserved = false;
+ m_failed = false;
+ m_failurePending = false;
+ }
+ return false;
+ }
+
+ source.AudioState(m_device->Snapshot().status);
+ return true;
+}
+
+void CAudioHub::Unbind(BackendId backend, uint32_t epoch)
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ IAudioSource * source = nullptr;
+ bool stop = false;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if ((!m_active && !m_failed && !m_reserved) ||
+ m_backend != backend || m_epoch != epoch || !m_source)
+ return;
+ source = m_source;
+ stop = m_running || m_reserved;
+ m_active = false;
+ m_running = false;
+ m_reserved = true;
+ m_failed = false;
+ m_failurePending = false;
+ }
+
+ {
+ // Serialize the request and barrier publication with state callbacks so
+ // a fast inactive callback cannot pass before the barrier is visible.
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ // Start deactivation before asking the source to quiesce, then repeat it
+ // after Stop so a target call already in flight cannot reactivate the
+ // device on behalf of the departing source.
+ m_device->SetActive(false);
+ if (stop)
+ source->Stop();
+ const LGAudioDeviceSnapshot before = m_device->Snapshot();
+ const AudioResult deactivate = m_device->SetActive(false);
+ const LGAudioDeviceSnapshot after = m_device->Snapshot();
+ const bool pending = deactivate == AudioResult::ACCEPTED &&
+ (before.status.active || before.activeRequested ||
+ after.status.active || after.activeRequested ||
+ after.deactivating);
+ {
+ CSRWExclusiveLock lock(m_lock);
+ m_deactivating = pending;
+ m_deactivationGeneration = pending ?
+ before.status.generation : 0;
+ }
+ }
+
+ CSRWExclusiveLock lock(m_lock);
+ if (m_source == source && m_backend == backend && m_epoch == epoch)
+ {
+ m_source = nullptr;
+ m_backend = 0;
+ m_epoch = 0;
+ m_active = false;
+ m_running = false;
+ m_reserved = false;
+ m_failed = false;
+ m_failurePending = false;
+ }
+}
+
+bool CAudioHub::TakeFailure(SourceKey& source)
+{
+ CSRWSharedLock lifecycleLock(m_lifecycleLock);
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_failurePending)
+ return false;
+ source = {};
+ source.backend = m_backend;
+ source.epoch = m_epoch;
+ m_failurePending = false;
+ return true;
+}
+
+LGAudioDeviceSnapshot CAudioHub::Snapshot() const
+{
+ CSRWSharedLock lock(const_cast(m_lock));
+ return m_device ? m_device->Snapshot() : LGAudioDeviceSnapshot {};
+}
+
+void CAudioHub::Stop()
+{
+ CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
+ IAudioSource * source = nullptr;
+ bool stopSource = false;
+ bool stopDevice = false;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_stopped)
+ return;
+ m_stopped = true;
+ source = m_source;
+ stopSource = source && (m_running || m_reserved);
+ stopDevice = m_deviceStarted;
+ m_active = false;
+ m_running = false;
+ m_reserved = source != nullptr;
+ m_failed = false;
+ m_failurePending = false;
+ }
+
+ if (m_device)
+ m_device->SetActive(false);
+ {
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ if (stopSource)
+ source->Stop();
+ }
+ if (stopDevice)
+ m_device->Stop();
+
+ CSRWExclusiveLock lock(m_lock);
+ m_source = nullptr;
+ m_backend = 0;
+ m_epoch = 0;
+ m_deviceStarted = false;
+ m_active = false;
+ m_running = false;
+ m_reserved = false;
+ m_failed = false;
+ m_failurePending = false;
+ m_deactivating = false;
+ m_deactivationGeneration = 0;
+}
+
+AudioResult CAudioHub::SetAudioActive(bool active)
+{
+ IAudioSource * source = nullptr;
+ {
+ CSRWSharedLock lock(m_lock);
+ if (m_stopped || !m_active || !m_running || m_failed ||
+ !m_deviceStarted)
+ return AudioResult::UNAVAILABLE;
+ source = m_source;
+ }
+ const AudioResult result = m_device->SetActive(active);
+ if (result == AudioResult::FAILED)
+ MarkFailed(source);
+ return result;
+}
+
+AudioResult CAudioHub::SendCapture(const AudioPacket& packet,
+ const uint8_t * data, size_t size)
+{
+ IAudioSource * source = nullptr;
+ {
+ CSRWSharedLock lock(m_lock);
+ if (m_stopped || !m_active || !m_running || m_failed ||
+ !m_deviceStarted)
+ return AudioResult::UNAVAILABLE;
+ source = m_source;
+ }
+ const AudioResult result = m_device->SendCapture(packet, data, size);
+ if (result == AudioResult::FAILED)
+ MarkFailed(source);
+ return result;
+}
+
+AudioResult CAudioHub::SetClockFeedback(
+ const AudioClockFeedback& feedback)
+{
+ IAudioSource * source = nullptr;
+ {
+ CSRWSharedLock lock(m_lock);
+ if (m_stopped || !m_active || !m_running || m_failed ||
+ !m_deviceStarted)
+ return AudioResult::UNAVAILABLE;
+ source = m_source;
+ }
+ const AudioResult result = m_device->SetClockFeedback(feedback);
+ if (result == AudioResult::FAILED)
+ MarkFailed(source);
+ return result;
+}
+
+void CAudioHub::PlaybackReceiveReady()
+{
+ {
+ CSRWSharedLock lock(m_lock);
+ if (m_stopped || !m_active || !m_running || m_failed ||
+ !m_deviceStarted)
+ return;
+ }
+ m_device->PlaybackReceiveReady();
+}
+
+void CAudioHub::AudioFailed()
+{
+ IAudioSource * source = nullptr;
+ {
+ CSRWSharedLock lock(m_lock);
+ source = m_source;
+ }
+ if (source)
+ MarkFailed(source);
+}
+
+void CAudioHub::LGAudioState(const AudioEndpointStatus& state)
+{
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ IAudioSource * source = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_deactivating && !state.active &&
+ state.generation != m_deactivationGeneration)
+ {
+ m_deactivating = false;
+ m_deactivationGeneration = 0;
+ }
+ if (!m_stopped && m_active && m_running && !m_failed)
+ source = m_source;
+ }
+ if (source)
+ source->AudioState(state);
+}
+
+AudioResult CAudioHub::LGAudioPlayback(const AudioPacket& packet,
+ const uint8_t * data, size_t size)
+{
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ IAudioSource * source = nullptr;
+ {
+ CSRWSharedLock lock(m_lock);
+ if (!m_stopped && m_active && m_running && !m_failed)
+ source = m_source;
+ }
+ if (!source)
+ return AudioResult::UNAVAILABLE;
+ const AudioResult result = source->SendPlayback(packet, data, size);
+ if (result == AudioResult::FAILED)
+ MarkFailed(source);
+ return result;
+}
+
+void CAudioHub::LGAudioCaptureReady()
+{
+ CSRWExclusiveLock callbackLock(m_callbackLock);
+ IAudioSource * source = nullptr;
+ {
+ CSRWSharedLock lock(m_lock);
+ if (!m_stopped && m_active && m_running && !m_failed)
+ source = m_source;
+ }
+ if (source)
+ source->CaptureReceiveReady();
+}
diff --git a/idd/LGIdd/transport/CAudioHub.h b/idd/LGIdd/transport/CAudioHub.h
new file mode 100644
index 00000000..cbc17f72
--- /dev/null
+++ b/idd/LGIdd/transport/CAudioHub.h
@@ -0,0 +1,83 @@
+/**
+ * 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 "CSRWLock.h"
+#include "audio/CLGAudioDevice.h"
+#include "transport/IAudioSource.h"
+#include "transport/ITransport.h"
+
+#include
+
+class CAudioHub final : public IAudioTarget,
+ private ILGAudioDeviceEvents
+{
+private:
+ CSRWLock m_lifecycleLock;
+ CSRWLock m_callbackLock;
+ CSRWLock m_lock;
+ std::unique_ptr m_device;
+ IAudioSource * m_source = nullptr;
+ BackendId m_backend = 0;
+ uint32_t m_epoch = 0;
+ bool m_deviceStarted = false;
+ bool m_active = false;
+ bool m_running = false;
+ bool m_reserved = false;
+ bool m_failed = false;
+ bool m_failurePending = false;
+ bool m_deactivating = false;
+ uint64_t m_deactivationGeneration = 0;
+ bool m_stopped = false;
+
+ bool StartDevice();
+ bool MarkFailed(IAudioSource * source);
+
+ void LGAudioState(const AudioEndpointStatus& state) override;
+ AudioResult LGAudioPlayback(const AudioPacket& packet,
+ const uint8_t * data, size_t size) override;
+ void LGAudioCaptureReady() override;
+
+public:
+ CAudioHub();
+ explicit CAudioHub(std::unique_ptr device);
+ ~CAudioHub() override;
+
+ CAudioHub(const CAudioHub&) = delete;
+ CAudioHub& operator=(const CAudioHub&) = delete;
+
+ // Start only creates the passive connector worker. Broker absence or an
+ // incompatible/busy broker is reported through state and is not failure.
+ bool Start();
+ bool Bind(BackendId backend, uint32_t epoch, IAudioSource& source);
+ void Unbind(BackendId backend, uint32_t epoch);
+ bool TakeFailure(SourceKey& source);
+ LGAudioDeviceSnapshot Snapshot() const;
+ void Stop();
+
+ AudioResult SetAudioActive(bool active) override;
+ AudioResult SendCapture(const AudioPacket& packet,
+ const uint8_t * data, size_t size) override;
+ AudioResult SetClockFeedback(
+ const AudioClockFeedback& feedback) override;
+ void PlaybackReceiveReady() override;
+ void AudioFailed() override;
+};
diff --git a/idd/LGIdd/transport/CTransportManager.cpp b/idd/LGIdd/transport/CTransportManager.cpp
index 30d10504..ae847ddc 100644
--- a/idd/LGIdd/transport/CTransportManager.cpp
+++ b/idd/LGIdd/transport/CTransportManager.cpp
@@ -26,6 +26,7 @@
#include "ipc/CPipeServer.h"
#include "Seq.h"
#include "transport/IClipboardSource.h"
+#include "transport/IAudioSource.h"
#include "transport/ITexStage.h"
#include
@@ -489,9 +490,69 @@ bool CTransportManager::InitializeEntry(Entry& entry)
if (entry.id == id && entry.epoch == epoch && entry.recoveryAttached)
entry.syncPending = true;
}
+ AddAudioService(entry);
return true;
}
+void CTransportManager::AddAudioService(Entry& entry)
+{
+ std::shared_ptr transport;
+ BackendId id = 0;
+ uint32_t epoch = 0;
+ uint32_t services = 0;
+ bool audioAdded = false;
+ bool audioAbsent = false;
+ uint64_t retryAt = 0;
+ State state = State::CLOSED;
+ {
+ CSRWSharedLock entryLock(entry.lock);
+ transport = entry.transport;
+ id = entry.id;
+ epoch = entry.epoch;
+ services = entry.config.services;
+ audioAdded = entry.audioAdded;
+ audioAbsent = entry.audioAbsent;
+ retryAt = entry.audioRetryAt;
+ state = entry.state;
+ }
+
+ if (state != State::INITIALIZED && state != State::READY)
+ return;
+ if (!(services & TRANSPORT_SERVICE_AUDIO))
+ {
+ CSRWExclusiveLock entryLock(entry.lock);
+ entry.audioAbsent = true;
+ return;
+ }
+ if (!transport || audioAdded || audioAbsent ||
+ GetTickCount64() < retryAt)
+ return;
+
+ IAudioSource * audio = transport->Audio();
+ if (!audio)
+ {
+ CSRWExclusiveLock entryLock(entry.lock);
+ if (entry.transport == transport && entry.epoch == epoch)
+ entry.audioAbsent = true;
+ return;
+ }
+
+ if (m_audio.Bind(id, epoch, *audio))
+ {
+ CSRWExclusiveLock entryLock(entry.lock);
+ if (entry.transport == transport && entry.epoch == epoch)
+ {
+ entry.audioAdded = true;
+ entry.audioRetryAt = 0;
+ }
+ return;
+ }
+
+ CSRWExclusiveLock entryLock(entry.lock);
+ if (entry.transport == transport && entry.epoch == epoch)
+ entry.audioRetryAt = GetTickCount64() + SERVICE_RETRY_DELAY_MS;
+}
+
bool CTransportManager::AddServices(Entry& entry)
{
std::shared_ptr transport;
@@ -665,6 +726,28 @@ bool CTransportManager::AddServices(Entry& entry)
void CTransportManager::HandleServiceFailures()
{
+ SourceKey audioSource;
+ while (m_audio.TakeFailure(audioSource))
+ {
+ Entry * entries[FRAME_MAX_SINKS] = {};
+ const unsigned count = Entries(entries);
+ for (unsigned i = 0; i < count; ++i)
+ {
+ Entry& entry = *entries[i];
+ {
+ CSRWExclusiveLock entryLock(entry.lock);
+ if (entry.id != audioSource.backend ||
+ entry.epoch != audioSource.epoch || !entry.audioAdded)
+ continue;
+ entry.audioAdded = false;
+ entry.audioRetryAt =
+ GetTickCount64() + SERVICE_RETRY_DELAY_MS;
+ }
+ m_audio.Unbind(audioSource.backend, audioSource.epoch);
+ break;
+ }
+ }
+
BackendId frameId = 0;
uint32_t frameEpoch = 0;
while (m_tex.TakeFailure(frameId, frameEpoch))
@@ -840,6 +923,7 @@ void CTransportManager::RemoveServices(Entry& entry)
bool controlAdded = false;
bool inputAdded = false;
bool clipboardAdded = false;
+ bool audioAdded = false;
{
CSRWExclusiveLock entryLock(entry.lock);
id = entry.id;
@@ -849,16 +933,22 @@ void CTransportManager::RemoveServices(Entry& entry)
controlAdded = entry.controlAdded;
inputAdded = entry.inputAdded;
clipboardAdded = entry.clipboardAdded;
+ audioAdded = entry.audioAdded;
entry.frameAdded = false;
entry.frameLegacy = false;
entry.texSink = nullptr;
entry.controlAdded = false;
entry.inputAdded = false;
entry.clipboardAdded = false;
+ entry.audioAdded = false;
+ entry.audioAbsent = false;
+ entry.audioRetryAt = 0;
entry.frameRetryAt = 0;
}
m_input.RevokeInteraction(id, epoch);
+ if (audioAdded)
+ m_audio.Unbind(id, epoch);
m_tex.Drop(id, epoch);
if (inputAdded)
m_input.Unbind(id, epoch);
@@ -905,6 +995,8 @@ void CTransportManager::RetryEntry(Entry& entry, uint64_t now,
entry.inputAbsent = false;
entry.clipboardFailed = false;
entry.clipboardAbsent = false;
+ entry.audioAbsent = false;
+ entry.audioRetryAt = 0;
entry.frameAbsent = false;
entry.serviceRetryAt = 0;
Seq::Inc(entry.epoch);
@@ -1542,6 +1634,7 @@ ITransport::ProcessResult CTransportManager::Process(
continue;
RetryEntry(entry, now, initialized, setup, alignment);
+ AddAudioService(entry);
std::shared_ptr transport;
BackendId id = 0;
@@ -1685,6 +1778,7 @@ void CTransportManager::Stop()
m_input.Stop();
m_clipboard.Stop();
+ m_audio.Stop();
for (unsigned i = count; i > 0; --i)
{
diff --git a/idd/LGIdd/transport/CTransportManager.h b/idd/LGIdd/transport/CTransportManager.h
index 142eafe4..e38de902 100644
--- a/idd/LGIdd/transport/CTransportManager.h
+++ b/idd/LGIdd/transport/CTransportManager.h
@@ -22,6 +22,7 @@
#include "Atomic.h"
#include "CSRWLock.h"
+#include "transport/CAudioHub.h"
#include "transport/CClipboardHub.h"
#include "transport/CControlHub.h"
#include "transport/CFrameHub.h"
@@ -113,6 +114,9 @@ private:
bool clipboardAdded = false;
bool clipboardFailed = false;
bool clipboardAbsent = false;
+ bool audioAdded = false;
+ bool audioAbsent = false;
+ uint64_t audioRetryAt = 0;
bool frameAdded = false;
bool frameLegacy = false;
ITexSink * texSink = nullptr;
@@ -137,6 +141,7 @@ private:
CTexHub m_tex;
CInputHub m_input;
CClipboardHub m_clipboard;
+ CAudioHub m_audio;
CRecoveryHub m_recovery;
Entry * m_primary = nullptr;
bool m_initialized = false;
@@ -165,6 +170,7 @@ private:
OpenResult OpenEntry(Entry& entry);
bool InitializeEntry(Entry& entry);
bool SetupEntry(Entry& entry, size_t alignment);
+ void AddAudioService(Entry& entry);
bool AddServices(Entry& entry);
void HandleServiceFailures();
void RetryEntry(Entry& entry, uint64_t now, bool initialized,
diff --git a/idd/LGIdd/transport/IAudioSource.h b/idd/LGIdd/transport/IAudioSource.h
new file mode 100644
index 00000000..1f5bff2f
--- /dev/null
+++ b/idd/LGIdd/transport/IAudioSource.h
@@ -0,0 +1,175 @@
+/**
+ * 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
+#include
+
+static constexpr uint32_t AUDIO_MAX_CHANNELS = 18;
+static constexpr uint32_t AUDIO_STREAM_MAX_CHANNELS = 8;
+static constexpr uint32_t AUDIO_MAX_FRAMES_PER_PACKET = 2048;
+static constexpr uint32_t AUDIO_MAX_BYTES_PER_SAMPLE = 3;
+static constexpr uint32_t AUDIO_MAX_PCM_BYTES =
+ AUDIO_MAX_FRAMES_PER_PACKET * AUDIO_STREAM_MAX_CHANNELS *
+ AUDIO_MAX_BYTES_PER_SAMPLE;
+static constexpr uint32_t AUDIO_CHANNEL_POSITION_MASK = 0x0003ffffU;
+
+enum AudioCapability : uint32_t
+{
+ AUDIO_CAP_PLAYBACK = 1U << 0,
+ AUDIO_CAP_CAPTURE = 1U << 1,
+ AUDIO_CAP_CLOCK_FEEDBACK = 1U << 2,
+};
+
+enum AudioPacketFlag : uint32_t
+{
+ AUDIO_PACKET_DISCONTINUITY = 1U << 0,
+ AUDIO_PACKET_SILENT = 1U << 1,
+ AUDIO_PACKET_CLOCK_VALID = 1U << 2,
+ AUDIO_PACKET_CLOCK_STABLE = 1U << 3,
+};
+
+enum AudioClockFlag : uint32_t
+{
+ AUDIO_CLOCK_VALID = 1U << 0,
+ AUDIO_CLOCK_STABLE = 1U << 1,
+};
+
+enum class AudioStreamState : uint32_t
+{
+ UNAVAILABLE,
+ STOPPED,
+ ACQUIRED,
+ PAUSED,
+ RUNNING,
+};
+
+enum class AudioResult
+{
+ ACCEPTED,
+ BUSY,
+ UNAVAILABLE,
+ STALE,
+ REJECTED,
+ FAILED,
+};
+
+struct AudioFormat
+{
+ uint32_t sampleRate = 0;
+ uint32_t channelMask = 0;
+ uint16_t channels = 0;
+ uint16_t containerBits = 0;
+ uint16_t validBits = 0;
+ uint16_t blockAlign = 0;
+};
+
+struct AudioStreamStatus
+{
+ uint64_t generation = 0;
+ AudioStreamState state = AudioStreamState::UNAVAILABLE;
+ bool formatValid = false;
+ AudioFormat format;
+};
+
+struct AudioEndpointStatus
+{
+ uint64_t generation = 1;
+ uint32_t capabilities = 0;
+ bool available = false;
+ bool active = false;
+ AudioStreamStatus playback;
+ AudioStreamStatus capture;
+};
+
+struct AudioPacket
+{
+ uint64_t endpointGeneration = 0;
+ uint64_t streamGeneration = 0;
+ uint32_t flags = 0;
+ uint32_t frames = 0;
+ uint64_t position = 0;
+ int64_t timeNs = 0;
+ uint64_t rateQ32 = 0;
+ AudioFormat format;
+};
+
+struct AudioClockFeedback
+{
+ uint64_t endpointGeneration = 0;
+ uint64_t playbackGeneration = 0;
+ uint64_t position = 0;
+ int64_t timeNs = 0;
+ uint64_t targetRateQ32 = 0;
+ uint32_t flags = 0;
+};
+
+class IAudioTarget
+{
+public:
+ virtual ~IAudioTarget() = default;
+
+ // Methods are thread-safe and nonblocking. Packet generations and format
+ // must exactly match the latest AudioState. A packet contains 1..2048
+ // frames and only the declared AudioPacketFlag bits. Non-silent payload
+ // size is frames * blockAlign; a silent packet has size zero and is
+ // expanded to zero PCM by the target. CLOCK_STABLE requires CLOCK_VALID;
+ // position, timeNs, and rateQ32 are meaningful only when CLOCK_VALID.
+
+ // Activation is asynchronous. AudioState reports when the local endpoint
+ // is actually bound. Deactivation restores the driver's local loopback.
+ // A new activation returns BUSY until a preceding deactivation has reached
+ // an inactive AudioState, preventing PCM from crossing owner handoffs.
+ virtual AudioResult SetAudioActive(bool active) = 0;
+
+ // ACCEPTED consumes a copied packet. BUSY consumes nothing, and the source
+ // retries the exact packet after CaptureReceiveReady.
+ virtual AudioResult SendCapture(const AudioPacket& packet,
+ const uint8_t * data, size_t size) = 0;
+ virtual AudioResult SetClockFeedback(
+ const AudioClockFeedback& feedback) = 0;
+
+ // The source calls this after SendPlayback returned BUSY and it can accept
+ // an exact retry of the retained playback packet.
+ virtual void PlaybackReceiveReady() = 0;
+ virtual void AudioFailed() = 0;
+};
+
+class IAudioSource
+{
+public:
+ virtual ~IAudioSource() = default;
+
+ // Methods are thread-safe and nonblocking except Stop. Start attaches the
+ // target. The source must wait for its initial AudioState callback before
+ // making target calls. Stop is a callback-quiescence barrier and leaves no
+ // work which can access target.
+ virtual bool Start(IAudioTarget& target) = 0;
+ virtual void Stop() = 0;
+
+ // All callbacks into a source are serialized. BUSY consumes neither the
+ // packet nor its borrowed payload; the hub retains and retries both after
+ // PlaybackReceiveReady.
+ virtual void AudioState(const AudioEndpointStatus& state) = 0;
+ virtual AudioResult SendPlayback(const AudioPacket& packet,
+ const uint8_t * data, size_t size) = 0;
+ virtual void CaptureReceiveReady() = 0;
+};
diff --git a/idd/LGIdd/transport/ITransport.h b/idd/LGIdd/transport/ITransport.h
index b486db4a..68994e53 100644
--- a/idd/LGIdd/transport/ITransport.h
+++ b/idd/LGIdd/transport/ITransport.h
@@ -30,6 +30,7 @@
#include
class IClipboardSource;
+class IAudioSource;
class IControlSink;
class IFrameSink;
class IInputSource;
@@ -174,7 +175,9 @@ public:
}
virtual DirectFrameBufferMemory GetDirectMemory() const = 0;
- // Component pointers are fixed after Setup and remain valid until Stop.
+ // Audio is fixed after Initialize. The remaining component pointers are
+ // fixed after Setup. All remain valid until Stop.
+ virtual IAudioSource * Audio() { return nullptr; }
virtual IFrameSink * FrameSink() { return nullptr; }
virtual ITexSink * TexSink() { return nullptr; }
virtual IControlSink * Control() { return nullptr; }
diff --git a/idd/LGIdd/transport/TransportConfig.cpp b/idd/LGIdd/transport/TransportConfig.cpp
index bf86d3de..78896321 100644
--- a/idd/LGIdd/transport/TransportConfig.cpp
+++ b/idd/LGIdd/transport/TransportConfig.cpp
@@ -127,6 +127,8 @@ namespace
service = TRANSPORT_SERVICE_INPUT;
else if (Equal(name, L"clipboard"))
service = TRANSPORT_SERVICE_CLIPBOARD;
+ else if (Equal(name, L"audio"))
+ service = TRANSPORT_SERVICE_AUDIO;
else
return false;
diff --git a/idd/LGIdd/transport/TransportConfig.h b/idd/LGIdd/transport/TransportConfig.h
index b0d68847..1ab5ba54 100644
--- a/idd/LGIdd/transport/TransportConfig.h
+++ b/idd/LGIdd/transport/TransportConfig.h
@@ -32,9 +32,10 @@ enum TransportService : uint32_t
TRANSPORT_SERVICE_CONTROL = 1U << 1,
TRANSPORT_SERVICE_INPUT = 1U << 2,
TRANSPORT_SERVICE_CLIPBOARD = 1U << 3,
+ TRANSPORT_SERVICE_AUDIO = 1U << 4,
TRANSPORT_SERVICE_ALL = TRANSPORT_SERVICE_FRAME |
TRANSPORT_SERVICE_CONTROL | TRANSPORT_SERVICE_INPUT |
- TRANSPORT_SERVICE_CLIPBOARD,
+ TRANSPORT_SERVICE_CLIPBOARD | TRANSPORT_SERVICE_AUDIO,
};
struct TransportInstance