mirror of
https://github.com/gnif/LookingGlass.git
synced 2024-11-10 00:28:20 +00:00
[client] audio: initial addition of PipeWire audio support via SPICE
This commit is contained in:
parent
8ba4b56dba
commit
e810577317
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -43,7 +43,8 @@ jobs:
|
||||
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
|
||||
-DCMAKE_LINKER:FILEPATH=/usr/bin/ld \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DENABLE_LIBDECOR=${{ matrix.wayland_shell == 'libdecor' }} \
|
||||
-DENABLE_LIBDECOR=${{ matrix.wayland_shell == 'libdecor' }} \
|
||||
-DENABLE_PIPEWIRE=0 \
|
||||
..
|
||||
- name: Build client
|
||||
run: |
|
||||
|
@ -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
|
||||
|
43
client/audiodevs/CMakeLists.txt
Normal file
43
client/audiodevs/CMakeLists.txt
Normal 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})
|
21
client/audiodevs/PipeWire/CMakeLists.txt
Normal file
21
client/audiodevs/PipeWire/CMakeLists.txt
Normal 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
|
||||
)
|
220
client/audiodevs/PipeWire/pipewire.c
Normal file
220
client/audiodevs/PipeWire/pipewire.c
Normal 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
|
||||
};
|
63
client/include/interface/audiodev.h
Normal file
63
client/include/interface/audiodev.h
Normal 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
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user