Compare commits

...

31 Commits

Author SHA1 Message Date
Max Kellermann
b7fdff46f2 release v0.23.2 2021-10-22 12:45:45 +02:00
Max Kellermann
e16109330d input/last: clear "uri" in OnCloseTimer()
Without clearing the "uri" field, the next Open() call attempts to
reuse the old InputStream, but it has already been closed, so Open()
always returns nullptr.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1300
2021-10-22 12:45:18 +02:00
Max Kellermann
72621531e0 protocol/Result: convert to Client method 2021-10-22 11:55:39 +02:00
Max Kellermann
0a48146efc client/Client: pass std::string_view to Write()
Almost all callers have string literal, and the length is known at
compile time.
2021-10-22 11:54:14 +02:00
Max Kellermann
0c4bf12bfd player/CrossFade: fix inverted check and wrong variable
The inverted check was introduced by commit 46d00dd85f, and commit
8ad17d25ef added a check for the wrong variable.  D'oh!

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1303
2021-10-22 11:49:38 +02:00
Max Kellermann
b8e0855ef3 output/pipewire: obey PipeWire's DSD bit order and interleave
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1297
2021-10-21 21:15:16 +02:00
Max Kellermann
6467502b9d output/pipewire: restore SampleFormat::DSD after ToPipeWireAudioFormat() call 2021-10-21 21:15:13 +02:00
Max Kellermann
15b67f20e5 output/pipewire: un-inline ParamChanged() 2021-10-21 20:11:22 +02:00
Max Kellermann
0825179f00 output/pipewire: add local reference variables 2021-10-21 20:02:59 +02:00
Max Kellermann
97211d0aad output/pipewire: rename field "buffer" to "pod_buffer" 2021-10-21 20:02:32 +02:00
Max Kellermann
029c499bfa output/pipewire: use std::fill_n() 2021-10-21 20:01:44 +02:00
Max Kellermann
0ba867ec16 output/pipewire: use MAX_CHANNELS, not SPA_AUDIO_MAX_CHANNELS
MPD supports only 8 channels, so MAX_CHANNELS is enough, the array
doens't need to be SPA_AUDIO_MAX_CHANNELS (which is 64).
2021-10-21 20:01:01 +02:00
Max Kellermann
866d147122 output/pipewire: make field "channels" unsigned 2021-10-21 19:59:48 +02:00
Max Kellermann
32851d1bc7 output/pipewire: DSD support
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1297
2021-10-20 11:39:54 +02:00
Max Kellermann
78257408b4 output/pipewire: report errors from the "state_changed" callback 2021-10-20 11:24:57 +02:00
Max Kellermann
f447b7615e output/pipewire: check pw_stream_connect() errors 2021-10-20 11:24:51 +02:00
Max Kellermann
1f780b7209 output/Thread: log exception details 2021-10-20 11:24:51 +02:00
Max Kellermann
04bf8a6b1a output/pipewire: fix memory leak in SendTag() 2021-10-20 10:16:36 +02:00
Max Kellermann
c4c64854d4 output/pipewire: evaluate errno after libpipewire function calls 2021-10-20 10:13:27 +02:00
Max Kellermann
17562dc90b output/pipewire: remove misplaced noexcept 2021-10-20 09:41:27 +02:00
Max Kellermann
7b24316734 output/pipewire: fix coding style 2021-10-20 09:41:10 +02:00
Max Kellermann
5fab107fd3 lib/nfs/FileReader: use the thread-safe InjectEvent
.. instead of DeferEvent, which is not thread-safe.  This caused
various playback problems, which was initially caused by the
DeferEvent/InjectEvent split in commit 774b4313f2

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1298
2021-10-20 09:38:09 +02:00
Max Kellermann
f31920e092 event/Loop: add thread assert() to AddDefer()
Currently fails in class NfsFileReader due to
https://github.com/MusicPlayerDaemon/MPD/issues/1298
2021-10-20 09:26:27 +02:00
Max Kellermann
eb111a10e7 output/pipewire: remove redundant prefix and newline from log message 2021-10-19 14:38:37 +02:00
Max Kellermann
80b09360c6 NEWS: mention the previous commit 2021-10-19 14:38:37 +02:00
Nicolai Syvertsen
5ccf78855d Implement SendTag for PipeWire output plugin 2021-10-19 14:31:40 +02:00
Max Kellermann
fd5a3b5880 client/Response: reimplement Error() without FmtError()
With libfmt versions older than 7, this leads to an endless recursion
between Error() and FmtError(), resulting in a crash due to stack
overflow.  D'oh!

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1295
2021-10-19 13:40:11 +02:00
Max Kellermann
6120c1360c neighbor/Glue: remove unreachable "throw" statement
Should have been removed by commit a8087dc12c
2021-10-19 13:40:11 +02:00
Max Kellermann
a8087dc12c neighbor/Glue: mention failed plugin name in error message 2021-10-19 13:29:00 +02:00
Max Kellermann
070c03dbf7 event/Thread, ...: fix printf->libfmt remains 2021-10-19 13:19:07 +02:00
Max Kellermann
0a9bec3754 increment version number to 0.23.2 2021-10-19 10:29:49 +02:00
24 changed files with 405 additions and 88 deletions

