Compare commits

...

49 Commits

Author SHA1 Message Date
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
62 changed files with 807 additions and 355 deletions

11
NEWS

@@ -1,3 +1,14 @@
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)
* protocol
- "findadd"/"searchadd"/"searchaddpl" support the "sort" and

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

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

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

@@ -770,6 +770,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).
.. _realtime:
Real-Time Scheduling
--------------------
@@ -1096,34 +1098,66 @@ The :program:`MPD` project runs a `forum <https://forum.musicpd.org/>`_ and an I
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.
* 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 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.
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.
: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.

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.22',
version: '0.22.1',
meson_version: '>= 0.49.0',
default_options: [
'c_std=c99',
@@ -112,6 +112,13 @@ if get_option('buildtype') != 'debug'
]
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_cflags + c_compiler.get_supported_arguments(test_cflags), language: 'c')
add_global_link_arguments(compiler.get_supported_link_arguments(test_ldflags), language: 'cpp')
@@ -147,8 +154,6 @@ add_global_arguments(common_cppflags, language: 'cpp')
enable_daemon = not is_windows and not is_android and get_option('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_GETPWUID_R', compiler.has_function('getpwuid_r'))
conf.set('HAVE_INITGROUPS', compiler.has_function('initgroups'))
@@ -504,6 +509,7 @@ mpd = build_target(
chromaprint_dep,
],
link_args: link_args,
build_by_default: not get_option('fuzzer'),
install: not is_android and not is_haiku,
)
@@ -544,3 +550,7 @@ subdir('doc')
if get_option('test')
subdir('test')
endif
if get_option('fuzzer')
subdir('test/fuzzer')
endif

@@ -2,8 +2,6 @@ option('documentation', type: 'feature', description: 'Build documentation')
option('html_manual', type: 'boolean', value: true, description: 'Build the HTML manual')
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('inotify', type: 'boolean', value: true, description: 'inotify support (for automatic database update)')
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_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
#

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

@@ -111,7 +111,7 @@
#include <climits>
#ifdef HAVE_CLOCALE
#ifndef ANDROID
#include <clocale>
#endif
@@ -358,11 +358,9 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
#endif
#ifndef ANDROID
#ifdef HAVE_CLOCALE
/* initialize locale */
std::setlocale(LC_CTYPE,"");
std::setlocale(LC_COLLATE, "");
#endif
#endif
const ScopeIcuInit icu_init;

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

@@ -21,17 +21,16 @@
#define MPD_STATE_FILE_CONFIG_HXX
#include "fs/AllocatedPath.hxx"
#include <chrono>
#include "event/Chrono.hxx"
struct ConfigData;
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;
std::chrono::steady_clock::duration interval;
Event::Duration interval;
bool restore_paused;

@@ -24,7 +24,7 @@
#define CLIENT_MAX_COMMAND_LIST_DEFAULT (2048*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_output_buffer_size;

@@ -20,11 +20,11 @@
#ifndef MPD_CLIENT_CONFIG_HXX
#define MPD_CLIENT_CONFIG_HXX
#include <chrono>
#include "event/Chrono.hxx"
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_output_buffer_size;

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

@@ -47,28 +47,13 @@ BlockParam::GetIntValue() const
unsigned
BlockParam::GetUnsignedValue() const
{
const char *const s = value.c_str();
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;
return With(ParseUnsigned);
}
unsigned
BlockParam::GetPositiveValue() const
{
const char *const s = value.c_str();
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;
return With(ParsePositive);
}
bool

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

@@ -78,22 +78,14 @@ struct ConfigData {
std::chrono::steady_clock::duration
GetUnsigned(ConfigOption option,
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);
}
std::chrono::steady_clock::duration default_value) const;
unsigned GetPositive(ConfigOption option,
unsigned default_value) const;
std::chrono::steady_clock::duration
GetPositive(ConfigOption option,
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);
}
std::chrono::steady_clock::duration default_value) const;
bool GetBool(ConfigOption option, bool default_value) const;

