mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-11 01:31:31 +00:00
[client] audio: add transport provider interface
Some checks failed
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / module (push) Has been cancelled
build / host-linux (push) Has been cancelled
build / host-windows-cross (push) Has been cancelled
build / host-windows-native (push) Has been cancelled
build / idd (push) Has been cancelled
build / obs (clang) (push) Has been cancelled
build / obs (gcc) (push) Has been cancelled
build / docs (push) Has been cancelled
Some checks failed
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / module (push) Has been cancelled
build / host-linux (push) Has been cancelled
build / host-windows-cross (push) Has been cancelled
build / host-windows-native (push) Has been cancelled
build / idd (push) Has been cancelled
build / obs (clang) (push) Has been cancelled
build / obs (gcc) (push) Has been cancelled
build / docs (push) Has been cancelled
Route audio through a provider interface. Connected transports take priority while SPICE remains the fallback. Carry explicit sample formats, rates, channel layouts, and source clocks through playback and recording. Add optional presentation clock feedback for active synchronization.
This commit is contained in:
1485
client/src/audio.c
1485
client/src/audio.c
File diff suppressed because it is too large
Load Diff
@@ -18,32 +18,37 @@
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifndef _H_LG_CLIENT_AUDIO_
|
||||
#define _H_LG_CLIENT_AUDIO_
|
||||
|
||||
#include "interface/audio.h"
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <purespice.h>
|
||||
void lgAudio_init(void);
|
||||
void lgAudio_free(void);
|
||||
|
||||
void audio_init(void);
|
||||
void audio_free(void);
|
||||
void lgAudio_setFallback(const LG_AudioOps * ops, void * opaque);
|
||||
void lgAudio_setTransport(const LG_AudioOps * ops, void * opaque);
|
||||
void lgAudio_dropTransport(void);
|
||||
|
||||
bool audio_supportsPlayback(void);
|
||||
void audio_playbackStart(int channels, int sampleRate, PSAudioFormat format,
|
||||
uint32_t time);
|
||||
void audio_playbackStop(void);
|
||||
void audio_playbackVolume(int channels, const uint16_t volume[]);
|
||||
void audio_playbackMute(bool mute);
|
||||
void audio_playbackData(uint8_t * data, size_t size, uint32_t time);
|
||||
|
||||
bool audio_supportsRecord(void);
|
||||
void audio_recordStart(int channels, int sampleRate, PSAudioFormat format);
|
||||
void audio_recordToggleKeybind(int sc, void * opaque);
|
||||
void audio_recordStop(void);
|
||||
void audio_recordVolume(int channels, const uint16_t volume[]);
|
||||
void audio_recordMute(bool mute);
|
||||
bool lgAudio_supportsPlayback(void);
|
||||
bool lgAudio_supportsRecord(void);
|
||||
void lgAudio_recordToggleKeybind(int sc, void * opaque);
|
||||
|
||||
#else
|
||||
|
||||
static inline void audio_init(void) {}
|
||||
static inline void audio_free(void) {}
|
||||
static inline void lgAudio_init(void) {}
|
||||
static inline void lgAudio_free(void) {}
|
||||
static inline void lgAudio_setFallback(
|
||||
const LG_AudioOps * ops, void * opaque) {}
|
||||
static inline void lgAudio_setTransport(
|
||||
const LG_AudioOps * ops, void * opaque) {}
|
||||
static inline void lgAudio_dropTransport(void) {}
|
||||
static inline bool lgAudio_supportsPlayback(void) { return false; }
|
||||
static inline bool lgAudio_supportsRecord(void) { return false; }
|
||||
static inline void lgAudio_recordToggleKeybind(int sc, void * opaque) {}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
842
client/src/audio_spice.c
Normal file
842
client/src/audio_spice.c
Normal file
@@ -0,0 +1,842 @@
|
||||
/**
|
||||
* 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_spice.h"
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/locking.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS 2000
|
||||
|
||||
typedef struct SpiceAudioStream
|
||||
{
|
||||
bool active;
|
||||
uint32_t generation;
|
||||
LG_AudioFormat format;
|
||||
|
||||
bool volumeValid;
|
||||
uint8_t volumeChannels;
|
||||
uint16_t volume[LG_AUDIO_MAX_CHANNELS];
|
||||
bool muteValid;
|
||||
bool mute;
|
||||
}
|
||||
SpiceAudioStream;
|
||||
|
||||
typedef struct SpiceAudioEventTarget
|
||||
{
|
||||
const LG_AudioEventOps * events;
|
||||
void * opaque;
|
||||
uint32_t generation;
|
||||
}
|
||||
SpiceAudioEventTarget;
|
||||
|
||||
static struct
|
||||
{
|
||||
LG_RWLock lock;
|
||||
|
||||
bool available;
|
||||
uint32_t statusGeneration;
|
||||
LG_AudioStatusFn statusCallback;
|
||||
void * statusOpaque;
|
||||
atomic_uint statusInFlight;
|
||||
|
||||
const LG_AudioEventOps * events;
|
||||
void * eventOpaque;
|
||||
uint32_t eventGeneration;
|
||||
atomic_uint inFlight;
|
||||
|
||||
SpiceAudioStream playback;
|
||||
SpiceAudioStream record;
|
||||
|
||||
bool playbackClockValid;
|
||||
uint32_t playbackMediaTime;
|
||||
int64_t playbackTime;
|
||||
uint64_t playbackPosition;
|
||||
LG_AudioClock playbackClock;
|
||||
}
|
||||
l_spice =
|
||||
{
|
||||
.lock =
|
||||
{
|
||||
.readers = ATOMIC_VAR_INIT(0),
|
||||
.writers = ATOMIC_VAR_INIT(0),
|
||||
.writer = ATOMIC_FLAG_INIT,
|
||||
},
|
||||
.statusInFlight = ATOMIC_VAR_INIT(0),
|
||||
.inFlight = ATOMIC_VAR_INIT(0),
|
||||
};
|
||||
|
||||
static _Thread_local unsigned int l_eventDepth;
|
||||
static _Thread_local unsigned int l_statusDepth;
|
||||
|
||||
static uint32_t nextGeneration(uint32_t generation)
|
||||
{
|
||||
if (++generation == 0)
|
||||
++generation;
|
||||
return generation;
|
||||
}
|
||||
|
||||
/* l_spice.lock must be held exclusively while admitting a callback so detach
|
||||
* cannot invalidate the target between the snapshot and in-flight increment. */
|
||||
static bool beginEventNL(SpiceAudioEventTarget * target)
|
||||
{
|
||||
if (!l_spice.events)
|
||||
return false;
|
||||
|
||||
target->events = l_spice.events;
|
||||
target->opaque = l_spice.eventOpaque;
|
||||
target->generation = l_spice.eventGeneration;
|
||||
atomic_fetch_add_explicit(&l_spice.inFlight, 1, memory_order_relaxed);
|
||||
++l_eventDepth;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool playbackEventCurrent(const SpiceAudioEventTarget * target,
|
||||
uint32_t generation, bool active)
|
||||
{
|
||||
LG_LOCK_SHARED(l_spice.lock);
|
||||
const bool result =
|
||||
target->events == l_spice.events &&
|
||||
target->opaque == l_spice.eventOpaque &&
|
||||
target->generation == l_spice.eventGeneration &&
|
||||
l_spice.playback.generation == generation &&
|
||||
l_spice.playback.active == active;
|
||||
LG_UNLOCK_SHARED(l_spice.lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool recordEventCurrent(const SpiceAudioEventTarget * target,
|
||||
uint32_t generation, bool active)
|
||||
{
|
||||
LG_LOCK_SHARED(l_spice.lock);
|
||||
const bool result =
|
||||
target->events == l_spice.events &&
|
||||
target->opaque == l_spice.eventOpaque &&
|
||||
target->generation == l_spice.eventGeneration &&
|
||||
l_spice.record.generation == generation &&
|
||||
l_spice.record.active == active;
|
||||
LG_UNLOCK_SHARED(l_spice.lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void endEvent(void)
|
||||
{
|
||||
--l_eventDepth;
|
||||
atomic_fetch_sub_explicit(&l_spice.inFlight, 1, memory_order_release);
|
||||
}
|
||||
|
||||
static bool sampleFormat(PSAudioFormat source,
|
||||
LG_AudioSampleFormat * format)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case PS_AUDIO_FMT_S16:
|
||||
*format = LG_AUDIO_FMT_S16_LE;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void channelLayout(LG_AudioFormat * format)
|
||||
{
|
||||
memset(format->channels, 0, sizeof(format->channels));
|
||||
|
||||
switch (format->channelCount)
|
||||
{
|
||||
case 1:
|
||||
format->channels[0] = LG_AUDIO_CH_MONO;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
||||
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
||||
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
||||
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
||||
break;
|
||||
|
||||
case 4:
|
||||
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
||||
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
||||
format->channels[2] = LG_AUDIO_CH_REAR_LEFT;
|
||||
format->channels[3] = LG_AUDIO_CH_REAR_RIGHT;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
||||
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
||||
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
||||
format->channels[3] = LG_AUDIO_CH_REAR_LEFT;
|
||||
format->channels[4] = LG_AUDIO_CH_REAR_RIGHT;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
||||
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
||||
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
||||
format->channels[3] = LG_AUDIO_CH_LFE;
|
||||
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
|
||||
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
|
||||
break;
|
||||
|
||||
case 7:
|
||||
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
||||
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
||||
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
||||
format->channels[3] = LG_AUDIO_CH_LFE;
|
||||
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
|
||||
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
|
||||
format->channels[6] = LG_AUDIO_CH_REAR_CENTER;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
format->channels[0] = LG_AUDIO_CH_FRONT_LEFT;
|
||||
format->channels[1] = LG_AUDIO_CH_FRONT_RIGHT;
|
||||
format->channels[2] = LG_AUDIO_CH_FRONT_CENTER;
|
||||
format->channels[3] = LG_AUDIO_CH_LFE;
|
||||
format->channels[4] = LG_AUDIO_CH_REAR_LEFT;
|
||||
format->channels[5] = LG_AUDIO_CH_REAR_RIGHT;
|
||||
format->channels[6] = LG_AUDIO_CH_SIDE_LEFT;
|
||||
format->channels[7] = LG_AUDIO_CH_SIDE_RIGHT;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool makeFormat(int channels, int sampleRate, PSAudioFormat source,
|
||||
LG_AudioFormat * format)
|
||||
{
|
||||
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || sampleRate < 1 ||
|
||||
!sampleFormat(source, &format->sampleFormat))
|
||||
return false;
|
||||
|
||||
format->sampleRate = sampleRate;
|
||||
format->channelCount = channels;
|
||||
channelLayout(format);
|
||||
return true;
|
||||
}
|
||||
|
||||
static size_t sampleSize(LG_AudioSampleFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case LG_AUDIO_FMT_U8: return 1;
|
||||
case LG_AUDIO_FMT_S16_LE: return 2;
|
||||
case LG_AUDIO_FMT_S24_LE: return 3;
|
||||
case LG_AUDIO_FMT_S32_LE: return 4;
|
||||
case LG_AUDIO_FMT_F32_LE: return 4;
|
||||
case LG_AUDIO_FMT_F64_LE: return 8;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static LG_AudioClock playbackClockNL(uint32_t time)
|
||||
{
|
||||
bool discontinuity = false;
|
||||
|
||||
if (!l_spice.playbackClockValid)
|
||||
{
|
||||
l_spice.playbackClockValid = true;
|
||||
l_spice.playbackMediaTime = time;
|
||||
l_spice.playbackTime = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
const int32_t delta = (int32_t)(time - l_spice.playbackMediaTime);
|
||||
l_spice.playbackMediaTime = time;
|
||||
if (delta < 0 || delta > SPICE_AUDIO_TIMESTAMP_DISCONTINUITY_MS)
|
||||
{
|
||||
l_spice.playbackTime = 0;
|
||||
discontinuity = true;
|
||||
}
|
||||
else
|
||||
l_spice.playbackTime += (int64_t)delta * 1000000;
|
||||
}
|
||||
|
||||
l_spice.playbackClock = (LG_AudioClock)
|
||||
{
|
||||
.position = l_spice.playbackPosition,
|
||||
.time = l_spice.playbackTime,
|
||||
.rate = 0.0,
|
||||
.stable = !discontinuity,
|
||||
.discontinuity = discontinuity,
|
||||
};
|
||||
return l_spice.playbackClock;
|
||||
}
|
||||
|
||||
static void spiceSetStatusListener(void * opaque,
|
||||
LG_AudioStatusFn callback, void * callbackOpaque)
|
||||
{
|
||||
LG_AudioStatus status;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
l_spice.statusCallback = callback;
|
||||
l_spice.statusOpaque = callbackOpaque;
|
||||
status = (LG_AudioStatus)
|
||||
{
|
||||
.available = l_spice.available,
|
||||
.generation = l_spice.statusGeneration,
|
||||
};
|
||||
if (callback)
|
||||
{
|
||||
atomic_fetch_add_explicit(
|
||||
&l_spice.statusInFlight, 1, memory_order_relaxed);
|
||||
++l_statusDepth;
|
||||
}
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (callback)
|
||||
{
|
||||
callback(callbackOpaque, &status);
|
||||
--l_statusDepth;
|
||||
atomic_fetch_sub_explicit(
|
||||
&l_spice.statusInFlight, 1, memory_order_release);
|
||||
}
|
||||
else
|
||||
while (atomic_load_explicit(
|
||||
&l_spice.statusInFlight, memory_order_acquire) > l_statusDepth)
|
||||
;
|
||||
}
|
||||
|
||||
static bool spiceAttach(void * opaque, const LG_AudioEventOps * events,
|
||||
void * eventOpaque)
|
||||
{
|
||||
if (!events)
|
||||
return false;
|
||||
|
||||
SpiceAudioEventTarget target;
|
||||
SpiceAudioStream playback;
|
||||
SpiceAudioStream record;
|
||||
LG_AudioClock playbackClock;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return false;
|
||||
}
|
||||
|
||||
l_spice.events = events;
|
||||
l_spice.eventOpaque = eventOpaque;
|
||||
l_spice.eventGeneration =
|
||||
nextGeneration(l_spice.eventGeneration);
|
||||
playback = l_spice.playback;
|
||||
record = l_spice.record;
|
||||
playbackClock = l_spice.playbackClock;
|
||||
dispatch = beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return true;
|
||||
|
||||
if (playback.active)
|
||||
{
|
||||
if (playbackEventCurrent(&target, playback.generation, true) &&
|
||||
target.events->playbackStart)
|
||||
{
|
||||
target.events->playbackStart(target.opaque,
|
||||
playback.generation, &playback.format, &playbackClock);
|
||||
if (playback.volumeValid &&
|
||||
playbackEventCurrent(&target, playback.generation, true) &&
|
||||
target.events->playbackVolume)
|
||||
target.events->playbackVolume(target.opaque,
|
||||
playback.generation,
|
||||
playback.volumeChannels, playback.volume);
|
||||
if (playback.muteValid &&
|
||||
playbackEventCurrent(&target, playback.generation, true) &&
|
||||
target.events->playbackMute)
|
||||
target.events->playbackMute(target.opaque,
|
||||
playback.generation, playback.mute);
|
||||
}
|
||||
}
|
||||
|
||||
if (record.active)
|
||||
{
|
||||
if (recordEventCurrent(&target, record.generation, true) &&
|
||||
target.events->recordStart)
|
||||
{
|
||||
target.events->recordStart(target.opaque,
|
||||
record.generation, &record.format);
|
||||
if (record.volumeValid &&
|
||||
recordEventCurrent(&target, record.generation, true) &&
|
||||
target.events->recordVolume)
|
||||
target.events->recordVolume(target.opaque, record.generation,
|
||||
record.volumeChannels, record.volume);
|
||||
if (record.muteValid &&
|
||||
recordEventCurrent(&target, record.generation, true) &&
|
||||
target.events->recordMute)
|
||||
target.events->recordMute(target.opaque,
|
||||
record.generation, record.mute);
|
||||
}
|
||||
}
|
||||
|
||||
endEvent();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void spiceDetach(void * opaque)
|
||||
{
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
l_spice.events = NULL;
|
||||
l_spice.eventOpaque = NULL;
|
||||
l_spice.eventGeneration =
|
||||
nextGeneration(l_spice.eventGeneration);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
while (atomic_load_explicit(
|
||||
&l_spice.inFlight, memory_order_acquire) > l_eventDepth)
|
||||
;
|
||||
}
|
||||
|
||||
static bool spiceRecordData(void * opaque, uint32_t generation,
|
||||
const void * data, size_t frames, const LG_AudioClock * sourceClock)
|
||||
{
|
||||
size_t size = 0;
|
||||
bool valid = false;
|
||||
|
||||
LG_LOCK_SHARED(l_spice.lock);
|
||||
if (l_spice.available && l_spice.events && l_spice.record.active &&
|
||||
generation == l_spice.record.generation)
|
||||
{
|
||||
const size_t bytesPerSample =
|
||||
sampleSize(l_spice.record.format.sampleFormat);
|
||||
const size_t channels = l_spice.record.format.channelCount;
|
||||
if (bytesPerSample && frames <= SIZE_MAX / bytesPerSample / channels &&
|
||||
(frames == 0 || data))
|
||||
{
|
||||
size = frames * bytesPerSample * channels;
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
LG_UNLOCK_SHARED(l_spice.lock);
|
||||
|
||||
return valid && purespice_writeAudio((void *)data, size, 0);
|
||||
}
|
||||
|
||||
const LG_AudioOps LGA_Spice =
|
||||
{
|
||||
.name = "SPICE",
|
||||
.setStatusListener = spiceSetStatusListener,
|
||||
.attach = spiceAttach,
|
||||
.detach = spiceDetach,
|
||||
.recordData = spiceRecordData,
|
||||
.clockFeedback = NULL,
|
||||
};
|
||||
|
||||
void lgaSpice_setAvailable(bool available)
|
||||
{
|
||||
LG_AudioStatusFn callback;
|
||||
void * callbackOpaque;
|
||||
LG_AudioStatus status;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (l_spice.available == available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
l_spice.available = available;
|
||||
l_spice.statusGeneration =
|
||||
nextGeneration(l_spice.statusGeneration);
|
||||
if (!available)
|
||||
{
|
||||
l_spice.playback.active = false;
|
||||
l_spice.record.active = false;
|
||||
l_spice.playbackClockValid = false;
|
||||
l_spice.playbackPosition = 0;
|
||||
l_spice.playbackClock = (LG_AudioClock) { 0 };
|
||||
l_spice.playback.volumeValid = false;
|
||||
l_spice.playback.muteValid = false;
|
||||
l_spice.record.volumeValid = false;
|
||||
l_spice.record.muteValid = false;
|
||||
}
|
||||
|
||||
callback = l_spice.statusCallback;
|
||||
callbackOpaque = l_spice.statusOpaque;
|
||||
status = (LG_AudioStatus)
|
||||
{
|
||||
.available = available,
|
||||
.generation = l_spice.statusGeneration,
|
||||
};
|
||||
if (callback)
|
||||
{
|
||||
atomic_fetch_add_explicit(
|
||||
&l_spice.statusInFlight, 1, memory_order_relaxed);
|
||||
++l_statusDepth;
|
||||
}
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (callback)
|
||||
{
|
||||
callback(callbackOpaque, &status);
|
||||
--l_statusDepth;
|
||||
atomic_fetch_sub_explicit(
|
||||
&l_spice.statusInFlight, 1, memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
void lgaSpice_playbackStart(int channels, int sampleRate,
|
||||
PSAudioFormat sourceFormat, uint32_t time)
|
||||
{
|
||||
LG_AudioFormat format;
|
||||
if (!makeFormat(channels, sampleRate, sourceFormat, &format))
|
||||
{
|
||||
DEBUG_ERROR("Invalid SPICE playback format: %d channels, %d Hz, %d",
|
||||
channels, sampleRate, sourceFormat);
|
||||
return;
|
||||
}
|
||||
|
||||
SpiceAudioEventTarget target;
|
||||
SpiceAudioStream playback;
|
||||
LG_AudioClock clock;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
l_spice.playback.active = true;
|
||||
l_spice.playback.generation =
|
||||
nextGeneration(l_spice.playback.generation);
|
||||
l_spice.playback.format = format;
|
||||
l_spice.playbackClockValid = false;
|
||||
l_spice.playbackPosition = 0;
|
||||
clock = playbackClockNL(time);
|
||||
clock.stable = false;
|
||||
clock.discontinuity = true;
|
||||
l_spice.playbackClock = clock;
|
||||
playback = l_spice.playback;
|
||||
dispatch = beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (playbackEventCurrent(&target, playback.generation, true) &&
|
||||
target.events->playbackStart)
|
||||
{
|
||||
target.events->playbackStart(target.opaque,
|
||||
playback.generation, &playback.format, &clock);
|
||||
if (playback.volumeValid &&
|
||||
playbackEventCurrent(&target, playback.generation, true) &&
|
||||
target.events->playbackVolume)
|
||||
target.events->playbackVolume(target.opaque,
|
||||
playback.generation, playback.volumeChannels, playback.volume);
|
||||
if (playback.muteValid &&
|
||||
playbackEventCurrent(&target, playback.generation, true) &&
|
||||
target.events->playbackMute)
|
||||
target.events->playbackMute(target.opaque,
|
||||
playback.generation, playback.mute);
|
||||
}
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_playbackStop(void)
|
||||
{
|
||||
SpiceAudioEventTarget target;
|
||||
uint32_t generation;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.playback.active)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
generation = l_spice.playback.generation;
|
||||
l_spice.playback.active = false;
|
||||
l_spice.playbackClockValid = false;
|
||||
dispatch = beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (target.events->playbackStop &&
|
||||
playbackEventCurrent(&target, generation, false))
|
||||
target.events->playbackStop(target.opaque, generation);
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_playbackVolume(int channels, const uint16_t volume[])
|
||||
{
|
||||
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume)
|
||||
return;
|
||||
|
||||
SpiceAudioEventTarget target;
|
||||
uint32_t generation;
|
||||
uint16_t snapshot[LG_AUDIO_MAX_CHANNELS];
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(l_spice.playback.volume, volume,
|
||||
(size_t)channels * sizeof(*volume));
|
||||
l_spice.playback.volumeChannels = channels;
|
||||
l_spice.playback.volumeValid = true;
|
||||
generation = l_spice.playback.generation;
|
||||
memcpy(snapshot, volume, (size_t)channels * sizeof(*volume));
|
||||
dispatch = l_spice.playback.active && beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (target.events->playbackVolume &&
|
||||
playbackEventCurrent(&target, generation, true))
|
||||
target.events->playbackVolume(
|
||||
target.opaque, generation, channels, snapshot);
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_playbackMute(bool mute)
|
||||
{
|
||||
SpiceAudioEventTarget target;
|
||||
uint32_t generation;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
l_spice.playback.mute = mute;
|
||||
l_spice.playback.muteValid = true;
|
||||
generation = l_spice.playback.generation;
|
||||
dispatch = l_spice.playback.active && beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (target.events->playbackMute &&
|
||||
playbackEventCurrent(&target, generation, true))
|
||||
target.events->playbackMute(target.opaque, generation, mute);
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_playbackData(uint8_t * data, size_t size, uint32_t time)
|
||||
{
|
||||
SpiceAudioEventTarget target;
|
||||
uint32_t generation;
|
||||
size_t frames;
|
||||
LG_AudioClock clock;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available || !l_spice.playback.active)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t bytesPerSample =
|
||||
sampleSize(l_spice.playback.format.sampleFormat);
|
||||
const size_t stride =
|
||||
bytesPerSample * l_spice.playback.format.channelCount;
|
||||
if (!stride || !size || !data || size % stride)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
DEBUG_ERROR("Invalid SPICE playback packet size: %zu", size);
|
||||
return;
|
||||
}
|
||||
|
||||
frames = size / stride;
|
||||
generation = l_spice.playback.generation;
|
||||
clock = playbackClockNL(time);
|
||||
l_spice.playbackPosition += frames;
|
||||
dispatch = beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (target.events->playbackData &&
|
||||
playbackEventCurrent(&target, generation, true))
|
||||
target.events->playbackData(target.opaque, generation,
|
||||
data, frames, &clock);
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_recordStart(int channels, int sampleRate,
|
||||
PSAudioFormat sourceFormat)
|
||||
{
|
||||
LG_AudioFormat format;
|
||||
if (!makeFormat(channels, sampleRate, sourceFormat, &format))
|
||||
{
|
||||
DEBUG_ERROR("Invalid SPICE record format: %d channels, %d Hz, %d",
|
||||
channels, sampleRate, sourceFormat);
|
||||
return;
|
||||
}
|
||||
|
||||
SpiceAudioEventTarget target;
|
||||
SpiceAudioStream record;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
l_spice.record.active = true;
|
||||
l_spice.record.generation = nextGeneration(l_spice.record.generation);
|
||||
l_spice.record.format = format;
|
||||
record = l_spice.record;
|
||||
dispatch = beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (recordEventCurrent(&target, record.generation, true) &&
|
||||
target.events->recordStart)
|
||||
{
|
||||
target.events->recordStart(
|
||||
target.opaque, record.generation, &record.format);
|
||||
if (record.volumeValid &&
|
||||
recordEventCurrent(&target, record.generation, true) &&
|
||||
target.events->recordVolume)
|
||||
target.events->recordVolume(target.opaque, record.generation,
|
||||
record.volumeChannels, record.volume);
|
||||
if (record.muteValid &&
|
||||
recordEventCurrent(&target, record.generation, true) &&
|
||||
target.events->recordMute)
|
||||
target.events->recordMute(
|
||||
target.opaque, record.generation, record.mute);
|
||||
}
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_recordStop(void)
|
||||
{
|
||||
SpiceAudioEventTarget target;
|
||||
uint32_t generation;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.record.active)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
generation = l_spice.record.generation;
|
||||
l_spice.record.active = false;
|
||||
dispatch = beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (target.events->recordStop &&
|
||||
recordEventCurrent(&target, generation, false))
|
||||
target.events->recordStop(target.opaque, generation);
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_recordVolume(int channels, const uint16_t volume[])
|
||||
{
|
||||
if (channels < 1 || channels > LG_AUDIO_MAX_CHANNELS || !volume)
|
||||
return;
|
||||
|
||||
SpiceAudioEventTarget target;
|
||||
uint32_t generation;
|
||||
uint16_t snapshot[LG_AUDIO_MAX_CHANNELS];
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(l_spice.record.volume, volume,
|
||||
(size_t)channels * sizeof(*volume));
|
||||
l_spice.record.volumeChannels = channels;
|
||||
l_spice.record.volumeValid = true;
|
||||
generation = l_spice.record.generation;
|
||||
memcpy(snapshot, volume, (size_t)channels * sizeof(*volume));
|
||||
dispatch = l_spice.record.active && beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (target.events->recordVolume &&
|
||||
recordEventCurrent(&target, generation, true))
|
||||
target.events->recordVolume(
|
||||
target.opaque, generation, channels, snapshot);
|
||||
endEvent();
|
||||
}
|
||||
|
||||
void lgaSpice_recordMute(bool mute)
|
||||
{
|
||||
SpiceAudioEventTarget target;
|
||||
uint32_t generation;
|
||||
bool dispatch;
|
||||
|
||||
LG_LOCK_EXCLUSIVE(l_spice.lock);
|
||||
if (!l_spice.available)
|
||||
{
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
return;
|
||||
}
|
||||
|
||||
l_spice.record.mute = mute;
|
||||
l_spice.record.muteValid = true;
|
||||
generation = l_spice.record.generation;
|
||||
dispatch = l_spice.record.active && beginEventNL(&target);
|
||||
LG_UNLOCK_EXCLUSIVE(l_spice.lock);
|
||||
|
||||
if (!dispatch)
|
||||
return;
|
||||
|
||||
if (target.events->recordMute &&
|
||||
recordEventCurrent(&target, generation, true))
|
||||
target.events->recordMute(target.opaque, generation, mute);
|
||||
endEvent();
|
||||
}
|
||||
45
client/src/audio_spice.h
Normal file
45
client/src/audio_spice.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef _H_LG_CLIENT_AUDIO_SPICE_
|
||||
#define _H_LG_CLIENT_AUDIO_SPICE_
|
||||
|
||||
#include "interface/audio.h"
|
||||
|
||||
#include <purespice.h>
|
||||
|
||||
extern const LG_AudioOps LGA_Spice;
|
||||
|
||||
void lgaSpice_setAvailable(bool available);
|
||||
|
||||
void lgaSpice_playbackStart(int channels, int sampleRate,
|
||||
PSAudioFormat format, uint32_t time);
|
||||
void lgaSpice_playbackStop(void);
|
||||
void lgaSpice_playbackVolume(int channels, const uint16_t volume[]);
|
||||
void lgaSpice_playbackMute(bool mute);
|
||||
void lgaSpice_playbackData(uint8_t * data, size_t size, uint32_t time);
|
||||
|
||||
void lgaSpice_recordStart(int channels, int sampleRate,
|
||||
PSAudioFormat format);
|
||||
void lgaSpice_recordStop(void);
|
||||
void lgaSpice_recordVolume(int channels, const uint16_t volume[]);
|
||||
void lgaSpice_recordMute(bool mute);
|
||||
|
||||
#endif
|
||||
@@ -207,9 +207,9 @@ void keybind_inputRegister(void)
|
||||
"Send RWin to the guest");
|
||||
|
||||
#if ENABLE_AUDIO
|
||||
if (audio_supportsRecord())
|
||||
if (lgAudio_supportsRecord())
|
||||
{
|
||||
app_registerKeybind(KEY_E, audio_recordToggleKeybind, NULL,
|
||||
app_registerKeybind(KEY_E, lgAudio_recordToggleKeybind, NULL,
|
||||
"Toggle audio recording");
|
||||
app_registerKeybind(KEY_C, bind_toggleMicDefault, NULL,
|
||||
"Cycle audio recording default");
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
#include "core.h"
|
||||
#include "app.h"
|
||||
#include "audio.h"
|
||||
#if ENABLE_AUDIO
|
||||
#include "audio_spice.h"
|
||||
#endif
|
||||
#include "keybind.h"
|
||||
#include "clipboard.h"
|
||||
#include "kb.h"
|
||||
@@ -1025,9 +1028,15 @@ static int renderThread(void * unused)
|
||||
|
||||
if (g_state.transport &&
|
||||
g_state.transportOps->sessionValid(g_state.transport))
|
||||
{
|
||||
lgInput_setTransport(NULL, NULL);
|
||||
lgAudio_setTransport(NULL, NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
lgInput_dropTransport();
|
||||
lgAudio_dropTransport();
|
||||
}
|
||||
|
||||
core_stopCursorThread();
|
||||
core_stopFrameThread();
|
||||
@@ -1075,7 +1084,10 @@ int main_cursorThread(void * unused)
|
||||
}
|
||||
|
||||
if (status == LG_TRANSPORT_DISCONNECTED)
|
||||
{
|
||||
lgInput_dropTransport();
|
||||
lgAudio_dropTransport();
|
||||
}
|
||||
app_setState(status == LG_TRANSPORT_DISCONNECTED ?
|
||||
APP_STATE_RESTART : APP_STATE_SHUTDOWN);
|
||||
if (status != LG_TRANSPORT_DISCONNECTED)
|
||||
@@ -1216,6 +1228,7 @@ int main_frameThread(void * unused)
|
||||
if (status == LG_TRANSPORT_DISCONNECTED)
|
||||
{
|
||||
lgInput_dropTransport();
|
||||
lgAudio_dropTransport();
|
||||
app_setState(APP_STATE_RESTART);
|
||||
}
|
||||
else if (status == LG_TRANSPORT_END)
|
||||
@@ -1503,6 +1516,13 @@ void spiceReady(void)
|
||||
atomic_store_explicit(&g_state.spiceReady, true, memory_order_release);
|
||||
if (g_params.useSpiceInput)
|
||||
lgInput_setFallback(&LGI_Spice, NULL);
|
||||
#if ENABLE_AUDIO
|
||||
if (g_params.useSpiceAudio)
|
||||
{
|
||||
lgaSpice_setAvailable(true);
|
||||
lgAudio_setFallback(&LGA_Spice, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (atomic_load_explicit(&g_state.spiceDisplayRequested,
|
||||
memory_order_acquire))
|
||||
@@ -1687,9 +1707,6 @@ static void spice_setCursorState(bool visible, int x, int y)
|
||||
|
||||
int spiceThread(void * arg)
|
||||
{
|
||||
if (g_params.useSpiceAudio)
|
||||
audio_init();
|
||||
|
||||
const struct PSConfig config =
|
||||
{
|
||||
.host = g_params.spiceHost,
|
||||
@@ -1730,22 +1747,22 @@ int spiceThread(void * arg)
|
||||
#if ENABLE_AUDIO
|
||||
.playback =
|
||||
{
|
||||
.enable = audio_supportsPlayback(),
|
||||
.enable = g_params.useSpiceAudio && lgAudio_supportsPlayback(),
|
||||
.autoConnect = true,
|
||||
.start = audio_playbackStart,
|
||||
.volume = audio_playbackVolume,
|
||||
.mute = audio_playbackMute,
|
||||
.stop = audio_playbackStop,
|
||||
.data = audio_playbackData
|
||||
.start = lgaSpice_playbackStart,
|
||||
.volume = lgaSpice_playbackVolume,
|
||||
.mute = lgaSpice_playbackMute,
|
||||
.stop = lgaSpice_playbackStop,
|
||||
.data = lgaSpice_playbackData
|
||||
},
|
||||
.record =
|
||||
{
|
||||
.enable = audio_supportsRecord(),
|
||||
.enable = g_params.useSpiceAudio && lgAudio_supportsRecord(),
|
||||
.autoConnect = true,
|
||||
.start = audio_recordStart,
|
||||
.volume = audio_recordVolume,
|
||||
.mute = audio_recordMute,
|
||||
.stop = audio_recordStop
|
||||
.start = lgaSpice_recordStart,
|
||||
.volume = lgaSpice_recordVolume,
|
||||
.mute = lgaSpice_recordMute,
|
||||
.stop = lgaSpice_recordStop
|
||||
}
|
||||
#endif
|
||||
};
|
||||
@@ -1770,12 +1787,19 @@ int spiceThread(void * arg)
|
||||
}
|
||||
|
||||
lgInput_setFallback(NULL, NULL);
|
||||
lgAudio_setFallback(NULL, NULL);
|
||||
#if ENABLE_AUDIO
|
||||
lgaSpice_setAvailable(false);
|
||||
#endif
|
||||
purespice_disconnect();
|
||||
|
||||
end:
|
||||
|
||||
lgInput_setFallback(NULL, NULL);
|
||||
audio_free();
|
||||
lgAudio_setFallback(NULL, NULL);
|
||||
#if ENABLE_AUDIO
|
||||
lgaSpice_setAvailable(false);
|
||||
#endif
|
||||
|
||||
// if the connection was disconnected intentionally we don't want to shutdown
|
||||
// so that the user can see the message box and take action
|
||||
@@ -1886,6 +1910,7 @@ static int lg_run(void)
|
||||
LG_LOCK_INIT(l_cursorRepaint.lock);
|
||||
frameTimingInit();
|
||||
lgInput_init();
|
||||
lgAudio_init();
|
||||
|
||||
#ifdef ENABLE_TESTS
|
||||
memset(&l_testCapture, 0, sizeof(l_testCapture));
|
||||
@@ -2356,6 +2381,11 @@ restart:
|
||||
g_state.transportOps->getInputOps(g_state.transport, &inputOpaque) : NULL;
|
||||
lgInput_setTransport(inputOps, inputOpaque);
|
||||
|
||||
void * audioOpaque = NULL;
|
||||
const LG_AudioOps * audioOps = g_state.transportOps->getAudioOps ?
|
||||
g_state.transportOps->getAudioOps(g_state.transport, &audioOpaque) : NULL;
|
||||
lgAudio_setTransport(audioOps, audioOpaque);
|
||||
|
||||
if (inputOps || lgInput_available())
|
||||
keybind_inputRegister();
|
||||
checkUUID();
|
||||
@@ -2373,6 +2403,7 @@ restart:
|
||||
if (unlikely(!g_state.transportOps->sessionValid(g_state.transport)))
|
||||
{
|
||||
lgInput_dropTransport();
|
||||
lgAudio_dropTransport();
|
||||
atomic_store_explicit(
|
||||
&g_state.lgHostConnected, false, memory_order_release);
|
||||
DEBUG_INFO("Waiting for the host to restart...");
|
||||
@@ -2394,6 +2425,7 @@ restart:
|
||||
core_stopFrameThread();
|
||||
core_stopCursorThread();
|
||||
lgInput_dropTransport();
|
||||
lgAudio_dropTransport();
|
||||
g_state.transportOps->disconnect(g_state.transport);
|
||||
|
||||
app_setState(APP_STATE_RUNNING);
|
||||
@@ -2440,10 +2472,16 @@ static void lg_shutdown(void)
|
||||
if (g_state.transportOps)
|
||||
{
|
||||
lgInput_dropTransport();
|
||||
if (g_state.transport &&
|
||||
g_state.transportOps->sessionValid(g_state.transport))
|
||||
lgAudio_setTransport(NULL, NULL);
|
||||
else
|
||||
lgAudio_dropTransport();
|
||||
g_state.transportOps->destroy(&g_state.transport);
|
||||
}
|
||||
|
||||
lgInput_free();
|
||||
lgAudio_free();
|
||||
|
||||
if (g_state.frameEvent)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user