From ec2323637072ad8358b0d7d55a708e3056b0047d Mon Sep 17 00:00:00 2001 From: Lukas Rusak Date: Thu, 1 Apr 2021 08:50:48 -0700 Subject: [PATCH] pipewire: add initial support --- Makefile.am | 4 + audio.c | 6 + audio_pw.c | 501 +++++++++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 15 ++ 4 files changed, 526 insertions(+) create mode 100644 audio_pw.c diff --git a/Makefile.am b/Makefile.am index c9ccb75e..0de2c4ba 100644 --- a/Makefile.am +++ b/Makefile.am @@ -80,6 +80,10 @@ if USE_PA shairport_sync_SOURCES += audio_pa.c endif +if USE_PW +shairport_sync_SOURCES += audio_pw.c +endif + if USE_CONVOLUTION shairport_sync_SOURCES += FFTConvolver/AudioFFT.cpp FFTConvolver/FFTConvolver.cpp FFTConvolver/Utilities.cpp FFTConvolver/convolver.cpp AM_CXXFLAGS += -std=c++11 diff --git a/audio.c b/audio.c index 0fe7b04f..10efbd9e 100644 --- a/audio.c +++ b/audio.c @@ -43,6 +43,9 @@ extern audio_output audio_ao; #ifdef CONFIG_SOUNDIO extern audio_output audio_soundio; #endif +#ifdef CONFIG_PW +extern audio_output audio_pw; +#endif #ifdef CONFIG_PA extern audio_output audio_pa; #endif @@ -66,6 +69,9 @@ static audio_output *outputs[] = { #ifdef CONFIG_SNDIO &audio_sndio, #endif +#ifdef CONFIG_PW + &audio_pw, +#endif #ifdef CONFIG_PA &audio_pa, #endif diff --git a/audio_pw.c b/audio_pw.c new file mode 100644 index 00000000..58a5fd72 --- /dev/null +++ b/audio_pw.c @@ -0,0 +1,501 @@ +/* + * Asynchronous Pipewire Backend. This file is part of Shairport Sync. + * Copyright (c) Shairport Sync 2021 + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "audio.h" +#include "common.h" + +#include +#include +#include +#include +#include + +#include + +struct pw_data { + struct pw_thread_loop *mainloop; + struct pw_context *context; + + struct pw_core *core; + struct spa_hook core_listener; + + struct pw_registry *registry; + struct spa_hook registry_listener; + + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct pw_buffer *pw_buffer; + + struct pw_properties *props; + int sync; + + enum spa_audio_format format; + uint32_t rate; + uint32_t channels; + uint32_t stride; + uint32_t latency; + +} data; + +static void on_core_info(__attribute__((unused)) void *userdata, const struct pw_core_info *info) +{ + debug(1, "pw: remote %"PRIu32" is named \"%s\"", info->id, info->name); +} + +static void on_core_error(__attribute__((unused)) void *userdata, uint32_t id, int seq, int res, const char *message) +{ + warn("pw: remote error: id=%"PRIu32" seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .info = on_core_info, + .error = on_core_error, +}; + +static void registry_event_global(__attribute__((unused)) void *userdata, uint32_t id, + __attribute__((unused)) uint32_t permissions, const char *type, __attribute__((unused)) uint32_t version, + const struct spa_dict *props) +{ + const struct spa_dict_item *item; + const char *name, *media_class; + + if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { + name = spa_dict_lookup(props, PW_KEY_NODE_NAME); + media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + + if (!name || !media_class) + return; + + debug(1, "pw: registry: id=%"PRIu32" type=%s name=\"%s\" media_class=\"%s\"", id, type, name, media_class); + + spa_dict_for_each(item, props) { + debug(1, "pw: \t\t%s = \"%s\"", item->key, item->value); + } + } +} + +static void registry_event_global_remove(__attribute__((unused)) void *userdata, uint32_t id) +{ + debug(1, "pw: registry: remove id=%"PRIu32"", id); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error) +{ + struct pw_data *pipewire = userdata; + + debug(1, "pw: stream state changed %s -> %s", pw_stream_state_as_string(old), pw_stream_state_as_string(state)); + + if (state == PW_STREAM_STATE_STREAMING) + debug(1, "pw: stream node %"PRIu32"", pw_stream_get_node_id(pipewire->stream)); + + if (state == PW_STREAM_STATE_ERROR) + debug(1, "pw: stream node %"PRIu32" error: %s", pw_stream_get_node_id(pipewire->stream), error); + + pw_thread_loop_signal(pipewire->mainloop, 0); +} + +static void on_process(void *userdata) +{ + struct pw_data *pipewire = userdata; + + pw_thread_loop_signal(pipewire->mainloop, 0); +} + +static void on_drained(void *userdata) +{ + struct pw_data *pipewire = userdata; + + pw_stream_set_active(pipewire->stream, false); + + pw_thread_loop_signal(pipewire->mainloop, 0); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_state_changed, + .process = on_process, + .drained = on_drained, +}; + +static void deinit() { + pw_thread_loop_stop(data.mainloop); + + if (data.stream) { + pw_stream_destroy(data.stream); + data.stream = NULL; + } + + if (data.registry) { + pw_proxy_destroy((struct pw_proxy*)data.registry); + data.registry = NULL; + } + + if (data.core) { + pw_core_disconnect(data.core); + data.core = NULL; + } + + if (data.context) { + pw_context_destroy(data.context); + data.context = NULL; + } + + if (data.mainloop) { + pw_thread_loop_destroy(data.mainloop); + data.mainloop = NULL; + } + + if (data.props) { + pw_properties_free(data.props); + data.props = NULL; + } +} + +static int init(__attribute__((unused)) int argc, __attribute__((unused)) char** argv) { + + struct pw_loop *loop; + struct pw_properties* props; + + // set up default values first + config.audio_backend_buffer_desired_length = 0.35; + config.audio_backend_buffer_interpolation_threshold_in_seconds = 0.02; + config.audio_backend_latency_offset = 0; + + pw_init(NULL, NULL); + + debug(1, "pw: compiled with libpipewire %s", pw_get_headers_version()); + debug(1, "pw: linked with libpipewire: %s", pw_get_library_version()); + + data.props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + PW_KEY_APP_NAME, "shairport-sync", + PW_KEY_NODE_NAME, "shairport-sync", + NULL); + + if (!data.props) { + deinit(); + die("pw: pw_properties_new() failed: %m"); + } + + data.mainloop = pw_thread_loop_new("pipewire", NULL); + if (!data.mainloop) { + deinit(); + die("pw: pw_thread_loop_new_full() failed: %m"); + } + + props = pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL); + if (!props) { + deinit(); + die("pw: pw_properties_new() failed: %m"); + } + + loop = pw_thread_loop_get_loop(data.mainloop); + + data.context = pw_context_new(loop, props, 0); + if (!data.context) { + deinit(); + die("pw: pw_context_new() failed: %m"); + } + + props = pw_properties_new(PW_KEY_REMOTE_NAME, NULL, NULL); + if (!props) { + deinit(); + die("pw: pw_properties_new() failed: %m"); + } + + pw_thread_loop_lock(data.mainloop); + + if (pw_thread_loop_start(data.mainloop) != 0) { + deinit(); + die("pw: pw_thread_loop_start() failed: %m"); + } + + data.core = pw_context_connect(data.context, props, 0); + if (!data.core) { + deinit(); + die("pw: pw_context_connect() failed: %m"); + } + + pw_core_add_listener(data.core, &data.core_listener, &core_events, &data); + + data.registry = pw_core_get_registry(data.core, PW_VERSION_REGISTRY, 0); + if (!data.registry) { + deinit(); + die("pw: pw_core_get_registry() failed: %m"); + } + + pw_registry_add_listener(data.registry, &data.registry_listener, ®istry_events, &data); + + data.sync = pw_core_sync(data.core, 0, data.sync); + + pw_thread_loop_unlock(data.mainloop); + + return 0; +} + +static enum spa_audio_format sps_format_to_spa_format(sps_format_t sps_format) { + switch (sps_format) { + case SPS_FORMAT_S8: + return SPA_AUDIO_FORMAT_S8; + case SPS_FORMAT_U8: + return SPA_AUDIO_FORMAT_U8; + case SPS_FORMAT_S16: + return SPA_AUDIO_FORMAT_S16; + case SPS_FORMAT_S16_LE: + return SPA_AUDIO_FORMAT_S16_LE; + case SPS_FORMAT_S16_BE: + return SPA_AUDIO_FORMAT_S16_BE; + case SPS_FORMAT_S24: + return SPA_AUDIO_FORMAT_S24_32; + case SPS_FORMAT_S24_LE: + return SPA_AUDIO_FORMAT_S24_32_LE; + case SPS_FORMAT_S24_BE: + return SPA_AUDIO_FORMAT_S24_32_BE; + case SPS_FORMAT_S24_3LE: + return SPA_AUDIO_FORMAT_S24_LE; + case SPS_FORMAT_S24_3BE: + return SPA_AUDIO_FORMAT_S24_BE; + case SPS_FORMAT_S32: + return SPA_AUDIO_FORMAT_S32; + case SPS_FORMAT_S32_LE: + return SPA_AUDIO_FORMAT_S32_LE; + case SPS_FORMAT_S32_BE: + return SPA_AUDIO_FORMAT_S32_BE; + + case SPS_FORMAT_UNKNOWN: + case SPS_FORMAT_AUTO: + case SPS_FORMAT_INVALID: + default: + return SPA_AUDIO_FORMAT_S16; + } +} + +static int spa_format_samplesize(enum spa_audio_format audio_format) +{ + switch(audio_format) { + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_U8: + return 1; + case SPA_AUDIO_FORMAT_S16: + return 2; + case SPA_AUDIO_FORMAT_S24: + return 3; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S32: + return 4; + default: + die("pw: unhandled spa_audio_format: %d", audio_format); + return -1; + } +} + +static const char* spa_format_to_str(enum spa_audio_format audio_format) +{ + switch(audio_format) { + case SPA_AUDIO_FORMAT_U8: + return "u8"; + case SPA_AUDIO_FORMAT_S8: + return "s8"; + case SPA_AUDIO_FORMAT_S16: + return "s16"; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_32: + return "s24"; + case SPA_AUDIO_FORMAT_S32: + return "s32"; + default: + die("pw: unhandled spa_audio_format: %d", audio_format); + return "(invalid)"; + } +} + +static void start(int sample_rate, int sample_format) { + + pw_thread_loop_lock(data.mainloop); + + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + struct spa_audio_info_raw info; + uint32_t nom; + int ret; + + data.format = sps_format_to_spa_format(sample_format); + data.rate = sample_rate; + data.channels = 2; + data.stride = spa_format_samplesize(data.format) * data.channels; + data.latency = 20000; + + nom = nearbyint((data.latency * data.rate) / 1000000.0); + + pw_properties_setf(data.props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data.rate); + + debug(1, "pw: rate: %d", data.rate); + debug(1, "pw: channgels: %d", data.channels); + debug(1, "pw: format: %s", spa_format_to_str(data.format)); + debug(1, "pw: samplesize: %d", spa_format_samplesize(data.format)); + debug(1, "pw: stride: %d", data.stride); + debug(1, "pw: latency: %d samples (%.3fs)", nom, (double)nom/data.rate); + + info = SPA_AUDIO_INFO_RAW_INIT( + .flags = SPA_AUDIO_FLAG_NONE, + .format = data.format, + .rate = data.rate, + .channels = data.channels); + + params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &info); + + data.stream = pw_stream_new(data.core, "shairport-sync", data.props); + + if (!data.stream) { + deinit(); + die("pw: pw_stream_new() failed: %m"); + } + + debug(1, "pw: connecting stream: target_id=%"PRIu32"", PW_ID_ANY); + + pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data); + + ret = pw_stream_connect(data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, + params, 1); + + if (ret < 0) { + deinit(); + die("pw: pw_stream_connect() failed: %s", spa_strerror(ret)); + } + + const struct pw_properties *props; + void *pstate; + const char *key, *val; + + if ((props = pw_stream_get_properties(data.stream)) != NULL) { + debug(1, "pw: stream properties:"); + pstate = NULL; + while ((key = pw_properties_iterate(props, &pstate)) != NULL && + (val = pw_properties_get(props, key)) != NULL) { + debug(1, "pw: \t%s = \"%s\"", key, val); + } + } + + while(1) { + enum pw_stream_state stream_state = pw_stream_get_state(data.stream, NULL); + if (stream_state == PW_STREAM_STATE_PAUSED) + break; + + pw_thread_loop_wait(data.mainloop); + } + + pw_thread_loop_unlock(data.mainloop); +} + +static void stop() { + pw_thread_loop_lock(data.mainloop); + + pw_stream_flush(data.stream, true); + + pw_thread_loop_unlock(data.mainloop); +} + +static void flush() { + pw_thread_loop_lock(data.mainloop); + + pw_stream_flush(data.stream, false); + + pw_thread_loop_unlock(data.mainloop); +} + +static int play(void *buf, int samples) { + struct pw_buffer *pw_buffer; + struct spa_buffer *spa_buffer; + struct spa_data *spa_data; + + pw_thread_loop_lock(data.mainloop); + + if (pw_stream_get_state(data.stream, NULL) == PW_STREAM_STATE_PAUSED) + pw_stream_set_active(data.stream, true); + + while (pw_buffer == NULL) { + pw_buffer = pw_stream_dequeue_buffer(data.stream); + if (pw_buffer) + break; + + pw_thread_loop_wait(data.mainloop); + } + + spa_buffer = pw_buffer->buffer; + spa_data = &spa_buffer->datas[0]; + + size_t bytes_to_copy = samples * data.stride; + + debug(3, "pw: bytes_to_copy: %d", bytes_to_copy); + + if (spa_data->maxsize < bytes_to_copy) + bytes_to_copy = spa_data->maxsize; + + debug(3, "pw: spa_data->maxsize: %d", spa_data->maxsize); + + memcpy(spa_data->data, buf, bytes_to_copy); + + spa_data->chunk->offset = 0; + spa_data->chunk->stride = data.stride; + spa_data->chunk->size = bytes_to_copy; + + pw_stream_queue_buffer(data.stream, pw_buffer); + + pw_thread_loop_unlock(data.mainloop); + + return 0; +} + +audio_output audio_pw = { + .name = "pw", + .help = NULL, + .init = &init, + .deinit = &deinit, + .prepare = NULL, + .start = &start, + .stop = &stop, + .is_running = NULL, + .flush = &flush, + .delay = NULL, + .play = &play, + .volume = NULL, + .parameters = NULL, + .mute = NULL +}; diff --git a/configure.ac b/configure.ac index 21ba3cec..34701f84 100644 --- a/configure.ac +++ b/configure.ac @@ -299,6 +299,21 @@ if test "x$with_pa" = "xyes" ; then fi AM_CONDITIONAL([USE_PA], [test "x$with_pa" = "xyes"]) +# Look for pipewire flag +AC_ARG_WITH(pw, [AS_HELP_STRING([--with-pw],[choose Pipewire support.])]) +if test "x$with_pw" = "xyes" ; then + AC_DEFINE([CONFIG_PW], 1, [Include Pipewire support.]) + if test "x${with_pkg_config}" = xyes ; then + PKG_CHECK_MODULES( + [PIPEWIRE], [libpipewire-0.3 >= 0.3.24], + [CFLAGS="${PIPEWIRE_CFLAGS} ${CFLAGS} -Wno-missing-field-initializers" LIBS="${PIPEWIRE_LIBS} ${LIBS}"], + [AC_MSG_ERROR(Pipewire support requires the libpipewire-dev library!)]) + else + AC_CHECK_LIB([pipewire], [pw_stream_queue_buffer], , AC_MSG_ERROR(Pipewire support requires the libpipewire-dev library.)) + fi +fi +AM_CONDITIONAL([USE_PW], [test "x$with_pw" = "xyes"]) + # Look for Convolution flag AC_ARG_WITH(convolution, [AS_HELP_STRING([--with-convolution],[choose audio DSP convolution support])]) if test "x$with_convolution" = "xyes" ; then -- 2.47.2