[client] audio: initial addition of PipeWire audio support via SPICE

This commit is contained in:
Geoffrey McRae 2021-12-24 18:43:20 +11:00
parent 8ba4b56dba
commit e810577317
12 changed files with 444 additions and 4 deletions

View File

@ -44,6 +44,7 @@ jobs:
-DCMAKE_LINKER:FILEPATH=/usr/bin/ld \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DENABLE_LIBDECOR=${{ matrix.wayland_shell == 'libdecor' }} \
-DENABLE_PIPEWIRE=0 \
..
- name: Build client
run: |

View File

@ -51,6 +51,9 @@ add_feature_info(ENABLE_WAYLAND ENABLE_WAYLAND "Wayland support.")
option(ENABLE_LIBDECOR "Build with libdecor support" OFF)
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.")
if (NOT ENABLE_X11 AND NOT ENABLE_WAYLAND)
message(FATAL_ERROR "Either ENABLE_X11 or ENABLE_WAYLAND must be on")
endif()
@ -144,6 +147,7 @@ add_subdirectory("${PROJECT_TOP}/repos/cimgui" "${CMAKE_BINARY_DIR}/cimgui" E
add_subdirectory(displayservers)
add_subdirectory(renderers)
add_subdirectory(audiodevs)
add_executable(looking-glass-client ${SOURCES})
@ -158,6 +162,7 @@ target_link_libraries(looking-glass-client
purespice
renderers
cimgui
audiodevs
)
install(TARGETS looking-glass-client

View File

@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.0)
project(audiodevs LANGUAGES C)
set(AUDIODEV_H "${CMAKE_BINARY_DIR}/include/dynamic/audiodev.h")
set(AUDIODEV_C "${CMAKE_BINARY_DIR}/src/audiodev.c")
file(WRITE ${AUDIODEV_H} "#include \"interface/audiodev.h\"\n\n")
file(APPEND ${AUDIODEV_H} "extern struct LG_AudioDevOps * LG_AudioDevs[];\n\n")
file(WRITE ${AUDIODEV_C} "#include \"interface/audiodev.h\"\n\n")
file(APPEND ${AUDIODEV_C} "#include <stddef.h>\n\n")
set(AUDIODEVS "_")
set(AUDIODEVS_LINK "_")
function(add_audiodev name)
set(AUDIODEVS "${AUDIODEVS};${name}" PARENT_SCOPE)
set(AUDIODEVS_LINK "${AUDIODEVS_LINK};audiodev_${name}" PARENT_SCOPE)
add_subdirectory(${name})
endfunction()
# Add/remove audiodevs here!
if(ENABLE_PIPEWIRE)
add_audiodev(PipeWire)
endif()
list(REMOVE_AT AUDIODEVS 0)
list(REMOVE_AT AUDIODEVS_LINK 0)
list(LENGTH AUDIODEVS AUDIODEV_COUNT)
file(APPEND ${AUDIODEV_H} "#define LG_AUDIODEV_COUNT ${AUDIODEV_COUNT}\n")
foreach(audiodev ${AUDIODEVS})
file(APPEND ${AUDIODEV_C} "extern struct LG_AudioDevOps LGAD_${audiodev};\n")
endforeach()
file(APPEND ${AUDIODEV_C} "\nconst struct LG_AudioDevOps * LG_AudioDevs[] =\n{\n")
foreach(audiodev ${AUDIODEVS})
file(APPEND ${AUDIODEV_C} " &LGAD_${audiodev},\n")
endforeach()
file(APPEND ${AUDIODEV_C} " NULL\n};")
add_library(audiodevs STATIC ${AUDIODEV_C})
target_link_libraries(audiodevs ${AUDIODEVS_LINK})

View File

@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.0)
project(audiodev_PipeWire LANGUAGES C)
find_package(PkgConfig)
pkg_check_modules(AUDIODEV_PipeWire REQUIRED IMPORTED_TARGET
libpipewire-0.3
)
add_library(audiodev_PipeWire STATIC
pipewire.c
)
target_link_libraries(audiodev_PipeWire
PkgConfig::AUDIODEV_PipeWire
lg_common
)
target_include_directories(audiodev_PipeWire
PRIVATE
src
)

View File

