Compare commits

...

65 Commits

Author SHA1 Message Date
Max Kellermann
ae19bda1f2 release v0.21.12 2019-08-03 12:48:20 +02:00
Max Kellermann
f2d8fd769d player/Thread: don't restart unseekable song after failed seek attempt
The check IsSeekableCurrentSong() was added by commit
44b200240f in version 0.20.19, but it
caused a regression: by doing the branch only if the current song is
seekable, the player would restart the current song if it was not
seekable, and later the initial seek would fail; but we already know
it's not seekable, and so we should fail early.
2019-08-03 12:30:10 +02:00
Max Kellermann
9661062ae2 decoder/mad: pass const reference to RecoverFrameError() 2019-08-03 11:59:41 +02:00
Max Kellermann
2a07354cad decoder/mad: change integers to size_t 2019-08-03 11:44:02 +02:00
Max Kellermann
fc18fd571c decoder/mad: return from SynthAndSubmit() early 2019-08-03 11:42:05 +02:00
Max Kellermann
51abed9732 decoder/mad: pass mad_pcm to mad_fixed_to_24_buffer() 2019-08-03 11:40:06 +02:00
Max Kellermann
d00afc912c decoder/mad: eliminate the loop in SubmitPCM()
libmad has a hard-coded maximum PCM buffer size; if we make our
output_buffer just as large, we can avoid the loop, because any
possible size will fit.
2019-08-03 11:36:05 +02:00
Max Kellermann
9d0fe725eb decoder/mad: rename a few misnamed methods 2019-08-03 11:32:42 +02:00
Max Kellermann
8a432c9b7f decoder/mad: move code to LoadNextFrame() 2019-08-03 11:32:06 +02:00
Max Kellermann
187204f03c decoder/mad: move code to HandleCurrentFrame() 2019-08-03 11:32:06 +02:00
Max Kellermann
5e5fadb5f2 decoder/mad: remove unnecessary initializers
These will not be used until they are initialized in SyncAndSend().
2019-08-03 08:49:26 +02:00
Max Kellermann
952c793235 decoder/mad: subtract libmad decoder delay from LAME encoder padding
Apparently, libmad not only inserts 529 samples of silence at the
beginning of the file, but also removes them at the end.

This solves the last piece of
https://github.com/MusicPlayerDaemon/MPD/issues/601

Closes https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-03 08:35:00 +02:00
Max Kellermann
3e3d8c7f9d decoder/mad: pad the input buffer with zero bytes and end of file
libmad requires padding the input buffer with "MAD_BUFFER_GUARD" zero
bytes at the end of the file, or else it is unable to decode the last
frame.

This fixes yet another bug which prevented this plugin from decoding
the last frame, see
https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-03 08:32:27 +02:00
Max Kellermann
9b99a9897a decoder/mad: don't count the Xing/LAME metadata frame
The Xing/LAME frame indicates how many frames there are, but that
excludes the initial Xing/LAME frame.  Therefore, it should not be
counted.

This fixes an off-by-one bug which caused the last frame to be
skipped, fixing one part of
https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-03 08:25:48 +02:00
Max Kellermann
4f56fdc397 decoder/mad: make "current_frame" zero-based
Increment "current_frame" after processing the frame.
2019-08-03 08:24:25 +02:00
Max Kellermann
c87d6825ec decoder/mad: add API documentation 2019-08-03 08:07:30 +02:00
Max Kellermann
00830a20e3 decoder/mad: convert to class, make almost everything private 2019-08-03 07:52:51 +02:00
Max Kellermann
d39d2874b4 decoder/mad: move code to methods RunDecoder(), RunScan() 2019-08-03 07:49:41 +02:00
Max Kellermann
a0a74951b8 decoder/mad: eliminate attribute "bit_rate"
This also fixes a bug which caused the bit rate to not update after
seeking.
2019-08-03 00:38:45 +02:00
Max Kellermann
779a6855ff decoder/mad: add noexcept 2019-08-03 00:28:59 +02:00
Max Kellermann
f7ed7446ae decoder/mad: use MAD_F_MIN and MAD_F_MAX 2019-08-03 00:27:59 +02:00
Max Kellermann
9d44a6d2ae decoder/mad: use Clamp() 2019-08-03 00:26:57 +02:00
Max Kellermann
10da9ee7ba decoder/mad: refactor local variables in FillBuffer() 2019-08-02 23:19:11 +02:00
Max Kellermann
f9eff31205 decoder/mad: use sizeof(input_buffer) 2019-08-02 23:19:11 +02:00
Max Kellermann
1d74a029a2 decoder/mad: simplify variable initialization in FillBuffer() 2019-08-02 23:19:11 +02:00
Max Kellermann
6b8ca514bb decoder/mad: fix broken log message
Broken since commit f8bfea8bae
2019-08-02 22:58:16 +02:00
Max Kellermann
f51e555154 decoder/mad: change "mp3_" suffix to "mad_" 2019-08-02 22:49:55 +02:00
Max Kellermann
61a3c69a06 decoder/mad: make enums strictly-typed 2019-08-02 22:49:55 +02:00
Max Kellermann
089615a01e decoder/mad: include cleanup 2019-08-02 22:49:55 +02:00
Max Kellermann
52bee8f81f util/StaticFifoBuffer: add GetAvailable() 2019-08-02 22:49:55 +02:00
Max Kellermann
adc25e648f util/StaticFifoBuffer: add constexpr 2019-08-02 22:49:33 +02:00
Max Kellermann
31da8eac9b util/StaticFifoBuffer: add noexcept 2019-08-02 22:49:05 +02:00
Max Kellermann
e00464435b util/Compiler.h: move compiler version checks to meson.build 2019-08-02 15:53:16 +02:00
Diomendius
b81138bda1 Fix JACK plugin outputting only to left channel
The JACK output plugin would not correctly upmix mono input files when exactly 2 output ports were configured. This fixes that.
2019-08-02 15:52:20 +02:00
Max Kellermann
6de088140b lib/xiph/OggVisitor: invoke OnOggPacket() with the "E_O_S" packet
The "end of stream" packet is not special; it contains normal data,
and thus we should pass it to OnOggPacket().

This fixes one part of https://github.com/MusicPlayerDaemon/MPD/issues/601
2019-08-02 14:04:08 +02:00
Max Kellermann
86d0534638 lib/xiph/OggVisitor: more API documentation 2019-08-02 13:56:00 +02:00
Max Kellermann
1033dbca2b playlist/Song: add missing includes 2019-07-29 11:31:30 +02:00
Max Kellermann
b955334882 decoder/opus: ignore case in replay gain tag names
Closes https://github.com/MusicPlayerDaemon/MPD/issues/604
2019-07-29 10:40:37 +02:00
Max Kellermann
90ea3bf985 playlist/Song: support backslash in relative URIs
Closes https://github.com/MusicPlayerDaemon/MPD/issues/607
2019-07-29 09:58:53 +02:00
Max Kellermann
83b0871248 test/test_translate_song: remove unused variable "s1" 2019-07-29 09:52:57 +02:00
Max Kellermann
d8aec4b2dc test/run_decoder: catch StopDecoder
This exception is usually thrown by class DecoderBridge, but the Opus
plugin (ab)uses it as well, so we need to catch it.
2019-07-12 17:49:12 +02:00
Max Kellermann
39b302dcad increment version number to 0.21.12 2019-07-12 17:22:20 +02:00
Max Kellermann
f6125f0c35 release v0.21.11 2019-07-03 15:16:27 +02:00
Max Kellermann
f780ac418a output/alsa: log when generating silence due to slow decoder
MPD used to do that when this code lived in the player thread, but it
was removed by commit 98a7c62d7a4f716d90af6d78e18d1a3b10bc54b3; and
the replacement code in the ALSA output plugin didn't have it.
2019-06-28 18:15:30 +02:00
Max Kellermann
61a72a5d13 output/alsa: schedule a timer to generate silence
Without this timer, DispatchSockets() may disable the
MultiSocketMonitor and if Play() doesn't get called soon, it never
gets a chance to generate silence.  However if Play() gets called,
generating silence isn't necessary anymore...

Resulting from this misdesign (added by commit ccafe3f3cf in 0.21.3),
the silence generator didn't work reliably.
2019-06-28 18:04:49 +02:00
Max Kellermann
0c0a354753 output/alsa: add a new flag "waiting" for xrun management
In DispatchSockets(), when there was not enough data, but enough for
current playback, the method would disable the "active" flag so the
next Play() call would re-enable the MultiSocketMonitor.

This was an abuse of the flag which could result in a crash
in Cancel(), because that method asserts that the period_buffer is
empty, which it may be not.

