Compare commits

..

119 Commits

Author SHA1 Message Date
Max Kellermann
f084bf7872 release v0.23.4 2021-11-11 10:16:36 +01:00
Max Kellermann
1112d3907a Revert "systemd: add "RuntimeDirectory" directive"
This reverts commit 552c30eae4.

It has caused various problems; for example, MPD wasn't able to write
the pid_file (which was already mitigated by commit a4e4217204).

And apparently, the socket file created in the same directory by
mpd.socket disappears when mpd.service (re)creates the directory.  I
could not reproduce this problem with 247.3, but maybe this is a bug
in older systemd versions?

Until we figure out why this happens, let's remove the
RuntimeDirectory directive.  A future MPD version may be launched as
regular user, not as root, which will eliminate one major problem with
RuntimeDirectory.
2021-11-11 10:16:13 +01:00
Max Kellermann
3464497880 command/database: add optional position parameter to "searchaddpl"
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1328
2021-11-11 09:52:49 +01:00
Max Kellermann
651f57bced command/playlist: save only if at least one song was added 2021-11-11 09:50:31 +01:00
Max Kellermann
b4e72aba6c command/playlist: move code to SearchInsertIntoPlaylist() 2021-11-11 09:40:41 +01:00
0xC0ncord
061dd2dfef output/plugins: fix build error with clang and -stdlib=libc++
This fixes this build error observed with clang and -stdlib=libc++:

../mpd-0.23.3/src/output/plugins/PipeWireOutputPlugin.cxx:661:55: error: implicit instantiation of undefined template 'std::array<std::byte, 64>'
        std::array<std::byte, MAX_CHANNELS * MAX_INTERLEAVE> buffer;
                                                             ^
/usr/include/c++/v1/__tuple:219:64: note: template is declared here
template <class _Tp, size_t _Size> struct _LIBCPP_TEMPLATE_VIS array;
                                                               ^
2021-11-10 15:35:56 -05:00
Max Kellermann
5f4ec7de5b decoder/ffmpeg, lib/ffmpeg: make AVCodec pointers "const"
For libavcodec 59 support.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1333
2021-11-09 21:09:14 +01:00
Max Audron
6f81bb4b09 upnp: add option to configure interface for db plugin
Add an option to the UPnP database plugin to configure which interface
is used by upnp to discover servers.

upnp by default selects the first interface that is not loopback, which
in some cases might not be the desired interface. For example if wanting
to access a DLNA server over a VPN connection.

The "interface" option can now be set to the name of the desired
interface to achieve this.

The default behaviour remains unchanged.
2021-11-08 23:04:07 +01:00
Max Audron
4ed60a5711 upnp: expose interface configuration on UpnpInit2()
Adds the Interface Name as an argument to the *Init functions to make it
possible to select which interface is used by upnp to detect servers.

Currently "nullptr" is passed in to let the upnp library select an
interface, as before.
2021-11-08 22:53:01 +01:00
Max Kellermann
c93195c94b NEWS: fix typo 2021-11-05 14:45:43 +01:00
Max Kellermann
f30adac4bb doc/mpdconf.example: add comments recommending not to use log_file and pid_file 2021-11-05 09:06:27 +01:00
Max Kellermann
a4e4217204 Main: ignore the "pid_file" setting if started as systemd service
Commit 552c30eae caused problems for those people who still had a
"pid_file" setting (even though that is obsolete with systemd),
because now /run/mpd is owned by root:root (our mpd.service has no
User=mpd directive, so systemd starts MPD as root).

To work around this problem, and to be able to keep
RuntimeDirectory=mpd (which solved a problem of other MPD users), the
best compromise seems to just ignore the "pid_file" setting when it is
of no use.
2021-11-05 09:02:56 +01:00
Max Kellermann
8754d705a1 CommandLine: rename struct options 2021-11-05 08:57:12 +01:00
Max Kellermann
23d4a2d6a5 Main: pass struct options by reference 2021-11-05 08:56:05 +01:00
Max Kellermann
ce77b148d9 CommandLine: add option --systemd
This way, MPD can reliably detect whether it was started as systemd
service, which is better than checking sd_booted(), which only checks
whether systemd manages all services, but still MPD could be started
manually.
2021-11-05 08:51:49 +01:00
Max Kellermann
be3eca39e8 NEWS: add missing lines 2021-11-04 17:59:02 +01:00
Max Kellermann
3413b1aeb4 output/alsa: add option thesycon_dsd_workaround 2021-11-04 17:55:53 +01:00
Max Kellermann
356d13e9dd lib/alsa/HwSetup: add missing include 2021-11-04 17:55:15 +01:00
Max Kellermann
fa34bf0aaf Merge branch 'feature/win32-disable-openmpt123' of git://github.com/ibmibmibm/MPD 2021-11-04 15:11:26 +01:00
Max Kellermann
5d0941476a lib/alsa/Error: a std::system_error category for libasound errors 2021-11-04 14:59:00 +01:00
Max Kellermann
5ff0bbd0f8 lib/fmt/AudioFormatFormatter: add formatter for SampleFormat 2021-11-04 14:55:01 +01:00
Shen-Ta Hsieh
a3764e533c python/build/libs.py: disable building libopenmpt cli
Signed-off-by: Shen-Ta Hsieh <ibmibmibm.tw@gmail.com>
2021-11-04 21:54:12 +08:00
Shen-Ta Hsieh
3e05cba30e python/build/libs.py: update libopenmpt configure flags
Signed-off-by: Shen-Ta Hsieh <ibmibmibm.tw@gmail.com>
2021-11-04 15:23:24 +08:00
Max Kellermann
14b3c0f0af event/Loop: destruct the Uring::Manager in the destructor before assert()
Fixes assertion failure when the EventLoop gets destructed before
Run() was ever called.

Fixes https://bugs.debian.org/998310
2021-11-03 18:32:14 +01:00
Max Kellermann
67aff05051 increment version number to 0.23.4 2021-10-31 18:17:35 +01:00
Max Kellermann
19a101c3ac release v0.23.3 2021-10-31 18:13:10 +01:00
Max Kellermann
8da17a8211 doc/user.rst: add optimized build options to examples 2021-10-31 17:09:16 +01:00
Max Kellermann
2748929039 doc/user.rst: add -Dwrap_mode=forcefallback to Android/Windows examples 2021-10-31 17:08:59 +01:00
Max Kellermann
0c900a4bfa doc/user.rst: pass -Dandroid_debug_keystore=... to ./android/build.py 2021-10-31 17:03:37 +01:00
Max Kellermann
f1d5d70010 android/run-javac.sh: switch to Java 7 2021-10-31 16:55:40 +01:00
Max Kellermann
56ebc7637d python/build/libs.py: update FFmpeg to 4.4.1 2021-10-31 16:44:11 +01:00
Max Kellermann
996dd9fc8b python/build/libs.py: update libopenmpt to 0.5.12 2021-10-31 16:42:50 +01:00
Max Kellermann
056514d598 output/snapcast: reset unflushed_input after successful read
With the "wave" encoder, this has no effect, but it's more correct.
2021-10-31 16:35:42 +01:00
Max Kellermann
9a21bdfd6a output/snapcast: implement Pause()
This uncomments the code which had been present already in the first
Snapcast commit (copied from the "httpd" output plugin), but I
commented it because I did not know whether I needed to send silence
samples to all Snapcast clients.

