Compare commits

..

51 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
Max Kellermann
fff25ac753 release v0.23.1 2021-10-19 10:27:28 +02:00
Max Kellermann
4f1e79b6b8 filter/ReplayGain: emit "mixer" event when replay gain changes volume
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1294
2021-10-19 10:03:21 +02:00
Max Kellermann
aa9933c0b5 output/pipewire: add noexcept 2021-10-19 08:58:50 +02:00
Max Kellermann
0697d1f859 output/pipewire: include cleanup 2021-10-19 08:57:33 +02:00
Max Kellermann
df033fa4aa NEWS: mention the previous commit 2021-10-19 08:56:32 +02:00
Nicolai Syvertsen
b941a7df83 Implement volume updates for pipewire output 2021-10-19 00:01:45 +02:00
Max Kellermann
31151cec3c command/playlist: "load" supports relative positions
This commit also increases the PROTOCOL_VERSION so clients can detect
the availability of the feature.
2021-10-18 22:08:22 +02:00
Max Kellermann
07e8c338df command/queue: move position parameter functions to separate library 2021-10-18 22:07:04 +02:00
Max Kellermann
b22d7218aa command/player, ...: use decimal notation
During the libfmt migration, I converted "%1.3f" to just "{:1.3}"
without the "f" suffix, but libfmt defaults to scientific notation,
which can break some MPD clients.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1291
2021-10-18 16:54:53 +02:00
Max Kellermann
d5be8c74b0 output/pipewire: attempt to change the graph sample rate
Requires PipeWire 0.3.32.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1283
2021-10-18 16:46:23 +02:00
Max Kellermann
c112cb60da output/snapcast: fix typo which caused "Failed to get chunk"
This bug caused a 9 second offset in all time stamps.  Due to that,
the Snapcast server thought the chunks are too old and discarded them.

Fixes https://github.com/MusicPlayerDaemon/MPD/discussions/1287
2021-10-18 16:40:11 +02:00
Max Kellermann
677fa4f9bc doc/plugins.rst: mention that the snapcast output requires a format 2021-10-17 20:01:21 +02:00
Max Kellermann
907af2ad02 Permission: refactor getPermissionFromPassword() to return std::optional
This replaces the output parameter (which is bad API design).  As a
side effect, it fixes the bad [[gnu::pure]] attribute added by commit
a636d2127 which caused optimizing compilers to miscompile calls to
that function.  "Pure" functions can be assumed to have no output
arguments, so the compiler can assume the function doesn't modify
them.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1282
2021-10-17 19:58:50 +02:00
Thomas Zander
6a2e7bbc02 protocol/ArgParser.cxx: Add missing #include <stdio.h>
Fixes a build problem on platforms where stdio.h is not included
transitively. snprintf() is defined in stdio.h.
2021-10-16 17:38:07 +02:00
Max Kellermann
771c46032f meson.build: add missing libfmt dependencies
Fixes https://github.com/MusicPlayerDaemon/MPD/discussions/1281

The problem occurred when there was libfmt-dev installed, but it was
too old (e.g. on Debian Buster), and Meson used the wrap fallback.
Those internal MPD libraries where the libfmt dependency was not
declared were still using the old system libfmt headers, which are not
ABI-compatible with MPD's own libfmt build.
2021-10-15 14:26:59 +02:00
Max Kellermann
85611aa456 storage/smbclient: add StoragePlugin.prefixes
Should have been part of commit
ef24cfa523

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1279
2021-10-15 10:24:30 +02:00
Max Kellermann
466b5cb08d neighbor/smbclient: FmtError() instead of FormatErrno()
Fixes part 2 of https://github.com/MusicPlayerDaemon/MPD/issues/1279
2021-10-15 09:40:36 +02:00
Max Kellermann
3f2f3251cb neighbor/smbclient: use [[gnu::pure]]
Fixes part 1 of https://github.com/MusicPlayerDaemon/MPD/issues/1279
2021-10-15 09:39:34 +02:00
Max Kellermann
8ae85f3991 doc/protocol.rst: move POSITION from "search" to "findadd"
Whoops, I misplaced this one.
2021-10-14 15:36:25 +02:00
Max Kellermann
781fe4ff28 increment version number to 0.23.1 2021-10-14 15:36:16 +02:00
49 changed files with 721 additions and 206 deletions