@ -0,0 +1,220 @@
/**
* 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 <spa/param/audio/format-utils.h>
#include <pipewire/pipewire.h>
#include "common/debug.h"
#include "common/ringbuffer.h"
struct PipeWire
{
struct pw_loop * loop;
struct pw_thread_loop * thread;
struct pw_stream * stream;
int channels;
int stride;
RingBuffer buffer;
};
static struct PipeWire pw = {0};
static void pipewire_on_process(void * userdata)
{
struct pw_buffer * pbuf;
const int avail = ringbuffer_getCount(pw.buffer);
if (!avail)
return;
if (!(pbuf = pw_stream_dequeue_buffer(pw.stream))) {
DEBUG_WARN("out of buffers");
return;
}
struct spa_buffer * sbuf = pbuf->buffer;
uint8_t * dst;
if (!(dst = sbuf->datas[0].data))
return;
int frames = sbuf->datas[0].maxsize / pw.stride;
if (frames > avail)
frames = avail;
for(int i = 0; i < frames; ++i)
{
ringbuffer_shift(pw.buffer, dst);
dst += pw.stride;
}
sbuf->datas[0].chunk->offset = 0;
sbuf->datas[0].chunk->stride = pw.stride;
sbuf->datas[0].chunk->size = frames * pw.stride;
pw_stream_queue_buffer(pw.stream, pbuf);
}
static bool pipewire_init(void)
{
pw_init(NULL, NULL);
pw.loop = pw_loop_new(NULL);
struct pw_context * context = pw_context_new(pw.loop, NULL, 0);
if (!context)
{
DEBUG_ERROR("Failed to create a context");
goto err;
}
/* this is just to test for PipeWire availabillity */
struct pw_core * core = pw_context_connect(context, NULL, 0);
if (!core)
goto err_context;
pw_context_destroy(context);
/* PipeWire is available so create the loop thread and start it */
pw.thread = pw_thread_loop_new_full(pw.loop, "Playback", NULL);
if (!pw.thread)
{
DEBUG_ERROR("Failed to create the thread loop");
goto err;
}
pw_thread_loop_start(pw.thread);
return true;
err_context:
pw_context_destroy(context);
err:
pw_deinit();
return false;
}
static void pipewire_free(void)
{
if (pw.thread)
{
pw_thread_loop_lock(pw.thread);
if (pw.stream)
{
pw_stream_destroy(pw.stream);
pw.stream = NULL;
}
pw_thread_loop_signal(pw.thread, true);
pw_thread_loop_destroy(pw.thread);
pw.loop = NULL;
}
ringbuffer_free(&pw.buffer);
pw_deinit();
}
static void pipewire_start(int channels, int sampleRate)
{
const struct spa_pod * params[1];
uint8_t buffer[1024];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
static const struct pw_stream_events events =
{
.version = PW_VERSION_STREAM_EVENTS,
.process = pipewire_on_process
};
pw.channels = channels;
pw.stride = sizeof(uint16_t) * channels;
pw.buffer = ringbuffer_new(sampleRate, channels * sizeof(uint16_t));
pw_thread_loop_lock(pw.thread);
pw.stream = pw_stream_new_simple(
pw.loop,
"LookingGlass",
pw_properties_new(
PW_KEY_MEDIA_TYPE , "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE , "Music",
NULL
),
&events,
NULL
);
if (!pw.stream)
{
pw_thread_loop_unlock(pw.thread);
DEBUG_ERROR("Failed to create the stream");
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
));
pw_stream_connect(
pw.stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS,
params, 1);
pw_thread_loop_unlock(pw.thread);
}
static void pipewire_play(uint8_t * data, int size)
{
if (!pw.stream)
return;
for(int i = 0; i < size; i += pw.stride)
ringbuffer_push(pw.buffer, data + i);
}
static void pipewire_stop(void)
{
if (!pw.stream)
return;
pw_thread_loop_lock(pw.thread);
pw_stream_flush(pw.stream, true);
pw_stream_destroy(pw.stream);
pw.stream = NULL;
pw_thread_loop_unlock(pw.thread);
}
struct LG_AudioDevOps LGAD_PipeWire =
{
.name = "PipeWire",
.init = pipewire_init,
.free = pipewire_free,
.start = pipewire_start,
.play = pipewire_play,
.stop = pipewire_stop
};

View File

@ -0,0 +1,63 @@
/**
* 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
*/
#ifndef _H_I_AUDIODEV_
#define _H_I_AUDIODEV_
#include <stdbool.h>
#include <stdint.h>
struct LG_AudioDevOps
{
/* internal name of the audio for debugging */
const char * name;
/* called very early to allow for option registration, optional */
void (*earlyInit)(void);
/* called to initialize the audio backend */
bool (*init)(void);
/* final free */
void (*free)(void);
/* setup the playback audio stream
* Note: currently SPICE only supports S16 samples so always assume so
*/
void (*start)(int channels, int sampleRate);
/* called for each packet of output audio to play
* Note: size is the size of data in bytes, not frames/samples
*/
void (*play)(uint8_t * data, int size);
/* called when SPICE reports the audio stream has stopped */
void (*stop)(void);
};
#define ASSERT_LG_AUDIODEV_VALID(x) \
DEBUG_ASSERT((x)->name ); \
DEBUG_ASSERT((x)->init ); \
DEBUG_ASSERT((x)->free ); \
DEBUG_ASSERT((x)->start ); \
DEBUG_ASSERT((x)->play ); \
DEBUG_ASSERT((x)->stop );
#endif