As a side effect, this fixes playback when no Snapcast client is
connected; this was broken because Pause() always returned a positive
value when there were no clients.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1310
2021-10-31 16:26:29 +01:00
Max Kellermann
03f99dd26e db/update/Walk: use GetFilenameSuffix() instead of uri_get_suffix()
Unlike GetFilenameSuffix(), uri_get_suffix() removes the query string
first, which breaks file names with question marks in the name.
Therefore, uri_get_suffix() shall only be applied to remote URIs.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1316
2021-10-31 13:18:24 +01:00
Max Kellermann
bfb1b641f9 db/update/InotifyUpdate: fix use-after-free bug
Regression by commit 2d8847f428
2021-10-28 13:39:38 +02:00
Naglis Jonaitis
72ba98c464 doc/protocol.rst: add missing backtick 2021-10-27 02:11:39 +03:00
Max Kellermann
dcd19c0592 config/Path: use StringView::Split() 2021-10-26 12:55:01 +02:00
Max Kellermann
109159e0f7 Permission: use StringView::Split() 2021-10-26 12:25:47 +02:00
Max Kellermann
409b877eea output/ao: include cleanup 2021-10-26 12:20:18 +02:00
Max Kellermann
c5bf7948ff fs/StandardDirectory: use the RUNTIME_DIRECTORY environment variable 2021-10-26 09:30:16 +02:00
Max Kellermann
b9f7127691 fs/StandardDirectory: add GetAppRuntimeDir() 2021-10-26 09:30:16 +02:00
Max Kellermann
1e6f5f012c fs/StandardDirectory: add GetUserRuntimeDir() 2021-10-26 09:30:16 +02:00
Max Kellermann
225d85fd9b fs/StandardDirectory: use "if" with initializer 2021-10-26 09:29:57 +02:00
Max Kellermann
1bb22f118d fs/StandardDirectory: add more pure/const attributes 2021-10-26 09:04:20 +02:00
Max Kellermann
552c30eae4 systemd: add "RuntimeDirectory" directive 2021-10-26 08:38:36 +02:00
Max Kellermann
48e8a26813 command/playlist: allow range in playlistdelete 2021-10-25 12:23:37 +02:00
Max Kellermann
ade847bc89 PlaylistFile: fold spl_move_index() into handle_playlistmove() 2021-10-25 12:13:45 +02:00
Max Kellermann
a6173e0eae command/playlist: add position parameter to "playlistadd"
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1106
2021-10-25 12:10:47 +02:00
Max Kellermann
4529bb4a83 doc/protocol.rst: add "since" version notes 2021-10-25 08:47:23 +02:00
Max Kellermann
258ecb764f PlaylistFile: add class PlaylistFileEditor 2021-10-23 13:54:50 +02:00
Max Kellermann
6f595e9abb command/queue: add optional position parameter to "add"
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1285
2021-10-23 13:12:44 +02:00
Max Kellermann
35c4c7e8bf command/queue: move #ifdef out of AddDatabaseSelection() 2021-10-23 13:09:04 +02:00
Max Kellermann
293ed924d1 command/queue: pass Partition to AddDatabaseSelection() 2021-10-23 13:06:31 +02:00
Max Kellermann
c8121176b3 output/alsa: add option "stop_dsd_silence" to work around DSD DAC noise 2021-10-23 12:25:32 +02:00
Max Kellermann
ee270f9b00 meson.build: log_dep is only needed internally 2021-10-23 12:08:43 +02:00
Max Kellermann
bf1d77a4d8 output/alsa: un-inline several methods 2021-10-23 12:02:27 +02:00
Max Kellermann
a9344fafe9 lib/alsa/AllowedFormat: use StringView::RemoveSuffix() 2021-10-23 11:43:31 +02:00
Max Kellermann
b8890726f2 lib/alsa/AllowedFormat: use std::string_view 2021-10-23 11:42:30 +02:00
Max Kellermann
0f84332654 output/alsa: make "mode" const 2021-10-23 11:39:59 +02:00
Max Kellermann
46c82259f7 output/Control: make config fields const 2021-10-22 20:22:22 +02:00
Max Kellermann
2d03823283 output/Control: fold Configure() into the constructor 2021-10-22 20:21:58 +02:00
Max Kellermann
bba144eca5 output/Control: use C++ initializers 2021-10-22 20:21:43 +02:00
Max Kellermann
9af73dad93 output/Multiple: remove unused method Add() 2021-10-22 20:21:35 +02:00
Max Kellermann
f0d66bf6a6 output/Control: pass rvalue reference to move constructor 2021-10-22 20:14:37 +02:00
Max Kellermann
5ad53a7554 output/Thread: remove duplicate code by calling InternalCloseOutput() 2021-10-22 19:54:47 +02:00
Max Kellermann
7b2e3331f2 output/Filtered: improve API docs 2021-10-22 19:54:38 +02:00
Max Kellermann
3cb44f6652 increment version number to 0.23.3 2021-10-22 12:50:11 +02:00
Max Kellermann
b7fdff46f2 release v0.23.2 2021-10-22 12:45:45 +02:00
Max Kellermann
e16109330d input/last: clear "uri" in OnCloseTimer()
Without clearing the "uri" field, the next Open() call attempts to
reuse the old InputStream, but it has already been closed, so Open()
always returns nullptr.

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

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

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

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1295
2021-10-19 13:40:11 +02:00
Max Kellermann
6120c1360c neighbor/Glue: remove unreachable "throw" statement
Should have been removed by commit a8087dc12c
2021-10-19 13:40:11 +02:00
Max Kellermann
a8087dc12c neighbor/Glue: mention failed plugin name in error message 2021-10-19 13:29:00 +02:00
Max Kellermann
070c03dbf7 event/Thread, ...: fix printf->libfmt remains 2021-10-19 13:19:07 +02:00
Max Kellermann
0a9bec3754 increment version number to 0.23.2 2021-10-19 10:29:49 +02:00
Max Kellermann
fff25ac753 release v0.23.1 2021-10-19 10:27:28 +02:00
Max Kellermann
4f1e79b6b8 filter/ReplayGain: emit "mixer" event when replay gain changes volume
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1294
2021-10-19 10:03:21 +02:00
Max Kellermann
aa9933c0b5 output/pipewire: add noexcept 2021-10-19 08:58:50 +02:00
Max Kellermann
0697d1f859 output/pipewire: include cleanup 2021-10-19 08:57:33 +02:00
Max Kellermann
df033fa4aa NEWS: mention the previous commit 2021-10-19 08:56:32 +02:00
Nicolai Syvertsen
b941a7df83 Implement volume updates for pipewire output 2021-10-19 00:01:45 +02:00
Max Kellermann
31151cec3c command/playlist: "load" supports relative positions
This commit also increases the PROTOCOL_VERSION so clients can detect
the availability of the feature.
2021-10-18 22:08:22 +02:00
Max Kellermann
07e8c338df command/queue: move position parameter functions to separate library 2021-10-18 22:07:04 +02:00
Max Kellermann
b22d7218aa command/player, ...: use decimal notation
During the libfmt migration, I converted "%1.3f" to just "{:1.3}"
without the "f" suffix, but libfmt defaults to scientific notation,
which can break some MPD clients.

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

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

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

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

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

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