26
NEWS

@@ -1,3 +1,29 @@
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
- "load" supports relative positions
* output
- emit "mixer" idle event when replay gain changes volume
- pipewire: emit "mixer" idle events on external volume change
- pipewire: attempt to change the graph sample rate
- snapcast: fix time stamp bug which caused "Failed to get chunk"
* fix libfmt linker problems
* fix broken password authentication
ver 0.23 (2021/10/14)
* protocol
- new command "getvol"

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

@@ -38,9 +38,9 @@ author = 'Max Kellermann'
# built documents.
#
# The short X.Y version.
version = '0.23'
version = '0.23.2'
# The full version, including alpha/beta/rc tags.
release = version + '~git'
#release = version + '~git'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

@@ -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:
@@ -1193,6 +1195,8 @@ allows MPD to act as a `Snapcast
<https://github.com/badaix/snapcast>`__ server. Snapcast clients
connect to it and receive audio data from MPD.
You must set a format.
.. list-table::
:widths: 20 80
:header-rows: 1

@@ -923,8 +923,9 @@ remote playlists (absolute URI with a supported scheme).
only a part of the playlist.
The ``POSITION`` parameter specifies where the songs will be
inserted into the queue. (This requires specifying the range as
well; the special value `0:` can be used if the whole playlist
inserted into the queue; it can be relative as described in
:ref:`addid <command_addid>`. (This requires specifying the range
as well; the special value `0:` can be used if the whole playlist
shall be loaded at a certain queue position.)
.. _command_playlistadd:
@@ -1059,11 +1060,11 @@ The music database
.. _command_findadd:
:command:`findadd {FILTER} [sort {TYPE}] [window {START:END}]`
:command:`findadd {FILTER} [sort {TYPE}] [window {START:END}] [position POS]`
Search the database for songs matching
``FILTER`` (see :ref:`Filters <filter_syntax>`) and add them to
the queue. Parameters have the same meaning as for
:ref:`find <command_find>`.
:ref:`find <command_find>` and :ref:`searchadd <command_searchadd>`.
.. _command_list:
@@ -1196,15 +1197,12 @@ The music database
.. _command_search:
:command:`search {FILTER} [sort {TYPE}] [window {START:END}] [position POS]`
:command:`search {FILTER} [sort {TYPE}] [window {START:END}]
Search the database for songs matching
``FILTER`` (see :ref:`Filters <filter_syntax>`). Parameters
have the same meaning as for :ref:`find <command_find>`,
except that search is not case sensitive.
The ``position`` parameter specifies where the songs will be
inserted.
.. _command_searchadd:
:command:`searchadd {FILTER} [sort {TYPE}] [window {START:END}] [position POS]`
@@ -1214,6 +1212,9 @@ The music database
Parameters have the same meaning as for :ref:`search <command_search>`.
The ``position`` parameter specifies where the songs will be
inserted.
.. _command_searchaddpl:
:command:`searchaddpl {NAME} {FILTER} [sort {TYPE}] [window {START:END}]`

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.23',
version: '0.23.2',
meson_version: '>= 0.56.0',
default_options: [
'c_std=c11',
@@ -44,7 +44,7 @@ version_conf = configuration_data()
version_conf.set_quoted('PACKAGE', meson.project_name())
version_conf.set_quoted('PACKAGE_NAME', meson.project_name())
version_conf.set_quoted('VERSION', meson.project_version())
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.0')
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.1')
configure_file(output: 'Version.h', configuration: version_conf)
conf = configuration_data()
@@ -265,8 +265,8 @@ 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',
'src/command/QueueCommands.cxx',
'src/command/TagCommands.cxx',

@@ -161,15 +161,14 @@ GetPermissionsFromAddress(SocketAddress address) noexcept
#endif
int
getPermissionFromPassword(const char *password, unsigned *permission) noexcept
std::optional<unsigned>
GetPermissionFromPassword(const char *password) noexcept
{
auto i = permission_passwords.find(password);
if (i == permission_passwords.end())
return -1;
return std::nullopt;
*permission = i->second;
return 0;
return i->second;
}
unsigned

@@ -22,6 +22,8 @@
#include "config.h"
#include <optional>
struct ConfigData;
class SocketAddress;
@@ -32,9 +34,13 @@ static constexpr unsigned PERMISSION_CONTROL = 4;
static constexpr unsigned PERMISSION_ADMIN = 8;
static constexpr unsigned PERMISSION_PLAYER = 16;
/**
* @return the permissions for the given password or std::nullopt if
* the password is not accepted
*/
[[gnu::pure]]
int
getPermissionFromPassword(const char *password, unsigned *permission) noexcept;
std::optional<unsigned>
GetPermissionFromPassword(const char *password) noexcept;
[[gnu::const]]
unsigned

@@ -100,7 +100,7 @@ song_print_info(Response &r, const LightSong &song, bool base) noexcept
const auto duration = song.GetDuration();
if (!duration.IsNegative())
r.Fmt(FMT_STRING("Time: {}\n"
"duration: {:1.3}\n"),
"duration: {:1.3f}\n"),
duration.RoundS(),
duration.ToDoubleS());
}
@@ -123,7 +123,7 @@ song_print_info(Response &r, const DetachedSong &song, bool base) noexcept
const auto duration = song.GetDuration();
if (!duration.IsNegative())
r.Fmt(FMT_STRING("Time: {}\n"
"duration: {:1.3}\n"),
"duration: {:1.3f}\n"),
duration.RoundS(),
duration.ToDoubleS());
}

@@ -60,7 +60,7 @@ tag_print(Response &r, const Tag &tag) noexcept
{
if (!tag.duration.IsNegative())
r.Fmt(FMT_STRING("Time: {}\n"
"duration: {:1.3}\n"),
"duration: {:1.3f}\n"),
tag.duration.RoundS(),
tag.duration.ToDoubleS());

@@ -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));
}

@@ -58,13 +58,13 @@ handle_binary_limit(Client &client, Request args,
CommandResult
handle_password(Client &client, Request args, Response &r)
{
unsigned permission = 0;
if (getPermissionFromPassword(args.front(), &permission) < 0) {
const auto permission = GetPermissionFromPassword(args.front());
if (!permission) {
r.Error(ACK_ERROR_PASSWORD, "incorrect password");
return CommandResult::ERROR;
}
client.SetPermission(permission);
client.SetPermission(*permission);
return CommandResult::OK;
}

@@ -171,7 +171,7 @@ handle_status(Client &client, [[maybe_unused]] Request args, Response &r)
if (player_status.state != PlayerState::STOP) {
r.Fmt(FMT_STRING(COMMAND_STATUS_TIME ": {}:{}\n"
"elapsed: {:1.3}\n"
"elapsed: {:1.3f}\n"
COMMAND_STATUS_BITRATE ": {}\n"),
player_status.elapsed_time.RoundS(),
player_status.total_time.IsNegative()
@@ -181,7 +181,7 @@ handle_status(Client &client, [[maybe_unused]] Request args, Response &r)
player_status.bit_rate);
if (!player_status.total_time.IsNegative())
r.Fmt(FMT_STRING("duration: {:1.3}\n"),
r.Fmt(FMT_STRING("duration: {:1.3f}\n"),
player_status.total_time.ToDoubleS());
if (player_status.audio_format.IsDefined())

@@ -19,6 +19,7 @@
#include "config.h"
#include "PlaylistCommands.hxx"
#include "PositionArg.hxx"
#include "Request.hxx"
#include "Instance.hxx"
#include "db/Selection.hxx"
@@ -86,7 +87,7 @@ handle_load(Client &client, Request args, [[maybe_unused]] Response &r)
const unsigned old_size = playlist.GetLength();
const unsigned position = args.size > 2
? args.ParseUnsigned(2, old_size)
? ParseInsertPosition(args[2], partition.playlist)
: old_size;
const SongLoader loader(client);

103
src/command/PositionArg.cxx Normal file

@@ -0,0 +1,103 @@
/*
* 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 "PositionArg.hxx"
#include "protocol/Ack.hxx"
#include "protocol/ArgParser.hxx"
#include "protocol/RangeArg.hxx"
#include "queue/Playlist.hxx"
static unsigned
RequireCurrentPosition(const playlist &p)
{
int position = p.GetCurrentPosition();
if (position < 0)
throw ProtocolError(ACK_ERROR_PLAYER_SYNC,
"No current song");
return position;
}
unsigned
ParseInsertPosition(const char *s, const playlist &playlist)
{
const auto queue_length = playlist.queue.GetLength();
if (*s == '+') {
/* after the current song */
const unsigned current = RequireCurrentPosition(playlist);
assert(current < queue_length);
return current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - 1);
} else if (*s == '-') {
/* before the current song */
const unsigned current = RequireCurrentPosition(playlist);
assert(current < queue_length);
return current - ParseCommandArgUnsigned(s + 1, current);
} else
/* absolute position */
return ParseCommandArgUnsigned(s, queue_length);
}
unsigned
ParseMoveDestination(const char *s, const RangeArg range, const playlist &p)
{
assert(!range.IsEmpty());
assert(!range.IsOpenEnded());
const unsigned queue_length = p.queue.GetLength();
if (*s == '+') {
/* after the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else if (*s == '-') {
/* before the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current -
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else
/* absolute position */
return ParseCommandArgUnsigned(s,
queue_length - range.Count());
}

@@ -17,12 +17,16 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef MPD_PROTOCOL_RESULT_HXX
#define MPD_PROTOCOL_RESULT_HXX
#pragma once
class Client;
struct playlist;
struct RangeArg;
void
command_success(Client &client);
/**
* Throws #ProtocolError on error.
*/
unsigned
ParseInsertPosition(const char *s, const playlist &playlist);
#endif
unsigned
ParseMoveDestination(const char *s, const RangeArg range, const playlist &p);

@@ -19,6 +19,7 @@
#include "config.h"
#include "QueueCommands.hxx"
#include "PositionArg.hxx"
#include "Request.hxx"
#include "protocol/RangeArg.hxx"
#include "db/DatabaseQueue.hxx"
@@ -43,17 +44,6 @@
#include <limits>
static unsigned
RequireCurrentPosition(const playlist &p)
{
int position = p.GetCurrentPosition();
if (position < 0)
throw ProtocolError(ACK_ERROR_PLAYER_SYNC,
"No current song");
return position;
}
static void
AddUri(Client &client, const LocatedUri &uri)
{
@@ -129,30 +119,8 @@ handle_addid(Client &client, Request args, Response &r)
const auto queue_length = partition.playlist.queue.GetLength();
if (args.size > 1) {
const char *const s = args[1];
if (*s == '+') {
/* after the current song */
const unsigned current =
RequireCurrentPosition(partition.playlist);
assert(current < queue_length);
to = current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - 1);
} else if (*s == '-') {
/* before the current song */
const unsigned current =
RequireCurrentPosition(partition.playlist);
assert(current < queue_length);
to = current - ParseCommandArgUnsigned(s + 1, current);
} else
/* absolute position */
to = args.ParseUnsigned(1, queue_length);
}
if (args.size > 1)
to = ParseInsertPosition(args[1], partition.playlist);
const SongLoader loader(client);
const unsigned added_position = queue_length;
@@ -363,49 +331,6 @@ handle_prioid(Client &client, Request args, [[maybe_unused]] Response &r)
return CommandResult::OK;
}
static unsigned
ParseMoveDestination(const char *s, const RangeArg range,
const playlist &p)
{
assert(!range.IsEmpty());
assert(!range.IsOpenEnded());
const unsigned queue_length = p.queue.GetLength();
if (*s == '+') {
/* after the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current + 1 +
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else if (*s == '-') {
/* before the current song */
unsigned current = RequireCurrentPosition(p);
assert(current < queue_length);
if (range.Contains(current))
throw ProtocolError(ACK_ERROR_ARG, "Cannot move current song relative to itself");
if (current >= range.end)
current -= range.Count();
return current -
ParseCommandArgUnsigned(s + 1,
queue_length - current - range.Count());
} else
/* absolute position */
return ParseCommandArgUnsigned(s,
queue_length - range.Count());
}
static CommandResult
handle_move(Partition &partition, RangeArg range, const char *to)
{

@@ -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());
}
}

@@ -27,6 +27,7 @@
#include "pcm/Volume.hxx"
#include "util/ConstBuffer.hxx"
#include "util/Domain.hxx"
#include "Idle.hxx"
#include "Log.hxx"
#include <cassert>
@@ -169,6 +170,10 @@ ReplayGainFilter::Update()
try {
mixer_set_volume(mixer, _volume);
/* TODO: emit this idle event only for the
current partition */
idle_add(IDLE_MIXER);
} catch (...) {
LogError(std::current_exception(),
"Failed to update hardware mixer");

@@ -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

@@ -0,0 +1,45 @@
/*
* 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.
*/
#pragma once
#include <system_error>
struct AvahiClient;
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,
)

@@ -51,6 +51,7 @@ upnp = static_library(
'Util.cxx',
include_directories: inc,
dependencies: [
log_dep,
upnp_dep,
curl_dep,
expat_dep,

@@ -37,6 +37,8 @@ public:
{
}
~PipeWireMixer() noexcept override;
PipeWireMixer(const PipeWireMixer &) = delete;
PipeWireMixer &operator=(const PipeWireMixer &) = delete;
@@ -82,7 +84,14 @@ pipewire_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao,
const ConfigBlock &)
{
auto &po = (PipeWireOutput &)ao;
return new PipeWireMixer(po, listener);
auto *pm = new PipeWireMixer(po, listener);
pipewire_output_set_mixer(po, *pm);
return pm;
}
PipeWireMixer::~PipeWireMixer() noexcept
{
pipewire_output_clear_mixer(output, *this);
}
const MixerPlugin pipewire_mixer_plugin = {

@@ -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;
};

@@ -33,6 +33,8 @@
#include <libsmbclient.h>
#include <cerrno>
#include <cstring>
#include <utility>
class SmbclientNeighborExplorer final : public NeighborExplorer {
@@ -45,12 +47,12 @@ class SmbclientNeighborExplorer final : public NeighborExplorer {
Server(const Server &) = delete;
gcc_pure
[[gnu::pure]]
bool operator==(const Server &other) const noexcept {
return name == other.name;
}
[[nodiscard]] gcc_pure
[[nodiscard]] [[gnu::pure]]
NeighborInfo Export() const noexcept {
return { "smb://" + name + "/", comment };
}
@@ -165,11 +167,11 @@ ReadServers(SmbclientContext &ctx, const char *uri,
ReadServers(ctx, handle, list);
ctx.CloseDirectory(handle);
} else
FormatErrno(smbclient_domain, "smbc_opendir('%s') failed",
uri);
FmtError(smbclient_domain, "smbc_opendir('{}') failed: {}",
uri, strerror(errno));
}
gcc_pure
[[gnu::pure]]
static NeighborExplorer::List
DetectServers(SmbclientContext &ctx) noexcept
{
@@ -178,7 +180,7 @@ DetectServers(SmbclientContext &ctx) noexcept
return list;
}
gcc_pure
[[gnu::pure]]
static NeighborExplorer::List::iterator
FindBeforeServerByURI(NeighborExplorer::List::iterator prev,
NeighborExplorer::List::iterator end,

@@ -25,6 +25,7 @@ neighbor_plugins = static_library(
neighbor_plugins_sources,
include_directories: inc,
dependencies: [
log_dep,
dbus_dep,
smbclient_dep,
upnp_dep,

@@ -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
@@ -41,13 +46,17 @@
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
#include <cmath>
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#include <boost/lockfree/spsc_queue.hpp>
#include <algorithm>
#include <stdexcept>
#include <string>
static constexpr Domain pipewire_output_domain("pipewire_output");
@@ -60,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;
@@ -75,11 +86,39 @@ class PipeWireOutput final : AudioOutput {
float volume = 1.0;
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;
/**
@@ -124,15 +163,29 @@ public:
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)
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,
@@ -163,6 +216,46 @@ private:
o.Drained();
}
void ControlInfo(const struct pw_stream_control *control) noexcept {
float sum = 0;
unsigned c;
for (c = 0; c < control->n_values; c++)
sum += control->values[c];
sum /= control->n_values;
if (mixer != nullptr)
pipewire_mixer_on_change(*mixer, std::cbrt(sum));
pw_thread_loop_signal(thread_loop, false);
}
static void ControlInfo(void *data,
[[maybe_unused]] uint32_t id,
const struct pw_stream_control *control) noexcept {
auto &o = *(PipeWireOutput *)data;
if (StringIsEqual(control->name, "Channel Volumes"))
o.ControlInfo(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 == NULL)
return;
auto &o = *(PipeWireOutput *)data;
o.ParamChanged(id, param);
}
/* virtual methods from class AudioOutput */
void Enable() override;
void Disable() noexcept override;
@@ -185,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();
@@ -195,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))
@@ -214,11 +312,17 @@ PipeWireOutput::SetVolume(float _volume)
{
const PipeWire::ThreadLoopLock lock(thread_loop);
if (stream != nullptr && !restore_volume &&
pw_stream_set_control(stream,
SPA_PROP_volume, 1, &_volume,
float newvol = _volume*_volume*_volume;
if (stream != nullptr && !restore_volume) {
float vol[MAX_CHANNELS];
std::fill_n(vol, channels, newvol);
if (pw_stream_set_control(stream,
SPA_PROP_channelVolumes, channels, vol,
0) != 0)
throw std::runtime_error("pw_stream_set_control() failed");
throw std::runtime_error("pw_stream_set_control() failed");
}
volume = _volume;
}
@@ -228,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);
}
@@ -357,6 +461,7 @@ ToPipeWireAudioFormat(AudioFormat &audio_format) noexcept
void
PipeWireOutput::Open(AudioFormat &audio_format)
{
error_message.clear();
disconnected = false;
restore_volume = true;
@@ -383,6 +488,13 @@ PipeWireOutput::Open(AudioFormat &audio_format)
if (target != nullptr && target_id == PW_ID_ANY)
pw_properties_setf(props, PW_KEY_NODE_TARGET, "%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),
@@ -391,12 +503,30 @@ 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;
interrupted = false;
/* allocate a ring buffer of 0.5 seconds */
@@ -407,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
@@ -441,19 +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)
pw_thread_loop_signal(thread_loop, false);
if (!was_disconnected && disconnected) {
if (error != nullptr)
error_message = error;
if (state == PW_STREAM_STATE_STREAMING && restore_volume) {
/* restore the last known volume after creating a new
pw_stream */
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;
pw_stream_set_control(stream,
SPA_PROP_volume, 1, &volume,
0);
}
#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
{
@@ -463,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);
@@ -485,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);
@@ -578,6 +855,61 @@ PipeWireOutput::Pause() noexcept
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();
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
{
po.SetMixer(pm);
}
void
pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept
{
po.ClearMixer(pm);
}
const struct AudioOutputPlugin pipewire_output_plugin = {
"pipewire",
nullptr,

@@ -21,9 +21,16 @@
#define MPD_PIPEWIRE_OUTPUT_PLUGIN_HXX
class PipeWireOutput;
class PipeWireMixer;
extern const struct AudioOutputPlugin pipewire_output_plugin;
void
pipewire_output_set_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept;
void
pipewire_output_clear_mixer(PipeWireOutput &po, PipeWireMixer &pm) noexcept;
void
pipewire_output_set_volume(PipeWireOutput &output, float volume);

@@ -45,7 +45,7 @@ struct SnapcastTimestamp {
if (a_usec < b_usec) {
--result_sec;
result_usec += 1'000'0000;
result_usec += 1'000'000;
}
return {result_sec, result_usec};

@@ -228,22 +228,22 @@ SoxrPcmResampler::Open(AudioFormat &af, unsigned new_sample_rate)
FmtDebug(soxr_domain, "soxr engine '{}'", soxr_engine(soxr));
if (soxr_use_custom_recipe)
FmtDebug(soxr_domain,
"soxr precision={:0.0}, phase_response={:0.2}, "
"passband_end={:0.2}, stopband_begin={:0.2} scale={:0.2}",
"soxr precision={:0.0f}, phase_response={:0.2f}, "
"passband_end={:0.2f}, stopband_begin={:0.2f} scale={:0.2f}",
soxr_quality.precision, soxr_quality.phase_response,
soxr_quality.passband_end, soxr_quality.stopband_begin,
soxr_io_custom_recipe.scale);
else
FmtDebug(soxr_domain,
"soxr precision={:0.0}, phase_response={:0.2}, "
"passband_end={:0.2}, stopband_begin={:0.2}",
"soxr precision={:0.0f}, phase_response={:0.2f}, "
"passband_end={:0.2f}, stopband_begin={:0.2f}",
soxr_quality.precision, soxr_quality.phase_response,
soxr_quality.passband_end, soxr_quality.stopband_begin);
channels = af.channels;
ratio = float(new_sample_rate) / float(af.sample_rate);
FmtDebug(soxr_domain, "samplerate conversion ratio to {:.2}", ratio);
FmtDebug(soxr_domain, "samplerate conversion ratio to {:0.2f}", ratio);
/* libsoxr works with floating point samples */
af.format = SampleFormat::FLOAT;

@@ -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

@@ -5,6 +5,7 @@ playlist_plugins_sources = [
]
playlist_plugins_deps = [
log_dep,
expat_dep,
flac_dep,
]

@@ -23,6 +23,7 @@
#include "Chrono.hxx"
#include "util/NumberParser.hxx"
#include <stdio.h>
#include <stdlib.h>
static inline ProtocolError

@@ -186,15 +186,15 @@ SmbclientDirectoryReader::GetInfo([[maybe_unused]] bool follow)
static std::unique_ptr<Storage>
CreateSmbclientStorageURI([[maybe_unused]] EventLoop &event_loop, const char *base)
{
if (!StringStartsWithCaseASCII(base, "smb://"))
return nullptr;
SmbclientInit();
return std::make_unique<SmbclientStorage>(base);
}
static constexpr const char *smbclient_prefixes[] = { "smb://", nullptr };
const StoragePlugin smbclient_storage_plugin = {
"smbclient",
smbclient_prefixes,
CreateSmbclientStorageURI,
};

@@ -44,6 +44,7 @@ storage_plugins = static_library(
storage_plugins_sources,
include_directories: inc,
dependencies: [
log_dep,
curl_dep,
dbus_dep,
expat_dep,

@@ -13,6 +13,7 @@ avahi = static_library(
'Publisher.cxx',
include_directories: inc,
dependencies: [
log_dep,
libavahi_client,
],
)

@@ -127,6 +127,7 @@ if enable_inotify
'../src/db/update/InotifySource.cxx',
include_directories: inc,
dependencies: [
log_dep,
event_dep,
util_dep,
],