[idd] audio: add the mediated device connector

Open the optional audio broker from a passive worker and move bounded
playback and capture packets through a transport-neutral audio hub.

Attach audio sources after transport initialization without making audio
driver availability part of display transport readiness.
This commit is contained in:
Geoffrey McRae
2026-09-02 00:45:47 +10:00
parent 5e07786468
commit 947f03ede6
12 changed files with 2301 additions and 2 deletions

View File

@@ -66,6 +66,8 @@
<ClCompile Include="postprocess\effect\CFormatEffect.cpp" />
<ClCompile Include="postprocess\effect\CHDR16to10Effect.cpp" />
<ClCompile Include="postprocess\effect\CRGB24Effect.cpp" />
<ClCompile Include="audio\CLGAudioDevice.cpp" />
<ClCompile Include="transport\CAudioHub.cpp" />
<ClCompile Include="transport\CClipboardHub.cpp" />
<ClCompile Include="transport\CControlHub.cpp" />
<ClCompile Include="transport\CFrameHub.cpp" />
@@ -127,6 +129,8 @@
<ClInclude Include="postprocess\effect\CFormatEffect.h" />
<ClInclude Include="postprocess\effect\CHDR16to10Effect.h" />
<ClInclude Include="postprocess\effect\CRGB24Effect.h" />
<ClInclude Include="audio\CLGAudioDevice.h" />
<ClInclude Include="transport\CAudioHub.h" />
<ClInclude Include="transport\CClipboardHub.h" />
<ClInclude Include="transport\CControlHub.h" />
<ClInclude Include="transport\CFrameHub.h" />
@@ -139,6 +143,7 @@
<ClInclude Include="transport\FrameProfile.h" />
<ClInclude Include="transport\FrameIn.h" />
<ClInclude Include="transport\IClipboardSource.h" />
<ClInclude Include="transport\IAudioSource.h" />
<ClInclude Include="transport\IControlSink.h" />
<ClInclude Include="transport\IControlTransport.h" />
<ClInclude Include="transport\IFrameSink.h" />

View File

@@ -17,6 +17,9 @@
<Filter Include="Capture">
<UniqueIdentifier>{202B79B2-5CCF-4AD4-B4C5-F5602419683D}</UniqueIdentifier>
</Filter>
<Filter Include="Audio">
<UniqueIdentifier>{86750913-8C21-4F76-8E51-9737D4963E50}</UniqueIdentifier>
</Filter>
<Filter Include="D3D">
<UniqueIdentifier>{4B3C58D3-5F40-49AB-894E-B83EB44E6CF5}</UniqueIdentifier>
</Filter>
@@ -166,6 +169,15 @@
<ClInclude Include="postprocess\effect\CRGB24Effect.h">
<Filter>Post-processing\Effects</Filter>
</ClInclude>
<ClInclude Include="audio\CLGAudioDevice.h">
<Filter>Audio</Filter>
</ClInclude>
<ClInclude Include="transport\CAudioHub.h">
<Filter>Transport</Filter>
</ClInclude>
<ClInclude Include="transport\IAudioSource.h">
<Filter>Transport</Filter>
</ClInclude>
<ClInclude Include="transport\CClipboardHub.h">
<Filter>Transport</Filter>
</ClInclude>
@@ -378,6 +390,12 @@
<ClCompile Include="postprocess\effect\CRGB24Effect.cpp">
<Filter>Post-processing\Effects</Filter>
</ClCompile>
<ClCompile Include="audio\CLGAudioDevice.cpp">
<Filter>Audio</Filter>
</ClCompile>
<ClCompile Include="transport\CAudioHub.cpp">
<Filter>Transport</Filter>
</ClCompile>
<ClCompile Include="transport\CClipboardHub.cpp">
<Filter>Transport</Filter>
</ClCompile>

File diff suppressed because it is too large Load Diff

View File

@@ -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 <memory>
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<CState> 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;
};

View File

@@ -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 <new>
CAudioHub::CAudioHub() :
m_device(new (std::nothrow) CLGAudioDevice())
{
}
CAudioHub::CAudioHub(std::unique_ptr<ILGAudioDevice> 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<CSRWLock&>(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();
}

View File

@@ -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 <memory>
class CAudioHub final : public IAudioTarget,
private ILGAudioDeviceEvents
{
private:
CSRWLock m_lifecycleLock;
CSRWLock m_callbackLock;
CSRWLock m_lock;
std::unique_ptr<ILGAudioDevice> 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<ILGAudioDevice> 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;
};

View File

@@ -26,6 +26,7 @@
#include "ipc/CPipeServer.h"
#include "Seq.h"
#include "transport/IClipboardSource.h"
#include "transport/IAudioSource.h"
#include "transport/ITexStage.h"
#include <Windows.h>
@@ -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<ITransport> 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<ITransport> 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<ITransport> 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)
{

View File

@@ -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,

View File

@@ -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 <stddef.h>
#include <stdint.h>
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;
};

View File

@@ -30,6 +30,7 @@
#include <stdint.h>
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; }

View File

@@ -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;

View File

@@ -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