52
NEWS
View File

@@ -1,3 +1,55 @@
ver 0.23.4 (2021/11/11)
* protocol
- add optional position parameter to "searchaddpl"
* decoder
- ffmpeg: support libavcodec 59
* output
- alsa: add option "thesycon_dsd_workaround" to work around device bug
* fix crash on debug builds if startup fails
* systemd
- remove "RuntimeDirectory" directive because it caused problems
- ignore the "pid_file" setting if started as systemd service
* Windows
- enable the "openmpt" decoder plugin
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"

View File

@@ -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="64"
android:versionName="0.23.4">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>

View File

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

View File

@@ -38,9 +38,9 @@ author = 'Max Kellermann'
# built documents.
#
# The short X.Y version.
version = '0.23'
version = '0.23.4'
# 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.

View File

@@ -26,22 +26,25 @@
# files over an accepted protocol.
#
#db_file "~/.mpd/database"
#
# These settings are the locations for the daemon log files for the daemon.
# These logs are great for troubleshooting, depending on your log_level
# settings.
#
# The special value "syslog" makes MPD use the local syslog daemon. This
# setting defaults to logging to syslog.
#
#log_file "~/.mpd/log"
# If you use systemd, do not configure a log_file. With systemd, MPD
# defaults to the systemd journal, which is fine.
#
#log_file "~/.mpd/log"
# This setting sets the location of the file which stores the process ID
# for use of mpd --kill and some init scripts. This setting is disabled by
# default and the pid file will not be stored.
#
#pid_file "~/.mpd/pid"
# If you use systemd, do not configure a pid_file.
#
#pid_file "~/.mpd/pid"
# This setting sets the location of the file which contains information about
# most variables to get MPD back into the same general shape it was in before
# it was brought down. This setting is disabled by default and the server

View File

@@ -61,6 +61,15 @@ upnp
Provides access to UPnP media servers.
.. list-table::
:widths: 20 80
:header-rows: 1
* - Setting
- Description
* - **interface**
- Interface used to discover media servers. Decided by upnp if left unconfigured.
Storage plugins
===============
@@ -836,6 +845,16 @@ 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.
* - **thesycon_dsd_workaround yes|no**
- If enabled, enables a workaround for a bug in Thesycon USB
audio receivers. On these devices, playing DSD512 or PCM
causes all subsequent attempts to play other DSD rates to fail,
which can be fixed by briefly playing PCM at 44.1 kHz.
* - **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 +1113,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 +1214,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

View File

