mpd/src/output/plugins/PipeWireOutputPlugin.cxx
2023-03-06 18:44:20 +01:00

975 lines
23 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#include "PipeWireOutputPlugin.hxx"
#include "lib/pipewire/Error.hxx"
#include "lib/pipewire/ThreadLoop.hxx"
#include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "mixer/plugins/PipeWireMixerPlugin.hxx"
#include "pcm/Silence.hxx"
#include "lib/fmt/ExceptionFormatter.hxx"
#include "system/Error.hxx"
#include "util/BitReverse.hxx"
#include "util/Domain.hxx"
#include "util/RingBuffer.hxx"
#include "util/ScopeExit.hxx"
#include "util/StaticVector.hxx"
#include "util/StringCompare.hxx"
#include "Log.hxx"
#include "tag/Format.hxx"
#include "config.h" // for ENABLE_DSD
#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>
#include <spa/param/props.h>
#include <cmath>
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#include <algorithm>
#include <array>
#include <numeric>
#include <stdexcept>
#include <string>
static constexpr Domain pipewire_output_domain("pipewire_output");
class PipeWireOutput final : AudioOutput {
const char *const name;
const char *const remote;
const char *const target;
struct pw_thread_loop *thread_loop = nullptr;
struct pw_stream *stream;
std::string error_message;
std::byte pod_buffer[1024];
struct spa_pod_builder pod_builder;
std::size_t frame_size;
/**
* This buffer passes PCM data from Play() to Process().
*/
using RingBuffer = ::RingBuffer<std::byte>;
RingBuffer ring_buffer;
uint32_t target_id = PW_ID_ANY;
/**
* The current volume level (0.0 .. 1.0).
*
* This get initialized to -1 which means "unknown", so
* restore_volume will not attempt to override PipeWire's
* initial volume level.
*/
float volume = -1;
PipeWireMixer *mixer = nullptr;
unsigned channels;
/**
* The active sample format, needed for PcmSilence().
*/
SampleFormat sample_format;
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
/**
* Is the "dsd" setting enabled, i.e. is DSD playback allowed?
*/
const bool enable_dsd;
/**
* Are we currently playing in native DSD mode?
*/
bool use_dsd;
/**
* Reverse the 8 bits in each DSD byte? This is necessary if
* PipeWire wants LSB (because MPD uses MSB internally).
*/
bool dsd_reverse_bits;
/**
* Pack this many bytes of each frame together. MPD uses 1
* internally, and if PipeWire wants more than one
* (e.g. because it uses DSD_U32), we need to reorder bytes.
*/
uint_least8_t dsd_interleave;
#endif
bool disconnected;
/**
* Shall the previously known volume be restored as soon as
* PW_STREAM_STATE_STREAMING is reached? This needs to be
* done each time after the pw_stream got created, thus this
* flag gets set by Open().
*/
bool restore_volume;
bool interrupted;
bool paused;
/**
* Is the PipeWire stream active, i.e. has
* pw_stream_set_active() been called successfully?
*/
bool active;
/**
* Has Drain() been called? This causes Process() to invoke
* pw_stream_flush() to drain PipeWire as soon as the
* #ring_buffer has been drained.
*/
bool drain_requested;
bool drained;
explicit PipeWireOutput(const ConfigBlock &block);
public:
static AudioOutput *Create(EventLoop &,
const ConfigBlock &block) {
pw_init(nullptr, 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.state_changed = StateChanged;
events.process = Process;
events.drained = Drained;
events.control_info = ControlInfo;
events.param_changed = ParamChanged;
return events;
}
void SetVolume(float volume);
void SetMixer(PipeWireMixer &_mixer) noexcept;
void ClearMixer([[maybe_unused]] PipeWireMixer &old_mixer) noexcept {
assert(mixer == &old_mixer);
mixer = nullptr;
}
private:
void CheckThrowError() {
if (disconnected) {
if (error_message.empty())
throw std::runtime_error("Disconnected from PipeWire");
else
throw std::runtime_error(error_message);
}
}
void StateChanged(enum pw_stream_state state,
const char *error) noexcept;
static void StateChanged(void *data,
[[maybe_unused]] enum pw_stream_state old,
enum pw_stream_state state,
const char *error) noexcept {
auto &o = *(PipeWireOutput *)data;
o.StateChanged(state, error);
}
void Process() noexcept;
static void Process(void *data) noexcept {
auto &o = *(PipeWireOutput *)data;
o.Process();
}
void Drained() noexcept {
drained = true;
pw_thread_loop_signal(thread_loop, false);
}
static void Drained(void *data) noexcept {
auto &o = *(PipeWireOutput *)data;
o.Drained();
}
void OnChannelVolumes(const struct pw_stream_control &control) noexcept {
if (control.n_values < 1)
return;
float sum = std::accumulate(control.values,
control.values + control.n_values,
0.0f);
volume = std::cbrt(sum / control.n_values);
if (mixer != nullptr)
pipewire_mixer_on_change(*mixer, volume);
pw_thread_loop_signal(thread_loop, false);
}
void ControlInfo([[maybe_unused]] uint32_t id,
const struct pw_stream_control &control) noexcept {
switch (id) {
case SPA_PROP_channelVolumes:
OnChannelVolumes(control);
break;
}
}
static void ControlInfo(void *data, uint32_t id,
const struct pw_stream_control *control) noexcept {
auto &o = *(PipeWireOutput *)data;
o.ControlInfo(id, *control);
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
void DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept;
void DsdFormatChanged(const struct spa_pod &param) noexcept;
#endif
void ParamChanged(uint32_t id, const struct spa_pod *param) noexcept;
static void ParamChanged(void *data,
uint32_t id,
const struct spa_pod *param) noexcept
{
if (id != SPA_PARAM_Format || param == nullptr)
return;
auto &o = *(PipeWireOutput *)data;
o.ParamChanged(id, param);
}
/* 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 {
if (thread_loop == nullptr)
return;
const PipeWire::ThreadLoopLock lock(thread_loop);
interrupted = true;
pw_thread_loop_signal(thread_loop, false);
}
[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
std::size_t Play(std::span<const std::byte> src) override;
void Drain() override;
void Cancel() noexcept override;
bool Pause() noexcept override;
void SendTag(const Tag &tag) override;
};
static constexpr auto stream_events = PipeWireOutput::MakeStreamEvents();
inline
PipeWireOutput::PipeWireOutput(const ConfigBlock &block)
:AudioOutput(FLAG_ENABLE_DISABLE),
name(block.GetBlockValue("name", "pipewire")),
remote(block.GetBlockValue("remote", nullptr)),
target(block.GetBlockValue("target", nullptr))
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
, enable_dsd(block.GetBlockValue("dsd", false))
#endif
{
if (target != nullptr) {
if (StringIsEmpty(target))
throw std::runtime_error("target must not be empty");
char *endptr;
const auto _target_id = strtoul(target, &endptr, 10);
if (endptr > target && *endptr == 0)
/* numeric value means target_id, not target
name */
target_id = (uint32_t)_target_id;
}
}
/**
* Throws on error.
*
* @param volume a volume level between 0.0 and 1.0
*/
static void
SetVolume(struct pw_stream &stream, unsigned channels, float volume)
{
float value[MAX_CHANNELS];
std::fill_n(value, channels, volume * volume * volume);
if (pw_stream_set_control(&stream,
SPA_PROP_channelVolumes, channels, value,
0) != 0)
throw std::runtime_error("pw_stream_set_control() failed");
}
void
PipeWireOutput::SetVolume(float _volume)
{
if (thread_loop == nullptr) {
/* the mixer is open (because it is a "global" mixer),
but Enable() on this output has not yet been
called */
volume = _volume;
return;
}
const PipeWire::ThreadLoopLock lock(thread_loop);
if (stream != nullptr && !restore_volume)
::SetVolume(*stream, channels, _volume);
volume = _volume;
}
void
PipeWireOutput::Enable()
{
thread_loop = pw_thread_loop_new(name, nullptr);
if (thread_loop == nullptr)
throw MakeErrno("pw_thread_loop_new() failed");
pw_thread_loop_start(thread_loop);
stream = nullptr;
}
void
PipeWireOutput::Disable() noexcept
{
pw_thread_loop_destroy(thread_loop);
thread_loop = nullptr;
}
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;
/* MPD uses the FLAC channel assignment
(https://xiph.org/flac/format.html) */
switch (audio_format.channels) {
case 1:
raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
break;
case 2:
raw.position[0] = SPA_AUDIO_CHANNEL_FL;
raw.position[1] = SPA_AUDIO_CHANNEL_FR;
break;
case 3:
raw.position[0] = SPA_AUDIO_CHANNEL_FL;
raw.position[1] = SPA_AUDIO_CHANNEL_FR;
raw.position[2] = SPA_AUDIO_CHANNEL_FC;
break;
case 4:
raw.position[0] = SPA_AUDIO_CHANNEL_FL;
raw.position[1] = SPA_AUDIO_CHANNEL_FR;
raw.position[2] = SPA_AUDIO_CHANNEL_RL;
raw.position[3] = SPA_AUDIO_CHANNEL_RR;
break;
case 5:
raw.position[0] = SPA_AUDIO_CHANNEL_FL;
raw.position[1] = SPA_AUDIO_CHANNEL_FR;
raw.position[2] = SPA_AUDIO_CHANNEL_FC;
raw.position[3] = SPA_AUDIO_CHANNEL_RL;
raw.position[4] = SPA_AUDIO_CHANNEL_RR;
break;
case 6:
raw.position[0] = SPA_AUDIO_CHANNEL_FL;
raw.position[1] = SPA_AUDIO_CHANNEL_FR;
raw.position[2] = SPA_AUDIO_CHANNEL_FC;
raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
raw.position[4] = SPA_AUDIO_CHANNEL_RL;
raw.position[5] = SPA_AUDIO_CHANNEL_RR;
break;
case 7:
raw.position[0] = SPA_AUDIO_CHANNEL_FL;
raw.position[1] = SPA_AUDIO_CHANNEL_FR;
raw.position[2] = SPA_AUDIO_CHANNEL_FC;
raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
raw.position[4] = SPA_AUDIO_CHANNEL_RC;
raw.position[5] = SPA_AUDIO_CHANNEL_SL;
raw.position[6] = SPA_AUDIO_CHANNEL_SR;
break;
case 8:
raw.position[0] = SPA_AUDIO_CHANNEL_FL;
raw.position[1] = SPA_AUDIO_CHANNEL_FR;
raw.position[2] = SPA_AUDIO_CHANNEL_FC;
raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
raw.position[4] = SPA_AUDIO_CHANNEL_RL;
raw.position[5] = SPA_AUDIO_CHANNEL_RR;
raw.position[6] = SPA_AUDIO_CHANNEL_SL;
raw.position[7] = SPA_AUDIO_CHANNEL_SR;
break;
default:
raw.flags |= SPA_AUDIO_FLAG_UNPOSITIONED;
}
return raw;
}
void
PipeWireOutput::Open(AudioFormat &audio_format)
{
error_message.clear();
disconnected = false;
restore_volume = true;
paused = false;
/* stay inactive (PW_STREAM_FLAG_INACTIVE) until the ring
buffer has been filled */
active = false;
drain_requested = false;
drained = true;
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_APP_ICON_NAME, "mpd",
nullptr);
pw_properties_setf(props, PW_KEY_NODE_NAME, "mpd.%s", name);
if (remote != nullptr && target_id == PW_ID_ANY)
pw_properties_setf(props, PW_KEY_REMOTE_NAME, "%s", remote);
if (target != nullptr && target_id == PW_ID_ANY)
pw_properties_setf(props,
#if PW_CHECK_VERSION(0, 3, 64)
PW_KEY_TARGET_OBJECT,
#else
PW_KEY_NODE_TARGET,
#endif
"%s", target);
#ifdef PW_KEY_NODE_RATE
/* ask PipeWire to change the graph sample rate to ours
(requires PipeWire 0.3.32) */
pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u",
audio_format.sample_rate);
#endif
const PipeWire::ThreadLoopLock lock(thread_loop);
stream = pw_stream_new_simple(pw_thread_loop_get_loop(thread_loop),
"mpd",
props,
&stream_events,
this);
if (stream == nullptr)
throw MakeErrno("pw_stream_new_simple() failed");
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
/* this needs to be determined before ToPipeWireAudioFormat()
switches DSD to S16 */
use_dsd = enable_dsd &&
audio_format.format == SampleFormat::DSD;
dsd_reverse_bits = false;
dsd_interleave = 0;
#endif
auto raw = ToPipeWireAudioFormat(audio_format);
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
if (use_dsd)
/* restore the DSD format which was overwritten by
ToPipeWireAudioFormat(), because DSD is a special
case in PipeWire */
audio_format.format = SampleFormat::DSD;
#endif
frame_size = audio_format.GetFrameSize();
sample_format = audio_format.format;
channels = audio_format.channels;
interrupted = false;
/* allocate a ring buffer of 0.5 seconds */
ring_buffer = RingBuffer{frame_size * (audio_format.sample_rate / 2)};
const struct spa_pod *params[1];
pod_builder = {};
pod_builder.data = pod_buffer;
pod_builder.size = sizeof(pod_buffer);
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
struct spa_audio_info_dsd dsd;
if (use_dsd) {
dsd = {};
/* copy all relevant settings from the
ToPipeWireAudioFormat() return value */
dsd.flags = raw.flags;
dsd.rate = raw.rate;
dsd.channels = raw.channels;
if ((dsd.flags & SPA_AUDIO_FLAG_UNPOSITIONED) == 0)
std::copy_n(raw.position, dsd.channels, dsd.position);
params[0] = spa_format_audio_dsd_build(&pod_builder,
SPA_PARAM_EnumFormat,
&dsd);
} else
#endif
params[0] = spa_format_audio_raw_build(&pod_builder,
SPA_PARAM_EnumFormat,
&raw);
int error =
pw_stream_connect(stream,
PW_DIRECTION_OUTPUT,
target_id,
(enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_INACTIVE |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS),
params, 1);
if (error < 0)
throw PipeWire::MakeError(error, "Failed to connect stream");
}
void
PipeWireOutput::Close() noexcept
{
{
const PipeWire::ThreadLoopLock lock(thread_loop);
pw_stream_destroy(stream);
stream = nullptr;
}
ring_buffer = {};
}
inline void
PipeWireOutput::StateChanged(enum pw_stream_state state,
[[maybe_unused]] const char *error) noexcept
{
const bool was_disconnected = disconnected;
disconnected = state == PW_STREAM_STATE_ERROR ||
state == PW_STREAM_STATE_UNCONNECTED;
if (!was_disconnected && disconnected) {
if (error != nullptr)
error_message = error;
pw_thread_loop_signal(thread_loop, false);
}
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
inline void
PipeWireOutput::DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept
{
/* MPD uses MSB internally, which means if PipeWire asks LSB
from us, we need to reverse the bits in each DSD byte */
dsd_reverse_bits = dsd.bitorder == SPA_PARAM_BITORDER_lsb;
dsd_interleave = dsd.interleave;
}
inline void
PipeWireOutput::DsdFormatChanged(const struct spa_pod &param) noexcept
{
uint32_t media_type, media_subtype;
struct spa_audio_info_dsd dsd;
if (spa_format_parse(&param, &media_type, &media_subtype) >= 0 &&
media_type == SPA_MEDIA_TYPE_audio &&
media_subtype == SPA_MEDIA_SUBTYPE_dsd &&
spa_format_audio_dsd_parse(&param, &dsd) >= 0)
DsdFormatChanged(dsd);
}
#endif
inline void
PipeWireOutput::ParamChanged([[maybe_unused]] uint32_t id,
[[maybe_unused]] const struct spa_pod *param) noexcept
{
if (restore_volume) {
restore_volume = false;
if (volume >= 0) {
try {
::SetVolume(*stream, channels, volume);
} catch (...) {
FmtError(pipewire_output_domain,
FMT_STRING("Failed to restore volume: {}"),
std::current_exception());
}
}
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
if (use_dsd && id == SPA_PARAM_Format && param != nullptr)
DsdFormatChanged(*param);
#endif
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
static void
Interleave(std::byte *data, std::byte *end,
std::size_t channels, std::size_t interleave) noexcept
{
assert(channels > 1);
assert(channels <= MAX_CHANNELS);
constexpr std::size_t MAX_INTERLEAVE = 8;
assert(interleave > 1);
assert(interleave <= MAX_INTERLEAVE);
std::array<std::byte, MAX_CHANNELS * MAX_INTERLEAVE> buffer;
std::size_t buffer_size = channels * interleave;
while (data < end) {
std::copy_n(data, buffer_size, buffer.data());
const std::byte *src0 = buffer.data();
for (std::size_t channel = 0; channel < channels;
++channel, ++src0) {
const std::byte *src = src0;
for (std::size_t i = 0; i < interleave;
++i, src += channels)
*data++ = *src;
}
}
}
static void
BitReverse(std::byte *data, std::size_t n) noexcept
{
while (n-- > 0)
*data = BitReverse(*data);
}
static void
PostProcessDsd(std::byte *data, struct spa_chunk &chunk, unsigned channels,
bool reverse_bits, unsigned interleave) noexcept
{
assert(chunk.size % channels == 0);
if (interleave > 1 && channels > 1) {
assert(chunk.size % (channels * interleave) == 0);
Interleave(data, data + chunk.size, channels, interleave);
chunk.stride *= interleave;
}
if (reverse_bits)
BitReverse(data, chunk.size);
}
#endif
inline void
PipeWireOutput::Process() noexcept
{
auto *b = pw_stream_dequeue_buffer(stream);
if (b == nullptr) {
pw_log_warn("out of buffers: %m");
return;
}
auto &buffer = *b->buffer;
auto &d = buffer.datas[0];
auto dest = (std::byte *)d.data;
if (dest == nullptr)
return;
std::size_t chunk_size = frame_size;
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
if (use_dsd && dsd_interleave > 1) {
/* make sure we don't get partial interleave frames */
chunk_size *= dsd_interleave;
}
#endif
size_t nbytes = ring_buffer.ReadFramesTo({dest, d.maxsize}, chunk_size);
assert(nbytes % chunk_size == 0);
if (nbytes == 0) {
if (drain_requested) {
pw_stream_flush(stream, true);
return;
}
/* buffer underrun: generate some silence */
std::size_t max_chunks = d.maxsize / chunk_size;
nbytes = max_chunks * chunk_size;
PcmSilence({dest, nbytes}, sample_format);
LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun");
}
auto &chunk = *d.chunk;
chunk.offset = 0;
chunk.stride = frame_size;
chunk.size = nbytes;
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
if (use_dsd)
PostProcessDsd(dest, chunk, channels,
dsd_reverse_bits, dsd_interleave);
#endif
pw_stream_queue_buffer(stream, b);
pw_thread_loop_signal(thread_loop, false);
}
std::chrono::steady_clock::duration
PipeWireOutput::Delay() const noexcept
{
const PipeWire::ThreadLoopLock lock(thread_loop);
auto result = std::chrono::steady_clock::duration::zero();
if (paused)
/* idle while paused */
result = std::chrono::seconds(1);
return result;
}
std::size_t
PipeWireOutput::Play(std::span<const std::byte> src)
{
const PipeWire::ThreadLoopLock lock(thread_loop);
paused = false;
while (true) {
CheckThrowError();
std::size_t bytes_written =
ring_buffer.WriteFrom(src);
if (bytes_written > 0) {
drained = false;
return bytes_written;
}
if (!active) {
/* now that the ring_buffer is full, there is
enough data for Process(), so let's resume
the stream now */
active = true;
pw_stream_set_active(stream, true);
}
if (interrupted)
throw AudioOutputInterrupted{};
pw_thread_loop_wait(thread_loop);
}
}
void
PipeWireOutput::Drain()
{
const PipeWire::ThreadLoopLock lock(thread_loop);
if (drained)
return;
if (!active) {
/* there is data in the ring_buffer, but the stream is
not yet active; activate it now to ensure it is
played before this method returns */
active = true;
pw_stream_set_active(stream, true);
}
drain_requested = true;
AtScopeExit(this) { drain_requested = false; };
while (!drained && !interrupted) {
CheckThrowError();
pw_thread_loop_wait(thread_loop);
}
}
void
PipeWireOutput::Cancel() noexcept
{
const PipeWire::ThreadLoopLock lock(thread_loop);
interrupted = false;
if (drained)
return;
/* clear MPD's ring buffer */
ring_buffer.Clear();
/* clear libpipewire's buffer */
pw_stream_flush(stream, false);
drained = true;
/* pause the PipeWire stream so libpipewire ceases invoking
the "process" callback (we have no data until our Play()
method gets called again); the stream will be resume by
Play() after the ring_buffer has been refilled */
if (active) {
active = false;
pw_stream_set_active(stream, false);
}
}
bool
PipeWireOutput::Pause() noexcept
{
const PipeWire::ThreadLoopLock lock(thread_loop);
interrupted = false;
paused = true;
if (active) {
active = false;
pw_stream_set_active(stream, false);
}
return true;
}
inline void
PipeWireOutput::SetMixer(PipeWireMixer &_mixer) noexcept
{
assert(mixer == nullptr);
mixer = &_mixer;
// TODO: Check if context and stream is ready and trigger a volume update...
}
void
PipeWireOutput::SendTag(const Tag &tag)
{
CheckThrowError();
static constexpr struct {
TagType mpd;
const char *pipewire;
} tag_map[] = {
{ TAG_ARTIST, PW_KEY_MEDIA_ARTIST },
{ TAG_TITLE, PW_KEY_MEDIA_TITLE },
{ TAG_DATE, PW_KEY_MEDIA_DATE },
{ TAG_COMMENT, PW_KEY_MEDIA_COMMENT },
};
StaticVector<spa_dict_item, 1 + std::size(tag_map)> items;
char *medianame = FormatTag(tag, "%artist% - %title%");
AtScopeExit(medianame) { free(medianame); };
items.push_back(SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, medianame));
for (const auto &i : tag_map)
if (const char *value = tag.GetValue(i.mpd))
items.push_back(SPA_DICT_ITEM_INIT(i.pipewire, value));
struct spa_dict dict = SPA_DICT_INIT(items.data(), (uint32_t)items.size());
const PipeWire::ThreadLoopLock lock(thread_loop);
auto rc = pw_stream_update_properties(stream, &dict);
if (rc < 0)
LogWarning(pipewire_output_domain, "Error updating properties");
}
void
pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
{
po.SetMixer(pm);
}
void
pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
{
po.ClearMixer(pm);
}
const struct AudioOutputPlugin pipewire_output_plugin = {
"pipewire",
nullptr,
&PipeWireOutput::Create,
&pipewire_mixer_plugin,
};
void
pipewire_output_set_volume(PipeWireOutput &output, float volume)
{
output.SetVolume(volume);
}