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