@@ -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,9 +1220,12 @@ 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}]`
:command:`searchaddpl {NAME} {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 playlist named ``NAME``.
@@ -1225,6 +1234,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_4]_
.. _command_update:
:command:`update [URI]`
@@ -1643,3 +1655,7 @@ 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
.. [#since_0_23_4] Since :program:`MPD` 0.23.4

View File

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

View File

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.23',
version: '0.23.4',
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.4')
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',

View File

@@ -112,12 +112,16 @@ 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'
'--disable-shared', '--enable-static',
'--disable-openmpt123',
'--without-mpg123', '--without-ogg', '--without-vorbis', '--without-vorbisfile',
'--without-portaudio', '--without-portaudiocpp', '--without-sndfile',
],
base='libopenmpt-0.5.12+release.autotools',
)
wildmidi = CmakeProject(
@@ -147,8 +151,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',

View File

@@ -86,6 +86,9 @@ enum Option {
OPTION_KILL,
OPTION_NO_CONFIG,
OPTION_NO_DAEMON,
#ifdef __linux__
OPTION_SYSTEMD,
#endif
OPTION_STDOUT,
OPTION_STDERR,
OPTION_VERBOSE,
@@ -98,6 +101,9 @@ static constexpr OptionDef option_defs[] = {
{"kill", "kill the currently running mpd session"},
{"no-config", "don't read from config"},
{"no-daemon", "don't detach from console"},
#ifdef __linux__
{"systemd", "systemd service mode"},
#endif
{"stdout", nullptr}, // hidden, compatibility with old versions
{"stderr", "print messages to stderr"},
{"verbose", 'v', "verbose logging"},
@@ -328,7 +334,7 @@ bool ConfigLoader::TryFile(const AllocatedPath &base_path, Path path)
}
void
ParseCommandLine(int argc, char **argv, struct options &options,
ParseCommandLine(int argc, char **argv, CommandLineOptions &options,
ConfigData &config)
{
bool use_config_file = true;
@@ -349,6 +355,13 @@ ParseCommandLine(int argc, char **argv, struct options &options,
options.daemon = false;
break;
#ifdef __linux__
case OPTION_SYSTEMD:
options.daemon = false;
options.systemd = true;
break;
#endif
case OPTION_STDOUT:
case OPTION_STDERR:
options.log_stderr = true;

View File

@@ -22,15 +22,20 @@
struct ConfigData;
struct options {
struct CommandLineOptions {
bool kill = false;
bool daemon = true;
#ifdef __linux__
bool systemd = false;
#endif
bool log_stderr = false;
bool verbose = false;
};
void
ParseCommandLine(int argc, char **argv, struct options &options,
ParseCommandLine(int argc, char **argv, CommandLineOptions &options,
ConfigData &config);
#endif

View File

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

View File

@@ -142,14 +142,24 @@ struct Config {
#ifdef ENABLE_DAEMON
static void
glue_daemonize_init(const struct options *options,
glue_daemonize_init(const CommandLineOptions &options,
const ConfigData &config)
{
auto pid_file = config.GetPath(ConfigOption::PID_FILE);
#ifdef __linux__
if (options.systemd && pid_file != nullptr) {
pid_file = nullptr;
fprintf(stderr,
"Ignoring the 'pid_file' setting in systemd mode\n");
}
#endif
daemonize_init(config.GetString(ConfigOption::USER),
config.GetString(ConfigOption::GROUP),
config.GetPath(ConfigOption::PID_FILE));
std::move(pid_file));
if (options->kill)
if (options.kill)
daemonize_kill();
}
@@ -361,7 +371,8 @@ Instance::BeginShutdownPartitions() noexcept
}
static inline void
MainConfigured(const struct options &options, const ConfigData &raw_config)
MainConfigured(const CommandLineOptions &options,
const ConfigData &raw_config)
{
#ifdef ENABLE_DAEMON
daemonize_close_stdin();
@@ -384,7 +395,7 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
const Config config(raw_config);
#ifdef ENABLE_DAEMON
glue_daemonize_init(&options, raw_config);
glue_daemonize_init(options, raw_config);
#endif
TagLoadConfig(raw_config);
@@ -582,7 +593,7 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
static void
AndroidMain()
{
struct options options;
CommandLineOptions options;
ConfigData raw_config;
const auto sdcard = Environment::getExternalStorageDirectory();
@@ -642,7 +653,7 @@ Java_org_musicpd_Bridge_pause(JNIEnv *, jclass)
static inline void
MainOrThrow(int argc, char *argv[])
{
struct options options;
CommandLineOptions options;
ConfigData raw_config;
ParseCommandLine(argc, argv, options, raw_config);

View File

@@ -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 &param : 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -199,13 +199,20 @@ handle_searchaddpl(Client &client, Request args, Response &)
{
const char *playlist = args.shift();
const unsigned position = ParseQueuePosition(args, UINT_MAX);
SongFilter filter;
const auto selection = ParseDatabaseSelection(args, true, filter);
const Database &db = client.GetDatabaseOrThrow();
search_add_to_playlist(db, client.GetStorage(),
playlist, selection);
if (position == UINT_MAX)
search_add_to_playlist(db, client.GetStorage(),
playlist, selection);
else
SearchInsertIntoPlaylist(db, client.GetStorage(), selection,
playlist, position);
return CommandResult::OK;
}

View File

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

View File

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

View File

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

View File

@@ -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,55 @@ 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 DatabaseSelection selection(uri, true, nullptr);
if (SearchInsertIntoPlaylist(client.GetDatabaseOrThrow(),
client.GetStorage(),
selection,
editor, position) == 0)
/* no song was found, don't need to save */
return CommandResult::OK;
#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
View 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());
}

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@
#include "PlaylistFile.hxx"
#include "Interface.hxx"
#include "song/DetachedSong.hxx"
#include "protocol/Ack.hxx"
#include <functional>
@@ -41,3 +42,42 @@ search_add_to_playlist(const Database &db, const Storage *storage,
const auto f = [=](auto && arg1) { return AddSong(storage, playlist_path_utf8, arg1); };
db.Visit(selection, f);
}
unsigned
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
const DatabaseSelection &selection,
PlaylistFileEditor &playlist,
unsigned position)
{
assert(position <= playlist.size());
unsigned n = 0;
db.Visit(selection, [&playlist, &position, &n, storage](const auto &song){
playlist.Insert(position + n,
DatabaseDetachSong(storage, song));
++position;
++n;
});
return n;
}
void
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
const DatabaseSelection &selection,
const char *playlist_name,
unsigned position)
{
PlaylistFileEditor editor{
playlist_name,
PlaylistFileEditor::LoadMode::TRY,
};
if (position > editor.size())
throw ProtocolError{ACK_ERROR_ARG, "Bad position"};
if (SearchInsertIntoPlaylist(db, storage, selection,
editor, position) > 0)
editor.Save();
}

View File

@@ -25,6 +25,7 @@
class Database;
class Storage;
struct DatabaseSelection;
class PlaylistFileEditor;
gcc_nonnull(3)
void
@@ -32,4 +33,19 @@ search_add_to_playlist(const Database &db, const Storage *storage,
const char *playlist_path_utf8,
const DatabaseSelection &selection);
/**
* @return the number of songs added
*/
unsigned
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
const DatabaseSelection &selection,
PlaylistFileEditor &playlist,
unsigned position);
void
SearchInsertIntoPlaylist(const Database &db, const Storage *storage,
const DatabaseSelection &selection,
const char *playlist_name,
unsigned position);
#endif

View File

@@ -39,6 +39,7 @@
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
#include "util/SplitString.hxx"
#include "config/Block.hxx"
#include <cassert>
#include <string>
@@ -76,10 +77,13 @@ class UpnpDatabase : public Database {
UpnpClient_Handle handle;
UPnPDeviceDirectory *discovery;
const char* interface;
public:
explicit UpnpDatabase(EventLoop &_event_loop) noexcept
explicit UpnpDatabase(EventLoop &_event_loop, const ConfigBlock &block) noexcept
:Database(upnp_db_plugin),
event_loop(_event_loop) {}
event_loop(_event_loop),
interface(block.GetBlockValue("interface", nullptr)) {}
static DatabasePtr Create(EventLoop &main_event_loop,
EventLoop &io_event_loop,
@@ -147,15 +151,15 @@ private:
DatabasePtr
UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
[[maybe_unused]] DatabaseListener &listener,
const ConfigBlock &) noexcept
const ConfigBlock &block) noexcept
{
return std::make_unique<UpnpDatabase>(io_event_loop);
return std::make_unique<UpnpDatabase>(io_event_loop, block);;
}
void
UpnpDatabase::Open()
{
handle = UpnpClientGlobalInit();
handle = UpnpClientGlobalInit(interface);
discovery = new UPnPDeviceDirectory(event_loop, handle);
try {

View File

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

View File

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

View File

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

View File

@@ -502,7 +502,7 @@ FfmpegDecode(DecoderClient &client, InputStream *input,
FmtDebug(ffmpeg_domain, "codec '{}'",
codec_descriptor->name);
AVCodec *codec = avcodec_find_decoder(codec_params.codec_id);
const AVCodec *codec = avcodec_find_decoder(codec_params.codec_id);
if (!codec) {
LogError(ffmpeg_domain, "Unsupported audio codec");

View File

@@ -52,6 +52,13 @@ EventLoop::EventLoop(
EventLoop::~EventLoop() noexcept
{
#if defined(HAVE_URING) && !defined(NDEBUG)
/* if Run() was never called (maybe because startup failed and
an exception is pending), we need to destruct the
Uring::Manager here or else the assertions below fail */
uring.reset();
#endif
assert(defer.empty());
assert(idle.empty());
#ifdef HAVE_THREADED_EVENT_LOOP
@@ -175,6 +182,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;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (...) {

View File

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

View File

@@ -26,6 +26,7 @@
#include "AlsaInputPlugin.hxx"
#include "lib/alsa/NonBlock.hxx"
#include "lib/alsa/Error.hxx"
#include "lib/alsa/Format.hxx"
#include "../InputPlugin.hxx"
#include "../AsyncInputStream.hxx"
@@ -332,28 +333,23 @@ AlsaInputStream::ConfigureCapture(AudioFormat audio_format)
snd_pcm_hw_params_alloca(&hw_params);
if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0)
throw FormatRuntimeError("Cannot initialize hardware parameter structure (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params,
SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
throw FormatRuntimeError("Cannot set access type (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params,
ToAlsaPcmFormat(audio_format.format))) < 0)
throw FormatRuntimeError("Cannot set sample format (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "Cannot set sample format");
if ((err = snd_pcm_hw_params_set_channels(capture_handle,
hw_params, audio_format.channels)) < 0)
throw FormatRuntimeError("Cannot set channels (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "Cannot set channels");
if ((err = snd_pcm_hw_params_set_rate(capture_handle,
hw_params, audio_format.sample_rate, 0)) < 0)
throw FormatRuntimeError("Cannot set sample rate (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "Cannot set sample rate");
snd_pcm_uframes_t buffer_size_min, buffer_size_max;
snd_pcm_hw_params_get_buffer_size_min(hw_params, &buffer_size_min);
@@ -388,26 +384,22 @@ AlsaInputStream::ConfigureCapture(AudioFormat audio_format)
int direction = -1;
if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle,
hw_params, &period_size, &direction)) < 0)
throw FormatRuntimeError("Cannot set period size (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "Cannot set period size");
}
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0)
throw FormatRuntimeError("Cannot set parameters (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
snd_pcm_uframes_t alsa_buffer_size;
err = snd_pcm_hw_params_get_buffer_size(hw_params, &alsa_buffer_size);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_get_buffer_size() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_buffer_size() failed");
snd_pcm_uframes_t alsa_period_size;
err = snd_pcm_hw_params_get_period_size(hw_params, &alsa_period_size,
nullptr);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_get_period_size() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_period_size() failed");
FmtDebug(alsa_input_domain, "buffer_size={} period_size={}",
alsa_buffer_size, alsa_period_size);
@@ -418,8 +410,7 @@ AlsaInputStream::ConfigureCapture(AudioFormat audio_format)
snd_pcm_sw_params_current(capture_handle, sw_params);
if ((err = snd_pcm_sw_params(capture_handle, sw_params)) < 0)
throw FormatRuntimeError("unable to install sw params (%s)",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
}
inline void
@@ -430,8 +421,9 @@ AlsaInputStream::OpenDevice(const SourceSpec &spec)
if ((err = snd_pcm_open(&capture_handle, spec.GetDeviceName(),
SND_PCM_STREAM_CAPTURE,
SND_PCM_NONBLOCK | global_config.mode)) < 0)
throw FormatRuntimeError("Failed to open device: %s (%s)",
spec.GetDeviceName(), snd_strerror(err));
throw Alsa::MakeError(err,
fmt::format("Failed to open device {}",
spec.GetDeviceName()).c_str());
try {
ConfigureCapture(spec.GetAudioFormat());

View File

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

View File

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

44
src/lib/alsa/Error.cxx Normal file
View File

@@ -0,0 +1,44 @@
/*
* Copyright 2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Error.hxx"
#include <alsa/error.h>
namespace Alsa {
ErrorCategory error_category;
std::string
ErrorCategory::message(int condition) const
{
return snd_strerror(condition);
}
} // namespace Avahi

53
src/lib/alsa/Error.hxx Normal file
View File

@@ -0,0 +1,53 @@
/*
* Copyright 2021 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <system_error>
namespace Alsa {
class ErrorCategory final : public std::error_category {
public:
const char *name() const noexcept override {
return "libasound";
}
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 Avahi

View File

@@ -18,7 +18,9 @@
*/
#include "HwSetup.hxx"
#include "Error.hxx"
#include "Format.hxx"
#include "lib/fmt/AudioFormatFormatter.hxx"
#include "util/ByteOrder.hxx"
#include "util/Domain.hxx"
#include "util/RuntimeError.hxx"
@@ -185,29 +187,27 @@ SetupHw(snd_pcm_t *pcm,
/* configure HW params */
err = snd_pcm_hw_params_any(pcm, hwparams);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_any() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
err = snd_pcm_hw_params_set_access(pcm, hwparams,
SND_PCM_ACCESS_RW_INTERLEAVED);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_set_access() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
err = SetupSampleFormat(pcm, hwparams,
audio_format.format, params);
if (err < 0)
throw FormatRuntimeError("Failed to configure format %s: %s",
sample_format_to_string(audio_format.format),
snd_strerror(-err));
throw Alsa::MakeError(err,
fmt::format("Failed to configure format {}",
audio_format.format).c_str());
unsigned int channels = audio_format.channels;
err = snd_pcm_hw_params_set_channels_near(pcm, hwparams,
&channels);
if (err < 0)
throw FormatRuntimeError("Failed to configure %i channels: %s",
(int)audio_format.channels,
snd_strerror(-err));
throw Alsa::MakeError(err,
fmt::format("Failed to configure {} channels",
audio_format.channels).c_str());
audio_format.channels = (int8_t)channels;
@@ -218,9 +218,9 @@ SetupHw(snd_pcm_t *pcm,
err = snd_pcm_hw_params_set_rate_near(pcm, hwparams,
&output_sample_rate, nullptr);
if (err < 0)
throw FormatRuntimeError("Failed to configure sample rate %u Hz: %s",
requested_sample_rate,
snd_strerror(-err));
throw Alsa::MakeError(err,
fmt::format("Failed to configure sample rate {} Hz",
requested_sample_rate).c_str());
if (output_sample_rate == 0)
throw FormatRuntimeError("Failed to configure sample rate %u Hz",
@@ -253,8 +253,7 @@ SetupHw(snd_pcm_t *pcm,
err = snd_pcm_hw_params_set_buffer_time_near(pcm, hwparams,
&buffer_time, nullptr);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_set_buffer_time_near() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_buffer_time_near() failed");
} else {
err = snd_pcm_hw_params_get_buffer_time(hwparams, &buffer_time,
nullptr);
@@ -275,32 +274,27 @@ SetupHw(snd_pcm_t *pcm,
err = snd_pcm_hw_params_set_period_time_near(pcm, hwparams,
&period_time, nullptr);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_set_period_time_near() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_period_time_near() failed");
}
err = snd_pcm_hw_params(pcm, hwparams);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
HwResult result;
err = snd_pcm_hw_params_get_format(hwparams, &result.format);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_get_format() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_format() failed");
err = snd_pcm_hw_params_get_buffer_size(hwparams, &result.buffer_size);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_get_buffer_size() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_buffer_size() failed");
err = snd_pcm_hw_params_get_period_size(hwparams, &result.period_size,
nullptr);
if (err < 0)
throw FormatRuntimeError("snd_pcm_hw_params_get_period_size() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_hw_params_get_period_size() failed");
return result;
}

View File

@@ -18,6 +18,7 @@
*/
#include "NonBlock.hxx"
#include "Error.hxx"
#include "event/MultiSocketMonitor.hxx"
#include "util/RuntimeError.hxx"
@@ -29,8 +30,7 @@ AlsaNonBlockPcm::PrepareSockets(MultiSocketMonitor &m, snd_pcm_t *pcm)
if (count == 0)
throw std::runtime_error("snd_pcm_poll_descriptors_count() failed");
else
throw FormatRuntimeError("snd_pcm_poll_descriptors_count() failed: %s",
snd_strerror(-count));
throw Alsa::MakeError(count, "snd_pcm_poll_descriptors_count() failed");
}
struct pollfd *pfds = pfd_buffer.Get(count);
@@ -40,8 +40,7 @@ AlsaNonBlockPcm::PrepareSockets(MultiSocketMonitor &m, snd_pcm_t *pcm)
if (count == 0)
throw std::runtime_error("snd_pcm_poll_descriptors() failed");
else
throw FormatRuntimeError("snd_pcm_poll_descriptors() failed: %s",
snd_strerror(-count));
throw Alsa::MakeError(count, "snd_pcm_poll_descriptors() failed");
}
m.ReplaceSocketList(pfds, count);
@@ -71,8 +70,7 @@ AlsaNonBlockPcm::DispatchSockets(MultiSocketMonitor &m,
unsigned short dummy;
int err = snd_pcm_poll_descriptors_revents(pcm, pfds, i - pfds, &dummy);
if (err < 0)
throw FormatRuntimeError("snd_pcm_poll_descriptors_revents() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_poll_descriptors_revents() failed");
}
Event::Duration

View File

@@ -14,6 +14,7 @@ conf.set('ENABLE_ALSA', true)
alsa = static_library(
'alsa',
'Version.cxx',
'Error.cxx',
'AllowedFormat.cxx',
'HwSetup.cxx',
'NonBlock.cxx',

View File

@@ -36,7 +36,7 @@ class CodecContext {
public:
CodecContext() = default;
explicit CodecContext(AVCodec &codec)
explicit CodecContext(const AVCodec &codec)
:codec_context(avcodec_alloc_context3(&codec))
{
if (codec_context == nullptr)

View File

@@ -35,6 +35,16 @@
#include <fmt/format.h>
template<>
struct fmt::formatter<SampleFormat> : formatter<string_view>
{
template<typename FormatContext>
auto format(const SampleFormat format, FormatContext &ctx) {
return formatter<string_view>::format(sample_format_to_string(format),
ctx);
}
};
template<>
struct fmt::formatter<AudioFormat> : formatter<string_view>
{

View File

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

View File

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

View 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

View File

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

View File

@@ -57,9 +57,9 @@ DoInit()
}
UpnpClient_Handle
UpnpClientGlobalInit()
UpnpClientGlobalInit(const char* iface)
{
UpnpGlobalInit();
UpnpGlobalInit(iface);
try {
const std::lock_guard<Mutex> protect(upnp_client_init_mutex);

View File

@@ -23,7 +23,7 @@
#include "Compat.hxx"
UpnpClient_Handle
UpnpClientGlobalInit();
UpnpClientGlobalInit(const char* iface);
void
UpnpClientGlobalFinish() noexcept;

View File

@@ -33,12 +33,13 @@ static Mutex upnp_init_mutex;
static unsigned upnp_ref;
static void
DoInit()
DoInit(const char* iface)
{
#ifdef UPNP_ENABLE_IPV6
auto code = UpnpInit2(nullptr, 0);
auto code = UpnpInit2(iface, 0);
#else
auto code = UpnpInit(nullptr, 0);
auto code = UpnpInit(iface, 0);
#endif
if (code != UPNP_E_SUCCESS)
throw FormatRuntimeError("UpnpInit() failed: %s",
@@ -53,12 +54,12 @@ DoInit()
}
void
UpnpGlobalInit()
UpnpGlobalInit(const char* iface)
{
const std::lock_guard<Mutex> protect(upnp_init_mutex);
if (upnp_ref == 0)
DoInit();
DoInit(iface);
++upnp_ref;
}

View File

@@ -21,7 +21,7 @@
#define MPD_UPNP_INIT_HXX
void
UpnpGlobalInit();
UpnpGlobalInit(const char* iface);
void
UpnpGlobalFinish() noexcept;

View File

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

View File

@@ -18,6 +18,7 @@
*/
#include "lib/alsa/NonBlock.hxx"
#include "lib/alsa/Error.hxx"
#include "mixer/MixerInternal.hxx"
#include "mixer/Listener.hxx"
#include "output/OutputAPI.hxx"
@@ -264,16 +265,15 @@ AlsaMixer::Setup()
int err;
if ((err = snd_mixer_attach(handle, device)) < 0)
throw FormatRuntimeError("failed to attach to %s: %s",
device, snd_strerror(err));
throw Alsa::MakeError(err,
fmt::format("failed to attach to {}",
device).c_str());
if ((err = snd_mixer_selem_register(handle, nullptr, nullptr)) < 0)
throw FormatRuntimeError("snd_mixer_selem_register() failed: %s",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_mixer_selem_register() failed");
if ((err = snd_mixer_load(handle)) < 0)
throw FormatRuntimeError("snd_mixer_load() failed: %s\n",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_mixer_load() failed");
elem = alsa_mixer_lookup_elem(handle, control, index);
if (elem == nullptr)
@@ -294,8 +294,7 @@ AlsaMixer::Open()
err = snd_mixer_open(&handle, 0);
if (err < 0)
throw FormatRuntimeError("snd_mixer_open() failed: %s",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_mixer_open() failed");
try {
Setup();
@@ -325,8 +324,7 @@ AlsaMixer::GetVolume()
err = snd_mixer_handle_events(handle);
if (err < 0)
throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
snd_strerror(err));
throw Alsa::MakeError(err, "snd_mixer_handle_events() failed");
int volume = GetPercentVolume();
if (resulting_volume >= 0 && volume == resulting_volume)
@@ -343,8 +341,7 @@ AlsaMixer::SetVolume(unsigned volume)
int err = set_normalized_playback_volume(elem, 0.01*volume, 1);
if (err < 0)
throw FormatRuntimeError("failed to set ALSA volume: %s",
snd_strerror(err));
throw Alsa::MakeError(err, "failed to set ALSA volume");
desired_volume = volume;
resulting_volume = GetPercentVolume();

View File

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

View File

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

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ private:
void
UpnpNeighborExplorer::Open()
{
auto handle = UpnpClientGlobalInit();
auto handle = UpnpClientGlobalInit(nullptr);
discovery = new UPnPDeviceDirectory(event_loop, handle, this);

View File

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

View File

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

View File

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

View File

@@ -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.
*/

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
#include "config.h"
#include "AlsaOutputPlugin.hxx"
#include "lib/alsa/AllowedFormat.hxx"
#include "lib/alsa/Error.hxx"
#include "lib/alsa/HwSetup.hxx"
#include "lib/alsa/NonBlock.hxx"
#include "lib/alsa/PeriodBuffer.hxx"
@@ -42,6 +43,10 @@
#include "event/Call.hxx"
#include "Log.hxx"
#ifdef ENABLE_DSD
#include "util/AllocatedArray.hxx"
#endif
#include <alsa/asoundlib.h>
#include <boost/lockfree/spsc_queue.hpp>
@@ -84,6 +89,33 @@ 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;
/**
* Enable the DSD sync workaround for Thesycon USB audio
* receivers? On this device, playing DSD512 or PCM causes
* all subsequent attempts to play other DSD rates to fail,
* which can be fixed by briefly playing PCM at 44.1 kHz.
*/
const bool thesycon_dsd_workaround;
bool need_thesycon_dsd_workaround = thesycon_dsd_workaround;
#endif
/** libasound's buffer_time setting (in microseconds) */
@@ -93,7 +125,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 +376,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 +387,9 @@ private:
error = std::current_exception();
active = false;
waiting = false;
#ifdef ENABLE_DSD
in_stop_dsd_silence = false;
#endif
cond.notify_one();
}
@@ -408,21 +413,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 +433,28 @@ 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)),
thesycon_dsd_workaround(block.GetBlockValue("thesycon_dsd_workaround",
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 +479,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);
@@ -519,24 +536,20 @@ AlsaSetupSw(snd_pcm_t *pcm, snd_pcm_uframes_t start_threshold,
int err = snd_pcm_sw_params_current(pcm, swparams);
if (err < 0)
throw FormatRuntimeError("snd_pcm_sw_params_current() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed");
err = snd_pcm_sw_params_set_start_threshold(pcm, swparams,
start_threshold);
if (err < 0)
throw FormatRuntimeError("snd_pcm_sw_params_set_start_threshold() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed");
err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min);
if (err < 0)
throw FormatRuntimeError("snd_pcm_sw_params_set_avail_min() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_sw_params_set_avail_min() failed");
err = snd_pcm_sw_params(pcm, swparams);
if (err < 0)
throw FormatRuntimeError("snd_pcm_sw_params() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
}
inline void
@@ -678,6 +691,97 @@ BestMatch(const std::forward_list<Alsa::AllowedFormat> &haystack,
return haystack.front();
}
#ifdef ENABLE_DSD
static void
Play_44_1_Silence(snd_pcm_t *pcm)
{
snd_pcm_hw_params_t *hw;
snd_pcm_hw_params_alloca(&hw);
int err;
err = snd_pcm_hw_params_any(pcm, hw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
err = snd_pcm_hw_params_set_access(pcm, hw,
SND_PCM_ACCESS_RW_INTERLEAVED);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
err = snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S16);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_format() failed");
unsigned channels = 1;
err = snd_pcm_hw_params_set_channels_near(pcm, hw, &channels);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_channels_near() failed");
constexpr snd_pcm_uframes_t rate = 44100;
err = snd_pcm_hw_params_set_rate(pcm, hw, rate, 0);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_rate() failed");
snd_pcm_uframes_t buffer_size = 1;
err = snd_pcm_hw_params_set_buffer_size_near(pcm, hw, &buffer_size);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_buffer_size_near() failed");
snd_pcm_uframes_t period_size = 1;
int dir = 0;
err = snd_pcm_hw_params_set_period_size_near(pcm, hw, &period_size,
&dir);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_period_size_near() failed");
err = snd_pcm_hw_params(pcm, hw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
snd_pcm_sw_params_t *sw;
snd_pcm_sw_params_alloca(&sw);
err = snd_pcm_sw_params_current(pcm, sw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed");
err = snd_pcm_sw_params_set_start_threshold(pcm, sw, period_size);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed");
err = snd_pcm_sw_params(pcm, sw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
err = snd_pcm_prepare(pcm);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_prepare() failed");
AllocatedArray<int16_t> buffer{channels * period_size};
std::fill(buffer.begin(), buffer.end(), 0);
/* play at least 250ms of silence */
for (snd_pcm_uframes_t remaining_frames = rate / 4;;) {
auto n = snd_pcm_writei(pcm, buffer.data(),
period_size);
if (n < 0)
throw Alsa::MakeError(err, "snd_pcm_writei() failed");
if (snd_pcm_uframes_t(n) >= remaining_frames)
break;
remaining_frames -= snd_pcm_uframes_t(n);
}
err = snd_pcm_drain(pcm);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_drain() failed");
}
#endif
void
AlsaOutput::Open(AudioFormat &audio_format)
{
@@ -704,13 +808,30 @@ AlsaOutput::Open(AudioFormat &audio_format)
int err = snd_pcm_open(&pcm, GetDevice(),
SND_PCM_STREAM_PLAYBACK, mode);
if (err < 0)
throw FormatRuntimeError("Failed to open ALSA device \"%s\": %s",
GetDevice(), snd_strerror(err));
throw Alsa::MakeError(err,
fmt::format("Failed to open ALSA device \"{}\"",
GetDevice()).c_str());
FmtDebug(alsa_output_domain, "opened {} type={}",
snd_pcm_name(pcm),
snd_pcm_type_name(snd_pcm_type(pcm)));
#ifdef ENABLE_DSD
if (need_thesycon_dsd_workaround &&
audio_format.format == SampleFormat::DSD &&
audio_format.sample_rate <= 256 * 44100 / 8) {
LogDebug(alsa_output_domain, "Playing some 44.1 kHz silence");
try {
Play_44_1_Silence(pcm);
} catch (...) {
LogError(std::current_exception());
}
need_thesycon_dsd_workaround = false;
}
#endif
PcmExport::Params params;
params.alsa_channel_order = true;
@@ -732,6 +853,14 @@ 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 (thesycon_dsd_workaround &&
(!use_dsd ||
audio_format.sample_rate > 256 * 44100 / 8))
need_thesycon_dsd_workaround = true;
if (params.dsd_mode == PcmExport::DsdMode::DOP)
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
#endif
@@ -824,9 +953,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();
@@ -844,8 +1023,8 @@ AlsaOutput::DrainInternal()
if (frames_written == -EAGAIN)
return false;
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
snd_strerror(-frames_written));
throw Alsa::MakeError(frames_written,
"snd_pcm_writei() failed");
}
/* need to call CopyRingToPeriodBuffer() and
@@ -894,8 +1073,7 @@ AlsaOutput::DrainInternal()
else if (result == -EAGAIN)
return false;
else
throw FormatRuntimeError("snd_pcm_drain() failed: %s",
snd_strerror(-result));
throw Alsa::MakeError(result, "snd_pcm_drain() failed");
}
void
@@ -957,6 +1135,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();
});
@@ -1083,8 +1272,7 @@ try {
int err = snd_pcm_prepare(pcm);
if (err < 0)
throw FormatRuntimeError("snd_pcm_prepare() failed: %s",
snd_strerror(-err));
throw Alsa::MakeError(err, "snd_pcm_prepare() failed");
}
{
@@ -1172,8 +1360,8 @@ try {
return;
if (Recover(frames_written) < 0)
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
snd_strerror(-frames_written));
throw Alsa::MakeError(frames_written,
"snd_pcm_writei() failed");
/* recovered; try again in the next DispatchSockets()
call */

View File

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

View File

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

View File

@@ -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,18 @@
#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 <array>
#include <stdexcept>
#include <string>
static constexpr Domain pipewire_output_domain("pipewire_output");
@@ -60,7 +70,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 +87,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 +164,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 +217,46 @@ private:
o.Drained();
}
void ControlInfo(const struct pw_stream_control *control) noexcept {
float sum = 0;
unsigned c;
for (c = 0; c < control->n_values; c++)
sum += control->values[c];
sum /= control->n_values;
if (mixer != nullptr)
pipewire_mixer_on_change(*mixer, std::cbrt(sum));
pw_thread_loop_signal(thread_loop, false);
}
static void ControlInfo(void *data,
[[maybe_unused]] uint32_t id,
const struct pw_stream_control *control) noexcept {
auto &o = *(PipeWireOutput *)data;
if (StringIsEqual(control->name, "Channel Volumes"))
o.ControlInfo(control);
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
void DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept;
void DsdFormatChanged(const struct spa_pod &param) noexcept;
#endif
void ParamChanged(uint32_t id, const struct spa_pod *param) noexcept;
static void ParamChanged(void *data,
uint32_t id,
const struct spa_pod *param) noexcept
{
if (id != SPA_PARAM_Format || param == NULL)
return;
auto &o = *(PipeWireOutput *)data;
o.ParamChanged(id, param);
}
/* virtual methods from class AudioOutput */
void Enable() override;
void Disable() noexcept override;
@@ -185,6 +279,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 +291,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 +313,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 +333,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 +462,7 @@ ToPipeWireAudioFormat(AudioFormat &audio_format) noexcept
void
PipeWireOutput::Open(AudioFormat &audio_format)
{
error_message.clear();
disconnected = false;
restore_volume = true;
@@ -383,6 +489,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 +504,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 +538,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 +595,119 @@ PipeWireOutput::StateChanged(enum pw_stream_state state,
const bool was_disconnected = disconnected;
disconnected = state == PW_STREAM_STATE_ERROR ||
state == PW_STREAM_STATE_UNCONNECTED;
if (!was_disconnected && disconnected)
pw_thread_loop_signal(thread_loop, false);
if (!was_disconnected && disconnected) {
if (error != nullptr)
error_message = error;
if (state == PW_STREAM_STATE_STREAMING && restore_volume) {
/* restore the last known volume after creating a new
pw_stream */
pw_thread_loop_signal(thread_loop, false);
}
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
inline void
PipeWireOutput::DsdFormatChanged(const struct spa_audio_info_dsd &dsd) noexcept
{
/* MPD uses MSB internally, which means if PipeWire asks LSB
from us, we need to reverse the bits in each DSD byte */
dsd_reverse_bits = dsd.bitorder == SPA_PARAM_BITORDER_lsb;
dsd_interleave = dsd.interleave;
}
inline void
PipeWireOutput::DsdFormatChanged(const struct spa_pod &param) noexcept
{
uint32_t media_type, media_subtype;
struct spa_audio_info_dsd dsd;
if (spa_format_parse(&param, &media_type, &media_subtype) >= 0 &&
media_type == SPA_MEDIA_TYPE_audio &&
media_subtype == SPA_MEDIA_SUBTYPE_dsd &&
spa_format_audio_dsd_parse(&param, &dsd) >= 0)
DsdFormatChanged(dsd);
}
#endif
inline void
PipeWireOutput::ParamChanged([[maybe_unused]] uint32_t id,
[[maybe_unused]] const struct spa_pod *param) noexcept
{
if (restore_volume) {
SetVolume(volume);
restore_volume = false;
pw_stream_set_control(stream,
SPA_PROP_volume, 1, &volume,
0);
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
if (use_dsd && id == SPA_PARAM_Format && param != nullptr)
DsdFormatChanged(*param);
#endif
}
#if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
static void
Interleave(std::byte *data, std::byte *end,
std::size_t channels, std::size_t interleave) noexcept
{
assert(channels > 1);
assert(channels <= MAX_CHANNELS);
constexpr std::size_t MAX_INTERLEAVE = 8;
assert(interleave > 1);
assert(interleave <= MAX_INTERLEAVE);
std::array<std::byte, MAX_CHANNELS * MAX_INTERLEAVE> buffer;
std::size_t buffer_size = channels * interleave;
while (data < end) {
std::copy_n(data, buffer_size, buffer.data());
const std::byte *src0 = buffer.data();
for (std::size_t channel = 0; channel < channels;
++channel, ++src0) {
const std::byte *src = src0;
for (std::size_t i = 0; i < interleave;
++i, src += channels)
*data++ = *src;
}
}
}
static void
BitReverse(uint8_t *data, std::size_t n) noexcept
{
while (n-- > 0)
*data = bit_reverse(*data);
}
static void
BitReverse(std::byte *data, std::size_t n) noexcept
{
BitReverse((uint8_t *)data, n);
}
static void
PostProcessDsd(std::byte *data, struct spa_chunk &chunk, unsigned channels,
bool reverse_bits, unsigned interleave) noexcept
{
assert(chunk.size % channels == 0);
if (interleave > 1 && channels > 1) {
assert(chunk.size % (channels * interleave) == 0);
Interleave(data, data + chunk.size, channels, interleave);
chunk.stride *= interleave;
}
if (reverse_bits)
BitReverse(data, chunk.size);
}
#endif
inline void
PipeWireOutput::Process() noexcept
{
@@ -463,15 +717,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 +756,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 +856,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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
],
)

View File

@@ -5,7 +5,7 @@ After=network.target sound.target
[Service]
Type=notify
ExecStart=@prefix@/bin/mpd --no-daemon
ExecStart=@prefix@/bin/mpd --systemd
# Enable this setting to ask systemd to watch over MPD, see
# systemd.service(5). This is disabled by default because it causes

View File

@@ -5,7 +5,7 @@ After=network.target sound.target
[Service]
Type=notify
ExecStart=@prefix@/bin/mpd --no-daemon
ExecStart=@prefix@/bin/mpd --systemd
# Enable this setting to ask systemd to watch over MPD, see
# systemd.service(5). This is disabled by default because it causes

Some files were not shown because too many files have changed in this diff Show More