output/pipewire: new output plugin
Very rough draft. Barely works.
This commit is contained in:
parent
133c8834df
commit
428f769c38
1
NEWS
1
NEWS
|
@ -3,6 +3,7 @@ ver 0.23 (not yet released)
|
||||||
- new command "getvol"
|
- new command "getvol"
|
||||||
- show the audio format in "playlistinfo"
|
- show the audio format in "playlistinfo"
|
||||||
* output
|
* output
|
||||||
|
- pipewire: new plugin
|
||||||
- snapcast: new plugin
|
- snapcast: new plugin
|
||||||
|
|
||||||
ver 0.22.7 (not yet released)
|
ver 0.22.7 (not yet released)
|
||||||
|
|
|
@ -1054,6 +1054,21 @@ The pipe plugin starts a program and writes raw PCM data into its standard input
|
||||||
* - **command CMD**
|
* - **command CMD**
|
||||||
- This command is invoked with the shell.
|
- This command is invoked with the shell.
|
||||||
|
|
||||||
|
pipewire
|
||||||
|
--------
|
||||||
|
|
||||||
|
Connect to a `PipeWire <https://pipewire.org/>``_ server. Requires
|
||||||
|
``libpipewire``.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Setting
|
||||||
|
- Description
|
||||||
|
* - **target ID**
|
||||||
|
- Link to the given target id.
|
||||||
|
|
||||||
.. _pulse_plugin:
|
.. _pulse_plugin:
|
||||||
|
|
||||||
pulse
|
pulse
|
||||||
|
|
|
@ -360,6 +360,7 @@ subdir('src/lib/gcrypt')
|
||||||
subdir('src/lib/nfs')
|
subdir('src/lib/nfs')
|
||||||
subdir('src/lib/oss')
|
subdir('src/lib/oss')
|
||||||
subdir('src/lib/pcre')
|
subdir('src/lib/pcre')
|
||||||
|
subdir('src/lib/pipewire')
|
||||||
subdir('src/lib/pulse')
|
subdir('src/lib/pulse')
|
||||||
subdir('src/lib/sndio')
|
subdir('src/lib/sndio')
|
||||||
subdir('src/lib/sqlite')
|
subdir('src/lib/sqlite')
|
||||||
|
|
|
@ -174,6 +174,7 @@ option('jack', type: 'feature', description: 'JACK output plugin')
|
||||||
option('openal', type: 'feature', description: 'OpenAL output plugin')
|
option('openal', type: 'feature', description: 'OpenAL output plugin')
|
||||||
option('oss', type: 'feature', description: 'Open Sound System support')
|
option('oss', type: 'feature', description: 'Open Sound System support')
|
||||||
option('pipe', type: 'boolean', value: true, description: 'Pipe output plugin')
|
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('pulse', type: 'feature', description: 'PulseAudio support')
|
||||||
option('recorder', type: 'boolean', value: true, description: 'Recorder output plugin')
|
option('recorder', type: 'boolean', value: true, description: 'Recorder output plugin')
|
||||||
option('shout', type: 'feature', description: 'Shoutcast streaming support using libshout')
|
option('shout', type: 'feature', description: 'Shoutcast streaming support using libshout')
|
||||||
|
|
|
@ -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'],
|
||||||
|
)
|
|
@ -34,6 +34,7 @@
|
||||||
#include "plugins/OssOutputPlugin.hxx"
|
#include "plugins/OssOutputPlugin.hxx"
|
||||||
#include "plugins/OSXOutputPlugin.hxx"
|
#include "plugins/OSXOutputPlugin.hxx"
|
||||||
#include "plugins/PipeOutputPlugin.hxx"
|
#include "plugins/PipeOutputPlugin.hxx"
|
||||||
|
#include "plugins/PipeWireOutputPlugin.hxx"
|
||||||
#include "plugins/PulseOutputPlugin.hxx"
|
#include "plugins/PulseOutputPlugin.hxx"
|
||||||
#include "plugins/RecorderOutputPlugin.hxx"
|
#include "plugins/RecorderOutputPlugin.hxx"
|
||||||
#include "plugins/ShoutOutputPlugin.hxx"
|
#include "plugins/ShoutOutputPlugin.hxx"
|
||||||
|
@ -85,6 +86,9 @@ constexpr const AudioOutputPlugin *audio_output_plugins[] = {
|
||||||
#ifdef ENABLE_SOLARIS_OUTPUT
|
#ifdef ENABLE_SOLARIS_OUTPUT
|
||||||
&solaris_output_plugin,
|
&solaris_output_plugin,
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef ENABLE_PIPEWIRE
|
||||||
|
&pipewire_output_plugin,
|
||||||
|
#endif
|
||||||
#ifdef ENABLE_PULSE
|
#ifdef ENABLE_PULSE
|
||||||
&pulse_output_plugin,
|
&pulse_output_plugin,
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -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 <pipewire/pipewire.h>
|
||||||
|
#include <spa/param/audio/format-utils.h>
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <boost/lockfree/spsc_queue.hpp>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
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<std::byte> *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<std::byte>(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,
|
||||||
|
};
|
|
@ -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
|
|
@ -85,6 +85,10 @@ if enable_pipe_output
|
||||||
output_plugins_sources += 'PipeOutputPlugin.cxx'
|
output_plugins_sources += 'PipeOutputPlugin.cxx'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if pipewire_dep.found()
|
||||||
|
output_plugins_sources += 'PipeWireOutputPlugin.cxx'
|
||||||
|
endif
|
||||||
|
|
||||||
if pulse_dep.found()
|
if pulse_dep.found()
|
||||||
output_plugins_sources += 'PulseOutputPlugin.cxx'
|
output_plugins_sources += 'PulseOutputPlugin.cxx'
|
||||||
endif
|
endif
|
||||||
|
@ -169,6 +173,7 @@ output_plugins = static_library(
|
||||||
apple_dep,
|
apple_dep,
|
||||||
libao_dep,
|
libao_dep,
|
||||||
libjack_dep,
|
libjack_dep,
|
||||||
|
pipewire_dep,
|
||||||
pulse_dep,
|
pulse_dep,
|
||||||
libshout_dep,
|
libshout_dep,
|
||||||
libsndio_dep,
|
libsndio_dep,
|
||||||
|
|
Loading…
Reference in New Issue