The solution is to add anther flag called "waiting" which shares some
behavior with the old flag.
2019-06-28 18:04:49 +02:00
Max Kellermann
3c5f860fb8 output/alsa: Cancel() also affects "active" (documentation) 2019-06-28 18:04:49 +02:00
Max Kellermann
3da1fa88d0 output/alsa: fix comment typo 2019-06-28 18:04:49 +02:00
Max Kellermann
fac15aaffb output/alsa: fix comment typo 2019-06-28 14:39:54 +02:00
Max Kellermann
c926021599 output/alsa: always redo DrainInternal() after writing
Draining isn't finished just because the period_buffer has run empty.
It is only finished after snd_pcm_drain() has succeeded.
2019-06-28 09:10:16 +02:00
Max Kellermann
543776d9c9 output/alsa: check PCM state before calling snd_pcm_drain()
Apparently, if snd_pcm_drain() returns EAGAIN, it does not actually
want to be called again; the next call will snd_pcm_drain() will also
return EAGAIN, forever, even though the PCM state has meanwhile
switched to SND_PCM_STATE_SETUP.  This causes a busy loop; to fix
this, we should always check snd_pcm_state() to see if draining is
really required.
2019-06-28 08:55:25 +02:00
Max Kellermann
8bf3f9b874 input/tidal: deprecated because Tidal has changed the protocol
See https://github.com/MusicPlayerDaemon/MPD/issues/545
2019-06-26 23:14:07 +02:00
Max Kellermann
f07f8f7d88 decoder/wildmidi: add fallbacks for libwildmidi<0.4
Fix build breakage from commit ea639269d8
2019-06-26 23:13:23 +02:00
Max Kellermann
39b40ac1fd decoder/wildmidi: remove unused variable wildmidi_domain 2019-06-26 23:10:20 +02:00
Max Kellermann
ea639269d8 decoder/wildmidi: throw PluginUnavailable on WildMidi_Init() error
Closes https://github.com/MusicPlayerDaemon/MPD/issues/589
2019-06-26 22:40:27 +02:00
Max Kellermann
0abaa3ecc5 decoder/wildmidi: throw PluginUnavailable if config file does not exist
This makes the configuration error more visible, possibly addressing
one part of https://github.com/MusicPlayerDaemon/MPD/issues/589
2019-06-26 22:38:40 +02:00
Max Kellermann
c4d3efe71d decoder/List: handle exception PluginUnavailable 2019-06-26 22:02:54 +02:00
Max Kellermann
85e82e3d4d decoder/List: annotate exceptions thrown by DecoderPlugin::Init() 2019-06-26 22:01:45 +02:00
Max Kellermann
f44011519c meson.build: increase protocol version to 0.21.11
Commit 1eae9339f2 added support for
multiple "groups" in the "list" command, and this change allows
clients to detect that this behavior, which had been documented for
several years, is now implemented properly.
2019-06-18 15:35:38 +02:00
Max Kellermann
2c3eeb7194 MusicChunk: pad MusicChunkInfo to a multiple of 8 bytes
Workaround for a regression caused by commit
a06bf388d9, revealing a problem with
discarding odd numer of frames in the DSD_U32 and DoP converters,
causing distortions with DSD_U32 and DoP on 32 bit CPUs.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/469
2019-06-17 21:24:32 +02:00
Max Kellermann
79839db3a3 output/oss: return early if PcmExport::Export() returns empty array
This can happen if the DoP converter doesn't get enough source samples
for one destination quad.  This isn't a critical bug, because the OSS
plugin doesn't support DoP yet, but it's good to be prepared.
2019-06-17 21:07:30 +02:00
Max Kellermann
d478bdda8e pcm/Export: document that Export() may return an empty buffer 2019-06-17 21:07:29 +02:00
Max Kellermann
1eae9339f2 db/Interface: CollectUniqueTags() allows multiple "groups"
Instead of passing tag and group, pass an array of tags.  To support a
nested return value, return a nested std::map of std::maps.  Each key
specifies the tag value, and each value may be another nesting level.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/408
2019-06-16 10:39:29 +02:00
Max Kellermann
923c1b6220 doc/include: remove obsolete DocBook fragment 2019-06-11 09:29:20 +02:00
Max Kellermann
09884e608b increment version number to 0.21.11 2019-06-11 09:29:05 +02:00
37 changed files with 689 additions and 545 deletions

27
NEWS

@@ -1,3 +1,30 @@
ver 0.21.12 (2019/08/03)
* decoder
- mad: update bit rate after seeking
- mad: fix several bugs preventing the plugin from decoding the last frame
- opus: ignore case in replay gain tag names
- opus, vorbis: decode the "end of stream" packet
* output
- jack: fix mono-to-stereo conversion
* player
- don't restart unseekable song after failed seek attempt
* Windows
- support backslash in relative URIs loaded from playlists
ver 0.21.11 (2019/07/03)
* input
- tidal: deprecated because Tidal has changed the protocol
* decoder
- wildmidi: log error if library initialization fails
* output
- alsa: fix busy loop while draining
- alsa: fix missing drain call
- alsa: improve xrun-avoiding silence generator
- alsa: log when generating silence due to slow decoder
- alsa, osx: fix distortions with DSD_U32 and DoP on 32 bit CPUs
* protocol
- fix "list" with multiple "group" levels
ver 0.21.10 (2019/06/05)
* decoder
- opus: fix duplicate tags

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
android:versionCode="33"
android:versionName="0.21.10">
android:versionCode="35"
android:versionName="0.21.12">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/>

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

@@ -1,165 +0,0 @@
<?xml version='1.0' encoding="utf-8"?>
<!DOCTYPE itemizedlist PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<itemizedlist>
<listitem>
<para>
<varname>artist</varname>: the artist name. Its meaning is not
well-defined; see <varname>composer</varname> and
<varname>performer</varname> for more specific tags.
</para>
</listitem>
<listitem>
<para>
<varname>artistsort</varname>: same as
<varname>artist</varname>, but for sorting. This usually omits
prefixes such as "The".
</para>
</listitem>
<listitem>
<para>
<varname>album</varname>: the album name.
</para>
</listitem>
<listitem>
<para>
<varname>albumsort</varname>: same as <varname>album</varname>,
but for sorting.
</para>
</listitem>
<listitem>
<para>
<varname>albumartist</varname>: on multi-artist albums, this is
the artist name which shall be used for the whole album. The
exact meaning of this tag is not well-defined.
</para>
</listitem>
<listitem>
<para>
<varname>albumartistsort</varname>: same as
<varname>albumartist</varname>, but for sorting.
</para>
</listitem>
<listitem>
<para>
<varname>title</varname>: the song title.
</para>
</listitem>
<listitem>
<para>
<varname>track</varname>: the decimal track number within the
album.
</para>
</listitem>
<listitem>
<para>
<varname>name</varname>: a name for this song. This is not the
song title. The exact meaning of this tag is not well-defined.
It is often used by badly configured internet radio stations
with broken tags to squeeze both the artist name and the song
title in one tag.
</para>
</listitem>
<listitem>
<para>
<varname>genre</varname>: the music genre.
</para>
</listitem>
<listitem>
<para>
<varname>date</varname>: the song's release date. This is
usually a 4-digit year.
</para>
</listitem>
<listitem>
<para>
<varname>composer</varname>: the artist who composed the song.
</para>
</listitem>
<listitem>
<para>
<varname>performer</varname>: the artist who performed the song.
</para>
</listitem>
<listitem>
<para>
<varname>comment</varname>: a human-readable comment about this
song. The exact meaning of this tag is not well-defined.
</para>
</listitem>
<listitem>
<para>
<varname>disc</varname>: the decimal disc number in a multi-disc
album.
</para>
</listitem>
<listitem>
<para>
<varname>musicbrainz_artistid</varname>: the artist id in the
<ulink
url="https://picard.musicbrainz.org/docs/mappings/">MusicBrainz</ulink>
database.
</para>
</listitem>
<listitem>
<para>
<varname>musicbrainz_albumid</varname>: the album id in the
<ulink
url="https://picard.musicbrainz.org/docs/mappings/">MusicBrainz</ulink>
database.
</para>
</listitem>
<listitem>
<para>
<varname>musicbrainz_albumartistid</varname>: the album artist
id in the <ulink
url="https://picard.musicbrainz.org/docs/mappings/">MusicBrainz</ulink>
database.
</para>
</listitem>
<listitem>
<para>
<varname>musicbrainz_trackid</varname>: the track id in the
<ulink
url="https://picard.musicbrainz.org/docs/mappings/">MusicBrainz</ulink>
database.
</para>
</listitem>
<listitem>
<para>
<varname>musicbrainz_releasetrackid</varname>: the release track
id in the <ulink
url="https://picard.musicbrainz.org/docs/mappings/">MusicBrainz</ulink>
database.
</para>
</listitem>
<listitem>
<para>
<varname>musicbrainz_workid</varname>: the work id in the
<ulink
url="https://picard.musicbrainz.org/docs/mappings/">MusicBrainz</ulink>
database.
</para>
</listitem>
</itemizedlist>