14
NEWS

@@ -1,3 +1,17 @@
ver 0.23.2 (2021/10/22)
* protocol
- fix "albumart" timeout bug
* input
- nfs: fix playback bug
* output
- pipewire: send artist and title to PipeWire
- pipewire: DSD support
* neighbor
- mention failed plugin name in error message
* player
- fix cross-fade regression
* fix crash with libfmt versions older than 7
ver 0.23.1 (2021/10/19)
* protocol
- use decimal notation instead of scientific notation

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
android:versionCode="61"
android:versionName="0.23.1">
android:versionCode="62"
android:versionName="0.23.2">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>

@@ -38,7 +38,7 @@ author = 'Max Kellermann'
# built documents.
#
# The short X.Y version.
version = '0.23.1'
version = '0.23.2'
# The full version, including alpha/beta/rc tags.
#release = version + '~git'

@@ -1094,6 +1094,8 @@ Connect to a `PipeWire <https://pipewire.org/>`_ server. Requires
* - **remote NAME**
- The name of the remote to connect to. The default is
``pipewire-0``.
* - **dsd yes|no**
- Enable DSD playback. This requires PipeWire 0.38.
.. _pulse_plugin:

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.23.1',
version: '0.23.2',
meson_version: '>= 0.56.0',
default_options: [
'c_std=c11',
@@ -265,7 +265,6 @@ sources = [
version_cxx,
'src/Main.cxx',
'src/protocol/ArgParser.cxx',
'src/protocol/Result.cxx',
'src/command/CommandError.cxx',
'src/command/PositionArg.cxx',
'src/command/AllCommands.cxx',

@@ -150,7 +150,13 @@ public:
/**
* Write a null-terminated string.
*/
bool Write(const char *data) noexcept;
bool Write(std::string_view s) noexcept {
return Write(s.data(), s.size());
}
bool WriteOK() noexcept {
return Write("OK\n");
}
/**
* returns the uid of the client process, or a negative value

@@ -20,7 +20,6 @@
#include "Client.hxx"
#include "Config.hxx"
#include "Domain.hxx"
#include "protocol/Result.hxx"
#include "command/AllCommands.hxx"
#include "Log.hxx"
#include "util/StringAPI.hxx"
@@ -72,7 +71,7 @@ Client::ProcessLine(char *line) noexcept
if (idle_waiting) {
/* send empty idle response and leave idle mode */
idle_waiting = false;
command_success(*this);
WriteOK();
}
/* do nothing if the client wasn't idling: the client
@@ -108,7 +107,7 @@ Client::ProcessLine(char *line) noexcept
"list returned {}", id, unsigned(ret));
if (ret == CommandResult::OK)
command_success(*this);
WriteOK();
return ret;
} else {
@@ -144,7 +143,7 @@ Client::ProcessLine(char *line) noexcept
return CommandResult::CLOSE;
if (ret == CommandResult::OK)
command_success(*this);
WriteOK();
return ret;
}

@@ -66,7 +66,11 @@ Response::WriteBinary(ConstBuffer<void> payload) noexcept
void
Response::Error(enum ack code, const char *msg) noexcept
{
FmtError(code, FMT_STRING("{}"), msg);
Fmt(FMT_STRING("ACK [{}@{}] {{{}}} "),
(int)code, list_index, command);
Write(msg);
Write("\n");
}
void

@@ -21,7 +21,6 @@
#include "Client.hxx"
#include "Response.hxx"
#include "command/CommandError.hxx"
#include "protocol/Result.hxx"
ThreadBackgroundCommand::ThreadBackgroundCommand(Client &_client) noexcept
:thread(BIND_THIS_METHOD(_Run)),
@@ -57,7 +56,7 @@ ThreadBackgroundCommand::DeferredFinish() noexcept
PrintError(response, error);
} else {
SendResponse(response);
command_success(client);
client.WriteOK();
}
/* delete this object */

@@ -27,9 +27,3 @@ Client::Write(const void *data, size_t length) noexcept
/* if the client is going to be closed, do nothing */
return !IsExpired() && FullyBufferedSocket::Write(data, length);
}
bool
Client::Write(const char *data) noexcept
{
return Write(data, strlen(data));
}

