[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

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:
Geoffrey McRae
2026-08-09 16:37:39 +10:00
parent 40d6342d46
commit 0da4bfa8dd
12 changed files with 2441 additions and 485 deletions

View File

@@ -188,7 +188,10 @@ if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
endif()
if(ENABLE_AUDIO)
list(APPEND SOURCES src/audio.c)
list(APPEND SOURCES
src/audio.c
src/audio_spice.c
)
endif()
add_subdirectory("${PROJECT_TOP}/resources" "${CMAKE_BINARY_DIR}/resources")

View File

@@ -30,6 +30,7 @@
#include <semaphore.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include "common/debug.h"
#include "common/stringutils.h"
@@ -67,8 +68,7 @@ struct PipeWire
enum pw_stream_state connectionState;
bool resamplerEnabled;
int channels;
int sampleRate;
LG_AudioFormat format;
int stride;
LG_AudioPullFn pullFn;
int maxPeriodFrames;
@@ -82,8 +82,7 @@ struct PipeWire
{
struct pw_stream * stream;
int channels;
int sampleRate;
LG_AudioFormat format;
int stride;
LG_AudioPushFn pushFn;
@@ -105,6 +104,94 @@ struct PipeWire
static struct PipeWire pw = {0};
static bool pipewire_audioFormatEqual(const LG_AudioFormat * a,
const LG_AudioFormat * b)
{
return
a->sampleFormat == b->sampleFormat &&
a->sampleRate == b->sampleRate &&
a->channelCount == b->channelCount &&
memcmp(a->channels, b->channels,
a->channelCount * sizeof(*a->channels)) == 0;
}
static enum spa_audio_channel pipewire_channel(
LG_AudioChannel channel, uint8_t index)
{
switch (channel)
{
case LG_AUDIO_CH_UNKNOWN:
return (enum spa_audio_channel)(SPA_AUDIO_CHANNEL_AUX0 + index);
case LG_AUDIO_CH_MONO : return SPA_AUDIO_CHANNEL_MONO;
case LG_AUDIO_CH_FRONT_LEFT : return SPA_AUDIO_CHANNEL_FL;
case LG_AUDIO_CH_FRONT_RIGHT : return SPA_AUDIO_CHANNEL_FR;
case LG_AUDIO_CH_FRONT_CENTER : return SPA_AUDIO_CHANNEL_FC;
case LG_AUDIO_CH_LFE : return SPA_AUDIO_CHANNEL_LFE;
case LG_AUDIO_CH_REAR_LEFT : return SPA_AUDIO_CHANNEL_RL;
case LG_AUDIO_CH_REAR_RIGHT : return SPA_AUDIO_CHANNEL_RR;
case LG_AUDIO_CH_FRONT_LEFT_CENTER : return SPA_AUDIO_CHANNEL_FLC;
case LG_AUDIO_CH_FRONT_RIGHT_CENTER : return SPA_AUDIO_CHANNEL_FRC;
case LG_AUDIO_CH_REAR_CENTER : return SPA_AUDIO_CHANNEL_RC;
case LG_AUDIO_CH_SIDE_LEFT : return SPA_AUDIO_CHANNEL_SL;
case LG_AUDIO_CH_SIDE_RIGHT : return SPA_AUDIO_CHANNEL_SR;
case LG_AUDIO_CH_TOP_CENTER : return SPA_AUDIO_CHANNEL_TC;
case LG_AUDIO_CH_TOP_FRONT_LEFT : return SPA_AUDIO_CHANNEL_TFL;
case LG_AUDIO_CH_TOP_FRONT_CENTER : return SPA_AUDIO_CHANNEL_TFC;
case LG_AUDIO_CH_TOP_FRONT_RIGHT : return SPA_AUDIO_CHANNEL_TFR;
case LG_AUDIO_CH_TOP_REAR_LEFT : return SPA_AUDIO_CHANNEL_TRL;
case LG_AUDIO_CH_TOP_REAR_CENTER : return SPA_AUDIO_CHANNEL_TRC;
case LG_AUDIO_CH_TOP_REAR_RIGHT : return SPA_AUDIO_CHANNEL_TRR;
}
return (enum spa_audio_channel)(SPA_AUDIO_CHANNEL_AUX0 + index);
}
static enum spa_audio_format pipewire_sampleFormat(
LG_AudioSampleFormat format)
{
switch (format)
{
case LG_AUDIO_FMT_U8 : return SPA_AUDIO_FORMAT_U8;
case LG_AUDIO_FMT_S16_LE : return SPA_AUDIO_FORMAT_S16_LE;
case LG_AUDIO_FMT_S24_LE : return SPA_AUDIO_FORMAT_S24_LE;
case LG_AUDIO_FMT_S32_LE : return SPA_AUDIO_FORMAT_S32_LE;
case LG_AUDIO_FMT_F32_LE : return SPA_AUDIO_FORMAT_F32_LE;
case LG_AUDIO_FMT_F64_LE : return SPA_AUDIO_FORMAT_F64_LE;
}
return SPA_AUDIO_FORMAT_UNKNOWN;
}
static int pipewire_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 :
case LG_AUDIO_FMT_F32_LE : return 4;
case LG_AUDIO_FMT_F64_LE : return 8;
}
return 0;
}
static struct spa_audio_info_raw pipewire_audioInfo(
const LG_AudioFormat * format, enum spa_audio_format sampleFormat)
{
struct spa_audio_info_raw info = SPA_AUDIO_INFO_RAW_INIT(
.format = sampleFormat,
.channels = format->channelCount,
.rate = format->sampleRate
);
for (uint8_t i = 0; i < format->channelCount; ++i)
info.position[i] = pipewire_channel(format->channels[i], i);
return info;
}
static void pipewire_reportPlaybackErrors(void)
{
const unsigned int bufferErrors = atomic_exchange_explicit(
@@ -411,13 +498,16 @@ static void pipewire_playbackStopStream(void)
pipewire_reportPlaybackErrors();
}
static bool pipewire_playbackSetup(int channels, int sampleRate,
static bool pipewire_playbackSetup(const LG_AudioFormat * format,
int requestedPeriodFrames, bool requestResampler,
bool * resamplerEnabled, int * maxPeriodFrames, int * startFrames,
LG_AudioPullFn pullFn)
{
*resamplerEnabled = false;
const int channels = format->channelCount;
const int sampleRate = format->sampleRate;
const struct spa_pod * params[1];
uint8_t buffer[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
@@ -431,8 +521,7 @@ static bool pipewire_playbackSetup(int channels, int sampleRate,
};
if (pw.playback.stream &&
pw.playback.channels == channels &&
pw.playback.sampleRate == sampleRate)
pipewire_audioFormatEqual(&pw.playback.format, format))
{
#if PW_CHECK_VERSION(1, 4, 0)
atomic_store_explicit(
@@ -451,10 +540,9 @@ static bool pipewire_playbackSetup(int channels, int sampleRate,
snprintf(requestedNodeLatency, sizeof(requestedNodeLatency), "%d/%d",
requestedPeriodFrames, sampleRate);
pw.playback.channels = channels;
pw.playback.sampleRate = sampleRate;
pw.playback.stride = sizeof(float) * channels;
pw.playback.pullFn = pullFn;
pw.playback.format = *format;
pw.playback.stride = sizeof(float) * channels;
pw.playback.pullFn = pullFn;
pw_thread_loop_lock(pw.thread);
@@ -538,12 +626,10 @@ static bool pipewire_playbackSetup(int channels, int sampleRate,
*maxPeriodFrames = pw.playback.maxPeriodFrames;
*startFrames = pw.playback.startFrames;
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
&SPA_AUDIO_INFO_RAW_INIT(
.format = SPA_AUDIO_FORMAT_F32,
.channels = channels,
.rate = sampleRate
));
struct spa_audio_info_raw info =
pipewire_audioInfo(format, SPA_AUDIO_FORMAT_F32);
params[0] = spa_format_audio_raw_build(
&b, SPA_PARAM_EnumFormat, &info);
pw.playback.connectionState = PW_STREAM_STATE_CONNECTING;
const int result = pw_stream_connect(
@@ -654,7 +740,7 @@ done:
static void pipewire_playbackVolume(int channels, const uint16_t volume[])
{
if (channels != pw.playback.channels)
if (channels != pw.playback.format.channelCount)
return;
float param[channels];
@@ -864,9 +950,12 @@ static void pipewire_onRecordProcess(void * userdata)
pw_stream_queue_buffer(pw.record.stream, pbuf);
}
static void pipewire_recordStart(int channels, int sampleRate,
static void pipewire_recordStart(const LG_AudioFormat * format,
LG_AudioPushFn pushFn)
{
const int channels = format->channelCount;
const int sampleRate = format->sampleRate;
const int sampleSize = pipewire_sampleSize(format->sampleFormat);
const struct spa_pod * params[1];
uint8_t buffer[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
@@ -877,8 +966,7 @@ static void pipewire_recordStart(int channels, int sampleRate,
};
if (pw.record.stream &&
pw.record.channels == channels &&
pw.record.sampleRate == sampleRate)
pipewire_audioFormatEqual(&pw.record.format, format))
{
if (!pw.record.active)
{
@@ -892,10 +980,9 @@ static void pipewire_recordStart(int channels, int sampleRate,
pipewire_recordStopStream();
pw.record.channels = channels;
pw.record.sampleRate = sampleRate;
pw.record.stride = sizeof(uint16_t) * channels;
pw.record.pushFn = pushFn;
pw.record.format = *format;
pw.record.stride = sampleSize * channels;
pw.record.pushFn = pushFn;
if (!pipewire_recordStartSender(sampleRate))
return;
@@ -941,12 +1028,11 @@ static void pipewire_recordStart(int channels, int sampleRate,
return;
}
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
&SPA_AUDIO_INFO_RAW_INIT(
.format = SPA_AUDIO_FORMAT_S16,
.channels = channels,
.rate = sampleRate
));
struct spa_audio_info_raw info =
pipewire_audioInfo(format,
pipewire_sampleFormat(format->sampleFormat));
params[0] = spa_format_audio_raw_build(
&b, SPA_PARAM_EnumFormat, &info);
const int result = pw_stream_connect(
pw.record.stream,
@@ -974,18 +1060,12 @@ static void pipewire_recordStart(int channels, int sampleRate,
static void pipewire_recordStop(void)
{
if (!pw.record.active)
return;
pw_thread_loop_lock(pw.thread);
pw_stream_set_active(pw.record.stream, false);
pw.record.active = false;
pw_thread_loop_unlock(pw.thread);
pipewire_recordStopStream();
}
static void pipewire_recordVolume(int channels, const uint16_t volume[])
{
if (channels != pw.record.channels)
if (channels != pw.record.format.channelCount)
return;
float param[channels];

View File

@@ -42,8 +42,7 @@ struct PulseAudio
bool sinkStarting;
int sinkMaxPeriodFrames;
int sinkStartFrames;
int sinkSampleRate;
int sinkChannels;
LG_AudioFormat sinkFormat;
int sinkStride;
LG_AudioPullFn sinkPullFn;
_Atomic(int64_t) sinkPresentationDeadline;
@@ -51,6 +50,68 @@ struct PulseAudio
static struct PulseAudio pa = {0};
static bool pulseaudio_audioFormatEqual(const LG_AudioFormat * a,
const LG_AudioFormat * b)
{
return
a->sampleFormat == b->sampleFormat &&
a->sampleRate == b->sampleRate &&
a->channelCount == b->channelCount &&
memcmp(a->channels, b->channels,
a->channelCount * sizeof(*a->channels)) == 0;
}
static pa_channel_position_t pulseaudio_channel(
LG_AudioChannel channel, uint8_t index)
{
switch (channel)
{
case LG_AUDIO_CH_UNKNOWN:
return (pa_channel_position_t)(PA_CHANNEL_POSITION_AUX0 + index);
case LG_AUDIO_CH_MONO :
return PA_CHANNEL_POSITION_MONO;
case LG_AUDIO_CH_FRONT_LEFT :
return PA_CHANNEL_POSITION_FRONT_LEFT;
case LG_AUDIO_CH_FRONT_RIGHT :
return PA_CHANNEL_POSITION_FRONT_RIGHT;
case LG_AUDIO_CH_FRONT_CENTER :
return PA_CHANNEL_POSITION_FRONT_CENTER;
case LG_AUDIO_CH_LFE :
return PA_CHANNEL_POSITION_LFE;
case LG_AUDIO_CH_REAR_LEFT :
return PA_CHANNEL_POSITION_REAR_LEFT;
case LG_AUDIO_CH_REAR_RIGHT :
return PA_CHANNEL_POSITION_REAR_RIGHT;
case LG_AUDIO_CH_FRONT_LEFT_CENTER :
return PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER;
case LG_AUDIO_CH_FRONT_RIGHT_CENTER :
return PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER;
case LG_AUDIO_CH_REAR_CENTER :
return PA_CHANNEL_POSITION_REAR_CENTER;
case LG_AUDIO_CH_SIDE_LEFT :
return PA_CHANNEL_POSITION_SIDE_LEFT;
case LG_AUDIO_CH_SIDE_RIGHT :
return PA_CHANNEL_POSITION_SIDE_RIGHT;
case LG_AUDIO_CH_TOP_CENTER :
return PA_CHANNEL_POSITION_TOP_CENTER;
case LG_AUDIO_CH_TOP_FRONT_LEFT :
return PA_CHANNEL_POSITION_TOP_FRONT_LEFT;
case LG_AUDIO_CH_TOP_FRONT_CENTER :
return PA_CHANNEL_POSITION_TOP_FRONT_CENTER;
case LG_AUDIO_CH_TOP_FRONT_RIGHT :
return PA_CHANNEL_POSITION_TOP_FRONT_RIGHT;
case LG_AUDIO_CH_TOP_REAR_LEFT :
return PA_CHANNEL_POSITION_TOP_REAR_LEFT;
case LG_AUDIO_CH_TOP_REAR_CENTER :
return PA_CHANNEL_POSITION_TOP_REAR_CENTER;
case LG_AUDIO_CH_TOP_REAR_RIGHT :
return PA_CHANNEL_POSITION_TOP_REAR_RIGHT;
}
return (pa_channel_position_t)(PA_CHANNEL_POSITION_AUX0 + index);
}
static void pulseaudio_unrefOperation(pa_operation * operation)
{
if (operation)
@@ -299,14 +360,17 @@ static void pulseaudio_overflow_cb(pa_stream * p, void * userdata)
DEBUG_WARN("Overflow");
}
static bool pulseaudio_setup(int channels, int sampleRate,
static bool pulseaudio_setup(const LG_AudioFormat * format,
int requestedPeriodFrames, bool requestResampler,
bool * resamplerEnabled, int * maxPeriodFrames, int * startFrames,
LG_AudioPullFn pullFn)
{
*resamplerEnabled = false;
if (pa.sink && pa.sinkChannels == channels && pa.sinkSampleRate == sampleRate)
const int channels = format->channelCount;
const int sampleRate = format->sampleRate;
if (pa.sink && pulseaudio_audioFormatEqual(&pa.sinkFormat, format))
{
*maxPeriodFrames = pa.sinkMaxPeriodFrames;
*startFrames = pa.sinkStartFrames;
@@ -318,6 +382,9 @@ static bool pulseaudio_setup(int channels, int sampleRate,
.rate = sampleRate,
.channels = channels
};
pa_channel_map channelMap = { .channels = channels };
for (uint8_t i = 0; i < format->channelCount; ++i)
channelMap.map[i] = pulseaudio_channel(format->channels[i], i);
int stride = channels * sizeof(float);
int bufferSize = requestedPeriodFrames * 2 * stride;
@@ -332,14 +399,14 @@ static bool pulseaudio_setup(int channels, int sampleRate,
pa_threaded_mainloop_lock(pa.loop);
pulseaudio_sink_close_nl();
pa.sinkChannels = channels;
pa.sinkSampleRate = sampleRate;
pa.sinkStride = stride;
pa.sinkPullFn = pullFn;
pa.sinkCorked = true;
pa.sinkStarting = false;
pa.sinkFormat = *format;
pa.sinkStride = stride;
pa.sinkPullFn = pullFn;
pa.sinkCorked = true;
pa.sinkStarting = false;
pa.sink = pa_stream_new(pa.context, "Looking Glass", &spec, NULL);
pa.sink = pa_stream_new(
pa.context, "Looking Glass", &spec, &channelMap);
if (!pa.sink)
{
DEBUG_ERROR("Failed to create PulseAudio stream: %s",

View File

@@ -0,0 +1,166 @@
/**
* 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_INTERFACE_
#define _H_LG_CLIENT_AUDIO_INTERFACE_
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define LG_AUDIO_MAX_CHANNELS 32
typedef enum LG_AudioSampleFormat
{
LG_AUDIO_FMT_U8,
LG_AUDIO_FMT_S16_LE,
/* Signed 24-bit little-endian samples packed into three bytes. */
LG_AUDIO_FMT_S24_LE,
LG_AUDIO_FMT_S32_LE,
/* IEEE 754 little-endian floating-point samples. */
LG_AUDIO_FMT_F32_LE,
LG_AUDIO_FMT_F64_LE,
}
LG_AudioSampleFormat;
typedef enum LG_AudioChannel
{
LG_AUDIO_CH_UNKNOWN,
LG_AUDIO_CH_MONO,
LG_AUDIO_CH_FRONT_LEFT,
LG_AUDIO_CH_FRONT_RIGHT,
LG_AUDIO_CH_FRONT_CENTER,
LG_AUDIO_CH_LFE,
LG_AUDIO_CH_REAR_LEFT,
LG_AUDIO_CH_REAR_RIGHT,
LG_AUDIO_CH_FRONT_LEFT_CENTER,
LG_AUDIO_CH_FRONT_RIGHT_CENTER,
LG_AUDIO_CH_REAR_CENTER,
LG_AUDIO_CH_SIDE_LEFT,
LG_AUDIO_CH_SIDE_RIGHT,
LG_AUDIO_CH_TOP_CENTER,
LG_AUDIO_CH_TOP_FRONT_LEFT,
LG_AUDIO_CH_TOP_FRONT_CENTER,
LG_AUDIO_CH_TOP_FRONT_RIGHT,
LG_AUDIO_CH_TOP_REAR_LEFT,
LG_AUDIO_CH_TOP_REAR_CENTER,
LG_AUDIO_CH_TOP_REAR_RIGHT,
}
LG_AudioChannel;
typedef struct LG_AudioFormat
{
LG_AudioSampleFormat sampleFormat;
uint32_t sampleRate;
uint8_t channelCount;
/* Samples are interleaved in this order. Unknown positions are explicit. */
LG_AudioChannel channels[LG_AUDIO_MAX_CHANNELS];
}
LG_AudioFormat;
typedef struct LG_AudioClock
{
/* Frame position and time describe the same point on the stream timeline.
* Time is monotonic nanoseconds in the publisher's clock domain; consumers
* must use differences unless they know that they share that domain. */
uint64_t position;
int64_t time;
double rate; /* measured frames per second, or zero when unavailable */
bool stable;
bool discontinuity;
}
LG_AudioClock;
typedef struct LG_AudioStatus
{
bool available;
/* Changes whenever the provider endpoint is replaced or restarted. */
uint32_t generation;
}
LG_AudioStatus;
typedef void (*LG_AudioStatusFn)(void * opaque,
const LG_AudioStatus * status);
typedef struct LG_AudioEventOps
{
/* Format and clock pointers are borrowed for the duration of each call.
* A NULL source clock indicates that the provider has no usable clock.
* Providers must serialize event delivery for an attachment. Stream
* generations are nonzero and uniquely identify each stream instance. */
void (*playbackStart)(void * opaque, uint32_t generation,
const LG_AudioFormat * format, const LG_AudioClock * sourceClock);
void (*playbackStop)(void * opaque, uint32_t generation);
void (*playbackVolume)(void * opaque, uint32_t generation,
uint8_t channels, const uint16_t volume[]);
void (*playbackMute)(void * opaque, uint32_t generation, bool mute);
/* Sample data is borrowed for the duration of the call. When supplied, the
* source clock position and time identify the first frame in this packet. */
void (*playbackData)(void * opaque, uint32_t generation,
const void * data, size_t frames,
const LG_AudioClock * sourceClock);
void (*recordStart)(void * opaque, uint32_t generation,
const LG_AudioFormat * format);
void (*recordStop)(void * opaque, uint32_t generation);
void (*recordVolume)(void * opaque, uint32_t generation,
uint8_t channels, const uint16_t volume[]);
void (*recordMute)(void * opaque, uint32_t generation, bool mute);
}
LG_AudioEventOps;
typedef struct LG_AudioOps
{
const char * name;
/* Registration must synchronously report the current status after releasing
* any backend locks. Passing NULL unregisters the listener. Providers with
* no status listener are assumed to remain available while their transport
* session is connected. Status callbacks must not be invoked synchronously
* from event delivery or from another operation in this interface. Passing
* NULL must synchronously quiesce any in-flight status callback. */
void (*setStatusListener)(void * opaque, LG_AudioStatusFn callback,
void * callbackOpaque);
/* Attach begins event delivery and replays any active streams. Detach must
* synchronously quiesce event callbacks before returning. The owner must
* stop event delivery before dropping an endpoint that can no longer be
* detached. */
bool (*attach)(void * opaque, const LG_AudioEventOps * events,
void * eventOpaque);
void (*detach)(void * opaque);
/* Send microphone frames to the guest. Data is borrowed for the duration of
* the call and uses the format supplied with the matching recordStart. The
* source clock, when present, identifies the first frame in the buffer. */
bool (*recordData)(void * opaque, uint32_t generation,
const void * data, size_t frames, const LG_AudioClock * sourceClock);
/* Optional active synchronization feedback. This is called outside the
* realtime audio callback with the measured playback device clock. Its
* position uses the device's independent output-frame timeline and its time
* includes the backend's estimated presentation latency. */
bool (*clockFeedback)(void * opaque, uint32_t generation,
const LG_AudioClock * playbackClock);
}
LG_AudioOps;
#endif

View File

@@ -25,6 +25,8 @@
#include <stdint.h>
#include <stddef.h>
#include "interface/audio.h"
typedef int (*LG_AudioPullFn)(uint8_t * dst, int frames);
typedef void (*LG_AudioPushFn)(uint8_t * src, int frames);
@@ -50,14 +52,14 @@ struct LG_AudioDevOps
* stream.
* Note: the pull function returns f32 samples
*/
bool (*setup)(int channels, int sampleRate, int requestedPeriodFrames,
bool (*setup)(const LG_AudioFormat * format, int requestedPeriodFrames,
bool requestResampler, bool * resamplerEnabled,
int * maxPeriodFrames, int * startFrames, LG_AudioPullFn pullFn);
/* called when there is data available to start playback */
void (*start)(void);
/* called when SPICE reports the audio stream has stopped */
/* called when the source reports the audio stream has stopped */
void (*stop)(void);
/* [optional] called to set the volume of the channels */
@@ -77,12 +79,10 @@ struct LG_AudioDevOps
struct
{
/* start the record stream
* Note: currently SPICE only supports S16 samples so always assume so
*/
void (*start)(int channels, int sampleRate, LG_AudioPushFn pushFn);
/* start the record stream using the requested interleaved format */
void (*start)(const LG_AudioFormat * format, LG_AudioPushFn pushFn);
/* called when SPICE reports the audio stream has stopped */
/* called when the source reports the audio stream has stopped */
void (*stop)(void);
/* [optional] called to set the volume of the channels */

View File

@@ -27,6 +27,7 @@
#include "common/framebuffer.h"
#include "common/types.h"
#include "interface/audio.h"
#include "interface/input.h"
#define LG_TRANSPORT_MAX_DAMAGE_RECTS LG_MAX_FRAME_DAMAGE_RECTS
@@ -250,6 +251,10 @@ typedef struct LG_TransportOps
* valid until disconnect; NULL indicates that this session has no input. */
const LG_InputOps *(*getInputOps)(LG_Transport * transport,
void ** opaque);
/* Queried after connect. The returned operations and opaque value remain
* valid until disconnect; NULL indicates that this session has no audio. */
const LG_AudioOps *(*getAudioOps)(LG_Transport * transport,
void ** opaque);
bool (*attachRenderer)(LG_Transport * transport,
const LG_RendererInterop * interop);
void (*detachRenderer)(LG_Transport * transport);

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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