@@ -245,6 +245,11 @@ tidal
Play songs from the commercial streaming service `Tidal <http://tidal.com/>`_. It plays URLs in the form tidal://track/ID, e.g.:
.. warning::
This plugin is currently defunct because Tidal has changed the
protocol and decided not to share documentation.
.. code-block:: none
mpc add tidal://track/59727857

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.21.10',
version: '0.21.12',
meson_version: '>= 0.49.0',
default_options: [
'c_std=c99',
@@ -15,12 +15,18 @@ version_cxx = vcs_tag(input: 'src/GitVersion.cxx', output: 'GitVersion.cxx')
compiler = meson.get_compiler('cpp')
c_compiler = meson.get_compiler('c')
if compiler.get_id() == 'gcc' and compiler.version().version_compare('<6')
warning('Your GCC version is too old. You need at least version 6.')
elif compiler.get_id() == 'clang' and compiler.version().version_compare('<3')
warning('Your clang version is too old. You need at least version 3.')
endif
conf = configuration_data()
conf.set_quoted('PACKAGE', meson.project_name())
conf.set_quoted('PACKAGE_NAME', meson.project_name())
conf.set_quoted('PACKAGE_VERSION', meson.project_version())
conf.set_quoted('VERSION', meson.project_version())
conf.set_quoted('PROTOCOL_VERSION', '0.21.6')
conf.set_quoted('PROTOCOL_VERSION', '0.21.11')
conf.set_quoted('SYSTEM_CONFIG_FILE_LOCATION', join_paths(get_option('prefix'), get_option('sysconfdir'), 'mpd.conf'))
common_cppflags = [

@@ -43,7 +43,15 @@ struct MusicChunk;
/**
* Meta information for #MusicChunk.
*/
struct MusicChunkInfo {
struct alignas(8) MusicChunkInfo {
/* align to multiple of 8 bytes, which adds padding at the
end, so the size of MusicChunk::data is also a multiple of
8 bytes; this is a workaround for a bug in the DSD_U32 and
DoP converters which require processing 8 bytes at a time,
discarding the remainder */
/* TODO: once all converters have been fixed, we should remove
this workaround */
/** the next chunk in a linked list */
MusicChunkPtr next;
@@ -119,6 +127,10 @@ struct MusicChunk : MusicChunkInfo {
/** the data (probably PCM) */
uint8_t data[CHUNK_SIZE - sizeof(MusicChunkInfo)];
/* TODO: remove this check once all converters have been fixed
(see comment in struct MusicChunkInfo for details) */
static_assert(sizeof(data) % 8 == 0, "Wrong alignment");
/**
* Prepares appending to the music chunk. Returns a buffer
* where you may write into. After you are finished, call

@@ -266,7 +266,7 @@ handle_list(Client &client, Request args, Response &r)
}
std::unique_ptr<SongFilter> filter;
TagType group = TAG_NUM_OF_ITEM_TYPES;
std::vector<TagType> tag_types;
if (args.size == 1 &&
/* parantheses are the syntax for filter expressions: no
@@ -284,20 +284,31 @@ handle_list(Client &client, Request args, Response &r)
args.shift()));
}
if (args.size >= 2 &&
StringIsEqual(args[args.size - 2], "group")) {
while (args.size >= 2 &&
StringIsEqual(args[args.size - 2], "group")) {
const char *s = args[args.size - 1];
group = tag_name_parse_i(s);
const auto group = tag_name_parse_i(s);
if (group == TAG_NUM_OF_ITEM_TYPES) {
r.FormatError(ACK_ERROR_ARG,
"Unknown tag type: %s", s);
return CommandResult::ERROR;
}
if (group == tagType ||
std::find(tag_types.begin(), tag_types.end(),
group) != tag_types.end()) {
r.Error(ACK_ERROR_ARG, "Conflicting group");
return CommandResult::ERROR;
}
tag_types.emplace_back(group);
args.pop_back();
args.pop_back();
}
tag_types.emplace_back(tagType);
if (!args.empty()) {
filter.reset(new SongFilter());
try {
@@ -310,13 +321,9 @@ handle_list(Client &client, Request args, Response &r)
filter->Optimize();
}
if (tagType < TAG_NUM_OF_ITEM_TYPES && tagType == group) {
r.Error(ACK_ERROR_ARG, "Conflicting group");
return CommandResult::ERROR;
}
PrintUniqueTags(r, client.GetPartition(),
tagType, group, filter.get());
{&tag_types.front(), tag_types.size()},
filter.get());
return CommandResult::OK;
}

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* Copyright 2003-2019 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -35,6 +35,7 @@
#include "Interface.hxx"
#include "fs/Traits.hxx"
#include "util/ChronoUtil.hxx"
#include "util/RecursiveMap.hxx"
#include <functional>
@@ -186,42 +187,29 @@ PrintSongUris(Response &r, Partition &partition,
}
static void
PrintUniqueTags(Response &r, TagType tag_type,
const std::set<std::string> &values)
PrintUniqueTags(Response &r, ConstBuffer<TagType> tag_types,
const RecursiveMap<std::string> &map) noexcept
{
const char *const name = tag_item_names[tag_type];
for (const auto &i : values)
r.Format("%s: %s\n", name, i.c_str());
}
const char *const name = tag_item_names[tag_types.front()];
tag_types.pop_front();
static void
PrintGroupedUniqueTags(Response &r, TagType tag_type, TagType group,
const std::map<std::string, std::set<std::string>> &groups)
{
if (group == TAG_NUM_OF_ITEM_TYPES) {
for (const auto &i : groups)
PrintUniqueTags(r, tag_type, i.second);
return;
}
for (const auto &i : map) {
r.Format("%s: %s\n", name, i.first.c_str());
const char *const group_name = tag_item_names[group];
for (const auto &i : groups) {
r.Format("%s: %s\n", group_name, i.first.c_str());
PrintUniqueTags(r, tag_type, i.second);
if (!tag_types.empty())
PrintUniqueTags(r, tag_types, i.second);
}
}
void
PrintUniqueTags(Response &r, Partition &partition,
TagType type, TagType group,
ConstBuffer<TagType> tag_types,
const SongFilter *filter)
{
assert(type < TAG_NUM_OF_ITEM_TYPES);
const Database &db = partition.GetDatabaseOrThrow();
const DatabaseSelection selection("", true, filter);
PrintGroupedUniqueTags(r, type, group,
db.CollectUniqueTags(selection, type, group));
PrintUniqueTags(r, tag_types,
db.CollectUniqueTags(selection, tag_types));
}

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* Copyright 2003-2019 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -22,6 +22,7 @@
#include <stdint.h>
template<typename T> struct ConstBuffer;
enum TagType : uint8_t;
class TagMask;
class SongFilter;
@@ -45,7 +46,7 @@ PrintSongUris(Response &r, Partition &partition,
void
PrintUniqueTags(Response &r, Partition &partition,
TagType type, TagType group,
ConstBuffer<TagType> tag_types,
const SongFilter *filter);
#endif

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* Copyright 2003-2019 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -25,15 +25,14 @@
#include "util/Compiler.h"
#include <chrono>
#include <map>
#include <set>
#include <string>
struct DatabasePlugin;
struct DatabaseStats;
struct DatabaseSelection;
struct LightSong;
class TagMask;
template<typename Key> class RecursiveMap;
template<typename T> struct ConstBuffer;
class Database {
const DatabasePlugin &plugin;
@@ -106,13 +105,14 @@ public:
}
/**
* Collect unique values of the given tag type.
* Collect unique values of the given tag types. Each item in
* the #tag_types parameter results in one nesting level in
* the return value.
*
* Throws on error.
*/
virtual std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group=TAG_NUM_OF_ITEM_TYPES) const = 0;
virtual RecursiveMap<std::string> CollectUniqueTags(const DatabaseSelection &selection,
ConstBuffer<TagType> tag_types) const = 0;
/**
* Throws on error.

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* Copyright 2003-2019 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -21,36 +21,32 @@
#include "Interface.hxx"
#include "song/LightSong.hxx"
#include "tag/VisitFallback.hxx"
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
static void
CollectTags(std::set<std::string> &result,
const Tag &tag,
TagType tag_type) noexcept
CollectUniqueTags(RecursiveMap<std::string> &result,
const Tag &tag,
ConstBuffer<TagType> tag_types) noexcept
{
VisitTagWithFallbackOrEmpty(tag, tag_type, [&result](const char *value){
result.emplace(value);
if (tag_types.empty())
return;
const auto tag_type = tag_types.shift();
VisitTagWithFallbackOrEmpty(tag, tag_type, [&result, &tag, tag_types](const char *value){
CollectUniqueTags(result[value], tag, tag_types);
});
}
static void
CollectGroupTags(std::map<std::string, std::set<std::string>> &result,
const Tag &tag,
TagType tag_type,
TagType group) noexcept
{
VisitTagWithFallbackOrEmpty(tag, group, [&](const char *group_name){
CollectTags(result[group_name], tag, tag_type);
});
}
std::map<std::string, std::set<std::string>>
RecursiveMap<std::string>
CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
TagType tag_type, TagType group)
ConstBuffer<TagType> tag_types)
{
std::map<std::string, std::set<std::string>> result;
RecursiveMap<std::string> result;
db.Visit(selection, [&result, tag_type, group](const LightSong &song){
CollectGroupTags(result, song.tag, tag_type, group);
db.Visit(selection, [&result, tag_types](const LightSong &song){
CollectUniqueTags(result, song.tag, tag_types);
});
return result;

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* Copyright 2003-2019 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -29,9 +29,11 @@
class TagMask;
class Database;
struct DatabaseSelection;
template<typename Key> class RecursiveMap;
template<typename T> struct ConstBuffer;
std::map<std::string, std::set<std::string>>
RecursiveMap<std::string>
CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
TagType tag_type, TagType group);
ConstBuffer<TagType> tag_types);
#endif

@@ -38,6 +38,8 @@
#include "tag/Tag.hxx"
#include "tag/Mask.hxx"
#include "tag/ParseName.hxx"
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
#include "util/ScopeExit.hxx"
#include "util/RuntimeError.hxx"
#include "protocol/Ack.hxx"
@@ -127,9 +129,8 @@ public:
VisitSong visit_song,
VisitPlaylist visit_playlist) const override;
std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group) const override;
RecursiveMap<std::string> CollectUniqueTags(const DatabaseSelection &selection,
ConstBuffer<TagType> tag_types) const override;
DatabaseStats GetStats(const DatabaseSelection &selection) const override;
@@ -412,8 +413,7 @@ SendConstraints(mpd_connection *connection, const DatabaseSelection &selection)
static bool
SendGroup(mpd_connection *connection, TagType group)
{
if (group == TAG_NUM_OF_ITEM_TYPES)
return true;
assert(group != TAG_NUM_OF_ITEM_TYPES);
#if LIBMPDCLIENT_CHECK_VERSION(2,12,0)
const auto tag = Convert(group);
@@ -428,6 +428,19 @@ SendGroup(mpd_connection *connection, TagType group)
#endif
}
static bool
SendGroup(mpd_connection *connection, ConstBuffer<TagType> group)
{
while (!group.empty()) {
if (!SendGroup(connection, group.back()))
return false;
group.pop_back();
}
return true;
}
ProxyDatabase::ProxyDatabase(EventLoop &_loop, DatabaseListener &_listener,
const ConfigBlock &block)
:Database(proxy_db_plugin),
@@ -983,17 +996,20 @@ ProxyDatabase::Visit(const DatabaseSelection &selection,
helper.Commit();
}
std::map<std::string, std::set<std::string>>
RecursiveMap<std::string>
ProxyDatabase::CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type, TagType group) const
ConstBuffer<TagType> tag_types) const
try {
// TODO: eliminate the const_cast
const_cast<ProxyDatabase *>(this)->EnsureConnected();
enum mpd_tag_type tag_type2 = Convert(tag_type);
enum mpd_tag_type tag_type2 = Convert(tag_types.back());
if (tag_type2 == MPD_TAG_COUNT)
throw std::runtime_error("Unsupported tag");
auto group = tag_types;
group.pop_back();
if (!mpd_search_db_tags(connection, tag_type2) ||
!SendConstraints(connection, selection) ||
!SendGroup(connection, group))
@@ -1002,44 +1018,33 @@ try {
if (!mpd_search_commit(connection))
ThrowError(connection);
std::map<std::string, std::set<std::string>> result;
RecursiveMap<std::string> result;
std::vector<RecursiveMap<std::string> *> position;
position.emplace_back(&result);
if (group == TAG_NUM_OF_ITEM_TYPES) {
auto &values = result[std::string()];
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
auto it = std::find(tag_types.begin(), tag_types.end(),
current_type);
if (it == tag_types.end())
continue;
if (current_type == tag_type)
values.emplace(pair->value);
}
} else {
std::set<std::string> *current_group = nullptr;
size_t i = std::distance(tag_types.begin(), it);
if (i > position.size())
continue;
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
if (i + 1 < position.size())
position.resize(i + 1);
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
if (current_type == tag_type) {
if (current_group == nullptr)
current_group = &result[std::string()];
current_group->emplace(pair->value);
} else if (current_type == group) {
current_group = &result[pair->value];
}
}
auto &parent = *position[i];
position.emplace_back(&parent[pair->value]);
}
if (!mpd_response_finish(connection))

@@ -42,6 +42,8 @@
#include "fs/FileSystem.hxx"
#include "util/CharUtil.hxx"
#include "util/Domain.hxx"
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
#include "Log.hxx"
#ifdef ENABLE_ZLIB
@@ -329,11 +331,11 @@ SimpleDatabase::Visit(const DatabaseSelection &selection,
"No such directory");
}
std::map<std::string, std::set<std::string>>
RecursiveMap<std::string>
SimpleDatabase::CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type, TagType group) const
ConstBuffer<TagType> tag_types) const
{
return ::CollectUniqueTags(*this, selection, tag_type, group);
return ::CollectUniqueTags(*this, selection, tag_types);
}
DatabaseStats

@@ -122,9 +122,8 @@ public:
VisitSong visit_song,
VisitPlaylist visit_playlist) const override;
std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group) const override;
RecursiveMap<std::string> CollectUniqueTags(const DatabaseSelection &selection,
ConstBuffer<TagType> tag_types) const override;
DatabaseStats GetStats(const DatabaseSelection &selection) const override;

@@ -40,10 +40,11 @@
#include "tag/Mask.hxx"
#include "fs/Traits.hxx"
#include "Log.hxx"
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
#include "util/SplitString.hxx"
#include <string>
#include <set>
#include <assert.h>
#include <string.h>
@@ -97,9 +98,8 @@ public:
VisitSong visit_song,
VisitPlaylist visit_playlist) const override;
std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group) const override;
RecursiveMap<std::string> CollectUniqueTags(const DatabaseSelection &selection,
ConstBuffer<TagType> tag_types) const override;
DatabaseStats GetStats(const DatabaseSelection &selection) const override;
@@ -624,11 +624,11 @@ UpnpDatabase::Visit(const DatabaseSelection &selection,
helper.Commit();
}
std::map<std::string, std::set<std::string>>
RecursiveMap<std::string>
UpnpDatabase::CollectUniqueTags(const DatabaseSelection &selection,
TagType tag, TagType group) const
ConstBuffer<TagType> tag_types) const
{
return ::CollectUniqueTags(*this, selection, tag, group);
return ::CollectUniqueTags(*this, selection, tag_types);
}
DatabaseStats

@@ -320,6 +320,11 @@ public:
gcc_pure
bool IsCurrentSong(const DetachedSong &_song) const noexcept;
gcc_pure
bool IsUnseekableCurrentSong(const DetachedSong &_song) const noexcept {
return !seekable && IsCurrentSong(_song);
}
gcc_pure
bool IsSeekableCurrentSong(const DetachedSong &_song) const noexcept {
return seekable && IsCurrentSong(_song);

@@ -20,6 +20,8 @@
#include "config.h"
#include "DecoderList.hxx"
#include "DecoderPlugin.hxx"
#include "PluginUnavailable.hxx"
#include "Log.hxx"
#include "config/Data.hxx"
#include "config/Block.hxx"
#include "plugins/AudiofileDecoderPlugin.hxx"
@@ -45,6 +47,7 @@
#include "plugins/FluidsynthDecoderPlugin.hxx"
#include "plugins/SidplayDecoderPlugin.hxx"
#include "util/Macros.hxx"
#include "util/RuntimeError.hxx"
#include <string.h>
@@ -147,8 +150,17 @@ decoder_plugin_init_all(const ConfigData &config)
if (param != nullptr)
param->SetUsed();
if (plugin.Init(*param))
decoder_plugins_enabled[i] = true;
try {
if (plugin.Init(*param))
decoder_plugins_enabled[i] = true;
} catch (const PluginUnavailable &e) {
FormatError(e,
"Decoder plugin '%s' is unavailable",
plugin.name);
} catch (...) {
std::throw_with_nested(FormatRuntimeError("Failed to initialize decoder plugin '%s'",
plugin.name));
}
}
}

@@ -21,14 +21,13 @@
#include "MadDecoderPlugin.hxx"
#include "../DecoderAPI.hxx"
#include "input/InputStream.hxx"
#include "config/Block.hxx"
#include "tag/Id3Scan.hxx"
#include "tag/Id3ReplayGain.hxx"
#include "tag/Rva2.hxx"
#include "tag/Handler.hxx"
#include "tag/ReplayGain.hxx"
#include "tag/MixRamp.hxx"
#include "CheckAudioFormat.hxx"
#include "util/Clamp.hxx"
#include "util/StringCompare.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
@@ -40,8 +39,6 @@
#include <id3tag.h>
#endif
#include <stdexcept>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
@@ -49,17 +46,17 @@
static constexpr unsigned long FRAMES_CUSHION = 2000;
enum mp3_action {
DECODE_SKIP = -3,
DECODE_BREAK = -2,
DECODE_CONT = -1,
DECODE_OK = 0
enum class MadDecoderAction {
SKIP,
BREAK,
CONT,
OK
};
enum muteframe {
MUTEFRAME_NONE,
MUTEFRAME_SKIP,
MUTEFRAME_SEEK
enum class MadDecoderMuteFrame {
NONE,
SKIP,
SEEK
};
/* the number of samples of silence the decoder inserts at start */
@@ -79,82 +76,86 @@ ToSongTime(mad_timer_t t) noexcept
}
static inline int32_t
mad_fixed_to_24_sample(mad_fixed_t sample)
mad_fixed_to_24_sample(mad_fixed_t sample) noexcept
{
static constexpr unsigned bits = 24;
static constexpr mad_fixed_t MIN = -MAD_F_ONE;
static constexpr mad_fixed_t MAX = MAD_F_ONE - 1;
/* round */
sample = sample + (1L << (MAD_F_FRACBITS - bits));
/* clip */
if (gcc_unlikely(sample > MAX))
sample = MAX;
else if (gcc_unlikely(sample < MIN))
sample = MIN;
/* quantize */
return sample >> (MAD_F_FRACBITS + 1 - bits);
return Clamp(sample, MAD_F_MIN, MAD_F_MAX)
>> (MAD_F_FRACBITS + 1 - bits);
}
static void
mad_fixed_to_24_buffer(int32_t *dest, const struct mad_synth *synth,
unsigned int start, unsigned int end,
mad_fixed_to_24_buffer(int32_t *dest, const struct mad_pcm &src,
size_t start, size_t end,
unsigned int num_channels)
{
for (unsigned i = start; i < end; ++i)
for (size_t i = start; i < end; ++i)
for (unsigned c = 0; c < num_channels; ++c)
*dest++ = mad_fixed_to_24_sample(synth->pcm.samples[c][i]);
*dest++ = mad_fixed_to_24_sample(src.samples[c][i]);
}
static bool
mp3_plugin_init(const ConfigBlock &block)
mad_plugin_init(const ConfigBlock &block)
{
gapless_playback = block.GetBlockValue("gapless",
DEFAULT_GAPLESS_MP3_PLAYBACK);
return true;
}
struct MadDecoder {
class MadDecoder {
static constexpr size_t READ_BUFFER_SIZE = 40960;
static constexpr size_t MP3_DATA_OUTPUT_BUFFER_SIZE = 2048;
struct mad_stream stream;
struct mad_frame frame;
struct mad_synth synth;
mad_timer_t timer;
unsigned char input_buffer[READ_BUFFER_SIZE];
int32_t output_buffer[MP3_DATA_OUTPUT_BUFFER_SIZE];
int32_t output_buffer[sizeof(mad_pcm::samples) / sizeof(mad_fixed_t)];
SignedSongTime total_time;
SongTime elapsed_time;
SongTime seek_time;
enum muteframe mute_frame = MUTEFRAME_NONE;
MadDecoderMuteFrame mute_frame = MadDecoderMuteFrame::NONE;
long *frame_offsets = nullptr;
mad_timer_t *times = nullptr;
unsigned long highest_frame = 0;
unsigned long max_frames = 0;
unsigned long current_frame = 0;
unsigned int drop_start_frames = 0;
unsigned int drop_end_frames = 0;
size_t highest_frame = 0;
size_t max_frames = 0;
size_t current_frame = 0;
unsigned int drop_start_frames;
unsigned int drop_end_frames;
unsigned int drop_start_samples = 0;
unsigned int drop_end_samples = 0;
bool found_replay_gain = false;
bool found_first_frame = false;
bool decoded_first_frame = false;
unsigned long bit_rate;
/**
* If this flag is true, then end-of-file was seen and a
* padding of 8 zero bytes were appended to #input_buffer, to
* allow libmad to decode the last frame.
*/
bool was_eof = false;
DecoderClient *const client;
InputStream &input_stream;
enum mad_layer layer = mad_layer(0);
MadDecoder(DecoderClient *client, InputStream &input_stream);
~MadDecoder();
public:
MadDecoder(DecoderClient *client, InputStream &input_stream) noexcept;
~MadDecoder() noexcept;
bool Seek(long offset);
bool FillBuffer();
void ParseId3(size_t tagsize, Tag *tag);
enum mp3_action DecodeNextFrameHeader(Tag *tag);
enum mp3_action DecodeNextFrame();
void RunDecoder() noexcept;
bool RunScan(TagHandler &handler) noexcept;
private:
bool Seek(long offset) noexcept;
bool FillBuffer() noexcept;
void ParseId3(size_t tagsize, Tag *tag) noexcept;
MadDecoderAction DecodeNextFrameHeader(Tag *tag) noexcept;
MadDecoderAction DecodeNextFrame() noexcept;
gcc_pure
offset_type ThisFrameOffset() const noexcept;
@@ -165,11 +166,11 @@ struct MadDecoder {
/**
* Attempt to calulcate the length of the song from filesize
*/
void FileSizeToSongLength();
void FileSizeToSongLength() noexcept;
bool DecodeFirstFrame(Tag *tag);
bool DecodeFirstFrame(Tag *tag) noexcept;
void AllocateBuffers() {
void AllocateBuffers() noexcept {
assert(max_frames > 0);
assert(frame_offsets == nullptr);
assert(times == nullptr);
@@ -179,27 +180,39 @@ struct MadDecoder {
}
gcc_pure
long TimeToFrame(SongTime t) const noexcept;
size_t TimeToFrame(SongTime t) const noexcept;
void UpdateTimerNextFrame();
/**
* Record the current frame's offset in the "frame_offsets"
* buffer and go forward to the next frame, updating the
* attributes "current_frame" and "timer".
*/
void UpdateTimerNextFrame() noexcept;
/**
* Sends the synthesized current frame via
* DecoderClient::SubmitData().
*/
DecoderCommand SendPCM(unsigned i, unsigned pcm_length);
DecoderCommand SubmitPCM(size_t start, size_t n) noexcept;
/**
* Synthesize the current frame and send it via
* DecoderClient::SubmitData().
*/
DecoderCommand SyncAndSend();
DecoderCommand SynthAndSubmit() noexcept;
bool Read();
/**
* @return false to stop decoding
*/
bool HandleCurrentFrame() noexcept;
bool LoadNextFrame() noexcept;
bool Read() noexcept;
};
MadDecoder::MadDecoder(DecoderClient *_client,
InputStream &_input_stream)
InputStream &_input_stream) noexcept
:client(_client), input_stream(_input_stream)
{
mad_stream_init(&stream);
@@ -210,7 +223,7 @@ MadDecoder::MadDecoder(DecoderClient *_client,
}
inline bool
MadDecoder::Seek(long offset)
MadDecoder::Seek(long offset) noexcept
{
try {
input_stream.LockSeek(offset);
@@ -225,32 +238,38 @@ MadDecoder::Seek(long offset)
}
inline bool
MadDecoder::FillBuffer()
MadDecoder::FillBuffer() noexcept
{
size_t remaining, length;
unsigned char *dest;
/* amount of rest data still residing in the buffer */
size_t rest_size = 0;
size_t max_read_size = sizeof(input_buffer);
unsigned char *dest = input_buffer;
if (stream.next_frame != nullptr) {
remaining = stream.bufend - stream.next_frame;
memmove(input_buffer, stream.next_frame, remaining);
dest = input_buffer + remaining;
length = READ_BUFFER_SIZE - remaining;
} else {
remaining = 0;
length = READ_BUFFER_SIZE;
dest = input_buffer;
rest_size = stream.bufend - stream.next_frame;
memmove(input_buffer, stream.next_frame, rest_size);
dest += rest_size;
max_read_size -= rest_size;
}
/* we've exhausted the read buffer, so give up!, these potential
* mp3 frames are way too big, and thus unlikely to be mp3 frames */
if (length == 0)
if (max_read_size == 0)
return false;
length = decoder_read(client, input_stream, dest, length);
if (length == 0)
return false;
size_t nbytes = decoder_read(client, input_stream,
dest, max_read_size);
if (nbytes == 0) {
if (was_eof || max_read_size < MAD_BUFFER_GUARD)
return false;
mad_stream_buffer(&stream, input_buffer, length + remaining);
was_eof = true;
nbytes = MAD_BUFFER_GUARD;
memset(dest, 0, nbytes);
}
mad_stream_buffer(&stream, input_buffer, rest_size + nbytes);
stream.error = MAD_ERROR_NONE;
return true;
@@ -286,7 +305,7 @@ parse_id3_mixramp(struct id3_tag *tag) noexcept
#endif
inline void
MadDecoder::ParseId3(size_t tagsize, Tag *mpd_tag)
MadDecoder::ParseId3(size_t tagsize, Tag *mpd_tag) noexcept
{
#ifdef ENABLE_ID3TAG
std::unique_ptr<id3_byte_t[]> allocated;
@@ -354,7 +373,7 @@ MadDecoder::ParseId3(size_t tagsize, Tag *mpd_tag)
* of the ID3 frame.
*/
static signed long
id3_tag_query(const void *p0, size_t length)
id3_tag_query(const void *p0, size_t length) noexcept
{
const char *p = (const char *)p0;
@@ -364,26 +383,26 @@ id3_tag_query(const void *p0, size_t length)
}
#endif /* !ENABLE_ID3TAG */
static enum mp3_action
RecoverFrameError(struct mad_stream &stream)
static MadDecoderAction
RecoverFrameError(const struct mad_stream &stream) noexcept
{
if (MAD_RECOVERABLE(stream.error))
return DECODE_SKIP;
return MadDecoderAction::SKIP;
else if (stream.error == MAD_ERROR_BUFLEN)
return DECODE_CONT;
return MadDecoderAction::CONT;
FormatWarning(mad_domain,
"unrecoverable frame level error: %s",
mad_stream_errorstr(&stream));
return DECODE_BREAK;
return MadDecoderAction::BREAK;
}
enum mp3_action
MadDecoder::DecodeNextFrameHeader(Tag *tag)
MadDecoderAction
MadDecoder::DecodeNextFrameHeader(Tag *tag) noexcept
{
if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) &&
!FillBuffer())
return DECODE_BREAK;
return MadDecoderAction::BREAK;
if (mad_header_decode(&frame.header, &stream)) {
if (stream.error == MAD_ERROR_LOSTSYNC && stream.this_frame) {
@@ -393,7 +412,7 @@ MadDecoder::DecodeNextFrameHeader(Tag *tag)
if (tagsize > 0) {
ParseId3((size_t)tagsize, tag);
return DECODE_CONT;
return MadDecoderAction::CONT;
}
}
@@ -404,24 +423,24 @@ MadDecoder::DecodeNextFrameHeader(Tag *tag)
if (layer == (mad_layer)0) {
if (new_layer != MAD_LAYER_II && new_layer != MAD_LAYER_III) {
/* Only layer 2 and 3 have been tested to work */
return DECODE_SKIP;
return MadDecoderAction::SKIP;
}
layer = new_layer;
} else if (new_layer != layer) {
/* Don't decode frames with a different layer than the first */
return DECODE_SKIP;
return MadDecoderAction::SKIP;
}
return DECODE_OK;
return MadDecoderAction::OK;
}
enum mp3_action
MadDecoder::DecodeNextFrame()
MadDecoderAction
MadDecoder::DecodeNextFrame() noexcept
{
if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) &&
!FillBuffer())
return DECODE_BREAK;
return MadDecoderAction::BREAK;
if (mad_frame_decode(&frame, &stream)) {
if (stream.error == MAD_ERROR_LOSTSYNC) {
@@ -430,14 +449,14 @@ MadDecoder::DecodeNextFrame()
stream.this_frame);
if (tagsize > 0) {
mad_stream_skip(&stream, tagsize);
return DECODE_CONT;
return MadDecoderAction::CONT;
}
}
return RecoverFrameError(stream);
}
return DECODE_OK;
return MadDecoderAction::OK;
}
/* xing stuff stolen from alsaplayer, and heavily modified by jat */
@@ -476,7 +495,7 @@ struct lame {
};
static bool
parse_xing(struct xing *xing, struct mad_bitptr *ptr, int *oldbitlen)
parse_xing(struct xing *xing, struct mad_bitptr *ptr, int *oldbitlen) noexcept
{
int bitlen = *oldbitlen;
@@ -556,7 +575,7 @@ parse_xing(struct xing *xing, struct mad_bitptr *ptr, int *oldbitlen)
}
static bool
parse_lame(struct lame *lame, struct mad_bitptr *ptr, int *bitlen)
parse_lame(struct lame *lame, struct mad_bitptr *ptr, int *bitlen) noexcept
{
/* Unlike the xing header, the lame tag has a fixed length. Fail if
* not all 36 bytes (288 bits) are there. */
@@ -647,7 +666,7 @@ parse_lame(struct lame *lame, struct mad_bitptr *ptr, int *bitlen)
}
static inline SongTime
mp3_frame_duration(const struct mad_frame *frame)
mad_frame_duration(const struct mad_frame *frame) noexcept
{
return ToSongTime(frame->header.duration);
}
@@ -672,12 +691,12 @@ MadDecoder::RestIncludingThisFrame() const noexcept
}
inline void
MadDecoder::FileSizeToSongLength()
MadDecoder::FileSizeToSongLength() noexcept
{
if (input_stream.KnownSize()) {
offset_type rest = RestIncludingThisFrame();
const SongTime frame_duration = mp3_frame_duration(&frame);
const SongTime frame_duration = mad_frame_duration(&frame);
const SongTime duration =
SongTime::FromScale<uint64_t>(rest,
frame.header.bitrate / 8);
@@ -694,25 +713,25 @@ MadDecoder::FileSizeToSongLength()
}
inline bool
MadDecoder::DecodeFirstFrame(Tag *tag)
MadDecoder::DecodeFirstFrame(Tag *tag) noexcept
{
struct xing xing;
while (true) {
enum mp3_action ret;
MadDecoderAction ret;
do {
ret = DecodeNextFrameHeader(tag);
} while (ret == DECODE_CONT);
if (ret == DECODE_BREAK)
} while (ret == MadDecoderAction::CONT);
if (ret == MadDecoderAction::BREAK)
return false;
if (ret == DECODE_SKIP) continue;
if (ret == MadDecoderAction::SKIP) continue;
do {
ret = DecodeNextFrame();
} while (ret == DECODE_CONT);
if (ret == DECODE_BREAK)
} while (ret == MadDecoderAction::CONT);
if (ret == MadDecoderAction::BREAK)
return false;
if (ret == DECODE_OK) break;
if (ret == MadDecoderAction::OK) break;
}
struct mad_bitptr ptr = stream.anc_ptr;
@@ -724,7 +743,7 @@ MadDecoder::DecodeFirstFrame(Tag *tag)
* if an xing tag exists, use that!
*/
if (parse_xing(&xing, &ptr, &bitlen)) {
mute_frame = MUTEFRAME_SKIP;
mute_frame = MadDecoderMuteFrame::SKIP;
if ((xing.flags & XING_FRAMES) && xing.frames) {
mad_timer_t duration = frame.header.duration;
@@ -736,9 +755,17 @@ MadDecoder::DecodeFirstFrame(Tag *tag)
struct lame lame;
if (parse_lame(&lame, &ptr, &bitlen)) {
if (gapless_playback && input_stream.IsSeekable()) {
/* libmad inserts 529 samples of
silence at the beginning and
removes those 529 samples at the
end */
drop_start_samples = lame.encoder_delay +
DECODERDELAY;
drop_end_samples = lame.encoder_padding;
if (drop_end_samples > DECODERDELAY)
drop_end_samples -= DECODERDELAY;
else
drop_end_samples = 0;
}
/* Album gain isn't currently used. See comment in
@@ -767,7 +794,7 @@ MadDecoder::DecodeFirstFrame(Tag *tag)
return true;
}
MadDecoder::~MadDecoder()
MadDecoder::~MadDecoder() noexcept
{
mad_synth_finish(&synth);
mad_frame_finish(&frame);
@@ -777,10 +804,10 @@ MadDecoder::~MadDecoder()
delete[] times;
}
long
size_t
MadDecoder::TimeToFrame(SongTime t) const noexcept
{
unsigned long i;
size_t i;
for (i = 0; i < highest_frame; ++i) {
auto frame_time = ToSongTime(times[i]);
@@ -792,12 +819,11 @@ MadDecoder::TimeToFrame(SongTime t) const noexcept
}
void
MadDecoder::UpdateTimerNextFrame()
MadDecoder::UpdateTimerNextFrame() noexcept
{
if (current_frame >= highest_frame) {
/* record this frame's properties in frame_offsets
(for seeking) and times */
bit_rate = frame.header.bitrate;
if (current_frame >= max_frames)
/* cap current_frame */
@@ -818,36 +844,22 @@ MadDecoder::UpdateTimerNextFrame()
}
DecoderCommand
MadDecoder::SendPCM(unsigned i, unsigned pcm_length)
MadDecoder::SubmitPCM(size_t i, size_t pcm_length) noexcept
{
unsigned max_samples = sizeof(output_buffer) /
sizeof(output_buffer[0]) /
MAD_NCHANNELS(&frame.header);
size_t num_samples = pcm_length - i;
while (i < pcm_length) {
unsigned int num_samples = pcm_length - i;
if (num_samples > max_samples)
num_samples = max_samples;
mad_fixed_to_24_buffer(output_buffer, synth.pcm,
i, i + num_samples,
MAD_NCHANNELS(&frame.header));
num_samples *= MAD_NCHANNELS(&frame.header);
i += num_samples;
mad_fixed_to_24_buffer(output_buffer, &synth,
i - num_samples, i,
MAD_NCHANNELS(&frame.header));
num_samples *= MAD_NCHANNELS(&frame.header);
auto cmd = client->SubmitData(input_stream, output_buffer,
sizeof(output_buffer[0]) * num_samples,
bit_rate / 1000);
if (cmd != DecoderCommand::NONE)
return cmd;
}
return DecoderCommand::NONE;
return client->SubmitData(input_stream, output_buffer,
sizeof(output_buffer[0]) * num_samples,
frame.header.bitrate / 1000);
}
inline DecoderCommand
MadDecoder::SyncAndSend()
MadDecoder::SynthAndSubmit() noexcept
{
mad_synth_frame(&synth, &frame);
@@ -864,33 +876,33 @@ MadDecoder::SyncAndSend()
drop_start_frames--;
return DecoderCommand::NONE;
} else if ((drop_end_frames > 0) &&
(current_frame == (max_frames + 1 - drop_end_frames))) {
current_frame == max_frames - drop_end_frames) {
/* stop decoding, effectively dropping all remaining
frames */
return DecoderCommand::STOP;
}
unsigned i = 0;
size_t i = 0;
if (!decoded_first_frame) {
i = drop_start_samples;
decoded_first_frame = true;
}
unsigned pcm_length = synth.pcm.length;
size_t pcm_length = synth.pcm.length;
if (drop_end_samples &&
(current_frame == max_frames - drop_end_frames)) {
current_frame == max_frames - drop_end_frames - 1) {
if (drop_end_samples >= pcm_length)
pcm_length = 0;
else
pcm_length -= drop_end_samples;
return DecoderCommand::STOP;
pcm_length -= drop_end_samples;
}
auto cmd = SendPCM(i, pcm_length);
auto cmd = SubmitPCM(i, pcm_length);
if (cmd != DecoderCommand::NONE)
return cmd;
if (drop_end_samples &&
(current_frame == max_frames - drop_end_frames))
current_frame == max_frames - drop_end_frames - 1)
/* stop decoding, effectively dropping
* all remaining samples */
return DecoderCommand::STOP;
@@ -899,44 +911,51 @@ MadDecoder::SyncAndSend()
}
inline bool
MadDecoder::Read()
MadDecoder::HandleCurrentFrame() noexcept
{
UpdateTimerNextFrame();
switch (mute_frame) {
DecoderCommand cmd;
case MUTEFRAME_SKIP:
mute_frame = MUTEFRAME_NONE;
case MadDecoderMuteFrame::SKIP:
mute_frame = MadDecoderMuteFrame::NONE;
break;
case MUTEFRAME_SEEK:
case MadDecoderMuteFrame::SEEK:
if (elapsed_time >= seek_time)
mute_frame = MUTEFRAME_NONE;
mute_frame = MadDecoderMuteFrame::NONE;
UpdateTimerNextFrame();
break;
case MUTEFRAME_NONE:
cmd = SyncAndSend();
case MadDecoderMuteFrame::NONE:
cmd = SynthAndSubmit();
UpdateTimerNextFrame();
if (cmd == DecoderCommand::SEEK) {
assert(input_stream.IsSeekable());
const auto t = client->GetSeekTime();
unsigned long j = TimeToFrame(t);
size_t j = TimeToFrame(t);
if (j < highest_frame) {
if (Seek(frame_offsets[j])) {
current_frame = j;
was_eof = false;
client->CommandFinished();
} else
client->SeekError();
} else {
seek_time = t;
mute_frame = MUTEFRAME_SEEK;
mute_frame = MadDecoderMuteFrame::SEEK;
client->CommandFinished();
}
} else if (cmd != DecoderCommand::NONE)
return false;
}
return true;
}
inline bool
MadDecoder::LoadNextFrame() noexcept
{
while (true) {
enum mp3_action ret;
MadDecoderAction ret;
do {
Tag tag;
@@ -945,84 +964,104 @@ MadDecoder::Read()
if (!tag.IsEmpty())
client->SubmitTag(input_stream,
std::move(tag));
} while (ret == DECODE_CONT);
if (ret == DECODE_BREAK)
} while (ret == MadDecoderAction::CONT);
if (ret == MadDecoderAction::BREAK)
return false;
const bool skip = ret == DECODE_SKIP;
const bool skip = ret == MadDecoderAction::SKIP;
if (mute_frame == MUTEFRAME_NONE) {
if (mute_frame == MadDecoderMuteFrame::NONE) {
do {
ret = DecodeNextFrame();
} while (ret == DECODE_CONT);
if (ret == DECODE_BREAK)
} while (ret == MadDecoderAction::CONT);
if (ret == MadDecoderAction::BREAK)
return false;
}
if (!skip && ret == DECODE_OK)
if (!skip && ret == MadDecoderAction::OK)
return true;
}
}
static void
mp3_decode(DecoderClient &client, InputStream &input_stream)
inline bool
MadDecoder::Read() noexcept
{
MadDecoder data(&client, input_stream);
return HandleCurrentFrame() &&
LoadNextFrame();
}
inline void
MadDecoder::RunDecoder() noexcept
{
assert(client != nullptr);
Tag tag;
if (!data.DecodeFirstFrame(&tag)) {
if (client.GetCommand() == DecoderCommand::NONE)
if (!DecodeFirstFrame(&tag)) {
if (client->GetCommand() == DecoderCommand::NONE)
LogError(mad_domain,
"input/Input does not appear to be a mp3 bit stream");
"input does not appear to be a mp3 bit stream");
return;
}
data.AllocateBuffers();
AllocateBuffers();
client.Ready(CheckAudioFormat(data.frame.header.samplerate,
SampleFormat::S24_P32,
MAD_NCHANNELS(&data.frame.header)),
input_stream.IsSeekable(),
data.total_time);
client->Ready(CheckAudioFormat(frame.header.samplerate,
SampleFormat::S24_P32,
MAD_NCHANNELS(&frame.header)),
input_stream.IsSeekable(),
total_time);
if (!tag.IsEmpty())
client.SubmitTag(input_stream, std::move(tag));
client->SubmitTag(input_stream, std::move(tag));
while (data.Read()) {}
while (Read()) {}
}
static bool
mad_decoder_scan_stream(InputStream &is, TagHandler &handler) noexcept
static void
mad_decode(DecoderClient &client, InputStream &input_stream)
{
MadDecoder data(nullptr, is);
if (!data.DecodeFirstFrame(nullptr))
MadDecoder data(&client, input_stream);
data.RunDecoder();
}
inline bool
MadDecoder::RunScan(TagHandler &handler) noexcept
{
if (!DecodeFirstFrame(nullptr))
return false;
if (!data.total_time.IsNegative())
handler.OnDuration(SongTime(data.total_time));
if (!total_time.IsNegative())
handler.OnDuration(SongTime(total_time));
try {
handler.OnAudioFormat(CheckAudioFormat(data.frame.header.samplerate,
handler.OnAudioFormat(CheckAudioFormat(frame.header.samplerate,
SampleFormat::S24_P32,
MAD_NCHANNELS(&data.frame.header)));
MAD_NCHANNELS(&frame.header)));
} catch (...) {
}
return true;
}
static const char *const mp3_suffixes[] = { "mp3", "mp2", nullptr };
static const char *const mp3_mime_types[] = { "audio/mpeg", nullptr };
static bool
mad_decoder_scan_stream(InputStream &is, TagHandler &handler) noexcept
{
MadDecoder data(nullptr, is);
return data.RunScan(handler);
}
static const char *const mad_suffixes[] = { "mp3", "mp2", nullptr };
static const char *const mad_mime_types[] = { "audio/mpeg", nullptr };
const struct DecoderPlugin mad_decoder_plugin = {
"mad",
mp3_plugin_init,
mad_plugin_init,
nullptr,
mp3_decode,
mad_decode,
nullptr,
nullptr,
mad_decoder_scan_stream,
nullptr,
mp3_suffixes,
mp3_mime_types,
mad_suffixes,
mad_mime_types,
};

@@ -22,12 +22,12 @@
#include "lib/xiph/XiphTags.hxx"
#include "tag/Handler.hxx"
#include "tag/ParseName.hxx"
#include "util/ASCII.hxx"
#include "ReplayGainInfo.hxx"
#include <string>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
gcc_pure
@@ -46,7 +46,7 @@ ScanOneOpusTag(const char *name, const char *value,
ReplayGainInfo *rgi,
TagHandler &handler) noexcept
{
if (rgi != nullptr && strcmp(name, "R128_TRACK_GAIN") == 0) {
if (rgi != nullptr && StringEqualsCaseASCII(name, "R128_TRACK_GAIN")) {
/* R128_TRACK_GAIN is a Q7.8 fixed point number in
dB */
@@ -54,7 +54,8 @@ ScanOneOpusTag(const char *name, const char *value,
long l = strtol(value, &endptr, 10);
if (endptr > value && *endptr == 0)
rgi->track.gain = double(l) / 256.;
} else if (rgi != nullptr && strcmp(name, "R128_ALBUM_GAIN") == 0) {
} else if (rgi != nullptr &&
StringEqualsCaseASCII(name, "R128_ALBUM_GAIN")) {
/* R128_ALBUM_GAIN is a Q7.8 fixed point number in
dB */

@@ -20,18 +20,18 @@
#include "WildmidiDecoderPlugin.hxx"
#include "../DecoderAPI.hxx"
#include "tag/Handler.hxx"
#include "util/Domain.hxx"
#include "util/ScopeExit.hxx"
#include "util/StringFormat.hxx"
#include "fs/AllocatedPath.hxx"
#include "fs/FileSystem.hxx"
#include "fs/Path.hxx"
#include "Log.hxx"
#include "PluginUnavailable.hxx"
extern "C" {
#include <wildmidi_lib.h>
}
static constexpr Domain wildmidi_domain("wildmidi");
static constexpr AudioFormat wildmidi_audio_format{48000, SampleFormat::S16, 2};
static bool
@@ -43,14 +43,27 @@ wildmidi_init(const ConfigBlock &block)
if (!FileExists(path)) {
const auto utf8 = path.ToUTF8();
FormatDebug(wildmidi_domain,
"configuration file does not exist: %s",
utf8.c_str());
return false;
throw PluginUnavailable(StringFormat<1024>("configuration file does not exist: %s",
utf8.c_str()));
}
return WildMidi_Init(path.c_str(), wildmidi_audio_format.sample_rate,
0) == 0;
#ifdef LIBWILDMIDI_VERSION
/* WildMidi_ClearError() requires libwildmidi 0.4 */
WildMidi_ClearError();
AtScopeExit() { WildMidi_ClearError(); };
#endif
if (WildMidi_Init(path.c_str(), wildmidi_audio_format.sample_rate,
0) != 0) {
#ifdef LIBWILDMIDI_VERSION
/* WildMidi_GetError() requires libwildmidi 0.4 */
throw PluginUnavailable(WildMidi_GetError());
#else
throw PluginUnavailable("WildMidi_Init() failed");
#endif
}
return true;
}
static void

@@ -180,6 +180,8 @@ InitTidalInput(EventLoop &event_loop, const ConfigBlock &block)
if (password == nullptr)
throw PluginUnavailable("No Tidal password configured");
FormatWarning(tidal_domain, "The Tidal input plugin is deprecated because Tidal has changed the protocol and doesn't share documentation");
tidal_audioquality = block.GetBlockValue("audioquality", "HIGH");
tidal_session = new TidalSessionManager(event_loop, base_url, token,

@@ -69,12 +69,12 @@ OggVisitor::HandlePacket(const ogg_packet &packet)
/* fail if BOS is missing */
throw std::runtime_error("BOS packet expected");
OnOggPacket(packet);
if (packet.e_o_s) {
EndStream();
return;
}
OnOggPacket(packet);
}
inline void

@@ -69,8 +69,21 @@ private:
void HandlePackets();
protected:
/**
* Called when the "beginning of stream" packet has been seen.
*
* @param packet the "beginning of stream" packet
*/
virtual void OnOggBeginning(const ogg_packet &packet) = 0;
/**
* Called for each follow-up packet.
*/
virtual void OnOggPacket(const ogg_packet &packet) = 0;
/**
* Called after the "end of stream" packet has been processed.
*/
virtual void OnOggEnd() = 0;
};

@@ -27,6 +27,7 @@
#include "../OutputAPI.hxx"
#include "mixer/MixerList.hxx"
#include "pcm/PcmExport.hxx"
#include "system/PeriodClock.hxx"
#include "thread/Mutex.hxx"
#include "thread/Cond.hxx"
#include "util/Manual.hxx"
@@ -56,6 +57,17 @@ class AlsaOutput final
DeferEvent defer_invalidate_sockets;
/**
* This timer is used to re-schedule the #MultiSocketMonitor
* after it had been disabled to wait for the next Play() call
* to deliver more data. This timer is necessary to start
* generating silence if Play() doesn't get called soon enough
* to avoid the xrun.
*/
TimerEvent silence_timer;
PeriodClock throttle_silence_log;
Manual<PcmExport> pcm_export;
/**
@@ -109,6 +121,8 @@ class AlsaOutput final
*/
snd_pcm_uframes_t period_frames;
std::chrono::steady_clock::duration effective_period_duration;
/**
* If snd_pcm_avail() goes above this value and no more data
* is available in the #ring_buffer, we need to play some
@@ -128,13 +142,20 @@ class AlsaOutput final
bool work_around_drain_bug;
/**
* After Open(), has this output been activated by a Play()
* command?
* After Open() or Cancel(), has this output been activated by
* a Play() command?
*
* Protected by #mutex.
*/
bool active;
/**
* Is this output waiting for more data?
*
* Protected by #mutex.
*/
bool waiting;
/**
* Do we need to call snd_pcm_prepare() before the next write?
* It means that we put the device to SND_PCM_STATE_SETUP by
@@ -176,7 +197,7 @@ class AlsaOutput final
Alsa::PeriodBuffer period_buffer;
/**
* Protects #cond, #error, #active, #drain.
* Protects #cond, #error, #active, #waiting, #drain.
*/
mutable Mutex mutex;
@@ -248,6 +269,12 @@ private:
return active;
}
gcc_pure
bool LockIsActiveAndNotWaiting() const noexcept {
const std::lock_guard<Mutex> lock(mutex);
return active && !waiting;
}
/**
* Activate the output by registering the sockets in the
* #EventLoop. Before calling this, filling the ring buffer
@@ -260,10 +287,11 @@ private:
* was never unlocked
*/
bool Activate() noexcept {
if (active)
if (active && !waiting)
return false;
active = true;
waiting = false;
const ScopeUnlock unlock(mutex);
defer_invalidate_sockets.Schedule();
@@ -330,9 +358,23 @@ private:
const std::lock_guard<Mutex> lock(mutex);
error = std::current_exception();
active = false;
waiting = false;
cond.signal();
}
/**
* Callback for @silence_timer
*/
void OnSilenceTimer() noexcept {
{
const std::lock_guard<Mutex> lock(mutex);
assert(active);
waiting = false;
}
MultiSocketMonitor::InvalidateSockets();
}
/* virtual methods from class MultiSocketMonitor */
std::chrono::steady_clock::duration PrepareSockets() noexcept override;
void DispatchSockets() noexcept override;
@@ -344,6 +386,7 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
:AudioOutput(FLAG_ENABLE_DISABLE),
MultiSocketMonitor(_loop),
defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)),
silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)),
device(block.GetBlockValue("device", "")),
#ifdef ENABLE_DSD
dop_setting(block.GetBlockValue("dop", false) ||
@@ -500,8 +543,9 @@ AlsaOutput::Setup(AudioFormat &audio_format,
alsa_period_size = 1;
period_frames = alsa_period_size;
effective_period_duration = audio_format.FramesToTime<decltype(effective_period_duration)>(period_frames);
/* generate silence if there's less than once period of data
/* generate silence if there's less than one period of data
in the ALSA-PCM buffer */
max_avail_frames = hw_result.buffer_size - hw_result.period_size;
@@ -684,6 +728,7 @@ AlsaOutput::Open(AudioFormat &audio_format)
period_buffer.Allocate(period_frames, out_frame_size);
active = false;
waiting = false;
must_prepare = false;
written = false;
error = {};
@@ -766,7 +811,7 @@ AlsaOutput::DrainInternal()
/* need to call CopyRingToPeriodBuffer() and
WriteFromPeriodBuffer() again in the next
iteration, so don't finish the drain just yet */
return period_buffer.IsEmpty();
return false;
}
if (!written)
@@ -774,6 +819,24 @@ AlsaOutput::DrainInternal()
don't need to drain it */
return true;
switch (snd_pcm_state(pcm)) {
case SND_PCM_STATE_PREPARED:
case SND_PCM_STATE_RUNNING:
/* these states require a call to snd_pcm_drain() */
break;
case SND_PCM_STATE_DRAINING:
/* already draining, but not yet finished; this is
probably a spurious epoll event, and we should wait
for the next one */
return false;
default:
/* all other states cannot be drained, and we're
done */
return true;
}
/* .. and finally drain the ALSA hardware buffer */
int result;
@@ -827,9 +890,11 @@ AlsaOutput::CancelInternal() noexcept
ring_buffer->reset();
active = false;
waiting = false;
MultiSocketMonitor::Reset();
defer_invalidate_sockets.Cancel();
silence_timer.Cancel();
}
void
@@ -858,6 +923,7 @@ AlsaOutput::Close() noexcept
BlockingCall(GetEventLoop(), [this](){
MultiSocketMonitor::Reset();
defer_invalidate_sockets.Cancel();
silence_timer.Cancel();
});
period_buffer.Free();
@@ -912,7 +978,7 @@ AlsaOutput::Play(const void *chunk, size_t size)
std::chrono::steady_clock::duration
AlsaOutput::PrepareSockets() noexcept
{
if (!LockIsActive()) {
if (!LockIsActiveAndNotWaiting()) {
ClearSocketList();
return std::chrono::steady_clock::duration(-1);
}
@@ -977,28 +1043,42 @@ try {
whenever more data arrives */
/* the same applies when there is still enough
data in the ALSA-PCM buffer (determined by
snd_pcm_avail()); this can happend at the
snd_pcm_avail()); this can happen at the
start of playback, when our ring_buffer is
smaller than the ALSA-PCM buffer */
{
const std::lock_guard<Mutex> lock(mutex);
active = false;
waiting = true;
cond.signal();
}
/* avoid race condition: see if data has
arrived meanwhile before disabling the
event (but after clearing the "active"
event (but after setting the "waiting"
flag) */
if (!CopyRingToPeriodBuffer()) {
MultiSocketMonitor::Reset();
defer_invalidate_sockets.Cancel();
/* just in case Play() doesn't get
called soon enough, schedule a
timer which generates silence
before the xrun occurs */
/* the timer fires in half of a
period; this short duration may
produce a few more wakeups than
necessary, but should be small
enough to avoid the xrun */
silence_timer.Schedule(effective_period_duration / 2);
}
return;
}
if (throttle_silence_log.CheckUpdate(std::chrono::seconds(5)))
FormatWarning(alsa_output_domain, "Decoder is too slow; playing silence to avoid xrun");
/* insert some silence if the buffer has not enough
data yet, to avoid ALSA xrun */
period_buffer.FillWithSilence(silence, out_frame_size);

@@ -540,7 +540,7 @@ JackOutput::Start()
std::fill(dports + num_dports, dports + audio_format.channels,
dports[0]);
} else if (num_dports > audio_format.channels) {
if (audio_format.channels == 1 && num_dports > 2) {
if (audio_format.channels == 1 && num_dports >= 2) {
/* mono input file: connect the one source
channel to the both destination channels */
duplicate_port = dports[1];

@@ -670,12 +670,13 @@ OssOutput::Play(const void *chunk, size_t size)
#ifdef AFMT_S24_PACKED
const auto e = pcm_export->Export({chunk, size});
if (e.empty())
return size;
chunk = e.data;
size = e.size;
#endif
assert(size > 0);
while (true) {
ret = fd.Write(chunk, size);
if (ret > 0) {

@@ -173,7 +173,8 @@ public:
* Export a PCM buffer.
*
* @param src the source PCM buffer
* @return the destination buffer (may be a pointer to the source buffer)
* @return the destination buffer; may be empty (and may be a
* pointer to the source buffer)
*/
ConstBuffer<void> Export(ConstBuffer<void> src) noexcept;

@@ -599,6 +599,19 @@ Player::SeekDecoder() noexcept
{
assert(pc.next_song != nullptr);
if (pc.seek_time > SongTime::zero() && // TODO: allow this only if the song duration is known
dc.IsUnseekableCurrentSong(*pc.next_song)) {
/* seeking into the current song; but we already know
it's not seekable, so let's fail early */
/* note the seek_time>0 check: if seeking to the
beginning, we can simply restart the decoder */
pc.next_song.reset();
pc.SetError(PlayerError::DECODER,
std::make_exception_ptr(std::runtime_error("Not seekable")));
pc.CommandFinished();
return true;
}
CancelPendingSeek();
{

@@ -25,6 +25,9 @@
#include "util/UriUtil.hxx"
#include "song/DetachedSong.hxx"
#include <algorithm>
#include <string>
#include <string.h>
static void
@@ -66,6 +69,22 @@ playlist_check_translate_song(DetachedSong &song, const char *base_uri,
base_uri = nullptr;
const char *uri = song.GetURI();
#ifdef _WIN32
if (!PathTraitsUTF8::IsAbsolute(uri) && strchr(uri, '\\') != nullptr) {
/* Windows uses the backslash as path separator, but
the MPD protocol uses the (forward) slash by
definition; to allow backslashes in relative URIs
loaded from playlist files, this step converts all
backslashes to (forward) slashes */
std::string new_uri(uri);
std::replace(new_uri.begin(), new_uri.end(), '\\', '/');
song.SetURI(std::move(new_uri));
uri = song.GetURI();
}
#endif
if (base_uri != nullptr && !uri_has_scheme(uri) &&
!PathTraitsUTF8::IsAbsolute(uri))
song.SetURI(PathTraitsUTF8::Build(base_uri, uri));

@@ -57,18 +57,6 @@
(GCC_VERSION > 0 && CLANG_VERSION == 0 && \
GCC_VERSION < GCC_MAKE_VERSION(major, minor, 0))
#ifdef __clang__
# if __clang_major__ < 3
# error Sorry, your clang version is too old. You need at least version 3.1.
# endif
#elif defined(__GNUC__)
# if GCC_OLDER_THAN(6,0)
# error Sorry, your gcc version is too old. You need at least version 6.0.
# endif
#else
# warning Untested compiler. Use at your own risk!
#endif
/**
* Are we building with the specified version of clang or newer?
*/

41
src/util/RecursiveMap.hxx Normal file

@@ -0,0 +1,41 @@
/*
* Copyright 2019 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.
*/
#ifndef RECURSIVE_MAP_HXX
#define RECURSIVE_MAP_HXX
#include <map>
/**
* A #std::map which contains instances of itself.
*/
template<typename Key>
class RecursiveMap : public std::map<Key, RecursiveMap<Key>> {};
#endif

@@ -56,11 +56,11 @@ protected:
T data[size];
public:
constexpr size_type GetCapacity() const {
constexpr size_type GetCapacity() const noexcept {
return size;
}
void Shift() {
void Shift() noexcept {
if (head == 0)
return;
@@ -74,15 +74,15 @@ public:
head = 0;
}
void Clear() {
void Clear() noexcept {
head = tail = 0;
}
bool empty() const {
constexpr bool empty() const noexcept {
return head == tail;
}
bool IsFull() const {
constexpr bool IsFull() const noexcept {
return head == 0 && tail == size;
}
@@ -90,7 +90,7 @@ public:
* Prepares writing. Returns a buffer range which may be written.
* When you are finished, call Append().
*/
Range Write() {
Range Write() noexcept {
if (empty())
Clear();
else if (tail == size)
@@ -103,7 +103,7 @@ public:
* Expands the tail of the buffer, after data has been written to
* the buffer returned by Write().
*/
void Append(size_type n) {
void Append(size_type n) noexcept {
assert(tail <= size);
assert(n <= size);
assert(tail + n <= size);
@@ -111,18 +111,22 @@ public:
tail += n;
}
constexpr size_type GetAvailable() const noexcept {
return tail - head;
}
/**
* Return a buffer range which may be read. The buffer pointer is
* writable, to allow modifications while parsing.
*/
Range Read() {
constexpr Range Read() noexcept {
return Range(data + head, tail - head);
}
/**
* Marks a chunk as consumed.
*/
void Consume(size_type n) {
void Consume(size_type n) noexcept {
assert(tail <= size);
assert(head <= tail);
assert(n <= tail);

@@ -23,7 +23,7 @@
#include "event/Thread.hxx"
#include "decoder/DecoderList.hxx"
#include "decoder/DecoderPlugin.hxx"
#include "decoder/Client.hxx"
#include "decoder/DecoderAPI.hxx" /* for class StopDecoder */
#include "input/Init.hxx"
#include "input/InputStream.hxx"
#include "fs/Path.hxx"
@@ -244,10 +244,16 @@ try {
ChromaprintDecoderClient client;
if (plugin->file_decode != nullptr) {
plugin->FileDecode(client, Path::FromFS(c.uri));
try {
plugin->FileDecode(client, Path::FromFS(c.uri));
} catch (StopDecoder) {
}
} else if (plugin->stream_decode != nullptr) {
auto is = InputStream::OpenReady(c.uri, client.mutex);
plugin->StreamDecode(client, *is);
try {
plugin->StreamDecode(client, *is);
} catch (StopDecoder) {
}
} else {
fprintf(stderr, "Decoder plugin is not usable\n");
return EXIT_FAILURE;

@@ -21,6 +21,7 @@
#include "event/Thread.hxx"
#include "decoder/DecoderList.hxx"
#include "decoder/DecoderPlugin.hxx"
#include "decoder/DecoderAPI.hxx" /* for class StopDecoder */
#include "DumpDecoderClient.hxx"
#include "input/Init.hxx"
#include "input/InputStream.hxx"
@@ -116,10 +117,16 @@ try {
DumpDecoderClient client;
if (plugin->file_decode != nullptr) {
plugin->FileDecode(client, Path::FromFS(c.uri));
try {
plugin->FileDecode(client, Path::FromFS(c.uri));
} catch (StopDecoder) {
}
} else if (plugin->stream_decode != nullptr) {
auto is = InputStream::OpenReady(c.uri, client.mutex);
plugin->StreamDecode(client, *is);
try {
plugin->StreamDecode(client, *is);
} catch (StopDecoder) {
}
} else {
fprintf(stderr, "Decoder plugin is not usable\n");
return EXIT_FAILURE;

@@ -207,7 +207,6 @@ TEST_F(TranslateSongTest, Insecure)
TEST_F(TranslateSongTest, Secure)
{
DetachedSong song1(uri1, MakeTag1b());
auto s1 = ToString(song1);
auto se = ToString(DetachedSong(uri1, MakeTag1c()));
const SongLoader loader(nullptr, nullptr);
@@ -226,14 +225,12 @@ TEST_F(TranslateSongTest, InDatabase)
loader));
DetachedSong song2(uri2, MakeTag2b());
auto s1 = ToString(song2);
auto se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song2, nullptr,
loader));
EXPECT_EQ(se, ToString(song2));
DetachedSong song3("/music/foo/bar.ogg", MakeTag2b());
s1 = ToString(song3);
se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song3, nullptr,
loader));
@@ -249,7 +246,6 @@ TEST_F(TranslateSongTest, Relative)
/* map to music_directory */
DetachedSong song1("bar.ogg", MakeTag2b());
auto s1 = ToString(song1);
auto se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song1, "/music/foo",
insecure_loader));
@@ -262,7 +258,6 @@ TEST_F(TranslateSongTest, Relative)
/* legal because secure=true */
DetachedSong song3("bar.ogg", MakeTag1b());
s1 = ToString(song3);
se = ToString(DetachedSong(uri1, MakeTag1c()));
EXPECT_TRUE(playlist_check_translate_song(song3, "/foo",
secure_loader));
@@ -270,9 +265,28 @@ TEST_F(TranslateSongTest, Relative)
/* relative to http:// */
DetachedSong song4("bar.ogg", MakeTag2a());
s1 = ToString(song4);
se = ToString(DetachedSong("http://example.com/foo/bar.ogg", MakeTag2a()));
EXPECT_TRUE(playlist_check_translate_song(song4, "http://example.com/foo",
insecure_loader));
EXPECT_EQ(se, ToString(song4));
}
TEST_F(TranslateSongTest, Backslash)
{
const SongLoader loader(reinterpret_cast<const Database *>(1),
storage);
DetachedSong song1("foo\\bar.ogg", MakeTag2b());
#ifdef _WIN32
/* on Windows, all backslashes are converted to slashes in
relative paths from playlists */
auto se = ToString(DetachedSong(uri2, MakeTag2c()));
EXPECT_TRUE(playlist_check_translate_song(song1, nullptr,
loader));
EXPECT_EQ(se, ToString(song1));
#else
/* backslash only supported on Windows */
EXPECT_FALSE(playlist_check_translate_song(song1, nullptr,
loader));
#endif
}