Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
19a101c3ac | ||
![]() |
8da17a8211 | ||
![]() |
2748929039 | ||
![]() |
0c900a4bfa | ||
![]() |
f1d5d70010 | ||
![]() |
56ebc7637d | ||
![]() |
996dd9fc8b | ||
![]() |
056514d598 | ||
![]() |
9a21bdfd6a | ||
![]() |
03f99dd26e | ||
![]() |
bfb1b641f9 | ||
![]() |
72ba98c464 | ||
![]() |
dcd19c0592 | ||
![]() |
109159e0f7 | ||
![]() |
409b877eea | ||
![]() |
c5bf7948ff | ||
![]() |
b9f7127691 | ||
![]() |
1e6f5f012c | ||
![]() |
225d85fd9b | ||
![]() |
1bb22f118d | ||
![]() |
552c30eae4 | ||
![]() |
48e8a26813 | ||
![]() |
ade847bc89 | ||
![]() |
a6173e0eae | ||
![]() |
4529bb4a83 | ||
![]() |
258ecb764f | ||
![]() |
6f595e9abb | ||
![]() |
35c4c7e8bf | ||
![]() |
293ed924d1 | ||
![]() |
c8121176b3 | ||
![]() |
ee270f9b00 | ||
![]() |
bf1d77a4d8 | ||
![]() |
a9344fafe9 | ||
![]() |
b8890726f2 | ||
![]() |
0f84332654 | ||
![]() |
46c82259f7 | ||
![]() |
2d03823283 | ||
![]() |
bba144eca5 | ||
![]() |
9af73dad93 | ||
![]() |
f0d66bf6a6 | ||
![]() |
5ad53a7554 | ||
![]() |
7b2e3331f2 | ||
![]() |
3cb44f6652 | ||
![]() |
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.buildpython/build
src
Listen.cxxPermission.cxxPermission.hxxPlaylistFile.cxxPlaylistFile.hxxSongPrint.cxxTagPrint.cxx
client
command
AllCommands.cxxClientCommands.cxxFingerprintCommands.cxxPartitionCommands.cxxPlayerCommands.cxxPlaylistCommands.cxxPositionArg.cxxPositionArg.hxxQueueCommands.cxx
config
db
decoder
event
filter
plugins
fs
input
lib
alsa
nfs
pipewire
upnp
mixer
plugins
neighbor
output
pcm
player
playlist
plugins
protocol
storage
zeroconf
systemd
test
38
NEWS
38
NEWS
@@ -1,3 +1,41 @@
|
||||
ver 0.23.3 (2021/10/31)
|
||||
* protocol
|
||||
- add optional position parameter to "add" and "playlistadd"
|
||||
- allow range in "playlistdelete"
|
||||
* database
|
||||
- fix scanning files with question mark in the name
|
||||
- inotify: fix use-after-free bug
|
||||
* output
|
||||
- alsa: add option "stop_dsd_silence" to work around DSD DAC noise
|
||||
* macOS: fix libfmt related build failure
|
||||
* systemd: add "RuntimeDirectory" directive
|
||||
|
||||
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="63"
|
||||
android:versionName="0.23.3">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
|
||||
|
||||
|
@@ -13,7 +13,7 @@ GENCLASS="$D/classes"
|
||||
GENINCLUDE="$D/include"
|
||||
|
||||
mkdir -p "$GENSRC/$JAVA_PKG_PATH"
|
||||
"$JAVAC" -source 1.6 -target 1.6 -Xlint:-options \
|
||||
"$JAVAC" -source 1.7 -target 1.7 -Xlint:-options \
|
||||
-cp "$CLASSPATH" \
|
||||
-h "$GENINCLUDE" \
|
||||
-d "$GENCLASS" \
|
||||
|
@@ -38,9 +38,9 @@ author = 'Max Kellermann'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.23'
|
||||
version = '0.23.3'
|
||||
# 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.
|
||||
|
@@ -836,6 +836,11 @@ The `Advanced Linux Sound Architecture (ALSA) <http://www.alsa-project.org/>`_ p
|
||||
- If set to no, then libasound will not attempt to convert between different sample formats (16 bit, 24 bit, floating point, ...).
|
||||
* - **dop yes|no**
|
||||
- If set to yes, then DSD over PCM according to the `DoP standard <http://dsd-guide.com/dop-open-standard>`_ is enabled. This wraps DSD samples in fake 24 bit PCM, and is understood by some DSD capable products, but may be harmful to other hardware. Therefore, the default is no and you can enable the option at your own risk.
|
||||
* - **stop_dsd_silence yes|no**
|
||||
- If enabled, silence is played before manually stopping playback
|
||||
("stop" or "pause") in DSD mode (native DSD or DoP). This is a
|
||||
workaround for some DACs which emit noise when stopping DSD
|
||||
playback.
|
||||
* - **allowed_formats F1 F2 ...**
|
||||
- Specifies a list of allowed audio formats, separated by a space. All items may contain asterisks as a wild card, and may be followed by "=dop" to enable DoP (DSD over PCM) for this particular format. The first matching format is used, and if none matches, MPD chooses the best fallback of this list.
|
||||
|
||||
@@ -1094,6 +1099,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 +1200,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
|
||||
|
@@ -551,7 +551,7 @@ Playback options
|
||||
|
||||
.. _command_getvol:
|
||||
|
||||
:command:`getvol`
|
||||
:command:`getvol` [#since_0_23]_
|
||||
|
||||
Read the volume. The result is a ``volume:`` line like in
|
||||
:ref:`status <command_status>`. If there is no mixer, MPD will
|
||||
@@ -689,11 +689,14 @@ Whenever possible, ids should be used.
|
||||
|
||||
.. _command_add:
|
||||
|
||||
:command:`add {URI}`
|
||||
:command:`add {URI} [POSITION]`
|
||||
Adds the file ``URI`` to the playlist
|
||||
(directories add recursively). ``URI``
|
||||
can also be a single file.
|
||||
|
||||
The position parameter is the same as in :ref:`addid
|
||||
<command_addid>`. [#since_0_23_3]_
|
||||
|
||||
Clients that are connected via local socket may add arbitrary
|
||||
local files (URI is an absolute path). Example::
|
||||
|
||||
@@ -711,10 +714,10 @@ Whenever possible, ids should be used.
|
||||
|
||||
If the second parameter is given, then the song is inserted at the
|
||||
specified position. If the parameter starts with ``+`` or ``-``,
|
||||
then it is relative to the current song; e.g. ``+0`` inserts right
|
||||
after the current song and ``-0`` inserts right before the current
|
||||
song (i.e. zero songs between the current song and the newly added
|
||||
song).
|
||||
then it is relative to the current song [#since_0_23]_; e.g. ``+0``
|
||||
inserts right after the current song and ``-0`` inserts right
|
||||
before the current song (i.e. zero songs between the current song
|
||||
and the newly added song).
|
||||
|
||||
.. _command_clear:
|
||||
|
||||
@@ -923,18 +926,22 @@ 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
|
||||
shall be loaded at a certain queue position.)
|
||||
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.) [#since_0_23_1]_
|
||||
|
||||
.. _command_playlistadd:
|
||||
|
||||
:command:`playlistadd {NAME} {URI}`
|
||||
:command:`playlistadd {NAME} {URI} [POSITION]`
|
||||
Adds ``URI`` to the playlist
|
||||
`NAME.m3u`.
|
||||
`NAME.m3u` will be created if it does
|
||||
not exist.
|
||||
|
||||
The ``POSITION`` parameter specifies where the songs will be
|
||||
inserted into the playlist. [#since_0_23_3]_
|
||||
|
||||
.. _command_playlistclear:
|
||||
|
||||
:command:`playlistclear {NAME}`
|
||||
@@ -946,6 +953,8 @@ remote playlists (absolute URI with a supported scheme).
|
||||
Deletes ``SONGPOS`` from the
|
||||
playlist `NAME.m3u`.
|
||||
|
||||
The second parameter can be a range. [#since_0_23_3]_
|
||||
|
||||
.. _command_playlistmove:
|
||||
|
||||
:command:`playlistmove {NAME} {FROM} {TO}`
|
||||
@@ -1059,11 +1068,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 +1205,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 +1220,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. [#since_0_23]_
|
||||
|
||||
.. _command_searchaddpl:
|
||||
|
||||
:command:`searchaddpl {NAME} {FILTER} [sort {TYPE}] [window {START:END}]`
|
||||
@@ -1643,3 +1652,6 @@ client-to-client messages are local to the current partition.
|
||||
.. [#since_0_20] Since :program:`MPD` 0.20
|
||||
.. [#since_0_21] Since :program:`MPD` 0.21
|
||||
.. [#since_0_22_4] Since :program:`MPD` 0.22.4
|
||||
.. [#since_0_23] Since :program:`MPD` 0.23
|
||||
.. [#since_0_23_1] Since :program:`MPD` 0.23.1
|
||||
.. [#since_0_23_3] Since :program:`MPD` 0.23.3
|
||||
|
15
doc/user.rst
15
doc/user.rst
@@ -172,7 +172,9 @@ tarball and change into the directory. Then, instead of
|
||||
|
||||
mkdir -p output/win64
|
||||
cd output/win64
|
||||
../../win32/build.py --64
|
||||
../../win32/build.py --64 \
|
||||
--buildtype=debugoptimized -Db_ndebug=true \
|
||||
-Dwrap_mode=forcefallback
|
||||
|
||||
This downloads various library sources, and then configures and builds
|
||||
:program:`MPD` (for x64; to build a 32 bit binary, pass
|
||||
@@ -182,6 +184,11 @@ around. It is large, but easy to use. If you wish to have a small
|
||||
mpd.exe with DLLs, you need to compile manually, without the
|
||||
:file:`build.py` script.
|
||||
|
||||
The option ``-Dwrap_mode=forcefallback`` tells Meson to download and
|
||||
cross-compile several libraries used by MPD instead of looking for
|
||||
them on your computer.
|
||||
|
||||
|
||||
Compiling for Android
|
||||
---------------------
|
||||
|
||||
@@ -205,8 +212,10 @@ tarball and change into the directory. Then, instead of
|
||||
|
||||
mkdir -p output/android
|
||||
cd output/android
|
||||
../../android/build.py SDK_PATH NDK_PATH ABI
|
||||
meson configure -Dandroid_debug_keystore=$HOME/.android/debug.keystore
|
||||
../../android/build.py SDK_PATH NDK_PATH ABI \
|
||||
--buildtype=debugoptimized -Db_ndebug=true \
|
||||
-Dwrap_mode=forcefallback \
|
||||
-Dandroid_debug_keystore=$HOME/.android/debug.keystore
|
||||
ninja android/apk/mpd-debug.apk
|
||||
|
||||
:envvar:`SDK_PATH` is the absolute path where you installed the
|
||||
|
@@ -1,7 +1,7 @@
|
||||
project(
|
||||
'mpd',
|
||||
['c', 'cpp'],
|
||||
version: '0.23',
|
||||
version: '0.23.3',
|
||||
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.3')
|
||||
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',
|
||||
|
@@ -112,8 +112,8 @@ libmodplug = AutotoolsProject(
|
||||
)
|
||||
|
||||
libopenmpt = AutotoolsProject(
|
||||
'https://lib.openmpt.org/files/libopenmpt/src/libopenmpt-0.5.8+release.autotools.tar.gz',
|
||||
'61de7cc0c011b10472ca16adcc123689',
|
||||
'https://lib.openmpt.org/files/libopenmpt/src/libopenmpt-0.5.12+release.autotools.tar.gz',
|
||||
'892aea7a599b5d21842bebf463b5aafdad5711be7008dd84401920c6234820af',
|
||||
'lib/libopenmpt.a',
|
||||
[
|
||||
'--disable-shared', '--enable-static'
|
||||
@@ -147,8 +147,8 @@ gme = CmakeProject(
|
||||
)
|
||||
|
||||
ffmpeg = FfmpegProject(
|
||||
'http://ffmpeg.org/releases/ffmpeg-4.4.tar.xz',
|
||||
'06b10a183ce5371f915c6bb15b7b1fffbe046e8275099c96affc29e17645d909',
|
||||
'http://ffmpeg.org/releases/ffmpeg-4.4.1.tar.xz',
|
||||
'eadbad9e9ab30b25f5520fbfde99fae4a92a1ae3c0257a8d68569a4651e30e02',
|
||||
'lib/libavcodec.a',
|
||||
[
|
||||
'--disable-shared', '--enable-static',
|
||||
|
@@ -32,6 +32,7 @@
|
||||
#include "net/SocketUtil.hxx"
|
||||
#include "system/Error.hxx"
|
||||
#include "fs/AllocatedPath.hxx"
|
||||
#include "fs/StandardDirectory.hxx"
|
||||
#include "fs/XDG.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
@@ -85,13 +86,10 @@ ListenXdgRuntimeDir(ClientListener &listener) noexcept
|
||||
use $XDG_RUNTIME_DIR */
|
||||
return false;
|
||||
|
||||
Path xdg_runtime_dir = Path::FromFS(getenv("XDG_RUNTIME_DIR"));
|
||||
if (xdg_runtime_dir.IsNull())
|
||||
const auto mpd_runtime_dir = GetAppRuntimeDir();
|
||||
if (mpd_runtime_dir.IsNull())
|
||||
return false;
|
||||
|
||||
const auto mpd_runtime_dir = xdg_runtime_dir / Path::FromFS("mpd");
|
||||
mkdir(mpd_runtime_dir.c_str(), 0700);
|
||||
|
||||
const auto socket_path = mpd_runtime_dir / Path::FromFS("socket");
|
||||
unlink(socket_path.c_str());
|
||||
|
||||
|
@@ -30,7 +30,6 @@
|
||||
#include "util/StringView.hxx"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -100,18 +99,15 @@ initPermissions(const ConfigData &config)
|
||||
for (const auto ¶m : config.GetParamList(ConfigOption::PASSWORD)) {
|
||||
permission_default = 0;
|
||||
|
||||
param.With([](const char *value){
|
||||
const char *separator = std::strchr(value,
|
||||
PERMISSION_PASSWORD_CHAR);
|
||||
|
||||
if (separator == nullptr)
|
||||
param.With([](const StringView value){
|
||||
const auto [password, permissions] =
|
||||
value.Split(PERMISSION_PASSWORD_CHAR);
|
||||
if (permissions == nullptr)
|
||||
throw FormatRuntimeError("\"%c\" not found in password string",
|
||||
PERMISSION_PASSWORD_CHAR);
|
||||
|
||||
std::string password(value, separator);
|
||||
|
||||
unsigned permission = parsePermissions(separator + 1);
|
||||
permission_passwords.emplace(std::move(password), permission);
|
||||
permission_passwords.emplace(password,
|
||||
parsePermissions(permissions));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,15 +157,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
|
||||
|
@@ -26,6 +26,7 @@
|
||||
#include "song/DetachedSong.hxx"
|
||||
#include "SongLoader.hxx"
|
||||
#include "Mapper.hxx"
|
||||
#include "protocol/RangeArg.hxx"
|
||||
#include "fs/io/TextFile.hxx"
|
||||
#include "fs/io/FileOutputStream.hxx"
|
||||
#include "fs/io/BufferedOutputStream.hxx"
|
||||
@@ -34,7 +35,6 @@
|
||||
#include "config/Defaults.hxx"
|
||||
#include "Idle.hxx"
|
||||
#include "fs/Limits.hxx"
|
||||
#include "fs/AllocatedPath.hxx"
|
||||
#include "fs/Traits.hxx"
|
||||
#include "fs/FileSystem.hxx"
|
||||
#include "fs/FileInfo.hxx"
|
||||
@@ -173,11 +173,8 @@ ListPlaylistFiles()
|
||||
}
|
||||
|
||||
static void
|
||||
SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path)
|
||||
SavePlaylistFile(Path path_fs, const PlaylistFileContents &contents)
|
||||
{
|
||||
assert(utf8path != nullptr);
|
||||
|
||||
const auto path_fs = spl_map_to_fs(utf8path);
|
||||
assert(!path_fs.IsNull());
|
||||
|
||||
FileOutputStream fos(path_fs);
|
||||
@@ -191,12 +188,11 @@ SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path)
|
||||
fos.Commit();
|
||||
}
|
||||
|
||||
PlaylistFileContents
|
||||
LoadPlaylistFile(const char *utf8path)
|
||||
static PlaylistFileContents
|
||||
LoadPlaylistFile(Path path_fs)
|
||||
try {
|
||||
PlaylistFileContents contents;
|
||||
|
||||
const auto path_fs = spl_map_to_fs(utf8path);
|
||||
assert(!path_fs.IsNull());
|
||||
|
||||
TextFile file(path_fs);
|
||||
@@ -251,16 +247,54 @@ try {
|
||||
throw;
|
||||
}
|
||||
|
||||
void
|
||||
spl_move_index(const char *utf8path, unsigned src, unsigned dest)
|
||||
static PlaylistFileContents
|
||||
MaybeLoadPlaylistFile(Path path_fs, PlaylistFileEditor::LoadMode load_mode)
|
||||
try {
|
||||
if (load_mode == PlaylistFileEditor::LoadMode::NO)
|
||||
return {};
|
||||
|
||||
return LoadPlaylistFile(path_fs);
|
||||
} catch (const PlaylistError &error) {
|
||||
if (error.GetCode() == PlaylistResult::NO_SUCH_LIST &&
|
||||
load_mode == PlaylistFileEditor::LoadMode::TRY)
|
||||
return {};
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
PlaylistFileEditor::PlaylistFileEditor(const char *name_utf8,
|
||||
LoadMode load_mode)
|
||||
:path(spl_map_to_fs(name_utf8)),
|
||||
contents(MaybeLoadPlaylistFile(path, load_mode))
|
||||
{
|
||||
if (src == dest)
|
||||
/* this doesn't check whether the playlist exists, but
|
||||
what the hell.. */
|
||||
return;
|
||||
}
|
||||
|
||||
auto contents = LoadPlaylistFile(utf8path);
|
||||
void
|
||||
PlaylistFileEditor::Insert(std::size_t i, const char *uri)
|
||||
{
|
||||
if (i > size())
|
||||
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad position");
|
||||
|
||||
if (size() >= playlist_max_length)
|
||||
throw PlaylistError(PlaylistResult::TOO_LARGE,
|
||||
"Stored playlist is too large");
|
||||
|
||||
contents.emplace(std::next(contents.begin(), i), uri);
|
||||
}
|
||||
|
||||
void
|
||||
PlaylistFileEditor::Insert(std::size_t i, const DetachedSong &song)
|
||||
{
|
||||
const char *uri = playlist_saveAbsolutePaths
|
||||
? song.GetRealURI()
|
||||
: song.GetURI();
|
||||
|
||||
Insert(i, uri);
|
||||
}
|
||||
|
||||
void
|
||||
PlaylistFileEditor::MoveIndex(unsigned src, unsigned dest)
|
||||
{
|
||||
if (src >= contents.size() || dest >= contents.size())
|
||||
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
|
||||
|
||||
@@ -270,9 +304,31 @@ spl_move_index(const char *utf8path, unsigned src, unsigned dest)
|
||||
|
||||
const auto dest_i = std::next(contents.begin(), dest);
|
||||
contents.insert(dest_i, std::move(value));
|
||||
}
|
||||
|
||||
SavePlaylistFile(contents, utf8path);
|
||||
void
|
||||
PlaylistFileEditor::RemoveIndex(unsigned i)
|
||||
{
|
||||
if (i >= contents.size())
|
||||
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
|
||||
|
||||
contents.erase(std::next(contents.begin(), i));
|
||||
}
|
||||
|
||||
void
|
||||
PlaylistFileEditor::RemoveRange(RangeArg range)
|
||||
{
|
||||
if (!range.CheckClip(size()))
|
||||
throw PlaylistError::BadRange();
|
||||
|
||||
contents.erase(std::next(contents.begin(), range.start),
|
||||
std::next(contents.begin(), range.end));
|
||||
}
|
||||
|
||||
void
|
||||
PlaylistFileEditor::Save()
|
||||
{
|
||||
SavePlaylistFile(path, contents);
|
||||
idle_add(IDLE_STORED_PLAYLIST);
|
||||
}
|
||||
|
||||
@@ -314,20 +370,6 @@ spl_delete(const char *name_utf8)
|
||||
idle_add(IDLE_STORED_PLAYLIST);
|
||||
}
|
||||
|
||||
void
|
||||
spl_remove_index(const char *utf8path, unsigned pos)
|
||||
{
|
||||
auto contents = LoadPlaylistFile(utf8path);
|
||||
|
||||
if (pos >= contents.size())
|
||||
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
|
||||
|
||||
contents.erase(std::next(contents.begin(), pos));
|
||||
|
||||
SavePlaylistFile(contents, utf8path);
|
||||
idle_add(IDLE_STORED_PLAYLIST);
|
||||
}
|
||||
|
||||
void
|
||||
spl_append_song(const char *utf8path, const DetachedSong &song)
|
||||
try {
|
||||
|
@@ -20,19 +20,55 @@
|
||||
#ifndef MPD_PLAYLIST_FILE_HXX
|
||||
#define MPD_PLAYLIST_FILE_HXX
|
||||
|
||||
#include "fs/AllocatedPath.hxx"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
struct ConfigData;
|
||||
struct RangeArg;
|
||||
class DetachedSong;
|
||||
class SongLoader;
|
||||
class PlaylistVector;
|
||||
class AllocatedPath;
|
||||
|
||||
typedef std::vector<std::string> PlaylistFileContents;
|
||||
|
||||
extern bool playlist_saveAbsolutePaths;
|
||||
|
||||
class PlaylistFileEditor {
|
||||
const AllocatedPath path;
|
||||
|
||||
PlaylistFileContents contents;
|
||||
|
||||
public:
|
||||
enum class LoadMode {
|
||||
NO,
|
||||
YES,
|
||||
TRY,
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws on error.
|
||||
*/
|
||||
explicit PlaylistFileEditor(const char *name_utf8, LoadMode load_mode);
|
||||
|
||||
auto size() const noexcept {
|
||||
return contents.size();
|
||||
}
|
||||
|
||||
void Insert(std::size_t i, const char *uri);
|
||||
void Insert(std::size_t i, const DetachedSong &song);
|
||||
|
||||
void MoveIndex(unsigned src, unsigned dest);
|
||||
void RemoveIndex(unsigned i);
|
||||
void RemoveRange(RangeArg range);
|
||||
|
||||
void Save();
|
||||
|
||||
private:
|
||||
void Load();
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform some global initialization, e.g. load configuration values.
|
||||
*/
|
||||
@@ -55,21 +91,12 @@ spl_map_to_fs(const char *name_utf8);
|
||||
PlaylistVector
|
||||
ListPlaylistFiles();
|
||||
|
||||
PlaylistFileContents
|
||||
LoadPlaylistFile(const char *utf8path);
|
||||
|
||||
void
|
||||
spl_move_index(const char *utf8path, unsigned src, unsigned dest);
|
||||
|
||||
void
|
||||
spl_clear(const char *utf8path);
|
||||
|
||||
void
|
||||
spl_delete(const char *name_utf8);
|
||||
|
||||
void
|
||||
spl_remove_index(const char *utf8path, unsigned pos);
|
||||
|
||||
void
|
||||
spl_append_song(const char *utf8path, const DetachedSong &song);
|
||||
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -85,7 +85,7 @@ handle_not_commands(Client &client, Request request, Response &response);
|
||||
* This array must be sorted!
|
||||
*/
|
||||
static constexpr struct command commands[] = {
|
||||
{ "add", PERMISSION_ADD, 1, 1, handle_add },
|
||||
{ "add", PERMISSION_ADD, 1, 2, handle_add },
|
||||
{ "addid", PERMISSION_ADD, 1, 2, handle_addid },
|
||||
{ "addtagid", PERMISSION_ADD, 3, 3, handle_addtagid },
|
||||
{ "albumart", PERMISSION_READ, 2, 2, handle_album_art },
|
||||
@@ -157,7 +157,7 @@ static constexpr struct command commands[] = {
|
||||
{ "play", PERMISSION_PLAYER, 0, 1, handle_play },
|
||||
{ "playid", PERMISSION_PLAYER, 0, 1, handle_playid },
|
||||
{ "playlist", PERMISSION_READ, 0, 0, handle_playlist },
|
||||
{ "playlistadd", PERMISSION_CONTROL, 2, 2, handle_playlistadd },
|
||||
{ "playlistadd", PERMISSION_CONTROL, 2, 3, handle_playlistadd },
|
||||
{ "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
|
||||
{ "playlistdelete", PERMISSION_CONTROL, 2, 2, handle_playlistdelete },
|
||||
{ "playlistfind", PERMISSION_READ, 1, -1, handle_playlistfind },
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -224,10 +224,12 @@ GetChromaprintCommand::DecodeFile(std::string_view suffix, InputStream &is,
|
||||
inline void
|
||||
GetChromaprintCommand::DecodeFile()
|
||||
{
|
||||
const auto suffix = uri_get_suffix(uri);
|
||||
if (suffix.empty())
|
||||
const char *_suffix = PathTraitsUTF8::GetFilenameSuffix(uri.c_str());
|
||||
if (_suffix == nullptr)
|
||||
return;
|
||||
|
||||
const std::string_view suffix{_suffix};
|
||||
|
||||
InputStreamPtr input_stream;
|
||||
|
||||
try {
|
||||
|
@@ -186,7 +186,8 @@ handle_moveoutput(Client &client, Request request, Response &response)
|
||||
was_enabled);
|
||||
else
|
||||
/* copy the AudioOutputControl and add it to the output list */
|
||||
dest_partition.outputs.AddCopy(output,was_enabled);
|
||||
dest_partition.outputs.AddMoveFrom(std::move(*output),
|
||||
was_enabled);
|
||||
|
||||
instance.EmitIdle(IDLE_OUTPUT);
|
||||
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,10 +19,13 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "PlaylistCommands.hxx"
|
||||
#include "PositionArg.hxx"
|
||||
#include "Request.hxx"
|
||||
#include "Instance.hxx"
|
||||
#include "db/Interface.hxx"
|
||||
#include "db/Selection.hxx"
|
||||
#include "db/DatabasePlaylist.hxx"
|
||||
#include "db/DatabaseSong.hxx"
|
||||
#include "PlaylistSave.hxx"
|
||||
#include "PlaylistFile.hxx"
|
||||
#include "PlaylistError.hxx"
|
||||
@@ -86,7 +89,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);
|
||||
@@ -172,9 +175,11 @@ handle_playlistdelete([[maybe_unused]] Client &client,
|
||||
Request args, [[maybe_unused]] Response &r)
|
||||
{
|
||||
const char *const name = args[0];
|
||||
unsigned from = args.ParseUnsigned(1);
|
||||
const auto range = args.ParseRange(1);
|
||||
|
||||
spl_remove_index(name, from);
|
||||
PlaylistFileEditor editor(name, PlaylistFileEditor::LoadMode::YES);
|
||||
editor.RemoveRange(range);
|
||||
editor.Save();
|
||||
return CommandResult::OK;
|
||||
}
|
||||
|
||||
@@ -186,7 +191,14 @@ handle_playlistmove([[maybe_unused]] Client &client,
|
||||
unsigned from = args.ParseUnsigned(1);
|
||||
unsigned to = args.ParseUnsigned(2);
|
||||
|
||||
spl_move_index(name, from, to);
|
||||
if (from == to)
|
||||
/* this doesn't check whether the playlist exists, but
|
||||
what the hell.. */
|
||||
return CommandResult::OK;
|
||||
|
||||
PlaylistFileEditor editor(name, PlaylistFileEditor::LoadMode::YES);
|
||||
editor.MoveIndex(from, to);
|
||||
editor.Save();
|
||||
return CommandResult::OK;
|
||||
}
|
||||
|
||||
@@ -200,12 +212,56 @@ handle_playlistclear([[maybe_unused]] Client &client,
|
||||
return CommandResult::OK;
|
||||
}
|
||||
|
||||
static CommandResult
|
||||
handle_playlistadd_position(Client &client, const char *playlist_name,
|
||||
const char *uri, unsigned position,
|
||||
Response &r)
|
||||
{
|
||||
PlaylistFileEditor editor{
|
||||
playlist_name,
|
||||
PlaylistFileEditor::LoadMode::TRY,
|
||||
};
|
||||
|
||||
if (position > editor.size()) {
|
||||
r.Error(ACK_ERROR_ARG, "Bad position");
|
||||
return CommandResult::ERROR;
|
||||
}
|
||||
|
||||
if (uri_has_scheme(uri)) {
|
||||
editor.Insert(position, uri);
|
||||
} else {
|
||||
#ifdef ENABLE_DATABASE
|
||||
const auto &db = client.GetDatabaseOrThrow();
|
||||
const auto *storage = client.GetStorage();
|
||||
const DatabaseSelection selection(uri, true, nullptr);
|
||||
|
||||
db.Visit(selection, [&editor, &position, storage](const auto &song){
|
||||
editor.Insert(position,
|
||||
DatabaseDetachSong(storage, song));
|
||||
++position;
|
||||
});
|
||||
#else
|
||||
(void)client;
|
||||
r.Error(ACK_ERROR_NO_EXIST, "No database");
|
||||
return CommandResult::ERROR;
|
||||
#endif
|
||||
}
|
||||
|
||||
editor.Save();
|
||||
|
||||
return CommandResult::OK;
|
||||
}
|
||||
|
||||
CommandResult
|
||||
handle_playlistadd(Client &client, Request args, [[maybe_unused]] Response &r)
|
||||
{
|
||||
const char *const playlist = args[0];
|
||||
const char *const uri = args[1];
|
||||
|
||||
if (args.size >= 3)
|
||||
return handle_playlistadd_position(client, playlist, uri,
|
||||
args.ParseUnsigned(2), r);
|
||||
|
||||
if (uri_has_scheme(uri)) {
|
||||
const SongLoader loader(client);
|
||||
spl_append_uri(playlist, loader, uri);
|
||||
|
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)
|
||||
{
|
||||
@@ -62,29 +52,24 @@ AddUri(Client &client, const LocatedUri &uri)
|
||||
SongLoader(client).LoadSong(uri));
|
||||
}
|
||||
|
||||
static CommandResult
|
||||
AddDatabaseSelection(Client &client, const char *uri,
|
||||
[[maybe_unused]] Response &r)
|
||||
{
|
||||
#ifdef ENABLE_DATABASE
|
||||
auto &partition = client.GetPartition();
|
||||
|
||||
static void
|
||||
AddDatabaseSelection(Partition &partition, const char *uri)
|
||||
{
|
||||
const ScopeBulkEdit bulk_edit(partition);
|
||||
|
||||
const DatabaseSelection selection(uri, true);
|
||||
AddFromDatabase(partition, selection);
|
||||
return CommandResult::OK;
|
||||
#else
|
||||
(void)client;
|
||||
(void)uri;
|
||||
|
||||
r.Error(ACK_ERROR_NO_EXIST, "No database");
|
||||
return CommandResult::ERROR;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
CommandResult
|
||||
handle_add(Client &client, Request args, Response &r)
|
||||
handle_add(Client &client, Request args, [[maybe_unused]] Response &r)
|
||||
{
|
||||
auto &partition = client.GetPartition();
|
||||
|
||||
const char *uri = args.front();
|
||||
if (StringIsEqual(uri, "/"))
|
||||
/* this URI is malformed, but some clients are buggy
|
||||
@@ -94,6 +79,11 @@ handle_add(Client &client, Request args, Response &r)
|
||||
here */
|
||||
uri = "";
|
||||
|
||||
const auto old_size = partition.playlist.GetLength();
|
||||
const unsigned position = args.size > 1
|
||||
? ParseInsertPosition(args[1], partition.playlist)
|
||||
: old_size;
|
||||
|
||||
const auto located_uri = LocateUri(UriPluginKind::INPUT, uri,
|
||||
&client
|
||||
#ifdef ENABLE_DATABASE
|
||||
@@ -104,18 +94,34 @@ handle_add(Client &client, Request args, Response &r)
|
||||
case LocatedUri::Type::ABSOLUTE:
|
||||
AddUri(client, located_uri);
|
||||
client.GetInstance().LookupRemoteTag(located_uri.canonical_uri);
|
||||
return CommandResult::OK;
|
||||
break;
|
||||
|
||||
case LocatedUri::Type::PATH:
|
||||
AddUri(client, located_uri);
|
||||
return CommandResult::OK;
|
||||
break;
|
||||
|
||||
case LocatedUri::Type::RELATIVE:
|
||||
return AddDatabaseSelection(client, located_uri.canonical_uri,
|
||||
r);
|
||||
#ifdef ENABLE_DATABASE
|
||||
AddDatabaseSelection(partition, located_uri.canonical_uri);
|
||||
break;
|
||||
#else
|
||||
r.Error(ACK_ERROR_NO_EXIST, "No database");
|
||||
return CommandResult::ERROR;
|
||||
#endif
|
||||
}
|
||||
|
||||
gcc_unreachable();
|
||||
if (position < old_size) {
|
||||
const unsigned new_size = partition.playlist.GetLength();
|
||||
const RangeArg move_range{old_size, new_size};
|
||||
|
||||
try {
|
||||
partition.MoveRange(move_range, position);
|
||||
} catch (...) {
|
||||
/* ignore - shall we handle it? */
|
||||
}
|
||||
}
|
||||
|
||||
return CommandResult::OK;
|
||||
}
|
||||
|
||||
CommandResult
|
||||
@@ -129,30 +135,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 +347,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)
|
||||
{
|
||||
|
@@ -23,11 +23,10 @@
|
||||
#include "fs/Traits.hxx"
|
||||
#include "fs/StandardDirectory.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/StringView.hxx"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <pwd.h>
|
||||
|
||||
@@ -96,30 +95,18 @@ ParsePath(const char *path)
|
||||
if (*path == '\0')
|
||||
return GetConfiguredHome();
|
||||
|
||||
AllocatedPath home = nullptr;
|
||||
|
||||
if (*path == '/') {
|
||||
home = GetConfiguredHome();
|
||||
|
||||
++path;
|
||||
|
||||
return GetConfiguredHome() /
|
||||
AllocatedPath::FromUTF8Throw(path);
|
||||
} else {
|
||||
const char *slash = std::strchr(path, '/');
|
||||
const char *end = slash == nullptr
|
||||
? path + strlen(path)
|
||||
: slash;
|
||||
const std::string user(path, end);
|
||||
home = GetHome(user.c_str());
|
||||
const auto [user, rest] =
|
||||
StringView{path}.Split('/');
|
||||
|
||||
if (slash == nullptr)
|
||||
return home;
|
||||
|
||||
path = slash + 1;
|
||||
return GetHome(std::string{user}.c_str())
|
||||
/ AllocatedPath::FromUTF8Throw(rest);
|
||||
}
|
||||
|
||||
if (home.IsNull())
|
||||
return nullptr;
|
||||
|
||||
return home / AllocatedPath::FromUTF8Throw(path);
|
||||
} else if (!PathTraitsUTF8::IsAbsolute(path)) {
|
||||
throw FormatRuntimeError("not an absolute path: %s", path);
|
||||
} else {
|
||||
|
@@ -282,7 +282,7 @@ InotifyUpdate::InotifyCallback(int wd, unsigned mask,
|
||||
(mask & IN_ISDIR) != 0) {
|
||||
/* a sub directory was changed: register those in
|
||||
inotify */
|
||||
const Path root_path = root->name;
|
||||
const auto root_path = root->name;
|
||||
|
||||
const auto path_fs = uri_fs.IsNull()
|
||||
? root_path
|
||||
|
@@ -188,8 +188,8 @@ UpdateWalk::UpdateRegularFile(Directory &directory,
|
||||
const char *name,
|
||||
const StorageFileInfo &info) noexcept
|
||||
{
|
||||
const auto suffix = uri_get_suffix(name);
|
||||
if (suffix.empty())
|
||||
const char *suffix = PathTraitsUTF8::GetFilenameSuffix(name);
|
||||
if (suffix == nullptr)
|
||||
return false;
|
||||
|
||||
return UpdateSongFile(directory, name, suffix, info) ||
|
||||
|
@@ -395,10 +395,12 @@ TryContainerDecoder(DecoderBridge &bridge, Path path_fs,
|
||||
static bool
|
||||
decoder_run_file(DecoderBridge &bridge, const char *uri_utf8, Path path_fs)
|
||||
{
|
||||
const auto suffix = uri_get_suffix(uri_utf8);
|
||||
if (suffix.empty())
|
||||
const char *_suffix = PathTraitsUTF8::GetFilenameSuffix(uri_utf8);
|
||||
if (_suffix == nullptr)
|
||||
return false;
|
||||
|
||||
const std::string_view suffix{_suffix};
|
||||
|
||||
InputStreamPtr input_stream;
|
||||
|
||||
try {
|
||||
|
@@ -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");
|
||||
|
@@ -24,6 +24,7 @@
|
||||
#include "StandardDirectory.hxx"
|
||||
#include "FileSystem.hxx"
|
||||
#include "XDG.hxx"
|
||||
#include "util/StringView.hxx"
|
||||
#include "config.h"
|
||||
|
||||
#include <array>
|
||||
@@ -228,13 +229,12 @@ GetUserConfigDir() noexcept
|
||||
return GetStandardDir(CSIDL_LOCAL_APPDATA);
|
||||
#elif defined(USE_XDG)
|
||||
// Check for $XDG_CONFIG_HOME
|
||||
auto config_home = getenv("XDG_CONFIG_HOME");
|
||||
if (IsValidPathString(config_home) && IsValidDir(config_home))
|
||||
if (const auto config_home = getenv("XDG_CONFIG_HOME");
|
||||
IsValidPathString(config_home) && IsValidDir(config_home))
|
||||
return AllocatedPath::FromFS(config_home);
|
||||
|
||||
// Check for $HOME/.config
|
||||
auto home = GetHomeDir();
|
||||
if (!home.IsNull()) {
|
||||
if (const auto home = GetHomeDir(); !home.IsNull()) {
|
||||
auto fallback = home / Path::FromFS(".config");
|
||||
if (IsValidDir(fallback.c_str()))
|
||||
return fallback;
|
||||
@@ -265,17 +265,15 @@ GetUserCacheDir() noexcept
|
||||
{
|
||||
#ifdef USE_XDG
|
||||
// Check for $XDG_CACHE_HOME
|
||||
auto cache_home = getenv("XDG_CACHE_HOME");
|
||||
if (IsValidPathString(cache_home) && IsValidDir(cache_home))
|
||||
if (const auto cache_home = getenv("XDG_CACHE_HOME");
|
||||
IsValidPathString(cache_home) && IsValidDir(cache_home))
|
||||
return AllocatedPath::FromFS(cache_home);
|
||||
|
||||
// Check for $HOME/.cache
|
||||
auto home = GetHomeDir();
|
||||
if (!home.IsNull()) {
|
||||
auto fallback = home / Path::FromFS(".cache");
|
||||
if (IsValidDir(fallback.c_str()))
|
||||
if (const auto home = GetHomeDir(); !home.IsNull())
|
||||
if (auto fallback = home / Path::FromFS(".cache");
|
||||
IsValidDir(fallback.c_str()))
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
#elif defined(ANDROID)
|
||||
@@ -285,6 +283,38 @@ GetUserCacheDir() noexcept
|
||||
#endif
|
||||
}
|
||||
|
||||
AllocatedPath
|
||||
GetUserRuntimeDir() noexcept
|
||||
{
|
||||
#ifdef USE_XDG
|
||||
return SafePathFromFS(getenv("XDG_RUNTIME_DIR"));
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
AllocatedPath
|
||||
GetAppRuntimeDir() noexcept
|
||||
{
|
||||
#ifdef __linux__
|
||||
/* systemd specific; see systemd.exec(5) */
|
||||
if (const char *runtime_directory = getenv("RUNTIME_DIRECTORY"))
|
||||
if (auto dir = StringView{runtime_directory}.Split(':').first;
|
||||
!dir.empty())
|
||||
return AllocatedPath::FromFS(dir);
|
||||
#endif
|
||||
|
||||
#ifdef USE_XDG
|
||||
if (const auto user_dir = GetUserRuntimeDir(); !user_dir.IsNull()) {
|
||||
auto dir = user_dir / Path::FromFS("mpd");
|
||||
mkdir(dir.c_str(), 0700);
|
||||
return dir;
|
||||
}
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
AllocatedPath
|
||||
@@ -317,11 +347,11 @@ AllocatedPath
|
||||
GetHomeDir() noexcept
|
||||
{
|
||||
#ifndef ANDROID
|
||||
auto home = getenv("HOME");
|
||||
if (IsValidPathString(home) && IsValidDir(home))
|
||||
if (const auto home = getenv("HOME");
|
||||
IsValidPathString(home) && IsValidDir(home))
|
||||
return AllocatedPath::FromFS(home);
|
||||
PasswdEntry pw;
|
||||
if (pw.ReadByUid(getuid()))
|
||||
|
||||
if (PasswdEntry pw; pw.ReadByUid(getuid()))
|
||||
return SafePathFromFS(pw->pw_dir);
|
||||
#endif
|
||||
return nullptr;
|
||||
@@ -334,8 +364,8 @@ GetHomeDir(const char *user_name) noexcept
|
||||
(void)user_name;
|
||||
#else
|
||||
assert(user_name != nullptr);
|
||||
PasswdEntry pw;
|
||||
if (pw.ReadByName(user_name))
|
||||
|
||||
if (PasswdEntry pw; pw.ReadByName(user_name))
|
||||
return SafePathFromFS(pw->pw_dir);
|
||||
#endif
|
||||
return nullptr;
|
||||
|
@@ -25,27 +25,44 @@
|
||||
/**
|
||||
* Obtains configuration directory for the current user.
|
||||
*/
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetUserConfigDir() noexcept;
|
||||
|
||||
/**
|
||||
* Obtains music directory for the current user.
|
||||
*/
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetUserMusicDir() noexcept;
|
||||
|
||||
/**
|
||||
* Obtains cache directory for the current user.
|
||||
*/
|
||||
[[gnu::pure]]
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetUserCacheDir() noexcept;
|
||||
|
||||
/**
|
||||
* Obtains the runtime directory for the current user.
|
||||
*/
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetUserRuntimeDir() noexcept;
|
||||
|
||||
/**
|
||||
* Obtains the runtime directory for this application.
|
||||
*/
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetAppRuntimeDir() noexcept;
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
/**
|
||||
* Obtains system configuration directory.
|
||||
*/
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetSystemConfigDir() noexcept;
|
||||
|
||||
@@ -54,6 +71,7 @@ GetSystemConfigDir() noexcept;
|
||||
* Application base directory is a directory that contains 'bin' folder
|
||||
* for current executable.
|
||||
*/
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetAppBaseDir() noexcept;
|
||||
|
||||
@@ -62,12 +80,14 @@ GetAppBaseDir() noexcept;
|
||||
/**
|
||||
* Obtains home directory for the current user.
|
||||
*/
|
||||
[[gnu::const]]
|
||||
AllocatedPath
|
||||
GetHomeDir() noexcept;
|
||||
|
||||
/**
|
||||
* Obtains home directory for the specified user.
|
||||
*/
|
||||
[[gnu::pure]]
|
||||
AllocatedPath
|
||||
GetHomeDir(const char *user_name) noexcept;
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -30,12 +30,7 @@ namespace Alsa {
|
||||
AllowedFormat::AllowedFormat(StringView s)
|
||||
{
|
||||
#ifdef ENABLE_DSD
|
||||
const StringView dop_tail("=dop");
|
||||
if (s.EndsWith(dop_tail)) {
|
||||
dop = true;
|
||||
s.size -= dop_tail.size;
|
||||
} else
|
||||
dop = false;
|
||||
dop = s.RemoveSuffix("=dop");
|
||||
#endif
|
||||
|
||||
char buffer[64];
|
||||
@@ -54,7 +49,7 @@ AllowedFormat::AllowedFormat(StringView s)
|
||||
}
|
||||
|
||||
std::forward_list<AllowedFormat>
|
||||
AllowedFormat::ParseList(StringView s)
|
||||
AllowedFormat::ParseList(std::string_view s)
|
||||
{
|
||||
std::forward_list<AllowedFormat> list;
|
||||
auto tail = list.before_begin();
|
||||
|
@@ -52,7 +52,7 @@ struct AllowedFormat {
|
||||
*
|
||||
* Throws std::runtime_error on error.
|
||||
*/
|
||||
static std::forward_list<AllowedFormat> ParseList(StringView s);
|
||||
static std::forward_list<AllowedFormat> ParseList(std::string_view s);
|
||||
};
|
||||
|
||||
std::string
|
||||
|
@@ -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,
|
||||
|
@@ -33,23 +33,27 @@
|
||||
static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10);
|
||||
|
||||
AudioOutputControl::AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
||||
AudioOutputClient &_client) noexcept
|
||||
AudioOutputClient &_client,
|
||||
const ConfigBlock &block)
|
||||
:output(std::move(_output)),
|
||||
name(output->GetName()),
|
||||
client(_client),
|
||||
thread(BIND_THIS_METHOD(Task))
|
||||
thread(BIND_THIS_METHOD(Task)),
|
||||
tags(block.GetBlockValue("tags", true)),
|
||||
always_on(block.GetBlockValue("always_on", false)),
|
||||
enabled(block.GetBlockValue("enabled", true))
|
||||
{
|
||||
}
|
||||
|
||||
AudioOutputControl::AudioOutputControl(AudioOutputControl *_output,
|
||||
AudioOutputControl::AudioOutputControl(AudioOutputControl &&src,
|
||||
AudioOutputClient &_client) noexcept
|
||||
:output(_output->Steal()),
|
||||
:output(src.Steal()),
|
||||
name(output->GetName()),
|
||||
client(_client),
|
||||
thread(BIND_THIS_METHOD(Task))
|
||||
thread(BIND_THIS_METHOD(Task)),
|
||||
tags(src.tags),
|
||||
always_on(src.always_on)
|
||||
{
|
||||
tags =_output->tags;
|
||||
always_on=_output->always_on;
|
||||
}
|
||||
|
||||
AudioOutputControl::~AudioOutputControl() noexcept
|
||||
@@ -57,14 +61,6 @@ AudioOutputControl::~AudioOutputControl() noexcept
|
||||
StopThread();
|
||||
}
|
||||
|
||||
void
|
||||
AudioOutputControl::Configure(const ConfigBlock &block)
|
||||
{
|
||||
tags = block.GetBlockValue("tags", true);
|
||||
always_on = block.GetBlockValue("always_on", false);
|
||||
enabled = block.GetBlockValue("enabled", true);
|
||||
}
|
||||
|
||||
std::unique_ptr<FilteredAudioOutput>
|
||||
AudioOutputControl::Steal() noexcept
|
||||
{
|
||||
|
@@ -151,13 +151,13 @@ class AudioOutputControl {
|
||||
* default is true, but it may be configured to false to
|
||||
* suppress sending tags to the output.
|
||||
*/
|
||||
bool tags;
|
||||
const bool tags;
|
||||
|
||||
/**
|
||||
* Shall this output always play something (i.e. silence),
|
||||
* even when playback is stopped?
|
||||
*/
|
||||
bool always_on;
|
||||
const bool always_on;
|
||||
|
||||
/**
|
||||
* Has the user enabled this device?
|
||||
@@ -249,10 +249,18 @@ public:
|
||||
*/
|
||||
mutable Mutex mutex;
|
||||
|
||||
/**
|
||||
* Throws on error.
|
||||
*/
|
||||
AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
||||
AudioOutputClient &_client) noexcept;
|
||||
AudioOutputClient &_client,
|
||||
const ConfigBlock &block);
|
||||
|
||||
AudioOutputControl(AudioOutputControl *_outputControl,
|
||||
/**
|
||||
* Move the contents of an existing instance, and convert that
|
||||
* existing instance to a "dummy" output.
|
||||
*/
|
||||
AudioOutputControl(AudioOutputControl &&src,
|
||||
AudioOutputClient &_client) noexcept;
|
||||
|
||||
~AudioOutputControl() noexcept;
|
||||
@@ -260,11 +268,6 @@ public:
|
||||
AudioOutputControl(const AudioOutputControl &) = delete;
|
||||
AudioOutputControl &operator=(const AudioOutputControl &) = delete;
|
||||
|
||||
/**
|
||||
* Throws on error.
|
||||
*/
|
||||
void Configure(const ConfigBlock &block);
|
||||
|
||||
[[gnu::pure]]
|
||||
const char *GetName() const noexcept;
|
||||
|
||||
|
@@ -181,7 +181,8 @@ public:
|
||||
void Disable() noexcept;
|
||||
|
||||
/**
|
||||
* Invoke OutputPlugin::close().
|
||||
* Close everything: the output (via CloseOutput()) and the
|
||||
* software mixer (via CloseSoftwareMixer()).
|
||||
*
|
||||
* Caller must not lock the mutex.
|
||||
*/
|
||||
@@ -200,7 +201,7 @@ public:
|
||||
void OpenOutputAndConvert(AudioFormat audio_format);
|
||||
|
||||
/**
|
||||
* Close the output plugin.
|
||||
* Invoke AudioOutput::Close(), but nothing else.
|
||||
*
|
||||
* Mutex must not be locked.
|
||||
*/
|
||||
|
@@ -80,9 +80,8 @@ LoadOutputControl(EventLoop &event_loop, EventLoop &rt_event_loop,
|
||||
replay_gain_config,
|
||||
mixer_listener,
|
||||
block, defaults, filter_factory);
|
||||
auto control = std::make_unique<AudioOutputControl>(std::move(output), client);
|
||||
control->Configure(block);
|
||||
return control;
|
||||
return std::make_unique<AudioOutputControl>(std::move(output),
|
||||
client, block);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -130,24 +129,12 @@ MultipleOutputs::FindByName(const char *name) noexcept
|
||||
}
|
||||
|
||||
void
|
||||
MultipleOutputs::Add(std::unique_ptr<FilteredAudioOutput> output,
|
||||
bool enable) noexcept
|
||||
MultipleOutputs::AddMoveFrom(AudioOutputControl &&src,
|
||||
bool enable) noexcept
|
||||
{
|
||||
// TODO: this operation needs to be protected with a mutex
|
||||
outputs.push_back(std::make_unique<AudioOutputControl>(std::move(output),
|
||||
client));
|
||||
|
||||
outputs.back()->LockSetEnabled(enable);
|
||||
|
||||
client.ApplyEnabled();
|
||||
}
|
||||
|
||||
void
|
||||
MultipleOutputs::AddCopy(AudioOutputControl *outputControl,
|
||||
bool enable) noexcept
|
||||
{
|
||||
// TODO: this operation needs to be protected with a mutex
|
||||
outputs.push_back(std::make_unique<AudioOutputControl>(outputControl, client));
|
||||
outputs.push_back(std::make_unique<AudioOutputControl>(std::move(src),
|
||||
client));
|
||||
|
||||
outputs.back()->LockSetEnabled(enable);
|
||||
|
||||
|
@@ -125,11 +125,8 @@ public:
|
||||
return FindByName(name) != nullptr;
|
||||
}
|
||||
|
||||
void Add(std::unique_ptr<FilteredAudioOutput> output,
|
||||
bool enable) noexcept;
|
||||
|
||||
void AddCopy(AudioOutputControl *outputControl,
|
||||
bool enable) noexcept;
|
||||
void AddMoveFrom(AudioOutputControl &&src,
|
||||
bool enable) noexcept;
|
||||
|
||||
|
||||
void SetReplayGainMode(ReplayGainMode mode) noexcept;
|
||||
|
@@ -74,13 +74,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
|
||||
try {
|
||||
output->ConfigureConvertFilter();
|
||||
} catch (...) {
|
||||
open = false;
|
||||
|
||||
{
|
||||
const ScopeUnlock unlock(mutex);
|
||||
output->CloseOutput(false);
|
||||
}
|
||||
|
||||
InternalCloseOutput(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -279,7 +273,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 +429,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());
|
||||
}
|
||||
|
||||
|
@@ -84,6 +84,23 @@ class AlsaOutput final
|
||||
* @see http://dsd-guide.com/dop-open-standard
|
||||
*/
|
||||
bool dop_setting;
|
||||
|
||||
/**
|
||||
* Are we currently playing DSD? (Native DSD or DoP)
|
||||
*/
|
||||
bool use_dsd;
|
||||
|
||||
/**
|
||||
* Play some silence before closing the output in DSD mode?
|
||||
* This is a workaround for some DACs which emit noise when
|
||||
* stopping DSD playback.
|
||||
*/
|
||||
const bool stop_dsd_silence;
|
||||
|
||||
/**
|
||||
* Are we currently draining with #stop_dsd_silence?
|
||||
*/
|
||||
bool in_stop_dsd_silence;
|
||||
#endif
|
||||
|
||||
/** libasound's buffer_time setting (in microseconds) */
|
||||
@@ -93,7 +110,7 @@ class AlsaOutput final
|
||||
const unsigned period_time;
|
||||
|
||||
/** the mode flags passed to snd_pcm_open */
|
||||
int mode = 0;
|
||||
const int mode;
|
||||
|
||||
std::forward_list<Alsa::AllowedFormat> allowed_formats;
|
||||
|
||||
@@ -344,39 +361,9 @@ private:
|
||||
/**
|
||||
* @return false if no data was moved
|
||||
*/
|
||||
bool CopyRingToPeriodBuffer() noexcept {
|
||||
if (period_buffer.IsFull())
|
||||
return false;
|
||||
bool CopyRingToPeriodBuffer() noexcept;
|
||||
|
||||
size_t nbytes = ring_buffer->pop(period_buffer.GetTail(),
|
||||
period_buffer.GetSpaceBytes());
|
||||
if (nbytes == 0)
|
||||
return false;
|
||||
|
||||
period_buffer.AppendBytes(nbytes);
|
||||
|
||||
const std::lock_guard<Mutex> lock(mutex);
|
||||
/* notify the OutputThread that there is now
|
||||
room in ring_buffer */
|
||||
cond.notify_one();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept {
|
||||
assert(period_buffer.IsFull());
|
||||
assert(period_buffer.GetFrames(out_frame_size) > 0);
|
||||
|
||||
auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(),
|
||||
period_buffer.GetFrames(out_frame_size));
|
||||
if (frames_written > 0) {
|
||||
written = true;
|
||||
period_buffer.ConsumeFrames(frames_written,
|
||||
out_frame_size);
|
||||
}
|
||||
|
||||
return frames_written;
|
||||
}
|
||||
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept;
|
||||
|
||||
void LockCaughtError() noexcept {
|
||||
period_buffer.Clear();
|
||||
@@ -385,6 +372,9 @@ private:
|
||||
error = std::current_exception();
|
||||
active = false;
|
||||
waiting = false;
|
||||
#ifdef ENABLE_DSD
|
||||
in_stop_dsd_silence = false;
|
||||
#endif
|
||||
cond.notify_one();
|
||||
}
|
||||
|
||||
@@ -408,21 +398,11 @@ private:
|
||||
|
||||
static constexpr Domain alsa_output_domain("alsa_output");
|
||||
|
||||
AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
|
||||
:AudioOutput(FLAG_ENABLE_DISABLE),
|
||||
MultiSocketMonitor(_loop),
|
||||
defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)),
|
||||
silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)),
|
||||
device(block.GetBlockValue("device", "")),
|
||||
#ifdef ENABLE_DSD
|
||||
dop_setting(block.GetBlockValue("dop", false) ||
|
||||
/* legacy name from MPD 0.18 and older: */
|
||||
block.GetBlockValue("dsd_usb", false)),
|
||||
#endif
|
||||
buffer_time(block.GetPositiveValue("buffer_time",
|
||||
MPD_ALSA_BUFFER_TIME_US)),
|
||||
period_time(block.GetPositiveValue("period_time", 0U))
|
||||
static int
|
||||
GetAlsaOpenMode(const ConfigBlock &block)
|
||||
{
|
||||
int mode = 0;
|
||||
|
||||
#ifdef SND_PCM_NO_AUTO_RESAMPLE
|
||||
if (!block.GetBlockValue("auto_resample", true))
|
||||
mode |= SND_PCM_NO_AUTO_RESAMPLE;
|
||||
@@ -438,6 +418,26 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
|
||||
mode |= SND_PCM_NO_AUTO_FORMAT;
|
||||
#endif
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
|
||||
:AudioOutput(FLAG_ENABLE_DISABLE),
|
||||
MultiSocketMonitor(_loop),
|
||||
defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)),
|
||||
silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)),
|
||||
device(block.GetBlockValue("device", "")),
|
||||
#ifdef ENABLE_DSD
|
||||
dop_setting(block.GetBlockValue("dop", false) ||
|
||||
/* legacy name from MPD 0.18 and older: */
|
||||
block.GetBlockValue("dsd_usb", false)),
|
||||
stop_dsd_silence(block.GetBlockValue("stop_dsd_silence", false)),
|
||||
#endif
|
||||
buffer_time(block.GetPositiveValue("buffer_time",
|
||||
MPD_ALSA_BUFFER_TIME_US)),
|
||||
period_time(block.GetPositiveValue("period_time", 0U)),
|
||||
mode(GetAlsaOpenMode(block))
|
||||
{
|
||||
const char *allowed_formats_string =
|
||||
block.GetBlockValue("allowed_formats", nullptr);
|
||||
if (allowed_formats_string != nullptr)
|
||||
@@ -462,7 +462,7 @@ AlsaOutput::SetAttribute(std::string &&name, std::string &&value)
|
||||
{
|
||||
if (name == "allowed_formats") {
|
||||
const std::lock_guard<Mutex> lock(attributes_mutex);
|
||||
allowed_formats = Alsa::AllowedFormat::ParseList({value.data(), value.length()});
|
||||
allowed_formats = Alsa::AllowedFormat::ParseList(value);
|
||||
#ifdef ENABLE_DSD
|
||||
} else if (name == "dop") {
|
||||
const std::lock_guard<Mutex> lock(attributes_mutex);
|
||||
@@ -732,6 +732,9 @@ AlsaOutput::Open(AudioFormat &audio_format)
|
||||
snd_pcm_nonblock(pcm, 1);
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
use_dsd = audio_format.format == SampleFormat::DSD;
|
||||
in_stop_dsd_silence = false;
|
||||
|
||||
if (params.dsd_mode == PcmExport::DsdMode::DOP)
|
||||
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
|
||||
#endif
|
||||
@@ -824,9 +827,59 @@ AlsaOutput::Recover(int err) noexcept
|
||||
return err;
|
||||
}
|
||||
|
||||
bool
|
||||
AlsaOutput::CopyRingToPeriodBuffer() noexcept
|
||||
{
|
||||
if (period_buffer.IsFull())
|
||||
return false;
|
||||
|
||||
size_t nbytes = ring_buffer->pop(period_buffer.GetTail(),
|
||||
period_buffer.GetSpaceBytes());
|
||||
if (nbytes == 0)
|
||||
return false;
|
||||
|
||||
period_buffer.AppendBytes(nbytes);
|
||||
|
||||
const std::lock_guard<Mutex> lock(mutex);
|
||||
/* notify the OutputThread that there is now
|
||||
room in ring_buffer */
|
||||
cond.notify_one();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
snd_pcm_sframes_t
|
||||
AlsaOutput::WriteFromPeriodBuffer() noexcept
|
||||
{
|
||||
assert(period_buffer.IsFull());
|
||||
assert(period_buffer.GetFrames(out_frame_size) > 0);
|
||||
|
||||
auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(),
|
||||
period_buffer.GetFrames(out_frame_size));
|
||||
if (frames_written > 0) {
|
||||
written = true;
|
||||
period_buffer.ConsumeFrames(frames_written,
|
||||
out_frame_size);
|
||||
}
|
||||
|
||||
return frames_written;
|
||||
}
|
||||
|
||||
inline bool
|
||||
AlsaOutput::DrainInternal()
|
||||
{
|
||||
#ifdef ENABLE_DSD
|
||||
if (in_stop_dsd_silence) {
|
||||
/* "stop_dsd_silence" is in progress: clear internal
|
||||
buffers and instead, fill the period buffer with
|
||||
silence */
|
||||
in_stop_dsd_silence = false;
|
||||
ring_buffer->reset();
|
||||
period_buffer.Clear();
|
||||
period_buffer.FillWithSilence(silence, out_frame_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* drain ring_buffer */
|
||||
CopyRingToPeriodBuffer();
|
||||
|
||||
@@ -957,6 +1010,17 @@ AlsaOutput::Cancel() noexcept
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (stop_dsd_silence && use_dsd) {
|
||||
/* play some DSD silence instead of snd_pcm_drop() */
|
||||
std::unique_lock<Mutex> lock(mutex);
|
||||
in_stop_dsd_silence = true;
|
||||
drain = true;
|
||||
cond.wait(lock, [this]{ return !drain || !active; });
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
BlockingCall(GetEventLoop(), [this](){
|
||||
CancelInternal();
|
||||
});
|
||||
|
@@ -21,7 +21,6 @@
|
||||
#include "../OutputAPI.hxx"
|
||||
#include "thread/SafeSingleton.hxx"
|
||||
#include "system/Error.hxx"
|
||||
#include "util/DivideString.hxx"
|
||||
#include "util/IterableSplitString.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -51,6 +51,12 @@ class SnapcastOutput final : AudioOutput, ServerSocket {
|
||||
*/
|
||||
bool open;
|
||||
|
||||
/**
|
||||
* Is the output current paused? This is set by Pause() and
|
||||
* is cleared by the next Play() call. It is used in Delay().
|
||||
*/
|
||||
bool pause;
|
||||
|
||||
InjectEvent inject_event;
|
||||
|
||||
#ifdef HAVE_ZEROCONF
|
||||
|
@@ -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};
|
||||
|
@@ -161,6 +161,7 @@ SnapcastOutput::Open(AudioFormat &audio_format)
|
||||
timer = new Timer(audio_format);
|
||||
|
||||
open = true;
|
||||
pause = false;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -213,7 +214,7 @@ SnapcastOutput::RemoveClient(SnapcastClient &client) noexcept
|
||||
std::chrono::steady_clock::duration
|
||||
SnapcastOutput::Delay() const noexcept
|
||||
{
|
||||
if (!LockHasClients() /*&& pause*/) {
|
||||
if (!LockHasClients() && pause) {
|
||||
/* if there's no client and this output is paused,
|
||||
then Pause() will not do anything, it will not fill
|
||||
the buffer and it will not update the timer;
|
||||
@@ -307,7 +308,7 @@ SnapcastOutput::SendTag(const Tag &tag)
|
||||
size_t
|
||||
SnapcastOutput::Play(const void *chunk, size_t size)
|
||||
{
|
||||
//pause = false;
|
||||
pause = false;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
@@ -341,6 +342,8 @@ SnapcastOutput::Play(const void *chunk, size_t size)
|
||||
if (nbytes == 0)
|
||||
break;
|
||||
|
||||
unflushed_input = 0;
|
||||
|
||||
const std::lock_guard<Mutex> protect(mutex);
|
||||
if (chunks.empty())
|
||||
inject_event.Schedule();
|
||||
@@ -355,8 +358,7 @@ SnapcastOutput::Play(const void *chunk, size_t size)
|
||||
bool
|
||||
SnapcastOutput::Pause()
|
||||
{
|
||||
// TODO: implement
|
||||
//pause = true;
|
||||
pause = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -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,
|
||||
],
|
||||
)
|
||||
|
@@ -25,7 +25,6 @@ if zeroconf_option == 'bonjour'
|
||||
endif
|
||||
|
||||
bonjour_deps = [
|
||||
log_dep,
|
||||
]
|
||||
|
||||
if not is_darwin
|
||||
@@ -41,6 +40,7 @@ if zeroconf_option == 'bonjour'
|
||||
include_directories: inc,
|
||||
dependencies: [
|
||||
event_dep,
|
||||
log_dep,
|
||||
],
|
||||
)
|
||||
|
||||
|
@@ -7,6 +7,10 @@ After=network.target sound.target
|
||||
Type=notify
|
||||
ExecStart=@prefix@/bin/mpd --no-daemon
|
||||
|
||||
# Create /run/mpd (if MPD is launched without the socket unit and is
|
||||
# configured to bind listener sockets there).
|
||||
RuntimeDirectory=mpd
|
||||
|
||||
# Enable this setting to ask systemd to watch over MPD, see
|
||||
# systemd.service(5). This is disabled by default because it causes
|
||||
# periodic wakeups which are unnecessary if MPD is not playing.
|
||||
|
@@ -7,6 +7,10 @@ After=network.target sound.target
|
||||
Type=notify
|
||||
ExecStart=@prefix@/bin/mpd --no-daemon
|
||||
|
||||
# Create /run/user/$UID/mpd (if MPD is launched without the socket
|
||||
# unit and is configured to bind listener sockets there).
|
||||
RuntimeDirectory=mpd
|
||||
|
||||
# Enable this setting to ask systemd to watch over MPD, see
|
||||
# systemd.service(5). This is disabled by default because it causes
|
||||
# periodic wakeups which are unnecessary if MPD is not playing.
|
||||
|
@@ -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