Files
LookingGlass/client/audiodevs/PulseAudio/pulseaudio.c
Geoffrey McRae 0da4bfa8dd
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
[client] audio: add transport provider interface
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.
2026-08-09 17:57:09 +10:00

567 lines
15 KiB
C

/**
* 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 "interface/audiodev.h"
#include <pulse/pulseaudio.h>
#include <string.h>
#include <math.h>
#include <stdatomic.h>
#include "common/debug.h"
#include "common/time.h"
struct PulseAudio
{
pa_threaded_mainloop * loop;
pa_mainloop_api * api;
pa_context * context;
pa_operation * contextSub;
pa_stream * sink;
int sinkIndex;
bool sinkCorked;
bool sinkMuted;
bool sinkStarting;
int sinkMaxPeriodFrames;
int sinkStartFrames;
LG_AudioFormat sinkFormat;
int sinkStride;
LG_AudioPullFn sinkPullFn;
_Atomic(int64_t) sinkPresentationDeadline;
};
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)
pa_operation_unref(operation);
}
static void pulseaudio_sink_input_cb(pa_context *c, const pa_sink_input_info *i,
int eol, void *userdata)
{
if (eol < 0 || eol == 1)
return;
pa.sinkIndex = i->index;
}
static void pulseaudio_subscribe_cb(pa_context *c,
pa_subscription_event_type_t t, uint32_t index, void *userdata)
{
switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK)
{
case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
pa.sinkIndex = 0;
else
{
pa_operation *o = pa_context_get_sink_input_info(c, index,
pulseaudio_sink_input_cb, NULL);
pulseaudio_unrefOperation(o);
}
break;
}
}
static void pulseaudio_ctx_state_change_cb(pa_context * c, void * userdata)
{
switch (pa_context_get_state(c))
{
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
break;
case PA_CONTEXT_READY:
DEBUG_INFO("Connected to PulseAudio server");
pa_context_set_subscribe_callback(c, pulseaudio_subscribe_cb, NULL);
pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SINK_INPUT, NULL, NULL);
pa_threaded_mainloop_signal(pa.loop, 0);
break;
case PA_CONTEXT_TERMINATED:
if (pa.contextSub)
{
pa_operation_unref(pa.contextSub);
pa.contextSub = NULL;
}
break;
case PA_CONTEXT_FAILED:
default:
DEBUG_ERROR("context error: %s", pa_strerror(pa_context_errno(c)));
break;
}
}
static bool pulseaudio_init(void)
{
pa.loop = pa_threaded_mainloop_new();
if (!pa.loop)
{
DEBUG_ERROR("Failed to create the main loop");
goto err;
}
pa.api = pa_threaded_mainloop_get_api(pa.loop);
if (pa_signal_init(pa.api) != 0)
{
DEBUG_ERROR("Failed to init signals");
goto err_loop;
}
if (pa_threaded_mainloop_start(pa.loop) < 0)
{
DEBUG_ERROR("Failed to start the main loop");
goto err_loop;
}
pa_proplist * propList = pa_proplist_new();
if (!propList)
{
DEBUG_ERROR("Failed to create the proplist");
goto err_thread;
}
pa_proplist_sets(propList, PA_PROP_MEDIA_ROLE, "video");
pa_threaded_mainloop_lock(pa.loop);
pa.context = pa_context_new_with_proplist(
pa.api,
"Looking Glass",
propList);
if (!pa.context)
{
DEBUG_ERROR("Failed to create the context");
goto err_context;
}
pa_context_set_state_callback(pa.context,
pulseaudio_ctx_state_change_cb, NULL);
if (pa_context_connect(pa.context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) < 0)
{
DEBUG_ERROR("Failed to connect to the context server");
goto err_context;
}
for(;;)
{
pa_context_state_t state = pa_context_get_state(pa.context);
if(!PA_CONTEXT_IS_GOOD(state))
{
DEBUG_ERROR("Context is bad");
goto err_context;
}
if (state == PA_CONTEXT_READY)
break;
pa_threaded_mainloop_wait(pa.loop);
}
pa_threaded_mainloop_unlock(pa.loop);
pa_proplist_free(propList);
return true;
err_context:
pa_threaded_mainloop_unlock(pa.loop);
pa_proplist_free(propList);
err_thread:
pa_threaded_mainloop_stop(pa.loop);
err_loop:
pa_threaded_mainloop_free(pa.loop);
err:
return false;
}
static void pulseaudio_sink_close_nl(void)
{
if (!pa.sink)
return;
pa_stream_set_state_callback(pa.sink, NULL, NULL);
pa_stream_set_write_callback(pa.sink, NULL, NULL);
pa_stream_set_underflow_callback(pa.sink, NULL, NULL);
pa_stream_set_overflow_callback(pa.sink, NULL, NULL);
pulseaudio_unrefOperation(pa_stream_flush(pa.sink, NULL, NULL));
pa_stream_unref(pa.sink);
pa.sink = NULL;
atomic_store_explicit(
&pa.sinkPresentationDeadline, 0, memory_order_release);
}
static void pulseaudio_free(void)
{
pa_threaded_mainloop_lock(pa.loop);
pulseaudio_sink_close_nl();
pa_context_set_state_callback(pa.context, NULL, NULL);
pa_context_set_subscribe_callback(pa.context, NULL, NULL);
pa_context_disconnect(pa.context);
pa_context_unref(pa.context);
if (pa.contextSub)
{
pa_operation_unref(pa.contextSub);
pa.contextSub = NULL;
}
pa_threaded_mainloop_unlock(pa.loop);
}
static void pulseaudio_state_cb(pa_stream * p, void * userdata)
{
if (pa.sinkStarting && pa_stream_get_state(pa.sink) == PA_STREAM_READY)
{
pulseaudio_unrefOperation(pa_stream_cork(pa.sink, 0, NULL, NULL));
pa.sinkCorked = false;
pa.sinkStarting = false;
}
pa_threaded_mainloop_signal(pa.loop, 0);
}
static void pulseaudio_write_cb(pa_stream * p, size_t nbytes, void * userdata)
{
// PulseAudio tries to pull data from the stream as soon as it is created for
// some reason, even though it is corked
if (pa.sinkCorked)
return;
uint8_t * dst;
if (pa_stream_begin_write(p, (void **)&dst, &nbytes) < 0)
{
DEBUG_ERROR("pa_stream_begin_write failed: %s",
pa_strerror(pa_context_errno(pa.context)));
return;
}
int frames = nbytes / pa.sinkStride;
frames = pa.sinkPullFn(dst, frames);
if (frames <= 0)
{
pa_stream_cancel_write(p);
return;
}
if (pa_stream_write(
p, dst, frames * pa.sinkStride, NULL, 0, PA_SEEK_RELATIVE) < 0)
{
DEBUG_ERROR("pa_stream_write failed: %s",
pa_strerror(pa_context_errno(pa.context)));
pa_stream_cancel_write(p);
return;
}
pa_usec_t latency;
int negative;
if (pa_stream_get_latency(p, &latency, &negative) == 0)
{
const int64_t latencyNs = negative ? 0 : (int64_t)latency * 1000;
atomic_store_explicit(&pa.sinkPresentationDeadline,
(int64_t)nanotime() + latencyNs, memory_order_release);
}
}
static void pulseaudio_underflow_cb(pa_stream * p, void * userdata)
{
DEBUG_WARN("Underflow");
}
static void pulseaudio_overflow_cb(pa_stream * p, void * userdata)
{
DEBUG_WARN("Overflow");
}
static bool pulseaudio_setup(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;
if (pa.sink && pulseaudio_audioFormatEqual(&pa.sinkFormat, format))
{
*maxPeriodFrames = pa.sinkMaxPeriodFrames;
*startFrames = pa.sinkStartFrames;
return true;
}
pa_sample_spec spec = {
.format = PA_SAMPLE_FLOAT32,
.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;
pa_buffer_attr attribs =
{
.maxlength = -1,
.tlength = bufferSize,
.prebuf = 0,
.minreq = (uint32_t)-1
};
pa_threaded_mainloop_lock(pa.loop);
pulseaudio_sink_close_nl();
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, &channelMap);
if (!pa.sink)
{
DEBUG_ERROR("Failed to create PulseAudio stream: %s",
pa_strerror(pa_context_errno(pa.context)));
pa_threaded_mainloop_unlock(pa.loop);
return false;
}
pa_stream_set_state_callback (pa.sink, pulseaudio_state_cb , NULL);
pa_stream_set_write_callback (pa.sink, pulseaudio_write_cb , NULL);
pa_stream_set_underflow_callback(pa.sink, pulseaudio_underflow_cb, NULL);
pa_stream_set_overflow_callback (pa.sink, pulseaudio_overflow_cb , NULL);
const pa_stream_flags_t flags =
PA_STREAM_START_CORKED |
PA_STREAM_ADJUST_LATENCY |
PA_STREAM_INTERPOLATE_TIMING |
PA_STREAM_AUTO_TIMING_UPDATE;
if (pa_stream_connect_playback(
pa.sink, NULL, &attribs, flags, NULL, NULL) < 0)
{
DEBUG_ERROR("Failed to connect PulseAudio stream: %s",
pa_strerror(pa_context_errno(pa.context)));
pulseaudio_sink_close_nl();
pa_threaded_mainloop_unlock(pa.loop);
return false;
}
while (pa_stream_get_state(pa.sink) == PA_STREAM_CREATING)
pa_threaded_mainloop_wait(pa.loop);
if (pa_stream_get_state(pa.sink) != PA_STREAM_READY)
{
DEBUG_ERROR("PulseAudio stream did not become ready: %s",
pa_strerror(pa_context_errno(pa.context)));
pulseaudio_sink_close_nl();
pa_threaded_mainloop_unlock(pa.loop);
return false;
}
const pa_buffer_attr * actual = pa_stream_get_buffer_attr(pa.sink);
const uint64_t minRequestFrames =
actual && actual->minreq != UINT32_MAX ?
actual->minreq / (uint64_t)stride : requestedPeriodFrames;
const uint64_t targetFrames =
actual && actual->tlength != UINT32_MAX ?
actual->tlength / (uint64_t)stride :
(uint64_t)requestedPeriodFrames * 2;
const int actualMinRequest =
clamp(minRequestFrames, UINT64_C(1), (uint64_t)INT_MAX);
const int actualTarget =
clamp(max(targetFrames, minRequestFrames),
UINT64_C(1), (uint64_t)INT_MAX);
pa.sinkMaxPeriodFrames = actualMinRequest;
pa.sinkStartFrames = actualTarget;
*maxPeriodFrames = pa.sinkMaxPeriodFrames;
*startFrames = pa.sinkStartFrames;
atomic_store_explicit(
&pa.sinkPresentationDeadline, 0, memory_order_release);
pa_threaded_mainloop_unlock(pa.loop);
return true;
}
static void pulseaudio_start(void)
{
if (!pa.sink)
return;
pa_threaded_mainloop_lock(pa.loop);
pa_stream_state_t state = pa_stream_get_state(pa.sink);
if (state == PA_STREAM_CREATING)
pa.sinkStarting = true;
else
{
pulseaudio_unrefOperation(pa_stream_cork(pa.sink, 0, NULL, NULL));
pa.sinkCorked = false;
}
pa_threaded_mainloop_unlock(pa.loop);
}
static void pulseaudio_stop(void)
{
if (!pa.sink)
return;
bool needLock = !pa_threaded_mainloop_in_thread(pa.loop);
if (needLock)
pa_threaded_mainloop_lock(pa.loop);
pulseaudio_unrefOperation(pa_stream_cork(pa.sink, 1, NULL, NULL));
pa.sinkCorked = true;
pa.sinkStarting = false;
atomic_store_explicit(
&pa.sinkPresentationDeadline, 0, memory_order_release);
if (needLock)
pa_threaded_mainloop_unlock(pa.loop);
}
static void pulseaudio_volume(int channels, const uint16_t volume[])
{
if (!pa.sink || !pa.sinkIndex)
return;
struct pa_cvolume v = { .channels = channels };
for(int i = 0; i < channels; ++i)
v.values[i] = pa_sw_volume_from_linear(
9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787);
pa_threaded_mainloop_lock(pa.loop);
pulseaudio_unrefOperation(pa_context_set_sink_input_volume(
pa.context, pa.sinkIndex, &v, NULL, NULL));
pa_threaded_mainloop_unlock(pa.loop);
}
static void pulseaudio_mute(bool mute)
{
if (!pa.sink || !pa.sinkIndex || pa.sinkMuted == mute)
return;
pa.sinkMuted = mute;
pa_threaded_mainloop_lock(pa.loop);
pulseaudio_unrefOperation(pa_context_set_sink_input_mute(
pa.context, pa.sinkIndex, mute, NULL, NULL));
pa_threaded_mainloop_unlock(pa.loop);
}
static uint64_t pulseaudio_latency(void)
{
const int64_t deadline = atomic_load_explicit(
&pa.sinkPresentationDeadline, memory_order_acquire);
if (deadline <= 0)
return 0;
return max(INT64_C(0), deadline - (int64_t)nanotime()) / 1000;
}
struct LG_AudioDevOps LGAD_PulseAudio =
{
.name = "PulseAudio",
.init = pulseaudio_init,
.free = pulseaudio_free,
.playback =
{
.setup = pulseaudio_setup,
.start = pulseaudio_start,
.stop = pulseaudio_stop,
.volume = pulseaudio_volume,
.mute = pulseaudio_mute,
.latency = pulseaudio_latency
}
};