Compare commits

...

88 Commits

Author SHA1 Message Date
Max Kellermann
f6c65cba58 release v0.22.3 2020-11-06 16:12:54 +01:00
Max Kellermann
f849b07766 storage/curl: fix nullptr dereference
Pass a std::string to PathTraitsUTF8::Relative(), implicitly casting
it to std::string_view.  This selects the right overload which returns
std::string_view instead of `const char *`; the latter could return
`nullptr` which would cause the implicit conversion of the return
value to std::string_view to crash.

Regression caused by commits ead208987d and a98d627c0b.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/995
2020-11-06 15:35:47 +01:00
Max Kellermann
2da3cff1e8 filter/LoadChain: use the AutoConvertFilter
This adds support for input samples other than 16 bit to the FFmpeg
filter plugin.
2020-11-04 20:15:19 +01:00
Max Kellermann
0c965d0573 filter/AutoConvert: move the Filter class to TwoFilters.cxx 2020-11-04 20:15:19 +01:00
Max Kellermann
77c14692c9 filter/AutoConvert: eliminate AutoConvertFilter if possible
If no conversion is necessary, return the child Filter as-is.  This
allows removing all nullptr checks from AutoConvertFilter.
2020-11-04 20:15:19 +01:00
Max Kellermann
226eb26300 filter/ffmpeg: interleave the output AVFrame
If the FFmpeg filter outputs planar data, interleave it, just like the
FFmpeg decoder plugin does.
2020-11-04 20:15:19 +01:00
Max Kellermann
2d606fa989 decoder/ffmpeg: move code to lib/ffmpeg/Interleave.cxx
To be reused by the FFmpeg filter plugin.
2020-11-04 20:15:19 +01:00
Max Kellermann
7a0342c8bb decoder/ffmpeg: use AVFrame fields instead of AVCodecContext fields 2020-11-04 20:06:45 +01:00
Max Kellermann
42c9d765cf lib/ffmpeg/Buffer: add missing include 2020-11-04 20:06:41 +01:00
Max Kellermann
a8a80ee689 lib/ffmpeg/Buffer: disallow copying 2020-11-04 19:51:21 +01:00
Max Kellermann
f9bdb4b0b8 lib/ffmpeg/Buffer: add noexcept 2020-11-04 19:50:38 +01:00
Max Kellermann
9332527872 lib/ffmpeg/{Buffer,Time}: remove obsolete "#undef SampleFormat"
This compatibility macro has been removed from FFmpeg long ago.
2020-11-04 19:50:33 +01:00
Max Kellermann
84f772357e filter/convert: convert_filter_new() returns std::unique_ptr 2020-11-04 16:47:11 +01:00
Max Kellermann
f2b9785a67 filter/chain: pass std::string_view to filter_chain_append() 2020-11-04 16:37:53 +01:00
Max Kellermann
eeaec99c59 filter/LoadChain: use IterableSplitString() 2020-11-04 16:36:11 +01:00
Max Kellermann
b0002e3b73 filter/chain: copy the child name
filter_chain_parse() passes a temporary string pointer which results
in a use-after-free in the PreparedChainFilter::Child::Open() error
message.
2020-11-04 16:34:38 +01:00
Max Kellermann
27c589da97 filter/chain: remove unused field ChainFilter::Child::name 2020-11-04 16:26:50 +01:00
Max Kellermann
6484af472b increment version number to 0.22.3 2020-11-04 16:14:40 +01:00
Max Kellermann
92a218b7a9 playlist/registry: add option "as_directory"
This allows users to disable the "CUE files as directories" feature
without having to disable the CUE playlist plugin completely.  This
feature has been annoying some users.
2020-11-04 16:13:12 +01:00
Max Kellermann
d69a1f98af doc/plugins.rst: more markup 2020-11-04 16:11:26 +01:00
Max Kellermann
23a6f62ea3 doc/user.rst: fix typo 2020-11-04 15:59:40 +01:00
Max Kellermann
e0d3ca71b3 meson.build: switch to C11
It's been 9 years already, and there's no point in insisting on the 21
year old C standard.  MPD doesn't have a lot of C code left, but why
not compile it with the latest language revision.
2020-11-04 14:38:58 +01:00
Max Kellermann
4f40b9f7cf meson.build: disable ld.so lazy binding and enable relro
Since MPD is a long-running daemon, it doesn't make sense to use
dynamic binding.  That allows the relocations to be read-only
("relro"), which a hardening feature.
2020-11-04 13:43:21 +01:00
Max Kellermann
bb009daf66 playlist/registry: simplify ExtractMimeTypeMainPart() 2020-11-04 13:34:04 +01:00
Max Kellermann
dc432f3ffa release v0.22.2 2020-10-28 17:25:33 +01:00
Max Kellermann
37710195ca meson_options.txt: disable the "smbclient" plugin by default
The bug https://bugzilla.samba.org/show_bug.cgi?id=11413 makes MPD
crash after at most a minute of using the plugin.  Since this bug is
five years old already and it doesn't look like it will ever be fixed,
all libsmbclient code in MPD is scheduled for removal.  For now, the
plugin is disabled by default so people are less likely to hit the
crash bug.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/991
2020-10-28 17:21:27 +01:00
Max Kellermann
7b9295ff99 lib/yajl/Handle: strip newlines from error messages
Closes https://github.com/MusicPlayerDaemon/MPD/issues/981
2020-10-28 16:06:52 +01:00
Max Kellermann
5f61d440eb lib/yajl/Handle: un-inline the throwing code
Reduces header dependencies.
2020-10-28 16:02:14 +01:00
Max Kellermann
6bc73a9ebe util/FormatString: update API documentation 2020-10-28 15:48:42 +01:00
Max Kellermann
1195eb266e protocol/Ack: remove unused variable ack_domain 2020-10-28 15:47:05 +01:00
Max Kellermann
3562a3e51e Main: save the state_file on shutdown
This got lost in commit 5d597a3646 (v0.21.19), but it was never
noticed because the state_file_interval was way too short due to
commit 3413d1bf23, fixed recently by commit 27cc7b352d
2020-10-28 15:29:47 +01:00
Max Kellermann
bbfa6fe632 db/simple: purge songs for unavailable decoder plugins on update 2020-10-28 14:36:20 +01:00
Max Kellermann
bf97d13d0b fs/Traits: add GetPathSuffix() 2020-10-28 14:29:46 +01:00
Max Kellermann
b5673b6333 db/simple/Directory: add pure attribute 2020-10-28 14:24:58 +01:00
Max Kellermann
ee802867df db/update/Walk: add code comments 2020-10-28 14:23:39 +01:00
Max Kellermann
ecaa51e322 db/simple: purge special directories for unavailable plugins on update 2020-10-27 19:14:31 +01:00
Max Kellermann
0779333064 db/update/Walk: adjust lamba indent 2020-10-27 19:14:31 +01:00
Max Kellermann
6f1a4a73b7 fs/Traits: add GetFilenameSuffix() 2020-10-27 19:14:31 +01:00
Max Kellermann
945ed2610a increment version number to 0.22.2 2020-10-27 18:34:39 +01:00
Max Kellermann
d7fcaf33b9 release v0.22.1 2020-10-17 13:56:12 +02:00
Max Kellermann
6a65b4c305 lib/nfs/patches: disable the snprintf->sprintf_s alias
snprintf() is available on mingw, and the libnfs kludge broke the
build with mingw, because sprintf_s() was now both an inline function
and a "dllimport" function (because the macro renamed the inline
function snprintf() to sprintf_s() in mingw's stdio.h).
2020-10-17 13:56:02 +02:00
Max Kellermann
a163beee69 python/build/libs.py: update CURL to 7.73.0 2020-10-16 18:53:47 +02:00
Max Kellermann
31268ad7cd decoder/opus: fix track/album ReplayGain fallback
Fixes regression by commit 23d5a2b862 -
that commit always pretended that any Opus file has both track and
album gain, and thus disabled the fallback to the other if one is not
set.

This patch changes the logic to only submit ReplayGain if at least one
value is set, and apply the offset only to that value.  If none is
available, then the new check in HandleAudio() will submit only the
output gain.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/977
2020-10-16 18:45:18 +02:00
Max Kellermann
a0d43dd87f decoder/opus: submit output_gain even if there is no OpusTags packet 2020-10-16 18:41:16 +02:00
Max Kellermann
1db533c8cf decoder/opus: move formula to EbuR128ToReplayGain() 2020-10-16 18:39:29 +02:00
Max Kellermann
78ee663660 decoder/opus: move comment to output_gain field 2020-10-16 18:30:51 +02:00
Max Kellermann
c32a809d38 decoder/opus: convert field output_gain to float 2020-10-16 18:28:57 +02:00
Max Kellermann
1406144210 lib/dbus/Watch: add missing include for assert() 2020-10-15 16:05:05 +02:00
Max Kellermann
bb6ab67175 output/osx: fix several -Wdouble-promotion warnings 2020-10-15 15:01:28 +02:00
Max Kellermann
ed3d8222d6 net/SocketAddress: include cleanup 2020-10-15 15:01:19 +02:00
Max Kellermann
41c0bbab13 event/SocketMonitor: don't filter out ERROR/HANGUP
By bit-wise ANDing the reported flags with GetScheduledFlags(),
ERROR/HANGUP always get cleared.  This means the MPD event loop could
never report those conditions.
2020-10-08 21:16:18 +02:00
Max Kellermann
eeb96eb367 event/TimerEvent: add type alias for std::chrono::steady_clock::duration 2020-10-08 20:48:50 +02:00
Max Kellermann
ce93e58944 event/TimerEvent: use using instead of typedef 2020-10-08 20:46:18 +02:00
Max Kellermann
263b0ffdbb event/TimerEvent: use auto_unlink hook 2020-10-08 20:46:15 +02:00
Max Kellermann
22bea5c97e event/Loop: reorder includes
This just happened to break the Windows build because of the
`GetObject` macro in `windows.h`, so I added a kludge to
PollResultGeneric.hxx.
2020-10-08 20:43:21 +02:00
Max Kellermann
75802ebcc6 StateFileConfig, ...: drop obsolete out-of-class definition 2020-10-08 20:38:11 +02:00
Max Kellermann
27cc7b352d config/Data: cast to std::chrono::steady_clock::duration properly
Oh no, 3413d1bf23 was broken!  Instead of passing a number as
"seconds" to the duration constructor, it just abused the duration
constructor as cast operator, which caused custom state_file_interval
settings to be extremely short.
2020-10-08 20:30:33 +02:00
Max Kellermann
d64729065e config/Parser: use std::size_t 2020-10-08 20:26:39 +02:00
Max Kellermann
ab318200db config/{Data,Block}: use With() in GetUnsigned(), GetPositive() 2020-10-08 20:21:09 +02:00
Max Kellermann
947856ca8e event/Loop: forward-declare class TimerEvent 2020-10-08 17:24:32 +02:00
Max Kellermann
cd9ff9d9b0 event/TimerEvent: use base_hook instead of member_hook 2020-10-08 17:00:09 +02:00
Max Kellermann
4cd0f661d6 event/Loop: use using instead of typedef 2020-10-08 16:59:21 +02:00
Max Kellermann
bf270a5663 doc/user.rst: document io_uring 2020-10-06 19:14:44 +02:00
Max Kellermann
6e893f40e3 doc/user.rst: common startup problems 2020-10-06 19:14:18 +02:00
Max Kellermann
7690905503 doc/user.rst: remove "Question" prefix from "Common Problems" 2020-10-06 19:03:03 +02:00
Max Kellermann
6f822a6f19 doc/user.rst: remove numbers from section headers 2020-10-06 18:59:01 +02:00
Max Kellermann
ca0179b2a9 event/Loop: set the uring_initialized flag
Don't attempt to initialize the io_uring subsystem more than once.
2020-10-06 18:58:54 +02:00
Max Kellermann
6682cf749f playlist/cue/parser: use lambda to fix ambiguous overload
On Windows, there is an IsWhitespaceOrNull() overload with TCHAR, and
the compiler doesn't know which one to pass to std::find_if().
2020-10-05 21:15:10 +02:00
Max Kellermann
492607ecbe playlist/cue/parser: use StringView internally
Don't copy the input StringView.
2020-10-05 21:04:49 +02:00
Max Kellermann
e0c75da266 playlist/cue/parser: pass StringView to Feed() 2020-10-05 20:33:58 +02:00
Max Kellermann
34bb53a29f playlist/cue/parser: add noexcept 2020-10-05 20:33:50 +02:00
Max Kellermann
cb4fdac469 playlist/cue/parser: fix nullptr dereference
Closes https://github.com/MusicPlayerDaemon/MPD/issues/974
2020-10-05 20:26:42 +02:00
Max Kellermann
ac46a84391 playlist/cue/parser: fix off-by-one buffer overflow
cue_next_word() can return a pointer one past the end of the string if
the word is followed by the terminating null byte.
2020-10-05 20:26:02 +02:00
Max Kellermann
dffd5831f8 test/fuzzer: a simple fuzzer using libFuzzer
This commit adds some basic infrastructure for fuzzers, and adds a
fuzzer for the CUE sheet parser.
2020-10-05 20:25:26 +02:00
Max Kellermann
8358b34efa meson_options.txt: move "test" to a new section 2020-10-05 19:44:52 +02:00
Max Kellermann
4484d7a5c2 output/jack: implement Interrupt() 2020-10-02 11:00:04 +02:00
Max Kellermann
b80a135cf3 output/pulse: implement Interrupt() 2020-10-02 10:52:25 +02:00
Max Kellermann
4ad525d939 output/alsa: implement Interrupt()
This allows canceling the blocking method LockWaitWriteAvailable(),
and thus allows breaking free of misbehaving ALSA drivers, avoiding a
MPD lockup.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/966
2020-10-02 10:35:18 +02:00
Max Kellermann
4cb5e69811 output/Interface: add virtual method Interrupt()
This allows interrupting the output thread (for some plugins which
implement this method).  This way, operations can be canceled
properly, instead of waiting for some external entity.
2020-10-02 10:20:39 +02:00
Max Kellermann
b0596291a8 output/Thread: simplify the main loop switch
Move the InternalPlay() call and the wake_cond.wait() call into the
`case Command::NONE` and revert all `continue` statements to a simple
`break`.
2020-10-02 10:10:53 +02:00
Max Kellermann
8f0a1a5d82 output/Interface: add noexcept 2020-10-01 20:44:14 +02:00
Max Kellermann
c0775d328c output/Filtered: move try/catch from IteratePause() to caller 2020-10-01 20:44:11 +02:00
Max Kellermann
4ca2c33181 doc/meson.build: check both html_manual and manpages
Closes https://github.com/MusicPlayerDaemon/MPD/issues/960
2020-09-30 12:11:20 +02:00
Max Kellermann
362f391b76 Merge remote-tracking branches 'neheb/defa', 'neheb/auto' and 'neheb/clocale' into master 2020-09-30 11:48:05 +02:00
Rosen Penev
980e32f69c remove clocale test
clocale is part of C++11.

In practical terms, gcc's libstdc++ comes with its own locale defines
when the libc does not have them.

Also reworked to be dependent on !ANDROID.

Signed-off-by: Rosen Penev <rosenp@gmail.com>
2020-09-29 14:51:17 -07:00
Rosen Penev
dd639e18b8 clang-tidy: remove pointless std::move
Found with performance-move-const-arg

Signed-off-by: Rosen Penev <rosenp@gmail.com>
2020-09-26 21:34:25 -07:00
Rosen Penev
c883f178b8 clang-tidy: use auto
Found with modernize-use-auto

Signed-off-by: Rosen Penev <rosenp@gmail.com>
2020-09-26 21:33:35 -07:00
Max Kellermann
65d257675f increment version number to 0.22.1 2020-09-23 16:15:44 +02:00
98 changed files with 1388 additions and 576 deletions

32
NEWS
View File

@@ -1,3 +1,35 @@
ver 0.22.3 (2020/11/06)
* playlist
- add option "as_directory", making CUE file expansion optional
* storage
- curl: fix crash bug
* filter
- fix garbage after "Audio format not supported by filter" message
- ffmpeg: support planar output
- ffmpeg: support sample formats other than 16 bit
ver 0.22.2 (2020/10/28)
* database
- simple: purge songs and virtual directories for unavailable plugins
on update
* input
- qobuz/tidal: fix protocol errors due to newlines in error messages
- smbclient: disable by default due to libsmbclient crash bug
* playlist
- soundcloud: fix protocol errors due to newlines in error messages
* state_file: save on shutdown
ver 0.22.1 (2020/10/17)
* decoder
- opus: apply the OpusHead output gain even if there is no EBU R128 tag
- opus: fix track/album ReplayGain fallback
* output
- alsa: don't deadlock when the ALSA driver is buggy
- jack, pulse: reduce the delay when stopping or pausing playback
* playlist
- cue: fix two crash bugs
* state_file: fix the state_file_interval setting
ver 0.22 (2020/09/23) ver 0.22 (2020/09/23)
* protocol * protocol
- "findadd"/"searchadd"/"searchaddpl" support the "sort" and - "findadd"/"searchadd"/"searchaddpl" support the "sort" and

View File

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd" package="org.musicpd"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="50" android:versionCode="51"
android:versionName="0.22"> android:versionName="0.22.1">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28"/> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28"/>

View File

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

View File

@@ -1,4 +1,4 @@
if not get_option('html_manual') if not get_option('html_manual') and not get_option('manpages')
subdir_done() subdir_done()
endif endif

View File

@@ -71,6 +71,11 @@ Load music files from a SMB/CIFS server. It is used when
:code:`music_directory` contains a ``smb://`` URI, for example :code:`music_directory` contains a ``smb://`` URI, for example
:samp:`smb://myfileserver/Music`. :samp:`smb://myfileserver/Music`.
Note that :file:`libsmbclient` has a serious bug which causes MPD to
crash, and therefore this plugin is disabled by default and should not
be used until the bug is fixed:
https://bugzilla.samba.org/show_bug.cgi?id=11413
nfs nfs
--- ---
@@ -1224,23 +1229,25 @@ Playlist plugins
asx asx
--- ---
Reads .asx playlist files. Reads :file:`.asx` playlist files.
.. _cue_playlist:
cue cue
--- ---
Reads .cue files. Reads :file:`.cue` files.
embcue embcue
------ ------
Reads CUE sheets from the "CUESHEET" tag of song files. Reads CUE sheets from the ``CUESHEET`` tag of song files.
m3u m3u
--- ---
Reads .m3u playlist files. Reads :file:`.m3u` playlist files.
extm3u extm3u
------ ------
Reads extended .m3u playlist files. Reads extended :file:`.m3u` playlist files.
flac flac
---- ----
@@ -1248,11 +1255,11 @@ Reads the cuesheet metablock from a FLAC file.
pls pls
--- ---
Reads .pls playlist files. Reads :file:`.pls` playlist files.
rss rss
--- ---
Reads music links from .rss files. Reads music links from :file:`.rss` files.
soundcloud soundcloud
---------- ----------

View File

@@ -86,7 +86,7 @@ For example, the following installs a fairly complete list of build dependencies
libpulse-dev libshout3-dev \ libpulse-dev libshout3-dev \
libsndio-dev \ libsndio-dev \
libmpdclient-dev \ libmpdclient-dev \
libnfs-dev libsmbclient-dev \ libnfs-dev \
libupnp-dev \ libupnp-dev \
libavahi-client-dev \ libavahi-client-dev \
libsqlite3-dev \ libsqlite3-dev \
@@ -413,7 +413,7 @@ The following table lists the audio_output options valid for all plugins:
* - **format samplerate:bits:channels** * - **format samplerate:bits:channels**
- Always open the audio output with the specified audio format, regardless of the format of the input file. This is optional for most plugins. - Always open the audio output with the specified audio format, regardless of the format of the input file. This is optional for most plugins.
See :ref:`audio_output_format` for a detailed description of the value. See :ref:`audio_output_format` for a detailed description of the value.
* - **enabed yes|no** * - **enabled yes|no**
- Specifies whether this audio output is enabled when :program:`MPD` is started. By default, all audio outputs are enabled. This is just the default setting when there is no state file; with a state file, the previous state is restored. - Specifies whether this audio output is enabled when :program:`MPD` is started. By default, all audio outputs are enabled. This is just the default setting when there is no state file; with a state file, the previous state is restored.
* - **tags yes|no** * - **tags yes|no**
- If set to no, then :program:`MPD` will not send tags to this output. This is only useful for output plugins that can receive tags, for example the httpd output plugin. - If set to no, then :program:`MPD` will not send tags to this output. This is only useful for output plugins that can receive tags, for example the httpd output plugin.
@@ -500,6 +500,11 @@ The following table lists the playlist_plugin options valid for all plugins:
- The name of the plugin - The name of the plugin
* - **enabled yes|no** * - **enabled yes|no**
- Allows you to disable a playlist plugin without recompiling. By default, all plugins are enabled. - Allows you to disable a playlist plugin without recompiling. By default, all plugins are enabled.
* - **as_directory yes|no**
- With this option, a playlist file of this type is parsed during
database update and converted to a virtual directory, allowing
MPD clients to access individual entries. By default, this is
only enabled for the :ref:`cue plugin <cue_playlist>`.
More information can be found in the :ref:`playlist_plugins` More information can be found in the :ref:`playlist_plugins`
reference. reference.
@@ -770,6 +775,8 @@ The :code:`music_directory` setting tells :program:`MPD` to read files from the
The database setting tells :program:`MPD` to pass all database queries on to the :program:`MPD` instance running on the file server (using the proxy plugin). The database setting tells :program:`MPD` to pass all database queries on to the :program:`MPD` instance running on the file server (using the proxy plugin).
.. _realtime:
Real-Time Scheduling Real-Time Scheduling
-------------------- --------------------
@@ -1096,34 +1103,66 @@ The :program:`MPD` project runs a `forum <https://forum.musicpd.org/>`_ and an I
Common Problems Common Problems
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
1. Database Startup
""""""""""" """""""
Question: I can't see my music in the MPD database! Error "could not get realtime scheduling"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See :ref:`realtime`. You can safely ignore this, but you won't
benefit from real-time scheduling. This only makes a difference if
your computer runs programs other than MPD.
Error "Failed to initialize io_uring"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Linux specific: the io_uring subsystem could not be initialized. This
is not a critical error - MPD will fall back to "classic" blocking
disk I/O. You can safely ignore this error, but you won't benefit
from io_uring's advantages.
* "Cannot allocate memory" usually means that your memlock limit
(``ulimit -l`` in bash or ``LimitMEMLOCK`` in systemd) is too low.
64 MB is a reasonable value for this limit.
* Your Linux kernel might be too old and does not support io_uring.
Error "bind to '0.0.0.0:6600' failed (continuing anyway, because binding to '[::]:6600' succeeded)"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This happens on Linux when :file:`/proc/sys/net/ipv6/bindv6only` is
disabled. MPD first binds to IPv6, and this automatically binds to
IPv4 as well; after that, MPD binds to IPv4, but that fails. You can
safely ignore this, because MPD works on both IPv4 and IPv6.
Database
""""""""
I can't see my music in the MPD database
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Check your :code:`music_directory` setting. * Check your :code:`music_directory` setting.
* Does the MPD user have read permission on all music files, and read+execute permission on all music directories (and all of their parent directories)? * Does the MPD user have read permission on all music files, and read+execute permission on all music directories (and all of their parent directories)?
* Did you update the database? (mpc update) * Did you update the database? (mpc update)
* Did you enable all relevant decoder plugins at compile time? :command:`mpd --version` will tell you. * Did you enable all relevant decoder plugins at compile time? :command:`mpd --version` will tell you.
Question: MPD doesn't read ID3 tags! MPD doesn't read ID3 tags!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
* You probably compiled :program:`MPD` without libid3tag. :command:`mpd --version` will tell you. * You probably compiled :program:`MPD` without libid3tag. :command:`mpd --version` will tell you.
2. Playback Playback
""""""""""" """"""""
Question: I can't hear music on my client! I can't hear music on my client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* That problem usually follows a misunderstanding of the nature of :program:`MPD`. :program:`MPD` is a remote-controlled music player, not a music distribution system. Usually, the speakers are connected to the box where :program:`MPD` runs, and the :program:`MPD` client only sends control commands, but the client does not actually play your music. * That problem usually follows a misunderstanding of the nature of :program:`MPD`. :program:`MPD` is a remote-controlled music player, not a music distribution system. Usually, the speakers are connected to the box where :program:`MPD` runs, and the :program:`MPD` client only sends control commands, but the client does not actually play your music.
:program:`MPD` has output plugins which allow hearing music on a remote host (such as httpd), but that is not :program:`MPD`'s primary design goal. :program:`MPD` has output plugins which allow hearing music on a remote host (such as httpd), but that is not :program:`MPD`'s primary design goal.
Question: "Device or resource busy" Error "Device or resource busy"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* This ALSA error means that another program uses your sound hardware exclusively. You can stop that program to allow :program:`MPD` to use it. * This ALSA error means that another program uses your sound hardware exclusively. You can stop that program to allow :program:`MPD` to use it.

View File

@@ -1,11 +1,11 @@
project( project(
'mpd', 'mpd',
['c', 'cpp'], ['c', 'cpp'],
version: '0.22', version: '0.22.3',
meson_version: '>= 0.49.0', meson_version: '>= 0.49.0',
default_options: [ default_options: [
'c_std=c99', 'c_std=c11',
'build.c_std=c99', 'build.c_std=c11',
'cpp_std=c++17', 'cpp_std=c++17',
'build.cpp_std=c++17', 'build.cpp_std=c++17',
'warning_level=3', 'warning_level=3',
@@ -96,6 +96,11 @@ test_cflags = test_common_flags + [
] ]
test_ldflags = [ test_ldflags = [
# make relocations read-only (hardening)
'-Wl,-z,relro',
# no lazy binding, please - not worth it for a daemon
'-Wl,-z,now',
] ]
if get_option('buildtype') != 'debug' if get_option('buildtype') != 'debug'
@@ -112,6 +117,13 @@ if get_option('buildtype') != 'debug'
] ]
endif endif
if get_option('fuzzer')
fuzzer_flags = ['-fsanitize=fuzzer,address,undefined']
add_global_arguments(fuzzer_flags, language: 'cpp')
add_global_arguments(fuzzer_flags, language: 'c')
add_global_link_arguments(fuzzer_flags, language: 'cpp')
endif
add_global_arguments(common_cxxflags + compiler.get_supported_arguments(test_cxxflags), language: 'cpp') add_global_arguments(common_cxxflags + compiler.get_supported_arguments(test_cxxflags), language: 'cpp')
add_global_arguments(common_cflags + c_compiler.get_supported_arguments(test_cflags), language: 'c') add_global_arguments(common_cflags + c_compiler.get_supported_arguments(test_cflags), language: 'c')
add_global_link_arguments(compiler.get_supported_link_arguments(test_ldflags), language: 'cpp') add_global_link_arguments(compiler.get_supported_link_arguments(test_ldflags), language: 'cpp')
@@ -147,8 +159,6 @@ add_global_arguments(common_cppflags, language: 'cpp')
enable_daemon = not is_windows and not is_android and get_option('daemon') enable_daemon = not is_windows and not is_android and get_option('daemon')
conf.set('ENABLE_DAEMON', enable_daemon) conf.set('ENABLE_DAEMON', enable_daemon)
conf.set('HAVE_CLOCALE', compiler.has_header('clocale'))
conf.set('HAVE_GETPWNAM_R', compiler.has_function('getpwnam_r')) conf.set('HAVE_GETPWNAM_R', compiler.has_function('getpwnam_r'))
conf.set('HAVE_GETPWUID_R', compiler.has_function('getpwuid_r')) conf.set('HAVE_GETPWUID_R', compiler.has_function('getpwuid_r'))
conf.set('HAVE_INITGROUPS', compiler.has_function('initgroups')) conf.set('HAVE_INITGROUPS', compiler.has_function('initgroups'))
@@ -219,7 +229,6 @@ log_dep = declare_dependency(
sources = [ sources = [
version_cxx, version_cxx,
'src/Main.cxx', 'src/Main.cxx',
'src/protocol/Ack.cxx',
'src/protocol/ArgParser.cxx', 'src/protocol/ArgParser.cxx',
'src/protocol/Result.cxx', 'src/protocol/Result.cxx',
'src/command/CommandError.cxx', 'src/command/CommandError.cxx',
@@ -504,6 +513,7 @@ mpd = build_target(
chromaprint_dep, chromaprint_dep,
], ],
link_args: link_args, link_args: link_args,
build_by_default: not get_option('fuzzer'),
install: not is_android and not is_haiku, install: not is_android and not is_haiku,
) )
@@ -544,3 +554,7 @@ subdir('doc')
if get_option('test') if get_option('test')
subdir('test') subdir('test')
endif endif
if get_option('fuzzer')
subdir('test/fuzzer')
endif

View File

@@ -2,8 +2,6 @@ option('documentation', type: 'feature', description: 'Build documentation')
option('html_manual', type: 'boolean', value: true, description: 'Build the HTML manual') option('html_manual', type: 'boolean', value: true, description: 'Build the HTML manual')
option('manpages', type: 'boolean', value: true, description: 'Build manual pages') option('manpages', type: 'boolean', value: true, description: 'Build manual pages')
option('test', type: 'boolean', value: false, description: 'Build the unit tests and debug programs')
option('syslog', type: 'feature', description: 'syslog support') option('syslog', type: 'feature', description: 'syslog support')
option('inotify', type: 'boolean', value: true, description: 'inotify support (for automatic database update)') option('inotify', type: 'boolean', value: true, description: 'inotify support (for automatic database update)')
option('io_uring', type: 'feature', description: 'Linux io_uring support using liburing') option('io_uring', type: 'feature', description: 'Linux io_uring support using liburing')
@@ -14,6 +12,13 @@ option('systemd', type: 'feature', description: 'systemd support')
option('systemd_system_unit_dir', type: 'string', description: 'systemd system service directory') option('systemd_system_unit_dir', type: 'string', description: 'systemd system service directory')
option('systemd_user_unit_dir', type: 'string', description: 'systemd user service directory') option('systemd_user_unit_dir', type: 'string', description: 'systemd user service directory')
#
# Options for developers
#
option('test', type: 'boolean', value: false, description: 'Build the unit tests and debug programs')
option('fuzzer', type: 'boolean', value: false, description: 'Build fuzzers (requires libFuzzer)')
# #
# Android # Android
# #
@@ -87,7 +92,11 @@ option('cdio_paranoia', type: 'feature', description: 'libcdio_paranoia input pl
option('curl', type: 'feature', description: 'HTTP client using CURL') option('curl', type: 'feature', description: 'HTTP client using CURL')
option('mms', type: 'feature', description: 'MMS protocol support using libmms') option('mms', type: 'feature', description: 'MMS protocol support using libmms')
option('nfs', type: 'feature', description: 'NFS protocol support using libnfs') option('nfs', type: 'feature', description: 'NFS protocol support using libnfs')
option('smbclient', type: 'feature', description: 'SMB support using libsmbclient')
# The "smbclient" plugin is disabled by default because libsmbclient
# has a serious bug which crashes MPD very quickly:
# https://bugzilla.samba.org/show_bug.cgi?id=11413
option('smbclient', type: 'feature', value: 'disabled', description: 'SMB support using libsmbclient')
# #
# Commercial services # Commercial services

View File

@@ -377,8 +377,8 @@ ffmpeg = FfmpegProject(
) )
curl = AutotoolsProject( curl = AutotoolsProject(
'http://curl.haxx.se/download/curl-7.72.0.tar.xz', 'http://curl.haxx.se/download/curl-7.73.0.tar.xz',
'0ded0808c4d85f2ee0db86980ae610cc9d165e9ca9da466196cc73c346513713', '7c4c7ca4ea88abe00fea4740dcf81075c031b1d0bb23aff2d5efde20a3c2408a',
'lib/libcurl.a', 'lib/libcurl.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
@@ -429,6 +429,7 @@ libnfs = AutotoolsProject(
'--disable-utils', '--disable-examples', '--disable-utils', '--disable-examples',
], ],
base='libnfs-libnfs-4.0.0', base='libnfs-libnfs-4.0.0',
patches='src/lib/nfs/patches',
autoreconf=True, autoreconf=True,
) )

View File

@@ -111,7 +111,7 @@
#include <climits> #include <climits>
#ifdef HAVE_CLOCALE #ifndef ANDROID
#include <clocale> #include <clocale>
#endif #endif
@@ -358,11 +358,9 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
#endif #endif
#ifndef ANDROID #ifndef ANDROID
#ifdef HAVE_CLOCALE
/* initialize locale */ /* initialize locale */
std::setlocale(LC_CTYPE,""); std::setlocale(LC_CTYPE,"");
std::setlocale(LC_COLLATE, ""); std::setlocale(LC_COLLATE, "");
#endif
#endif #endif
const ScopeIcuInit icu_init; const ScopeIcuInit icu_init;
@@ -535,6 +533,9 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
/* cleanup */ /* cleanup */
if (instance.state_file)
instance.state_file->Write();
instance.BeginShutdownUpdate(); instance.BeginShutdownUpdate();
ZeroconfDeinit(); ZeroconfDeinit();

View File

@@ -23,6 +23,7 @@
#include "db/plugins/simple/Directory.hxx" #include "db/plugins/simple/Directory.hxx"
#include "storage/StorageInterface.hxx" #include "storage/StorageInterface.hxx"
#include "storage/FileInfo.hxx" #include "storage/FileInfo.hxx"
#include "decoder/DecoderList.hxx"
#include "fs/AllocatedPath.hxx" #include "fs/AllocatedPath.hxx"
#include "fs/FileInfo.hxx" #include "fs/FileInfo.hxx"
#include "tag/Builder.hxx" #include "tag/Builder.hxx"
@@ -40,6 +41,14 @@
#ifdef ENABLE_DATABASE #ifdef ENABLE_DATABASE
bool
Song::IsPluginAvailable() const noexcept
{
const char *suffix = GetFilenameSuffix();
return suffix != nullptr &&
decoder_plugins_supports_suffix(suffix);
}
SongPtr SongPtr
Song::LoadFile(Storage &storage, const char *path_utf8, Directory &parent) Song::LoadFile(Storage &storage, const char *path_utf8, Directory &parent)
{ {

View File

@@ -24,8 +24,6 @@
#include "fs/StandardDirectory.hxx" #include "fs/StandardDirectory.hxx"
#endif #endif
constexpr std::chrono::steady_clock::duration StateFileConfig::DEFAULT_INTERVAL;
StateFileConfig::StateFileConfig(const ConfigData &config) StateFileConfig::StateFileConfig(const ConfigData &config)
:path(config.GetPath(ConfigOption::STATE_FILE)), :path(config.GetPath(ConfigOption::STATE_FILE)),
interval(config.GetUnsigned(ConfigOption::STATE_FILE_INTERVAL, interval(config.GetUnsigned(ConfigOption::STATE_FILE_INTERVAL,

View File

@@ -21,17 +21,16 @@
#define MPD_STATE_FILE_CONFIG_HXX #define MPD_STATE_FILE_CONFIG_HXX
#include "fs/AllocatedPath.hxx" #include "fs/AllocatedPath.hxx"
#include "event/Chrono.hxx"
#include <chrono>
struct ConfigData; struct ConfigData;
struct StateFileConfig { struct StateFileConfig {
static constexpr std::chrono::steady_clock::duration DEFAULT_INTERVAL = std::chrono::minutes(2); static constexpr Event::Duration DEFAULT_INTERVAL = std::chrono::minutes(2);
AllocatedPath path; AllocatedPath path;
std::chrono::steady_clock::duration interval; Event::Duration interval;
bool restore_paused; bool restore_paused;

View File

@@ -24,7 +24,7 @@
#define CLIENT_MAX_COMMAND_LIST_DEFAULT (2048*1024) #define CLIENT_MAX_COMMAND_LIST_DEFAULT (2048*1024)
#define CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT (8192*1024) #define CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT (8192*1024)
std::chrono::steady_clock::duration client_timeout; Event::Duration client_timeout;
size_t client_max_command_list_size; size_t client_max_command_list_size;
size_t client_max_output_buffer_size; size_t client_max_output_buffer_size;

View File

@@ -20,11 +20,11 @@
#ifndef MPD_CLIENT_CONFIG_HXX #ifndef MPD_CLIENT_CONFIG_HXX
#define MPD_CLIENT_CONFIG_HXX #define MPD_CLIENT_CONFIG_HXX
#include <chrono> #include "event/Chrono.hxx"
struct ConfigData; struct ConfigData;
extern std::chrono::steady_clock::duration client_timeout; extern Event::Duration client_timeout;
extern size_t client_max_command_list_size; extern size_t client_max_command_list_size;
extern size_t client_max_output_buffer_size; extern size_t client_max_output_buffer_size;

View File

@@ -34,7 +34,7 @@ Client::SetExpired() noexcept
} }
FullyBufferedSocket::Close(); FullyBufferedSocket::Close();
timeout_event.Schedule(std::chrono::steady_clock::duration::zero()); timeout_event.Schedule(Event::Duration::zero());
} }
void void

View File

@@ -47,28 +47,13 @@ BlockParam::GetIntValue() const
unsigned unsigned
BlockParam::GetUnsignedValue() const BlockParam::GetUnsignedValue() const
{ {
const char *const s = value.c_str(); return With(ParseUnsigned);
char *endptr;
unsigned long value2 = strtoul(s, &endptr, 0);
if (endptr == s || *endptr != 0)
throw FormatRuntimeError("Not a valid number in line %i", line);
return (unsigned)value2;
} }
unsigned unsigned
BlockParam::GetPositiveValue() const BlockParam::GetPositiveValue() const
{ {
const char *const s = value.c_str(); return With(ParsePositive);
char *endptr;
unsigned long value2 = strtoul(s, &endptr, 0);
if (endptr == s || *endptr != 0)
throw FormatRuntimeError("Not a valid number in line %i", line);
if (value2 <= 0)
throw FormatRuntimeError("Number in line %i must be positive", line);
return (unsigned)value2;
} }
bool bool

View File

@@ -84,43 +84,53 @@ ConfigData::GetPath(ConfigOption option) const
unsigned unsigned
ConfigData::GetUnsigned(ConfigOption option, unsigned default_value) const ConfigData::GetUnsigned(ConfigOption option, unsigned default_value) const
{ {
const auto *param = GetParam(option); return With(option, [default_value](const char *s){
long value; return s != nullptr
char *endptr; ? ParseUnsigned(s)
: default_value;
if (param == nullptr) });
return default_value;
const char *const s = param->value.c_str();
value = strtol(s, &endptr, 0);
if (endptr == s || *endptr != 0 || value < 0)
throw FormatRuntimeError("Not a valid non-negative number in line %i",
param->line);
return (unsigned)value;
} }
unsigned unsigned
ConfigData::GetPositive(ConfigOption option, unsigned default_value) const ConfigData::GetPositive(ConfigOption option, unsigned default_value) const
{ {
const auto *param = GetParam(option); return With(option, [default_value](const char *s){
long value; return s != nullptr
char *endptr; ? ParsePositive(s)
: default_value;
});
}
if (param == nullptr) std::chrono::steady_clock::duration
return default_value; ConfigData::GetUnsigned(ConfigOption option,
std::chrono::steady_clock::duration default_value) const
{
return With(option, [default_value](const char *s){
if (s == nullptr)
return default_value;
const char *const s = param->value.c_str(); auto value = ParseDuration(s);
value = strtol(s, &endptr, 0); if (value < std::chrono::steady_clock::duration{})
if (endptr == s || *endptr != 0) throw std::runtime_error("Value must not be negative");
throw FormatRuntimeError("Not a valid number in line %i",
param->line);
if (value <= 0) return value;
throw FormatRuntimeError("Not a positive number in line %i", });
param->line); }
return (unsigned)value; std::chrono::steady_clock::duration
ConfigData::GetPositive(ConfigOption option,
std::chrono::steady_clock::duration default_value) const
{
return With(option, [default_value](const char *s){
if (s == nullptr)
return default_value;
auto value = ParseDuration(s);
if (value <= std::chrono::steady_clock::duration{})
throw std::runtime_error("Value must be positive");
return value;
});
} }
bool bool

View File

@@ -78,22 +78,14 @@ struct ConfigData {
std::chrono::steady_clock::duration std::chrono::steady_clock::duration
GetUnsigned(ConfigOption option, GetUnsigned(ConfigOption option,
std::chrono::steady_clock::duration default_value) const { std::chrono::steady_clock::duration default_value) const;
// TODO: allow unit suffixes
auto u = GetUnsigned(option, default_value.count());
return std::chrono::steady_clock::duration(u);
}
unsigned GetPositive(ConfigOption option, unsigned GetPositive(ConfigOption option,
unsigned default_value) const; unsigned default_value) const;
std::chrono::steady_clock::duration std::chrono::steady_clock::duration
GetPositive(ConfigOption option, GetPositive(ConfigOption option,
std::chrono::steady_clock::duration default_value) const { std::chrono::steady_clock::duration default_value) const;
// TODO: allow unit suffixes
auto u = GetPositive(option, default_value.count());
return std::chrono::steady_clock::duration(u);
}
bool GetBool(ConfigOption option, bool default_value) const; bool GetBool(ConfigOption option, bool default_value) const;

View File

@@ -22,6 +22,8 @@
#include "util/StringStrip.hxx" #include "util/StringStrip.hxx"
#include "util/StringUtil.hxx" #include "util/StringUtil.hxx"
#include <cstdlib>
bool bool
ParseBool(const char *value) ParseBool(const char *value)
{ {
@@ -37,28 +39,59 @@ ParseBool(const char *value)
throw FormatRuntimeError(R"(Not a valid boolean ("yes" or "no"): "%s")", value); throw FormatRuntimeError(R"(Not a valid boolean ("yes" or "no"): "%s")", value);
} }
template<size_t OPERAND> long
static size_t ParseLong(const char *s)
Multiply(size_t value)
{ {
static constexpr size_t MAX_VALUE = SIZE_MAX / OPERAND; char *endptr;
long value = strtol(s, &endptr, 10);
if (endptr == s || *endptr != 0)
throw std::runtime_error("Failed to parse number");
return value;
}
unsigned
ParseUnsigned(const char *s)
{
auto value = ParseLong(s);
if (value < 0)
throw std::runtime_error("Value must not be negative");
return (unsigned)value;
}
unsigned
ParsePositive(const char *s)
{
auto value = ParseLong(s);
if (value <= 0)
throw std::runtime_error("Value must be positive");
return (unsigned)value;
}
template<std::size_t OPERAND>
static std::size_t
Multiply(std::size_t value)
{
static constexpr std::size_t MAX_VALUE = SIZE_MAX / OPERAND;
if (value > MAX_VALUE) if (value > MAX_VALUE)
throw std::runtime_error("Value too large"); throw std::runtime_error("Value too large");
return value * OPERAND; return value * OPERAND;
} }
size_t std::size_t
ParseSize(const char *s, size_t default_factor) ParseSize(const char *s, std::size_t default_factor)
{ {
char *endptr; char *endptr;
size_t value = strtoul(s, &endptr, 10); std::size_t value = strtoul(s, &endptr, 10);
if (endptr == s) if (endptr == s)
throw std::runtime_error("Failed to parse integer"); throw std::runtime_error("Failed to parse integer");
static constexpr size_t KILO = 1024; static constexpr std::size_t KILO = 1024;
static constexpr size_t MEGA = 1024 * KILO; static constexpr std::size_t MEGA = 1024 * KILO;
static constexpr size_t GIGA = 1024 * MEGA; static constexpr std::size_t GIGA = 1024 * MEGA;
s = StripLeft(endptr); s = StripLeft(endptr);
@@ -102,3 +135,11 @@ ParseSize(const char *s, size_t default_factor)
return value; return value;
} }
std::chrono::steady_clock::duration
ParseDuration(const char *s)
{
// TODO: allow unit suffixes
const std::chrono::seconds seconds(ParseLong(s));
return std::chrono::duration_cast<std::chrono::steady_clock::duration>(seconds);
}

View File

@@ -20,6 +20,7 @@
#ifndef MPD_CONFIG_PARSER_HXX #ifndef MPD_CONFIG_PARSER_HXX
#define MPD_CONFIG_PARSER_HXX #define MPD_CONFIG_PARSER_HXX
#include <chrono>
#include <cstddef> #include <cstddef>
/** /**
@@ -28,12 +29,36 @@
bool bool
ParseBool(const char *value); ParseBool(const char *value);
/**
* Throws on error.
*/
long
ParseLong(const char *s);
/**
* Throws on error.
*/
unsigned
ParseUnsigned(const char *s);
/**
* Throws on error.
*/
unsigned
ParsePositive(const char *s);
/** /**
* Parse a string as a byte size. * Parse a string as a byte size.
* *
* Throws on error. * Throws on error.
*/ */
size_t std::size_t
ParseSize(const char *s, size_t default_factor=1); ParseSize(const char *s, std::size_t default_factor=1);
/**
* Throws on error.
*/
std::chrono::steady_clock::duration
ParseDuration(const char *s);
#endif #endif

View File

@@ -31,6 +31,7 @@ db_glue_sources = [
'update/Remove.cxx', 'update/Remove.cxx',
'update/ExcludeList.cxx', 'update/ExcludeList.cxx',
'update/VirtualDirectory.cxx', 'update/VirtualDirectory.cxx',
'update/SpecialDirectory.cxx',
'DatabaseGlue.cxx', 'DatabaseGlue.cxx',
'Configured.cxx', 'Configured.cxx',
'DatabaseSong.cxx', 'DatabaseSong.cxx',

View File

@@ -132,6 +132,14 @@ public:
return mounted_database != nullptr; return mounted_database != nullptr;
} }
/**
* Checks whether this is a "special" directory
* (e.g. #DEVICE_PLAYLIST) and whether the underlying plugin
* is available.
*/
gcc_pure
bool IsPluginAvailable() const noexcept;
/** /**
* Remove this #Directory object from its parent and free it. This * Remove this #Directory object from its parent and free it. This
* must not be called with the root Directory. * must not be called with the root Directory.

View File

@@ -34,6 +34,14 @@ Song::Song(DetachedSong &&other, Directory &_parent) noexcept
{ {
} }
const char *
Song::GetFilenameSuffix() const noexcept
{
return target.empty()
? PathTraitsUTF8::GetFilenameSuffix(filename.c_str())
: PathTraitsUTF8::GetPathSuffix(target.c_str());
}
std::string std::string
Song::GetURI() const noexcept Song::GetURI() const noexcept
{ {

View File

@@ -108,6 +108,16 @@ struct Song {
Song(DetachedSong &&other, Directory &_parent) noexcept; Song(DetachedSong &&other, Directory &_parent) noexcept;
gcc_pure
const char *GetFilenameSuffix() const noexcept;
/**
* Checks whether the decoder plugin for this song is
* available.
*/
gcc_pure
bool IsPluginAvailable() const noexcept;
/** /**
* allocate a new song structure with a local file name and attempt to * allocate a new song structure with a local file name and attempt to
* load its metadata. If all decoder plugin fail to read its meta * load its metadata. If all decoder plugin fail to read its meta

View File

@@ -29,7 +29,7 @@
* UpdateService::Enqueue(). This increases the probability that * UpdateService::Enqueue(). This increases the probability that
* updates can be bundled. * updates can be bundled.
*/ */
static constexpr std::chrono::steady_clock::duration INOTIFY_UPDATE_DELAY = static constexpr Event::Duration INOTIFY_UPDATE_DELAY =
std::chrono::seconds(5); std::chrono::seconds(5);
void void

View File

@@ -95,7 +95,7 @@ UpdateWalk::UpdatePlaylistFile(Directory &directory,
if (plugin == nullptr) if (plugin == nullptr)
return false; return false;
if (plugin->as_folder) if (GetPlaylistPluginAsFolder(*plugin))
UpdatePlaylistFile(directory, name, info, *plugin); UpdatePlaylistFile(directory, name, info, *plugin);
PlaylistInfo pi(name, info.mtime); PlaylistInfo pi(name, info.mtime);

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2003-2020 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 "db/plugins/simple/Directory.hxx"
#include "archive/ArchiveList.hxx"
#include "decoder/DecoderList.hxx"
#include "playlist/PlaylistRegistry.hxx"
#include "fs/Traits.hxx"
gcc_pure
static bool
HaveArchivePluginForFilename(const char *filename) noexcept
{
#ifdef ENABLE_ARCHIVE
const char *suffix = PathTraitsUTF8::GetFilenameSuffix(filename);
return suffix != nullptr &&
archive_plugin_from_suffix(suffix) != nullptr;
#else
(void)filename;
return false;
#endif
}
gcc_pure
static bool
HaveContainerPluginForFilename(const char *filename) noexcept
{
const char *suffix = PathTraitsUTF8::GetFilenameSuffix(filename);
return suffix != nullptr &&
// TODO: check if this plugin really supports containers
decoder_plugins_supports_suffix(suffix);
}
gcc_pure
static bool
HavePlaylistPluginForFilename(const char *filename) noexcept
{
const char *suffix = PathTraitsUTF8::GetFilenameSuffix(filename);
if (suffix == nullptr)
return false;
const auto plugin = FindPlaylistPluginBySuffix(suffix);
if (plugin == nullptr)
return false;
/* discard the special directory if the user disables the
plugin's "as_directory" setting */
return GetPlaylistPluginAsFolder(*plugin);
}
bool
Directory::IsPluginAvailable() const noexcept
{
switch (device) {
case DEVICE_INARCHIVE:
return HaveArchivePluginForFilename(GetName());
case DEVICE_CONTAINER:
return HaveContainerPluginForFilename(GetName());
case DEVICE_PLAYLIST:
return HavePlaylistPluginForFilename(GetName());
default:
return true;
}
}

View File

@@ -69,46 +69,58 @@ UpdateWalk::RemoveExcludedFromDirectory(Directory &directory,
const ScopeDatabaseLock protect; const ScopeDatabaseLock protect;
directory.ForEachChildSafe([&](Directory &child){ directory.ForEachChildSafe([&](Directory &child){
const auto name_fs = const auto name_fs =
AllocatedPath::FromUTF8(child.GetName()); AllocatedPath::FromUTF8(child.GetName());
if (name_fs.IsNull() || exclude_list.Check(name_fs)) { if (name_fs.IsNull() || exclude_list.Check(name_fs)) {
editor.DeleteDirectory(&child); editor.DeleteDirectory(&child);
modified = true; modified = true;
} }
}); });
directory.ForEachSongSafe([&](Song &song){ directory.ForEachSongSafe([&](Song &song){
assert(&song.parent == &directory); assert(&song.parent == &directory);
const auto name_fs = AllocatedPath::FromUTF8(song.filename); const auto name_fs = AllocatedPath::FromUTF8(song.filename);
if (name_fs.IsNull() || exclude_list.Check(name_fs)) { if (name_fs.IsNull() || exclude_list.Check(name_fs)) {
editor.DeleteSong(directory, &song); editor.DeleteSong(directory, &song);
modified = true; modified = true;
} }
}); });
} }
inline void inline void
UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept
{ {
directory.ForEachChildSafe([&](Directory &child){ directory.ForEachChildSafe([&](Directory &child){
if (child.IsMount() || DirectoryExists(storage, child)) if (child.IsMount())
return; /* mount points are always preserved */
return;
editor.LockDeleteDirectory(&child); if (DirectoryExists(storage, child) &&
child.IsPluginAvailable())
return;
modified = true; /* the directory was deleted (or the plugin which
}); handles this "virtual" directory is unavailable) */
editor.LockDeleteDirectory(&child);
modified = true;
});
directory.ForEachSongSafe([&](Song &song){ directory.ForEachSongSafe([&](Song &song){
if (!directory_child_is_regular(storage, directory, if (!directory_child_is_regular(storage, directory,
song.filename)) { song.filename) ||
editor.LockDeleteSong(directory, &song); !song.IsPluginAvailable()) {
/* the song file was deleted (or the decoder
plugin is unavailable) */
modified = true; editor.LockDeleteSong(directory, &song);
}
}); modified = true;
}
});
for (auto i = directory.playlists.begin(), for (auto i = directory.playlists.begin(),
end = directory.playlists.end(); end = directory.playlists.end();

View File

@@ -46,7 +46,7 @@ size_t
decoder_read_much(DecoderClient *client, InputStream &is, decoder_read_much(DecoderClient *client, InputStream &is,
void *_buffer, size_t size) noexcept void *_buffer, size_t size) noexcept
{ {
uint8_t *buffer = (uint8_t *)_buffer; auto buffer = (uint8_t *)_buffer;
size_t total = 0; size_t total = 0;
@@ -67,7 +67,7 @@ bool
decoder_read_full(DecoderClient *client, InputStream &is, decoder_read_full(DecoderClient *client, InputStream &is,
void *_buffer, size_t size) noexcept void *_buffer, size_t size) noexcept
{ {
auto *buffer = (uint8_t *)_buffer; auto buffer = (uint8_t *)_buffer;
while (size > 0) { while (size > 0) {
size_t nbytes = decoder_read(client, is, buffer, size); size_t nbytes = decoder_read(client, is, buffer, size);

View File

@@ -25,6 +25,7 @@
#include "lib/ffmpeg/Domain.hxx" #include "lib/ffmpeg/Domain.hxx"
#include "lib/ffmpeg/Error.hxx" #include "lib/ffmpeg/Error.hxx"
#include "lib/ffmpeg/Init.hxx" #include "lib/ffmpeg/Init.hxx"
#include "lib/ffmpeg/Interleave.hxx"
#include "lib/ffmpeg/Buffer.hxx" #include "lib/ffmpeg/Buffer.hxx"
#include "lib/ffmpeg/Frame.hxx" #include "lib/ffmpeg/Frame.hxx"
#include "lib/ffmpeg/Format.hxx" #include "lib/ffmpeg/Format.hxx"
@@ -176,48 +177,6 @@ start_time_fallback(const AVStream &stream)
return FfmpegTimestampFallback(stream.start_time, 0); return FfmpegTimestampFallback(stream.start_time, 0);
} }
/**
* Copy PCM data from a non-empty AVFrame to an interleaved buffer.
*
* Throws #std::exception on error.
*/
static ConstBuffer<void>
copy_interleave_frame(const AVCodecContext &codec_context,
const AVFrame &frame,
FfmpegBuffer &global_buffer)
{
assert(frame.nb_samples > 0);
int plane_size;
const int data_size =
av_samples_get_buffer_size(&plane_size,
codec_context.channels,
frame.nb_samples,
codec_context.sample_fmt, 1);
assert(data_size != 0);
if (data_size < 0)
throw MakeFfmpegError(data_size);
void *output_buffer;
if (av_sample_fmt_is_planar(codec_context.sample_fmt) &&
codec_context.channels > 1) {
output_buffer = global_buffer.GetT<uint8_t>(data_size);
if (output_buffer == nullptr)
/* Not enough memory - shouldn't happen */
throw std::bad_alloc();
PcmInterleave(output_buffer,
ConstBuffer<const void *>((const void *const*)frame.extended_data,
codec_context.channels),
frame.nb_samples,
av_get_bytes_per_sample(codec_context.sample_fmt));
} else {
output_buffer = frame.extended_data[0];
}
return { output_buffer, (size_t)data_size };
}
/** /**
* Convert AVPacket::pts to a stream-relative time stamp (still in * Convert AVPacket::pts to a stream-relative time stamp (still in
* AVStream::time_base units). Returns a negative value on error. * AVStream::time_base units). Returns a negative value on error.
@@ -258,7 +217,7 @@ FfmpegSendFrame(DecoderClient &client, InputStream *is,
FfmpegBuffer &buffer) FfmpegBuffer &buffer)
{ {
ConstBuffer<void> output_buffer = ConstBuffer<void> output_buffer =
copy_interleave_frame(codec_context, frame, buffer); Ffmpeg::InterleaveFrame(frame, buffer);
if (skip_bytes > 0) { if (skip_bytes > 0) {
if (skip_bytes >= output_buffer.size) { if (skip_bytes >= output_buffer.size) {

View File

@@ -63,6 +63,17 @@ IsOpusTags(const ogg_packet &packet) noexcept
return packet.bytes >= 8 && memcmp(packet.packet, "OpusTags", 8) == 0; return packet.bytes >= 8 && memcmp(packet.packet, "OpusTags", 8) == 0;
} }
/**
* Convert an EBU R128 value to ReplayGain.
*/
constexpr float
EbuR128ToReplayGain(float ebu_r128) noexcept
{
/* add 5dB to compensate for the different reference levels
between ReplayGain (89dB) and EBU R128 (-23 LUFS) */
return ebu_r128 + 5;
}
bool bool
mpd_opus_init([[maybe_unused]] const ConfigBlock &block) mpd_opus_init([[maybe_unused]] const ConfigBlock &block)
{ {
@@ -76,10 +87,11 @@ class MPDOpusDecoder final : public OggDecoder {
opus_int16 *output_buffer = nullptr; opus_int16 *output_buffer = nullptr;
/** /**
* The output gain from the Opus header. Initialized by * The output gain from the Opus header in dB that should be
* OnOggBeginning(). * applied unconditionally, but is often used specifically for
* ReplayGain. Initialized by OnOggBeginning().
*/ */
signed output_gain; float output_gain;
/** /**
* The pre-skip value from the Opus header. Initialized by * The pre-skip value from the Opus header. Initialized by
@@ -111,6 +123,14 @@ class MPDOpusDecoder final : public OggDecoder {
*/ */
ogg_int64_t granulepos; ogg_int64_t granulepos;
/**
* Was DecoderClient::SubmitReplayGain() called? We need to
* keep track of this, because it will usually be called by
* HandleTags(), but if there is no OpusTags packet, we need
* to submit our #output_gain value from the OpusHead.
*/
bool submitted_replay_gain = false;
public: public:
explicit MPDOpusDecoder(DecoderReader &reader) explicit MPDOpusDecoder(DecoderReader &reader)
:OggDecoder(reader) {} :OggDecoder(reader) {}
@@ -170,10 +190,14 @@ MPDOpusDecoder::OnOggBeginning(const ogg_packet &packet)
throw std::runtime_error("BOS packet must be OpusHead"); throw std::runtime_error("BOS packet must be OpusHead");
unsigned channels; unsigned channels;
if (!ScanOpusHeader(packet.packet, packet.bytes, channels, output_gain, pre_skip) || signed output_gain_i;
if (!ScanOpusHeader(packet.packet, packet.bytes, channels, output_gain_i, pre_skip) ||
!audio_valid_channel_count(channels)) !audio_valid_channel_count(channels))
throw std::runtime_error("Malformed BOS packet"); throw std::runtime_error("Malformed BOS packet");
/* convert Q7.8 fixed-point to float */
output_gain = float(output_gain_i) / 256.0f;
granulepos = 0; granulepos = 0;
skip = pre_skip; skip = pre_skip;
@@ -245,22 +269,22 @@ MPDOpusDecoder::HandleTags(const ogg_packet &packet)
ReplayGainInfo rgi; ReplayGainInfo rgi;
rgi.Clear(); rgi.Clear();
/**
* Output gain is a Q7.8 fixed point number in dB that should be,
* applied unconditionally, but is often used specifically for
* ReplayGain. Add 5dB to compensate for the different
* reference levels between ReplayGain (89dB) and EBU R128 (-23 LUFS).
*/
rgi.track.gain = float(output_gain) / 256.0f + 5;
rgi.album.gain = float(output_gain) / 256.0f + 5;
TagBuilder tag_builder; TagBuilder tag_builder;
AddTagHandler h(tag_builder); AddTagHandler h(tag_builder);
if (!ScanOpusTags(packet.packet, packet.bytes, &rgi, h)) if (!ScanOpusTags(packet.packet, packet.bytes, &rgi, h))
return; return;
client.SubmitReplayGain(&rgi); if (rgi.IsDefined()) {
/* submit all valid EBU R128 values with output_gain
applied */
if (rgi.track.IsDefined())
rgi.track.gain += EbuR128ToReplayGain(output_gain);
if (rgi.album.IsDefined())
rgi.album.gain += EbuR128ToReplayGain(output_gain);
client.SubmitReplayGain(&rgi);
submitted_replay_gain = true;
}
if (!tag_builder.empty()) { if (!tag_builder.empty()) {
Tag tag = tag_builder.Commit(); Tag tag = tag_builder.Commit();
@@ -275,6 +299,18 @@ MPDOpusDecoder::HandleAudio(const ogg_packet &packet)
{ {
assert(opus_decoder != nullptr); assert(opus_decoder != nullptr);
if (!submitted_replay_gain) {
/* if we didn't see an OpusTags packet with EBU R128
values, we still need to apply the output gain
value from the OpusHead packet; submit it as "track
gain" value */
ReplayGainInfo rgi;
rgi.Clear();
rgi.track.gain = EbuR128ToReplayGain(output_gain);
client.SubmitReplayGain(&rgi);
submitted_replay_gain = true;
}
int nframes = opus_decode(opus_decoder, int nframes = opus_decode(opus_decoder,
(const unsigned char*)packet.packet, (const unsigned char*)packet.packet,
packet.bytes, packet.bytes,

View File

@@ -61,7 +61,7 @@ ScanOneOpusTag(StringView name, StringView value,
const char *endptr; const char *endptr;
const auto l = ParseInt64(value, &endptr, 10); const auto l = ParseInt64(value, &endptr, 10);
if (endptr > value.begin() && endptr == value.end()) if (endptr > value.begin() && endptr == value.end())
rgi->track.gain += float(l) / 256.0f; rgi->track.gain = float(l) / 256.0f;
} else if (rgi != nullptr && } else if (rgi != nullptr &&
name.EqualsIgnoreCase("R128_ALBUM_GAIN")) { name.EqualsIgnoreCase("R128_ALBUM_GAIN")) {
/* R128_ALBUM_GAIN is a Q7.8 fixed point number in /* R128_ALBUM_GAIN is a Q7.8 fixed point number in
@@ -70,7 +70,7 @@ ScanOneOpusTag(StringView name, StringView value,
const char *endptr; const char *endptr;
const auto l = ParseInt64(value, &endptr, 10); const auto l = ParseInt64(value, &endptr, 10);
if (endptr > value.begin() && endptr == value.end()) if (endptr > value.begin() && endptr == value.end())
rgi->album.gain += float(l) / 256.0f; rgi->album.gain = float(l) / 256.0f;
} }
handler.OnPair(name, value); handler.OnPair(name, value);

36
src/event/Chrono.hxx Normal file
View File

@@ -0,0 +1,36 @@
/*
* Copyright 2003-2020 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.
*/
#ifndef MPD_EVENT_CHRONO_HXX
#define MPD_EVENT_CHRONO_HXX
#include <chrono>
namespace Event {
/**
* The clock used by class #EventLoop and class #TimerEvent.
*/
using Clock = std::chrono::steady_clock;
using Duration = Clock::duration;
} // namespace Event
#endif /* MAIN_NOTIFY_H */

View File

@@ -18,6 +18,7 @@
*/ */
#include "Loop.hxx" #include "Loop.hxx"
#include "TimerEvent.hxx"
#include "SocketMonitor.hxx" #include "SocketMonitor.hxx"
#include "IdleMonitor.hxx" #include "IdleMonitor.hxx"
#include "DeferEvent.hxx" #include "DeferEvent.hxx"
@@ -29,6 +30,13 @@
#include <stdio.h> #include <stdio.h>
#endif #endif
constexpr bool
EventLoop::TimerCompare::operator()(const TimerEvent &a,
const TimerEvent &b) const noexcept
{
return a.due < b.due;
}
EventLoop::EventLoop(ThreadId _thread) EventLoop::EventLoop(ThreadId _thread)
:SocketMonitor(*this), :SocketMonitor(*this),
/* if this instance is hosted by an EventThread (no ThreadId /* if this instance is hosted by an EventThread (no ThreadId
@@ -55,6 +63,7 @@ Uring::Queue *
EventLoop::GetUring() noexcept EventLoop::GetUring() noexcept
{ {
if (!uring_initialized) { if (!uring_initialized) {
uring_initialized = true;
try { try {
uring = std::make_unique<Uring::Manager>(*this); uring = std::make_unique<Uring::Manager>(*this);
} catch (...) { } catch (...) {
@@ -113,7 +122,7 @@ EventLoop::RemoveIdle(IdleMonitor &i) noexcept
} }
void void
EventLoop::AddTimer(TimerEvent &t, std::chrono::steady_clock::duration d) noexcept EventLoop::AddTimer(TimerEvent &t, Event::Duration d) noexcept
{ {
assert(IsInside()); assert(IsInside());
@@ -122,18 +131,10 @@ EventLoop::AddTimer(TimerEvent &t, std::chrono::steady_clock::duration d) noexce
again = true; again = true;
} }
void inline Event::Duration
EventLoop::CancelTimer(TimerEvent &t) noexcept
{
assert(IsInside());
timers.erase(timers.iterator_to(t));
}
inline std::chrono::steady_clock::duration
EventLoop::HandleTimers() noexcept EventLoop::HandleTimers() noexcept
{ {
std::chrono::steady_clock::duration timeout; Event::Duration timeout;
while (!quit) { while (!quit) {
auto i = timers.begin(); auto i = timers.begin();
@@ -150,7 +151,7 @@ EventLoop::HandleTimers() noexcept
t.Run(); t.Run();
} }
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
/** /**
@@ -159,7 +160,7 @@ EventLoop::HandleTimers() noexcept
* value (= never times out) is translated to the magic value -1. * value (= never times out) is translated to the magic value -1.
*/ */
static constexpr int static constexpr int
ExportTimeoutMS(std::chrono::steady_clock::duration timeout) ExportTimeoutMS(Event::Duration timeout)
{ {
return timeout >= timeout.zero() return timeout >= timeout.zero()
/* round up (+1) to avoid unnecessary wakeups */ /* round up (+1) to avoid unnecessary wakeups */

View File

@@ -20,16 +20,15 @@
#ifndef MPD_EVENT_LOOP_HXX #ifndef MPD_EVENT_LOOP_HXX
#define MPD_EVENT_LOOP_HXX #define MPD_EVENT_LOOP_HXX
#include "thread/Id.hxx" #include "Chrono.hxx"
#include "util/Compiler.h"
#include "PollGroup.hxx" #include "PollGroup.hxx"
#include "thread/Mutex.hxx"
#include "WakeFD.hxx" #include "WakeFD.hxx"
#include "SocketMonitor.hxx" #include "SocketMonitor.hxx"
#include "TimerEvent.hxx"
#include "IdleMonitor.hxx" #include "IdleMonitor.hxx"
#include "DeferEvent.hxx" #include "DeferEvent.hxx"
#include "thread/Id.hxx"
#include "thread/Mutex.hxx"
#include "util/Compiler.h"
#include <boost/intrusive/set.hpp> #include <boost/intrusive/set.hpp>
#include <boost/intrusive/list.hpp> #include <boost/intrusive/list.hpp>
@@ -44,6 +43,8 @@
namespace Uring { class Queue; class Manager; } namespace Uring { class Queue; class Manager; }
#endif #endif
class TimerEvent;
/** /**
* An event loop that polls for events on file/socket descriptors. * An event loop that polls for events on file/socket descriptors.
* *
@@ -59,40 +60,39 @@ class EventLoop final : SocketMonitor
struct TimerCompare { struct TimerCompare {
constexpr bool operator()(const TimerEvent &a, constexpr bool operator()(const TimerEvent &a,
const TimerEvent &b) const { const TimerEvent &b) const noexcept;
return a.due < b.due;
}
}; };
typedef boost::intrusive::multiset<TimerEvent, using TimerSet =
boost::intrusive::member_hook<TimerEvent, boost::intrusive::multiset<TimerEvent,
TimerEvent::TimerSetHook, boost::intrusive::base_hook<boost::intrusive::set_base_hook<boost::intrusive::link_mode<boost::intrusive::auto_unlink>>>,
&TimerEvent::timer_set_hook>,
boost::intrusive::compare<TimerCompare>, boost::intrusive::compare<TimerCompare>,
boost::intrusive::constant_time_size<false>> TimerSet; boost::intrusive::constant_time_size<false>>;
TimerSet timers; TimerSet timers;
typedef boost::intrusive::list<IdleMonitor, using IdleList =
boost::intrusive::list<IdleMonitor,
boost::intrusive::member_hook<IdleMonitor, boost::intrusive::member_hook<IdleMonitor,
IdleMonitor::ListHook, IdleMonitor::ListHook,
&IdleMonitor::list_hook>, &IdleMonitor::list_hook>,
boost::intrusive::constant_time_size<false>> IdleList; boost::intrusive::constant_time_size<false>>;
IdleList idle; IdleList idle;
Mutex mutex; Mutex mutex;
typedef boost::intrusive::list<DeferEvent, using DeferredList =
boost::intrusive::list<DeferEvent,
boost::intrusive::member_hook<DeferEvent, boost::intrusive::member_hook<DeferEvent,
DeferEvent::ListHook, DeferEvent::ListHook,
&DeferEvent::list_hook>, &DeferEvent::list_hook>,
boost::intrusive::constant_time_size<false>> DeferredList; boost::intrusive::constant_time_size<false>>;
DeferredList deferred; DeferredList deferred;
#ifdef HAVE_URING #ifdef HAVE_URING
std::unique_ptr<Uring::Manager> uring; std::unique_ptr<Uring::Manager> uring;
#endif #endif
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); Event::Clock::time_point now = Event::Clock::now();
/** /**
* Is this #EventLoop alive, i.e. can events be scheduled? * Is this #EventLoop alive, i.e. can events be scheduled?
@@ -141,9 +141,9 @@ public:
~EventLoop() noexcept; ~EventLoop() noexcept;
/** /**
* A caching wrapper for std::chrono::steady_clock::now(). * A caching wrapper for Event::Clock::now().
*/ */
std::chrono::steady_clock::time_point GetTime() const { auto GetTime() const {
assert(IsInside()); assert(IsInside());
return now; return now;
@@ -185,9 +185,7 @@ public:
void AddIdle(IdleMonitor &i) noexcept; void AddIdle(IdleMonitor &i) noexcept;
void RemoveIdle(IdleMonitor &i) noexcept; void RemoveIdle(IdleMonitor &i) noexcept;
void AddTimer(TimerEvent &t, void AddTimer(TimerEvent &t, Event::Duration d) noexcept;
std::chrono::steady_clock::duration d) noexcept;
void CancelTimer(TimerEvent &t) noexcept;
/** /**
* Schedule a call to DeferEvent::RunDeferred(). * Schedule a call to DeferEvent::RunDeferred().
@@ -223,7 +221,7 @@ private:
* duration until the next timer expires. Returns a negative * duration until the next timer expires. Returns a negative
* duration if there is no timeout. * duration if there is no timeout.
*/ */
std::chrono::steady_clock::duration HandleTimers() noexcept; Event::Duration HandleTimers() noexcept;
bool OnSocketReady(unsigned flags) noexcept override; bool OnSocketReady(unsigned flags) noexcept override;

View File

@@ -117,7 +117,7 @@ MultiSocketMonitor::Prepare() noexcept
/* if there was at least one file descriptor not /* if there was at least one file descriptor not
supported by epoll, install a very short timeout supported by epoll, install a very short timeout
because we assume it's always ready */ because we assume it's always ready */
constexpr std::chrono::steady_clock::duration ready_timeout = constexpr Event::Duration ready_timeout =
std::chrono::milliseconds(1); std::chrono::milliseconds(1);
if (timeout < timeout.zero() || timeout > ready_timeout) if (timeout < timeout.zero() || timeout > ready_timeout)
timeout = ready_timeout; timeout = ready_timeout;

View File

@@ -226,7 +226,7 @@ protected:
* *
* @return timeout or a negative value for no timeout * @return timeout or a negative value for no timeout
*/ */
virtual std::chrono::steady_clock::duration PrepareSockets() noexcept = 0; virtual Event::Duration PrepareSockets() noexcept = 0;
/** /**
* At least one socket is ready or the timeout has expired. * At least one socket is ready or the timeout has expired.

View File

@@ -23,6 +23,14 @@
#include <cstddef> #include <cstddef>
#include <vector> #include <vector>
#ifdef _WIN32
#include <windows.h>
/* damn you, windows.h! */
#ifdef GetObject
#undef GetObject
#endif
#endif
class PollResultGeneric class PollResultGeneric
{ {
struct Item struct Item

View File

@@ -30,7 +30,7 @@
void void
SocketMonitor::Dispatch(unsigned flags) noexcept SocketMonitor::Dispatch(unsigned flags) noexcept
{ {
flags &= GetScheduledFlags(); flags &= GetScheduledFlags() | IMPLICIT_FLAGS;
if (flags != 0) if (flags != 0)
OnSocketReady(flags); OnSocketReady(flags);

View File

@@ -57,6 +57,12 @@ public:
static constexpr unsigned ERROR = PollGroup::ERROR; static constexpr unsigned ERROR = PollGroup::ERROR;
static constexpr unsigned HANGUP = PollGroup::HANGUP; static constexpr unsigned HANGUP = PollGroup::HANGUP;
/**
* These flags are always reported by epoll_wait() and don't
* need to be registered with epoll_ctl().
*/
static constexpr unsigned IMPLICIT_FLAGS = ERROR|HANGUP;
typedef std::make_signed<size_t>::type ssize_t; typedef std::make_signed<size_t>::type ssize_t;
explicit SocketMonitor(EventLoop &_loop) noexcept explicit SocketMonitor(EventLoop &_loop) noexcept

View File

@@ -21,14 +21,7 @@
#include "Loop.hxx" #include "Loop.hxx"
void void
TimerEvent::Cancel() noexcept TimerEvent::Schedule(Event::Duration d) noexcept
{
if (IsActive())
loop.CancelTimer(*this);
}
void
TimerEvent::Schedule(std::chrono::steady_clock::duration d) noexcept
{ {
Cancel(); Cancel();

View File

@@ -20,12 +20,11 @@
#ifndef MPD_TIMER_EVENT_HXX #ifndef MPD_TIMER_EVENT_HXX
#define MPD_TIMER_EVENT_HXX #define MPD_TIMER_EVENT_HXX
#include "Chrono.hxx"
#include "util/BindMethod.hxx" #include "util/BindMethod.hxx"
#include <boost/intrusive/set_hook.hpp> #include <boost/intrusive/set_hook.hpp>
#include <chrono>
class EventLoop; class EventLoop;
/** /**
@@ -36,42 +35,40 @@ class EventLoop;
* thread that runs the #EventLoop, except where explicitly documented * thread that runs the #EventLoop, except where explicitly documented
* as thread-safe. * as thread-safe.
*/ */
class TimerEvent final { class TimerEvent final
: public boost::intrusive::set_base_hook<boost::intrusive::link_mode<boost::intrusive::auto_unlink>>
{
friend class EventLoop; friend class EventLoop;
typedef boost::intrusive::set_member_hook<> TimerSetHook;
TimerSetHook timer_set_hook;
EventLoop &loop; EventLoop &loop;
typedef BoundMethod<void() noexcept> Callback; using Callback = BoundMethod<void() noexcept>;
const Callback callback; const Callback callback;
/** /**
* When is this timer due? This is only valid if IsActive() * When is this timer due? This is only valid if IsActive()
* returns true. * returns true.
*/ */
std::chrono::steady_clock::time_point due; Event::Clock::time_point due;
public: public:
TimerEvent(EventLoop &_loop, Callback _callback) noexcept TimerEvent(EventLoop &_loop, Callback _callback) noexcept
:loop(_loop), callback(_callback) { :loop(_loop), callback(_callback) {
} }
~TimerEvent() noexcept {
Cancel();
}
auto &GetEventLoop() const noexcept { auto &GetEventLoop() const noexcept {
return loop; return loop;
} }
bool IsActive() const noexcept { bool IsActive() const noexcept {
return timer_set_hook.is_linked(); return is_linked();
} }
void Schedule(std::chrono::steady_clock::duration d) noexcept; void Schedule(Event::Duration d) noexcept;
void Cancel() noexcept;
void Cancel() noexcept {
unlink();
}
private: private:
void Run() noexcept { void Run() noexcept {

View File

@@ -20,19 +20,24 @@
#include "LoadChain.hxx" #include "LoadChain.hxx"
#include "Factory.hxx" #include "Factory.hxx"
#include "Prepared.hxx" #include "Prepared.hxx"
#include "plugins/AutoConvertFilterPlugin.hxx"
#include "plugins/ChainFilterPlugin.hxx" #include "plugins/ChainFilterPlugin.hxx"
#include "util/IterableSplitString.hxx"
#include <algorithm>
#include <string> #include <string>
#include <string.h>
static void static void
filter_chain_append_new(PreparedFilter &chain, FilterFactory &factory, filter_chain_append_new(PreparedFilter &chain, FilterFactory &factory,
const char *template_name) std::string_view template_name)
{ {
/* using the AutoConvert filter just in case the specified
filter plugin does not support the exact input format */
filter_chain_append(chain, template_name, filter_chain_append(chain, template_name,
factory.MakeFilter(template_name)); /* unfortunately, MakeFilter() wants a
null-terminated string, so we need to
copy it here */
autoconvert_filter_new(factory.MakeFilter(std::string(template_name).c_str())));
} }
void void
@@ -40,18 +45,10 @@ filter_chain_parse(PreparedFilter &chain,
FilterFactory &factory, FilterFactory &factory,
const char *spec) const char *spec)
{ {
const char *const end = spec + strlen(spec); for (const std::string_view i : IterableSplitString(spec, ',')) {
if (i.empty())
continue;
while (true) { filter_chain_append_new(chain, factory, i);
const char *comma = std::find(spec, end, ',');
if (comma > spec) {
const std::string name(spec, comma);
filter_chain_append_new(chain, factory, name.c_str());
}
if (comma == end)
break;
spec = comma + 1;
} }
} }

View File

@@ -19,6 +19,7 @@
#include "AutoConvertFilterPlugin.hxx" #include "AutoConvertFilterPlugin.hxx"
#include "ConvertFilterPlugin.hxx" #include "ConvertFilterPlugin.hxx"
#include "TwoFilters.hxx"
#include "filter/Filter.hxx" #include "filter/Filter.hxx"
#include "filter/Prepared.hxx" #include "filter/Prepared.hxx"
#include "pcm/AudioFormat.hxx" #include "pcm/AudioFormat.hxx"
@@ -27,35 +28,6 @@
#include <cassert> #include <cassert>
#include <memory> #include <memory>
class AutoConvertFilter final : public Filter {
/**
* The underlying filter.
*/
std::unique_ptr<Filter> filter;
/**
* A convert_filter, just in case conversion is needed. nullptr
* if unused.
*/
std::unique_ptr<Filter> convert;
public:
AutoConvertFilter(std::unique_ptr<Filter> &&_filter,
std::unique_ptr<Filter> &&_convert)
:Filter(_filter->GetOutAudioFormat()),
filter(std::move(_filter)), convert(std::move(_convert)) {}
void Reset() noexcept override {
filter->Reset();
if (convert)
convert->Reset();
}
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
ConstBuffer<void> Flush() override;
};
class PreparedAutoConvertFilter final : public PreparedFilter { class PreparedAutoConvertFilter final : public PreparedFilter {
/** /**
* The underlying filter. * The underlying filter.
@@ -81,37 +53,17 @@ PreparedAutoConvertFilter::Open(AudioFormat &in_audio_format)
/* need to convert? */ /* need to convert? */
std::unique_ptr<Filter> convert; if (in_audio_format == child_audio_format)
if (in_audio_format != child_audio_format) { /* no */
/* yes - create a convert_filter */ return new_filter;
convert.reset(convert_filter_new(in_audio_format, /* yes - create a convert_filter */
child_audio_format));
}
return std::make_unique<AutoConvertFilter>(std::move(new_filter), auto convert = convert_filter_new(in_audio_format,
std::move(convert)); child_audio_format);
}
ConstBuffer<void> return std::make_unique<TwoFilters>(std::move(convert),
AutoConvertFilter::FilterPCM(ConstBuffer<void> src) std::move(new_filter));
{
if (convert != nullptr)
src = convert->FilterPCM(src);
return filter->FilterPCM(src);
}
ConstBuffer<void>
AutoConvertFilter::Flush()
{
if (convert != nullptr) {
auto result = convert->Flush();
if (!result.IsNull())
return filter->FilterPCM(result);
}
return filter->Flush();
} }
std::unique_ptr<PreparedFilter> std::unique_ptr<PreparedFilter>

View File

@@ -28,15 +28,14 @@
#include <cassert> #include <cassert>
#include <list> #include <list>
#include <memory> #include <memory>
#include <string>
class ChainFilter final : public Filter { class ChainFilter final : public Filter {
struct Child { struct Child {
const char *name;
std::unique_ptr<Filter> filter; std::unique_ptr<Filter> filter;
Child(const char *_name, explicit Child(std::unique_ptr<Filter> &&_filter) noexcept
std::unique_ptr<Filter> _filter) noexcept :filter(std::move(_filter)) {}
:name(_name), filter(std::move(_filter)) {}
}; };
std::list<Child> children; std::list<Child> children;
@@ -50,13 +49,12 @@ public:
explicit ChainFilter(AudioFormat _audio_format) explicit ChainFilter(AudioFormat _audio_format)
:Filter(_audio_format) {} :Filter(_audio_format) {}
void Append(const char *name, void Append(std::unique_ptr<Filter> filter) noexcept {
std::unique_ptr<Filter> filter) noexcept {
assert(out_audio_format.IsValid()); assert(out_audio_format.IsValid());
out_audio_format = filter->GetOutAudioFormat(); out_audio_format = filter->GetOutAudioFormat();
assert(out_audio_format.IsValid()); assert(out_audio_format.IsValid());
children.emplace_back(name, std::move(filter)); children.emplace_back(std::move(filter));
RewindFlush(); RewindFlush();
} }
@@ -75,10 +73,10 @@ private:
class PreparedChainFilter final : public PreparedFilter { class PreparedChainFilter final : public PreparedFilter {
struct Child { struct Child {
const char *name; const std::string name;
std::unique_ptr<PreparedFilter> filter; std::unique_ptr<PreparedFilter> filter;
Child(const char *_name, Child(std::string_view _name,
std::unique_ptr<PreparedFilter> _filter) std::unique_ptr<PreparedFilter> _filter)
:name(_name), filter(std::move(_filter)) {} :name(_name), filter(std::move(_filter)) {}
@@ -91,7 +89,7 @@ class PreparedChainFilter final : public PreparedFilter {
std::list<Child> children; std::list<Child> children;
public: public:
void Append(const char *name, void Append(std::string_view name,
std::unique_ptr<PreparedFilter> filter) noexcept { std::unique_ptr<PreparedFilter> filter) noexcept {
children.emplace_back(name, std::move(filter)); children.emplace_back(name, std::move(filter));
} }
@@ -108,7 +106,7 @@ PreparedChainFilter::Child::Open(const AudioFormat &prev_audio_format)
if (conv_audio_format != prev_audio_format) if (conv_audio_format != prev_audio_format)
throw FormatRuntimeError("Audio format not supported by filter '%s': %s", throw FormatRuntimeError("Audio format not supported by filter '%s': %s",
name, name.c_str(),
ToString(prev_audio_format).c_str()); ToString(prev_audio_format).c_str());
return new_filter; return new_filter;
@@ -121,7 +119,7 @@ PreparedChainFilter::Open(AudioFormat &in_audio_format)
for (auto &child : children) { for (auto &child : children) {
AudioFormat audio_format = chain->GetOutAudioFormat(); AudioFormat audio_format = chain->GetOutAudioFormat();
chain->Append(child.name, child.Open(audio_format)); chain->Append(child.Open(audio_format));
} }
return chain; return chain;
@@ -177,7 +175,7 @@ filter_chain_new() noexcept
} }
void void
filter_chain_append(PreparedFilter &_chain, const char *name, filter_chain_append(PreparedFilter &_chain, std::string_view name,
std::unique_ptr<PreparedFilter> filter) noexcept std::unique_ptr<PreparedFilter> filter) noexcept
{ {
auto &chain = (PreparedChainFilter &)_chain; auto &chain = (PreparedChainFilter &)_chain;

View File

@@ -28,6 +28,7 @@
#define MPD_FILTER_CHAIN_HXX #define MPD_FILTER_CHAIN_HXX
#include <memory> #include <memory>
#include <string_view>
class PreparedFilter; class PreparedFilter;
@@ -45,7 +46,7 @@ filter_chain_new() noexcept;
* @param filter the filter to be appended to #chain * @param filter the filter to be appended to #chain
*/ */
void void
filter_chain_append(PreparedFilter &chain, const char *name, filter_chain_append(PreparedFilter &chain, std::string_view name,
std::unique_ptr<PreparedFilter> filter) noexcept; std::unique_ptr<PreparedFilter> filter) noexcept;
#endif #endif

View File

@@ -117,13 +117,13 @@ convert_filter_prepare() noexcept
return std::make_unique<PreparedConvertFilter>(); return std::make_unique<PreparedConvertFilter>();
} }
Filter * std::unique_ptr<Filter>
convert_filter_new(const AudioFormat in_audio_format, convert_filter_new(const AudioFormat in_audio_format,
const AudioFormat out_audio_format) const AudioFormat out_audio_format)
{ {
std::unique_ptr<ConvertFilter> filter(new ConvertFilter(in_audio_format)); std::unique_ptr<ConvertFilter> filter(new ConvertFilter(in_audio_format));
filter->Set(out_audio_format); filter->Set(out_audio_format);
return filter.release(); return filter;
} }
void void

View File

@@ -29,7 +29,7 @@ struct AudioFormat;
std::unique_ptr<PreparedFilter> std::unique_ptr<PreparedFilter>
convert_filter_prepare() noexcept; convert_filter_prepare() noexcept;
Filter * std::unique_ptr<Filter>
convert_filter_new(AudioFormat in_audio_format, convert_filter_new(AudioFormat in_audio_format,
AudioFormat out_audio_format); AudioFormat out_audio_format);

View File

@@ -18,6 +18,7 @@
*/ */
#include "FfmpegFilter.hxx" #include "FfmpegFilter.hxx"
#include "lib/ffmpeg/Interleave.hxx"
#include "lib/ffmpeg/SampleFormat.hxx" #include "lib/ffmpeg/SampleFormat.hxx"
#include "util/ConstBuffer.hxx" #include "util/ConstBuffer.hxx"
@@ -79,5 +80,5 @@ FfmpegFilter::FilterPCM(ConstBuffer<void> src)
/* TODO: call av_buffersink_get_frame() repeatedly? Not /* TODO: call av_buffersink_get_frame() repeatedly? Not
possible with MPD's current Filter API */ possible with MPD's current Filter API */
return {frame.GetData(0), frame->nb_samples * GetOutAudioFormat().GetFrameSize()}; return Ffmpeg::InterleaveFrame(*frame, interleave_buffer);
} }

View File

@@ -21,6 +21,7 @@
#define MPD_FFMPEG_FILTER__HXX #define MPD_FFMPEG_FILTER__HXX
#include "filter/Filter.hxx" #include "filter/Filter.hxx"
#include "lib/ffmpeg/Buffer.hxx"
#include "lib/ffmpeg/Filter.hxx" #include "lib/ffmpeg/Filter.hxx"
#include "lib/ffmpeg/Frame.hxx" #include "lib/ffmpeg/Frame.hxx"
@@ -32,6 +33,8 @@ class FfmpegFilter final : public Filter {
Ffmpeg::FilterContext buffer_src, buffer_sink; Ffmpeg::FilterContext buffer_src, buffer_sink;
Ffmpeg::Frame frame; Ffmpeg::Frame frame;
FfmpegBuffer interleave_buffer;
const int in_format, in_sample_rate, in_channels; const int in_format, in_sample_rate, in_channels;
const size_t in_audio_frame_size; const size_t in_audio_frame_size;

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2003-2020 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 "TwoFilters.hxx"
#include "util/ConstBuffer.hxx"
ConstBuffer<void>
TwoFilters::FilterPCM(ConstBuffer<void> src)
{
return second->FilterPCM(first->FilterPCM(src));
}
ConstBuffer<void>
TwoFilters::Flush()
{
auto result = first->Flush();
if (!result.IsNull())
/* Flush() output from the first Filter must be
filtered by the second Filter */
return second->FilterPCM(result);
return second->Flush();
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2003-2020 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.
*/
#ifndef MPD_WITH_CONVERT_FILTER_HXX
#define MPD_WITH_CONVERT_FILTER_HXX
#include "filter/Filter.hxx"
#include <memory>
/**
* A #Filter implementation which chains two other filters.
*/
class TwoFilters final : public Filter {
std::unique_ptr<Filter> first, second;
public:
template<typename F, typename S>
TwoFilters(F &&_first, S &&_second) noexcept
:Filter(_second->GetOutAudioFormat()),
first(std::forward<F>(_first)),
second(std::forward<S>(_second)) {}
void Reset() noexcept override {
first->Reset();
second->Reset();
}
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
ConstBuffer<void> Flush() override;
};
#endif

View File

@@ -14,6 +14,7 @@ filter_plugins = static_library(
'filter_plugins', 'filter_plugins',
'../../AudioCompress/compress.c', '../../AudioCompress/compress.c',
'NullFilterPlugin.cxx', 'NullFilterPlugin.cxx',
'TwoFilters.cxx',
'ChainFilterPlugin.cxx', 'ChainFilterPlugin.cxx',
'AutoConvertFilterPlugin.cxx', 'AutoConvertFilterPlugin.cxx',
'ConvertFilterPlugin.cxx', 'ConvertFilterPlugin.cxx',

View File

@@ -88,6 +88,19 @@ struct PathTraitsFS {
#endif #endif
} }
gcc_pure
static const_pointer GetFilenameSuffix(const_pointer filename) noexcept {
const_pointer dot = StringFindLast(filename, '.');
return dot != nullptr && dot > filename && dot[1] != 0
? dot + 1
: nullptr;
}
gcc_pure
static const_pointer GetPathSuffix(const_pointer path) noexcept {
return GetFilenameSuffix(GetBase(path));
}
#ifdef _WIN32 #ifdef _WIN32
gcc_pure gcc_nonnull_all gcc_pure gcc_nonnull_all
static constexpr bool IsDrive(const_pointer p) noexcept { static constexpr bool IsDrive(const_pointer p) noexcept {
@@ -199,6 +212,19 @@ struct PathTraitsUTF8 {
return std::strrchr(p, SEPARATOR); return std::strrchr(p, SEPARATOR);
} }
gcc_pure
static const_pointer GetFilenameSuffix(const_pointer filename) noexcept {
const_pointer dot = StringFindLast(filename, '.');
return dot != nullptr && dot > filename && dot[1] != 0
? dot + 1
: nullptr;
}
gcc_pure
static const_pointer GetPathSuffix(const_pointer path) noexcept {
return GetFilenameSuffix(GetBase(path));
}
#ifdef _WIN32 #ifdef _WIN32
gcc_pure gcc_nonnull_all gcc_pure gcc_nonnull_all
static constexpr bool IsDrive(const_pointer p) noexcept { static constexpr bool IsDrive(const_pointer p) noexcept {

View File

@@ -127,7 +127,7 @@ private:
int Recover(int err); int Recover(int err);
/* virtual methods from class MultiSocketMonitor */ /* virtual methods from class MultiSocketMonitor */
std::chrono::steady_clock::duration PrepareSockets() noexcept override; Event::Duration PrepareSockets() noexcept override;
void DispatchSockets() noexcept override; void DispatchSockets() noexcept override;
}; };
@@ -219,12 +219,12 @@ AlsaInputStream::Create(EventLoop &event_loop, const char *uri,
return std::make_unique<AlsaInputStream>(event_loop, mutex, spec); return std::make_unique<AlsaInputStream>(event_loop, mutex, spec);
} }
std::chrono::steady_clock::duration Event::Duration
AlsaInputStream::PrepareSockets() noexcept AlsaInputStream::PrepareSockets() noexcept
{ {
if (IsPaused()) { if (IsPaused()) {
ClearSocketList(); ClearSocketList();
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
return non_block.PrepareSockets(*this, capture_handle); return non_block.PrepareSockets(*this, capture_handle);

View File

@@ -291,7 +291,7 @@ FileDescriptor::GetSize() const noexcept
void void
FileDescriptor::FullRead(void *_buffer, size_t length) FileDescriptor::FullRead(void *_buffer, size_t length)
{ {
auto *buffer = (uint8_t *)_buffer; auto buffer = (uint8_t *)_buffer;
while (length > 0) { while (length > 0) {
ssize_t nbytes = Read(buffer, length); ssize_t nbytes = Read(buffer, length);
@@ -309,7 +309,7 @@ FileDescriptor::FullRead(void *_buffer, size_t length)
void void
FileDescriptor::FullWrite(const void *_buffer, size_t length) FileDescriptor::FullWrite(const void *_buffer, size_t length)
{ {
const uint8_t *buffer = (const uint8_t *)_buffer; auto buffer = (const uint8_t *)_buffer;
while (length > 0) { while (length > 0) {
ssize_t nbytes = Write(buffer, length); ssize_t nbytes = Write(buffer, length);

View File

@@ -21,7 +21,7 @@
#include "event/MultiSocketMonitor.hxx" #include "event/MultiSocketMonitor.hxx"
#include "util/RuntimeError.hxx" #include "util/RuntimeError.hxx"
std::chrono::steady_clock::duration Event::Duration
AlsaNonBlockPcm::PrepareSockets(MultiSocketMonitor &m, snd_pcm_t *pcm) AlsaNonBlockPcm::PrepareSockets(MultiSocketMonitor &m, snd_pcm_t *pcm)
{ {
int count = snd_pcm_poll_descriptors_count(pcm); int count = snd_pcm_poll_descriptors_count(pcm);
@@ -45,7 +45,7 @@ AlsaNonBlockPcm::PrepareSockets(MultiSocketMonitor &m, snd_pcm_t *pcm)
} }
m.ReplaceSocketList(pfds, count); m.ReplaceSocketList(pfds, count);
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
void void
@@ -75,13 +75,13 @@ AlsaNonBlockPcm::DispatchSockets(MultiSocketMonitor &m,
snd_strerror(-err)); snd_strerror(-err));
} }
std::chrono::steady_clock::duration Event::Duration
AlsaNonBlockMixer::PrepareSockets(MultiSocketMonitor &m, snd_mixer_t *mixer) noexcept AlsaNonBlockMixer::PrepareSockets(MultiSocketMonitor &m, snd_mixer_t *mixer) noexcept
{ {
int count = snd_mixer_poll_descriptors_count(mixer); int count = snd_mixer_poll_descriptors_count(mixer);
if (count <= 0) { if (count <= 0) {
m.ClearSocketList(); m.ClearSocketList();
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
struct pollfd *pfds = pfd_buffer.Get(count); struct pollfd *pfds = pfd_buffer.Get(count);
@@ -91,7 +91,7 @@ AlsaNonBlockMixer::PrepareSockets(MultiSocketMonitor &m, snd_mixer_t *mixer) noe
count = 0; count = 0;
m.ReplaceSocketList(pfds, count); m.ReplaceSocketList(pfds, count);
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
void void

View File

@@ -20,12 +20,11 @@
#ifndef MPD_ALSA_NON_BLOCK_HXX #ifndef MPD_ALSA_NON_BLOCK_HXX
#define MPD_ALSA_NON_BLOCK_HXX #define MPD_ALSA_NON_BLOCK_HXX
#include "event/Chrono.hxx"
#include "util/ReusableArray.hxx" #include "util/ReusableArray.hxx"
#include <alsa/asoundlib.h> #include <alsa/asoundlib.h>
#include <chrono>
class MultiSocketMonitor; class MultiSocketMonitor;
/** /**
@@ -39,8 +38,8 @@ public:
/** /**
* Throws on error. * Throws on error.
*/ */
std::chrono::steady_clock::duration PrepareSockets(MultiSocketMonitor &m, Event::Duration PrepareSockets(MultiSocketMonitor &m,
snd_pcm_t *pcm); snd_pcm_t *pcm);
/** /**
* Wrapper for snd_pcm_poll_descriptors_revents(), to be * Wrapper for snd_pcm_poll_descriptors_revents(), to be
@@ -59,8 +58,8 @@ class AlsaNonBlockMixer {
ReusableArray<pollfd> pfd_buffer; ReusableArray<pollfd> pfd_buffer;
public: public:
std::chrono::steady_clock::duration PrepareSockets(MultiSocketMonitor &m, Event::Duration PrepareSockets(MultiSocketMonitor &m,
snd_mixer_t *mixer) noexcept; snd_mixer_t *mixer) noexcept;
/** /**
* Wrapper for snd_mixer_poll_descriptors_revents(), to be * Wrapper for snd_mixer_poll_descriptors_revents(), to be

View File

@@ -32,6 +32,8 @@
#include "Watch.hxx" #include "Watch.hxx"
#include <cassert>
namespace ODBus { namespace ODBus {
WatchManager::Watch::Watch(EventLoop &event_loop, WatchManager::Watch::Watch(EventLoop &event_loop,

View File

@@ -20,36 +20,36 @@
#ifndef MPD_FFMPEG_BUFFER_HXX #ifndef MPD_FFMPEG_BUFFER_HXX
#define MPD_FFMPEG_BUFFER_HXX #define MPD_FFMPEG_BUFFER_HXX
#include "util/Compiler.h"
extern "C" { extern "C" {
#include <libavutil/mem.h> #include <libavutil/mem.h>
} }
#include <cstddef> #include <cstddef>
/* suppress the ffmpeg compatibility macro */
#ifdef SampleFormat
#undef SampleFormat
#endif
class FfmpegBuffer { class FfmpegBuffer {
void *data; void *data = nullptr;
unsigned size; unsigned size = 0;
public: public:
FfmpegBuffer():data(nullptr), size(0) {} FfmpegBuffer() noexcept = default;
~FfmpegBuffer() { ~FfmpegBuffer() noexcept {
av_free(data); av_free(data);
} }
FfmpegBuffer(const FfmpegBuffer &) = delete;
FfmpegBuffer &operator=(const FfmpegBuffer &) = delete;
gcc_malloc gcc_malloc
void *Get(size_t min_size) { void *Get(size_t min_size) noexcept {
av_fast_malloc(&data, &size, min_size); av_fast_malloc(&data, &size, min_size);
return data; return data;
} }
template<typename T> template<typename T>
T *GetT(size_t n) { T *GetT(size_t n) noexcept {
return (T *)Get(n * sizeof(T)); return (T *)Get(n * sizeof(T));
} }
}; };

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2003-2020 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 "Interleave.hxx"
#include "Buffer.hxx"
#include "Error.hxx"
#include "pcm/Interleave.hxx"
#include "util/ConstBuffer.hxx"
extern "C" {
#include <libavutil/frame.h>
}
#include <cassert>
#include <new> // for std::bad_alloc
namespace Ffmpeg {
ConstBuffer<void>
InterleaveFrame(const AVFrame &frame, FfmpegBuffer &buffer)
{
assert(frame.nb_samples > 0);
const AVSampleFormat format = AVSampleFormat(frame.format);
const unsigned channels = frame.channels;
const std::size_t n_frames = frame.nb_samples;
int plane_size;
const int data_size =
av_samples_get_buffer_size(&plane_size, channels,
n_frames, format, 1);
assert(data_size != 0);
if (data_size < 0)
throw MakeFfmpegError(data_size);
void *output_buffer;
if (av_sample_fmt_is_planar(format) && channels > 1) {
output_buffer = buffer.GetT<uint8_t>(data_size);
if (output_buffer == nullptr)
/* Not enough memory - shouldn't happen */
throw std::bad_alloc();
PcmInterleave(output_buffer,
ConstBuffer<const void *>((const void *const*)frame.extended_data,
channels),
n_frames,
av_get_bytes_per_sample(format));
} else {
output_buffer = frame.extended_data[0];
}
return { output_buffer, (size_t)data_size };
}
} // namespace Ffmpeg

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2003-2020 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.
*/
#ifndef MPD_FFMPEG_INTERLEAVE_HXX
#define MPD_FFMPEG_INTERLEAVE_HXX
struct AVFrame;
template<typename T> struct ConstBuffer;
class FfmpegBuffer;
namespace Ffmpeg {
/**
* Return interleaved data from the given non-empty #AVFrame. If the
* data is planar, then the data is copied to a buffer.
*
* Throws on error.
*/
ConstBuffer<void>
InterleaveFrame(const AVFrame &frame, FfmpegBuffer &buffer);
} // namespace Ffmpeg
#endif

View File

@@ -31,11 +31,6 @@ extern "C" {
#include <cassert> #include <cassert>
#include <cstdint> #include <cstdint>
/* suppress the ffmpeg compatibility macro */
#ifdef SampleFormat
#undef SampleFormat
#endif
/* redefine AV_TIME_BASE_Q because libavutil's macro definition is a /* redefine AV_TIME_BASE_Q because libavutil's macro definition is a
compound literal, which is illegal in C++ */ compound literal, which is illegal in C++ */
#ifdef AV_TIME_BASE_Q #ifdef AV_TIME_BASE_Q

View File

@@ -26,6 +26,7 @@ endif
ffmpeg = static_library( ffmpeg = static_library(
'ffmpeg', 'ffmpeg',
'Init.cxx', 'Init.cxx',
'Interleave.cxx',
'LogError.cxx', 'LogError.cxx',
'LogCallback.cxx', 'LogCallback.cxx',
'Error.cxx', 'Error.cxx',

View File

@@ -21,8 +21,6 @@
#include "Connection.hxx" #include "Connection.hxx"
#include "event/Call.hxx" #include "event/Call.hxx"
constexpr std::chrono::steady_clock::duration BlockingNfsOperation::timeout;
void void
BlockingNfsOperation::Run() BlockingNfsOperation::Run()
{ {

View File

@@ -37,7 +37,7 @@ extern "C" {
#include <poll.h> /* for POLLIN, POLLOUT */ #include <poll.h> /* for POLLIN, POLLOUT */
#endif #endif
static constexpr std::chrono::steady_clock::duration NFS_MOUNT_TIMEOUT = static constexpr Event::Duration NFS_MOUNT_TIMEOUT =
std::chrono::minutes(1); std::chrono::minutes(1);
inline void inline void

View File

@@ -0,0 +1,14 @@
Index: libnfs-libnfs-4.0.0/include/win32/win32_compat.h
===================================================================
--- libnfs-libnfs-4.0.0.orig/include/win32/win32_compat.h
+++ libnfs-libnfs-4.0.0/include/win32/win32_compat.h
@@ -133,7 +133,9 @@ struct pollfd {
/* Wrapper macros to call misc. functions win32 is missing */
#define poll(x, y, z) win32_poll(x, y, z)
+#ifndef __MINGW32__
#define snprintf sprintf_s
+#endif
#define inet_pton(x,y,z) win32_inet_pton(x,y,z)
#define open(x, y, z) _open(x, y, z)
#ifndef lseek

View File

@@ -0,0 +1 @@
no_sprintf_s

View File

@@ -42,7 +42,7 @@ namespace Systemd {
class Watchdog { class Watchdog {
TimerEvent timer; TimerEvent timer;
std::chrono::steady_clock::duration interval; Event::Duration interval;
public: public:
explicit Watchdog(EventLoop &_loop) noexcept; explicit Watchdog(EventLoop &_loop) noexcept;

67
src/lib/yajl/Handle.cxx Normal file
View File

@@ -0,0 +1,67 @@
/*
* Copyright 2018-2020 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 "Handle.hxx"
#include "util/RuntimeError.hxx"
#include "util/ScopeExit.hxx"
#include "util/StringStrip.hxx"
#include <cstring>
/**
* Strip whitespace at the beginning and end and replace newline
* characters which are illegal in the MPD protocol.
*/
static const char *
StripErrorMessage(char *s) noexcept
{
s = Strip(s);
while (auto newline = std::strchr(s, '\n'))
*newline = ';';
return s;
}
namespace Yajl {
void
Handle::ThrowError()
{
unsigned char *str = yajl_get_error(handle, false,
nullptr, 0);
AtScopeExit(this, str) {
yajl_free_error(handle, str);
};
throw FormatRuntimeError("Failed to parse JSON: %s",
StripErrorMessage((char *)str));
}
} // namespace Yajl

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 Max Kellermann <max.kellermann@gmail.com> * Copyright 2018-2020 Max Kellermann <max.kellermann@gmail.com>
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions
@@ -30,12 +30,8 @@
#ifndef YAJL_HANDLE_HXX #ifndef YAJL_HANDLE_HXX
#define YAJL_HANDLE_HXX #define YAJL_HANDLE_HXX
#include "util/RuntimeError.hxx"
#include "util/ScopeExit.hxx"
#include <yajl/yajl_parse.h> #include <yajl/yajl_parse.h>
#include <stdexcept>
#include <algorithm> #include <algorithm>
namespace Yajl { namespace Yajl {
@@ -77,15 +73,12 @@ public:
private: private:
void HandleStatus(yajl_status status) { void HandleStatus(yajl_status status) {
if (status == yajl_status_error) { if (status == yajl_status_error)
unsigned char *str = yajl_get_error(handle, false, ThrowError();
nullptr, 0);
AtScopeExit(this, str) {
yajl_free_error(handle, str);
};
throw FormatRuntimeError("Failed to parse JSON: %s", str);
}
} }
[[noreturn]]
void ThrowError();
}; };
} // namespace Yajl } // namespace Yajl

View File

@@ -5,6 +5,7 @@ endif
yajl = static_library( yajl = static_library(
'yajl', 'yajl',
'Handle.cxx',
'ResponseParser.cxx', 'ResponseParser.cxx',
'ParseInputStream.cxx', 'ParseInputStream.cxx',
include_directories: inc, include_directories: inc,

View File

@@ -64,7 +64,7 @@ public:
} }
private: private:
std::chrono::steady_clock::duration PrepareSockets() noexcept override; Event::Duration PrepareSockets() noexcept override;
void DispatchSockets() noexcept override; void DispatchSockets() noexcept override;
}; };
@@ -99,12 +99,12 @@ public:
static constexpr Domain alsa_mixer_domain("alsa_mixer"); static constexpr Domain alsa_mixer_domain("alsa_mixer");
std::chrono::steady_clock::duration Event::Duration
AlsaMixerMonitor::PrepareSockets() noexcept AlsaMixerMonitor::PrepareSockets() noexcept
{ {
if (mixer == nullptr) { if (mixer == nullptr) {
ClearSocketList(); ClearSocketList();
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
return non_block.PrepareSockets(*this, mixer); return non_block.PrepareSockets(*this, mixer);

View File

@@ -33,8 +33,7 @@
#include "util/StringView.hxx" #include "util/StringView.hxx"
#include <cassert> #include <cassert>
#include <cstring>
#include <string.h>
#ifdef HAVE_UN #ifdef HAVE_UN
#include <sys/un.h> #include <sys/un.h>

View File

@@ -359,6 +359,9 @@ AudioOutputControl::LockPauseAsync() noexcept
mixer_auto_close()) */ mixer_auto_close()) */
mixer_auto_close(output->mixer); mixer_auto_close(output->mixer);
if (output)
output->Interrupt();
const std::lock_guard<Mutex> protect(mutex); const std::lock_guard<Mutex> protect(mutex);
assert(allow_play); assert(allow_play);
@@ -379,6 +382,9 @@ AudioOutputControl::LockDrainAsync() noexcept
void void
AudioOutputControl::LockCancelAsync() noexcept AudioOutputControl::LockCancelAsync() noexcept
{ {
if (output)
output->Interrupt();
const std::lock_guard<Mutex> protect(mutex); const std::lock_guard<Mutex> protect(mutex);
if (IsOpen()) { if (IsOpen()) {
@@ -403,6 +409,8 @@ AudioOutputControl::LockRelease() noexcept
if (!output) if (!output)
return; return;
output->Interrupt();
if (output->mixer != nullptr && if (output->mixer != nullptr &&
(!always_on || !output->SupportsPause())) (!always_on || !output->SupportsPause()))
/* the device has no pause mode: close the mixer, /* the device has no pause mode: close the mixer,
@@ -426,6 +434,9 @@ AudioOutputControl::LockCloseWait() noexcept
{ {
assert(!open || !fail_timer.IsDefined()); assert(!open || !fail_timer.IsDefined());
if (output)
output->Interrupt();
std::unique_lock<Mutex> lock(mutex); std::unique_lock<Mutex> lock(mutex);
CloseWait(lock); CloseWait(lock);
} }
@@ -434,6 +445,9 @@ void
AudioOutputControl::BeginDestroy() noexcept AudioOutputControl::BeginDestroy() noexcept
{ {
if (thread.IsDefined()) { if (thread.IsDefined()) {
if (output)
output->Interrupt();
const std::lock_guard<Mutex> protect(mutex); const std::lock_guard<Mutex> protect(mutex);
if (!killed) { if (!killed) {
killed = true; killed = true;

View File

@@ -196,6 +196,15 @@ class AudioOutputControl {
*/ */
bool allow_play = true; bool allow_play = true;
/**
* Was an #AudioOutputInterrupted caught? In this case,
* playback is suspended, and the output thread waits for a
* command.
*
* This field is only valid while the output is open.
*/
bool caught_interrupted;
/** /**
* True while the OutputThread is inside ao_play(). This * True while the OutputThread is inside ao_play(). This
* means the PlayerThread does not need to wake up the * means the PlayerThread does not need to wake up the

View File

@@ -17,7 +17,13 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/ */
#include "Ack.hxx" #ifndef MPD_AUDIO_OUTPUT_ERROR_HXX
#include "util/Domain.hxx" #define MPD_AUDIO_OUTPUT_ERROR_HXX
const Domain ack_domain("ack"); /**
* An exception class that will be thrown by various #AudioOutput
* methods after AudioOutput::Interrupt() has been called.
*/
class AudioOutputInterrupted {};
#endif

View File

@@ -183,6 +183,12 @@ FilteredAudioOutput::Drain()
output->Drain(); output->Drain();
} }
void
FilteredAudioOutput::Interrupt() noexcept
{
output->Interrupt();
}
void void
FilteredAudioOutput::Cancel() noexcept FilteredAudioOutput::Cancel() noexcept
{ {
@@ -196,13 +202,7 @@ FilteredAudioOutput::BeginPause() noexcept
} }
bool bool
FilteredAudioOutput::IteratePause() noexcept FilteredAudioOutput::IteratePause()
{ {
try { return output->Pause();
return output->Pause();
} catch (...) {
FormatError(std::current_exception(), "Failed to pause %s",
GetLogName());
return false;
}
} }

View File

@@ -216,6 +216,8 @@ public:
*/ */
void CloseSoftwareMixer() noexcept; void CloseSoftwareMixer() noexcept;
void Interrupt() noexcept;
gcc_pure gcc_pure
std::chrono::steady_clock::duration Delay() noexcept; std::chrono::steady_clock::duration Delay() noexcept;
@@ -227,7 +229,7 @@ public:
void Cancel() noexcept; void Cancel() noexcept;
void BeginPause() noexcept; void BeginPause() noexcept;
bool IteratePause() noexcept; bool IteratePause();
void EndPause() noexcept{ void EndPause() noexcept{
} }

View File

@@ -41,21 +41,21 @@ protected:
static constexpr unsigned FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT = 0x4; static constexpr unsigned FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT = 0x4;
public: public:
explicit AudioOutput(unsigned _flags):flags(_flags) {} explicit AudioOutput(unsigned _flags) noexcept:flags(_flags) {}
virtual ~AudioOutput() = default; virtual ~AudioOutput() noexcept = default;
AudioOutput(const AudioOutput &) = delete; AudioOutput(const AudioOutput &) = delete;
AudioOutput &operator=(const AudioOutput &) = delete; AudioOutput &operator=(const AudioOutput &) = delete;
bool SupportsEnableDisable() const { bool SupportsEnableDisable() const noexcept {
return flags & FLAG_ENABLE_DISABLE; return flags & FLAG_ENABLE_DISABLE;
} }
bool SupportsPause() const { bool SupportsPause() const noexcept {
return flags & FLAG_PAUSE; return flags & FLAG_PAUSE;
} }
bool GetNeedFullyDefinedAudioFormat() const { bool GetNeedFullyDefinedAudioFormat() const noexcept {
return flags & FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT; return flags & FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT;
} }
@@ -126,6 +126,24 @@ public:
return false; return false;
} }
/**
* Interrupt a blocking operation inside the plugin. This
* method will be called from outside the output thread (and
* therefore the method must be thread-safe), to make the
* output thread ready for receiving a command. For example,
* it will be called to prepare for an upcoming Close(),
* Cancel() or Pause() call.
*
* This method can be called any time, even if the output is
* not open or disabled.
*
* Implementations usually send some kind of message/signal to
* the output thread to wake it up and return to the output
* thread loop (e.g. by throwing #AudioOutputInterrupted),
* where the incoming command will be handled and dispatched.
*/
virtual void Interrupt() noexcept {}
/** /**
* Returns a positive number if the output thread shall further * Returns a positive number if the output thread shall further
* delay the next call to Play() or Pause(), which will happen * delay the next call to Play() or Pause(), which will happen
@@ -142,6 +160,11 @@ public:
/** /**
* Display metadata for the next chunk. Optional method, * Display metadata for the next chunk. Optional method,
* because not all devices can display metadata. * because not all devices can display metadata.
*
* Throws on error.
*
* May throw #AudioOutputInterrupted after Interrupt() has
* been called.
*/ */
virtual void SendTag(const Tag &) {} virtual void SendTag(const Tag &) {}
@@ -151,6 +174,9 @@ public:
* *
* Throws on error. * Throws on error.
* *
* May throw #AudioOutputInterrupted after Interrupt() has
* been called.
*
* @return the number of bytes played (must be a multiple of * @return the number of bytes played (must be a multiple of
* the frame size) * the frame size)
*/ */
@@ -177,6 +203,9 @@ public:
* disconnected. Plugins which do not support pausing will * disconnected. Plugins which do not support pausing will
* simply be closed, and have to be reopened when unpaused. * simply be closed, and have to be reopened when unpaused.
* *
* May throw #AudioOutputInterrupted after Interrupt() has
* been called.
*
* @return false on error (output will be closed by caller), * @return false on error (output will be closed by caller),
* true for continue to pause * true for continue to pause
* *

View File

@@ -18,6 +18,7 @@
*/ */
#include "Control.hxx" #include "Control.hxx"
#include "Error.hxx"
#include "Filtered.hxx" #include "Filtered.hxx"
#include "Client.hxx" #include "Client.hxx"
#include "Domain.hxx" #include "Domain.hxx"
@@ -135,6 +136,7 @@ AudioOutputControl::InternalOpen(const AudioFormat in_audio_format,
last_error = nullptr; last_error = nullptr;
fail_timer.Reset(); fail_timer.Reset();
caught_interrupted = false;
skip_delay = true; skip_delay = true;
AudioFormat f; AudioFormat f;
@@ -243,6 +245,9 @@ AudioOutputControl::PlayChunk(std::unique_lock<Mutex> &lock) noexcept
const ScopeUnlock unlock(mutex); const ScopeUnlock unlock(mutex);
try { try {
output->SendTag(*tag); output->SendTag(*tag);
} catch (AudioOutputInterrupted) {
caught_interrupted = true;
return false;
} catch (...) { } catch (...) {
FormatError(std::current_exception(), FormatError(std::current_exception(),
"Failed to send tag to %s", "Failed to send tag to %s",
@@ -267,6 +272,9 @@ AudioOutputControl::PlayChunk(std::unique_lock<Mutex> &lock) noexcept
nbytes = output->Play(data.data, data.size); nbytes = output->Play(data.data, data.size);
assert(nbytes > 0); assert(nbytes > 0);
assert(nbytes <= data.size); assert(nbytes <= data.size);
} catch (AudioOutputInterrupted) {
caught_interrupted = true;
return false;
} catch (...) { } catch (...) {
FormatError(std::current_exception(), FormatError(std::current_exception(),
"Failed to play on %s", GetLogName()); "Failed to play on %s", GetLogName());
@@ -338,10 +346,15 @@ AudioOutputControl::InternalPause(std::unique_lock<Mutex> &lock) noexcept
if (!WaitForDelay(lock)) if (!WaitForDelay(lock))
break; break;
bool success; bool success = false;
{ try {
const ScopeUnlock unlock(mutex); const ScopeUnlock unlock(mutex);
success = output->IteratePause(); success = output->IteratePause();
} catch (AudioOutputInterrupted) {
} catch (...) {
FormatError(std::current_exception(),
"Failed to pause %s",
GetLogName());
} }
if (!success) { if (!success) {
@@ -418,6 +431,17 @@ AudioOutputControl::Task() noexcept
while (true) { while (true) {
switch (command) { switch (command) {
case Command::NONE: case Command::NONE:
/* no pending command: play (or wait for a
command) */
if (open && allow_play && !caught_interrupted &&
InternalPlay(lock))
/* don't wait for an event if there
are more chunks in the pipe */
continue;
woken_for_play = false;
wake_cond.wait(lock);
break; break;
case Command::ENABLE: case Command::ENABLE:
@@ -449,12 +473,10 @@ AudioOutputControl::Task() noexcept
break; break;
} }
caught_interrupted = false;
InternalPause(lock); InternalPause(lock);
/* don't "break" here: this might cause break;
Play() to be called when command==CLOSE
ends the paused state - "continue" checks
the new command first */
continue;
case Command::RELEASE: case Command::RELEASE:
if (!open) { if (!open) {
@@ -465,6 +487,8 @@ AudioOutputControl::Task() noexcept
break; break;
} }
caught_interrupted = false;
if (always_on) { if (always_on) {
/* in "always_on" mode, the output is /* in "always_on" mode, the output is
paused instead of being closed; paused instead of being closed;
@@ -479,20 +503,18 @@ AudioOutputControl::Task() noexcept
CommandFinished(); CommandFinished();
} }
/* don't "break" here: this might cause break;
Play() to be called when command==CLOSE
ends the paused state - "continue" checks
the new command first */
continue;
case Command::DRAIN: case Command::DRAIN:
if (open) if (open)
InternalDrain(); InternalDrain();
CommandFinished(); CommandFinished();
continue; break;
case Command::CANCEL: case Command::CANCEL:
caught_interrupted = false;
source.Cancel(); source.Cancel();
if (open) { if (open) {
@@ -501,7 +523,7 @@ AudioOutputControl::Task() noexcept
} }
CommandFinished(); CommandFinished();
continue; break;
case Command::KILL: case Command::KILL:
InternalDisable(); InternalDisable();
@@ -509,16 +531,6 @@ AudioOutputControl::Task() noexcept
CommandFinished(); CommandFinished();
return; return;
} }
if (open && allow_play && InternalPlay(lock))
/* don't wait for an event if there are more
chunks in the pipe */
continue;
if (command == Command::NONE) {
woken_for_play = false;
wake_cond.wait(lock);
}
} }
} }

View File

@@ -25,6 +25,7 @@
#include "lib/alsa/PeriodBuffer.hxx" #include "lib/alsa/PeriodBuffer.hxx"
#include "lib/alsa/Version.hxx" #include "lib/alsa/Version.hxx"
#include "../OutputAPI.hxx" #include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "mixer/MixerList.hxx" #include "mixer/MixerList.hxx"
#include "pcm/Export.hxx" #include "pcm/Export.hxx"
#include "system/PeriodClock.hxx" #include "system/PeriodClock.hxx"
@@ -118,7 +119,7 @@ class AlsaOutput final
*/ */
snd_pcm_uframes_t period_frames; snd_pcm_uframes_t period_frames;
std::chrono::steady_clock::duration effective_period_duration; Event::Duration effective_period_duration;
/** /**
* If snd_pcm_avail() goes above this value and no more data * If snd_pcm_avail() goes above this value and no more data
@@ -177,6 +178,15 @@ class AlsaOutput final
bool drain; bool drain;
/**
* Was Interrupt() called? This will unblock
* LockWaitWriteAvailable(). It will be reset by Cancel() and
* Pause(), as documented by the #AudioOutput interface.
*
* Only initialized while the output is open.
*/
bool interrupted;
/** /**
* This buffer gets allocated after opening the ALSA device. * This buffer gets allocated after opening the ALSA device.
* It contains silence samples, enough to fill one period (see * It contains silence samples, enough to fill one period (see
@@ -237,9 +247,12 @@ private:
void Open(AudioFormat &audio_format) override; void Open(AudioFormat &audio_format) override;
void Close() noexcept override; void Close() noexcept override;
void Interrupt() noexcept override;
size_t Play(const void *chunk, size_t size) override; size_t Play(const void *chunk, size_t size) override;
void Drain() override; void Drain() override;
void Cancel() noexcept override; void Cancel() noexcept override;
bool Pause() noexcept override;
/** /**
* Set up the snd_pcm_t object which was opened by the caller. * Set up the snd_pcm_t object which was opened by the caller.
@@ -385,7 +398,7 @@ private:
} }
/* virtual methods from class MultiSocketMonitor */ /* virtual methods from class MultiSocketMonitor */
std::chrono::steady_clock::duration PrepareSockets() noexcept override; Event::Duration PrepareSockets() noexcept override;
void DispatchSockets() noexcept override; void DispatchSockets() noexcept override;
}; };
@@ -728,6 +741,7 @@ AlsaOutput::Open(AudioFormat &audio_format)
out_frame_size = pcm_export->GetOutputFrameSize(); out_frame_size = pcm_export->GetOutputFrameSize();
drain = false; drain = false;
interrupted = false;
size_t period_size = period_frames * out_frame_size; size_t period_size = period_frames * out_frame_size;
ring_buffer = new boost::lockfree::spsc_queue<uint8_t>(period_size * 4); ring_buffer = new boost::lockfree::spsc_queue<uint8_t>(period_size * 4);
@@ -741,6 +755,18 @@ AlsaOutput::Open(AudioFormat &audio_format)
error = {}; error = {};
} }
void
AlsaOutput::Interrupt() noexcept
{
std::unique_lock<Mutex> lock(mutex);
/* the "interrupted" flag will prevent
LockWaitWriteAvailable() from actually waiting, and will
instead throw AudioOutputInterrupted */
interrupted = true;
cond.notify_one();
}
inline int inline int
AlsaOutput::Recover(int err) noexcept AlsaOutput::Recover(int err) noexcept
{ {
@@ -912,6 +938,11 @@ AlsaOutput::CancelInternal() noexcept
void void
AlsaOutput::Cancel() noexcept AlsaOutput::Cancel() noexcept
{ {
{
std::unique_lock<Mutex> lock(mutex);
interrupted = false;
}
if (!LockIsActive()) { if (!LockIsActive()) {
/* early cancel, quick code path without thread /* early cancel, quick code path without thread
synchronization */ synchronization */
@@ -928,6 +959,17 @@ AlsaOutput::Cancel() noexcept
}); });
} }
bool
AlsaOutput::Pause() noexcept
{
std::unique_lock<Mutex> lock(mutex);
interrupted = false;
/* not implemented - this override exists only to reset the
"interrupted" flag */
return false;
}
void void
AlsaOutput::Close() noexcept AlsaOutput::Close() noexcept
{ {
@@ -956,6 +998,11 @@ AlsaOutput::LockWaitWriteAvailable()
if (error) if (error)
std::rethrow_exception(error); std::rethrow_exception(error);
if (interrupted)
/* a CANCEL command is in flight - don't block
here */
throw AudioOutputInterrupted{};
size_t write_available = ring_buffer->write_available(); size_t write_available = ring_buffer->write_available();
if (write_available >= min_available) { if (write_available >= min_available) {
/* reserve room for one extra block, just in /* reserve room for one extra block, just in
@@ -1005,12 +1052,12 @@ AlsaOutput::Play(const void *chunk, size_t size)
return size; return size;
} }
std::chrono::steady_clock::duration Event::Duration
AlsaOutput::PrepareSockets() noexcept AlsaOutput::PrepareSockets() noexcept
{ {
if (!LockIsActiveAndNotWaiting()) { if (!LockIsActiveAndNotWaiting()) {
ClearSocketList(); ClearSocketList();
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
try { try {
@@ -1018,7 +1065,7 @@ AlsaOutput::PrepareSockets() noexcept
} catch (...) { } catch (...) {
ClearSocketList(); ClearSocketList();
LockCaughtError(); LockCaughtError();
return std::chrono::steady_clock::duration(-1); return Event::Duration(-1);
} }
} }

View File

@@ -20,6 +20,7 @@
#include "config.h" #include "config.h"
#include "JackOutputPlugin.hxx" #include "JackOutputPlugin.hxx"
#include "../OutputAPI.hxx" #include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "output/Features.h" #include "output/Features.h"
#include "thread/Mutex.hxx" #include "thread/Mutex.hxx"
#include "util/ScopeExit.hxx" #include "util/ScopeExit.hxx"
@@ -79,6 +80,15 @@ class JackOutput final : public AudioOutput {
*/ */
std::atomic_bool pause; std::atomic_bool pause;
/**
* Was Interrupt() called? This will unblock Play(). It will
* be reset by Cancel() and Pause(), as documented by the
* #AudioOutput interface.
*
* Only initialized while the output is open.
*/
bool interrupted;
/** /**
* Protects #error. * Protects #error.
*/ */
@@ -156,6 +166,8 @@ public:
Stop(); Stop();
} }
void Interrupt() noexcept override;
std::chrono::steady_clock::duration Delay() const noexcept override { std::chrono::steady_clock::duration Delay() const noexcept override {
return pause && !LockWasShutdown() return pause && !LockWasShutdown()
? std::chrono::seconds(1) ? std::chrono::seconds(1)
@@ -164,6 +176,7 @@ public:
size_t Play(const void *chunk, size_t size) override; size_t Play(const void *chunk, size_t size) override;
void Cancel() noexcept override;
bool Pause() override; bool Pause() override;
private: private:
@@ -613,9 +626,21 @@ JackOutput::Open(AudioFormat &new_audio_format)
new_audio_format.format = SampleFormat::FLOAT; new_audio_format.format = SampleFormat::FLOAT;
audio_format = new_audio_format; audio_format = new_audio_format;
interrupted = false;
Start(); Start();
} }
void
JackOutput::Interrupt() noexcept
{
const std::unique_lock<Mutex> lock(mutex);
/* the "interrupted" flag will prevent Play() from waiting,
and will instead throw AudioOutputInterrupted */
interrupted = true;
}
inline size_t inline size_t
JackOutput::WriteSamples(const float *src, size_t n_frames) JackOutput::WriteSamples(const float *src, size_t n_frames)
{ {
@@ -671,6 +696,9 @@ JackOutput::Play(const void *chunk, size_t size)
const std::lock_guard<Mutex> lock(mutex); const std::lock_guard<Mutex> lock(mutex);
if (error) if (error)
std::rethrow_exception(error); std::rethrow_exception(error);
if (interrupted)
throw AudioOutputInterrupted{};
} }
size_t frames_written = size_t frames_written =
@@ -684,11 +712,19 @@ JackOutput::Play(const void *chunk, size_t size)
} }
} }
void
JackOutput::Cancel() noexcept
{
const std::unique_lock<Mutex> lock(mutex);
interrupted = false;
}
inline bool inline bool
JackOutput::Pause() JackOutput::Pause()
{ {
{ {
const std::lock_guard<Mutex> lock(mutex); const std::lock_guard<Mutex> lock(mutex);
interrupted = false;
if (error) if (error)
std::rethrow_exception(error); std::rethrow_exception(error);
} }

View File

@@ -203,7 +203,7 @@ OSXOutput::GetVolume()
const auto vol = AudioObjectGetPropertyDataT<Float32>(dev_id, const auto vol = AudioObjectGetPropertyDataT<Float32>(dev_id,
aopa); aopa);
return static_cast<int>(vol * 100.0); return static_cast<int>(vol * 100.0f);
} }
void void
@@ -420,10 +420,11 @@ osx_output_set_device_format(AudioDeviceID dev_id,
float score = osx_output_score_format(format_desc, target_format); float score = osx_output_score_format(format_desc, target_format);
// print all (linear pcm) formats and their rating // print all (linear pcm) formats and their rating
if (score > 0.0) if (score > 0.0f)
FormatDebug(osx_output_domain, FormatDebug(osx_output_domain,
"Format: %s rated %f", "Format: %s rated %f",
StreamDescriptionToString(format_desc).c_str(), score); StreamDescriptionToString(format_desc).c_str(),
(double)score);
if (score > output_score) { if (score > output_score) {
output_score = score; output_score = score;

View File

@@ -22,6 +22,7 @@
#include "lib/pulse/LogError.hxx" #include "lib/pulse/LogError.hxx"
#include "lib/pulse/LockGuard.hxx" #include "lib/pulse/LockGuard.hxx"
#include "../OutputAPI.hxx" #include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "mixer/MixerList.hxx" #include "mixer/MixerList.hxx"
#include "mixer/plugins/PulseMixerPlugin.hxx" #include "mixer/plugins/PulseMixerPlugin.hxx"
#include "util/ScopeExit.hxx" #include "util/ScopeExit.hxx"
@@ -57,6 +58,15 @@ class PulseOutput final : AudioOutput {
bool pause; bool pause;
/**
* Was Interrupt() called? This will unblock Play(). It will
* be reset by Cancel() and Pause(), as documented by the
* #AudioOutput interface.
*
* Only initialized while the output is open.
*/
bool interrupted;
explicit PulseOutput(const ConfigBlock &block); explicit PulseOutput(const ConfigBlock &block);
public: public:
@@ -99,6 +109,8 @@ public:
void Open(AudioFormat &audio_format) override; void Open(AudioFormat &audio_format) override;
void Close() noexcept override; void Close() noexcept override;
void Interrupt() noexcept override;
[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override; [[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
size_t Play(const void *chunk, size_t size) override; size_t Play(const void *chunk, size_t size) override;
void Cancel() noexcept override; void Cancel() noexcept override;
@@ -677,6 +689,7 @@ PulseOutput::Open(AudioFormat &audio_format)
} }
pause = false; pause = false;
interrupted = false;
} }
void void
@@ -704,6 +717,21 @@ PulseOutput::Close() noexcept
DeleteContext(); DeleteContext();
} }
void
PulseOutput::Interrupt() noexcept
{
if (mainloop == nullptr)
return;
const Pulse::LockGuard lock(mainloop);
/* the "interrupted" flag will prevent Play() from blocking,
and will instead throw AudioOutputInterrupted */
interrupted = true;
Signal();
}
void void
PulseOutput::WaitStream() PulseOutput::WaitStream()
{ {
@@ -719,6 +747,9 @@ PulseOutput::WaitStream()
"failed to connect the stream"); "failed to connect the stream");
case PA_STREAM_CREATING: case PA_STREAM_CREATING:
if (interrupted)
throw AudioOutputInterrupted{};
pa_threaded_mainloop_wait(mainloop); pa_threaded_mainloop_wait(mainloop);
break; break;
} }
@@ -784,6 +815,9 @@ PulseOutput::Play(const void *chunk, size_t size)
if (pa_stream_is_suspended(stream)) if (pa_stream_is_suspended(stream))
throw std::runtime_error("suspended"); throw std::runtime_error("suspended");
if (interrupted)
throw AudioOutputInterrupted{};
pa_threaded_mainloop_wait(mainloop); pa_threaded_mainloop_wait(mainloop);
if (pa_stream_get_state(stream) != PA_STREAM_READY) if (pa_stream_get_state(stream) != PA_STREAM_READY)
@@ -813,6 +847,7 @@ PulseOutput::Cancel() noexcept
assert(stream != nullptr); assert(stream != nullptr);
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
interrupted = false;
if (pa_stream_get_state(stream) != PA_STREAM_READY) { if (pa_stream_get_state(stream) != PA_STREAM_READY) {
/* no need to flush when the stream isn't connected /* no need to flush when the stream isn't connected
@@ -842,6 +877,7 @@ PulseOutput::Pause()
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
pause = true; pause = true;
interrupted = false;
/* check if the stream is (already/still) connected */ /* check if the stream is (already/still) connected */

View File

@@ -71,6 +71,9 @@ static constexpr unsigned n_playlist_plugins =
/** which plugins have been initialized successfully? */ /** which plugins have been initialized successfully? */
static bool playlist_plugins_enabled[n_playlist_plugins]; static bool playlist_plugins_enabled[n_playlist_plugins];
/** which plugins have the "as_folder" option enabled? */
static bool playlist_plugins_as_folder[n_playlist_plugins];
#define playlist_plugins_for_each_enabled(plugin) \ #define playlist_plugins_for_each_enabled(plugin) \
playlist_plugins_for_each(plugin) \ playlist_plugins_for_each(plugin) \
if (playlist_plugins_enabled[playlist_plugin_iterator - playlist_plugins]) if (playlist_plugins_enabled[playlist_plugin_iterator - playlist_plugins])
@@ -96,6 +99,10 @@ playlist_list_global_init(const ConfigData &config)
playlist_plugins_enabled[i] = playlist_plugins_enabled[i] =
playlist_plugin_init(playlist_plugins[i], *param); playlist_plugin_init(playlist_plugins[i], *param);
playlist_plugins_as_folder[i] =
param->GetBlockValue("as_directory",
playlist_plugins[i]->as_folder);
} }
} }
@@ -106,6 +113,16 @@ playlist_list_global_finish() noexcept
playlist_plugin_finish(plugin); playlist_plugin_finish(plugin);
} }
bool
GetPlaylistPluginAsFolder(const PlaylistPlugin &plugin) noexcept
{
/* this loop has no end condition because it must finish when
the plugin was found */
for (std::size_t i = 0;; ++i)
if (playlist_plugins[i] == &plugin)
return playlist_plugins_as_folder[i];
}
static std::unique_ptr<SongEnumerator> static std::unique_ptr<SongEnumerator>
playlist_list_open_uri_scheme(const char *uri, Mutex &mutex, playlist_list_open_uri_scheme(const char *uri, Mutex &mutex,
bool *tried) bool *tried)
@@ -207,11 +224,7 @@ gcc_pure
static StringView static StringView
ExtractMimeTypeMainPart(StringView s) noexcept ExtractMimeTypeMainPart(StringView s) noexcept
{ {
const auto separator = s.Find(';'); return s.Split(';').first;
if (separator != nullptr)
s.SetEnd(separator);
return s;
} }
static std::unique_ptr<SongEnumerator> static std::unique_ptr<SongEnumerator>

View File

@@ -59,6 +59,14 @@ public:
} }
}; };
/**
* Shall this playlists supported by this plugin be represented as
* directories in the database?
*/
gcc_const
bool
GetPlaylistPluginAsFolder(const PlaylistPlugin &plugin) noexcept;
/** /**
* Opens a playlist by its URI. * Opens a playlist by its URI.
*/ */

View File

@@ -19,91 +19,83 @@
#include "CueParser.hxx" #include "CueParser.hxx"
#include "tag/ParseName.hxx" #include "tag/ParseName.hxx"
#include "util/Alloc.hxx" #include "util/StringView.hxx"
#include "util/StringStrip.hxx"
#include "util/CharUtil.hxx" #include "util/CharUtil.hxx"
#include <algorithm>
#include <cassert> #include <cassert>
#include <cstring>
#include <stdlib.h> static StringView
cue_next_word(StringView &src) noexcept
static const char *
cue_next_word(char *p, char **pp)
{ {
assert(p >= *pp); assert(!src.empty());
assert(!IsWhitespaceNotNull(*p)); assert(!IsWhitespaceNotNull(src.front()));
const char *word = p; auto end = std::find_if(src.begin(), src.end(),
while (!IsWhitespaceOrNull(*p)) [](char ch){ return IsWhitespaceOrNull(ch); });
++p; StringView word(src.begin(), end);
src = src.substr(end);
*p = 0;
*pp = p + 1;
return word; return word;
} }
static const char * static StringView
cue_next_quoted(char *p, char **pp) cue_next_quoted(StringView &src) noexcept
{ {
assert(p >= *pp); assert(src.data[-1] == '"');
assert(p[-1] == '"');
char *end = std::strchr(p, '"'); auto end = src.Find('"');
if (end == nullptr) { if (end == nullptr)
/* syntax error - ignore it silently */ /* syntax error - ignore it silently */
*pp = p + strlen(p); return std::exchange(src, nullptr);
return p;
}
*end = 0; StringView value(src.data, end);
*pp = end + 1; src = src.substr(end + 1);
return value;
return p;
} }
static const char * static StringView
cue_next_token(char **pp) cue_next_token(StringView &src) noexcept
{ {
char *p = StripLeft(*pp); src.StripLeft();
if (*p == 0) if (src.empty())
return nullptr; return nullptr;
return cue_next_word(p, pp); return cue_next_word(src);
} }
static const char * static const StringView
cue_next_value(char **pp) cue_next_value(StringView &src) noexcept
{ {
char *p = StripLeft(*pp); src.StripLeft();
if (*p == 0) if (src.empty())
return nullptr; return nullptr;
if (*p == '"') if (src.front() == '"') {
return cue_next_quoted(p + 1, pp); src.pop_front();
else return cue_next_quoted(src);
return cue_next_word(p, pp); } else
return cue_next_word(src);
} }
static void static void
cue_add_tag(TagBuilder &tag, TagType type, char *p) cue_add_tag(TagBuilder &tag, TagType type, StringView src) noexcept
{ {
const char *value = cue_next_value(&p); auto value = cue_next_value(src);
if (value != nullptr) if (value != nullptr)
tag.AddItem(type, value); tag.AddItem(type, value);
} }
static void static void
cue_parse_rem(char *p, TagBuilder &tag) cue_parse_rem(StringView src, TagBuilder &tag) noexcept
{ {
const char *type = cue_next_token(&p); auto type = cue_next_token(src);
if (type == nullptr) if (type == nullptr)
return; return;
TagType type2 = tag_name_parse_i(type); TagType type2 = tag_name_parse_i(type);
if (type2 != TAG_NUM_OF_ITEM_TYPES) if (type2 != TAG_NUM_OF_ITEM_TYPES)
cue_add_tag(tag, type2, p); cue_add_tag(tag, type2, src);
} }
TagBuilder * TagBuilder *
@@ -117,22 +109,47 @@ CueParser::GetCurrentTag() noexcept
return nullptr; return nullptr;
} }
static int static bool
cue_parse_position(const char *p) IsDigit(StringView s) noexcept
{ {
char *endptr; return !s.empty() && IsDigitASCII(s.front());
unsigned long minutes = strtoul(p, &endptr, 10); }
if (endptr == p || *endptr != ':')
static unsigned
cue_next_unsigned(StringView &src) noexcept
{
if (!IsDigit(src)) {
src = nullptr;
return 0;
}
unsigned value = 0;
do {
char ch = src.front();
src.pop_front();
value = value * 10u + unsigned(ch - '0');
} while (IsDigit(src));
return value;
}
static int
cue_parse_position(StringView src) noexcept
{
unsigned minutes = cue_next_unsigned(src);
if (src.empty() || src.front() != ':')
return -1; return -1;
p = endptr + 1; src.pop_front();
unsigned long seconds = strtoul(p, &endptr, 10); unsigned seconds = cue_next_unsigned(src);
if (endptr == p || *endptr != ':') if (src.empty() || src.front() != ':')
return -1; return -1;
p = endptr + 1; src.pop_front();
unsigned long frames = strtoul(p, &endptr, 10); unsigned long frames = cue_next_unsigned(src);
if (endptr == p || *endptr != 0) if (src == nullptr || !src.empty())
return -1; return -1;
return minutes * 60000 + seconds * 1000 + frames * 1000 / 75; return minutes * 60000 + seconds * 1000 + frames * 1000 / 75;
@@ -158,20 +175,19 @@ CueParser::Commit() noexcept
} }
void void
CueParser::Feed2(char *p) noexcept CueParser::Feed(StringView src) noexcept
{ {
assert(!end); assert(!end);
assert(p != nullptr);
const char *command = cue_next_token(&p); auto command = cue_next_token(src);
if (command == nullptr) if (command == nullptr)
return; return;
if (strcmp(command, "REM") == 0) { if (command.Equals("REM")) {
TagBuilder *tag = GetCurrentTag(); TagBuilder *tag = GetCurrentTag();
if (tag != nullptr) if (tag != nullptr)
cue_parse_rem(p, *tag); cue_parse_rem(src, *tag);
} else if (strcmp(command, "PERFORMER") == 0) { } else if (command.Equals("PERFORMER")) {
/* MPD knows a "performer" tag, but it is not a good /* MPD knows a "performer" tag, but it is not a good
match for this CUE tag; from the Hydrogenaudio match for this CUE tag; from the Hydrogenaudio
Knowledgebase: "At top-level this will specify the Knowledgebase: "At top-level this will specify the
@@ -184,27 +200,27 @@ CueParser::Feed2(char *p) noexcept
TagBuilder *tag = GetCurrentTag(); TagBuilder *tag = GetCurrentTag();
if (tag != nullptr) if (tag != nullptr)
cue_add_tag(*tag, type, p); cue_add_tag(*tag, type, src);
} else if (strcmp(command, "TITLE") == 0) { } else if (command.Equals("TITLE")) {
if (state == HEADER) if (state == HEADER)
cue_add_tag(header_tag, TAG_ALBUM, p); cue_add_tag(header_tag, TAG_ALBUM, src);
else if (state == TRACK) else if (state == TRACK)
cue_add_tag(song_tag, TAG_TITLE, p); cue_add_tag(song_tag, TAG_TITLE, src);
} else if (strcmp(command, "FILE") == 0) { } else if (command.Equals("FILE")) {
Commit(); Commit();
const char *new_filename = cue_next_value(&p); const auto new_filename = cue_next_value(src);
if (new_filename == nullptr) if (new_filename == nullptr)
return; return;
const char *type = cue_next_token(&p); const auto type = cue_next_token(src);
if (type == nullptr) if (type == nullptr)
return; return;
if (strcmp(type, "WAVE") != 0 && if (!type.Equals("WAVE") &&
strcmp(type, "FLAC") != 0 && /* non-standard */ !type.Equals("FLAC") && /* non-standard */
strcmp(type, "MP3") != 0 && !type.Equals("MP3") &&
strcmp(type, "AIFF") != 0) { !type.Equals("AIFF")) {
state = IGNORE_FILE; state = IGNORE_FILE;
return; return;
} }
@@ -213,18 +229,18 @@ CueParser::Feed2(char *p) noexcept
filename = new_filename; filename = new_filename;
} else if (state == IGNORE_FILE) { } else if (state == IGNORE_FILE) {
return; return;
} else if (strcmp(command, "TRACK") == 0) { } else if (command.Equals("TRACK")) {
Commit(); Commit();
const char *nr = cue_next_token(&p); const auto nr = cue_next_token(src);
if (nr == nullptr) if (nr == nullptr)
return; return;
const char *type = cue_next_token(&p); const auto type = cue_next_token(src);
if (type == nullptr) if (type == nullptr)
return; return;
if (strcmp(type, "AUDIO") != 0) { if (!type.Equals("AUDIO")) {
state = IGNORE_TRACK; state = IGNORE_TRACK;
return; return;
} }
@@ -239,15 +255,15 @@ CueParser::Feed2(char *p) noexcept
} else if (state == IGNORE_TRACK) { } else if (state == IGNORE_TRACK) {
return; return;
} else if (state == TRACK && strcmp(command, "INDEX") == 0) { } else if (state == TRACK && command.Equals("INDEX")) {
if (ignore_index) if (ignore_index)
return; return;
const char *nr = cue_next_token(&p); const auto nr = cue_next_token(src);
if (nr == nullptr) if (nr == nullptr)
return; return;
const char *position = cue_next_token(&p); const auto position = cue_next_token(src);
if (position == nullptr) if (position == nullptr)
return; return;
@@ -258,23 +274,14 @@ CueParser::Feed2(char *p) noexcept
if (previous != nullptr && previous->GetStartTime().ToMS() < (unsigned)position_ms) if (previous != nullptr && previous->GetStartTime().ToMS() < (unsigned)position_ms)
previous->SetEndTime(SongTime::FromMS(position_ms)); previous->SetEndTime(SongTime::FromMS(position_ms));
current->SetStartTime(SongTime::FromMS(position_ms)); if (current != nullptr)
if(strcmp(nr, "00") != 0 || previous == nullptr) current->SetStartTime(SongTime::FromMS(position_ms));
if (!nr.Equals("00") || previous == nullptr)
ignore_index = true; ignore_index = true;
} }
} }
void
CueParser::Feed(const char *line) noexcept
{
assert(!end);
assert(line != nullptr);
char *allocated = xstrdup(line);
Feed2(allocated);
free(allocated);
}
void void
CueParser::Finish() noexcept CueParser::Finish() noexcept
{ {

View File

@@ -27,6 +27,8 @@
#include <string> #include <string>
#include <memory> #include <memory>
struct StringView;
class CueParser { class CueParser {
enum { enum {
/** /**
@@ -104,7 +106,7 @@ public:
* Feed a text line from the CUE file into the parser. Call * Feed a text line from the CUE file into the parser. Call
* Get() after this to see if a song has been finished. * Get() after this to see if a song has been finished.
*/ */
void Feed(const char *line) noexcept; void Feed(StringView line) noexcept;
/** /**
* Tell the parser that the end of the file has been reached. Call * Tell the parser that the end of the file has been reached. Call
@@ -132,8 +134,6 @@ private:
* song's start time). * song's start time).
*/ */
void Commit() noexcept; void Commit() noexcept;
void Feed2(char *p) noexcept;
}; };
#endif #endif

View File

@@ -22,6 +22,7 @@
#include "../SongEnumerator.hxx" #include "../SongEnumerator.hxx"
#include "../cue/CueParser.hxx" #include "../cue/CueParser.hxx"
#include "input/TextInputStream.hxx" #include "input/TextInputStream.hxx"
#include "util/StringView.hxx"
class CuePlaylist final : public SongEnumerator { class CuePlaylist final : public SongEnumerator {
TextInputStream tis; TextInputStream tis;

View File

@@ -25,8 +25,6 @@
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
class Domain;
enum ack { enum ack {
ACK_ERROR_NOT_LIST = 1, ACK_ERROR_NOT_LIST = 1,
ACK_ERROR_ARG = 2, ACK_ERROR_ARG = 2,
@@ -43,8 +41,6 @@ enum ack {
ACK_ERROR_EXIST = 56, ACK_ERROR_EXIST = 56,
}; };
extern const Domain ack_domain;
class ProtocolError : public std::runtime_error { class ProtocolError : public std::runtime_error {
enum ack code; enum ack code;

View File

@@ -120,8 +120,7 @@ CompositeStorage::Directory::Make(std::string_view uri)
if (name.empty()) if (name.empty())
continue; continue;
auto i = directory->children.emplace(std::move(name), auto i = directory->children.emplace(name, Directory());
Directory());
directory = &i.first->second; directory = &i.first->second;
} }

View File

@@ -80,7 +80,7 @@ std::string_view
CurlStorage::MapToRelativeUTF8(std::string_view uri_utf8) const noexcept CurlStorage::MapToRelativeUTF8(std::string_view uri_utf8) const noexcept
{ {
return PathTraitsUTF8::Relative(base, return PathTraitsUTF8::Relative(base,
CurlUnescape(uri_utf8).c_str()); CurlUnescape(uri_utf8));
} }
class BlockingHttpRequest : protected CurlResponseHandler { class BlockingHttpRequest : protected CurlResponseHandler {

View File

@@ -27,16 +27,14 @@
template<typename T> class AllocatedString; template<typename T> class AllocatedString;
/** /**
* Format into a newly allocated string. The caller frees the return * Format into an #AllocatedString.
* value with delete[].
*/ */
gcc_nonnull_all gcc_nonnull_all
AllocatedString<char> AllocatedString<char>
FormatStringV(const char *fmt, std::va_list args) noexcept; FormatStringV(const char *fmt, std::va_list args) noexcept;
/** /**
* Format into a newly allocated string. The caller frees the return * Format into an #AllocatedString.
* value with delete[].
*/ */
gcc_nonnull(1) gcc_printf(1,2) gcc_nonnull(1) gcc_printf(1,2)
AllocatedString<char> AllocatedString<char>

View File

@@ -0,0 +1,24 @@
#include "playlist/cue/CueParser.hxx"
#include "util/IterableSplitString.hxx"
#include "util/StringView.hxx"
extern "C" {
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
CueParser parser;
const std::string_view src{(const char *)data, size};
for (const auto line : IterableSplitString(src, '\n')) {
parser.Feed(line);
parser.Get();
}
parser.Finish();
parser.Get();
return 0;
}

9
test/fuzzer/meson.build Normal file
View File

@@ -0,0 +1,9 @@
executable(
'FuzzCueParser',
'FuzzCueParser.cxx',
'../../src/playlist/cue/CueParser.cxx',
include_directories: inc,
dependencies: [
tag_dep,
],
)

View File

@@ -264,7 +264,6 @@ if enable_database
executable( executable(
'DumpDatabase', 'DumpDatabase',
'DumpDatabase.cxx', 'DumpDatabase.cxx',
'../src/protocol/Ack.cxx',
'../src/db/Registry.cxx', '../src/db/Registry.cxx',
'../src/db/Selection.cxx', '../src/db/Selection.cxx',
'../src/db/PlaylistVector.cxx', '../src/db/PlaylistVector.cxx',