Compare commits
51 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b7fdff46f2 | ||
![]() |
e16109330d | ||
![]() |
72621531e0 | ||
![]() |
0a48146efc | ||
![]() |
0c4bf12bfd | ||
![]() |
b8e0855ef3 | ||
![]() |
6467502b9d | ||
![]() |
15b67f20e5 | ||
![]() |
0825179f00 | ||
![]() |
97211d0aad | ||
![]() |
029c499bfa | ||
![]() |
0ba867ec16 | ||
![]() |
866d147122 | ||
![]() |
32851d1bc7 | ||
![]() |
78257408b4 | ||
![]() |
f447b7615e | ||
![]() |
1f780b7209 | ||
![]() |
04bf8a6b1a | ||
![]() |
c4c64854d4 | ||
![]() |
17562dc90b | ||
![]() |
7b24316734 | ||
![]() |
5fab107fd3 | ||
![]() |
f31920e092 | ||
![]() |
eb111a10e7 | ||
![]() |
80b09360c6 | ||
![]() |
5ccf78855d | ||
![]() |
fd5a3b5880 | ||
![]() |
6120c1360c | ||
![]() |
a8087dc12c | ||
![]() |
070c03dbf7 | ||
![]() |
0a9bec3754 | ||
![]() |
fff25ac753 | ||
![]() |
4f1e79b6b8 | ||
![]() |
aa9933c0b5 | ||
![]() |
0697d1f859 | ||
![]() |
df033fa4aa | ||
![]() |
b941a7df83 | ||
![]() |
31151cec3c | ||
![]() |
07e8c338df | ||
![]() |
b22d7218aa | ||
![]() |
d5be8c74b0 | ||
![]() |
c112cb60da | ||
![]() |
677fa4f9bc | ||
![]() |
907af2ad02 | ||
![]() |
6a2e7bbc02 | ||
![]() |
771c46032f | ||
![]() |
85611aa456 | ||
![]() |
466b5cb08d | ||
![]() |
3f2f3251cb | ||
![]() |
8ae85f3991 | ||
![]() |
781fe4ff28 |
NEWS
android
doc
meson.buildsrc
Permission.cxxPermission.hxxSongPrint.cxxTagPrint.cxx
client
command
ClientCommands.cxxPlayerCommands.cxxPlaylistCommands.cxxPositionArg.cxxPositionArg.hxxQueueCommands.cxx
event
filter
plugins
input
lib
mixer
plugins
neighbor
output
Thread.cxx
plugins
pcm
player
playlist
plugins
protocol
storage
zeroconf
avahi
test
26
NEWS
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
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
|
45
src/lib/pipewire/Error.hxx
Normal file
45
src/lib/pipewire/Error.hxx
Normal file
@@ -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 ¶m) 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 ¶m) noexcept
|
||||
{
|
||||
uint32_t media_type, media_subtype;
|
||||
struct spa_audio_info_dsd dsd;
|
||||
|
||||
if (spa_format_parse(¶m, &media_type, &media_subtype) >= 0 &&
|
||||
media_type == SPA_MEDIA_TYPE_audio &&
|
||||
media_subtype == SPA_MEDIA_SUBTYPE_dsd &&
|
||||
spa_format_audio_dsd_parse(¶m, &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,
|
||||
],
|
||||
|
Reference in New Issue
Block a user