@@ -22,6 +22,8 @@
#include "util/StringStrip.hxx"
#include "util/StringUtil.hxx"
#include <cstdlib>
bool
ParseBool(const char *value)
{
@@ -37,28 +39,59 @@ ParseBool(const char *value)
throw FormatRuntimeError(R"(Not a valid boolean ("yes" or "no"): "%s")", value);
}
template<size_t OPERAND>
static size_t
Multiply(size_t value)
long
ParseLong(const char *s)
{
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)
throw std::runtime_error("Value too large");
return value * OPERAND;
}
size_t
ParseSize(const char *s, size_t default_factor)
std::size_t
ParseSize(const char *s, std::size_t default_factor)
{
char *endptr;
size_t value = strtoul(s, &endptr, 10);
std::size_t value = strtoul(s, &endptr, 10);
if (endptr == s)
throw std::runtime_error("Failed to parse integer");
static constexpr size_t KILO = 1024;
static constexpr size_t MEGA = 1024 * KILO;
static constexpr size_t GIGA = 1024 * MEGA;
static constexpr std::size_t KILO = 1024;
static constexpr std::size_t MEGA = 1024 * KILO;
static constexpr std::size_t GIGA = 1024 * MEGA;
s = StripLeft(endptr);
@@ -102,3 +135,11 @@ ParseSize(const char *s, size_t default_factor)
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);
}

@@ -20,6 +20,7 @@
#ifndef MPD_CONFIG_PARSER_HXX
#define MPD_CONFIG_PARSER_HXX
#include <chrono>
#include <cstddef>
/**
@@ -28,12 +29,36 @@
bool
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.
*
* Throws on error.
*/
size_t
ParseSize(const char *s, size_t default_factor=1);
std::size_t
ParseSize(const char *s, std::size_t default_factor=1);
/**
* Throws on error.
*/
std::chrono::steady_clock::duration
ParseDuration(const char *s);
#endif

@@ -29,7 +29,7 @@
* UpdateService::Enqueue(). This increases the probability that
* 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);
void

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

