Compare commits

..

43 Commits

Author SHA1 Message Date
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
39 changed files with 533 additions and 268 deletions

12
NEWS

@@ -1,3 +1,15 @@
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

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

@@ -13,7 +13,7 @@ GENCLASS="$D/classes"
GENINCLUDE="$D/include"
mkdir -p "$GENSRC/$JAVA_PKG_PATH"
"$JAVAC" -source 1.6 -target 1.6 -Xlint:-options \
"$JAVAC" -source 1.7 -target 1.7 -Xlint:-options \
-cp "$CLASSPATH" \
-h "$GENINCLUDE" \
-d "$GENCLASS" \

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

@@ -836,6 +836,11 @@ The `Advanced Linux Sound Architecture (ALSA) <http://www.alsa-project.org/>`_ p
- If set to no, then libasound will not attempt to convert between different sample formats (16 bit, 24 bit, floating point, ...).
* - **dop yes|no**
- If set to yes, then DSD over PCM according to the `DoP standard <http://dsd-guide.com/dop-open-standard>`_ is enabled. This wraps DSD samples in fake 24 bit PCM, and is understood by some DSD capable products, but may be harmful to other hardware. Therefore, the default is no and you can enable the option at your own risk.
* - **stop_dsd_silence yes|no**
- If enabled, silence is played before manually stopping playback
("stop" or "pause") in DSD mode (native DSD or DoP). This is a
workaround for some DACs which emit noise when stopping DSD
playback.
* - **allowed_formats F1 F2 ...**
- Specifies a list of allowed audio formats, separated by a space. All items may contain asterisks as a wild card, and may be followed by "=dop" to enable DoP (DSD over PCM) for this particular format. The first matching format is used, and if none matches, MPD chooses the best fallback of this list.

