Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6c65cba58 | ||
|
|
f849b07766 | ||
|
|
2da3cff1e8 | ||
|
|
0c965d0573 | ||
|
|
77c14692c9 | ||
|
|
226eb26300 | ||
|
|
2d606fa989 | ||
|
|
7a0342c8bb | ||
|
|
42c9d765cf | ||
|
|
a8a80ee689 | ||
|
|
f9bdb4b0b8 | ||
|
|
9332527872 | ||
|
|
84f772357e | ||
|
|
f2b9785a67 | ||
|
|
eeaec99c59 | ||
|
|
b0002e3b73 | ||
|
|
27c589da97 | ||
|
|
6484af472b | ||
|
|
92a218b7a9 | ||
|
|
d69a1f98af | ||
|
|
23a6f62ea3 | ||
|
|
e0d3ca71b3 | ||
|
|
4f40b9f7cf | ||
|
|
bb009daf66 | ||
|
|
dc432f3ffa | ||
|
|
37710195ca | ||
|
|
7b9295ff99 | ||
|
|
5f61d440eb | ||
|
|
6bc73a9ebe | ||
|
|
1195eb266e | ||
|
|
3562a3e51e | ||
|
|
bbfa6fe632 | ||
|
|
bf97d13d0b | ||
|
|
b5673b6333 | ||
|
|
ee802867df | ||
|
|
ecaa51e322 | ||
|
|
0779333064 | ||
|
|
6f1a4a73b7 | ||
|
|
945ed2610a | ||
|
|
d7fcaf33b9 | ||
|
|
6a65b4c305 | ||
|
|
a163beee69 | ||
|
|
31268ad7cd | ||
|
|
a0d43dd87f | ||
|
|
1db533c8cf | ||
|
|
78ee663660 | ||
|
|
c32a809d38 | ||
|
|
1406144210 | ||
|
|
bb6ab67175 | ||
|
|
ed3d8222d6 | ||
|
|
41c0bbab13 | ||
|
|
eeb96eb367 | ||
|
|
ce93e58944 | ||
|
|
263b0ffdbb | ||
|
|
22bea5c97e | ||
|
|
75802ebcc6 | ||
|
|
27cc7b352d | ||
|
|
d64729065e | ||
|
|
ab318200db | ||
|
|
947856ca8e | ||
|
|
cd9ff9d9b0 | ||
|
|
4cd0f661d6 | ||
|
|
bf270a5663 | ||
|
|
6e893f40e3 | ||
|
|
7690905503 | ||
|
|
6f822a6f19 | ||
|
|
ca0179b2a9 | ||
|
|
6682cf749f | ||
|
|
492607ecbe | ||
|
|
e0c75da266 | ||
|
|
34bb53a29f | ||
|
|
cb4fdac469 | ||
|
|
ac46a84391 | ||
|
|
dffd5831f8 | ||
|
|
8358b34efa | ||
|
|
4484d7a5c2 | ||
|
|
b80a135cf3 | ||
|
|
4ad525d939 | ||
|
|
4cb5e69811 | ||
|
|
b0596291a8 | ||
|
|
8f0a1a5d82 | ||
|
|
c0775d328c | ||
|
|
4ca2c33181 | ||
|
|
362f391b76 | ||
|
|
980e32f69c | ||
|
|
dd639e18b8 | ||
|
|
c883f178b8 | ||
|
|
65d257675f |
32
NEWS
32
NEWS
@@ -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)
|
||||
* 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.3'
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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
|
||||
: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
|
||||
---
|
||||
|
||||
@@ -1224,23 +1229,25 @@ Playlist plugins
|
||||
asx
|
||||
---
|
||||
|
||||
Reads .asx playlist files.
|
||||
Reads :file:`.asx` playlist files.
|
||||
|
||||
.. _cue_playlist:
|
||||
|
||||
cue
|
||||
---
|
||||
Reads .cue files.
|
||||
Reads :file:`.cue` files.
|
||||
|
||||
embcue
|
||||
------
|
||||
Reads CUE sheets from the "CUESHEET" tag of song files.
|
||||
Reads CUE sheets from the ``CUESHEET`` tag of song files.
|
||||
|
||||
m3u
|
||||
---
|
||||
Reads .m3u playlist files.
|
||||
Reads :file:`.m3u` playlist files.
|
||||
|
||||
extm3u
|
||||
------
|
||||
Reads extended .m3u playlist files.
|
||||
Reads extended :file:`.m3u` playlist files.
|
||||
|
||||
flac
|
||||
----
|
||||
@@ -1248,11 +1255,11 @@ Reads the cuesheet metablock from a FLAC file.
|
||||
|
||||
pls
|
||||
---
|
||||
Reads .pls playlist files.
|
||||
Reads :file:`.pls` playlist files.
|
||||
|
||||
rss
|
||||
---
|
||||
Reads music links from .rss files.
|
||||
Reads music links from :file:`.rss` files.
|
||||
|
||||
soundcloud
|
||||
----------
|
||||
|
||||
67
doc/user.rst
67
doc/user.rst
@@ -86,7 +86,7 @@ For example, the following installs a fairly complete list of build dependencies
|
||||
libpulse-dev libshout3-dev \
|
||||
libsndio-dev \
|
||||
libmpdclient-dev \
|
||||
libnfs-dev libsmbclient-dev \
|
||||
libnfs-dev \
|
||||
libupnp-dev \
|
||||
libavahi-client-dev \
|
||||
libsqlite3-dev \
|
||||
@@ -413,7 +413,7 @@ The following table lists the audio_output options valid for all plugins:
|
||||
* - **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.
|
||||
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.
|
||||
* - **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.
|
||||
@@ -500,6 +500,11 @@ The following table lists the playlist_plugin options valid for all plugins:
|
||||
- The name of the plugin
|
||||
* - **enabled yes|no**
|
||||
- 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`
|
||||
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).
|
||||
|
||||
.. _realtime:
|
||||
|
||||
Real-Time Scheduling
|
||||
--------------------
|
||||
|
||||
@@ -1096,34 +1103,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.
|
||||
|
||||
|
||||
26
meson.build
26
meson.build
@@ -1,11 +1,11 @@
|
||||
project(
|
||||
'mpd',
|
||||
['c', 'cpp'],
|
||||
version: '0.22',
|
||||
version: '0.22.3',
|
||||
meson_version: '>= 0.49.0',
|
||||
default_options: [
|
||||
'c_std=c99',
|
||||
'build.c_std=c99',
|
||||
'c_std=c11',
|
||||
'build.c_std=c11',
|
||||
'cpp_std=c++17',
|
||||
'build.cpp_std=c++17',
|
||||
'warning_level=3',
|
||||
@@ -96,6 +96,11 @@ test_cflags = test_common_flags + [
|
||||
]
|
||||
|
||||
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'
|
||||
@@ -112,6 +117,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 +159,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'))
|
||||
@@ -219,7 +229,6 @@ log_dep = declare_dependency(
|
||||
sources = [
|
||||
version_cxx,
|
||||
'src/Main.cxx',
|
||||
'src/protocol/Ack.cxx',
|
||||
'src/protocol/ArgParser.cxx',
|
||||
'src/protocol/Result.cxx',
|
||||
'src/command/CommandError.cxx',
|
||||
@@ -504,6 +513,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 +554,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
|
||||
#
|
||||
@@ -87,7 +92,11 @@ option('cdio_paranoia', type: 'feature', description: 'libcdio_paranoia input pl
|
||||
option('curl', type: 'feature', description: 'HTTP client using CURL')
|
||||
option('mms', type: 'feature', description: 'MMS protocol support using libmms')
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
@@ -535,6 +533,9 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
|
||||
|
||||
/* cleanup */
|
||||
|
||||
if (instance.state_file)
|
||||
instance.state_file->Write();
|
||||
|
||||
instance.BeginShutdownUpdate();
|
||||
|
||||
ZeroconfDeinit();
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "db/plugins/simple/Directory.hxx"
|
||||
#include "storage/StorageInterface.hxx"
|
||||
#include "storage/FileInfo.hxx"
|
||||
#include "decoder/DecoderList.hxx"
|
||||
#include "fs/AllocatedPath.hxx"
|
||||
#include "fs/FileInfo.hxx"
|
||||
#include "tag/Builder.hxx"
|
||||
@@ -40,6 +41,14 @@
|
||||
|
||||
#ifdef ENABLE_DATABASE
|
||||
|
||||
bool
|
||||
Song::IsPluginAvailable() const noexcept
|
||||
{
|
||||
const char *suffix = GetFilenameSuffix();
|
||||
return suffix != nullptr &&
|
||||
decoder_plugins_supports_suffix(suffix);
|
||||
}
|
||||
|
||||
SongPtr
|
||||
Song::LoadFile(Storage &storage, const char *path_utf8, Directory &parent)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
@@ -31,6 +31,7 @@ db_glue_sources = [
|
||||
'update/Remove.cxx',
|
||||
'update/ExcludeList.cxx',
|
||||
'update/VirtualDirectory.cxx',
|
||||
'update/SpecialDirectory.cxx',
|
||||
'DatabaseGlue.cxx',
|
||||
'Configured.cxx',
|
||||
'DatabaseSong.cxx',
|
||||
|
||||
@@ -132,6 +132,14 @@ public:
|
||||
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
|
||||
* must not be called with the root Directory.
|
||||
|
||||
@@ -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
|
||||
Song::GetURI() const noexcept
|
||||
{
|
||||
|
||||
@@ -108,6 +108,16 @@ struct Song {
|
||||
|
||||
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
|
||||
* load its metadata. If all decoder plugin fail to read its meta
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,7 +95,7 @@ UpdateWalk::UpdatePlaylistFile(Directory &directory,
|
||||
if (plugin == nullptr)
|
||||
return false;
|
||||
|
||||
if (plugin->as_folder)
|
||||
if (GetPlaylistPluginAsFolder(*plugin))
|
||||
UpdatePlaylistFile(directory, name, info, *plugin);
|
||||
|
||||
PlaylistInfo pi(name, info.mtime);
|
||||
|
||||
83
src/db/update/SpecialDirectory.cxx
Normal file
83
src/db/update/SpecialDirectory.cxx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,17 @@ inline void
|
||||
UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept
|
||||
{
|
||||
directory.ForEachChildSafe([&](Directory &child){
|
||||
if (child.IsMount() || DirectoryExists(storage, child))
|
||||
if (child.IsMount())
|
||||
/* mount points are always preserved */
|
||||
return;
|
||||
|
||||
if (DirectoryExists(storage, child) &&
|
||||
child.IsPluginAvailable())
|
||||
return;
|
||||
|
||||
/* the directory was deleted (or the plugin which
|
||||
handles this "virtual" directory is unavailable) */
|
||||
|
||||
editor.LockDeleteDirectory(&child);
|
||||
|
||||
modified = true;
|
||||
@@ -103,7 +111,11 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept
|
||||
|
||||
directory.ForEachSongSafe([&](Song &song){
|
||||
if (!directory_child_is_regular(storage, directory,
|
||||
song.filename)) {
|
||||
song.filename) ||
|
||||
!song.IsPluginAvailable()) {
|
||||
/* the song file was deleted (or the decoder
|
||||
plugin is unavailable) */
|
||||
|
||||
editor.LockDeleteSong(directory, &song);
|
||||
|
||||
modified = true;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "lib/ffmpeg/Domain.hxx"
|
||||
#include "lib/ffmpeg/Error.hxx"
|
||||
#include "lib/ffmpeg/Init.hxx"
|
||||
#include "lib/ffmpeg/Interleave.hxx"
|
||||
#include "lib/ffmpeg/Buffer.hxx"
|
||||
#include "lib/ffmpeg/Frame.hxx"
|
||||
#include "lib/ffmpeg/Format.hxx"
|
||||
@@ -176,48 +177,6 @@ start_time_fallback(const AVStream &stream)
|
||||
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
|
||||
* AVStream::time_base units). Returns a negative value on error.
|
||||
@@ -258,7 +217,7 @@ FfmpegSendFrame(DecoderClient &client, InputStream *is,
|
||||
FfmpegBuffer &buffer)
|
||||
{
|
||||
ConstBuffer<void> output_buffer =
|
||||
copy_interleave_frame(codec_context, frame, buffer);
|
||||
Ffmpeg::InterleaveFrame(frame, buffer);
|
||||
|
||||
if (skip_bytes > 0) {
|
||||
if (skip_bytes >= output_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;
|
||||
|
||||
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
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 {
|
||||
|
||||
@@ -20,19 +20,24 @@
|
||||
#include "LoadChain.hxx"
|
||||
#include "Factory.hxx"
|
||||
#include "Prepared.hxx"
|
||||
#include "plugins/AutoConvertFilterPlugin.hxx"
|
||||
#include "plugins/ChainFilterPlugin.hxx"
|
||||
#include "util/IterableSplitString.hxx"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static void
|
||||
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,
|
||||
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
|
||||
@@ -40,18 +45,10 @@ filter_chain_parse(PreparedFilter &chain,
|
||||
FilterFactory &factory,
|
||||
const char *spec)
|
||||
{
|
||||
const char *const end = spec + strlen(spec);
|
||||
for (const std::string_view i : IterableSplitString(spec, ',')) {
|
||||
if (i.empty())
|
||||
continue;
|
||||
|
||||
while (true) {
|
||||
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;
|
||||
filter_chain_append_new(chain, factory, i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "AutoConvertFilterPlugin.hxx"
|
||||
#include "ConvertFilterPlugin.hxx"
|
||||
#include "TwoFilters.hxx"
|
||||
#include "filter/Filter.hxx"
|
||||
#include "filter/Prepared.hxx"
|
||||
#include "pcm/AudioFormat.hxx"
|
||||
@@ -27,35 +28,6 @@
|
||||
#include <cassert>
|
||||
#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 {
|
||||
/**
|
||||
* The underlying filter.
|
||||
@@ -81,37 +53,17 @@ PreparedAutoConvertFilter::Open(AudioFormat &in_audio_format)
|
||||
|
||||
/* need to convert? */
|
||||
|
||||
std::unique_ptr<Filter> convert;
|
||||
if (in_audio_format != child_audio_format) {
|
||||
if (in_audio_format == child_audio_format)
|
||||
/* no */
|
||||
return new_filter;
|
||||
|
||||
/* yes - create a convert_filter */
|
||||
|
||||
convert.reset(convert_filter_new(in_audio_format,
|
||||
child_audio_format));
|
||||
}
|
||||
auto convert = convert_filter_new(in_audio_format,
|
||||
child_audio_format);
|
||||
|
||||
return std::make_unique<AutoConvertFilter>(std::move(new_filter),
|
||||
std::move(convert));
|
||||
}
|
||||
|
||||
ConstBuffer<void>
|
||||
AutoConvertFilter::FilterPCM(ConstBuffer<void> src)
|
||||
{
|
||||
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();
|
||||
return std::make_unique<TwoFilters>(std::move(convert),
|
||||
std::move(new_filter));
|
||||
}
|
||||
|
||||
std::unique_ptr<PreparedFilter>
|
||||
|
||||
@@ -28,15 +28,14 @@
|
||||
#include <cassert>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class ChainFilter final : public Filter {
|
||||
struct Child {
|
||||
const char *name;
|
||||
std::unique_ptr<Filter> filter;
|
||||
|
||||
Child(const char *_name,
|
||||
std::unique_ptr<Filter> _filter) noexcept
|
||||
:name(_name), filter(std::move(_filter)) {}
|
||||
explicit Child(std::unique_ptr<Filter> &&_filter) noexcept
|
||||
:filter(std::move(_filter)) {}
|
||||
};
|
||||
|
||||
std::list<Child> children;
|
||||
@@ -50,13 +49,12 @@ public:
|
||||
explicit ChainFilter(AudioFormat _audio_format)
|
||||
:Filter(_audio_format) {}
|
||||
|
||||
void Append(const char *name,
|
||||
std::unique_ptr<Filter> filter) noexcept {
|
||||
void Append(std::unique_ptr<Filter> filter) noexcept {
|
||||
assert(out_audio_format.IsValid());
|
||||
out_audio_format = filter->GetOutAudioFormat();
|
||||
assert(out_audio_format.IsValid());
|
||||
|
||||
children.emplace_back(name, std::move(filter));
|
||||
children.emplace_back(std::move(filter));
|
||||
|
||||
RewindFlush();
|
||||
}
|
||||
@@ -75,10 +73,10 @@ private:
|
||||
|
||||
class PreparedChainFilter final : public PreparedFilter {
|
||||
struct Child {
|
||||
const char *name;
|
||||
const std::string name;
|
||||
std::unique_ptr<PreparedFilter> filter;
|
||||
|
||||
Child(const char *_name,
|
||||
Child(std::string_view _name,
|
||||
std::unique_ptr<PreparedFilter> _filter)
|
||||
:name(_name), filter(std::move(_filter)) {}
|
||||
|
||||
@@ -91,7 +89,7 @@ class PreparedChainFilter final : public PreparedFilter {
|
||||
std::list<Child> children;
|
||||
|
||||
public:
|
||||
void Append(const char *name,
|
||||
void Append(std::string_view name,
|
||||
std::unique_ptr<PreparedFilter> filter) noexcept {
|
||||
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)
|
||||
throw FormatRuntimeError("Audio format not supported by filter '%s': %s",
|
||||
name,
|
||||
name.c_str(),
|
||||
ToString(prev_audio_format).c_str());
|
||||
|
||||
return new_filter;
|
||||
@@ -121,7 +119,7 @@ PreparedChainFilter::Open(AudioFormat &in_audio_format)
|
||||
|
||||
for (auto &child : children) {
|
||||
AudioFormat audio_format = chain->GetOutAudioFormat();
|
||||
chain->Append(child.name, child.Open(audio_format));
|
||||
chain->Append(child.Open(audio_format));
|
||||
}
|
||||
|
||||
return chain;
|
||||
@@ -177,7 +175,7 @@ filter_chain_new() noexcept
|
||||
}
|
||||
|
||||
void
|
||||
filter_chain_append(PreparedFilter &_chain, const char *name,
|
||||
filter_chain_append(PreparedFilter &_chain, std::string_view name,
|
||||
std::unique_ptr<PreparedFilter> filter) noexcept
|
||||
{
|
||||
auto &chain = (PreparedChainFilter &)_chain;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#define MPD_FILTER_CHAIN_HXX
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
class PreparedFilter;
|
||||
|
||||
@@ -45,7 +46,7 @@ filter_chain_new() noexcept;
|
||||
* @param filter the filter to be appended to #chain
|
||||
*/
|
||||
void
|
||||
filter_chain_append(PreparedFilter &chain, const char *name,
|
||||
filter_chain_append(PreparedFilter &chain, std::string_view name,
|
||||
std::unique_ptr<PreparedFilter> filter) noexcept;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -117,13 +117,13 @@ convert_filter_prepare() noexcept
|
||||
return std::make_unique<PreparedConvertFilter>();
|
||||
}
|
||||
|
||||
Filter *
|
||||
std::unique_ptr<Filter>
|
||||
convert_filter_new(const AudioFormat in_audio_format,
|
||||
const AudioFormat out_audio_format)
|
||||
{
|
||||
std::unique_ptr<ConvertFilter> filter(new ConvertFilter(in_audio_format));
|
||||
filter->Set(out_audio_format);
|
||||
return filter.release();
|
||||
return filter;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -29,7 +29,7 @@ struct AudioFormat;
|
||||
std::unique_ptr<PreparedFilter>
|
||||
convert_filter_prepare() noexcept;
|
||||
|
||||
Filter *
|
||||
std::unique_ptr<Filter>
|
||||
convert_filter_new(AudioFormat in_audio_format,
|
||||
AudioFormat out_audio_format);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "FfmpegFilter.hxx"
|
||||
#include "lib/ffmpeg/Interleave.hxx"
|
||||
#include "lib/ffmpeg/SampleFormat.hxx"
|
||||
#include "util/ConstBuffer.hxx"
|
||||
|
||||
@@ -79,5 +80,5 @@ FfmpegFilter::FilterPCM(ConstBuffer<void> src)
|
||||
/* TODO: call av_buffersink_get_frame() repeatedly? Not
|
||||
possible with MPD's current Filter API */
|
||||
|
||||
return {frame.GetData(0), frame->nb_samples * GetOutAudioFormat().GetFrameSize()};
|
||||
return Ffmpeg::InterleaveFrame(*frame, interleave_buffer);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#define MPD_FFMPEG_FILTER__HXX
|
||||
|
||||
#include "filter/Filter.hxx"
|
||||
#include "lib/ffmpeg/Buffer.hxx"
|
||||
#include "lib/ffmpeg/Filter.hxx"
|
||||
#include "lib/ffmpeg/Frame.hxx"
|
||||
|
||||
@@ -32,6 +33,8 @@ class FfmpegFilter final : public Filter {
|
||||
Ffmpeg::FilterContext buffer_src, buffer_sink;
|
||||
Ffmpeg::Frame frame;
|
||||
|
||||
FfmpegBuffer interleave_buffer;
|
||||
|
||||
const int in_format, in_sample_rate, in_channels;
|
||||
|
||||
const size_t in_audio_frame_size;
|
||||
|
||||
39
src/filter/plugins/TwoFilters.cxx
Normal file
39
src/filter/plugins/TwoFilters.cxx
Normal 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();
|
||||
}
|
||||
49
src/filter/plugins/TwoFilters.hxx
Normal file
49
src/filter/plugins/TwoFilters.hxx
Normal 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
|
||||
@@ -14,6 +14,7 @@ filter_plugins = static_library(
|
||||
'filter_plugins',
|
||||
'../../AudioCompress/compress.c',
|
||||
'NullFilterPlugin.cxx',
|
||||
'TwoFilters.cxx',
|
||||
'ChainFilterPlugin.cxx',
|
||||
'AutoConvertFilterPlugin.cxx',
|
||||
'ConvertFilterPlugin.cxx',
|
||||
|
||||
@@ -88,6 +88,19 @@ struct PathTraitsFS {
|
||||
#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
|
||||
gcc_pure gcc_nonnull_all
|
||||
static constexpr bool IsDrive(const_pointer p) noexcept {
|
||||
@@ -199,6 +212,19 @@ struct PathTraitsUTF8 {
|
||||
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
|
||||
gcc_pure gcc_nonnull_all
|
||||
static constexpr bool IsDrive(const_pointer p) 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,7 +38,7 @@ public:
|
||||
/**
|
||||
* Throws on error.
|
||||
*/
|
||||
std::chrono::steady_clock::duration PrepareSockets(MultiSocketMonitor &m,
|
||||
Event::Duration PrepareSockets(MultiSocketMonitor &m,
|
||||
snd_pcm_t *pcm);
|
||||
|
||||
/**
|
||||
@@ -59,7 +58,7 @@ class AlsaNonBlockMixer {
|
||||
ReusableArray<pollfd> pfd_buffer;
|
||||
|
||||
public:
|
||||
std::chrono::steady_clock::duration PrepareSockets(MultiSocketMonitor &m,
|
||||
Event::Duration PrepareSockets(MultiSocketMonitor &m,
|
||||
snd_mixer_t *mixer) noexcept;
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
#include "Watch.hxx"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace ODBus {
|
||||
|
||||
WatchManager::Watch::Watch(EventLoop &event_loop,
|
||||
|
||||
@@ -20,36 +20,36 @@
|
||||
#ifndef MPD_FFMPEG_BUFFER_HXX
|
||||
#define MPD_FFMPEG_BUFFER_HXX
|
||||
|
||||
#include "util/Compiler.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavutil/mem.h>
|
||||
}
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
/* suppress the ffmpeg compatibility macro */
|
||||
#ifdef SampleFormat
|
||||
#undef SampleFormat
|
||||
#endif
|
||||
|
||||
class FfmpegBuffer {
|
||||
void *data;
|
||||
unsigned size;
|
||||
void *data = nullptr;
|
||||
unsigned size = 0;
|
||||
|
||||
public:
|
||||
FfmpegBuffer():data(nullptr), size(0) {}
|
||||
FfmpegBuffer() noexcept = default;
|
||||
|
||||
~FfmpegBuffer() {
|
||||
~FfmpegBuffer() noexcept {
|
||||
av_free(data);
|
||||
}
|
||||
|
||||
FfmpegBuffer(const FfmpegBuffer &) = delete;
|
||||
FfmpegBuffer &operator=(const FfmpegBuffer &) = delete;
|
||||
|
||||
gcc_malloc
|
||||
void *Get(size_t min_size) {
|
||||
void *Get(size_t min_size) noexcept {
|
||||
av_fast_malloc(&data, &size, min_size);
|
||||
return data;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *GetT(size_t n) {
|
||||
T *GetT(size_t n) noexcept {
|
||||
return (T *)Get(n * sizeof(T));
|
||||
}
|
||||
};
|
||||
|
||||
71
src/lib/ffmpeg/Interleave.cxx
Normal file
71
src/lib/ffmpeg/Interleave.cxx
Normal 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
|
||||
40
src/lib/ffmpeg/Interleave.hxx
Normal file
40
src/lib/ffmpeg/Interleave.hxx
Normal 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
|
||||
@@ -31,11 +31,6 @@ extern "C" {
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
|
||||
/* suppress the ffmpeg compatibility macro */
|
||||
#ifdef SampleFormat
|
||||
#undef SampleFormat
|
||||
#endif
|
||||
|
||||
/* redefine AV_TIME_BASE_Q because libavutil's macro definition is a
|
||||
compound literal, which is illegal in C++ */
|
||||
#ifdef AV_TIME_BASE_Q
|
||||
|
||||
@@ -26,6 +26,7 @@ endif
|
||||
ffmpeg = static_library(
|
||||
'ffmpeg',
|
||||
'Init.cxx',
|
||||
'Interleave.cxx',
|
||||
'LogError.cxx',
|
||||
'LogCallback.cxx',
|
||||
'Error.cxx',
|
||||
|
||||
@@ -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
|
||||
|
||||
14
src/lib/nfs/patches/no_sprintf_s
Normal file
14
src/lib/nfs/patches/no_sprintf_s
Normal 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
|
||||
1
src/lib/nfs/patches/series
Normal file
1
src/lib/nfs/patches/series
Normal file
@@ -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;
|
||||
|
||||
67
src/lib/yajl/Handle.cxx
Normal file
67
src/lib/yajl/Handle.cxx
Normal 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
|
||||
@@ -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
|
||||
* modification, are permitted provided that the following conditions
|
||||
@@ -30,12 +30,8 @@
|
||||
#ifndef YAJL_HANDLE_HXX
|
||||
#define YAJL_HANDLE_HXX
|
||||
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/ScopeExit.hxx"
|
||||
|
||||
#include <yajl/yajl_parse.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Yajl {
|
||||
@@ -77,15 +73,12 @@ public:
|
||||
|
||||
private:
|
||||
void HandleStatus(yajl_status status) {
|
||||
if (status == yajl_status_error) {
|
||||
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", str);
|
||||
}
|
||||
if (status == yajl_status_error)
|
||||
ThrowError();
|
||||
}
|
||||
|
||||
[[noreturn]]
|
||||
void ThrowError();
|
||||
};
|
||||
|
||||
} // namespace Yajl
|
||||
|
||||
@@ -5,6 +5,7 @@ endif
|
||||
|
||||
yajl = static_library(
|
||||
'yajl',
|
||||
'Handle.cxx',
|
||||
'ResponseParser.cxx',
|
||||
'ParseInputStream.cxx',
|
||||
include_directories: inc,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,13 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "Ack.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#ifndef MPD_AUDIO_OUTPUT_ERROR_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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ static constexpr unsigned n_playlist_plugins =
|
||||
/** which plugins have been initialized successfully? */
|
||||
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) \
|
||||
playlist_plugins_for_each(plugin) \
|
||||
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_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);
|
||||
}
|
||||
|
||||
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>
|
||||
playlist_list_open_uri_scheme(const char *uri, Mutex &mutex,
|
||||
bool *tried)
|
||||
@@ -207,11 +224,7 @@ gcc_pure
|
||||
static StringView
|
||||
ExtractMimeTypeMainPart(StringView s) noexcept
|
||||
{
|
||||
const auto separator = s.Find(';');
|
||||
if (separator != nullptr)
|
||||
s.SetEnd(separator);
|
||||
|
||||
return s;
|
||||
return s.Split(';').first;
|
||||
}
|
||||
|
||||
static std::unique_ptr<SongEnumerator>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
StringView value(src.data, end);
|
||||
src = src.substr(end + 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
*end = 0;
|
||||
*pp = end + 1;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if (current != nullptr)
|
||||
current->SetStartTime(SongTime::FromMS(position_ms));
|
||||
if(strcmp(nr, "00") != 0 || previous == nullptr)
|
||||
|
||||
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;
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
class Domain;
|
||||
|
||||
enum ack {
|
||||
ACK_ERROR_NOT_LIST = 1,
|
||||
ACK_ERROR_ARG = 2,
|
||||
@@ -43,8 +41,6 @@ enum ack {
|
||||
ACK_ERROR_EXIST = 56,
|
||||
};
|
||||
|
||||
extern const Domain ack_domain;
|
||||
|
||||
class ProtocolError : public std::runtime_error {
|
||||
enum ack code;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ std::string_view
|
||||
CurlStorage::MapToRelativeUTF8(std::string_view uri_utf8) const noexcept
|
||||
{
|
||||
return PathTraitsUTF8::Relative(base,
|
||||
CurlUnescape(uri_utf8).c_str());
|
||||
CurlUnescape(uri_utf8));
|
||||
}
|
||||
|
||||
class BlockingHttpRequest : protected CurlResponseHandler {
|
||||
|
||||
@@ -27,16 +27,14 @@
|
||||
template<typename T> class AllocatedString;
|
||||
|
||||
/**
|
||||
* Format into a newly allocated string. The caller frees the return
|
||||
* value with delete[].
|
||||
* Format into an #AllocatedString.
|
||||
*/
|
||||
gcc_nonnull_all
|
||||
AllocatedString<char>
|
||||
FormatStringV(const char *fmt, std::va_list args) noexcept;
|
||||
|
||||
/**
|
||||
* Format into a newly allocated string. The caller frees the return
|
||||
* value with delete[].
|
||||
* Format into an #AllocatedString.
|
||||
*/
|
||||
gcc_nonnull(1) gcc_printf(1,2)
|
||||
AllocatedString<char>
|
||||
|
||||
24
test/fuzzer/FuzzCueParser.cxx
Normal file
24
test/fuzzer/FuzzCueParser.cxx
Normal 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
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,
|
||||
],
|
||||
)
|
||||
@@ -264,7 +264,6 @@ if enable_database
|
||||
executable(
|
||||
'DumpDatabase',
|
||||
'DumpDatabase.cxx',
|
||||
'../src/protocol/Ack.cxx',
|
||||
'../src/db/Registry.cxx',
|
||||
'../src/db/Selection.cxx',
|
||||
'../src/db/PlaylistVector.cxx',
|
||||
|
||||
Reference in New Issue
Block a user