Compare commits
85 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
96a31f554a | ||
![]() |
d14ec6aea5 | ||
![]() |
917cedf893 | ||
![]() |
193dd71600 | ||
![]() |
6c293a3d7f | ||
![]() |
e847ddf011 | ||
![]() |
7e8b448985 | ||
![]() |
d1f3a87c08 | ||
![]() |
9f8145e590 | ||
![]() |
791efc171a | ||
![]() |
144312a525 | ||
![]() |
92684112ed | ||
![]() |
ef114ee6cb | ||
![]() |
667f209742 | ||
![]() |
4ad0747c78 | ||
![]() |
c5cf66402c | ||
![]() |
05417049eb | ||
![]() |
c7b0c46d9f | ||
![]() |
df578c91ad | ||
![]() |
70008c47c9 | ||
![]() |
938affef32 | ||
![]() |
a3c33000ee | ||
![]() |
1e54b7b294 | ||
![]() |
cc0dbcf3f4 | ||
![]() |
c5a2cadccc | ||
![]() |
9aa43416b6 | ||
![]() |
8364029db8 | ||
![]() |
d842d21be0 | ||
![]() |
3514fd2433 | ||
![]() |
6778ff27ea | ||
![]() |
f32315d699 | ||
![]() |
8b754b24b6 | ||
![]() |
b1bee9ff38 | ||
![]() |
569be2d402 | ||
![]() |
78a73eac53 | ||
![]() |
533cb99c33 | ||
![]() |
79726940dc | ||
![]() |
27c7891169 | ||
![]() |
7a3a793a12 | ||
![]() |
8088469eca | ||
![]() |
3dcb082015 | ||
![]() |
bece023028 | ||
![]() |
9c4df66925 | ||
![]() |
2b43ceb6c6 | ||
![]() |
c143adba91 | ||
![]() |
142fdc8d86 | ||
![]() |
67778dcd3d | ||
![]() |
ed80863eac | ||
![]() |
c3fc84de12 | ||
![]() |
904f83cd85 | ||
![]() |
28bf100a50 | ||
![]() |
accbd4e82a | ||
![]() |
d7f478c154 | ||
![]() |
8f7f13fea4 | ||
![]() |
c82b03a74c | ||
![]() |
58fb36bdb9 | ||
![]() |
4297a7b0a4 | ||
![]() |
1bab6d0dd7 | ||
![]() |
13b85edbe2 | ||
![]() |
dc53098e43 | ||
![]() |
3c66feff5a | ||
![]() |
218c3bc0d5 | ||
![]() |
9f5eddcd13 | ||
![]() |
3cba76552b | ||
![]() |
e98a8b624b | ||
![]() |
6c6947b01f | ||
![]() |
78c91e9e5b | ||
![]() |
44493ca0c4 | ||
![]() |
42acf78b09 | ||
![]() |
3aa9f8af18 | ||
![]() |
8a32ee30a5 | ||
![]() |
981dc0626b | ||
![]() |
8986d14e98 | ||
![]() |
5163b1a624 | ||
![]() |
860aa9d6d0 | ||
![]() |
64dc5212f9 | ||
![]() |
6cff3214f3 | ||
![]() |
fd910bd5e9 | ||
![]() |
c6086bed41 | ||
![]() |
1a9dfdfab8 | ||
![]() |
5284cd11a9 | ||
![]() |
d1a47cffad | ||
![]() |
f469595eee | ||
![]() |
9cfc52f114 | ||
![]() |
30bfb756c2 |
Makefile.amNEWSconfigure.ac
doc
src
DetachedSong.hxxSongFilter.cxx
db
plugins
decoder
filter
input
lib
curl
expat
nfs
mixer
output
pcm
Dsd16.cxxDsd16.hxxDsd32.cxxDsd32.hxxGlueResampler.cxxGlueResampler.hxxLibsamplerateResampler.cxxLibsamplerateResampler.hxxPcmBuffer.hxxPcmConvert.cxxPcmConvert.hxxPcmDop.cxxPcmDsd.cxxPcmDsd.hxxPcmExport.cxxPcmExport.hxxResampler.hxx
storage
util
test
38
Makefile.am
38
Makefile.am
@@ -233,6 +233,15 @@ libmpd_a_SOURCES += \
|
||||
src/db/Selection.cxx src/db/Selection.hxx
|
||||
endif
|
||||
|
||||
CURL_SOURCES = \
|
||||
src/lib/curl/Version.cxx src/lib/curl/Version.hxx \
|
||||
src/lib/curl/Global.cxx src/lib/curl/Global.hxx \
|
||||
src/lib/curl/Request.cxx src/lib/curl/Request.hxx \
|
||||
src/lib/curl/Handler.hxx \
|
||||
src/lib/curl/Easy.hxx \
|
||||
src/lib/curl/Multi.hxx \
|
||||
src/lib/curl/Slist.hxx
|
||||
|
||||
UPNP_SOURCES = \
|
||||
src/lib/upnp/Init.cxx src/lib/upnp/Init.hxx \
|
||||
src/lib/upnp/ClientInit.cxx src/lib/upnp/ClientInit.hxx \
|
||||
@@ -420,6 +429,7 @@ libutil_a_SOURCES = \
|
||||
src/util/FormatString.cxx src/util/FormatString.hxx \
|
||||
src/util/Tokenizer.cxx src/util/Tokenizer.hxx \
|
||||
src/util/TextFile.hxx \
|
||||
src/util/TimeParser.cxx src/util/TimeParser.hxx \
|
||||
src/util/UriUtil.cxx src/util/UriUtil.hxx \
|
||||
src/util/Manual.hxx \
|
||||
src/util/RefCount.hxx \
|
||||
@@ -565,6 +575,8 @@ PCM_LIBS = \
|
||||
|
||||
if ENABLE_DSD
|
||||
libpcm_a_SOURCES += \
|
||||
src/pcm/Dsd16.cxx src/pcm/Dsd16.hxx \
|
||||
src/pcm/Dsd32.cxx src/pcm/Dsd32.hxx \
|
||||
src/pcm/PcmDsd.cxx src/pcm/PcmDsd.hxx \
|
||||
src/pcm/dsd2pcm/dsd2pcm.c src/pcm/dsd2pcm/dsd2pcm.h
|
||||
endif
|
||||
@@ -689,6 +701,8 @@ libstorage_a_CPPFLAGS = $(AM_CPPFLAGS) \
|
||||
|
||||
STORAGE_LIBS = \
|
||||
libstorage.a \
|
||||
$(CURL_LIBS) \
|
||||
$(EXPAT_LIBS) \
|
||||
$(NFS_LIBS) \
|
||||
$(SMBCLIENT_LIBS)
|
||||
|
||||
@@ -704,6 +718,12 @@ libstorage_a_SOURCES += \
|
||||
src/storage/plugins/NfsStorage.cxx src/storage/plugins/NfsStorage.hxx
|
||||
endif
|
||||
|
||||
if ENABLE_WEBDAV
|
||||
libstorage_a_SOURCES += \
|
||||
src/lib/expat/ExpatParser.cxx \
|
||||
src/storage/plugins/CurlStorage.cxx src/storage/plugins/CurlStorage.hxx
|
||||
endif
|
||||
|
||||
endif
|
||||
|
||||
# neighbor plugins
|
||||
@@ -1279,12 +1299,7 @@ if ENABLE_CURL
|
||||
libinput_a_SOURCES += \
|
||||
src/input/IcyInputStream.cxx src/input/IcyInputStream.hxx \
|
||||
src/input/plugins/CurlInputPlugin.cxx src/input/plugins/CurlInputPlugin.hxx \
|
||||
src/lib/curl/Version.cxx src/lib/curl/Version.hxx \
|
||||
src/lib/curl/Global.cxx src/lib/curl/Global.hxx \
|
||||
src/lib/curl/Request.cxx src/lib/curl/Request.hxx \
|
||||
src/lib/curl/Handler.hxx \
|
||||
src/lib/curl/Easy.hxx \
|
||||
src/lib/curl/Multi.hxx \
|
||||
$(CURL_SOURCES) \
|
||||
src/IcyMetaDataParser.cxx src/IcyMetaDataParser.hxx
|
||||
endif
|
||||
|
||||
@@ -1382,6 +1397,7 @@ libmixer_plugins_a_SOURCES = \
|
||||
src/mixer/plugins/NullMixerPlugin.cxx \
|
||||
src/mixer/plugins/SoftwareMixerPlugin.cxx \
|
||||
src/mixer/plugins/SoftwareMixerPlugin.hxx
|
||||
|
||||
libmixer_plugins_a_CPPFLAGS = $(AM_CPPFLAGS) \
|
||||
$(ALSA_CFLAGS) \
|
||||
$(PULSE_CFLAGS)
|
||||
@@ -1390,7 +1406,10 @@ if ENABLE_ALSA
|
||||
liboutput_plugins_a_SOURCES += \
|
||||
src/output/plugins/AlsaOutputPlugin.cxx \
|
||||
src/output/plugins/AlsaOutputPlugin.hxx
|
||||
libmixer_plugins_a_SOURCES += src/mixer/plugins/AlsaMixerPlugin.cxx
|
||||
libmixer_plugins_a_SOURCES += \
|
||||
src/mixer/plugins/volume_mapping.h \
|
||||
src/mixer/plugins/volume_mapping.c \
|
||||
src/mixer/plugins/AlsaMixerPlugin.cxx
|
||||
endif
|
||||
|
||||
if ANDROID
|
||||
@@ -1573,6 +1592,7 @@ endif
|
||||
|
||||
if ENABLE_EXPAT
|
||||
libplaylist_plugins_a_SOURCES += \
|
||||
src/lib/expat/StreamExpatParser.cxx \
|
||||
src/lib/expat/ExpatParser.cxx src/lib/expat/ExpatParser.hxx \
|
||||
src/playlist/plugins/XspfPlaylistPlugin.cxx \
|
||||
src/playlist/plugins/XspfPlaylistPlugin.hxx \
|
||||
@@ -1758,6 +1778,10 @@ test_run_storage_SOURCES = \
|
||||
test/ScopeIOThread.hxx \
|
||||
test/run_storage.cxx
|
||||
|
||||
if ENABLE_WEBDAV
|
||||
test_run_storage_SOURCES += $(CURL_SOURCES)
|
||||
endif
|
||||
|
||||
endif
|
||||
|
||||
test_run_input_LDADD = \
|
||||
|
29
NEWS
29
NEWS
@@ -1,3 +1,32 @@
|
||||
ver 0.20.2 (2017/01/15)
|
||||
* input
|
||||
- alsa: fix crash bug
|
||||
- alsa: fix buffer overruns
|
||||
* decoder
|
||||
- flac: add options "probesize" and "analyzeduration"
|
||||
* resampler
|
||||
- libsamplerate: reset state after seeking
|
||||
* output
|
||||
- fix static noise after changing to a different audio format
|
||||
- alsa: fix the DSD_U32 sample rate
|
||||
- alsa: fix the DSD_U32 byte order
|
||||
- alsa: support DSD_U16
|
||||
- recorder: fix error "Failed to create : No such file or directory"
|
||||
* playlist
|
||||
- cue: fix skipping songs
|
||||
|
||||
ver 0.20.1 (2017/01/09)
|
||||
* input
|
||||
- curl: fix crash bug
|
||||
- curl: fix freeze bug
|
||||
* decoder
|
||||
- wavpack: fix crash bug
|
||||
* storage
|
||||
- curl: new storage plugin for WebDAV (work in progress)
|
||||
* mixer
|
||||
- alsa: normalize displayed volume according to human perception
|
||||
* fix crash with volume_normalization enabled
|
||||
|
||||
ver 0.20 (2017/01/04)
|
||||
* protocol
|
||||
- "commands" returns playlist commands only if playlist_directory configured
|
||||
|
17
configure.ac
17
configure.ac
@@ -1,10 +1,10 @@
|
||||
AC_PREREQ(2.60)
|
||||
|
||||
AC_INIT(mpd, 0.20, musicpd-dev-team@lists.sourceforge.net)
|
||||
AC_INIT(mpd, 0.20.2, musicpd-dev-team@lists.sourceforge.net)
|
||||
|
||||
VERSION_MAJOR=0
|
||||
VERSION_MINOR=20
|
||||
VERSION_REVISION=0
|
||||
VERSION_REVISION=1
|
||||
VERSION_EXTRA=0
|
||||
|
||||
AC_CONFIG_SRCDIR([src/Main.cxx])
|
||||
@@ -720,6 +720,19 @@ fi
|
||||
MPD_ENABLE_AUTO_PKG(mms, MMS, [libmms >= 0.4],
|
||||
[libmms mms:// protocol support], [libmms not found])
|
||||
|
||||
dnl ---------------------------------------------------------------------------
|
||||
dnl Storage Plugins
|
||||
dnl ---------------------------------------------------------------------------
|
||||
|
||||
MPD_ENABLE_AUTO(webdav, WEBDAV, [WebDAV storage plugin],
|
||||
[WebDAV requires libcurl and libexpat],
|
||||
[auto],
|
||||
[if test x$enable_curl = xyes && test x$enable_expat = xyes; then
|
||||
found_webdav=yes
|
||||
else
|
||||
found_webdav=no
|
||||
fi])
|
||||
|
||||
dnl ---------------------------------------------------------------------------
|
||||
dnl Playlist Plugins
|
||||
dnl ---------------------------------------------------------------------------
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<book>
|
||||
<title>The Music Player Daemon - Developer's Manual</title>
|
||||
|
||||
<chapter>
|
||||
<chapter id="introduction">
|
||||
<title>Introduction</title>
|
||||
|
||||
<para>
|
||||
@@ -21,7 +21,7 @@
|
||||
</para>
|
||||
</chapter>
|
||||
|
||||
<chapter>
|
||||
<chapter id="code_style">
|
||||
<title>Code Style</title>
|
||||
|
||||
<itemizedlist>
|
||||
@@ -40,21 +40,47 @@
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
the code should be C++11 compliant, and must compile with
|
||||
comment your code, document your APIs
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
the code should be C++14 compliant, and must compile with
|
||||
<application>GCC</application> 4.9 and
|
||||
<application>clang</application> 3.4
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
report error conditions with C++ exceptions, preferable
|
||||
derived from <varname>std::runtime_error</varname>
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
all code must be exception-safe
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
classes and functions names use CamelCase; variables are
|
||||
lower-case with words separated by underscore
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<listitem>
|
||||
<para>
|
||||
Some example code:
|
||||
</para>
|
||||
|
||||
<programlisting lang="C">static inline int
|
||||
foo(const char *abc, int xyz)
|
||||
Foo(const char *abc, int xyz)
|
||||
{
|
||||
if (abc == NULL) {
|
||||
if (abc == nullptr) {
|
||||
LogWarning("Foo happened!");
|
||||
return -1;
|
||||
}
|
||||
@@ -66,7 +92,7 @@ foo(const char *abc, int xyz)
|
||||
</itemizedlist>
|
||||
</chapter>
|
||||
|
||||
<chapter>
|
||||
<chapter id="hacking">
|
||||
<title>Hacking The Source</title>
|
||||
|
||||
<para>
|
||||
@@ -151,7 +177,7 @@ foo(const char *abc, int xyz)
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
<chapter>
|
||||
<chapter id="submitting_patches">
|
||||
<title>Submitting Patches</title>
|
||||
|
||||
<para>
|
||||
@@ -167,9 +193,16 @@ foo(const char *abc, int xyz)
|
||||
url="http://git.musicpd.org/account-policy.html">an account on
|
||||
git.musicpd.org</ulink>, but any public git repository will do.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
There is <ulink url="https://github.com/MaxKellermann/MPD">a
|
||||
mirror of the <application>MPD</application> git repository on
|
||||
GitHub</ulink>, and you can use that as well to submit pull
|
||||
requests.
|
||||
</para>
|
||||
</chapter>
|
||||
|
||||
<chapter>
|
||||
<chapter id="tools">
|
||||
<title>Development Tools</title>
|
||||
|
||||
<section>
|
||||
|
58
doc/user.xml
58
doc/user.xml
@@ -1868,11 +1868,23 @@ run</programlisting>
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section id="curl_storage">
|
||||
<title><varname>curl</varname></title>
|
||||
|
||||
<para>
|
||||
A WebDAV client using <filename>libcurl</filename>. It is
|
||||
used when <varname>music_directory</varname> contains a
|
||||
<parameter>http://</parameter> or
|
||||
<parameter>https://</parameter> URI, for example
|
||||
"<parameter>https://the.server/dav/</parameter>".
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section id="smbclient_storage">
|
||||
<title><varname>smbclient</varname></title>
|
||||
|
||||
<para>
|
||||
Load music files from a SMB/CIFS server. It used used when
|
||||
Load music files from a SMB/CIFS server. It is used when
|
||||
<varname>music_directory</varname> contains a
|
||||
<parameter>smb://</parameter> URI, for example
|
||||
"<parameter>smb://myfileserver/Music</parameter>".
|
||||
@@ -1883,7 +1895,7 @@ run</programlisting>
|
||||
<title><varname>nfs</varname></title>
|
||||
|
||||
<para>
|
||||
Load music files from a NFS server. It used used when
|
||||
Load music files from a NFS server. It is used when
|
||||
<varname>music_directory</varname> contains a
|
||||
<parameter>nfs://</parameter> URI according to <ulink
|
||||
url="http://tools.ietf.org/html/rfc2224">RFC2224</ulink>,
|
||||
@@ -2194,6 +2206,48 @@ run</programlisting>
|
||||
Decodes various codecs using
|
||||
<application>FFmpeg</application>.
|
||||
</para>
|
||||
|
||||
<informaltable>
|
||||
<tgroup cols="2">
|
||||
<thead>
|
||||
<row>
|
||||
<entry>Setting</entry>
|
||||
<entry>Description</entry>
|
||||
</row>
|
||||
</thead>
|
||||
<tbody>
|
||||
<row>
|
||||
<entry>
|
||||
<varname>analyzeduration</varname>
|
||||
<parameter>VALUE</parameter>
|
||||
</entry>
|
||||
<entry>
|
||||
Sets the FFmpeg muxer option
|
||||
<varname>analyzeduration</varname>, which specifies
|
||||
how many microseconds are analyzed to probe the
|
||||
input. The <ulink
|
||||
url="https://ffmpeg.org/ffmpeg-formats.html">FFmpeg
|
||||
formats documentation</ulink> has more information.
|
||||
</entry>
|
||||
</row>
|
||||
|
||||
<row>
|
||||
<entry>
|
||||
<varname>probesize</varname>
|
||||
<parameter>VALUE</parameter>
|
||||
</entry>
|
||||
<entry>
|
||||
Sets the FFmpeg muxer option
|
||||
<varname>probesize</varname>, which specifies
|
||||
probing size in bytes, i.e. the size of the data to
|
||||
analyze to get stream information. The <ulink
|
||||
url="https://ffmpeg.org/ffmpeg-formats.html">FFmpeg
|
||||
formats documentation</ulink> has more information.
|
||||
</entry>
|
||||
</row>
|
||||
</tbody>
|
||||
</tgroup>
|
||||
</informaltable>
|
||||
</section>
|
||||
|
||||
<section id="flac_decoder">
|
||||
|
@@ -146,7 +146,9 @@ public:
|
||||
*/
|
||||
gcc_pure
|
||||
bool IsSame(const DetachedSong &other) const {
|
||||
return uri == other.uri;
|
||||
return uri == other.uri &&
|
||||
start_time == other.start_time &&
|
||||
end_time == other.end_time;
|
||||
}
|
||||
|
||||
gcc_pure gcc_nonnull_all
|
||||
|
@@ -25,9 +25,12 @@
|
||||
#include "util/ConstBuffer.hxx"
|
||||
#include "util/StringAPI.hxx"
|
||||
#include "util/ASCII.hxx"
|
||||
#include "util/TimeParser.hxx"
|
||||
#include "util/UriUtil.hxx"
|
||||
#include "lib/icu/Collate.hxx"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -180,24 +183,6 @@ SongFilter::~SongFilter()
|
||||
/* this destructor exists here just so it won't get inlined */
|
||||
}
|
||||
|
||||
#if !defined(__GLIBC__) && !defined(WIN32)
|
||||
|
||||
/**
|
||||
* Determine the time zone offset in a portable way.
|
||||
*/
|
||||
gcc_const
|
||||
static time_t
|
||||
GetTimeZoneOffset()
|
||||
{
|
||||
time_t t = 1234567890;
|
||||
struct tm tm;
|
||||
tm.tm_isdst = 0;
|
||||
gmtime_r(&t, &tm);
|
||||
return t - mktime(&tm);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
gcc_pure
|
||||
static time_t
|
||||
ParseTimeStamp(const char *s)
|
||||
@@ -210,26 +195,13 @@ ParseTimeStamp(const char *s)
|
||||
/* it's an integral UNIX time stamp */
|
||||
return (time_t)value;
|
||||
|
||||
#ifdef WIN32
|
||||
/* TODO: emulate strptime()? */
|
||||
return 0;
|
||||
#else
|
||||
/* try ISO 8601 */
|
||||
|
||||
struct tm tm;
|
||||
const char *end = strptime(s, "%FT%TZ", &tm);
|
||||
if (end == nullptr || *end != 0)
|
||||
try {
|
||||
/* try ISO 8601 */
|
||||
const auto t = ParseTimePoint(s, "%FT%TZ");
|
||||
return std::chrono::system_clock::to_time_t(t);
|
||||
} catch (const std::runtime_error &) {
|
||||
return 0;
|
||||
|
||||
#ifdef __GLIBC__
|
||||
/* timegm() is a GNU extension */
|
||||
return timegm(&tm);
|
||||
#else
|
||||
tm.tm_isdst = 0;
|
||||
return mktime(&tm) + GetTimeZoneOffset();
|
||||
#endif /* !__GLIBC__ */
|
||||
|
||||
#endif /* !WIN32 */
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
|
@@ -124,7 +124,7 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void StartElement(const XML_Char *name, const XML_Char **attrs)
|
||||
void StartElement(const XML_Char *name, const XML_Char **attrs) override
|
||||
{
|
||||
if (object.type != UPnPDirObject::Type::UNKNOWN &&
|
||||
tag_type == TAG_NUM_OF_ITEM_TYPES) {
|
||||
@@ -188,7 +188,7 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
virtual void EndElement(const XML_Char *name)
|
||||
void EndElement(const XML_Char *name) override
|
||||
{
|
||||
if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
|
||||
assert(object.type != UPnPDirObject::Type::UNKNOWN);
|
||||
@@ -212,7 +212,7 @@ protected:
|
||||
state = NONE;
|
||||
}
|
||||
|
||||
virtual void CharacterData(const XML_Char *s, int len)
|
||||
void CharacterData(const XML_Char *s, int len) override
|
||||
{
|
||||
if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
|
||||
assert(object.type != UPnPDirObject::Type::UNKNOWN);
|
||||
|
@@ -77,9 +77,9 @@ public:
|
||||
static Database *Create(EventLoop &loop, DatabaseListener &listener,
|
||||
const ConfigBlock &block);
|
||||
|
||||
virtual void Open() override;
|
||||
virtual void Close() override;
|
||||
virtual const LightSong *GetSong(const char *uri_utf8) const override;
|
||||
void Open() override;
|
||||
void Close() override;
|
||||
const LightSong *GetSong(const char *uri_utf8) const override;
|
||||
void ReturnSong(const LightSong *song) const override;
|
||||
|
||||
void Visit(const DatabaseSelection &selection,
|
||||
|
@@ -317,6 +317,9 @@ DecoderBridge::CommandFinished()
|
||||
|
||||
dc.pipe->Clear(*dc.buffer);
|
||||
|
||||
if (convert != nullptr)
|
||||
convert->Reset();
|
||||
|
||||
timestamp = dc.seek_time.ToDoubleS();
|
||||
}
|
||||
|
||||
|
@@ -56,6 +56,11 @@ extern "C" {
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* Muxer options to be passed to avformat_open_input().
|
||||
*/
|
||||
static AVDictionary *avformat_options = nullptr;
|
||||
|
||||
static AVFormatContext *
|
||||
FfmpegOpenInput(AVIOContext *pb,
|
||||
const char *filename,
|
||||
@@ -67,7 +72,11 @@ FfmpegOpenInput(AVIOContext *pb,
|
||||
|
||||
context->pb = pb;
|
||||
|
||||
int err = avformat_open_input(&context, filename, fmt, nullptr);
|
||||
AVDictionary *options = nullptr;
|
||||
AtScopeExit(&options) { av_dict_free(&options); };
|
||||
av_dict_copy(&options, avformat_options, 0);
|
||||
|
||||
int err = avformat_open_input(&context, filename, fmt, &options);
|
||||
if (err < 0)
|
||||
throw MakeFfmpegError(err, "avformat_open_input() failed");
|
||||
|
||||
@@ -75,12 +84,30 @@ FfmpegOpenInput(AVIOContext *pb,
|
||||
}
|
||||
|
||||
static bool
|
||||
ffmpeg_init(gcc_unused const ConfigBlock &block)
|
||||
ffmpeg_init(const ConfigBlock &block)
|
||||
{
|
||||
FfmpegInit();
|
||||
|
||||
static constexpr const char *option_names[] = {
|
||||
"probesize",
|
||||
"analyzeduration",
|
||||
};
|
||||
|
||||
for (const char *name : option_names) {
|
||||
const char *value = block.GetBlockValue(name);
|
||||
if (value != nullptr)
|
||||
av_dict_set(&avformat_options, name, value, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
ffmpeg_finish()
|
||||
{
|
||||
av_dict_free(&avformat_options);
|
||||
}
|
||||
|
||||
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 25, 0) /* FFmpeg 3.1 */
|
||||
|
||||
gcc_pure
|
||||
@@ -967,7 +994,7 @@ static const char *const ffmpeg_mime_types[] = {
|
||||
const struct DecoderPlugin ffmpeg_decoder_plugin = {
|
||||
"ffmpeg",
|
||||
ffmpeg_init,
|
||||
nullptr,
|
||||
ffmpeg_finish,
|
||||
ffmpeg_decode,
|
||||
nullptr,
|
||||
nullptr,
|
||||
|
@@ -536,7 +536,7 @@ wavpack_streamdecode(DecoderClient &client, InputStream &is)
|
||||
auto is_wvc = wavpack_open_wvc(client, is.GetURI());
|
||||
if (is_wvc) {
|
||||
open_flags |= OPEN_WVC;
|
||||
can_seek &= wvc->is.IsSeekable();
|
||||
can_seek &= is_wvc->IsSeekable();
|
||||
|
||||
wvc.reset(new WavpackInput(&client, *is_wvc));
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "AudioFormat.hxx"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
struct AudioFormat;
|
||||
@@ -36,9 +37,10 @@ class Filter {
|
||||
protected:
|
||||
AudioFormat out_audio_format;
|
||||
|
||||
Filter() = default;
|
||||
explicit Filter(AudioFormat _out_audio_format)
|
||||
:out_audio_format(_out_audio_format) {}
|
||||
:out_audio_format(_out_audio_format) {
|
||||
assert(out_audio_format.IsValid());
|
||||
}
|
||||
|
||||
public:
|
||||
virtual ~Filter() {}
|
||||
@@ -50,16 +52,21 @@ public:
|
||||
return out_audio_format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the filter's state, e.g. drop/flush buffers.
|
||||
*/
|
||||
virtual void Reset() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a block of PCM data.
|
||||
*
|
||||
* Throws std::runtime_error on error.
|
||||
*
|
||||
* @param src the input buffer
|
||||
* @param error location to store the error occurring
|
||||
* @return the destination buffer on success (will be
|
||||
* invalidated by deleting this object or the next FilterPCM()
|
||||
* call)
|
||||
* or Reset() call)
|
||||
*/
|
||||
virtual ConstBuffer<void> FilterPCM(ConstBuffer<void> src) = 0;
|
||||
};
|
||||
|
@@ -45,7 +45,15 @@ class AutoConvertFilter final : public Filter {
|
||||
public:
|
||||
AutoConvertFilter(std::unique_ptr<Filter> &&_filter,
|
||||
std::unique_ptr<Filter> &&_convert)
|
||||
:filter(std::move(_filter)), convert(std::move(_convert)) {}
|
||||
:Filter(_filter->GetOutAudioFormat()),
|
||||
filter(std::move(_filter)), convert(std::move(_convert)) {}
|
||||
|
||||
void Reset() override {
|
||||
filter->Reset();
|
||||
|
||||
if (convert)
|
||||
convert->Reset();
|
||||
}
|
||||
|
||||
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
|
||||
};
|
||||
@@ -92,11 +100,8 @@ PreparedAutoConvertFilter::Open(AudioFormat &in_audio_format)
|
||||
ConstBuffer<void>
|
||||
AutoConvertFilter::FilterPCM(ConstBuffer<void> src)
|
||||
{
|
||||
if (convert != nullptr) {
|
||||
if (convert != nullptr)
|
||||
src = convert->FilterPCM(src);
|
||||
if (src.IsNull())
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return filter->FilterPCM(src);
|
||||
}
|
||||
|
@@ -61,6 +61,7 @@ public:
|
||||
}
|
||||
|
||||
/* virtual methods from class Filter */
|
||||
void Reset() override;
|
||||
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
|
||||
};
|
||||
|
||||
@@ -130,6 +131,13 @@ PreparedChainFilter::Open(AudioFormat &in_audio_format)
|
||||
return chain.release();
|
||||
}
|
||||
|
||||
void
|
||||
ChainFilter::Reset()
|
||||
{
|
||||
for (auto &child : children)
|
||||
child.filter->Reset();
|
||||
}
|
||||
|
||||
ConstBuffer<void>
|
||||
ChainFilter::FilterPCM(ConstBuffer<void> src)
|
||||
{
|
||||
|
@@ -52,13 +52,15 @@ public:
|
||||
|
||||
void Set(const AudioFormat &_out_audio_format);
|
||||
|
||||
void Reset() override {
|
||||
state.Reset();
|
||||
}
|
||||
|
||||
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
|
||||
};
|
||||
|
||||
class PreparedConvertFilter final : public PreparedFilter {
|
||||
public:
|
||||
void Set(const AudioFormat &_out_audio_format);
|
||||
|
||||
Filter *Open(AudioFormat &af) override;
|
||||
};
|
||||
|
||||
|
@@ -28,6 +28,7 @@
|
||||
#include "AlsaInputPlugin.hxx"
|
||||
#include "../InputPlugin.hxx"
|
||||
#include "../AsyncInputStream.hxx"
|
||||
#include "event/Call.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/StringCompare.hxx"
|
||||
@@ -56,31 +57,30 @@ static constexpr unsigned int default_rate = 44100; // cd quality
|
||||
static constexpr size_t ALSA_MAX_BUFFERED = default_rate * default_channels * 2;
|
||||
static constexpr size_t ALSA_RESUME_AT = ALSA_MAX_BUFFERED / 2;
|
||||
|
||||
/**
|
||||
* This value should be the same as the read buffer size defined in
|
||||
* PcmDecoderPlugin.cxx:pcm_stream_decode().
|
||||
* We use it to calculate how many audio frames to buffer in the alsa driver
|
||||
* before reading from the device. snd_pcm_readi() blocks until that many
|
||||
* frames are ready.
|
||||
*/
|
||||
static constexpr size_t read_buffer_size = 4096;
|
||||
|
||||
class AlsaInputStream final
|
||||
: public AsyncInputStream,
|
||||
MultiSocketMonitor, DeferredMonitor {
|
||||
snd_pcm_t *capture_handle;
|
||||
size_t frame_size;
|
||||
|
||||
/**
|
||||
* The configured name of the ALSA device.
|
||||
*/
|
||||
const std::string device;
|
||||
|
||||
snd_pcm_t *const capture_handle;
|
||||
const size_t frame_size;
|
||||
|
||||
ReusableArray<pollfd> pfd_buffer;
|
||||
|
||||
public:
|
||||
AlsaInputStream(EventLoop &loop,
|
||||
const char *_uri, Mutex &_mutex, Cond &_cond,
|
||||
const char *_device,
|
||||
snd_pcm_t *_handle, int _frame_size)
|
||||
:AsyncInputStream(_uri, _mutex, _cond,
|
||||
ALSA_MAX_BUFFERED, ALSA_RESUME_AT),
|
||||
MultiSocketMonitor(loop),
|
||||
DeferredMonitor(loop),
|
||||
device(_device),
|
||||
capture_handle(_handle),
|
||||
frame_size(_frame_size)
|
||||
{
|
||||
@@ -99,6 +99,14 @@ public:
|
||||
}
|
||||
|
||||
~AlsaInputStream() {
|
||||
/* ClearSocketList must be called from within the
|
||||
IOThread; if we don't do it manually here, the
|
||||
~MultiSocketMonitor() will do it in the current
|
||||
thread */
|
||||
BlockingCall(MultiSocketMonitor::GetEventLoop(), [this](){
|
||||
ClearSocketList();
|
||||
});
|
||||
|
||||
snd_pcm_close(capture_handle);
|
||||
}
|
||||
|
||||
@@ -162,7 +170,7 @@ AlsaInputStream::Create(const char *uri, Mutex &mutex, Cond &cond)
|
||||
int frame_size = snd_pcm_format_width(format) / 8 * channels;
|
||||
return new AlsaInputStream(io_thread_get(),
|
||||
uri, mutex, cond,
|
||||
handle, frame_size);
|
||||
device, handle, frame_size);
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::duration
|
||||
@@ -205,6 +213,9 @@ AlsaInputStream::DispatchSockets()
|
||||
snd_pcm_sframes_t n_frames;
|
||||
while ((n_frames = snd_pcm_readi(capture_handle,
|
||||
w.data, w_frames)) < 0) {
|
||||
if (n_frames == -EAGAIN)
|
||||
return;
|
||||
|
||||
if (Recover(n_frames) < 0) {
|
||||
postponed_exception = std::make_exception_ptr(std::runtime_error("PCM error - stream aborted"));
|
||||
cond.broadcast();
|
||||
@@ -221,20 +232,51 @@ AlsaInputStream::Recover(int err)
|
||||
{
|
||||
switch(err) {
|
||||
case -EPIPE:
|
||||
LogDebug(alsa_input_domain, "Buffer Overrun");
|
||||
// drop through
|
||||
FormatDebug(alsa_input_domain,
|
||||
"Overrun on ALSA capture device \"%s\"",
|
||||
device.c_str());
|
||||
break;
|
||||
|
||||
case -ESTRPIPE:
|
||||
FormatDebug(alsa_input_domain,
|
||||
"ALSA capture device \"%s\" was suspended",
|
||||
device.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
switch (snd_pcm_state(capture_handle)) {
|
||||
case SND_PCM_STATE_PAUSED:
|
||||
err = snd_pcm_pause(capture_handle, /* disable */ 0);
|
||||
break;
|
||||
|
||||
case SND_PCM_STATE_SUSPENDED:
|
||||
err = snd_pcm_resume(capture_handle);
|
||||
if (err == -EAGAIN)
|
||||
return 0;
|
||||
/* fall-through to snd_pcm_prepare: */
|
||||
#if GCC_CHECK_VERSION(7,0)
|
||||
[[fallthrough]];
|
||||
#endif
|
||||
|
||||
case -ESTRPIPE:
|
||||
case -EINTR:
|
||||
err = snd_pcm_recover(capture_handle, err, 1);
|
||||
case SND_PCM_STATE_OPEN:
|
||||
case SND_PCM_STATE_SETUP:
|
||||
case SND_PCM_STATE_XRUN:
|
||||
err = snd_pcm_prepare(capture_handle);
|
||||
if (err == 0)
|
||||
err = snd_pcm_start(capture_handle);
|
||||
break;
|
||||
|
||||
case SND_PCM_STATE_DISCONNECTED:
|
||||
break;
|
||||
|
||||
case SND_PCM_STATE_PREPARED:
|
||||
case SND_PCM_STATE_RUNNING:
|
||||
case SND_PCM_STATE_DRAINING:
|
||||
/* this is no error, so just keep running */
|
||||
err = 0;
|
||||
break;
|
||||
default:
|
||||
// something broken somewhere, give up
|
||||
err = -1;
|
||||
}
|
||||
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -273,23 +315,63 @@ ConfigureCapture(snd_pcm_t *capture_handle,
|
||||
throw FormatRuntimeError("Cannot set sample rate (%s)",
|
||||
snd_strerror(err));
|
||||
|
||||
/* period needs to be big enough so that poll() doesn't fire too often,
|
||||
* but small enough that buffer overruns don't occur if Read() is not
|
||||
* invoked often enough.
|
||||
* the calculation here is empirical; however all measurements were
|
||||
* done using 44100:16:2. When we extend this plugin to support
|
||||
* other audio formats then this may need to be revisited */
|
||||
snd_pcm_uframes_t period = read_buffer_size * 2;
|
||||
int direction = -1;
|
||||
if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params,
|
||||
&period, &direction)) < 0)
|
||||
throw FormatRuntimeError("Cannot set period size (%s)",
|
||||
snd_strerror(err));
|
||||
snd_pcm_uframes_t buffer_size_min, buffer_size_max;
|
||||
snd_pcm_hw_params_get_buffer_size_min(hw_params, &buffer_size_min);
|
||||
snd_pcm_hw_params_get_buffer_size_max(hw_params, &buffer_size_max);
|
||||
unsigned buffer_time_min, buffer_time_max;
|
||||
snd_pcm_hw_params_get_buffer_time_min(hw_params, &buffer_time_min, 0);
|
||||
snd_pcm_hw_params_get_buffer_time_max(hw_params, &buffer_time_max, 0);
|
||||
FormatDebug(alsa_input_domain, "buffer: size=%u..%u time=%u..%u",
|
||||
(unsigned)buffer_size_min, (unsigned)buffer_size_max,
|
||||
buffer_time_min, buffer_time_max);
|
||||
|
||||
snd_pcm_uframes_t period_size_min, period_size_max;
|
||||
snd_pcm_hw_params_get_period_size_min(hw_params, &period_size_min, 0);
|
||||
snd_pcm_hw_params_get_period_size_max(hw_params, &period_size_max, 0);
|
||||
unsigned period_time_min, period_time_max;
|
||||
snd_pcm_hw_params_get_period_time_min(hw_params, &period_time_min, 0);
|
||||
snd_pcm_hw_params_get_period_time_max(hw_params, &period_time_max, 0);
|
||||
FormatDebug(alsa_input_domain, "period: size=%u..%u time=%u..%u",
|
||||
(unsigned)period_size_min, (unsigned)period_size_max,
|
||||
period_time_min, period_time_max);
|
||||
|
||||
/* choose the maximum possible buffer_size ... */
|
||||
snd_pcm_hw_params_set_buffer_size(capture_handle, hw_params,
|
||||
buffer_size_max);
|
||||
|
||||
/* ... and calculate the period_size to have four periods in
|
||||
one buffer; this way, we get woken up often enough to avoid
|
||||
buffer overruns, but not too often */
|
||||
snd_pcm_uframes_t buffer_size;
|
||||
if (snd_pcm_hw_params_get_buffer_size(hw_params, &buffer_size) == 0) {
|
||||
snd_pcm_uframes_t period_size = buffer_size / 4;
|
||||
int direction = -1;
|
||||
if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params,
|
||||
&period_size, &direction)) < 0)
|
||||
throw FormatRuntimeError("Cannot set period size (%s)",
|
||||
snd_strerror(err));
|
||||
}
|
||||
|
||||
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0)
|
||||
throw FormatRuntimeError("Cannot set parameters (%s)",
|
||||
snd_strerror(err));
|
||||
|
||||
snd_pcm_uframes_t alsa_buffer_size;
|
||||
err = snd_pcm_hw_params_get_buffer_size(hw_params, &alsa_buffer_size);
|
||||
if (err < 0)
|
||||
throw FormatRuntimeError("snd_pcm_hw_params_get_buffer_size() failed: %s",
|
||||
snd_strerror(-err));
|
||||
|
||||
snd_pcm_uframes_t alsa_period_size;
|
||||
err = snd_pcm_hw_params_get_period_size(hw_params, &alsa_period_size,
|
||||
nullptr);
|
||||
if (err < 0)
|
||||
throw FormatRuntimeError("snd_pcm_hw_params_get_period_size() failed: %s",
|
||||
snd_strerror(-err));
|
||||
|
||||
FormatDebug(alsa_input_domain, "buffer_size=%u period_size=%u",
|
||||
(unsigned)alsa_buffer_size, (unsigned)alsa_period_size);
|
||||
|
||||
snd_pcm_sw_params_t *sw_params;
|
||||
|
||||
snd_pcm_sw_params_malloc(&sw_params);
|
||||
@@ -299,11 +381,6 @@ ConfigureCapture(snd_pcm_t *capture_handle,
|
||||
snd_pcm_sw_params_free(sw_params);
|
||||
};
|
||||
|
||||
if ((err = snd_pcm_sw_params_set_start_threshold(capture_handle, sw_params,
|
||||
period)) < 0)
|
||||
throw FormatRuntimeError("unable to set start threshold (%s)",
|
||||
snd_strerror(err));
|
||||
|
||||
if ((err = snd_pcm_sw_params(capture_handle, sw_params)) < 0)
|
||||
throw FormatRuntimeError("unable to install sw params (%s)",
|
||||
snd_strerror(err));
|
||||
@@ -316,7 +393,8 @@ AlsaInputStream::OpenDevice(const char *device,
|
||||
snd_pcm_t *capture_handle;
|
||||
int err;
|
||||
if ((err = snd_pcm_open(&capture_handle, device,
|
||||
SND_PCM_STREAM_CAPTURE, 0)) < 0)
|
||||
SND_PCM_STREAM_CAPTURE,
|
||||
SND_PCM_NONBLOCK)) < 0)
|
||||
throw FormatRuntimeError("Failed to open device: %s (%s)",
|
||||
device, snd_strerror(err));
|
||||
|
||||
|
@@ -23,6 +23,7 @@
|
||||
#include "lib/curl/Global.hxx"
|
||||
#include "lib/curl/Request.hxx"
|
||||
#include "lib/curl/Handler.hxx"
|
||||
#include "lib/curl/Slist.hxx"
|
||||
#include "../AsyncInputStream.hxx"
|
||||
#include "../IcyInputStream.hxx"
|
||||
#include "../InputPlugin.hxx"
|
||||
@@ -64,7 +65,7 @@ struct CurlInputStream final : public AsyncInputStream, CurlResponseHandler {
|
||||
/* some buffers which were passed to libcurl, which we have
|
||||
too free */
|
||||
char range[32];
|
||||
struct curl_slist *request_headers;
|
||||
CurlSlist request_headers;
|
||||
|
||||
CurlRequest *request = nullptr;
|
||||
|
||||
@@ -75,7 +76,6 @@ struct CurlInputStream final : public AsyncInputStream, CurlResponseHandler {
|
||||
:AsyncInputStream(_url, _mutex, _cond,
|
||||
CURL_MAX_BUFFERED,
|
||||
CURL_RESUME_AT),
|
||||
request_headers(nullptr),
|
||||
icy(new IcyInputStream(this)) {
|
||||
}
|
||||
|
||||
@@ -155,8 +155,7 @@ CurlInputStream::FreeEasy()
|
||||
delete request;
|
||||
request = nullptr;
|
||||
|
||||
curl_slist_free_all(request_headers);
|
||||
request_headers = nullptr;
|
||||
request_headers.Clear();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -371,10 +370,11 @@ CurlInputStream::InitEasy()
|
||||
request->SetOption(CURLOPT_SSL_VERIFYPEER, verify_peer ? 1l : 0l);
|
||||
request->SetOption(CURLOPT_SSL_VERIFYHOST, verify_host ? 2l : 0l);
|
||||
|
||||
request_headers = nullptr;
|
||||
request_headers = curl_slist_append(request_headers,
|
||||
"Icy-Metadata: 1");
|
||||
request->SetOption(CURLOPT_HTTPHEADER, request_headers);
|
||||
request_headers.Clear();
|
||||
request_headers.Append("Icy-Metadata: 1");
|
||||
request->SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
|
||||
|
||||
request->Start();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -385,11 +385,13 @@ CurlInputStream::SeekInternal(offset_type new_offset)
|
||||
FreeEasy();
|
||||
|
||||
offset = new_offset;
|
||||
if (offset == size)
|
||||
if (offset == size) {
|
||||
/* seek to EOF: simulate empty result; avoid
|
||||
triggering a "416 Requested Range Not Satisfiable"
|
||||
response */
|
||||
SeekDone();
|
||||
return;
|
||||
}
|
||||
|
||||
InitEasy();
|
||||
|
||||
|
@@ -96,7 +96,7 @@ private:
|
||||
};
|
||||
|
||||
CurlGlobal::CurlGlobal(EventLoop &_loop)
|
||||
:TimeoutMonitor(_loop)
|
||||
:TimeoutMonitor(_loop), DeferredMonitor(_loop)
|
||||
{
|
||||
multi.SetOption(CURLMOPT_SOCKETFUNCTION, CurlSocket::SocketFunction);
|
||||
multi.SetOption(CURLMOPT_SOCKETDATA, this);
|
||||
@@ -217,25 +217,31 @@ CurlGlobal::ReadInfo()
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
CurlGlobal::TimerFunction(gcc_unused CURLM *_global, long timeout_ms, void *userp)
|
||||
inline void
|
||||
CurlGlobal::UpdateTimeout(long timeout_ms)
|
||||
{
|
||||
auto &global = *(CurlGlobal *)userp;
|
||||
assert(_global == global.multi.Get());
|
||||
|
||||
if (timeout_ms < 0) {
|
||||
global.Cancel();
|
||||
return 0;
|
||||
TimeoutMonitor::Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeout_ms >= 0 && timeout_ms < 10)
|
||||
if (timeout_ms < 10)
|
||||
/* CURL 7.21.1 likes to report "timeout=0", which
|
||||
means we're running in a busy loop. Quite a bad
|
||||
idea to waste so much CPU. Let's use a lower limit
|
||||
of 10ms. */
|
||||
timeout_ms = 10;
|
||||
|
||||
global.Schedule(std::chrono::milliseconds(timeout_ms));
|
||||
TimeoutMonitor::Schedule(std::chrono::milliseconds(timeout_ms));
|
||||
}
|
||||
|
||||
int
|
||||
CurlGlobal::TimerFunction(gcc_unused CURLM *_global, long timeout_ms, void *userp)
|
||||
{
|
||||
auto &global = *(CurlGlobal *)userp;
|
||||
assert(_global == global.multi.Get());
|
||||
|
||||
global.UpdateTimeout(timeout_ms);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -256,5 +262,11 @@ CurlGlobal::SocketAction(curl_socket_t fd, int ev_bitmask)
|
||||
"curl_multi_socket_action() failed: %s",
|
||||
curl_multi_strerror(mcode));
|
||||
|
||||
DeferredMonitor::Schedule();
|
||||
}
|
||||
|
||||
void
|
||||
CurlGlobal::RunDeferred()
|
||||
{
|
||||
ReadInfo();
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@
|
||||
|
||||
#include "Multi.hxx"
|
||||
#include "event/TimeoutMonitor.hxx"
|
||||
#include "event/DeferredMonitor.hxx"
|
||||
|
||||
class CurlSocket;
|
||||
class CurlRequest;
|
||||
@@ -39,12 +40,14 @@ class CurlRequest;
|
||||
/**
|
||||
* Manager for the global CURLM object.
|
||||
*/
|
||||
class CurlGlobal final : private TimeoutMonitor {
|
||||
class CurlGlobal final : TimeoutMonitor, DeferredMonitor {
|
||||
CurlMulti multi;
|
||||
|
||||
public:
|
||||
explicit CurlGlobal(EventLoop &_loop);
|
||||
|
||||
using TimeoutMonitor::GetEventLoop;
|
||||
|
||||
void Add(CURL *easy, CurlRequest &request);
|
||||
void Remove(CURL *easy);
|
||||
|
||||
@@ -76,9 +79,14 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void UpdateTimeout(long timeout_ms);
|
||||
static int TimerFunction(CURLM *global, long timeout_ms, void *userp);
|
||||
|
||||
virtual void OnTimeout() override;
|
||||
/* virtual methods from class TimeoutMonitor */
|
||||
void OnTimeout() override;
|
||||
|
||||
/* virtual methods from class DeferredMonitor */
|
||||
void RunDeferred() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@@ -76,7 +76,7 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
CURL *Get() {
|
||||
CURLM *Get() {
|
||||
return handle;
|
||||
}
|
||||
|
||||
|
@@ -46,7 +46,8 @@
|
||||
|
||||
CurlRequest::CurlRequest(CurlGlobal &_global, const char *url,
|
||||
CurlResponseHandler &_handler)
|
||||
:global(_global), handler(_handler)
|
||||
:DeferredMonitor(_global.GetEventLoop()),
|
||||
global(_global), handler(_handler)
|
||||
{
|
||||
error_buffer[0] = 0;
|
||||
|
||||
@@ -62,8 +63,6 @@ CurlRequest::CurlRequest(CurlGlobal &_global, const char *url,
|
||||
easy.SetOption(CURLOPT_NOSIGNAL, 1l);
|
||||
easy.SetOption(CURLOPT_CONNECTTIMEOUT, 10l);
|
||||
easy.SetOption(CURLOPT_URL, url);
|
||||
|
||||
global.Add(easy.Get(), *this);
|
||||
}
|
||||
|
||||
CurlRequest::~CurlRequest()
|
||||
@@ -71,19 +70,40 @@ CurlRequest::~CurlRequest()
|
||||
FreeEasy();
|
||||
}
|
||||
|
||||
void
|
||||
CurlRequest::Start()
|
||||
{
|
||||
assert(!registered);
|
||||
|
||||
global.Add(easy.Get(), *this);
|
||||
registered = true;
|
||||
}
|
||||
|
||||
void
|
||||
CurlRequest::Stop()
|
||||
{
|
||||
if (!registered)
|
||||
return;
|
||||
|
||||
global.Remove(easy.Get());
|
||||
registered = false;
|
||||
}
|
||||
|
||||
void
|
||||
CurlRequest::FreeEasy()
|
||||
{
|
||||
if (!easy)
|
||||
return;
|
||||
|
||||
global.Remove(easy.Get());
|
||||
Stop();
|
||||
easy = nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
CurlRequest::Resume()
|
||||
{
|
||||
assert(registered);
|
||||
|
||||
curl_easy_pause(easy.Get(), CURLPAUSE_CONT);
|
||||
|
||||
if (IsCurlOlderThan(0x072000))
|
||||
@@ -95,32 +115,24 @@ CurlRequest::Resume()
|
||||
global.InvalidateSockets();
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
CurlRequest::FinishHeaders()
|
||||
{
|
||||
if (state != State::HEADERS)
|
||||
return true;
|
||||
return;
|
||||
|
||||
state = State::BODY;
|
||||
|
||||
long status = 0;
|
||||
curl_easy_getinfo(easy.Get(), CURLINFO_RESPONSE_CODE, &status);
|
||||
|
||||
try {
|
||||
handler.OnHeaders(status, std::move(headers));
|
||||
return true;
|
||||
} catch (...) {
|
||||
state = State::CLOSED;
|
||||
handler.OnError(std::current_exception());
|
||||
return false;
|
||||
}
|
||||
handler.OnHeaders(status, std::move(headers));
|
||||
}
|
||||
|
||||
void
|
||||
CurlRequest::FinishBody()
|
||||
{
|
||||
if (!FinishHeaders())
|
||||
return;
|
||||
FinishHeaders();
|
||||
|
||||
if (state != State::BODY)
|
||||
return;
|
||||
@@ -132,7 +144,7 @@ CurlRequest::FinishBody()
|
||||
void
|
||||
CurlRequest::Done(CURLcode result)
|
||||
{
|
||||
FreeEasy();
|
||||
Stop();
|
||||
|
||||
try {
|
||||
if (result != CURLE_OK) {
|
||||
@@ -148,7 +160,12 @@ CurlRequest::Done(CURLcode result)
|
||||
return;
|
||||
}
|
||||
|
||||
FinishBody();
|
||||
try {
|
||||
FinishBody();
|
||||
} catch (...) {
|
||||
state = State::CLOSED;
|
||||
handler.OnError(std::current_exception());
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
@@ -202,18 +219,19 @@ CurlRequest::DataReceived(const void *ptr, size_t received_size)
|
||||
{
|
||||
assert(received_size > 0);
|
||||
|
||||
if (!FinishHeaders())
|
||||
return 0;
|
||||
|
||||
try {
|
||||
FinishHeaders();
|
||||
handler.OnData({ptr, received_size});
|
||||
return received_size;
|
||||
} catch (Pause) {
|
||||
return CURL_WRITEFUNC_PAUSE;
|
||||
} catch (...) {
|
||||
state = State::CLOSED;
|
||||
handler.OnError(std::current_exception());
|
||||
return 0;
|
||||
/* move the CurlResponseHandler::OnError() call into a
|
||||
"safe" stack frame */
|
||||
postponed_error = std::current_exception();
|
||||
DeferredMonitor::Schedule();
|
||||
return CURL_WRITEFUNC_PAUSE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -229,3 +247,11 @@ CurlRequest::WriteFunction(void *ptr, size_t size, size_t nmemb, void *stream)
|
||||
|
||||
return c.DataReceived(ptr, size);
|
||||
}
|
||||
|
||||
void
|
||||
CurlRequest::RunDeferred()
|
||||
{
|
||||
assert(postponed_error);
|
||||
|
||||
handler.OnError(postponed_error);
|
||||
}
|
||||
|
@@ -31,15 +31,17 @@
|
||||
#define CURL_REQUEST_HXX
|
||||
|
||||
#include "Easy.hxx"
|
||||
#include "event/DeferredMonitor.hxx"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <exception>
|
||||
|
||||
struct StringView;
|
||||
class CurlGlobal;
|
||||
class CurlResponseHandler;
|
||||
|
||||
class CurlRequest {
|
||||
class CurlRequest final : DeferredMonitor {
|
||||
CurlGlobal &global;
|
||||
|
||||
CurlResponseHandler &handler;
|
||||
@@ -55,10 +57,25 @@ class CurlRequest {
|
||||
|
||||
std::multimap<std::string, std::string> headers;
|
||||
|
||||
/**
|
||||
* An exception caught by DataReceived(), which will be
|
||||
* forwarded into a "safe" stack frame by
|
||||
* DeferredMonitor::RunDeferred(). This works around the
|
||||
* problem that libcurl crashes if you call
|
||||
* curl_multi_remove_handle() from within the WRITEFUNCTION
|
||||
* (i.e. DataReceived()).
|
||||
*/
|
||||
std::exception_ptr postponed_error;
|
||||
|
||||
/** error message provided by libcurl */
|
||||
char error_buffer[CURL_ERROR_SIZE];
|
||||
|
||||
bool registered = false;
|
||||
|
||||
public:
|
||||
/**
|
||||
* To start sending the request, call Start().
|
||||
*/
|
||||
CurlRequest(CurlGlobal &_global, const char *url,
|
||||
CurlResponseHandler &_handler);
|
||||
~CurlRequest();
|
||||
@@ -66,6 +83,21 @@ public:
|
||||
CurlRequest(const CurlRequest &) = delete;
|
||||
CurlRequest &operator=(const CurlRequest &) = delete;
|
||||
|
||||
/**
|
||||
* Register this request via CurlGlobal::Add(), which starts
|
||||
* the request.
|
||||
*
|
||||
* This method must be called in the event loop thread.
|
||||
*/
|
||||
void Start();
|
||||
|
||||
/**
|
||||
* Unregister this request via CurlGlobal::Remove().
|
||||
*
|
||||
* This method must be called in the event loop thread.
|
||||
*/
|
||||
void Stop();
|
||||
|
||||
CURL *Get() {
|
||||
return easy.Get();
|
||||
}
|
||||
@@ -95,7 +127,7 @@ private:
|
||||
*/
|
||||
void FreeEasy();
|
||||
|
||||
bool FinishHeaders();
|
||||
void FinishHeaders();
|
||||
void FinishBody();
|
||||
|
||||
size_t DataReceived(const void *ptr, size_t size);
|
||||
@@ -109,6 +141,9 @@ private:
|
||||
/** called by curl when new data is available */
|
||||
static size_t WriteFunction(void *ptr, size_t size, size_t nmemb,
|
||||
void *stream);
|
||||
|
||||
/* virtual methods from class DeferredMonitor */
|
||||
void RunDeferred() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
76
src/lib/curl/Slist.hxx
Normal file
76
src/lib/curl/Slist.hxx
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2017 Max Kellermann <max@duempel.org>
|
||||
*
|
||||
* 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 CURL_SLIST_HXX
|
||||
#define CURL_SLIST_HXX
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
* OO wrapper for "struct curl_slist *".
|
||||
*/
|
||||
class CurlSlist {
|
||||
struct curl_slist *head = nullptr;
|
||||
|
||||
public:
|
||||
CurlSlist() = default;
|
||||
|
||||
CurlSlist(CurlSlist &&src)
|
||||
:head(std::exchange(src.head, nullptr)) {}
|
||||
|
||||
~CurlSlist() {
|
||||
if (head != nullptr)
|
||||
curl_slist_free_all(head);
|
||||
}
|
||||
|
||||
CurlSlist &operator=(CurlSlist &&src) {
|
||||
std::swap(head, src.head);
|
||||
return *this;
|
||||
}
|
||||
|
||||
struct curl_slist *Get() {
|
||||
return head;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
curl_slist_free_all(head);
|
||||
head = nullptr;
|
||||
}
|
||||
|
||||
void Append(const char *value) {
|
||||
auto *new_head = curl_slist_append(head, value);
|
||||
if (new_head == nullptr)
|
||||
throw std::runtime_error("curl_slist_append() failed");
|
||||
head = new_head;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
@@ -19,7 +19,6 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "ExpatParser.hxx"
|
||||
#include "input/InputStream.hxx"
|
||||
#include "util/ASCII.hxx"
|
||||
|
||||
#include <string.h>
|
||||
@@ -31,23 +30,6 @@ ExpatParser::Parse(const char *data, size_t length, bool is_final)
|
||||
throw ExpatError(parser);
|
||||
}
|
||||
|
||||
void
|
||||
ExpatParser::Parse(InputStream &is)
|
||||
{
|
||||
assert(is.IsReady());
|
||||
|
||||
while (true) {
|
||||
char buffer[4096];
|
||||
size_t nbytes = is.LockRead(buffer, sizeof(buffer));
|
||||
if (nbytes == 0)
|
||||
break;
|
||||
|
||||
Parse(buffer, nbytes, false);
|
||||
}
|
||||
|
||||
Parse("", 0, true);
|
||||
}
|
||||
|
||||
const char *
|
||||
ExpatParser::GetAttribute(const XML_Char **atts,
|
||||
const char *name)
|
||||
|
@@ -31,22 +31,31 @@ class InputStream;
|
||||
|
||||
class ExpatError final : public std::runtime_error {
|
||||
public:
|
||||
ExpatError(XML_Error code)
|
||||
explicit ExpatError(XML_Error code)
|
||||
:std::runtime_error(XML_ErrorString(code)) {}
|
||||
|
||||
ExpatError(XML_Parser parser)
|
||||
explicit ExpatError(XML_Parser parser)
|
||||
:ExpatError(XML_GetErrorCode(parser)) {}
|
||||
};
|
||||
|
||||
struct ExpatNamespaceSeparator {
|
||||
char separator;
|
||||
};
|
||||
|
||||
class ExpatParser final {
|
||||
const XML_Parser parser;
|
||||
|
||||
public:
|
||||
ExpatParser(void *userData)
|
||||
explicit ExpatParser(void *userData)
|
||||
:parser(XML_ParserCreate(nullptr)) {
|
||||
XML_SetUserData(parser, userData);
|
||||
}
|
||||
|
||||
ExpatParser(ExpatNamespaceSeparator ns, void *userData)
|
||||
:parser(XML_ParserCreateNS(nullptr, ns.separator)) {
|
||||
XML_SetUserData(parser, userData);
|
||||
}
|
||||
|
||||
~ExpatParser() {
|
||||
XML_ParserFree(parser);
|
||||
}
|
||||
@@ -89,6 +98,12 @@ public:
|
||||
parser.SetCharacterDataHandler(CharacterData);
|
||||
}
|
||||
|
||||
explicit CommonExpatParser(ExpatNamespaceSeparator ns)
|
||||
:parser(ns, this) {
|
||||
parser.SetElementHandler(StartElement, EndElement);
|
||||
parser.SetCharacterDataHandler(CharacterData);
|
||||
}
|
||||
|
||||
void Parse(const char *data, size_t length, bool is_final) {
|
||||
parser.Parse(data, length, is_final);
|
||||
}
|
||||
|
39
src/lib/expat/StreamExpatParser.cxx
Normal file
39
src/lib/expat/StreamExpatParser.cxx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2003-2017 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "ExpatParser.hxx"
|
||||
#include "input/InputStream.hxx"
|
||||
|
||||
void
|
||||
ExpatParser::Parse(InputStream &is)
|
||||
{
|
||||
assert(is.IsReady());
|
||||
|
||||
while (true) {
|
||||
char buffer[4096];
|
||||
size_t nbytes = is.LockRead(buffer, sizeof(buffer));
|
||||
if (nbytes == 0)
|
||||
break;
|
||||
|
||||
Parse(buffer, nbytes, false);
|
||||
}
|
||||
|
||||
Parse("", 0, true);
|
||||
}
|
@@ -24,9 +24,21 @@
|
||||
|
||||
#include <exception>
|
||||
|
||||
/**
|
||||
* Callbacks for an asynchronous libnfs operation.
|
||||
*
|
||||
* Note that no callback is invoked for cancelled operations.
|
||||
*/
|
||||
class NfsCallback {
|
||||
public:
|
||||
/**
|
||||
* The operation completed successfully.
|
||||
*/
|
||||
virtual void OnNfsCallback(unsigned status, void *data) = 0;
|
||||
|
||||
/**
|
||||
* An error has occurred.
|
||||
*/
|
||||
virtual void OnNfsError(std::exception_ptr &&e) = 0;
|
||||
};
|
||||
|
||||
|
@@ -35,6 +35,14 @@
|
||||
struct nfsfh;
|
||||
class NfsConnection;
|
||||
|
||||
/**
|
||||
* A helper class which helps with reading from a file. It obtains a
|
||||
* connection lease (#NfsLease), opens the given file, "stats" the
|
||||
* file, and finally allos you to read its contents.
|
||||
*
|
||||
* To get started, derive your class from it and implement the pure
|
||||
* virtual methods, construct an instance, and call Open().
|
||||
*/
|
||||
class NfsFileReader : NfsLease, NfsCallback, DeferredMonitor {
|
||||
enum class State {
|
||||
INITIAL,
|
||||
@@ -63,14 +71,30 @@ public:
|
||||
void DeferClose();
|
||||
|
||||
/**
|
||||
* Open the file. This method is thread-safe.
|
||||
*
|
||||
* Throws std::runtime_error on error.
|
||||
*/
|
||||
void Open(const char *uri);
|
||||
|
||||
/**
|
||||
* Attempt to read from the file. This may only be done after
|
||||
* OnNfsFileOpen() has been called. Only one read operation
|
||||
* may be performed at a time.
|
||||
*
|
||||
* This method is not thread-safe and must be called from
|
||||
* within the I/O thread.
|
||||
*
|
||||
* Throws std::runtime_error on error.
|
||||
*/
|
||||
void Read(uint64_t offset, size_t size);
|
||||
|
||||
/**
|
||||
* Cancel the most recent Read() call.
|
||||
*
|
||||
* This method is not thread-safe and must be called from
|
||||
* within the I/O thread.
|
||||
*/
|
||||
void CancelRead();
|
||||
|
||||
bool IsIdle() const {
|
||||
@@ -78,8 +102,27 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
/**
|
||||
* The file has been opened successfully. It is a regular
|
||||
* file, and its size is known. It is ready to be read from
|
||||
* using Read().
|
||||
*
|
||||
* This method will be called from within the I/O thread.
|
||||
*/
|
||||
virtual void OnNfsFileOpen(uint64_t size) = 0;
|
||||
|
||||
/**
|
||||
* A Read() has completed successfully.
|
||||
*
|
||||
* This method will be called from within the I/O thread.
|
||||
*/
|
||||
virtual void OnNfsFileRead(const void *data, size_t size) = 0;
|
||||
|
||||
/**
|
||||
* An error has occurred, which can be either while waiting
|
||||
* for OnNfsFileOpen(), or while waiting for OnNfsFileRead(),
|
||||
* or if disconnected while idle.
|
||||
*/
|
||||
virtual void OnNfsFileError(std::exception_ptr &&e) = 0;
|
||||
|
||||
private:
|
||||
|
@@ -25,15 +25,18 @@
|
||||
#include "event/DeferredMonitor.hxx"
|
||||
#include "util/ASCII.hxx"
|
||||
#include "util/ReusableArray.hxx"
|
||||
#include "util/Clamp.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "Log.hxx"
|
||||
|
||||
#include <algorithm>
|
||||
extern "C" {
|
||||
#include "volume_mapping.h"
|
||||
}
|
||||
|
||||
#include <alsa/asoundlib.h>
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#define VOLUME_MIXER_ALSA_DEFAULT "default"
|
||||
#define VOLUME_MIXER_ALSA_CONTROL_DEFAULT "PCM"
|
||||
static constexpr unsigned VOLUME_MIXER_ALSA_INDEX_DEFAULT = 0;
|
||||
@@ -68,9 +71,6 @@ class AlsaMixer final : public Mixer {
|
||||
|
||||
snd_mixer_t *handle;
|
||||
snd_mixer_elem_t *elem;
|
||||
long volume_min;
|
||||
long volume_max;
|
||||
int volume_set;
|
||||
|
||||
AlsaMixerMonitor *monitor;
|
||||
|
||||
@@ -228,9 +228,6 @@ AlsaMixer::Setup()
|
||||
if (elem == nullptr)
|
||||
throw FormatRuntimeError("no such mixer control: %s", control);
|
||||
|
||||
snd_mixer_selem_get_playback_volume_range(elem, &volume_min,
|
||||
&volume_max);
|
||||
|
||||
snd_mixer_elem_set_callback_private(elem, this);
|
||||
snd_mixer_elem_set_callback(elem, alsa_mixer_elem_callback);
|
||||
|
||||
@@ -242,8 +239,6 @@ AlsaMixer::Open()
|
||||
{
|
||||
int err;
|
||||
|
||||
volume_set = -1;
|
||||
|
||||
err = snd_mixer_open(&handle, 0);
|
||||
if (err < 0)
|
||||
throw FormatRuntimeError("snd_mixer_open() failed: %s",
|
||||
@@ -272,8 +267,6 @@ int
|
||||
AlsaMixer::GetVolume()
|
||||
{
|
||||
int err;
|
||||
int ret;
|
||||
long level;
|
||||
|
||||
assert(handle != nullptr);
|
||||
|
||||
@@ -282,43 +275,15 @@ AlsaMixer::GetVolume()
|
||||
throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
|
||||
snd_strerror(err));
|
||||
|
||||
err = snd_mixer_selem_get_playback_volume(elem,
|
||||
SND_MIXER_SCHN_FRONT_LEFT,
|
||||
&level);
|
||||
if (err < 0)
|
||||
throw FormatRuntimeError("failed to read ALSA volume: %s",
|
||||
snd_strerror(err));
|
||||
|
||||
ret = ((volume_set / 100.0) * (volume_max - volume_min)
|
||||
+ volume_min) + 0.5;
|
||||
if (volume_set > 0 && ret == level) {
|
||||
ret = volume_set;
|
||||
} else {
|
||||
ret = (int)(100 * (((float)(level - volume_min)) /
|
||||
(volume_max - volume_min)) + 0.5);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return lrint(100 * get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT));
|
||||
}
|
||||
|
||||
void
|
||||
AlsaMixer::SetVolume(unsigned volume)
|
||||
{
|
||||
float vol;
|
||||
long level;
|
||||
int err;
|
||||
|
||||
assert(handle != nullptr);
|
||||
|
||||
vol = volume;
|
||||
|
||||
volume_set = vol + 0.5;
|
||||
|
||||
level = (long)(((vol / 100.0) * (volume_max - volume_min) +
|
||||
volume_min) + 0.5);
|
||||
level = Clamp(level, volume_min, volume_max);
|
||||
|
||||
err = snd_mixer_selem_set_playback_volume_all(elem, level);
|
||||
int err = set_normalized_playback_volume(elem, 0.01*volume, 1);
|
||||
if (err < 0)
|
||||
throw FormatRuntimeError("failed to set ALSA volume: %s",
|
||||
snd_strerror(err));
|
||||
|
180
src/mixer/plugins/volume_mapping.c
Normal file
180
src/mixer/plugins/volume_mapping.c
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Clemens Ladisch <clemens@ladisch.de>
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The functions in this file map the value ranges of ALSA mixer controls onto
|
||||
* the interval 0..1.
|
||||
*
|
||||
* The mapping is designed so that the position in the interval is proportional
|
||||
* to the volume as a human ear would perceive it (i.e., the position is the
|
||||
* cubic root of the linear sample multiplication factor). For controls with
|
||||
* a small range (24 dB or less), the mapping is linear in the dB values so
|
||||
* that each step has the same size visually. Only for controls without dB
|
||||
* information, a linear mapping of the hardware volume register values is used
|
||||
* (this is the same algorithm as used in the old alsamixer).
|
||||
*
|
||||
* When setting the volume, 'dir' is the rounding direction:
|
||||
* -1/0/1 = down/nearest/up.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include "volume_mapping.h"
|
||||
|
||||
#ifdef __UCLIBC__
|
||||
/* 10^x = 10^(log e^x) = (e^x)^log10 = e^(x * log 10) */
|
||||
#define exp10(x) (exp((x) * log(10)))
|
||||
#endif /* __UCLIBC__ */
|
||||
|
||||
#define MAX_LINEAR_DB_SCALE 24
|
||||
|
||||
static inline bool use_linear_dB_scale(long dBmin, long dBmax)
|
||||
{
|
||||
return dBmax - dBmin <= MAX_LINEAR_DB_SCALE * 100;
|
||||
}
|
||||
|
||||
static long lrint_dir(double x, int dir)
|
||||
{
|
||||
if (dir > 0)
|
||||
return lrint(ceil(x));
|
||||
else if (dir < 0)
|
||||
return lrint(floor(x));
|
||||
else
|
||||
return lrint(x);
|
||||
}
|
||||
|
||||
enum ctl_dir { PLAYBACK, CAPTURE };
|
||||
|
||||
static int (* const get_dB_range[2])(snd_mixer_elem_t *, long *, long *) = {
|
||||
snd_mixer_selem_get_playback_dB_range,
|
||||
snd_mixer_selem_get_capture_dB_range,
|
||||
};
|
||||
static int (* const get_raw_range[2])(snd_mixer_elem_t *, long *, long *) = {
|
||||
snd_mixer_selem_get_playback_volume_range,
|
||||
snd_mixer_selem_get_capture_volume_range,
|
||||
};
|
||||
static int (* const get_dB[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = {
|
||||
snd_mixer_selem_get_playback_dB,
|
||||
snd_mixer_selem_get_capture_dB,
|
||||
};
|
||||
static int (* const get_raw[2])(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *) = {
|
||||
snd_mixer_selem_get_playback_volume,
|
||||
snd_mixer_selem_get_capture_volume,
|
||||
};
|
||||
static int (* const set_dB[2])(snd_mixer_elem_t *, long, int) = {
|
||||
snd_mixer_selem_set_playback_dB_all,
|
||||
snd_mixer_selem_set_capture_dB_all,
|
||||
};
|
||||
static int (* const set_raw[2])(snd_mixer_elem_t *, long) = {
|
||||
snd_mixer_selem_set_playback_volume_all,
|
||||
snd_mixer_selem_set_capture_volume_all,
|
||||
};
|
||||
|
||||
static double get_normalized_volume(snd_mixer_elem_t *elem,
|
||||
snd_mixer_selem_channel_id_t channel,
|
||||
enum ctl_dir ctl_dir)
|
||||
{
|
||||
long min, max, value;
|
||||
double normalized, min_norm;
|
||||
int err;
|
||||
|
||||
err = get_dB_range[ctl_dir](elem, &min, &max);
|
||||
if (err < 0 || min >= max) {
|
||||
err = get_raw_range[ctl_dir](elem, &min, &max);
|
||||
if (err < 0 || min == max)
|
||||
return 0;
|
||||
|
||||
err = get_raw[ctl_dir](elem, channel, &value);
|
||||
if (err < 0)
|
||||
return 0;
|
||||
|
||||
return (value - min) / (double)(max - min);
|
||||
}
|
||||
|
||||
err = get_dB[ctl_dir](elem, channel, &value);
|
||||
if (err < 0)
|
||||
return 0;
|
||||
|
||||
if (use_linear_dB_scale(min, max))
|
||||
return (value - min) / (double)(max - min);
|
||||
|
||||
normalized = exp10((value - max) / 6000.0);
|
||||
if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
|
||||
min_norm = exp10((min - max) / 6000.0);
|
||||
normalized = (normalized - min_norm) / (1 - min_norm);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
static int set_normalized_volume(snd_mixer_elem_t *elem,
|
||||
double volume,
|
||||
int dir,
|
||||
enum ctl_dir ctl_dir)
|
||||
{
|
||||
long min, max, value;
|
||||
double min_norm;
|
||||
int err;
|
||||
|
||||
err = get_dB_range[ctl_dir](elem, &min, &max);
|
||||
if (err < 0 || min >= max) {
|
||||
err = get_raw_range[ctl_dir](elem, &min, &max);
|
||||
if (err < 0)
|
||||
return err;
|
||||
|
||||
value = lrint_dir(volume * (max - min), dir) + min;
|
||||
return set_raw[ctl_dir](elem, value);
|
||||
}
|
||||
|
||||
if (use_linear_dB_scale(min, max)) {
|
||||
value = lrint_dir(volume * (max - min), dir) + min;
|
||||
return set_dB[ctl_dir](elem, value, dir);
|
||||
}
|
||||
|
||||
if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
|
||||
min_norm = exp10((min - max) / 6000.0);
|
||||
volume = volume * (1 - min_norm) + min_norm;
|
||||
}
|
||||
value = lrint_dir(6000.0 * log10(volume), dir) + max;
|
||||
return set_dB[ctl_dir](elem, value, dir);
|
||||
}
|
||||
|
||||
double get_normalized_playback_volume(snd_mixer_elem_t *elem,
|
||||
snd_mixer_selem_channel_id_t channel)
|
||||
{
|
||||
return get_normalized_volume(elem, channel, PLAYBACK);
|
||||
}
|
||||
|
||||
double get_normalized_capture_volume(snd_mixer_elem_t *elem,
|
||||
snd_mixer_selem_channel_id_t channel)
|
||||
{
|
||||
return get_normalized_volume(elem, channel, CAPTURE);
|
||||
}
|
||||
|
||||
|
||||
int set_normalized_playback_volume(snd_mixer_elem_t *elem,
|
||||
double volume,
|
||||
int dir)
|
||||
{
|
||||
return set_normalized_volume(elem, volume, dir, PLAYBACK);
|
||||
}
|
||||
|
||||
int set_normalized_capture_volume(snd_mixer_elem_t *elem,
|
||||
double volume,
|
||||
int dir)
|
||||
{
|
||||
return set_normalized_volume(elem, volume, dir, CAPTURE);
|
||||
}
|
17
src/mixer/plugins/volume_mapping.h
Normal file
17
src/mixer/plugins/volume_mapping.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef VOLUME_MAPPING_H_INCLUDED
|
||||
#define VOLUME_MAPPING_H_INCLUDED
|
||||
|
||||
#include <alsa/asoundlib.h>
|
||||
|
||||
double get_normalized_playback_volume(snd_mixer_elem_t *elem,
|
||||
snd_mixer_selem_channel_id_t channel);
|
||||
double get_normalized_capture_volume(snd_mixer_elem_t *elem,
|
||||
snd_mixer_selem_channel_id_t channel);
|
||||
int set_normalized_playback_volume(snd_mixer_elem_t *elem,
|
||||
double volume,
|
||||
int dir);
|
||||
int set_normalized_capture_volume(snd_mixer_elem_t *elem,
|
||||
double volume,
|
||||
int dir);
|
||||
|
||||
#endif
|
@@ -111,8 +111,7 @@ AudioOutput::Open()
|
||||
f = source.Open(request.audio_format, *request.pipe,
|
||||
prepared_replay_gain_filter,
|
||||
prepared_other_replay_gain_filter,
|
||||
prepared_filter)
|
||||
.WithMask(config_audio_format);
|
||||
prepared_filter);
|
||||
|
||||
if (mixer != nullptr && mixer->IsPlugin(software_mixer_plugin))
|
||||
software_mixer_set_filter(*mixer, volume_filter.Get());
|
||||
@@ -121,14 +120,16 @@ AudioOutput::Open()
|
||||
name, plugin.name));
|
||||
}
|
||||
|
||||
if (open && f != filter_audio_format) {
|
||||
const auto cf = f.WithMask(config_audio_format);
|
||||
|
||||
if (open && cf != filter_audio_format) {
|
||||
/* if the filter's output format changes, the output
|
||||
must be reopened as well */
|
||||
CloseOutput(true);
|
||||
open = false;
|
||||
}
|
||||
|
||||
filter_audio_format = f;
|
||||
filter_audio_format = cf;
|
||||
|
||||
if (!open) {
|
||||
try {
|
||||
@@ -139,6 +140,27 @@ AudioOutput::Open()
|
||||
}
|
||||
|
||||
open = true;
|
||||
} else if (f != out_audio_format) {
|
||||
/* reconfigure the final ConvertFilter for its new
|
||||
input AudioFormat */
|
||||
|
||||
try {
|
||||
convert_filter_set(convert_filter.Get(),
|
||||
out_audio_format);
|
||||
} catch (const std::runtime_error &e) {
|
||||
Close(false);
|
||||
std::throw_with_nested(FormatRuntimeError("Failed to convert for \"%s\" [%s]",
|
||||
name, plugin.name));
|
||||
}
|
||||
}
|
||||
|
||||
if (f != source.GetInputAudioFormat() || f != out_audio_format) {
|
||||
struct audio_format_string afs1, afs2, afs3;
|
||||
FormatDebug(output_domain, "converting in=%s -> f=%s -> out=%s",
|
||||
audio_format_to_string(source.GetInputAudioFormat(),
|
||||
&afs1),
|
||||
audio_format_to_string(f, &afs2),
|
||||
audio_format_to_string(out_audio_format, &afs3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +176,12 @@ AudioOutput::OpenOutputAndConvert(AudioFormat desired_audio_format)
|
||||
name, plugin.name));
|
||||
}
|
||||
|
||||
struct audio_format_string af_string;
|
||||
FormatDebug(output_domain,
|
||||
"opened plugin=%s name=\"%s\" audio_format=%s",
|
||||
plugin.name, name,
|
||||
audio_format_to_string(out_audio_format, &af_string));
|
||||
|
||||
try {
|
||||
convert_filter_set(convert_filter.Get(), out_audio_format);
|
||||
} catch (const std::runtime_error &e) {
|
||||
@@ -177,17 +205,6 @@ AudioOutput::OpenOutputAndConvert(AudioFormat desired_audio_format)
|
||||
std::throw_with_nested(FormatRuntimeError("Failed to convert for \"%s\" [%s]",
|
||||
name, plugin.name));
|
||||
}
|
||||
|
||||
struct audio_format_string af_string;
|
||||
FormatDebug(output_domain,
|
||||
"opened plugin=%s name=\"%s\" audio_format=%s",
|
||||
plugin.name, name,
|
||||
audio_format_to_string(out_audio_format, &af_string));
|
||||
|
||||
if (source.GetInputAudioFormat() != out_audio_format)
|
||||
FormatDebug(output_domain, "converting from %s",
|
||||
audio_format_to_string(source.GetInputAudioFormat(),
|
||||
&af_string));
|
||||
}
|
||||
|
||||
void
|
||||
|
@@ -70,6 +70,22 @@ AudioOutputSource::Close()
|
||||
CloseFilter();
|
||||
}
|
||||
|
||||
void
|
||||
AudioOutputSource::Cancel()
|
||||
{
|
||||
current_chunk = nullptr;
|
||||
pipe.Cancel();
|
||||
|
||||
if (replay_gain_filter_instance != nullptr)
|
||||
replay_gain_filter_instance->Reset();
|
||||
|
||||
if (other_replay_gain_filter_instance != nullptr)
|
||||
other_replay_gain_filter_instance->Reset();
|
||||
|
||||
if (filter_instance != nullptr)
|
||||
filter_instance->Reset();
|
||||
}
|
||||
|
||||
void
|
||||
AudioOutputSource::OpenFilter(AudioFormat audio_format,
|
||||
PreparedFilter *prepared_replay_gain_filter,
|
||||
|
@@ -139,11 +139,7 @@ public:
|
||||
PreparedFilter *prepared_filter);
|
||||
|
||||
void Close();
|
||||
|
||||
void Cancel() {
|
||||
current_chunk = nullptr;
|
||||
pipe.Cancel();
|
||||
}
|
||||
void Cancel();
|
||||
|
||||
/**
|
||||
* Ensure that ReadTag() or PeekData() return any input.
|
||||
|
@@ -141,6 +141,7 @@ struct AlsaOutput {
|
||||
void Open(AudioFormat &audio_format);
|
||||
void Close();
|
||||
|
||||
size_t PlayRaw(ConstBuffer<void> data);
|
||||
size_t Play(const void *chunk, size_t size);
|
||||
void Drain();
|
||||
void Cancel();
|
||||
@@ -384,7 +385,7 @@ AlsaTryFormatOrByteSwap(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams,
|
||||
|
||||
/**
|
||||
* Attempts to configure the specified sample format. On DSD_U8
|
||||
* failure, attempt to switch to DSD_U32.
|
||||
* failure, attempt to switch to DSD_U32 or DSD_U16.
|
||||
*/
|
||||
static int
|
||||
AlsaTryFormatDsd(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams,
|
||||
@@ -393,8 +394,10 @@ AlsaTryFormatDsd(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams,
|
||||
int err = AlsaTryFormatOrByteSwap(pcm, hwparams, fmt, params);
|
||||
|
||||
#if defined(ENABLE_DSD) && defined(HAVE_ALSA_DSD_U32)
|
||||
if (err == 0)
|
||||
if (err == 0) {
|
||||
params.dsd_u16 = false;
|
||||
params.dsd_u32 = false;
|
||||
}
|
||||
|
||||
if (err == -EINVAL && fmt == SND_PCM_FORMAT_DSD_U8) {
|
||||
/* attempt to switch to DSD_U32 */
|
||||
@@ -404,6 +407,20 @@ AlsaTryFormatDsd(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwparams,
|
||||
err = AlsaTryFormatOrByteSwap(pcm, hwparams, fmt, params);
|
||||
if (err == 0)
|
||||
params.dsd_u32 = true;
|
||||
else
|
||||
fmt = SND_PCM_FORMAT_DSD_U8;
|
||||
}
|
||||
|
||||
if (err == -EINVAL && fmt == SND_PCM_FORMAT_DSD_U8) {
|
||||
/* attempt to switch to DSD_U16 */
|
||||
fmt = IsLittleEndian()
|
||||
? SND_PCM_FORMAT_DSD_U16_LE
|
||||
: SND_PCM_FORMAT_DSD_U16_BE;
|
||||
err = AlsaTryFormatOrByteSwap(pcm, hwparams, fmt, params);
|
||||
if (err == 0)
|
||||
params.dsd_u16 = true;
|
||||
else
|
||||
fmt = SND_PCM_FORMAT_DSD_U8;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -469,7 +486,6 @@ static void
|
||||
AlsaSetup(AlsaOutput *ad, AudioFormat &audio_format,
|
||||
PcmExport::Params ¶ms)
|
||||
{
|
||||
unsigned int sample_rate = audio_format.sample_rate;
|
||||
unsigned int channels = audio_format.channels;
|
||||
int err;
|
||||
unsigned retry = MPD_ALSA_RETRY_NR;
|
||||
@@ -513,18 +529,23 @@ configure_hw:
|
||||
|
||||
audio_format.channels = (int8_t)channels;
|
||||
|
||||
const unsigned requested_sample_rate =
|
||||
params.CalcOutputSampleRate(audio_format.sample_rate);
|
||||
unsigned output_sample_rate = requested_sample_rate;
|
||||
|
||||
err = snd_pcm_hw_params_set_rate_near(ad->pcm, hwparams,
|
||||
&sample_rate, nullptr);
|
||||
&output_sample_rate, nullptr);
|
||||
if (err < 0)
|
||||
throw FormatRuntimeError("Failed to configure sample rate %u Hz: %s",
|
||||
audio_format.sample_rate,
|
||||
requested_sample_rate,
|
||||
snd_strerror(-err));
|
||||
|
||||
if (sample_rate == 0)
|
||||
if (output_sample_rate == 0)
|
||||
throw FormatRuntimeError("Failed to configure sample rate %u Hz",
|
||||
audio_format.sample_rate);
|
||||
|
||||
audio_format.sample_rate = sample_rate;
|
||||
if (output_sample_rate != requested_sample_rate)
|
||||
audio_format.sample_rate = params.CalcInputSampleRate(output_sample_rate);
|
||||
|
||||
snd_pcm_uframes_t buffer_size_min, buffer_size_max;
|
||||
snd_pcm_hw_params_get_buffer_size_min(hwparams, &buffer_size_min);
|
||||
@@ -662,7 +683,6 @@ AlsaOutput::SetupDop(const AudioFormat audio_format,
|
||||
|
||||
AudioFormat dop_format = audio_format;
|
||||
dop_format.format = SampleFormat::S24_P32;
|
||||
dop_format.sample_rate /= 2;
|
||||
|
||||
const AudioFormat check = dop_format;
|
||||
|
||||
@@ -694,11 +714,12 @@ AlsaOutput::SetupOrDop(AudioFormat &audio_format, PcmExport::Params ¶ms)
|
||||
std::exception_ptr dop_error;
|
||||
if (dop && audio_format.format == SampleFormat::DSD) {
|
||||
try {
|
||||
SetupDop(audio_format, params);
|
||||
params.dop = true;
|
||||
SetupDop(audio_format, params);
|
||||
return;
|
||||
} catch (...) {
|
||||
dop_error = std::current_exception();
|
||||
params.dop = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,6 +763,11 @@ AlsaOutput::Open(AudioFormat &audio_format)
|
||||
GetDevice()));
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (params.dop)
|
||||
FormatDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
|
||||
#endif
|
||||
|
||||
pcm_export->Open(audio_format.format,
|
||||
audio_format.channels,
|
||||
params);
|
||||
@@ -777,6 +803,7 @@ AlsaOutput::Recover(int err)
|
||||
#if GCC_CHECK_VERSION(7,0)
|
||||
[[fallthrough]];
|
||||
#endif
|
||||
case SND_PCM_STATE_OPEN:
|
||||
case SND_PCM_STATE_SETUP:
|
||||
case SND_PCM_STATE_XRUN:
|
||||
period_position = 0;
|
||||
@@ -785,12 +812,11 @@ AlsaOutput::Recover(int err)
|
||||
case SND_PCM_STATE_DISCONNECTED:
|
||||
break;
|
||||
/* this is no error, so just keep running */
|
||||
case SND_PCM_STATE_PREPARED:
|
||||
case SND_PCM_STATE_RUNNING:
|
||||
case SND_PCM_STATE_DRAINING:
|
||||
err = 0;
|
||||
break;
|
||||
default:
|
||||
/* unknown state, do nothing */
|
||||
break;
|
||||
}
|
||||
|
||||
return err;
|
||||
@@ -822,6 +848,8 @@ AlsaOutput::Cancel()
|
||||
must_prepare = true;
|
||||
|
||||
snd_pcm_drop(pcm);
|
||||
|
||||
pcm_export->Reset();
|
||||
}
|
||||
|
||||
inline void
|
||||
@@ -831,6 +859,36 @@ AlsaOutput::Close()
|
||||
delete[] silence;
|
||||
}
|
||||
|
||||
inline size_t
|
||||
AlsaOutput::PlayRaw(ConstBuffer<void> data)
|
||||
{
|
||||
if (data.IsEmpty())
|
||||
return 0;
|
||||
|
||||
assert(data.size % out_frame_size == 0);
|
||||
|
||||
const size_t n_frames = data.size / out_frame_size;
|
||||
assert(n_frames > 0);
|
||||
|
||||
while (true) {
|
||||
const auto frames_written = snd_pcm_writei(pcm, data.data,
|
||||
n_frames);
|
||||
if (frames_written > 0) {
|
||||
period_position = (period_position + frames_written)
|
||||
% period_frames;
|
||||
|
||||
return frames_written * out_frame_size;
|
||||
}
|
||||
|
||||
if (frames_written < 0 && frames_written != -EAGAIN &&
|
||||
frames_written != -EINTR &&
|
||||
Recover(frames_written) < 0)
|
||||
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
|
||||
snd_strerror(-frames_written));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inline size_t
|
||||
AlsaOutput::Play(const void *chunk, size_t size)
|
||||
{
|
||||
@@ -856,29 +914,8 @@ AlsaOutput::Play(const void *chunk, size_t size)
|
||||
been played */
|
||||
return size;
|
||||
|
||||
chunk = e.data;
|
||||
size = e.size;
|
||||
|
||||
assert(size % out_frame_size == 0);
|
||||
|
||||
size /= out_frame_size;
|
||||
assert(size > 0);
|
||||
|
||||
while (true) {
|
||||
snd_pcm_sframes_t ret = snd_pcm_writei(pcm, chunk, size);
|
||||
if (ret > 0) {
|
||||
period_position = (period_position + ret)
|
||||
% period_frames;
|
||||
|
||||
size_t bytes_written = ret * out_frame_size;
|
||||
return pcm_export->CalcSourceSize(bytes_written);
|
||||
}
|
||||
|
||||
if (ret < 0 && ret != -EAGAIN && ret != -EINTR &&
|
||||
Recover(ret) < 0)
|
||||
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
|
||||
snd_strerror(-ret));
|
||||
}
|
||||
const size_t bytes_written = PlayRaw(e);
|
||||
return pcm_export->CalcSourceSize(bytes_written);
|
||||
}
|
||||
|
||||
typedef AudioOutputWrapper<AlsaOutput> Wrapper;
|
||||
|
@@ -659,6 +659,10 @@ OssOutput::Cancel()
|
||||
ioctl(fd, SNDCTL_DSP_RESET, 0);
|
||||
DoClose();
|
||||
}
|
||||
|
||||
#ifdef AFMT_S24_PACKED
|
||||
pcm_export->Reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
inline size_t
|
||||
|
@@ -274,7 +274,7 @@ RecorderOutput::ReopenFormat(AllocatedPath &&new_path)
|
||||
assert(path.IsNull());
|
||||
assert(file == nullptr);
|
||||
|
||||
FileOutputStream *new_file = new FileOutputStream(path);
|
||||
FileOutputStream *new_file = new FileOutputStream(new_path);
|
||||
|
||||
AudioFormat new_audio_format = effective_audio_format;
|
||||
|
||||
|
62
src/pcm/Dsd16.cxx
Normal file
62
src/pcm/Dsd16.cxx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2003-2017 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "Dsd16.hxx"
|
||||
#include "PcmBuffer.hxx"
|
||||
#include "util/ConstBuffer.hxx"
|
||||
|
||||
/**
|
||||
* Construct a 16 bit integer from two bytes.
|
||||
*/
|
||||
static constexpr inline uint16_t
|
||||
Construct16(uint8_t a, uint8_t b)
|
||||
{
|
||||
/* "a" is the oldest byte, which must be in the most
|
||||
significant byte */
|
||||
|
||||
return uint16_t(b) | (uint16_t(a) << 8);
|
||||
}
|
||||
|
||||
static constexpr inline uint16_t
|
||||
Dsd8To16Sample(const uint8_t *src, unsigned channels)
|
||||
{
|
||||
return Construct16(src[0], src[channels]);
|
||||
}
|
||||
|
||||
ConstBuffer<uint16_t>
|
||||
Dsd8To16(PcmBuffer &buffer, unsigned channels, ConstBuffer<uint8_t> _src)
|
||||
{
|
||||
const size_t in_frames = _src.size / channels;
|
||||
const size_t out_frames = in_frames / 2;
|
||||
const size_t out_samples = out_frames * channels;
|
||||
|
||||
const uint8_t *src = _src.data;
|
||||
uint16_t *const dest0 = buffer.GetT<uint16_t>(out_samples);
|
||||
uint16_t *dest = dest0;
|
||||
|
||||
for (size_t i = 0; i < out_frames; ++i) {
|
||||
for (size_t c = 0; c < channels; ++c)
|
||||
*dest++ = Dsd8To16Sample(src++, channels);
|
||||
|
||||
src += channels;
|
||||
}
|
||||
|
||||
return {dest0, out_samples};
|
||||
}
|
36
src/pcm/Dsd16.hxx
Normal file
36
src/pcm/Dsd16.hxx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2003-2017 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_PCM_DSD_16_HXX
|
||||
#define MPD_PCM_DSD_16_HXX
|
||||
|
||||
#include "check.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
template<typename T> struct ConstBuffer;
|
||||
class PcmBuffer;
|
||||
|
||||
/**
|
||||
* Convert DSD_U8 to DSD_U16 (native endian, oldest bits in MSB).
|
||||
*/
|
||||
ConstBuffer<uint16_t>
|
||||
Dsd8To16(PcmBuffer &buffer, unsigned channels, ConstBuffer<uint8_t> src);
|
||||
|
||||
#endif
|
64
src/pcm/Dsd32.cxx
Normal file
64
src/pcm/Dsd32.cxx
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2003-2017 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "Dsd32.hxx"
|
||||
#include "PcmBuffer.hxx"
|
||||
#include "util/ConstBuffer.hxx"
|
||||
|
||||
/**
|
||||
* Construct a 32 bit integer from four bytes.
|
||||
*/
|
||||
static constexpr inline uint32_t
|
||||
Construct32(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
|
||||
{
|
||||
/* "a" is the oldest byte, which must be in the most
|
||||
significant byte */
|
||||
|
||||
return uint32_t(d) | (uint32_t(c) << 8) |
|
||||
(uint32_t(b) << 16) | (uint32_t(a) << 24);
|
||||
}
|
||||
|
||||
static constexpr inline uint32_t
|
||||
Dsd8To32Sample(const uint8_t *src, unsigned channels)
|
||||
{
|
||||
return Construct32(src[0], src[channels],
|
||||
src[2 * channels], src[3 * channels]);
|
||||
}
|
||||
|
||||
ConstBuffer<uint32_t>
|
||||
Dsd8To32(PcmBuffer &buffer, unsigned channels, ConstBuffer<uint8_t> _src)
|
||||
{
|
||||
const size_t in_frames = _src.size / channels;
|
||||
const size_t out_frames = in_frames / 4;
|
||||
const size_t out_samples = out_frames * channels;
|
||||
|
||||
const uint8_t *src = _src.data;
|
||||
uint32_t *const dest0 = buffer.GetT<uint32_t>(out_samples);
|
||||
uint32_t *dest = dest0;
|
||||
|
||||
for (size_t i = 0; i < out_frames; ++i) {
|
||||
for (size_t c = 0; c < channels; ++c)
|
||||
*dest++ = Dsd8To32Sample(src++, channels);
|
||||
|
||||
src += 3 * channels;
|
||||
}
|
||||
|
||||
return {dest0, out_samples};
|
||||
}
|
36
src/pcm/Dsd32.hxx
Normal file
36
src/pcm/Dsd32.hxx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2003-2017 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_PCM_DSD_32_HXX
|
||||
#define MPD_PCM_DSD_32_HXX
|
||||
|
||||
#include "check.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
template<typename T> struct ConstBuffer;
|
||||
class PcmBuffer;
|
||||
|
||||
/**
|
||||
* Convert DSD_U8 to DSD_U32 (native endian, oldest bits in MSB).
|
||||
*/
|
||||
ConstBuffer<uint32_t>
|
||||
Dsd8To32(PcmBuffer &buffer, unsigned channels, ConstBuffer<uint8_t> src);
|
||||
|
||||
#endif
|
@@ -65,6 +65,12 @@ GluePcmResampler::Close()
|
||||
resampler->Close();
|
||||
}
|
||||
|
||||
void
|
||||
GluePcmResampler::Reset()
|
||||
{
|
||||
resampler->Reset();
|
||||
}
|
||||
|
||||
ConstBuffer<void>
|
||||
GluePcmResampler::Resample(ConstBuffer<void> src)
|
||||
{
|
||||
|
@@ -55,6 +55,11 @@ public:
|
||||
return output_sample_format;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PcmResampler::Reset()
|
||||
*/
|
||||
void Reset();
|
||||
|
||||
ConstBuffer<void> Resample(ConstBuffer<void> src);
|
||||
};
|
||||
|
||||
|
@@ -114,6 +114,12 @@ LibsampleratePcmResampler::Close()
|
||||
state = src_delete(state);
|
||||
}
|
||||
|
||||
void
|
||||
LibsampleratePcmResampler::Reset()
|
||||
{
|
||||
src_reset(state);
|
||||
}
|
||||
|
||||
inline ConstBuffer<float>
|
||||
LibsampleratePcmResampler::Resample2(ConstBuffer<float> src)
|
||||
{
|
||||
|
@@ -44,6 +44,7 @@ class LibsampleratePcmResampler final : public PcmResampler {
|
||||
public:
|
||||
AudioFormat Open(AudioFormat &af, unsigned new_sample_rate) override;
|
||||
void Close() override;
|
||||
void Reset() override;
|
||||
ConstBuffer<void> Resample(ConstBuffer<void> src) override;
|
||||
|
||||
private:
|
||||
|
@@ -40,7 +40,7 @@ public:
|
||||
|
||||
/**
|
||||
* Get the buffer, and guarantee a minimum size. This buffer becomes
|
||||
* invalid with the next pcm_buffer_get() call.
|
||||
* invalid with the next Get() call.
|
||||
*
|
||||
* This function will never return nullptr, even if size is
|
||||
* zero, because the PCM library uses the nullptr return value
|
||||
|
@@ -117,6 +117,17 @@ PcmConvert::Close()
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
PcmConvert::Reset()
|
||||
{
|
||||
if (enable_resampler)
|
||||
resampler.Reset();
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
dsd.Reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
ConstBuffer<void>
|
||||
PcmConvert::Convert(ConstBuffer<void> buffer)
|
||||
{
|
||||
|
@@ -67,6 +67,11 @@ public:
|
||||
*/
|
||||
void Close();
|
||||
|
||||
/**
|
||||
* Reset the filter's state, e.g. drop/flush buffers.
|
||||
*/
|
||||
void Reset();
|
||||
|
||||
/**
|
||||
* Converts PCM data between two audio formats.
|
||||
*
|
||||
|
@@ -44,8 +44,6 @@ pcm_dsd_to_dop(PcmBuffer &buffer, unsigned channels,
|
||||
ConstBuffer<uint8_t> _src)
|
||||
{
|
||||
assert(audio_valid_channel_count(channels));
|
||||
assert(!_src.IsNull());
|
||||
assert(_src.size > 0);
|
||||
assert(_src.size % channels == 0);
|
||||
|
||||
const unsigned num_src_samples = _src.size;
|
||||
|
@@ -71,41 +71,3 @@ PcmDsd::ToFloat(unsigned channels, ConstBuffer<uint8_t> src)
|
||||
|
||||
return { dest, num_samples };
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a 32 bit integer from four bytes.
|
||||
*/
|
||||
static constexpr inline uint32_t
|
||||
Construct32(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
|
||||
{
|
||||
return uint32_t(a) | (uint32_t(b) << 8) |
|
||||
(uint32_t(c) << 16) | (uint32_t(d) << 24);
|
||||
}
|
||||
|
||||
static constexpr inline uint32_t
|
||||
Dsd8To32Sample(const uint8_t *src, unsigned channels)
|
||||
{
|
||||
return Construct32(src[0], src[channels],
|
||||
src[2 * channels], src[3 * channels]);
|
||||
}
|
||||
|
||||
ConstBuffer<uint32_t>
|
||||
Dsd8To32(PcmBuffer &buffer, unsigned channels, ConstBuffer<uint8_t> _src)
|
||||
{
|
||||
const size_t in_frames = _src.size / channels;
|
||||
const size_t out_frames = in_frames / 4;
|
||||
const size_t out_samples = out_frames * channels;
|
||||
|
||||
const uint8_t *src = _src.data;
|
||||
uint32_t *const dest0 = buffer.GetT<uint32_t>(out_samples);
|
||||
uint32_t *dest = dest0;
|
||||
|
||||
for (size_t i = 0; i < out_frames; ++i) {
|
||||
for (size_t c = 0; c < channels; ++c)
|
||||
*dest++ = Dsd8To32Sample(src++, channels);
|
||||
|
||||
src += 3 * channels;
|
||||
}
|
||||
|
||||
return {dest0, out_samples};
|
||||
}
|
||||
|
@@ -48,10 +48,4 @@ public:
|
||||
ConstBuffer<uint8_t> src);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert DSD_U8 to DSD_U32 (native endian).
|
||||
*/
|
||||
ConstBuffer<uint32_t>
|
||||
Dsd8To32(PcmBuffer &buffer, unsigned channels, ConstBuffer<uint8_t> src);
|
||||
|
||||
#endif
|
||||
|
@@ -25,6 +25,8 @@
|
||||
#include "util/ConstBuffer.hxx"
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
#include "Dsd16.hxx"
|
||||
#include "Dsd32.hxx"
|
||||
#include "PcmDsd.hxx"
|
||||
#include "PcmDop.hxx"
|
||||
#endif
|
||||
@@ -41,9 +43,15 @@ PcmExport::Open(SampleFormat sample_format, unsigned _channels,
|
||||
: SampleFormat::UNDEFINED;
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
assert(!params.dsd_u32 || !params.dop);
|
||||
assert((params.dsd_u16 + params.dsd_u32 + params.dop) <= 1);
|
||||
assert(!params.dop || audio_valid_channel_count(_channels));
|
||||
|
||||
dsd_u16 = params.dsd_u16 && sample_format == SampleFormat::DSD;
|
||||
if (dsd_u16)
|
||||
/* after the conversion to DSD_U16, the DSD samples
|
||||
are stuffed inside fake 16 bit samples */
|
||||
sample_format = SampleFormat::S16;
|
||||
|
||||
dsd_u32 = params.dsd_u32 && sample_format == SampleFormat::DSD;
|
||||
if (dsd_u32)
|
||||
/* after the conversion to DSD_U32, the DSD samples
|
||||
@@ -55,8 +63,6 @@ PcmExport::Open(SampleFormat sample_format, unsigned _channels,
|
||||
/* after the conversion to DoP, the DSD
|
||||
samples are stuffed inside fake 24 bit samples */
|
||||
sample_format = SampleFormat::S24_P32;
|
||||
#else
|
||||
(void)_channels;
|
||||
#endif
|
||||
|
||||
shift8 = params.shift8 && sample_format == SampleFormat::S24_P32;
|
||||
@@ -84,6 +90,9 @@ PcmExport::GetFrameSize(const AudioFormat &audio_format) const
|
||||
return audio_format.channels * 3;
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (dsd_u16)
|
||||
return channels * 2;
|
||||
|
||||
if (dsd_u32)
|
||||
return channels * 4;
|
||||
|
||||
@@ -98,6 +107,46 @@ PcmExport::GetFrameSize(const AudioFormat &audio_format) const
|
||||
return audio_format.GetFrameSize();
|
||||
}
|
||||
|
||||
unsigned
|
||||
PcmExport::Params::CalcOutputSampleRate(unsigned sample_rate) const
|
||||
{
|
||||
#ifdef ENABLE_DSD
|
||||
if (dsd_u16)
|
||||
/* DSD_U16 combines two 8-bit "samples" in one 16-bit
|
||||
"sample" */
|
||||
sample_rate /= 2;
|
||||
|
||||
if (dsd_u32)
|
||||
/* DSD_U32 combines four 8-bit "samples" in one 32-bit
|
||||
"sample" */
|
||||
sample_rate /= 4;
|
||||
|
||||
if (dop)
|
||||
/* DoP packs two 8-bit "samples" in one 24-bit
|
||||
"sample" */
|
||||
sample_rate /= 2;
|
||||
#endif
|
||||
|
||||
return sample_rate;
|
||||
}
|
||||
|
||||
unsigned
|
||||
PcmExport::Params::CalcInputSampleRate(unsigned sample_rate) const
|
||||
{
|
||||
#ifdef ENABLE_DSD
|
||||
if (dsd_u16)
|
||||
sample_rate *= 2;
|
||||
|
||||
if (dsd_u32)
|
||||
sample_rate *= 4;
|
||||
|
||||
if (dop)
|
||||
sample_rate *= 2;
|
||||
#endif
|
||||
|
||||
return sample_rate;
|
||||
}
|
||||
|
||||
ConstBuffer<void>
|
||||
PcmExport::Export(ConstBuffer<void> data)
|
||||
{
|
||||
@@ -106,6 +155,11 @@ PcmExport::Export(ConstBuffer<void> data)
|
||||
alsa_channel_order, channels);
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (dsd_u16)
|
||||
data = Dsd8To16(dop_buffer, channels,
|
||||
ConstBuffer<uint8_t>::FromVoid(data))
|
||||
.ToVoid();
|
||||
|
||||
if (dsd_u32)
|
||||
data = Dsd8To32(dop_buffer, channels,
|
||||
ConstBuffer<uint8_t>::FromVoid(data))
|
||||
|
@@ -31,18 +31,7 @@ template<typename T> struct ConstBuffer;
|
||||
* outside of MPD. It has a few more options to tweak the binary
|
||||
* representation which are not supported by the pcm_convert library.
|
||||
*/
|
||||
struct PcmExport {
|
||||
struct Params {
|
||||
bool alsa_channel_order = false;
|
||||
#ifdef ENABLE_DSD
|
||||
bool dsd_u32 = false;
|
||||
bool dop = false;
|
||||
#endif
|
||||
bool shift8 = false;
|
||||
bool pack24 = false;
|
||||
bool reverse_endian = false;
|
||||
};
|
||||
|
||||
class PcmExport {
|
||||
/**
|
||||
* This buffer is used to reorder channels.
|
||||
*
|
||||
@@ -90,6 +79,11 @@ struct PcmExport {
|
||||
SampleFormat alsa_channel_order;
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
/**
|
||||
* Convert DSD (U8) to DSD_U16?
|
||||
*/
|
||||
bool dsd_u16;
|
||||
|
||||
/**
|
||||
* Convert DSD (U8) to DSD_U32?
|
||||
*/
|
||||
@@ -121,6 +115,34 @@ struct PcmExport {
|
||||
*/
|
||||
uint8_t reverse_endian;
|
||||
|
||||
public:
|
||||
struct Params {
|
||||
bool alsa_channel_order = false;
|
||||
#ifdef ENABLE_DSD
|
||||
bool dsd_u16 = false;
|
||||
bool dsd_u32 = false;
|
||||
bool dop = false;
|
||||
#endif
|
||||
bool shift8 = false;
|
||||
bool pack24 = false;
|
||||
bool reverse_endian = false;
|
||||
|
||||
/**
|
||||
* Calculate the output sample rate, given a specific input
|
||||
* sample rate. Usually, both are the same; however, with
|
||||
* DSD_U32, four input bytes (= 4 * 8 bits) are combined to
|
||||
* one output word (32 bits), dividing the sample rate by 4.
|
||||
*/
|
||||
gcc_pure
|
||||
unsigned CalcOutputSampleRate(unsigned input_sample_rate) const;
|
||||
|
||||
/**
|
||||
* The inverse of CalcOutputSampleRate().
|
||||
*/
|
||||
gcc_pure
|
||||
unsigned CalcInputSampleRate(unsigned output_sample_rate) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the object.
|
||||
*
|
||||
@@ -134,6 +156,12 @@ struct PcmExport {
|
||||
void Open(SampleFormat sample_format, unsigned channels,
|
||||
Params params);
|
||||
|
||||
/**
|
||||
* Reset the filter's state, e.g. drop/flush buffers.
|
||||
*/
|
||||
void Reset() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the size of one output frame.
|
||||
*/
|
||||
|
@@ -54,6 +54,12 @@ public:
|
||||
*/
|
||||
virtual void Close() = 0;
|
||||
|
||||
/**
|
||||
* Reset the filter's state, e.g. drop/flush buffers.
|
||||
*/
|
||||
virtual void Reset() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resamples a block of PCM data.
|
||||
*
|
||||
|
@@ -23,6 +23,7 @@
|
||||
#include "plugins/LocalStorage.hxx"
|
||||
#include "plugins/SmbclientStorage.hxx"
|
||||
#include "plugins/NfsStorage.hxx"
|
||||
#include "plugins/CurlStorage.hxx"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
@@ -34,6 +35,9 @@ const StoragePlugin *const storage_plugins[] = {
|
||||
#endif
|
||||
#ifdef ENABLE_NFS
|
||||
&nfs_storage_plugin,
|
||||
#endif
|
||||
#ifdef ENABLE_WEBDAV
|
||||
&curl_storage_plugin,
|
||||
#endif
|
||||
nullptr
|
||||
};
|
||||
|
590
src/storage/plugins/CurlStorage.cxx
Normal file
590
src/storage/plugins/CurlStorage.cxx
Normal file
@@ -0,0 +1,590 @@
|
||||
/*
|
||||
* Copyright 2003-2016 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "CurlStorage.hxx"
|
||||
#include "storage/StoragePlugin.hxx"
|
||||
#include "storage/StorageInterface.hxx"
|
||||
#include "storage/FileInfo.hxx"
|
||||
#include "storage/MemoryDirectoryReader.hxx"
|
||||
#include "lib/curl/Global.hxx"
|
||||
#include "lib/curl/Slist.hxx"
|
||||
#include "lib/curl/Request.hxx"
|
||||
#include "lib/curl/Handler.hxx"
|
||||
#include "lib/expat/ExpatParser.hxx"
|
||||
#include "fs/Traits.hxx"
|
||||
#include "event/Call.hxx"
|
||||
#include "event/DeferredMonitor.hxx"
|
||||
#include "thread/Mutex.hxx"
|
||||
#include "thread/Cond.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/StringCompare.hxx"
|
||||
#include "util/TimeParser.hxx"
|
||||
#include "util/UriUtil.hxx"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <list>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
class CurlStorage final : public Storage {
|
||||
const std::string base;
|
||||
|
||||
CurlGlobal *const curl;
|
||||
|
||||
public:
|
||||
CurlStorage(EventLoop &_loop, const char *_base)
|
||||
:base(_base),
|
||||
curl(new CurlGlobal(_loop)) {}
|
||||
|
||||
~CurlStorage() {
|
||||
BlockingCall(curl->GetEventLoop(), [this](){ delete curl; });
|
||||
}
|
||||
|
||||
/* virtual methods from class Storage */
|
||||
StorageFileInfo GetInfo(const char *uri_utf8, bool follow) override;
|
||||
|
||||
StorageDirectoryReader *OpenDirectory(const char *uri_utf8) override;
|
||||
|
||||
std::string MapUTF8(const char *uri_utf8) const override;
|
||||
|
||||
const char *MapToRelativeUTF8(const char *uri_utf8) const override;
|
||||
};
|
||||
|
||||
std::string
|
||||
CurlStorage::MapUTF8(const char *uri_utf8) const
|
||||
{
|
||||
assert(uri_utf8 != nullptr);
|
||||
|
||||
if (StringIsEmpty(uri_utf8))
|
||||
return base;
|
||||
|
||||
// TODO: escape the given URI
|
||||
|
||||
return PathTraitsUTF8::Build(base.c_str(), uri_utf8);
|
||||
}
|
||||
|
||||
const char *
|
||||
CurlStorage::MapToRelativeUTF8(const char *uri_utf8) const
|
||||
{
|
||||
// TODO: escape/unescape?
|
||||
|
||||
return PathTraitsUTF8::Relative(base.c_str(), uri_utf8);
|
||||
}
|
||||
|
||||
class BlockingHttpRequest : protected CurlResponseHandler, DeferredMonitor {
|
||||
std::exception_ptr postponed_error;
|
||||
|
||||
bool done = false;
|
||||
|
||||
protected:
|
||||
CurlRequest request;
|
||||
|
||||
Mutex mutex;
|
||||
Cond cond;
|
||||
|
||||
public:
|
||||
BlockingHttpRequest(CurlGlobal &curl, const char *uri)
|
||||
:DeferredMonitor(curl.GetEventLoop()),
|
||||
request(curl, uri, *this) {
|
||||
// TODO: use CurlInputStream's configuration
|
||||
|
||||
/* start the transfer inside the IOThread */
|
||||
DeferredMonitor::Schedule();
|
||||
}
|
||||
|
||||
void Wait() {
|
||||
const std::lock_guard<Mutex> lock(mutex);
|
||||
while (!done)
|
||||
cond.wait(mutex);
|
||||
|
||||
if (postponed_error)
|
||||
std::rethrow_exception(postponed_error);
|
||||
}
|
||||
|
||||
protected:
|
||||
void SetDone() {
|
||||
assert(!done);
|
||||
|
||||
request.Stop();
|
||||
done = true;
|
||||
cond.signal();
|
||||
}
|
||||
|
||||
void LockSetDone() {
|
||||
const std::lock_guard<Mutex> lock(mutex);
|
||||
SetDone();
|
||||
}
|
||||
|
||||
private:
|
||||
/* virtual methods from DeferredMonitor */
|
||||
void RunDeferred() final {
|
||||
assert(!done);
|
||||
|
||||
request.Start();
|
||||
}
|
||||
|
||||
/* virtual methods from CurlResponseHandler */
|
||||
void OnError(std::exception_ptr e) final {
|
||||
const std::lock_guard<Mutex> lock(mutex);
|
||||
postponed_error = std::move(e);
|
||||
SetDone();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper class which feeds a (foreign) memory buffer into the
|
||||
* CURLOPT_READFUNCTION.
|
||||
*/
|
||||
class CurlRequestBody {
|
||||
ConstBuffer<char> data;
|
||||
|
||||
public:
|
||||
explicit CurlRequestBody(ConstBuffer<void> _data)
|
||||
:data(ConstBuffer<char>::FromVoid(_data)) {}
|
||||
|
||||
explicit constexpr CurlRequestBody(StringView _data)
|
||||
:data(_data) {}
|
||||
|
||||
template<typename T>
|
||||
CurlRequestBody(CurlRequest &request, T _data)
|
||||
:CurlRequestBody(_data) {
|
||||
request.SetOption(CURLOPT_READFUNCTION, Callback);
|
||||
request.SetOption(CURLOPT_READDATA, this);
|
||||
}
|
||||
|
||||
private:
|
||||
size_t Read(char *buffer, size_t size) {
|
||||
size_t n = std::min(size, data.size);
|
||||
std::copy_n(data.begin(), n, buffer);
|
||||
return n;
|
||||
}
|
||||
|
||||
static size_t Callback(char *buffer, size_t size, size_t nitems,
|
||||
void *instream) {
|
||||
auto &rb = *(CurlRequestBody *)instream;
|
||||
return rb.Read(buffer, size * nitems);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The (relevant) contents of a "<D:response>" element.
|
||||
*/
|
||||
struct DavResponse {
|
||||
std::string href;
|
||||
unsigned status = 0;
|
||||
bool collection = false;
|
||||
std::chrono::system_clock::time_point mtime =
|
||||
std::chrono::system_clock::time_point::min();
|
||||
uint64_t length = 0;
|
||||
|
||||
bool Check() const {
|
||||
return !href.empty();
|
||||
}
|
||||
};
|
||||
|
||||
static unsigned
|
||||
ParseStatus(const char *s)
|
||||
{
|
||||
/* skip the "HTTP/1.1" prefix */
|
||||
const char *space = strchr(s, ' ');
|
||||
if (space == nullptr)
|
||||
return 0;
|
||||
|
||||
return strtoul(space + 1, nullptr, 10);
|
||||
}
|
||||
|
||||
static unsigned
|
||||
ParseStatus(const char *s, size_t length)
|
||||
{
|
||||
return ParseStatus(std::string(s, length).c_str());
|
||||
}
|
||||
|
||||
static std::chrono::system_clock::time_point
|
||||
ParseTimeStamp(const char *s)
|
||||
{
|
||||
try {
|
||||
// TODO: make this more robust
|
||||
return ParseTimePoint(s, "%a, %d %b %Y %T %Z");
|
||||
} catch (const std::runtime_error &) {
|
||||
return std::chrono::system_clock::time_point::min();
|
||||
}
|
||||
}
|
||||
|
||||
static std::chrono::system_clock::time_point
|
||||
ParseTimeStamp(const char *s, size_t length)
|
||||
{
|
||||
return ParseTimeStamp(std::string(s, length).c_str());
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
ParseU64(const char *s)
|
||||
{
|
||||
return strtoull(s, nullptr, 10);
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
ParseU64(const char *s, size_t length)
|
||||
{
|
||||
return ParseU64(std::string(s, length).c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* A WebDAV PROPFIND request. Each "response" element will be passed
|
||||
* to OnDavResponse() (to be implemented by a derived class).
|
||||
*/
|
||||
class PropfindOperation : BlockingHttpRequest, CommonExpatParser {
|
||||
CurlSlist request_headers;
|
||||
CurlRequestBody request_body;
|
||||
|
||||
enum class State {
|
||||
ROOT,
|
||||
RESPONSE,
|
||||
HREF,
|
||||
STATUS,
|
||||
TYPE,
|
||||
MTIME,
|
||||
LENGTH,
|
||||
} state = State::ROOT;
|
||||
|
||||
DavResponse response;
|
||||
|
||||
public:
|
||||
PropfindOperation(CurlGlobal &_curl, const char *_uri, unsigned depth)
|
||||
:BlockingHttpRequest(_curl, _uri),
|
||||
CommonExpatParser(ExpatNamespaceSeparator{'|'}),
|
||||
request_body(request,
|
||||
"<?xml version=\"1.0\"?>\n"
|
||||
"<a:propfind xmlns:a=\"DAV:\">"
|
||||
"<a:prop><a:getcontenttype/></a:prop>"
|
||||
"<a:prop><a:getcontentlength/></a:prop>"
|
||||
"</a:propfind>")
|
||||
{
|
||||
request.SetOption(CURLOPT_CUSTOMREQUEST, "PROPFIND");
|
||||
|
||||
char buffer[40];
|
||||
sprintf(buffer, "depth: %u", depth);
|
||||
request_headers.Append(buffer);
|
||||
|
||||
request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
|
||||
|
||||
// TODO: send request body
|
||||
}
|
||||
|
||||
using BlockingHttpRequest::Wait;
|
||||
|
||||
protected:
|
||||
virtual void OnDavResponse(DavResponse &&r) = 0;
|
||||
|
||||
private:
|
||||
void FinishResponse() {
|
||||
if (response.Check())
|
||||
OnDavResponse(std::move(response));
|
||||
response = DavResponse();
|
||||
}
|
||||
|
||||
/* virtual methods from CurlResponseHandler */
|
||||
void OnHeaders(unsigned status,
|
||||
std::multimap<std::string, std::string> &&headers) final {
|
||||
if (status != 207)
|
||||
throw FormatRuntimeError("Status %d from WebDAV server; expected \"207 Multi-Status\"",
|
||||
status);
|
||||
|
||||
auto i = headers.find("content-type");
|
||||
if (i == headers.end() ||
|
||||
strncmp(i->second.c_str(), "text/xml", 8) != 0)
|
||||
throw std::runtime_error("Unexpected Content-Type from WebDAV server");
|
||||
}
|
||||
|
||||
void OnData(ConstBuffer<void> _data) final {
|
||||
const auto data = ConstBuffer<char>::FromVoid(_data);
|
||||
Parse(data.data, data.size, false);
|
||||
}
|
||||
|
||||
void OnEnd() final {
|
||||
Parse("", 0, true);
|
||||
LockSetDone();
|
||||
}
|
||||
|
||||
/* virtual methods from CommonExpatParser */
|
||||
void StartElement(const XML_Char *name,
|
||||
gcc_unused const XML_Char **attrs) final {
|
||||
switch (state) {
|
||||
case State::ROOT:
|
||||
if (strcmp(name, "DAV:|response") == 0)
|
||||
state = State::RESPONSE;
|
||||
break;
|
||||
|
||||
case State::RESPONSE:
|
||||
if (strcmp(name, "DAV:|href") == 0)
|
||||
state = State::HREF;
|
||||
else if (strcmp(name, "DAV:|status") == 0)
|
||||
state = State::STATUS;
|
||||
else if (strcmp(name, "DAV:|resourcetype") == 0)
|
||||
state = State::TYPE;
|
||||
else if (strcmp(name, "DAV:|getlastmodified") == 0)
|
||||
state = State::MTIME;
|
||||
else if (strcmp(name, "DAV:|getcontentlength") == 0)
|
||||
state = State::LENGTH;
|
||||
break;
|
||||
|
||||
case State::TYPE:
|
||||
if (strcmp(name, "DAV:|collection") == 0)
|
||||
response.collection = true;
|
||||
break;
|
||||
|
||||
case State::HREF:
|
||||
case State::STATUS:
|
||||
case State::LENGTH:
|
||||
case State::MTIME:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EndElement(const XML_Char *name) final {
|
||||
switch (state) {
|
||||
case State::ROOT:
|
||||
break;
|
||||
|
||||
case State::RESPONSE:
|
||||
if (strcmp(name, "DAV:|response") == 0) {
|
||||
FinishResponse();
|
||||
state = State::ROOT;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case State::HREF:
|
||||
if (strcmp(name, "DAV:|href") == 0)
|
||||
state = State::RESPONSE;
|
||||
break;
|
||||
|
||||
case State::STATUS:
|
||||
if (strcmp(name, "DAV:|status") == 0)
|
||||
state = State::RESPONSE;
|
||||
break;
|
||||
|
||||
case State::TYPE:
|
||||
if (strcmp(name, "DAV:|resourcetype") == 0)
|
||||
state = State::RESPONSE;
|
||||
break;
|
||||
|
||||
case State::MTIME:
|
||||
if (strcmp(name, "DAV:|getlastmodified") == 0)
|
||||
state = State::RESPONSE;
|
||||
break;
|
||||
|
||||
case State::LENGTH:
|
||||
if (strcmp(name, "DAV:|getcontentlength") == 0)
|
||||
state = State::RESPONSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterData(const XML_Char *s, int len) final {
|
||||
switch (state) {
|
||||
case State::ROOT:
|
||||
case State::RESPONSE:
|
||||
case State::TYPE:
|
||||
break;
|
||||
|
||||
case State::HREF:
|
||||
response.href.assign(s, len);
|
||||
break;
|
||||
|
||||
case State::STATUS:
|
||||
response.status = ParseStatus(s, len);
|
||||
break;
|
||||
|
||||
case State::MTIME:
|
||||
response.mtime = ParseTimeStamp(s, len);
|
||||
break;
|
||||
|
||||
case State::LENGTH:
|
||||
response.length = ParseU64(s, len);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtain information about a single file using WebDAV PROPFIND.
|
||||
*/
|
||||
class HttpGetInfoOperation final : public PropfindOperation {
|
||||
StorageFileInfo info;
|
||||
|
||||
public:
|
||||
HttpGetInfoOperation(CurlGlobal &curl, const char *uri)
|
||||
:PropfindOperation(curl, uri, 0) {
|
||||
info.type = StorageFileInfo::Type::OTHER;
|
||||
info.size = 0;
|
||||
info.mtime = 0;
|
||||
info.device = info.inode = 0;
|
||||
}
|
||||
|
||||
const StorageFileInfo &Perform() {
|
||||
Wait();
|
||||
return info;
|
||||
}
|
||||
|
||||
protected:
|
||||
/* virtual methods from PropfindOperation */
|
||||
void OnDavResponse(DavResponse &&r) override {
|
||||
if (r.status != 200)
|
||||
return;
|
||||
|
||||
info.type = r.collection
|
||||
? StorageFileInfo::Type::DIRECTORY
|
||||
: StorageFileInfo::Type::REGULAR;
|
||||
info.size = r.length;
|
||||
info.mtime = r.mtime > std::chrono::system_clock::time_point()
|
||||
? std::chrono::system_clock::to_time_t(r.mtime)
|
||||
: 0;
|
||||
info.device = info.inode = 0;
|
||||
}
|
||||
};
|
||||
|
||||
StorageFileInfo
|
||||
CurlStorage::GetInfo(const char *uri_utf8, gcc_unused bool follow)
|
||||
{
|
||||
// TODO: escape the given URI
|
||||
|
||||
std::string uri = base;
|
||||
uri += uri_utf8;
|
||||
|
||||
return HttpGetInfoOperation(*curl, uri.c_str()).Perform();
|
||||
}
|
||||
|
||||
gcc_pure
|
||||
static const char *
|
||||
UriPathOrSlash(const char *uri)
|
||||
{
|
||||
const char *path = uri_get_path(uri);
|
||||
if (path == nullptr)
|
||||
path = "/";
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a directory listing using WebDAV PROPFIND.
|
||||
*/
|
||||
class HttpListDirectoryOperation final : public PropfindOperation {
|
||||
const std::string base_path;
|
||||
|
||||
MemoryStorageDirectoryReader::List entries;
|
||||
|
||||
public:
|
||||
HttpListDirectoryOperation(CurlGlobal &curl, const char *uri)
|
||||
:PropfindOperation(curl, uri, 1),
|
||||
base_path(UriPathOrSlash(uri)) {}
|
||||
|
||||
StorageDirectoryReader *Perform() {
|
||||
Wait();
|
||||
return ToReader();
|
||||
}
|
||||
|
||||
private:
|
||||
StorageDirectoryReader *ToReader() {
|
||||
return new MemoryStorageDirectoryReader(std::move(entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a "href" attribute (which may be an absolute URI)
|
||||
* to the base file name.
|
||||
*/
|
||||
gcc_pure
|
||||
StringView HrefToEscapedName(const char *href) const {
|
||||
const char *path = uri_get_path(href);
|
||||
if (path == nullptr)
|
||||
return nullptr;
|
||||
|
||||
path = StringAfterPrefix(path, base_path.c_str());
|
||||
if (path == nullptr || *path == 0)
|
||||
return nullptr;
|
||||
|
||||
const char *slash = strchr(path, '/');
|
||||
if (slash == nullptr)
|
||||
/* regular file */
|
||||
return path;
|
||||
else if (slash[1] == 0)
|
||||
/* trailing slash: collection; strip the slash */
|
||||
return {path, slash};
|
||||
else
|
||||
/* strange, better ignore it */
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
/* virtual methods from PropfindOperation */
|
||||
void OnDavResponse(DavResponse &&r) override {
|
||||
if (r.status != 200)
|
||||
return;
|
||||
|
||||
const auto escaped_name = HrefToEscapedName(r.href.c_str());
|
||||
if (escaped_name.IsNull())
|
||||
return;
|
||||
|
||||
// TODO: unescape
|
||||
const auto name = escaped_name;
|
||||
|
||||
entries.emplace_front(std::string(name.data, name.size));
|
||||
|
||||
auto &info = entries.front().info;
|
||||
info.type = r.collection
|
||||
? StorageFileInfo::Type::DIRECTORY
|
||||
: StorageFileInfo::Type::REGULAR;
|
||||
info.size = r.length;
|
||||
info.mtime = r.mtime > std::chrono::system_clock::time_point()
|
||||
? std::chrono::system_clock::to_time_t(r.mtime)
|
||||
: 0;
|
||||
info.device = info.inode = 0;
|
||||
}
|
||||
};
|
||||
|
||||
StorageDirectoryReader *
|
||||
CurlStorage::OpenDirectory(const char *uri_utf8)
|
||||
{
|
||||
// TODO: escape the given URI
|
||||
|
||||
std::string uri = base;
|
||||
uri += uri_utf8;
|
||||
|
||||
/* collection URIs must end with a slash */
|
||||
if (uri.back() != '/')
|
||||
uri.push_back('/');
|
||||
|
||||
return HttpListDirectoryOperation(*curl, uri.c_str()).Perform();
|
||||
}
|
||||
|
||||
static Storage *
|
||||
CreateCurlStorageURI(EventLoop &event_loop, const char *uri)
|
||||
{
|
||||
if (strncmp(uri, "http://", 7) != 0 &&
|
||||
strncmp(uri, "https://", 8) != 0)
|
||||
return nullptr;
|
||||
|
||||
return new CurlStorage(event_loop, uri);
|
||||
}
|
||||
|
||||
const StoragePlugin curl_storage_plugin = {
|
||||
"curl",
|
||||
CreateCurlStorageURI,
|
||||
};
|
29
src/storage/plugins/CurlStorage.hxx
Normal file
29
src/storage/plugins/CurlStorage.hxx
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2003-2016 The Music Player Daemon Project
|
||||
* http://www.musicpd.org
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#ifndef MPD_STORAGE_CURL_HXX
|
||||
#define MPD_STORAGE_CURL_HXX
|
||||
|
||||
#include "check.h"
|
||||
|
||||
struct StoragePlugin;
|
||||
|
||||
extern const StoragePlugin curl_storage_plugin;
|
||||
|
||||
#endif
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Max Kellermann <max@duempel.org>
|
||||
* Copyright (C) 2013-2017 Max Kellermann <max@duempel.org>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
@@ -30,10 +30,12 @@
|
||||
#ifndef REUSABLE_ARRAY_HXX
|
||||
#define REUSABLE_ARRAY_HXX
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "Compiler.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Manager for a temporary array which grows as needed. This attempts
|
||||
* to reduce the number of consecutive heap allocations and
|
||||
@@ -44,19 +46,30 @@
|
||||
*/
|
||||
template<typename T, size_t M=1>
|
||||
class ReusableArray {
|
||||
T *buffer;
|
||||
size_t capacity;
|
||||
T *buffer = nullptr;
|
||||
size_t capacity = 0;
|
||||
|
||||
public:
|
||||
ReusableArray():buffer(nullptr), capacity(0) {}
|
||||
ReusableArray() = default;
|
||||
|
||||
ReusableArray(const ReusableArray &other) = delete;
|
||||
ReusableArray &operator=(const ReusableArray &other) = delete;
|
||||
ReusableArray(ReusableArray &&src)
|
||||
:buffer(std::exchange(src.buffer, nullptr)),
|
||||
capacity(std::exchange(src.capacity, 0)) {}
|
||||
|
||||
ReusableArray &operator=(const ReusableArray &&src) {
|
||||
std::swap(buffer, src.buffer);
|
||||
std::swap(capacity, src.capacity);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~ReusableArray() {
|
||||
delete[] buffer;
|
||||
}
|
||||
|
||||
size_t GetCapacity() const {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free resources allocated by this object. This invalidates
|
||||
* the buffer returned by Get().
|
||||
|
81
src/util/TimeParser.cxx
Normal file
81
src/util/TimeParser.cxx
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2017 Max Kellermann <max@duempel.org>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* - Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the
|
||||
* distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||
* OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "TimeParser.hxx"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <assert.h>
|
||||
#include <time.h>
|
||||
|
||||
#if !defined(__GLIBC__) && !defined(WIN32)
|
||||
|
||||
/**
|
||||
* Determine the time zone offset in a portable way.
|
||||
*/
|
||||
gcc_const
|
||||
static time_t
|
||||
GetTimeZoneOffset()
|
||||
{
|
||||
time_t t = 1234567890;
|
||||
struct tm tm;
|
||||
tm.tm_isdst = 0;
|
||||
gmtime_r(&t, &tm);
|
||||
return t - mktime(&tm);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
std::chrono::system_clock::time_point
|
||||
ParseTimePoint(const char *s, const char *format)
|
||||
{
|
||||
assert(s != nullptr);
|
||||
assert(format != nullptr);
|
||||
|
||||
#ifdef WIN32
|
||||
/* TODO: emulate strptime()? */
|
||||
throw std::runtime_error("Time parsing not implemented on Windows");
|
||||
#else
|
||||
struct tm tm;
|
||||
const char *end = strptime(s, format, &tm);
|
||||
if (end == nullptr || *end != 0)
|
||||
throw std::runtime_error("Failed to parse time stamp");
|
||||
|
||||
#ifdef __GLIBC__
|
||||
/* timegm() is a GNU extension */
|
||||
const auto t = timegm(&tm);
|
||||
#else
|
||||
tm.tm_isdst = 0;
|
||||
const auto t = mktime(&tm) + GetTimeZoneOffset();
|
||||
#endif /* !__GLIBC__ */
|
||||
|
||||
return std::chrono::system_clock::from_time_t(t);
|
||||
|
||||
#endif /* !WIN32 */
|
||||
}
|
46
src/util/TimeParser.hxx
Normal file
46
src/util/TimeParser.hxx
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2017 Max Kellermann <max@duempel.org>
|
||||
*
|
||||
* 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 TIME_PARSER_HXX
|
||||
#define TIME_PARSER_HXX
|
||||
|
||||
#include "Compiler.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
/**
|
||||
* Parse a time stamp.
|
||||
*
|
||||
* Throws std::runtime_error on error.
|
||||
*/
|
||||
gcc_pure
|
||||
std::chrono::system_clock::time_point
|
||||
ParseTimePoint(const char *s, const char *format);
|
||||
|
||||
#endif
|
@@ -19,10 +19,57 @@
|
||||
|
||||
#include "UriUtil.hxx"
|
||||
#include "StringCompare.hxx"
|
||||
#include "CharUtil.hxx"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
static constexpr bool
|
||||
IsValidSchemeStart(char ch)
|
||||
{
|
||||
return IsLowerAlphaASCII(ch);
|
||||
}
|
||||
|
||||
static constexpr bool
|
||||
IsValidSchemeChar(char ch)
|
||||
{
|
||||
return IsLowerAlphaASCII(ch) || IsDigitASCII(ch) ||
|
||||
ch == '+' || ch == '.' || ch == '-';
|
||||
}
|
||||
|
||||
gcc_pure
|
||||
static bool
|
||||
IsValidScheme(StringView p)
|
||||
{
|
||||
if (p.IsEmpty() || !IsValidSchemeStart(p.front()))
|
||||
return false;
|
||||
|
||||
for (size_t i = 1; i < p.size; ++i)
|
||||
if (!IsValidSchemeChar(p[i]))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URI part after the scheme specification (and after the
|
||||
* double slash).
|
||||
*/
|
||||
gcc_pure
|
||||
static const char *
|
||||
uri_after_scheme(const char *uri)
|
||||
{
|
||||
if (uri[0] == '/' && uri[1] == '/' && uri[2] != '/')
|
||||
return uri + 2;
|
||||
|
||||
const char *colon = strchr(uri, ':');
|
||||
return colon != nullptr &&
|
||||
IsValidScheme({uri, colon}) &&
|
||||
colon[1] == '/' && colon[2] == '/'
|
||||
? colon + 3
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
bool uri_has_scheme(const char *uri)
|
||||
{
|
||||
return strstr(uri, "://") != nullptr;
|
||||
@@ -38,6 +85,16 @@ uri_get_scheme(const char *uri)
|
||||
return std::string(uri, end);
|
||||
}
|
||||
|
||||
const char *
|
||||
uri_get_path(const char *uri)
|
||||
{
|
||||
const char *ap = uri_after_scheme(uri);
|
||||
if (ap != nullptr)
|
||||
return strchr(ap, '/');
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/* suffixes should be ascii only characters */
|
||||
const char *
|
||||
uri_get_suffix(const char *uri)
|
||||
|
@@ -38,6 +38,14 @@ gcc_pure
|
||||
std::string
|
||||
uri_get_scheme(const char *uri);
|
||||
|
||||
/**
|
||||
* Returns the URI path (including the query string) or nullptr if the
|
||||
* given URI has no path.
|
||||
*/
|
||||
gcc_pure gcc_nonnull_all
|
||||
const char *
|
||||
uri_get_path(const char *uri);
|
||||
|
||||
gcc_pure
|
||||
const char *
|
||||
uri_get_suffix(const char *uri);
|
||||
|
@@ -67,8 +67,13 @@ Ls(Storage &storage, const char *path)
|
||||
break;
|
||||
}
|
||||
|
||||
char mtime[32];
|
||||
strftime(mtime, sizeof(mtime), "%F", gmtime(&info.mtime));
|
||||
char mtime_buffer[32];
|
||||
const char *mtime = " ";
|
||||
if (info.mtime > 0) {
|
||||
strftime(mtime_buffer, sizeof(mtime_buffer), "%F",
|
||||
gmtime(&info.mtime));
|
||||
mtime = mtime_buffer;
|
||||
}
|
||||
|
||||
printf("%s %10llu %s %s\n",
|
||||
type, (unsigned long long)info.size,
|
||||
|
@@ -20,6 +20,8 @@
|
||||
#ifndef MPD_TEST_PCM_ALL_HXX
|
||||
#define MPD_TEST_PCM_ALL_HXX
|
||||
|
||||
#include "check.h"
|
||||
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
@@ -126,6 +128,7 @@ class PcmExportTest : public CppUnit::TestFixture {
|
||||
CPPUNIT_TEST(TestPack24);
|
||||
CPPUNIT_TEST(TestReverseEndian);
|
||||
#ifdef ENABLE_DSD
|
||||
CPPUNIT_TEST(TestDsdU16);
|
||||
CPPUNIT_TEST(TestDsdU32);
|
||||
CPPUNIT_TEST(TestDop);
|
||||
#endif
|
||||
@@ -137,6 +140,7 @@ public:
|
||||
void TestPack24();
|
||||
void TestReverseEndian();
|
||||
#ifdef ENABLE_DSD
|
||||
void TestDsdU16();
|
||||
void TestDsdU32();
|
||||
void TestDop();
|
||||
#endif
|
||||
|
@@ -17,6 +17,7 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "test_pcm_all.hxx"
|
||||
#include "test_pcm_util.hxx"
|
||||
#include "pcm/PcmDither.cxx"
|
||||
|
@@ -35,6 +35,9 @@ PcmExportTest::TestShift8()
|
||||
PcmExport::Params params;
|
||||
params.shift8 = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(42u), 42u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(42u), 42u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(SampleFormat::S24_P32, 2, params);
|
||||
|
||||
@@ -71,6 +74,9 @@ PcmExportTest::TestPack24()
|
||||
PcmExport::Params params;
|
||||
params.pack24 = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(42u), 42u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(42u), 42u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(SampleFormat::S24_P32, 2, params);
|
||||
|
||||
@@ -97,6 +103,9 @@ PcmExportTest::TestReverseEndian()
|
||||
PcmExport::Params params;
|
||||
params.reverse_endian = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(42u), 42u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(42u), 42u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(SampleFormat::S8, 2, params);
|
||||
|
||||
@@ -117,6 +126,37 @@ PcmExportTest::TestReverseEndian()
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
|
||||
void
|
||||
PcmExportTest::TestDsdU16()
|
||||
{
|
||||
static constexpr uint8_t src[] = {
|
||||
0x01, 0x23, 0x45, 0x67,
|
||||
0x89, 0xab, 0xcd, 0xef,
|
||||
0x11, 0x22, 0x33, 0x44,
|
||||
0x55, 0x66, 0x77, 0x88,
|
||||
};
|
||||
|
||||
static constexpr uint16_t expected[] = {
|
||||
0x0145, 0x2367,
|
||||
0x89cd, 0xabef,
|
||||
0x1133, 0x2244,
|
||||
0x5577, 0x6688,
|
||||
};
|
||||
|
||||
PcmExport::Params params;
|
||||
params.dsd_u16 = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(705600u), 352800u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(352800u), 705600u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(SampleFormat::DSD, 2, params);
|
||||
|
||||
auto dest = e.Export({src, sizeof(src)});
|
||||
CPPUNIT_ASSERT_EQUAL(sizeof(expected), dest.size);
|
||||
CPPUNIT_ASSERT(memcmp(dest.data, expected, dest.size) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
PcmExportTest::TestDsdU32()
|
||||
{
|
||||
@@ -128,15 +168,18 @@ PcmExportTest::TestDsdU32()
|
||||
};
|
||||
|
||||
static constexpr uint32_t expected[] = {
|
||||
0xcd894501,
|
||||
0xefab6723,
|
||||
0x77553311,
|
||||
0x88664422,
|
||||
0x014589cd,
|
||||
0x2367abef,
|
||||
0x11335577,
|
||||
0x22446688,
|
||||
};
|
||||
|
||||
PcmExport::Params params;
|
||||
params.dsd_u32 = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(705600u), 176400u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(176400u), 705600u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(SampleFormat::DSD, 2, params);
|
||||
|
||||
@@ -163,6 +206,9 @@ PcmExportTest::TestDop()
|
||||
PcmExport::Params params;
|
||||
params.dop = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(705600u), 352800u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(352800u), 705600u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(SampleFormat::DSD, 2, params);
|
||||
|
||||
@@ -192,6 +238,9 @@ TestAlsaChannelOrder51()
|
||||
PcmExport::Params params;
|
||||
params.alsa_channel_order = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(42u), 42u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(42u), 42u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(F, 6, params);
|
||||
|
||||
@@ -219,6 +268,9 @@ TestAlsaChannelOrder71()
|
||||
PcmExport::Params params;
|
||||
params.alsa_channel_order = true;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcOutputSampleRate(42u), 42u);
|
||||
CPPUNIT_ASSERT_EQUAL(params.CalcInputSampleRate(42u), 42u);
|
||||
|
||||
PcmExport e;
|
||||
e.Open(F, 8, params);
|
||||
|
||||
|
@@ -17,6 +17,7 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "test_pcm_all.hxx"
|
||||
#include "Compiler.h"
|
||||
|
||||
|
@@ -17,6 +17,7 @@
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "test_pcm_all.hxx"
|
||||
#include "test_pcm_util.hxx"
|
||||
#include "pcm/PcmPack.hxx"
|
||||
|
Reference in New Issue
Block a user