@@ -63,6 +63,17 @@ IsOpusTags(const ogg_packet &packet) noexcept
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
mpd_opus_init([[maybe_unused]] const ConfigBlock &block)
{
@@ -76,10 +87,11 @@ class MPDOpusDecoder final : public OggDecoder {
opus_int16 *output_buffer = nullptr;
/**
* The output gain from the Opus header. Initialized by
* OnOggBeginning().
* The output gain from the Opus header in dB that should be
* 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
@@ -111,6 +123,14 @@ class MPDOpusDecoder final : public OggDecoder {
*/
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:
explicit MPDOpusDecoder(DecoderReader &reader)
:OggDecoder(reader) {}
@@ -170,10 +190,14 @@ MPDOpusDecoder::OnOggBeginning(const ogg_packet &packet)
throw std::runtime_error("BOS packet must be OpusHead");
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))
throw std::runtime_error("Malformed BOS packet");
/* convert Q7.8 fixed-point to float */
output_gain = float(output_gain_i) / 256.0f;
granulepos = 0;
skip = pre_skip;
@@ -245,22 +269,22 @@ MPDOpusDecoder::HandleTags(const ogg_packet &packet)
ReplayGainInfo rgi;
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;
AddTagHandler h(tag_builder);
if (!ScanOpusTags(packet.packet, packet.bytes, &rgi, h))
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()) {
Tag tag = tag_builder.Commit();
@@ -275,6 +299,18 @@ MPDOpusDecoder::HandleAudio(const ogg_packet &packet)
{
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,
(const unsigned char*)packet.packet,
packet.bytes,

@@ -61,7 +61,7 @@ ScanOneOpusTag(StringView name, StringView value,
const char *endptr;
const auto l = ParseInt64(value, &endptr, 10);
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 &&
name.EqualsIgnoreCase("R128_ALBUM_GAIN")) {
/* R128_ALBUM_GAIN is a Q7.8 fixed point number in
@@ -70,7 +70,7 @@ ScanOneOpusTag(StringView name, StringView value,
const char *endptr;
const auto l = ParseInt64(value, &endptr, 10);
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);

36
src/event/Chrono.hxx Normal 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 */

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

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

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

@@ -226,7 +226,7 @@ protected:
*
* @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.

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

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

@@ -57,6 +57,12 @@ public:
static constexpr unsigned ERROR = PollGroup::ERROR;
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;
explicit SocketMonitor(EventLoop &_loop) noexcept

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

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

@@ -127,7 +127,7 @@ private:
int Recover(int err);
/* virtual methods from class MultiSocketMonitor */
std::chrono::steady_clock::duration PrepareSockets() noexcept override;
Event::Duration PrepareSockets() 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);
}
std::chrono::steady_clock::duration
Event::Duration
AlsaInputStream::PrepareSockets() noexcept
{
if (IsPaused()) {
ClearSocketList();
return std::chrono::steady_clock::duration(-1);
return Event::Duration(-1);
}
return non_block.PrepareSockets(*this, capture_handle);

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

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

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

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

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

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

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

@@ -0,0 +1 @@
no_sprintf_s

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

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

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

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

@@ -196,6 +196,15 @@ class AudioOutputControl {
*/
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
* means the PlayerThread does not need to wake up the

29
src/output/Error.hxx Normal file

@@ -0,0 +1,29 @@
/*
* 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_AUDIO_OUTPUT_ERROR_HXX
#define MPD_AUDIO_OUTPUT_ERROR_HXX
/**
* An exception class that will be thrown by various #AudioOutput
* methods after AudioOutput::Interrupt() has been called.
*/
class AudioOutputInterrupted {};
#endif

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

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

@@ -41,21 +41,21 @@ protected:
static constexpr unsigned FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT = 0x4;
public:
explicit AudioOutput(unsigned _flags):flags(_flags) {}
virtual ~AudioOutput() = default;
explicit AudioOutput(unsigned _flags) noexcept:flags(_flags) {}
virtual ~AudioOutput() noexcept = default;
AudioOutput(const AudioOutput &) = delete;
AudioOutput &operator=(const AudioOutput &) = delete;
bool SupportsEnableDisable() const {
bool SupportsEnableDisable() const noexcept {
return flags & FLAG_ENABLE_DISABLE;
}
bool SupportsPause() const {
bool SupportsPause() const noexcept {
return flags & FLAG_PAUSE;
}
bool GetNeedFullyDefinedAudioFormat() const {
bool GetNeedFullyDefinedAudioFormat() const noexcept {
return flags & FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT;
}
@@ -126,6 +126,24 @@ public:
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
* delay the next call to Play() or Pause(), which will happen
@@ -142,6 +160,11 @@ public:
/**
* Display metadata for the next chunk. Optional method,
* because not all devices can display metadata.
*
* Throws on error.
*
* May throw #AudioOutputInterrupted after Interrupt() has
* been called.
*/
virtual void SendTag(const Tag &) {}
@@ -151,6 +174,9 @@ public:
*
* Throws on error.
*
* May throw #AudioOutputInterrupted after Interrupt() has
* been called.
*
* @return the number of bytes played (must be a multiple of
* the frame size)
*/
@@ -177,6 +203,9 @@ public:
* disconnected. Plugins which do not support pausing will
* 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),
* true for continue to pause
*

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

@@ -25,6 +25,7 @@
#include "lib/alsa/PeriodBuffer.hxx"
#include "lib/alsa/Version.hxx"
#include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "mixer/MixerList.hxx"
#include "pcm/Export.hxx"
#include "system/PeriodClock.hxx"
@@ -118,7 +119,7 @@ class AlsaOutput final
*/
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
@@ -177,6 +178,15 @@ class AlsaOutput final
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.
* It contains silence samples, enough to fill one period (see
@@ -237,9 +247,12 @@ private:
void Open(AudioFormat &audio_format) override;
void Close() noexcept override;
void Interrupt() noexcept override;
size_t Play(const void *chunk, size_t size) override;
void Drain() override;
void Cancel() noexcept override;
bool Pause() noexcept override;
/**
* Set up the snd_pcm_t object which was opened by the caller.
@@ -385,7 +398,7 @@ private:
}
/* virtual methods from class MultiSocketMonitor */
std::chrono::steady_clock::duration PrepareSockets() noexcept override;
Event::Duration PrepareSockets() noexcept override;
void DispatchSockets() noexcept override;
};
@@ -728,6 +741,7 @@ AlsaOutput::Open(AudioFormat &audio_format)
out_frame_size = pcm_export->GetOutputFrameSize();
drain = false;
interrupted = false;
size_t period_size = period_frames * out_frame_size;
ring_buffer = new boost::lockfree::spsc_queue<uint8_t>(period_size * 4);
@@ -741,6 +755,18 @@ AlsaOutput::Open(AudioFormat &audio_format)
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
AlsaOutput::Recover(int err) noexcept
{
@@ -912,6 +938,11 @@ AlsaOutput::CancelInternal() noexcept
void
AlsaOutput::Cancel() noexcept
{
{
std::unique_lock<Mutex> lock(mutex);
interrupted = false;
}
if (!LockIsActive()) {
/* early cancel, quick code path without thread
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
AlsaOutput::Close() noexcept
{
@@ -956,6 +998,11 @@ AlsaOutput::LockWaitWriteAvailable()
if (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();
if (write_available >= min_available) {
/* reserve room for one extra block, just in
@@ -1005,12 +1052,12 @@ AlsaOutput::Play(const void *chunk, size_t size)
return size;
}
std::chrono::steady_clock::duration
Event::Duration
AlsaOutput::PrepareSockets() noexcept
{
if (!LockIsActiveAndNotWaiting()) {
ClearSocketList();
return std::chrono::steady_clock::duration(-1);
return Event::Duration(-1);
}
try {
@@ -1018,7 +1065,7 @@ AlsaOutput::PrepareSockets() noexcept
} catch (...) {
ClearSocketList();
LockCaughtError();
return std::chrono::steady_clock::duration(-1);
return Event::Duration(-1);
}
}

@@ -20,6 +20,7 @@
#include "config.h"
#include "JackOutputPlugin.hxx"
#include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "output/Features.h"
#include "thread/Mutex.hxx"
#include "util/ScopeExit.hxx"
@@ -79,6 +80,15 @@ class JackOutput final : public AudioOutput {
*/
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.
*/
@@ -156,6 +166,8 @@ public:
Stop();
}
void Interrupt() noexcept override;
std::chrono::steady_clock::duration Delay() const noexcept override {
return pause && !LockWasShutdown()
? std::chrono::seconds(1)
@@ -164,6 +176,7 @@ public:
size_t Play(const void *chunk, size_t size) override;
void Cancel() noexcept override;
bool Pause() override;
private:
@@ -613,9 +626,21 @@ JackOutput::Open(AudioFormat &new_audio_format)
new_audio_format.format = SampleFormat::FLOAT;
audio_format = new_audio_format;
interrupted = false;
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
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);
if (error)
std::rethrow_exception(error);
if (interrupted)
throw AudioOutputInterrupted{};
}
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
JackOutput::Pause()
{
{
const std::lock_guard<Mutex> lock(mutex);
interrupted = false;
if (error)
std::rethrow_exception(error);
}

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

@@ -22,6 +22,7 @@
#include "lib/pulse/LogError.hxx"
#include "lib/pulse/LockGuard.hxx"
#include "../OutputAPI.hxx"
#include "../Error.hxx"
#include "mixer/MixerList.hxx"
#include "mixer/plugins/PulseMixerPlugin.hxx"
#include "util/ScopeExit.hxx"
@@ -57,6 +58,15 @@ class PulseOutput final : AudioOutput {
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);
public:
@@ -99,6 +109,8 @@ public:
void Open(AudioFormat &audio_format) override;
void Close() noexcept override;
void Interrupt() noexcept override;
[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
size_t Play(const void *chunk, size_t size) override;
void Cancel() noexcept override;
@@ -677,6 +689,7 @@ PulseOutput::Open(AudioFormat &audio_format)
}
pause = false;
interrupted = false;
}
void
@@ -704,6 +717,21 @@ PulseOutput::Close() noexcept
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
PulseOutput::WaitStream()
{
@@ -719,6 +747,9 @@ PulseOutput::WaitStream()
"failed to connect the stream");
case PA_STREAM_CREATING:
if (interrupted)
throw AudioOutputInterrupted{};
pa_threaded_mainloop_wait(mainloop);
break;
}
@@ -784,6 +815,9 @@ PulseOutput::Play(const void *chunk, size_t size)
if (pa_stream_is_suspended(stream))
throw std::runtime_error("suspended");
if (interrupted)
throw AudioOutputInterrupted{};
pa_threaded_mainloop_wait(mainloop);
if (pa_stream_get_state(stream) != PA_STREAM_READY)
@@ -813,6 +847,7 @@ PulseOutput::Cancel() noexcept
assert(stream != nullptr);
Pulse::LockGuard lock(mainloop);
interrupted = false;
if (pa_stream_get_state(stream) != PA_STREAM_READY) {
/* no need to flush when the stream isn't connected
@@ -842,6 +877,7 @@ PulseOutput::Pause()
Pulse::LockGuard lock(mainloop);
pause = true;
interrupted = false;
/* check if the stream is (already/still) connected */

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

@@ -27,6 +27,8 @@
#include <string>
#include <memory>
struct StringView;
class CueParser {
enum {
/**
@@ -104,7 +106,7 @@ public:
* Feed a text line from the CUE file into the parser. Call
* 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
@@ -132,8 +134,6 @@ private:
* song's start time).
*/
void Commit() noexcept;
void Feed2(char *p) noexcept;
};
#endif

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

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

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

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