Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ae19bda1f2 | ||
![]() |
f2d8fd769d | ||
![]() |
9661062ae2 | ||
![]() |
2a07354cad | ||
![]() |
fc18fd571c | ||
![]() |
51abed9732 | ||
![]() |
d00afc912c | ||
![]() |
9d0fe725eb | ||
![]() |
8a432c9b7f | ||
![]() |
187204f03c | ||
![]() |
5e5fadb5f2 | ||
![]() |
952c793235 | ||
![]() |
3e3d8c7f9d | ||
![]() |
9b99a9897a | ||
![]() |
4f56fdc397 | ||
![]() |
c87d6825ec | ||
![]() |
00830a20e3 | ||
![]() |
d39d2874b4 | ||
![]() |
a0a74951b8 | ||
![]() |
779a6855ff | ||
![]() |
f7ed7446ae | ||
![]() |
9d44a6d2ae | ||
![]() |
10da9ee7ba | ||
![]() |
f9eff31205 | ||
![]() |
1d74a029a2 | ||
![]() |
6b8ca514bb | ||
![]() |
f51e555154 | ||
![]() |
61a3c69a06 | ||
![]() |
089615a01e | ||
![]() |
52bee8f81f | ||
![]() |
adc25e648f | ||
![]() |
31da8eac9b | ||
![]() |
e00464435b | ||
![]() |
b81138bda1 | ||
![]() |
6de088140b | ||
![]() |
86d0534638 | ||
![]() |
1033dbca2b | ||
![]() |
b955334882 | ||
![]() |
90ea3bf985 | ||
![]() |
83b0871248 | ||
![]() |
d8aec4b2dc | ||
![]() |
39b302dcad | ||
![]() |
f6125f0c35 | ||
![]() |
f780ac418a | ||
![]() |
61a72a5d13 | ||
![]() |
0c0a354753 | ||
![]() |
3c5f860fb8 | ||
![]() |
3da1fa88d0 | ||
![]() |
fac15aaffb | ||
![]() |
c926021599 | ||
![]() |
543776d9c9 | ||
![]() |
8bf3f9b874 | ||
![]() |
f07f8f7d88 | ||
![]() |
39b40ac1fd | ||
![]() |
ea639269d8 | ||
![]() |
0abaa3ecc5 | ||
![]() |
c4d3efe71d | ||
![]() |
85e82e3d4d | ||
![]() |
f44011519c | ||
![]() |
2c3eeb7194 | ||
![]() |
79839db3a3 | ||
![]() |
d478bdda8e | ||
![]() |
1eae9339f2 | ||
![]() |
923c1b6220 | ||
![]() |
09884e608b |
NEWS
android
doc
meson.buildsrc
MusicChunk.hxx
command
db
decoder
input
plugins
lib
output
pcm
player
playlist
util
test
27
NEWS
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
|
||||
|
10
meson.build
10
meson.build
@@ -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
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user