@@ -175,6 +175,10 @@ EventLoop::HandleTimers() noexcept
void
EventLoop::AddDefer(DeferEvent &d) noexcept
{
#ifdef HAVE_THREADED_EVENT_LOOP
assert(!IsAlive() || IsInside());
#endif
defer.push_back(d);
again = true;
}

@@ -62,7 +62,7 @@ EventThread::Run() noexcept
SetThreadRealtime();
} catch (...) {
FmtInfo(event_domain,
"RTIOThread could not get realtime scheduling, continuing anyway: %s",
"RTIOThread could not get realtime scheduling, continuing anyway: {}",
std::current_exception());
}
}

@@ -71,12 +71,12 @@ input_stream_global_init(const ConfigData &config, EventLoop &event_loop)
input_plugins_enabled[i] = true;
} catch (const PluginUnconfigured &e) {
FmtDebug(input_domain,
"Input plugin '{}' is not configured: %s",
"Input plugin '{}' is not configured: {}",
plugin->name, e.what());
continue;
} catch (const PluginUnavailable &e) {
FmtDebug(input_domain,
"Input plugin '{}' is unavailable: %s",
"Input plugin '{}' is unavailable: {}",
plugin->name, e.what());
continue;
} catch (...) {

@@ -42,5 +42,6 @@ LastInputStream::OnCloseTimer() noexcept
{
assert(is);
uri.clear();
is.reset();
}

@@ -22,7 +22,7 @@
#include "Lease.hxx"
#include "Callback.hxx"
#include "event/DeferEvent.hxx"
#include "event/InjectEvent.hxx"
#include "util/Compiler.h"
#include <cstddef>
@@ -63,7 +63,10 @@ class NfsFileReader : NfsLease, NfsCallback {
nfsfh *fh;
DeferEvent defer_open;
/**
* To inject the Open() call into the I/O thread.
*/
InjectEvent defer_open;
public:
NfsFileReader() noexcept;
@@ -150,7 +153,7 @@ private:
void OnNfsCallback(unsigned status, void *data) noexcept final;
void OnNfsError(std::exception_ptr &&e) noexcept final;
/* DeferEvent callback */
/* InjectEvent callback */
void OnDeferredOpen() noexcept;
};

@@ -17,11 +17,18 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "Result.hxx"
#include "client/Client.hxx"
#include "Error.hxx"
void
command_success(Client &client)
#include <spa/utils/result.h>
namespace PipeWire {
ErrorCategory error_category;
std::string
ErrorCategory::message(int condition) const
{
client.Write("OK\n");
return spa_strerror(condition);
}
} // namespace Avahi

@@ -17,12 +17,29 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef MPD_PROTOCOL_RESULT_HXX
#define MPD_PROTOCOL_RESULT_HXX
#pragma once
class Client;
#include <system_error>
void
command_success(Client &client);
struct AvahiClient;
#endif
namespace PipeWire {
class ErrorCategory final : public std::error_category {
public:
const char *name() const noexcept override {
return "pipewire";
}
std::string message(int condition) const override;
};
extern ErrorCategory error_category;
inline std::system_error
MakeError(int error, const char *msg) noexcept
{
return std::system_error(error, error_category, msg);
}
} // namespace PipeWire

@@ -12,3 +12,17 @@ pipewire_dep = declare_dependency(
# disable it at the command line
compile_args: ['-Wno-pedantic'],
)
pipewire = static_library(
'pipewire',
'Error.cxx',
include_directories: inc,
dependencies: [
pipewire_dep,
],
)
pipewire_dep = declare_dependency(
link_with: pipewire,
dependencies: pipewire_dep,
)

@@ -33,12 +33,9 @@ NeighborGlue::~NeighborGlue() noexcept = default;
static std::unique_ptr<NeighborExplorer>
CreateNeighborExplorer(EventLoop &loop, NeighborListener &listener,
const char *plugin_name,
const ConfigBlock &block)
{
const char *plugin_name = block.GetBlockValue("plugin");
if (plugin_name == nullptr)
throw std::runtime_error("Missing \"plugin\" configuration");
const NeighborPlugin *plugin = GetNeighborPluginByName(plugin_name);
if (plugin == nullptr)
throw FormatRuntimeError("No such neighbor plugin: %s",
@@ -55,8 +52,14 @@ NeighborGlue::Init(const ConfigData &config,
block.SetUsed();
try {
explorers.emplace_front(CreateNeighborExplorer(loop,
const char *plugin_name = block.GetBlockValue("plugin");
if (plugin_name == nullptr)
throw std::runtime_error("Missing \"plugin\" configuration");
explorers.emplace_front(plugin_name,
CreateNeighborExplorer(loop,
listener,
plugin_name,
block));
} catch (...) {
std::throw_with_nested(FormatRuntimeError("Line %i: ",
@@ -76,7 +79,9 @@ NeighborGlue::Open()
/* roll back */
for (auto k = explorers.begin(); k != i; ++k)
k->explorer->Close();
throw;
std::throw_with_nested(FormatRuntimeError("Failed to open neighblor plugin '%s'",
i->name.c_str()));
}
}
}

@@ -24,6 +24,7 @@
#include <forward_list>
#include <memory>
#include <string>
struct ConfigData;
class EventLoop;
@@ -36,11 +37,13 @@ struct NeighborInfo;
*/
class NeighborGlue {
struct Explorer {
const std::string name;
std::unique_ptr<NeighborExplorer> explorer;
template<typename E>
Explorer(E &&_explorer) noexcept
:explorer(std::forward<E>(_explorer)) {}
template<typename N, typename E>
Explorer(N &&_name, E &&_explorer) noexcept
:name(std::forward<N>(_name)),
explorer(std::forward<E>(_explorer)) {}
Explorer(const Explorer &) = delete;
};

@@ -279,7 +279,7 @@ AudioOutputControl::PlayChunk(std::unique_lock<Mutex> &lock) noexcept
return false;
} catch (...) {
FmtError(output_domain,
"Failed to play on {}",
"Failed to play on {}: {}",
GetLogName(), std::current_exception());
InternalCloseError(std::current_exception());
return false;
@@ -435,7 +435,7 @@ AudioOutputControl::Task() noexcept
SetThreadRealtime();
} catch (...) {
FmtInfo(output_domain,
"OutputThread could not get realtime scheduling, continuing anyway: %s",
"OutputThread could not get realtime scheduling, continuing anyway: {}",
std::current_exception());
}

@@ -195,7 +195,7 @@ FifoOutput::Cancel() noexcept
if (bytes < 0 && errno != EAGAIN) {
FmtError(fifo_output_domain,
"Flush of FIFO \"{}\" failed: %s",
"Flush of FIFO \"{}\" failed: {}",
path_utf8, strerror(errno));
}
}

@@ -18,16 +18,21 @@
*/
#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 "system/Error.hxx"
#include "util/BitReverse.hxx"
#include "util/Domain.hxx"
#include "util/ScopeExit.hxx"
#include "util/StringCompare.hxx"
#include "util/WritableBuffer.hxx"
#include "Log.hxx"
#include "tag/Format.hxx"
#include "config.h" // for ENABLE_DSD
#ifdef __GNUC__
#pragma GCC diagnostic push
@@ -49,7 +54,9 @@
#include <boost/lockfree/spsc_queue.hpp>
#include <algorithm>
#include <stdexcept>
#include <string>
static constexpr Domain pipewire_output_domain("pipewire_output");
@@ -62,7 +69,9 @@ class PipeWireOutput final : AudioOutput {
struct pw_thread_loop *thread_loop = nullptr;
struct pw_stream *stream;
std::byte buffer[1024];
std::string error_message;
std::byte pod_buffer[1024];
struct spa_pod_builder pod_builder;
std::size_t frame_size;
@@ -78,13 +87,38 @@ class PipeWireOutput final : AudioOutput {
float volume = 1.0;
PipeWireMixer *mixer = nullptr;
int channels;
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;
/**
@@ -146,8 +180,12 @@ public:
private:
void CheckThrowError() {
if (disconnected)
throw std::runtime_error("Disconnected from PipeWire");
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,
@@ -200,12 +238,12 @@ private:
o.ControlInfo(control);
}
void ParamChanged() noexcept {
if (restore_volume) {
SetVolume(volume);
restore_volume = false;
}
}
#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,
@@ -215,7 +253,7 @@ private:
return;
auto &o = *(PipeWireOutput *)data;
o.ParamChanged();
o.ParamChanged(id, param);
}
/* virtual methods from class AudioOutput */
@@ -240,6 +278,8 @@ private:
void Drain() override;
void Cancel() noexcept override;
bool Pause() noexcept override;
void SendTag(const Tag &tag) override;
};
static constexpr auto stream_events = PipeWireOutput::MakeStreamEvents();
@@ -250,6 +290,9 @@ PipeWireOutput::PipeWireOutput(const ConfigBlock &block)
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))
@@ -272,12 +315,9 @@ PipeWireOutput::SetVolume(float _volume)
float newvol = _volume*_volume*_volume;
if (stream != nullptr && !restore_volume) {
float vol[SPA_AUDIO_MAX_CHANNELS];
int i;
float vol[MAX_CHANNELS];
std::fill_n(vol, channels, newvol);
for (i = 0; i < channels; i++) {
vol[i] = newvol;
}
if (pw_stream_set_control(stream,
SPA_PROP_channelVolumes, channels, vol,
0) != 0)
@@ -292,7 +332,7 @@ PipeWireOutput::Enable()
{
thread_loop = pw_thread_loop_new(name, nullptr);
if (thread_loop == nullptr)
throw std::runtime_error("pw_thread_loop_new() failed");
throw MakeErrno("pw_thread_loop_new() failed");
pw_thread_loop_start(thread_loop);
}
@@ -421,6 +461,7 @@ ToPipeWireAudioFormat(AudioFormat &audio_format) noexcept
void
PipeWireOutput::Open(AudioFormat &audio_format)
{
error_message.clear();
disconnected = false;
restore_volume = true;
@@ -462,10 +503,27 @@ PipeWireOutput::Open(AudioFormat &audio_format)
&stream_events,
this);
if (stream == nullptr)
throw std::runtime_error("pw_stream_new_simple() failed");
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;
@@ -479,19 +537,42 @@ PipeWireOutput::Open(AudioFormat &audio_format)
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);
pod_builder.data = pod_buffer;
pod_builder.size = sizeof(pod_buffer);
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 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
@@ -513,11 +594,119 @@ PipeWireOutput::StateChanged(enum pw_stream_state state,
const bool was_disconnected = disconnected;
disconnected = state == PW_STREAM_STATE_ERROR ||
state == PW_STREAM_STATE_UNCONNECTED;
if (!was_disconnected && disconnected)
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) {
SetVolume(volume);
restore_volume = false;
}
#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(uint8_t *data, std::size_t n) noexcept
{
while (n-- > 0)
*data = bit_reverse(*data);
}
static void
BitReverse(std::byte *data, std::size_t n) noexcept
{
BitReverse((uint8_t *)data, n);
}
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
{
@@ -527,15 +716,32 @@ PipeWireOutput::Process() noexcept
return;
}
auto *buf = b->buffer;
std::byte *dest = (std::byte *)buf->datas[0].data;
auto &buffer = *b->buffer;
auto &d = buffer.datas[0];
std::byte *dest = (std::byte *)d.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;
std::size_t max_frames = d.maxsize / 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 */
std::size_t interleave_size = frame_size * dsd_interleave;
std::size_t available_bytes = ring_buffer->read_available();
std::size_t available_interleaves =
available_bytes / interleave_size;
std::size_t available_frames =
available_interleaves * dsd_interleave;
if (max_frames > available_frames)
max_frames = available_frames;
}
#endif
const std::size_t max_size = max_frames * frame_size;
size_t nbytes = ring_buffer->pop(dest, max_size);
assert(nbytes % frame_size == 0);
if (nbytes == 0) {
if (drain_requested) {
pw_stream_flush(stream, true);
@@ -549,9 +755,16 @@ PipeWireOutput::Process() noexcept
LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun");
}
buf->datas[0].chunk->offset = 0;
buf->datas[0].chunk->stride = frame_size;
buf->datas[0].chunk->size = nbytes;
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);
@@ -652,6 +865,39 @@ PipeWireOutput::SetMixer(PipeWireMixer &_mixer) noexcept
// TODO: Check if context and stream is ready and trigger a volume update...
}
void
PipeWireOutput::SendTag(const Tag &tag)
{
CheckThrowError();
struct spa_dict_item items[3];
uint32_t n_items=0;
const char *artist, *title;
char *medianame = FormatTag(tag, "%artist% - %title%");
AtScopeExit(medianame) { free(medianame); };
items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, medianame);
artist = tag.GetValue(TAG_ARTIST);
title = tag.GetValue(TAG_TITLE);
if (artist != nullptr) {
items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_ARTIST, artist);
}
if (title != nullptr) {
items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_TITLE, title);
}
struct spa_dict dict = SPA_DICT_INIT(items, n_items);
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
{

@@ -34,8 +34,8 @@ inline bool
CrossFadeSettings::CanCrossFadeSong(SignedSongTime total_time) const noexcept
{
return !total_time.IsNegative() &&
duration >= MIN_TOTAL_TIME &&
duration >= std::chrono::duration_cast<FloatDuration>(total_time);
total_time >= MIN_TOTAL_TIME &&
duration < std::chrono::duration_cast<FloatDuration>(total_time);
}
gcc_pure