From 428f769c38da9f53059931aa5d722ce072344945 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Wed, 3 Mar 2021 15:15:24 +0100 Subject: [PATCH] output/pipewire: new output plugin Very rough draft. Barely works. --- NEWS | 1 + doc/plugins.rst | 15 + meson.build | 1 + meson_options.txt | 1 + src/lib/pipewire/meson.build | 14 + src/output/Registry.cxx | 4 + src/output/plugins/PipeWireOutputPlugin.cxx | 297 ++++++++++++++++++++ src/output/plugins/PipeWireOutputPlugin.hxx | 25 ++ src/output/plugins/meson.build | 5 + 9 files changed, 363 insertions(+) create mode 100644 src/lib/pipewire/meson.build create mode 100644 src/output/plugins/PipeWireOutputPlugin.cxx create mode 100644 src/output/plugins/PipeWireOutputPlugin.hxx diff --git a/NEWS b/NEWS index 63ea2db3b..3bb6cf8a8 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ ver 0.23 (not yet released) - new command "getvol" - show the audio format in "playlistinfo" * output + - pipewire: new plugin - snapcast: new plugin ver 0.22.7 (not yet released) diff --git a/doc/plugins.rst b/doc/plugins.rst index f85692ae4..f1468f278 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -1054,6 +1054,21 @@ The pipe plugin starts a program and writes raw PCM data into its standard input * - **command CMD** - This command is invoked with the shell. +pipewire +-------- + +Connect to a `PipeWire ``_ server. Requires +``libpipewire``. + +.. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Setting + - Description + * - **target ID** + - Link to the given target id. + .. _pulse_plugin: pulse diff --git a/meson.build b/meson.build index e9e95a7e1..0eb9a2a03 100644 --- a/meson.build +++ b/meson.build @@ -360,6 +360,7 @@ subdir('src/lib/gcrypt') subdir('src/lib/nfs') subdir('src/lib/oss') subdir('src/lib/pcre') +subdir('src/lib/pipewire') subdir('src/lib/pulse') subdir('src/lib/sndio') subdir('src/lib/sqlite') diff --git a/meson_options.txt b/meson_options.txt index e327f6bdd..23850eb29 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -174,6 +174,7 @@ option('jack', type: 'feature', description: 'JACK output plugin') option('openal', type: 'feature', description: 'OpenAL output plugin') option('oss', type: 'feature', description: 'Open Sound System support') option('pipe', type: 'boolean', value: true, description: 'Pipe output plugin') +option('pipewire', type: 'feature', description: 'PipeWire support') option('pulse', type: 'feature', description: 'PulseAudio support') option('recorder', type: 'boolean', value: true, description: 'Recorder output plugin') option('shout', type: 'feature', description: 'Shoutcast streaming support using libshout') diff --git a/src/lib/pipewire/meson.build b/src/lib/pipewire/meson.build new file mode 100644 index 000000000..0a98ed82f --- /dev/null +++ b/src/lib/pipewire/meson.build @@ -0,0 +1,14 @@ +pipewire_dep = dependency('libpipewire-0.3', required: get_option('pipewire')) +conf.set('ENABLE_PIPEWIRE', pipewire_dep.found()) +if not pipewire_dep.found() + subdir_done() +endif + +pipewire_dep = declare_dependency( + dependencies: pipewire_dep, + + # disabling -Wpedantic because libpipewire's headers are not + # compatible with C++; using the "#pragma" is not enough, we need to + # disable it at the command line + compile_args: ['-Wno-pedantic'], +) diff --git a/src/output/Registry.cxx b/src/output/Registry.cxx index b5977c352..5059998ed 100644 --- a/src/output/Registry.cxx +++ b/src/output/Registry.cxx @@ -34,6 +34,7 @@ #include "plugins/OssOutputPlugin.hxx" #include "plugins/OSXOutputPlugin.hxx" #include "plugins/PipeOutputPlugin.hxx" +#include "plugins/PipeWireOutputPlugin.hxx" #include "plugins/PulseOutputPlugin.hxx" #include "plugins/RecorderOutputPlugin.hxx" #include "plugins/ShoutOutputPlugin.hxx" @@ -85,6 +86,9 @@ constexpr const AudioOutputPlugin *audio_output_plugins[] = { #ifdef ENABLE_SOLARIS_OUTPUT &solaris_output_plugin, #endif +#ifdef ENABLE_PIPEWIRE + &pipewire_output_plugin, +#endif #ifdef ENABLE_PULSE &pulse_output_plugin, #endif diff --git a/src/output/plugins/PipeWireOutputPlugin.cxx b/src/output/plugins/PipeWireOutputPlugin.cxx new file mode 100644 index 000000000..7eb239884 --- /dev/null +++ b/src/output/plugins/PipeWireOutputPlugin.cxx @@ -0,0 +1,297 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "PipeWireOutputPlugin.hxx" +//#include "lib/pipewire/MainLoop.hxx" +#include "../OutputAPI.hxx" +#include "../Error.hxx" +#include "thread/Thread.hxx" + +#ifdef __GNUC__ +#pragma GCC diagnostic push +/* oh no, libspa likes to cast away "const"! */ +#pragma GCC diagnostic ignored "-Wcast-qual" +/* suppress more annoying warnings */ +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + +#include +#include + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#include + +#include + +class PipeWireOutput final : AudioOutput { + Thread thread{BIND_THIS_METHOD(RunThread)}; + struct pw_main_loop *loop; + struct pw_stream *stream; + + std::byte buffer[1024]; + struct spa_pod_builder pod_builder; + + std::size_t frame_size; + + boost::lockfree::spsc_queue *ring_buffer; + + const uint32_t target_id; + + volatile bool interrupted; + + explicit PipeWireOutput(const ConfigBlock &block); + +public: + static AudioOutput *Create(EventLoop &, + const ConfigBlock &block) { + pw_init(0, nullptr); + + return new PipeWireOutput(block); + } + + static constexpr struct pw_stream_events MakeStreamEvents() noexcept { + struct pw_stream_events events{}; + events.version = PW_VERSION_STREAM_EVENTS; + events.process = Process; + return events; + } + +private: + void Process() noexcept; + + static void Process(void *data) noexcept { + auto &o = *(PipeWireOutput *)data; + o.Process(); + } + + void RunThread() noexcept { + pw_main_loop_run(loop); + } + + /* virtual methods from class AudioOutput */ + void Enable() override; + void Disable() noexcept override; + + void Open(AudioFormat &audio_format) override; + void Close() noexcept override; + + void Interrupt() noexcept override { + interrupted = true; + } + + size_t Play(const void *chunk, size_t size) override; + + // TODO: void Drain() override; + // TODO: void Cancel() noexcept override; + // TODO: bool Pause() noexcept override; +}; + +static constexpr auto stream_events = PipeWireOutput::MakeStreamEvents(); + +inline +PipeWireOutput::PipeWireOutput(const ConfigBlock &block) + :AudioOutput(FLAG_ENABLE_DISABLE), + target_id(block.GetBlockValue("target", unsigned(PW_ID_ANY))) +{ +} + +void +PipeWireOutput::Enable() +{ + loop = pw_main_loop_new(nullptr); + if (loop == nullptr) + throw std::runtime_error("pw_main_loop_new() failed"); + + try { + thread.Start(); + } catch (...) { + pw_main_loop_destroy(loop); + throw; + } +} + +void +PipeWireOutput::Disable() noexcept +{ + pw_main_loop_quit(loop); + thread.Join(); + + pw_main_loop_destroy(loop); +} + +static constexpr enum spa_audio_format +ToPipeWireSampleFormat(SampleFormat format) noexcept +{ + switch (format) { + case SampleFormat::UNDEFINED: + break; + + case SampleFormat::S8: + return SPA_AUDIO_FORMAT_S8; + + case SampleFormat::S16: + return SPA_AUDIO_FORMAT_S16; + + case SampleFormat::S24_P32: + return SPA_AUDIO_FORMAT_S24_32; + + case SampleFormat::S32: + return SPA_AUDIO_FORMAT_S32; + + case SampleFormat::FLOAT: + return SPA_AUDIO_FORMAT_F32; + + case SampleFormat::DSD: + break; + } + + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static struct spa_audio_info_raw +ToPipeWireAudioFormat(AudioFormat &audio_format) noexcept +{ + struct spa_audio_info_raw raw{}; + + raw.format = ToPipeWireSampleFormat(audio_format.format); + if (raw.format == SPA_AUDIO_FORMAT_UNKNOWN) { + raw.format = SPA_AUDIO_FORMAT_S16; + audio_format.format = SampleFormat::S16; + } + + raw.flags = SPA_AUDIO_FLAG_NONE; + raw.rate = audio_format.sample_rate; + raw.channels = audio_format.channels; + + raw.flags |= SPA_AUDIO_FLAG_UNPOSITIONED; // TODO + // TODO raw.position[] + + return raw; +} + +void +PipeWireOutput::Open(AudioFormat &audio_format) +{ + auto props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + PW_KEY_APP_NAME, "Music Player Daemon", + PW_KEY_NODE_NAME, "mpd", + nullptr); + + stream = pw_stream_new_simple(pw_main_loop_get_loop(loop), + "mpd", + props, + &stream_events, + this); + if (stream == nullptr) + throw std::runtime_error("pw_stream_new_simple() failed"); + + auto raw = ToPipeWireAudioFormat(audio_format); + + frame_size = audio_format.GetFrameSize(); + interrupted = false; + + /* allocate a ring buffer of 1 second */ + ring_buffer = new boost::lockfree::spsc_queue(frame_size * + audio_format.sample_rate); + + const struct spa_pod *params[1]; + + pod_builder = {}; + pod_builder.data = buffer; + pod_builder.size = sizeof(buffer); + params[0] = spa_format_audio_raw_build(&pod_builder, + SPA_PARAM_EnumFormat, &raw); + + pw_stream_connect(stream, + PW_DIRECTION_OUTPUT, + target_id, + (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS), + params, 1); +} + +void +PipeWireOutput::Close() noexcept +{ + pw_stream_destroy(stream); + + // TODO synchronize with Process()? + delete ring_buffer; +} + +inline void +PipeWireOutput::Process() noexcept +{ + auto *b = pw_stream_dequeue_buffer(stream); + if (b == nullptr) { + pw_log_warn("out of buffers: %m"); + return; + } + + auto *buf = b->buffer; + std::byte *dest = (std::byte *)buf->datas[0].data; + if (dest == nullptr) + return; + + const std::size_t max_frames = buf->datas[0].maxsize / frame_size; + const std::size_t max_size = max_frames * frame_size; + + size_t nbytes = ring_buffer->pop(dest, max_size); + if (nbytes == 0) { + pw_stream_flush(stream, true); + return; + } + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = frame_size; + buf->datas[0].chunk->size = nbytes; + + pw_stream_queue_buffer(stream, b); +} + +size_t +PipeWireOutput::Play(const void *chunk, size_t size) +{ + while (true) { + std::size_t bytes_written = + ring_buffer->push((const std::byte *)chunk, size); + if (bytes_written > 0) + return bytes_written; + + if (interrupted) + throw AudioOutputInterrupted{}; + + usleep(1000); // TODO + } + + return size; +} + +const struct AudioOutputPlugin pipewire_output_plugin = { + "pipewire", + nullptr, + &PipeWireOutput::Create, + nullptr, +}; diff --git a/src/output/plugins/PipeWireOutputPlugin.hxx b/src/output/plugins/PipeWireOutputPlugin.hxx new file mode 100644 index 000000000..cf942ec64 --- /dev/null +++ b/src/output/plugins/PipeWireOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_PIPEWIRE_OUTPUT_PLUGIN_HXX +#define MPD_PIPEWIRE_OUTPUT_PLUGIN_HXX + +extern const struct AudioOutputPlugin pipewire_output_plugin; + +#endif diff --git a/src/output/plugins/meson.build b/src/output/plugins/meson.build index d0810fc47..a7534ff17 100644 --- a/src/output/plugins/meson.build +++ b/src/output/plugins/meson.build @@ -85,6 +85,10 @@ if enable_pipe_output output_plugins_sources += 'PipeOutputPlugin.cxx' endif +if pipewire_dep.found() + output_plugins_sources += 'PipeWireOutputPlugin.cxx' +endif + if pulse_dep.found() output_plugins_sources += 'PulseOutputPlugin.cxx' endif @@ -169,6 +173,7 @@ output_plugins = static_library( apple_dep, libao_dep, libjack_dep, + pipewire_dep, pulse_dep, libshout_dep, libsndio_dep,