Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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
output
pcm
util
14
NEWS
14
NEWS
@@ -1,3 +1,17 @@
|
||||
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="34"
|
||||
android:versionName="0.21.11">
|
||||
|
||||
<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.11'
|
||||
# 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.11',
|
||||
meson_version: '>= 0.49.0',
|
||||
default_options: [
|
||||
'c_std=c99',
|
||||
@@ -20,7 +20,7 @@ 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
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
||||
|
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
|
Reference in New Issue
Block a user