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,