View File

@ -428,6 +428,13 @@ static struct Option options[] =
.type = OPTION_TYPE_BOOL,
.value.x_bool = true
},
{
.module = "spice",
.name = "audio",
.description = "Enable SPICE audio support",
.type = OPTION_TYPE_BOOL,
.value.x_bool = true
},
{
.module = "spice",
.name = "scaleCursor",
@ -609,6 +616,7 @@ bool config_load(int argc, char * argv[])
g_params.useSpiceInput = option_get_bool("spice", "input" );
g_params.useSpiceClipboard = option_get_bool("spice", "clipboard");
g_params.useSpiceAudio = option_get_bool("spice", "audio" );
if (g_params.useSpiceClipboard)
{

View File

@ -846,6 +846,66 @@ static void reportBadVersion()
DEBUG_ERROR("Please install the matching host application for this client");
}
void audioStart(int channels, int sampleRate, PSAudioFormat format,
uint32_t time)
{
/*
* we probe here so that the audiodev is operating in the context of the SPICE
* thread/loop to avoid any audio API threading issues
*/
static int probed = false;
if (!probed)
{
probed = true;
// search for the best audiodev to use
for(int i = 0; i < LG_AUDIODEV_COUNT; ++i)
if (LG_AudioDevs[i]->init())
{
g_state.audioDev = LG_AudioDevs[i];
DEBUG_INFO("Using AudioDev: %s", g_state.audioDev->name);
break;
}
if (!g_state.audioDev)
DEBUG_WARN("Failed to initialize an audio backend");
}
if (g_state.audioDev)
{
static int lastChannels = 0;
static int lastSampleRate = 0;
if (g_state.audioStarted)
{
if (channels != lastChannels || sampleRate != lastSampleRate)
g_state.audioDev->stop();
else
return;
}
lastChannels = channels;
lastSampleRate = sampleRate;
g_state.audioStarted = true;
DEBUG_INFO("%d channels @ %dHz", channels, sampleRate);
g_state.audioDev->start(channels, sampleRate);
}
}
void audioStop(void)
{
if (g_state.audioDev)
g_state.audioDev->stop();
g_state.audioStarted = false;
}
void audioData(uint8_t * data, size_t size)
{
if (g_state.audioDev)
g_state.audioDev->play(data, size);
}
static int lg_run(void)
{
g_cursor.sens = g_params.mouseSens;
@ -921,7 +981,9 @@ static int lg_run(void)
}
// try to connect to the spice server
if (g_params.useSpiceInput || g_params.useSpiceClipboard)
if (g_params.useSpiceInput ||
g_params.useSpiceClipboard ||
g_params.useSpiceAudio)
{
if (g_params.useSpiceClipboard)
spice_set_clipboard_cb(
@ -930,7 +992,14 @@ static int lg_run(void)
cb_spiceRelease,
cb_spiceRequest);
if (!spice_connect(g_params.spiceHost, g_params.spicePort, ""))
if (g_params.useSpiceAudio)
spice_set_audio_cb(
audioStart,
audioStop,
audioData);
if (!spice_connect(g_params.spiceHost, g_params.spicePort, "",
g_params.useSpiceAudio))
{
DEBUG_ERROR("Failed to connect to spice server");
return -1;

View File

@ -24,6 +24,7 @@
#include "dynamic/displayservers.h"
#include "dynamic/renderers.h"
#include "dynamic/audiodev.h"
#include "common/thread.h"
#include "common/types.h"
@ -133,6 +134,9 @@ struct AppState
bool resizeDone;
bool autoIdleInhibitState;
struct LG_AudioDevOps * audioDev;
bool audioStarted;
};
struct AppParams
@ -154,6 +158,7 @@ struct AppParams
LG_RendererRotate winRotate;
bool useSpiceInput;
bool useSpiceClipboard;
bool useSpiceAudio;
const char * spiceHost;
unsigned int spicePort;
bool clipboardToVM;

View File

@ -93,6 +93,9 @@ feature is disabled when running :ref:`cmake <client_building>`.
- libwayland-dev
- wayland-protocols
- Disable with ``cmake -DENABLE_PIPEWIRE=no ..``
- libpipewire-0.3-dev
.. _client_deps_recommended:

View File

@ -6,6 +6,7 @@ cmake
config
dejavu
deuteranope
dev
dir
distros
dmabuf
@ -19,6 +20,7 @@ ini
kvmfr
laggy
libdecor
libpipewire
libvirt
linux
LookingGlass

@ -1 +1 @@
Subproject commit 8d8b47454e29586a1a3e3668aadd942cb5b0cfc0
Subproject commit 3ea156974b65d87ea9c2e21d031af2b60d79074a