mirror of
https://github.com/gnif/LookingGlass.git
synced 2024-12-22 13:33:40 +00:00
[client] audio/pa: added initial pulseaudio implementation
This commit is contained in:
parent
a8ddf72318
commit
90dd1f3913
@ -54,6 +54,9 @@ add_feature_info(ENABLE_LIBDECOR ENABLE_LIBDECOR "libdecor support.")
|
||||
option(ENABLE_PIPEWIRE "Build with PipeWire audio output support" ON)
|
||||
add_feature_info(ENABLE_PIPEWIRE ENABLE_PIPEWIRE "PipeWire audio support.")
|
||||
|
||||
option(ENABLE_PULSEAUDIO "Build with PulseAudio audio output support" ON)
|
||||
add_feature_info(ENABLE_PULSEAUDIO ENABLE_PULSEAUDIO "PulseAudio audio support.")
|
||||
|
||||
if (NOT ENABLE_X11 AND NOT ENABLE_WAYLAND)
|
||||
message(FATAL_ERROR "Either ENABLE_X11 or ENABLE_WAYLAND must be on")
|
||||
endif()
|
||||
|
@ -22,6 +22,9 @@ endfunction()
|
||||
if(ENABLE_PIPEWIRE)
|
||||
add_audiodev(PipeWire)
|
||||
endif()
|
||||
if(ENABLE_PULSEAUDIO)
|
||||
add_audiodev(PulseAudio)
|
||||
endif()
|
||||
|
||||
list(REMOVE_AT AUDIODEVS 0)
|
||||
list(REMOVE_AT AUDIODEVS_LINK 0)
|
||||
|
21
client/audiodevs/PulseAudio/CMakeLists.txt
Normal file
21
client/audiodevs/PulseAudio/CMakeLists.txt
Normal file
@ -0,0 +1,21 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
project(audiodev_PulseAudio LANGUAGES C)
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(AUDIODEV_PulseAudio REQUIRED IMPORTED_TARGET
|
||||
libpulse
|
||||
)
|
||||
|
||||
add_library(audiodev_PulseAudio STATIC
|
||||
pulseaudio.c
|
||||
)
|
||||
|
||||
target_link_libraries(audiodev_PulseAudio
|
||||
PkgConfig::AUDIODEV_PulseAudio
|
||||
lg_common
|
||||
)
|
||||
|
||||
target_include_directories(audiodev_PulseAudio
|
||||
PRIVATE
|
||||
src
|
||||
)
|
354
client/audiodevs/PulseAudio/pulseaudio.c
Normal file
354
client/audiodevs/PulseAudio/pulseaudio.c
Normal file
@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2021 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 "common/debug.h"
|
||||
#include "common/ringbuffer.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;
|
||||
int sinkStart;
|
||||
int sinkSampleRate;
|
||||
int sinkChannels;
|
||||
int sinkStride;
|
||||
RingBuffer sinkBuffer;
|
||||
};
|
||||
|
||||
static struct PulseAudio pa = {0};
|
||||
|
||||
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);
|
||||
pa_operation_unref(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()
|
||||
{
|
||||
if (!pa.sink)
|
||||
return;
|
||||
|
||||
pa_stream_set_write_callback(pa.sink, NULL, NULL);
|
||||
pa_stream_flush(pa.sink, NULL, NULL);
|
||||
pa_stream_unref(pa.sink);
|
||||
pa.sink = NULL;
|
||||
}
|
||||
|
||||
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_write_cb(pa_stream * p, size_t nbytes, void * userdata)
|
||||
{
|
||||
uint8_t * dst;
|
||||
|
||||
pa_stream_begin_write(p, (void **)&dst, &nbytes);
|
||||
|
||||
int frames = nbytes / pa.sinkStride;
|
||||
void * values = ringbuffer_consume(pa.sinkBuffer, &frames);
|
||||
|
||||
memcpy(dst, values, frames * pa.sinkStride);
|
||||
pa_stream_write(p, dst, frames * pa.sinkStride, NULL, 0, PA_SEEK_RELATIVE);
|
||||
}
|
||||
|
||||
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 void pulseaudio_start(int channels, int sampleRate)
|
||||
{
|
||||
if (pa.sink && pa.sinkChannels == channels && pa.sinkSampleRate == sampleRate)
|
||||
return;
|
||||
|
||||
//TODO: be smarter about this
|
||||
const int PERIOD_LEN = 80;
|
||||
|
||||
pa_sample_spec spec = {
|
||||
.format = PA_SAMPLE_S16LE,
|
||||
.rate = sampleRate,
|
||||
.channels = channels
|
||||
};
|
||||
|
||||
pa_buffer_attr attribs =
|
||||
{
|
||||
.maxlength = pa_usec_to_bytes((PERIOD_LEN * 2) * PA_USEC_PER_MSEC, &spec),
|
||||
.tlength = pa_usec_to_bytes(PERIOD_LEN * PA_USEC_PER_MSEC, &spec),
|
||||
.prebuf = 0,
|
||||
.fragsize = pa_usec_to_bytes(PERIOD_LEN * PA_USEC_PER_MSEC, &spec),
|
||||
.minreq = (uint32_t)-1
|
||||
};
|
||||
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
pulseaudio_sink_close_nl();
|
||||
|
||||
pa.sinkChannels = channels;
|
||||
pa.sinkSampleRate = sampleRate;
|
||||
|
||||
pa.sink = pa_stream_new(pa.context, "Looking Glass", &spec, 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);
|
||||
|
||||
pa_stream_connect_playback(pa.sink, NULL, &attribs,
|
||||
PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY,
|
||||
NULL, NULL);
|
||||
|
||||
pa.sinkStride = channels * sizeof(uint16_t);
|
||||
pa.sinkStart = attribs.tlength / pa.sinkStride;
|
||||
pa.sinkBuffer = ringbuffer_new(pa.sinkStart * 2, pa.sinkStride);
|
||||
pa.sinkCorked = true;
|
||||
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
}
|
||||
|
||||
static void pulseaudio_play(uint8_t * data, int size)
|
||||
{
|
||||
if (!pa.sink)
|
||||
return;
|
||||
|
||||
ringbuffer_append(pa.sinkBuffer, data, size / pa.sinkStride);
|
||||
|
||||
if (pa.sinkCorked && ringbuffer_getCount(pa.sinkBuffer) >= pa.sinkStart)
|
||||
{
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
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;
|
||||
|
||||
pa_threaded_mainloop_lock(pa.loop);
|
||||
pa_stream_cork(pa.sink, 1, NULL, NULL);
|
||||
pa.sinkCorked = true;
|
||||
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);
|
||||
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);
|
||||
pa_context_set_sink_input_mute(pa.context, pa.sinkIndex, mute, NULL, NULL);
|
||||
pa_threaded_mainloop_unlock(pa.loop);
|
||||
}
|
||||
|
||||
struct LG_AudioDevOps LGAD_PulseAudio =
|
||||
{
|
||||
.name = "PulseAudio",
|
||||
.init = pulseaudio_init,
|
||||
.free = pulseaudio_free,
|
||||
.start = pulseaudio_start,
|
||||
.play = pulseaudio_play,
|
||||
.stop = pulseaudio_stop,
|
||||
.volume = pulseaudio_volume,
|
||||
.mute = pulseaudio_mute
|
||||
};
|
@ -97,6 +97,10 @@ feature is disabled when running :ref:`cmake <client_building>`.
|
||||
|
||||
- libpipewire-0.3-dev
|
||||
|
||||
- Disable with ``cmake -DENABLE_PULSEAUDIO=no ..``
|
||||
|
||||
- libpulse-dev
|
||||
|
||||
.. _client_deps_recommended:
|
||||
|
||||
Recommended
|
||||
|
@ -21,6 +21,7 @@ kvmfr
|
||||
laggy
|
||||
libdecor
|
||||
libpipewire
|
||||
libpulse
|
||||
libvirt
|
||||
linux
|
||||
LookingGlass
|
||||
|
Loading…
Reference in New Issue
Block a user