Compare commits

...

85 Commits

Author SHA1 Message Date
Max Kellermann
96a31f554a release v0.20.2 2017-01-15 01:28:00 +01:00
Max Kellermann
d14ec6aea5 output/Thread: reconfigure ConvertFilter for its new input AudioFormat
If the input AudioFormat changes but the out_audio_format doesn't
change (e.g. because there is a fixed "format" setting in this
"audio_output" section), the ConvertFilter needs to be reconfigured.
This didn't happen, resulting in awful static noise after changing
songs.
2017-01-15 01:24:17 +01:00
Max Kellermann
917cedf893 output/Thread: move AudioFormat logging code around 2017-01-15 01:23:49 +01:00
Max Kellermann
193dd71600 output/Thread: remember the original filter audio format in local variable 2017-01-15 01:21:14 +01:00
Max Kellermann
6c293a3d7f lib/nfs: add more API documentation 2017-01-15 00:58:49 +01:00
Max Kellermann
e847ddf011 DetachedSong: compare start_time and end_time in IsSame()
This method is used by DecoderControl::IsCurrentSong(), which is used
by the player thread to check whether the current decoder instance can
be reused to seek.  When switching to another song in the same CUE
sheet, previously DetachedSong::IsSame() returned true, and thus the
old decoder instance was used for the new song, not considering the
new end_time.  This led to the old decoder quickly quitting.
2017-01-15 00:54:25 +01:00
Max Kellermann
7e8b448985 input/alsa: set period_size=buffer_size/4
This way, we have four periods instead of the default of two.  With
only two periods, we don't get woken up often enough, and we
frequently encounter buffer overruns.  With four periods, we have more
time to breathe, and the buffer overruns magically disappear.
2017-01-14 21:50:28 +01:00
Max Kellermann
d1f3a87c08 input/alsa: remove the start_threshold setting
This setting is mostly useless for capture devices.  There's no point
in configuring it.
2017-01-14 21:47:37 +01:00
Max Kellermann
9f8145e590 input/alsa: dump buffer/period sizes 2017-01-14 21:09:57 +01:00
Steven O'Brien
791efc171a input/alsa: enable non-blocking mode 2017-01-14 20:59:57 +01:00
Steven O'Brien
144312a525 input/alsa: handle EAGAIN 2017-01-14 20:59:23 +01:00
Max Kellermann
92684112ed input/alsa: call snd_pcm_start() after snd_pcm_prepare()
This is necessary because we'll never get woken up again by
epoll_wait() after a buffer overrun recovery, unless we start the PCM
explicitly before returning to the I/O loop.
2017-01-14 20:58:30 +01:00
Max Kellermann
ef114ee6cb input/alsa: improve logging in Recover()
Copy yet more code from the ALSA output plugin.
2017-01-14 20:52:41 +01:00
Max Kellermann
667f209742 input/alsa: check snd_pcm_state() in Recover()
Copy some good code from the ALSA output plugin.
2017-01-14 20:51:51 +01:00
Max Kellermann
4ad0747c78 output/alsa: explicitly mention all snd_pcm_state() enums
I want a compiler warning when a new state needs to be considered
here.
2017-01-14 20:49:15 +01:00
Max Kellermann
c5cf66402c input/alsa: make two attributes "const" 2017-01-13 20:26:36 +01:00
Max Kellermann
05417049eb input/alsa: clear sockets from within IOThread
Fixes assertion failure in implicit destructor.
2017-01-13 20:17:16 +01:00
Max Kellermann
c7b0c46d9f output/recorder: fix typo in variable name
Fixes the dreaded error "Failed to create : No such file or
directory".

 https://bugs.musicpd.org/view.php?id=4625