@@ -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:
@@ -926,16 +929,19 @@ remote playlists (absolute URI with a supported scheme).
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.)
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}`
@@ -947,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}`
@@ -1197,7 +1205,7 @@ The music database
.. _command_search:
:command:`search {FILTER} [sort {TYPE}] [window {START:END}]
: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>`,
@@ -1213,7 +1221,7 @@ The music database
Parameters have the same meaning as for :ref:`search <command_search>`.
The ``position`` parameter specifies where the songs will be
inserted.
inserted. [#since_0_23]_
.. _command_searchaddpl:
@@ -1644,3 +1652,6 @@ client-to-client messages are local to the current partition.
.. [#since_0_20] Since :program:`MPD` 0.20
.. [#since_0_21] Since :program:`MPD` 0.21
.. [#since_0_22_4] Since :program:`MPD` 0.22.4
.. [#since_0_23] Since :program:`MPD` 0.23
.. [#since_0_23_1] Since :program:`MPD` 0.23.1
.. [#since_0_23_3] Since :program:`MPD` 0.23.3

@@ -172,7 +172,9 @@ tarball and change into the directory. Then, instead of
mkdir -p output/win64
cd output/win64
../../win32/build.py --64
../../win32/build.py --64 \
--buildtype=debugoptimized -Db_ndebug=true \
-Dwrap_mode=forcefallback
This downloads various library sources, and then configures and builds
:program:`MPD` (for x64; to build a 32 bit binary, pass
@@ -182,6 +184,11 @@ around. It is large, but easy to use. If you wish to have a small
mpd.exe with DLLs, you need to compile manually, without the
:file:`build.py` script.
The option ``-Dwrap_mode=forcefallback`` tells Meson to download and
cross-compile several libraries used by MPD instead of looking for
them on your computer.
Compiling for Android
---------------------
@@ -205,8 +212,10 @@ tarball and change into the directory. Then, instead of
mkdir -p output/android
cd output/android
../../android/build.py SDK_PATH NDK_PATH ABI
meson configure -Dandroid_debug_keystore=$HOME/.android/debug.keystore
../../android/build.py SDK_PATH NDK_PATH ABI \
--buildtype=debugoptimized -Db_ndebug=true \
-Dwrap_mode=forcefallback \
-Dandroid_debug_keystore=$HOME/.android/debug.keystore
ninja android/apk/mpd-debug.apk
:envvar:`SDK_PATH` is the absolute path where you installed the

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.23.2',
version: '0.23.3',
meson_version: '>= 0.56.0',
default_options: [
'c_std=c11',
@@ -44,7 +44,7 @@ version_conf = configuration_data()
version_conf.set_quoted('PACKAGE', meson.project_name())
version_conf.set_quoted('PACKAGE_NAME', meson.project_name())
version_conf.set_quoted('VERSION', meson.project_version())
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.1')
version_conf.set_quoted('PROTOCOL_VERSION', '0.23.3')
configure_file(output: 'Version.h', configuration: version_conf)
conf = configuration_data()

@@ -112,8 +112,8 @@ libmodplug = AutotoolsProject(
)
libopenmpt = AutotoolsProject(
'https://lib.openmpt.org/files/libopenmpt/src/libopenmpt-0.5.8+release.autotools.tar.gz',
'61de7cc0c011b10472ca16adcc123689',
'https://lib.openmpt.org/files/libopenmpt/src/libopenmpt-0.5.12+release.autotools.tar.gz',
'892aea7a599b5d21842bebf463b5aafdad5711be7008dd84401920c6234820af',
'lib/libopenmpt.a',
[
'--disable-shared', '--enable-static'
@@ -147,8 +147,8 @@ gme = CmakeProject(
)
ffmpeg = FfmpegProject(
'http://ffmpeg.org/releases/ffmpeg-4.4.tar.xz',
'06b10a183ce5371f915c6bb15b7b1fffbe046e8275099c96affc29e17645d909',
'http://ffmpeg.org/releases/ffmpeg-4.4.1.tar.xz',
'eadbad9e9ab30b25f5520fbfde99fae4a92a1ae3c0257a8d68569a4651e30e02',
'lib/libavcodec.a',
[
'--disable-shared', '--enable-static',

@@ -32,6 +32,7 @@
#include "net/SocketUtil.hxx"
#include "system/Error.hxx"
#include "fs/AllocatedPath.hxx"
#include "fs/StandardDirectory.hxx"
#include "fs/XDG.hxx"
#include "util/Domain.hxx"
#include "util/RuntimeError.hxx"
@@ -85,13 +86,10 @@ ListenXdgRuntimeDir(ClientListener &listener) noexcept
use $XDG_RUNTIME_DIR */
return false;
Path xdg_runtime_dir = Path::FromFS(getenv("XDG_RUNTIME_DIR"));
if (xdg_runtime_dir.IsNull())
const auto mpd_runtime_dir = GetAppRuntimeDir();
if (mpd_runtime_dir.IsNull())
return false;
const auto mpd_runtime_dir = xdg_runtime_dir / Path::FromFS("mpd");
mkdir(mpd_runtime_dir.c_str(), 0700);
const auto socket_path = mpd_runtime_dir / Path::FromFS("socket");
unlink(socket_path.c_str());

@@ -30,7 +30,6 @@
#include "util/StringView.hxx"
#include <cassert>
#include <cstring>
#include <map>
#include <string>
#include <utility>
@@ -100,18 +99,15 @@ initPermissions(const ConfigData &config)
for (const auto &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));
});
}

@@ -26,6 +26,7 @@
#include "song/DetachedSong.hxx"
#include "SongLoader.hxx"
#include "Mapper.hxx"
#include "protocol/RangeArg.hxx"
#include "fs/io/TextFile.hxx"
#include "fs/io/FileOutputStream.hxx"
#include "fs/io/BufferedOutputStream.hxx"
@@ -34,7 +35,6 @@
#include "config/Defaults.hxx"
#include "Idle.hxx"
#include "fs/Limits.hxx"
#include "fs/AllocatedPath.hxx"
#include "fs/Traits.hxx"
#include "fs/FileSystem.hxx"
#include "fs/FileInfo.hxx"
@@ -173,11 +173,8 @@ ListPlaylistFiles()
}
static void
SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path)
SavePlaylistFile(Path path_fs, const PlaylistFileContents &contents)
{
assert(utf8path != nullptr);
const auto path_fs = spl_map_to_fs(utf8path);
assert(!path_fs.IsNull());
FileOutputStream fos(path_fs);
@@ -191,12 +188,11 @@ SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path)
fos.Commit();
}
PlaylistFileContents
LoadPlaylistFile(const char *utf8path)
static PlaylistFileContents
LoadPlaylistFile(Path path_fs)
try {
PlaylistFileContents contents;
const auto path_fs = spl_map_to_fs(utf8path);
assert(!path_fs.IsNull());
TextFile file(path_fs);
@@ -251,16 +247,54 @@ try {
throw;
}
void
spl_move_index(const char *utf8path, unsigned src, unsigned dest)
static PlaylistFileContents
MaybeLoadPlaylistFile(Path path_fs, PlaylistFileEditor::LoadMode load_mode)
try {
if (load_mode == PlaylistFileEditor::LoadMode::NO)
return {};
return LoadPlaylistFile(path_fs);
} catch (const PlaylistError &error) {
if (error.GetCode() == PlaylistResult::NO_SUCH_LIST &&
load_mode == PlaylistFileEditor::LoadMode::TRY)
return {};
throw;
}
PlaylistFileEditor::PlaylistFileEditor(const char *name_utf8,
LoadMode load_mode)
:path(spl_map_to_fs(name_utf8)),
contents(MaybeLoadPlaylistFile(path, load_mode))
{
if (src == dest)
/* this doesn't check whether the playlist exists, but
what the hell.. */
return;
}
auto contents = LoadPlaylistFile(utf8path);
void
PlaylistFileEditor::Insert(std::size_t i, const char *uri)
{
if (i > size())
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad position");
if (size() >= playlist_max_length)
throw PlaylistError(PlaylistResult::TOO_LARGE,
"Stored playlist is too large");
contents.emplace(std::next(contents.begin(), i), uri);
}
void
PlaylistFileEditor::Insert(std::size_t i, const DetachedSong &song)
{
const char *uri = playlist_saveAbsolutePaths
? song.GetRealURI()
: song.GetURI();
Insert(i, uri);
}
void
PlaylistFileEditor::MoveIndex(unsigned src, unsigned dest)
{
if (src >= contents.size() || dest >= contents.size())
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
@@ -270,9 +304,31 @@ spl_move_index(const char *utf8path, unsigned src, unsigned dest)
const auto dest_i = std::next(contents.begin(), dest);
contents.insert(dest_i, std::move(value));
}
SavePlaylistFile(contents, utf8path);
void
PlaylistFileEditor::RemoveIndex(unsigned i)
{
if (i >= contents.size())
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
contents.erase(std::next(contents.begin(), i));
}
void
PlaylistFileEditor::RemoveRange(RangeArg range)
{
if (!range.CheckClip(size()))
throw PlaylistError::BadRange();
contents.erase(std::next(contents.begin(), range.start),
std::next(contents.begin(), range.end));
}
void
PlaylistFileEditor::Save()
{
SavePlaylistFile(path, contents);
idle_add(IDLE_STORED_PLAYLIST);
}
@@ -314,20 +370,6 @@ spl_delete(const char *name_utf8)
idle_add(IDLE_STORED_PLAYLIST);
}
void
spl_remove_index(const char *utf8path, unsigned pos)
{
auto contents = LoadPlaylistFile(utf8path);
if (pos >= contents.size())
throw PlaylistError(PlaylistResult::BAD_RANGE, "Bad range");
contents.erase(std::next(contents.begin(), pos));
SavePlaylistFile(contents, utf8path);
idle_add(IDLE_STORED_PLAYLIST);
}
void
spl_append_song(const char *utf8path, const DetachedSong &song)
try {

@@ -20,19 +20,55 @@
#ifndef MPD_PLAYLIST_FILE_HXX
#define MPD_PLAYLIST_FILE_HXX
#include "fs/AllocatedPath.hxx"
#include <vector>
#include <string>
struct ConfigData;
struct RangeArg;
class DetachedSong;
class SongLoader;
class PlaylistVector;
class AllocatedPath;
typedef std::vector<std::string> PlaylistFileContents;
extern bool playlist_saveAbsolutePaths;
class PlaylistFileEditor {
const AllocatedPath path;
PlaylistFileContents contents;
public:
enum class LoadMode {
NO,
YES,
TRY,
};
/**
* Throws on error.
*/
explicit PlaylistFileEditor(const char *name_utf8, LoadMode load_mode);
auto size() const noexcept {
return contents.size();
}
void Insert(std::size_t i, const char *uri);
void Insert(std::size_t i, const DetachedSong &song);
void MoveIndex(unsigned src, unsigned dest);
void RemoveIndex(unsigned i);
void RemoveRange(RangeArg range);
void Save();
private:
void Load();
};
/**
* Perform some global initialization, e.g. load configuration values.
*/
@@ -55,21 +91,12 @@ spl_map_to_fs(const char *name_utf8);
PlaylistVector
ListPlaylistFiles();
PlaylistFileContents
LoadPlaylistFile(const char *utf8path);
void
spl_move_index(const char *utf8path, unsigned src, unsigned dest);
void
spl_clear(const char *utf8path);
void
spl_delete(const char *name_utf8);
void
spl_remove_index(const char *utf8path, unsigned pos);
void
spl_append_song(const char *utf8path, const DetachedSong &song);

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

@@ -224,10 +224,12 @@ GetChromaprintCommand::DecodeFile(std::string_view suffix, InputStream &is,
inline void
GetChromaprintCommand::DecodeFile()
{
const auto suffix = uri_get_suffix(uri);
if (suffix.empty())
const char *_suffix = PathTraitsUTF8::GetFilenameSuffix(uri.c_str());
if (_suffix == nullptr)
return;
const std::string_view suffix{_suffix};
InputStreamPtr input_stream;
try {

@@ -186,7 +186,8 @@ handle_moveoutput(Client &client, Request request, Response &response)
was_enabled);
else
/* copy the AudioOutputControl and add it to the output list */
dest_partition.outputs.AddCopy(output,was_enabled);
dest_partition.outputs.AddMoveFrom(std::move(*output),
was_enabled);
instance.EmitIdle(IDLE_OUTPUT);
return CommandResult::OK;

@@ -22,8 +22,10 @@
#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"
@@ -173,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;
}
@@ -187,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;
}
@@ -201,12 +212,56 @@ handle_playlistclear([[maybe_unused]] Client &client,
return CommandResult::OK;
}
static CommandResult
handle_playlistadd_position(Client &client, const char *playlist_name,
const char *uri, unsigned position,
Response &r)
{
PlaylistFileEditor editor{
playlist_name,
PlaylistFileEditor::LoadMode::TRY,
};
if (position > editor.size()) {
r.Error(ACK_ERROR_ARG, "Bad position");
return CommandResult::ERROR;
}
if (uri_has_scheme(uri)) {
editor.Insert(position, uri);
} else {
#ifdef ENABLE_DATABASE
const auto &db = client.GetDatabaseOrThrow();
const auto *storage = client.GetStorage();
const DatabaseSelection selection(uri, true, nullptr);
db.Visit(selection, [&editor, &position, storage](const auto &song){
editor.Insert(position,
DatabaseDetachSong(storage, song));
++position;
});
#else
(void)client;
r.Error(ACK_ERROR_NO_EXIST, "No database");
return CommandResult::ERROR;
#endif
}
editor.Save();
return CommandResult::OK;
}
CommandResult
handle_playlistadd(Client &client, Request args, [[maybe_unused]] Response &r)
{
const char *const playlist = args[0];
const char *const uri = args[1];
if (args.size >= 3)
return handle_playlistadd_position(client, playlist, uri,
args.ParseUnsigned(2), r);
if (uri_has_scheme(uri)) {
const SongLoader loader(client);
spl_append_uri(playlist, loader, uri);

@@ -52,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
@@ -84,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
@@ -94,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

@@ -23,11 +23,10 @@
#include "fs/Traits.hxx"
#include "fs/StandardDirectory.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringView.hxx"
#include <cassert>
#include <string.h>
#ifndef _WIN32
#include <pwd.h>
@@ -96,30 +95,18 @@ ParsePath(const char *path)
if (*path == '\0')
return GetConfiguredHome();
AllocatedPath home = nullptr;
if (*path == '/') {
home = GetConfiguredHome();
++path;
return GetConfiguredHome() /
AllocatedPath::FromUTF8Throw(path);
} else {
const char *slash = std::strchr(path, '/');
const char *end = slash == nullptr
? path + strlen(path)
: slash;
const std::string user(path, end);
home = GetHome(user.c_str());
const auto [user, rest] =
StringView{path}.Split('/');
if (slash == nullptr)
return home;
path = slash + 1;
return GetHome(std::string{user}.c_str())
/ AllocatedPath::FromUTF8Throw(rest);
}
if (home.IsNull())
return nullptr;
return home / AllocatedPath::FromUTF8Throw(path);
} else if (!PathTraitsUTF8::IsAbsolute(path)) {
throw FormatRuntimeError("not an absolute path: %s", path);
} else {

@@ -282,7 +282,7 @@ InotifyUpdate::InotifyCallback(int wd, unsigned mask,
(mask & IN_ISDIR) != 0) {
/* a sub directory was changed: register those in
inotify */
const Path root_path = root->name;
const auto root_path = root->name;
const auto path_fs = uri_fs.IsNull()
? root_path

@@ -188,8 +188,8 @@ UpdateWalk::UpdateRegularFile(Directory &directory,
const char *name,
const StorageFileInfo &info) noexcept
{
const auto suffix = uri_get_suffix(name);
if (suffix.empty())
const char *suffix = PathTraitsUTF8::GetFilenameSuffix(name);
if (suffix == nullptr)
return false;
return UpdateSongFile(directory, name, suffix, info) ||

@@ -395,10 +395,12 @@ TryContainerDecoder(DecoderBridge &bridge, Path path_fs,
static bool
decoder_run_file(DecoderBridge &bridge, const char *uri_utf8, Path path_fs)
{
const auto suffix = uri_get_suffix(uri_utf8);
if (suffix.empty())
const char *_suffix = PathTraitsUTF8::GetFilenameSuffix(uri_utf8);
if (_suffix == nullptr)
return false;
const std::string_view suffix{_suffix};
InputStreamPtr input_stream;
try {

@@ -24,6 +24,7 @@
#include "StandardDirectory.hxx"
#include "FileSystem.hxx"
#include "XDG.hxx"
#include "util/StringView.hxx"
#include "config.h"
#include <array>
@@ -228,13 +229,12 @@ GetUserConfigDir() noexcept
return GetStandardDir(CSIDL_LOCAL_APPDATA);
#elif defined(USE_XDG)
// Check for $XDG_CONFIG_HOME
auto config_home = getenv("XDG_CONFIG_HOME");
if (IsValidPathString(config_home) && IsValidDir(config_home))
if (const auto config_home = getenv("XDG_CONFIG_HOME");
IsValidPathString(config_home) && IsValidDir(config_home))
return AllocatedPath::FromFS(config_home);
// Check for $HOME/.config
auto home = GetHomeDir();
if (!home.IsNull()) {
if (const auto home = GetHomeDir(); !home.IsNull()) {
auto fallback = home / Path::FromFS(".config");
if (IsValidDir(fallback.c_str()))
return fallback;
@@ -265,17 +265,15 @@ GetUserCacheDir() noexcept
{
#ifdef USE_XDG
// Check for $XDG_CACHE_HOME
auto cache_home = getenv("XDG_CACHE_HOME");
if (IsValidPathString(cache_home) && IsValidDir(cache_home))
if (const auto cache_home = getenv("XDG_CACHE_HOME");
IsValidPathString(cache_home) && IsValidDir(cache_home))
return AllocatedPath::FromFS(cache_home);
// Check for $HOME/.cache
auto home = GetHomeDir();
if (!home.IsNull()) {
auto fallback = home / Path::FromFS(".cache");
if (IsValidDir(fallback.c_str()))
if (const auto home = GetHomeDir(); !home.IsNull())
if (auto fallback = home / Path::FromFS(".cache");
IsValidDir(fallback.c_str()))
return fallback;
}
return nullptr;
#elif defined(ANDROID)
@@ -285,6 +283,38 @@ GetUserCacheDir() noexcept
#endif
}
AllocatedPath
GetUserRuntimeDir() noexcept
{
#ifdef USE_XDG
return SafePathFromFS(getenv("XDG_RUNTIME_DIR"));
#else
return nullptr;
#endif
}
AllocatedPath
GetAppRuntimeDir() noexcept
{
#ifdef __linux__
/* systemd specific; see systemd.exec(5) */
if (const char *runtime_directory = getenv("RUNTIME_DIRECTORY"))
if (auto dir = StringView{runtime_directory}.Split(':').first;
!dir.empty())
return AllocatedPath::FromFS(dir);
#endif
#ifdef USE_XDG
if (const auto user_dir = GetUserRuntimeDir(); !user_dir.IsNull()) {
auto dir = user_dir / Path::FromFS("mpd");
mkdir(dir.c_str(), 0700);
return dir;
}
#endif
return nullptr;
}
#ifdef _WIN32
AllocatedPath
@@ -317,11 +347,11 @@ AllocatedPath
GetHomeDir() noexcept
{
#ifndef ANDROID
auto home = getenv("HOME");
if (IsValidPathString(home) && IsValidDir(home))
if (const auto home = getenv("HOME");
IsValidPathString(home) && IsValidDir(home))
return AllocatedPath::FromFS(home);
PasswdEntry pw;
if (pw.ReadByUid(getuid()))
if (PasswdEntry pw; pw.ReadByUid(getuid()))
return SafePathFromFS(pw->pw_dir);
#endif
return nullptr;
@@ -334,8 +364,8 @@ GetHomeDir(const char *user_name) noexcept
(void)user_name;
#else
assert(user_name != nullptr);
PasswdEntry pw;
if (pw.ReadByName(user_name))
if (PasswdEntry pw; pw.ReadByName(user_name))
return SafePathFromFS(pw->pw_dir);
#endif
return nullptr;

@@ -25,27 +25,44 @@
/**
* Obtains configuration directory for the current user.
*/
[[gnu::const]]
AllocatedPath
GetUserConfigDir() noexcept;
/**
* Obtains music directory for the current user.
*/
[[gnu::const]]
AllocatedPath
GetUserMusicDir() noexcept;
/**
* Obtains cache directory for the current user.
*/
[[gnu::pure]]
[[gnu::const]]
AllocatedPath
GetUserCacheDir() noexcept;
/**
* Obtains the runtime directory for the current user.
*/
[[gnu::const]]
AllocatedPath
GetUserRuntimeDir() noexcept;
/**
* Obtains the runtime directory for this application.
*/
[[gnu::const]]
AllocatedPath
GetAppRuntimeDir() noexcept;
#ifdef _WIN32
/**
* Obtains system configuration directory.
*/
[[gnu::const]]
AllocatedPath
GetSystemConfigDir() noexcept;
@@ -54,6 +71,7 @@ GetSystemConfigDir() noexcept;
* Application base directory is a directory that contains 'bin' folder
* for current executable.
*/
[[gnu::const]]
AllocatedPath
GetAppBaseDir() noexcept;
@@ -62,12 +80,14 @@ GetAppBaseDir() noexcept;
/**
* Obtains home directory for the current user.
*/
[[gnu::const]]
AllocatedPath
GetHomeDir() noexcept;
/**
* Obtains home directory for the specified user.
*/
[[gnu::pure]]
AllocatedPath
GetHomeDir(const char *user_name) noexcept;

@@ -30,12 +30,7 @@ namespace Alsa {
AllowedFormat::AllowedFormat(StringView s)
{
#ifdef ENABLE_DSD
const StringView dop_tail("=dop");
if (s.EndsWith(dop_tail)) {
dop = true;
s.size -= dop_tail.size;
} else
dop = false;
dop = s.RemoveSuffix("=dop");
#endif
char buffer[64];
@@ -54,7 +49,7 @@ AllowedFormat::AllowedFormat(StringView s)
}
std::forward_list<AllowedFormat>
AllowedFormat::ParseList(StringView s)
AllowedFormat::ParseList(std::string_view s)
{
std::forward_list<AllowedFormat> list;
auto tail = list.before_begin();

@@ -52,7 +52,7 @@ struct AllowedFormat {
*
* Throws std::runtime_error on error.
*/
static std::forward_list<AllowedFormat> ParseList(StringView s);
static std::forward_list<AllowedFormat> ParseList(std::string_view s);
};
std::string

@@ -33,23 +33,27 @@
static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10);
AudioOutputControl::AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
AudioOutputClient &_client) noexcept
AudioOutputClient &_client,
const ConfigBlock &block)
:output(std::move(_output)),
name(output->GetName()),
client(_client),
thread(BIND_THIS_METHOD(Task))
thread(BIND_THIS_METHOD(Task)),
tags(block.GetBlockValue("tags", true)),
always_on(block.GetBlockValue("always_on", false)),
enabled(block.GetBlockValue("enabled", true))
{
}
AudioOutputControl::AudioOutputControl(AudioOutputControl *_output,
AudioOutputControl::AudioOutputControl(AudioOutputControl &&src,
AudioOutputClient &_client) noexcept
:output(_output->Steal()),
:output(src.Steal()),
name(output->GetName()),
client(_client),
thread(BIND_THIS_METHOD(Task))
thread(BIND_THIS_METHOD(Task)),
tags(src.tags),
always_on(src.always_on)
{
tags =_output->tags;
always_on=_output->always_on;
}
AudioOutputControl::~AudioOutputControl() noexcept
@@ -57,14 +61,6 @@ AudioOutputControl::~AudioOutputControl() noexcept
StopThread();
}
void
AudioOutputControl::Configure(const ConfigBlock &block)
{
tags = block.GetBlockValue("tags", true);
always_on = block.GetBlockValue("always_on", false);
enabled = block.GetBlockValue("enabled", true);
}
std::unique_ptr<FilteredAudioOutput>
AudioOutputControl::Steal() noexcept
{

@@ -151,13 +151,13 @@ class AudioOutputControl {
* default is true, but it may be configured to false to
* suppress sending tags to the output.
*/
bool tags;
const bool tags;
/**
* Shall this output always play something (i.e. silence),
* even when playback is stopped?
*/
bool always_on;
const bool always_on;
/**
* Has the user enabled this device?
@@ -249,10 +249,18 @@ public:
*/
mutable Mutex mutex;
/**
* Throws on error.
*/
AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
AudioOutputClient &_client) noexcept;
AudioOutputClient &_client,
const ConfigBlock &block);
AudioOutputControl(AudioOutputControl *_outputControl,
/**
* Move the contents of an existing instance, and convert that
* existing instance to a "dummy" output.
*/
AudioOutputControl(AudioOutputControl &&src,
AudioOutputClient &_client) noexcept;
~AudioOutputControl() noexcept;
@@ -260,11 +268,6 @@ public:
AudioOutputControl(const AudioOutputControl &) = delete;
AudioOutputControl &operator=(const AudioOutputControl &) = delete;
/**
* Throws on error.
*/
void Configure(const ConfigBlock &block);
[[gnu::pure]]
const char *GetName() const noexcept;

@@ -181,7 +181,8 @@ public:
void Disable() noexcept;
/**
* Invoke OutputPlugin::close().
* Close everything: the output (via CloseOutput()) and the
* software mixer (via CloseSoftwareMixer()).
*
* Caller must not lock the mutex.
*/
@@ -200,7 +201,7 @@ public:
void OpenOutputAndConvert(AudioFormat audio_format);
/**
* Close the output plugin.
* Invoke AudioOutput::Close(), but nothing else.
*
* Mutex must not be locked.
*/

@@ -80,9 +80,8 @@ LoadOutputControl(EventLoop &event_loop, EventLoop &rt_event_loop,
replay_gain_config,
mixer_listener,
block, defaults, filter_factory);
auto control = std::make_unique<AudioOutputControl>(std::move(output), client);
control->Configure(block);
return control;
return std::make_unique<AudioOutputControl>(std::move(output),
client, block);
}
void
@@ -130,24 +129,12 @@ MultipleOutputs::FindByName(const char *name) noexcept
}
void
MultipleOutputs::Add(std::unique_ptr<FilteredAudioOutput> output,
bool enable) noexcept
MultipleOutputs::AddMoveFrom(AudioOutputControl &&src,
bool enable) noexcept
{
// TODO: this operation needs to be protected with a mutex
outputs.push_back(std::make_unique<AudioOutputControl>(std::move(output),
client));
outputs.back()->LockSetEnabled(enable);
client.ApplyEnabled();
}
void
MultipleOutputs::AddCopy(AudioOutputControl *outputControl,
bool enable) noexcept
{
// TODO: this operation needs to be protected with a mutex
outputs.push_back(std::make_unique<AudioOutputControl>(outputControl, client));
outputs.push_back(std::make_unique<AudioOutputControl>(std::move(src),
client));
outputs.back()->LockSetEnabled(enable);

@@ -125,11 +125,8 @@ public:
return FindByName(name) != nullptr;
}
void Add(std::unique_ptr<FilteredAudioOutput> output,
bool enable) noexcept;
void AddCopy(AudioOutputControl *outputControl,
bool enable) noexcept;
void AddMoveFrom(AudioOutputControl &&src,
bool enable) noexcept;
void SetReplayGainMode(ReplayGainMode mode) noexcept;

@@ -74,13 +74,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
try {
output->ConfigureConvertFilter();
} catch (...) {
open = false;
{
const ScopeUnlock unlock(mutex);
output->CloseOutput(false);
}
InternalCloseOutput(false);
throw;
}
}

@@ -84,6 +84,23 @@ class AlsaOutput final
* @see http://dsd-guide.com/dop-open-standard
*/
bool dop_setting;
/**
* Are we currently playing DSD? (Native DSD or DoP)
*/
bool use_dsd;
/**
* Play some silence before closing the output in DSD mode?
* This is a workaround for some DACs which emit noise when
* stopping DSD playback.
*/
const bool stop_dsd_silence;
/**
* Are we currently draining with #stop_dsd_silence?
*/
bool in_stop_dsd_silence;
#endif
/** libasound's buffer_time setting (in microseconds) */
@@ -93,7 +110,7 @@ class AlsaOutput final
const unsigned period_time;
/** the mode flags passed to snd_pcm_open */
int mode = 0;
const int mode;
std::forward_list<Alsa::AllowedFormat> allowed_formats;
@@ -344,39 +361,9 @@ private:
/**
* @return false if no data was moved
*/
bool CopyRingToPeriodBuffer() noexcept {
if (period_buffer.IsFull())
return false;
bool CopyRingToPeriodBuffer() noexcept;
size_t nbytes = ring_buffer->pop(period_buffer.GetTail(),
period_buffer.GetSpaceBytes());
if (nbytes == 0)
return false;
period_buffer.AppendBytes(nbytes);
const std::lock_guard<Mutex> lock(mutex);
/* notify the OutputThread that there is now
room in ring_buffer */
cond.notify_one();
return true;
}
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept {
assert(period_buffer.IsFull());
assert(period_buffer.GetFrames(out_frame_size) > 0);
auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(),
period_buffer.GetFrames(out_frame_size));
if (frames_written > 0) {
written = true;
period_buffer.ConsumeFrames(frames_written,
out_frame_size);
}
return frames_written;
}
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept;
void LockCaughtError() noexcept {
period_buffer.Clear();
@@ -385,6 +372,9 @@ private:
error = std::current_exception();
active = false;
waiting = false;
#ifdef ENABLE_DSD
in_stop_dsd_silence = false;
#endif
cond.notify_one();
}
@@ -408,21 +398,11 @@ private:
static constexpr Domain alsa_output_domain("alsa_output");
AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
:AudioOutput(FLAG_ENABLE_DISABLE),
MultiSocketMonitor(_loop),
defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)),
silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)),
device(block.GetBlockValue("device", "")),
#ifdef ENABLE_DSD
dop_setting(block.GetBlockValue("dop", false) ||
/* legacy name from MPD 0.18 and older: */
block.GetBlockValue("dsd_usb", false)),
#endif
buffer_time(block.GetPositiveValue("buffer_time",
MPD_ALSA_BUFFER_TIME_US)),
period_time(block.GetPositiveValue("period_time", 0U))
static int
GetAlsaOpenMode(const ConfigBlock &block)
{
int mode = 0;
#ifdef SND_PCM_NO_AUTO_RESAMPLE
if (!block.GetBlockValue("auto_resample", true))
mode |= SND_PCM_NO_AUTO_RESAMPLE;
@@ -438,6 +418,26 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
mode |= SND_PCM_NO_AUTO_FORMAT;
#endif
return mode;
}
AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
:AudioOutput(FLAG_ENABLE_DISABLE),
MultiSocketMonitor(_loop),
defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)),
silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)),
device(block.GetBlockValue("device", "")),
#ifdef ENABLE_DSD
dop_setting(block.GetBlockValue("dop", false) ||
/* legacy name from MPD 0.18 and older: */
block.GetBlockValue("dsd_usb", false)),
stop_dsd_silence(block.GetBlockValue("stop_dsd_silence", false)),
#endif
buffer_time(block.GetPositiveValue("buffer_time",
MPD_ALSA_BUFFER_TIME_US)),
period_time(block.GetPositiveValue("period_time", 0U)),
mode(GetAlsaOpenMode(block))
{
const char *allowed_formats_string =
block.GetBlockValue("allowed_formats", nullptr);
if (allowed_formats_string != nullptr)
@@ -462,7 +462,7 @@ AlsaOutput::SetAttribute(std::string &&name, std::string &&value)
{
if (name == "allowed_formats") {
const std::lock_guard<Mutex> lock(attributes_mutex);
allowed_formats = Alsa::AllowedFormat::ParseList({value.data(), value.length()});
allowed_formats = Alsa::AllowedFormat::ParseList(value);
#ifdef ENABLE_DSD
} else if (name == "dop") {
const std::lock_guard<Mutex> lock(attributes_mutex);
@@ -732,6 +732,9 @@ AlsaOutput::Open(AudioFormat &audio_format)
snd_pcm_nonblock(pcm, 1);
#ifdef ENABLE_DSD
use_dsd = audio_format.format == SampleFormat::DSD;
in_stop_dsd_silence = false;
if (params.dsd_mode == PcmExport::DsdMode::DOP)
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
#endif
@@ -824,9 +827,59 @@ AlsaOutput::Recover(int err) noexcept
return err;
}
bool
AlsaOutput::CopyRingToPeriodBuffer() noexcept
{
if (period_buffer.IsFull())
return false;
size_t nbytes = ring_buffer->pop(period_buffer.GetTail(),
period_buffer.GetSpaceBytes());
if (nbytes == 0)
return false;
period_buffer.AppendBytes(nbytes);
const std::lock_guard<Mutex> lock(mutex);
/* notify the OutputThread that there is now
room in ring_buffer */
cond.notify_one();
return true;
}
snd_pcm_sframes_t
AlsaOutput::WriteFromPeriodBuffer() noexcept
{
assert(period_buffer.IsFull());
assert(period_buffer.GetFrames(out_frame_size) > 0);
auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(),
period_buffer.GetFrames(out_frame_size));
if (frames_written > 0) {
written = true;
period_buffer.ConsumeFrames(frames_written,
out_frame_size);
}
return frames_written;
}
inline bool
AlsaOutput::DrainInternal()
{
#ifdef ENABLE_DSD
if (in_stop_dsd_silence) {
/* "stop_dsd_silence" is in progress: clear internal
buffers and instead, fill the period buffer with
silence */
in_stop_dsd_silence = false;
ring_buffer->reset();
period_buffer.Clear();
period_buffer.FillWithSilence(silence, out_frame_size);
}
#endif
/* drain ring_buffer */
CopyRingToPeriodBuffer();
@@ -957,6 +1010,17 @@ AlsaOutput::Cancel() noexcept
return;
}
#ifdef ENABLE_DSD
if (stop_dsd_silence && use_dsd) {
/* play some DSD silence instead of snd_pcm_drop() */
std::unique_lock<Mutex> lock(mutex);
in_stop_dsd_silence = true;
drain = true;
cond.wait(lock, [this]{ return !drain || !active; });
return;
}
#endif
BlockingCall(GetEventLoop(), [this](){
CancelInternal();
});

@@ -21,7 +21,6 @@
#include "../OutputAPI.hxx"
#include "thread/SafeSingleton.hxx"
#include "system/Error.hxx"
#include "util/DivideString.hxx"
#include "util/IterableSplitString.hxx"
#include "util/RuntimeError.hxx"
#include "util/Domain.hxx"

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

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

@@ -25,7 +25,6 @@ if zeroconf_option == 'bonjour'
endif
bonjour_deps = [
log_dep,
]
if not is_darwin
@@ -41,6 +40,7 @@ if zeroconf_option == 'bonjour'
include_directories: inc,
dependencies: [
event_dep,
log_dep,
],
)

@@ -7,6 +7,10 @@ After=network.target sound.target
Type=notify
ExecStart=@prefix@/bin/mpd --no-daemon
# Create /run/mpd (if MPD is launched without the socket unit and is
# configured to bind listener sockets there).
RuntimeDirectory=mpd
# Enable this setting to ask systemd to watch over MPD, see
# systemd.service(5). This is disabled by default because it causes
# periodic wakeups which are unnecessary if MPD is not playing.

@@ -7,6 +7,10 @@ After=network.target sound.target
Type=notify
ExecStart=@prefix@/bin/mpd --no-daemon
# Create /run/user/$UID/mpd (if MPD is launched without the socket
# unit and is configured to bind listener sockets there).
RuntimeDirectory=mpd
# Enable this setting to ask systemd to watch over MPD, see
# systemd.service(5). This is disabled by default because it causes
# periodic wakeups which are unnecessary if MPD is not playing.