2017-01-12 21:36:32 +01:00
Max Kellermann
df578c91ad output/alsa: log DoP mode 2017-01-11 22:50:40 +01:00
Max Kellermann
70008c47c9 output/alsa: support DSD_U16 2017-01-11 22:47:21 +01:00
Max Kellermann
938affef32 pcm/export: support DSD_U16 2017-01-11 22:47:12 +01:00
Max Kellermann
a3c33000ee pcm/Dsd32: include cleanup 2017-01-11 22:47:12 +01:00
Max Kellermann
1e54b7b294 test/test_pcm: fix the DSD_U32 byte order
The unit test was wrong as well.  D'oh!
2017-01-11 22:39:23 +01:00
Max Kellermann
cc0dbcf3f4 pcm/Dsd32: fix the byte order
The byte order of DSD_U32 was wrong from the start.  The oldest bits
must be in the MSB, not in the LSB, according to
snd_pcm_format_descriptions in alsa-lib.
2017-01-11 22:25:54 +01:00
Max Kellermann
c5a2cadccc pcm/Export: convert to class, make members private 2017-01-11 21:48:43 +01:00
Max Kellermann
9aa43416b6 pcm/dop: remove unnecessary assertions 2017-01-11 21:48:43 +01:00
Max Kellermann
8364029db8 output/alsa: move code to PlayRaw() 2017-01-11 21:38:05 +01:00
Max Kellermann
d842d21be0 util/ReusableArray: add method GetCapacity() 2017-01-11 20:37:12 +01:00
Max Kellermann
3514fd2433 util/ReusableArray: add move constructor/operator 2017-01-11 20:37:12 +01:00
Max Kellermann
6778ff27ea util/ReusableArray: use C++11 initializers 2017-01-11 20:33:01 +01:00
Max Kellermann
f32315d699 pcm/Export: remove obsolete gcc warning suppression 2017-01-11 20:31:48 +01:00
Max Kellermann
8b754b24b6 pcm/Buffer: update API documentation 2017-01-11 20:24:32 +01:00
Max Kellermann
b1bee9ff38 test/test_pcm: enable the DSD unit tests
These were disabled by accident.
2017-01-11 20:06:10 +01:00
Max Kellermann
569be2d402 test/test_pcm_export: fix TestDop() sample rate results 2017-01-11 20:06:10 +01:00
Max Kellermann
78a73eac53 pcm/Export: add (dummy) method Cancel()
We'll have some code for it soon.
2017-01-11 15:41:28 +01:00
Max Kellermann
533cb99c33 output/Source: reset all filters in Cancel() 2017-01-11 15:39:18 +01:00
Max Kellermann
79726940dc output/Source: un-inline Cancel() 2017-01-11 15:39:00 +01:00
Max Kellermann
27c7891169 filter/Internal: add method Reset() 2017-01-11 15:34:25 +01:00
Max Kellermann
7a3a793a12 decoder/Bridge: call PcmConvert::Reset() after seeking 2017-01-11 15:32:57 +01:00
Max Kellermann
8088469eca pcm/Convert: add method Reset() 2017-01-11 15:30:30 +01:00
Max Kellermann
3dcb082015 pcm/Resampler: add method Reset()
Hook for src_reset(), not yet used.
2017-01-11 15:26:48 +01:00
Max Kellermann
bece023028 pcm/PcmDsd: move Dsd8To32() to Dsd32.cxx 2017-01-11 15:22:43 +01:00
Max Kellermann
9c4df66925 pcm/Export: halve the sample rate for DoP
Move this sample rate fixup from the ALSA output plugin to PcmExport,
where it belongs.
2017-01-11 10:33:23 +01:00
Max Kellermann
2b43ceb6c6 pcm/Export: DSD_U32 quarters the sample rate
DSD_U32 packs four bytes instead of one large "sample", thus the
sample rate is one quarter of the input sample rate.  This fixes a
rather critical DSD_U32 playback problem.
2017-01-11 10:14:41 +01:00
Max Kellermann
c143adba91 pcm/Export: add CalcOutputSampleRate(), CalcInputSampleRate()
Prepare for DSD sample rate fixups.
2017-01-10 23:48:26 +01:00
Max Kellermann
142fdc8d86 decoder/flac: add options "probesize" and "analyzeduration"
https://bugs.musicpd.org/view.php?id=3876
2017-01-10 23:05:04 +01:00
Max Kellermann
67778dcd3d configure.ac: prepare for 0.20.2 2017-01-10 23:01:42 +01:00
Max Kellermann
ed80863eac release v0.20.1 2017-01-09 18:10:18 +01:00
Max Kellermann
c3fc84de12 input/curl: wake up client thread after seek to end of file
Call SeekDone() to avoid the freeze bug.
2017-01-09 18:08:33 +01:00
Max Kellermann
904f83cd85 doc/developer: add GitHub reference 2017-01-09 17:19:15 +01:00
Max Kellermann
28bf100a50 doc/developer: more code style 2017-01-09 17:13:28 +01:00
Max Kellermann
accbd4e82a doc/developer: change C++11 to C++14 2017-01-09 17:13:28 +01:00
Max Kellermann
d7f478c154 doc/developer: add XML ids 2017-01-09 17:13:28 +01:00
Wieland Hoffmann
8f7f13fea4 doc/user: Replace "It used used" with "It is used" 2017-01-08 18:23:13 +01:00
Max Kellermann
c82b03a74c decoder/wavpack: fix crash bug 2017-01-08 14:54:12 +01:00
Max Kellermann
58fb36bdb9 storage/http: new storage plugin 2017-01-08 14:40:20 +01:00
Max Kellermann
4297a7b0a4 lib/curl/Request: move exception handling out of the WRITEFUNCTION
libcurl's WRITEFUNCTION is pretty fragile; if we destroy the CURL*
instance or even unregister it using curl_multi_remove_handle(),
libcurl will crash instantly.  But still we need to be able to handle
exceptions from inside the WRITEFUNCTION, and call
CurlResponseHandler::OnError(), which may destroy the whole thing.  As
a workaround, I use DeferredMonitor to postpone the OnError() call
into a stack frame which is allowed to destroy the request.
2017-01-08 14:36:27 +01:00
Max Kellermann
1bab6d0dd7 lib/curl/Request: move catch clause out of FinishHeaders
Let the caller decide what to do with the exception.
2017-01-08 14:36:27 +01:00
Max Kellermann
13b85edbe2 lib/curl/Request: postpone the curl_easy_cleanup() call
When the request is done, only unregister the CURL* handle, but do not
delete it yet - it may still be needed for CURLINFO_RESPONSE_CODE.
2017-01-08 13:51:53 +01:00
Max Kellermann
dc53098e43 lib/curl/Request: allow Stop() to be called twice
Convert assertion to runtime check.  This is useful because this is a
public method, and the caller has no chance to check if the object is
still registered.
2017-01-08 13:51:53 +01:00
Max Kellermann
3c66feff5a lib/curl/Global: defer the ReadInfo() call
Fixes a crash that can occur due to recursion from InvalidateSockets()
to ReadInfo() to CurlRequest callbacks.
2017-01-08 12:46:35 +01:00
Max Kellermann
218c3bc0d5 lib/curl/Multi: fix typo 2017-01-08 12:46:35 +01:00
Max Kellermann
9f5eddcd13 lib/curl/Global: move code to UpdateTimeout() 2017-01-08 12:44:07 +01:00
Max Kellermann
3cba76552b lib/curl/Global: drop redundant ">=0" check 2017-01-08 12:44:04 +01:00
Max Kellermann
e98a8b624b lib/curl/Global: drop redundant "virtual" 2017-01-08 12:41:26 +01:00
Max Kellermann
6c6947b01f util/UriUtil: add uri_get_path() 2017-01-08 11:05:58 +01:00
Max Kellermann
78c91e9e5b test/run_storage: don't print unknown time stamps 2017-01-08 10:41:08 +01:00
Max Kellermann
44493ca0c4 util/TimeParser: add "pure" attribute 2017-01-08 10:41:08 +01:00
Max Kellermann
42acf78b09 util/TimeParser: wrapper for strptime()
Move code from SongFilter.cxx.
2017-01-07 22:11:45 +01:00
TermeHansen
3aa9f8af18 Rewrite of AlsaMixerPlugin to use volume_mapping
Changed AlsaMixerPlugin to use the get and set normalized functions from volume_mapping of alsa-utils/alsamixer
Changed volume_mapping set volume to be for all channels and not per channel
added volume_mapping files to Makefile.am
2017-01-07 16:30:19 +01:00
TermeHansen
8a32ee30a5 Adding volume_mapping from alsa-utils/alsamixer
source:
http://git.alsa-project.org/?p=alsa-utils.git;a=blob_plain;f=alsamixer/volume_mapping.c;hb=HEAD
http://git.alsa-project.org/?p=alsa-utils.git;a=blob_plain;f=alsamixer/volume_mapping.h;hb=HEAD
2017-01-07 16:26:36 +01:00
Max Kellermann
981dc0626b lib/expat/ExpatParser: add constructor overload for XML_ParserCreateNS() 2017-01-07 16:17:53 +01:00
Max Kellermann
8986d14e98 lib/expat/ExpatParser: make constructors "explicit" 2017-01-07 16:15:11 +01:00
Max Kellermann
5163b1a624 lib/curl/Request: require the caller to explicitly register the request
This allows constructing an instance in any thread, and register it
inside the IOThread later.
2017-01-07 16:01:58 +01:00
Max Kellermann
860aa9d6d0 lib/expat/ExpatParser: move InputStream overload to separate source file
Eliminate one unnecessary dependency for debug programs which don't
need the InputStream API.
2017-01-07 15:46:36 +01:00
Max Kellermann
64dc5212f9 Makefile.am: add variable CURL_SOURCES 2017-01-07 14:19:24 +01:00
Max Kellermann
6cff3214f3 lib/curl/Slist: new wrapper for curl_slist 2017-01-06 19:37:31 +01:00
Max Kellermann
fd910bd5e9 db/upnp: use "override" instead of "virtual" 2017-01-06 19:35:58 +01:00
Max Kellermann
c6086bed41 filter/Internal: remove the default constructor
Not used.  Force implementations to initialize out_audio_format.
2017-01-06 12:45:52 +01:00
Max Kellermann
1a9dfdfab8 filter/AutoConvert: initialize Filter::out_audio_format 2017-01-06 12:44:55 +01:00
Max Kellermann
5284cd11a9 filter/AutoConvert: remove obsolete NULL check 2017-01-06 12:35:06 +01:00
Max Kellermann
d1a47cffad filter/convert: remove obsolete method prototype 2017-01-06 12:34:39 +01:00
Max Kellermann
f469595eee filter/Internal: remove obsolete doxygen line 2017-01-06 12:34:39 +01:00
Max Kellermann
9cfc52f114 filter/Internal: add assertion to constructor 2017-01-06 11:17:55 +01:00
Max Kellermann
30bfb756c2 configure.ac: prepare for 0.20.1 2017-01-05 19:36:32 +01:00
69 changed files with 2264 additions and 359 deletions

@@ -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

@@ -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

@@ -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>

@@ -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

@@ -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);
}

@@ -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));

@@ -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);
}

@@ -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 &params)
{
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 &params)
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

@@ -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

@@ -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

@@ -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

@@ -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
};

@@ -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,
};

@@ -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

@@ -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

@@ -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"