From 5acb978f8f9cd42c80ccaa27a21443c909b89d8e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 18:43:40 +0100
Subject: [PATCH 01/21] increment version number to 0.20.17

---
 NEWS                        | 2 ++
 android/AndroidManifest.xml | 4 ++--
 configure.ac                | 4 ++--
 3 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/NEWS b/NEWS
index 9b3f03ff9..b82f7b6f0 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,5 @@
+ver 0.20.17 (not yet released)
+
 ver 0.20.16 (2018/02/03)
 * output
   - pulse: fix crash during auto-detection
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 16a75f5e1..e5bc6a87f 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -2,8 +2,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="org.musicpd"
           android:installLocation="auto"
-          android:versionCode="15"
-          android:versionName="0.20.16">
+          android:versionCode="16"
+          android:versionName="0.20.17">
 
   <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17"/>
 
diff --git a/configure.ac b/configure.ac
index fff32141c..800baa048 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,10 +1,10 @@
 AC_PREREQ(2.60)
 
-AC_INIT(mpd, 0.20.16, musicpd-dev-team@lists.sourceforge.net)
+AC_INIT(mpd, 0.20.17, musicpd-dev-team@lists.sourceforge.net)
 
 VERSION_MAJOR=0
 VERSION_MINOR=20
-VERSION_REVISION=16
+VERSION_REVISION=17
 VERSION_EXTRA=0
 
 AC_CONFIG_SRCDIR([src/Main.cxx])

From 6f00f97b66c65110ff20edb28a7ee8fa6074a2c6 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 18:27:01 +0100
Subject: [PATCH 02/21] thread/Util: rename ioprio_set() to linux_ioprio_set()

Juse in cas glibc gets a wrapper for the system call which would then
conflict with ours.
---
 src/thread/Util.cxx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/thread/Util.cxx b/src/thread/Util.cxx
index c4c93f82e..f30d57368 100644
--- a/src/thread/Util.cxx
+++ b/src/thread/Util.cxx
@@ -41,7 +41,7 @@
 #if defined(__linux__) && !defined(ANDROID)
 
 static int
-ioprio_set(int which, int who, int ioprio)
+linux_ioprio_set(int which, int who, int ioprio)
 {
 	return syscall(__NR_ioprio_set, which, who, ioprio);
 }
@@ -55,7 +55,7 @@ ioprio_set_idle()
 	static constexpr int _IOPRIO_IDLE =
 		(_IOPRIO_CLASS_IDLE << _IOPRIO_CLASS_SHIFT) | 7;
 
-	ioprio_set(_IOPRIO_WHO_PROCESS, 0, _IOPRIO_IDLE);
+	linux_ioprio_set(_IOPRIO_WHO_PROCESS, 0, _IOPRIO_IDLE);
 }
 
 #endif

From 817656504dea224dbf849e5e988e277d798ae08e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 18:30:24 +0100
Subject: [PATCH 03/21] thread/Util: implement system call wrapper for
 sched_setscheduler()

There is a POSIX definition for sched_setscheduler(), but Linux does
not implement that; instead of changing the process's scheduler, it
only affects one thread.  This has caused some confusion among
application developers and C library developers.

While glibc implements Linux semantics, Musl has made their
sched_setscheduler() function an always-failing no-op, causing the
error message "sched_setscheduler failed: Function not implemented".

 http://git.musl-libc.org/cgit/musl/commit/src/sched/sched_setscheduler.c?id=1e21e78bf7a5c24c217446d8760be7b7188711c2

Instead of relying on the C library which may be unreliable here, we
now roll our own system call wrapper.

Closes #218
---
 NEWS                |  1 +
 src/thread/Util.cxx | 21 ++++++++++++++++++---
 2 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index b82f7b6f0..b11108401 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,5 @@
 ver 0.20.17 (not yet released)
+* fix real-time and idle scheduling with Musl
 
 ver 0.20.16 (2018/02/03)
 * output
diff --git a/src/thread/Util.cxx b/src/thread/Util.cxx
index f30d57368..0dbf8181a 100644
--- a/src/thread/Util.cxx
+++ b/src/thread/Util.cxx
@@ -38,7 +38,9 @@
 #include <windows.h>
 #endif
 
-#if defined(__linux__) && !defined(ANDROID)
+#ifdef __linux__
+
+#ifndef ANDROID
 
 static int
 linux_ioprio_set(int which, int who, int ioprio)
@@ -58,6 +60,19 @@ ioprio_set_idle()
 	linux_ioprio_set(_IOPRIO_WHO_PROCESS, 0, _IOPRIO_IDLE);
 }
 
+#endif /* !ANDROID */
+
+/**
+ * Wrapper for the "sched_setscheduler" system call.  We don't use the
+ * one from the C library because Musl has an intentionally broken
+ * implementation.
+ */
+static int
+linux_sched_setscheduler(pid_t pid, int sched, const struct sched_param *param)
+{
+	return syscall(__NR_sched_setscheduler, pid, sched, param);
+}
+
 #endif
 
 void
@@ -66,7 +81,7 @@ SetThreadIdlePriority()
 #ifdef __linux__
 #ifdef SCHED_IDLE
 	static struct sched_param sched_param;
-	sched_setscheduler(0, SCHED_IDLE, &sched_param);
+	linux_sched_setscheduler(0, SCHED_IDLE, &sched_param);
 #endif
 
 #ifndef ANDROID
@@ -92,7 +107,7 @@ SetThreadRealtime()
 	policy |= SCHED_RESET_ON_FORK;
 #endif
 
-	if (sched_setscheduler(0, policy, &sched_param) < 0)
+	if (linux_sched_setscheduler(0, policy, &sched_param) < 0)
 		throw MakeErrno("sched_setscheduler failed");
 #endif	// __linux__
 };

From c801936e531222ba1cb526c585d8ece3e68315bf Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 18:48:14 +0100
Subject: [PATCH 04/21] db/update/Service: set the update thread name

---
 src/db/update/Service.cxx | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/db/update/Service.cxx b/src/db/update/Service.cxx
index 8660575c6..eee89c484 100644
--- a/src/db/update/Service.cxx
+++ b/src/db/update/Service.cxx
@@ -29,6 +29,7 @@
 #include "Idle.hxx"
 #include "Log.hxx"
 #include "thread/Thread.hxx"
+#include "thread/Name.hxx"
 #include "thread/Util.hxx"
 
 #ifndef NDEBUG
@@ -113,6 +114,8 @@ UpdateService::Task()
 {
 	assert(walk != nullptr);
 
+	SetThreadName("update");
+
 	if (!next.path_utf8.empty())
 		FormatDebug(update_domain, "starting: %s",
 			    next.path_utf8.c_str());

From 6de92bb42bb61e7eb849bd554429579d6c61a77e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 19:01:12 +0100
Subject: [PATCH 05/21] pcm/Order: fix size calculation with 8 channels

This was a buffer overflow bug which could cause MPD crahes when
playing back 8 channels with the ALSA output plugin.

Closes #216
---
 NEWS              | 2 ++
 src/pcm/Order.cxx | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index b11108401..b9057693d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
 ver 0.20.17 (not yet released)
+* output
+  - alsa: fix crash bug with 8 channels
 * fix real-time and idle scheduling with Musl
 
 ver 0.20.16 (2018/02/03)
diff --git a/src/pcm/Order.cxx b/src/pcm/Order.cxx
index 1812b14dd..736eb472d 100644
--- a/src/pcm/Order.cxx
+++ b/src/pcm/Order.cxx
@@ -88,7 +88,7 @@ static inline ConstBuffer<V>
 ToAlsaChannelOrder71(PcmBuffer &buffer, ConstBuffer<V> src)
 {
 	auto dest = buffer.GetT<V>(src.size);
-	ToAlsaChannelOrder71(dest, src.data, src.size / 6);
+	ToAlsaChannelOrder71(dest, src.data, src.size / 8);
 	return { dest, src.size };
 }
 

From 56b74ad990d8d6180fe47fbb271469b2825d2d42 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 19:04:45 +0100
Subject: [PATCH 06/21] filter/convert: add method IsActive()

---
 src/filter/plugins/ConvertFilterPlugin.cxx | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/filter/plugins/ConvertFilterPlugin.cxx b/src/filter/plugins/ConvertFilterPlugin.cxx
index 1ede2c03e..bcc70fcd5 100644
--- a/src/filter/plugins/ConvertFilterPlugin.cxx
+++ b/src/filter/plugins/ConvertFilterPlugin.cxx
@@ -57,6 +57,11 @@ public:
 	}
 
 	ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
+
+private:
+	bool IsActive() const noexcept {
+		return out_audio_format != in_audio_format;
+	}
 };
 
 class PreparedConvertFilter final : public PreparedFilter {
@@ -80,7 +85,7 @@ ConvertFilter::Set(const AudioFormat &_out_audio_format)
 		/* no change */
 		return;
 
-	if (out_audio_format != in_audio_format) {
+	if (IsActive()) {
 		out_audio_format = in_audio_format;
 		state.Close();
 	}
@@ -111,7 +116,7 @@ ConvertFilter::~ConvertFilter()
 {
 	assert(in_audio_format.IsValid());
 
-	if (out_audio_format != in_audio_format)
+	if (IsActive())
 		state.Close();
 }
 
@@ -120,11 +125,10 @@ ConvertFilter::FilterPCM(ConstBuffer<void> src)
 {
 	assert(in_audio_format.IsValid());
 
-	if (out_audio_format == in_audio_format)
+	return IsActive()
+		? state.Convert(src)
 		/* optimized special case: no-op */
-		return src;
-
-	return state.Convert(src);
+		: src;
 }
 
 const FilterPlugin convert_filter_plugin = {

From e46fbd0780808835c75ada242b536f030459c8a6 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 19:05:45 +0100
Subject: [PATCH 07/21] filter/convert: set the PcmConvert instance only if it
 was initialized

Fixes valgrind warning.
---
 src/filter/plugins/ConvertFilterPlugin.cxx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/filter/plugins/ConvertFilterPlugin.cxx b/src/filter/plugins/ConvertFilterPlugin.cxx
index bcc70fcd5..cec95222b 100644
--- a/src/filter/plugins/ConvertFilterPlugin.cxx
+++ b/src/filter/plugins/ConvertFilterPlugin.cxx
@@ -53,7 +53,8 @@ public:
 	void Set(const AudioFormat &_out_audio_format);
 
 	void Reset() override {
-		state.Reset();
+		if (IsActive())
+			state.Reset();
 	}
 
 	ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;

From 738317bf34a783388535768eaa777df6f7c8e071 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 19:11:39 +0100
Subject: [PATCH 08/21] doc/user: document MPD on Android

Closes #217
---
 doc/user.xml | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/doc/user.xml b/doc/user.xml
index 5cc741227..feab01cc6 100644
--- a/doc/user.xml
+++ b/doc/user.xml
@@ -66,6 +66,23 @@
       </para>
     </section>
 
+    <section id="install_android">
+      <title>Installing on Android</title>
+
+      <para>
+        An experimental Android build is available on <ulink
+        url="https://play.google.com/store/apps/details?id=org.musicpd">Google
+        Play</ulink>.  After installing and launching it, MPD will
+        scan the music in your <filename>Music</filename> directory
+        and you can control it as usual with a MPD client.
+      </para>
+
+      <para>
+        If you need to tweak the configuration, you can create a file
+        called <filename>mpd.conf</filename> on the data partition.
+      </para>
+    </section>
+
     <section id="install_source">
       <title>Compiling from source</title>
 
@@ -323,7 +340,9 @@ systemctl start mpd.socket</programlisting>
         <application>MPD</application> as a user daemon (and not as a
         system daemon), the configuration is read from
         <filename>$XDG_CONFIG_HOME/mpd/mpd.conf</filename> (usually
-        <filename>~/.config/mpd/mpd.conf</filename>).
+        <filename>~/.config/mpd/mpd.conf</filename>).  On Android,
+        <filename>mpd.conf</filename> will be loaded from the
+        top-level directory of the data partition.
       </para>
 
       <para>

From 2988bb77e816c1d502bbb58a55e7ef7a30662640 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 22:54:02 +0100
Subject: [PATCH 09/21] python/build/project: allow trailing digit after letter
 in version number

For version numbers such as OpenSSH's, e.g.: "7.2p2"
---
 python/build/project.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/python/build/project.py b/python/build/project.py
index 087ca3d07..352806fcb 100644
--- a/python/build/project.py
+++ b/python/build/project.py
@@ -18,7 +18,7 @@ class Project:
             self.base = base
 
         if name is None or version is None:
-            m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?)$', self.base)
+            m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*)$', self.base)
             if name is None: name = m.group(1)
             if version is None: version = m.group(2)
 

From b8a094470b157894aa49db4466fb0296bb164573 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 22:26:07 +0100
Subject: [PATCH 10/21] python/build/libs.py: build only the library

---
 python/build/libs.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/python/build/libs.py b/python/build/libs.py
index a46902a19..569159f34 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -358,6 +358,12 @@ curl = AutotoolsProject(
         '--disable-crypto-auth', '--disable-ntlm-wb', '--disable-tls-srp', '--disable-cookies',
         '--without-ssl', '--without-gnutls', '--without-nss', '--without-libssh2',
     ],
+
+    edits={
+        # build only the library, not the "curl" command-line tool
+        'Makefile.in': lambda data: re.sub(r'^SUBDIRS = lib src$', r'SUBDIRS = lib',
+                                           data, count=1, flags=re.MULTILINE),
+    }
 )
 
 boost = BoostProject(

From 19a2885fd5174f65a17ccb20d2fc790269baea02 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 20:25:32 +0100
Subject: [PATCH 11/21] protocol/ArgParser: use strtod() instead of strtof() on
 Android

For Android pre-5.0 compatibility (#213).
---
 src/protocol/ArgParser.cxx | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/protocol/ArgParser.cxx b/src/protocol/ArgParser.cxx
index 185c57863..2665270e6 100644
--- a/src/protocol/ArgParser.cxx
+++ b/src/protocol/ArgParser.cxx
@@ -151,7 +151,12 @@ float
 ParseCommandArgFloat(const char *s)
 {
 	char *endptr;
+#ifdef ANDROID
+	/* strtof() requires API level 21 */
+	auto value = strtod(s, &endptr);
+#else
 	auto value = strtof(s, &endptr);
+#endif
 	if (endptr == s || *endptr != 0)
 		throw FormatProtocolError(ACK_ERROR_ARG,
 					  "Float expected: %s", s);

From 0f8d223c7fcc7e61808716dbb740786b9d95a991 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 20:27:00 +0100
Subject: [PATCH 12/21] protocol/ArgParser: move strtof()/strtod() switch to
 util/NumberParser.hxx

---
 src/protocol/ArgParser.cxx | 8 ++------
 src/util/NumberParser.hxx  | 5 +++++
 2 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/src/protocol/ArgParser.cxx b/src/protocol/ArgParser.cxx
index 2665270e6..47fdfa405 100644
--- a/src/protocol/ArgParser.cxx
+++ b/src/protocol/ArgParser.cxx
@@ -21,6 +21,7 @@
 #include "ArgParser.hxx"
 #include "Ack.hxx"
 #include "Chrono.hxx"
+#include "util/NumberParser.hxx"
 
 #include <stdlib.h>
 
@@ -151,12 +152,7 @@ float
 ParseCommandArgFloat(const char *s)
 {
 	char *endptr;
-#ifdef ANDROID
-	/* strtof() requires API level 21 */
-	auto value = strtod(s, &endptr);
-#else
-	auto value = strtof(s, &endptr);
-#endif
+	auto value = ParseFloat(s, &endptr);
 	if (endptr == s || *endptr != 0)
 		throw FormatProtocolError(ACK_ERROR_ARG,
 					  "Float expected: %s", s);
diff --git a/src/util/NumberParser.hxx b/src/util/NumberParser.hxx
index 47e9aacbd..67d42affa 100644
--- a/src/util/NumberParser.hxx
+++ b/src/util/NumberParser.hxx
@@ -78,7 +78,12 @@ ParseDouble(const char *p, char **endptr=nullptr)
 static inline float
 ParseFloat(const char *p, char **endptr=nullptr)
 {
+#if defined(__BIONIC__) && __ANDROID_API__ < 21
+	/* strtof() requires API level 21 */
 	return (float)ParseDouble(p, endptr);
+#else
+	return strtof(p, endptr);
+#endif
 }
 
 #endif

From e8b70dbca43340c4d8c4ed3b7aff508b5dce40d3 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 20:46:55 +0100
Subject: [PATCH 13/21] SongSave, queue/PlaylistState, tag/ReplayGain: use
 portable atof() wrappers

For Android pre-5.0 compatibility (#213).
---
 src/SongSave.cxx            | 3 ++-
 src/queue/PlaylistState.cxx | 7 ++++---
 src/tag/ReplayGain.cxx      | 9 +++++----
 3 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/src/SongSave.cxx b/src/SongSave.cxx
index ace38d4fe..ed5ab6d30 100644
--- a/src/SongSave.cxx
+++ b/src/SongSave.cxx
@@ -28,6 +28,7 @@
 #include "tag/TagBuilder.hxx"
 #include "util/StringUtil.hxx"
 #include "util/RuntimeError.hxx"
+#include "util/NumberParser.hxx"
 
 #include <string.h>
 #include <stdlib.h>
@@ -94,7 +95,7 @@ song_load(TextFile &file, const char *uri)
 		if ((type = tag_name_parse(line)) != TAG_NUM_OF_ITEM_TYPES) {
 			tag.AddItem(type, value);
 		} else if (strcmp(line, "Time") == 0) {
-			tag.SetDuration(SignedSongTime::FromS(atof(value)));
+			tag.SetDuration(SignedSongTime::FromS(ParseDouble(value)));
 		} else if (strcmp(line, "Playlist") == 0) {
 			tag.SetHasPlaylist(strcmp(value, "yes") == 0);
 		} else if (strcmp(line, SONG_MTIME) == 0) {
diff --git a/src/queue/PlaylistState.cxx b/src/queue/PlaylistState.cxx
index d08610efc..614f01c19 100644
--- a/src/queue/PlaylistState.cxx
+++ b/src/queue/PlaylistState.cxx
@@ -35,6 +35,7 @@
 #include "util/CharUtil.hxx"
 #include "util/StringAPI.hxx"
 #include "util/StringCompare.hxx"
+#include "util/NumberParser.hxx"
 #include "Log.hxx"
 
 #include <string.h>
@@ -148,7 +149,7 @@ playlist_state_restore(const char *line, TextFile &file,
 	while ((line = file.ReadLine()) != nullptr) {
 		const char *p;
 		if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_TIME))) {
-			seek_time = SongTime::FromS(atof(p));
+			seek_time = SongTime::FromS(ParseDouble(p));
 		} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_REPEAT))) {
 			playlist.SetRepeat(pc, StringIsEqual(p, "1"));
 		} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_SINGLE))) {
@@ -158,12 +159,12 @@ playlist_state_restore(const char *line, TextFile &file,
 		} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_CROSSFADE))) {
 			pc.SetCrossFade(atoi(p));
 		} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_MIXRAMPDB))) {
-			pc.SetMixRampDb(atof(p));
+			pc.SetMixRampDb(ParseFloat(p));
 		} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_MIXRAMPDELAY))) {
 			/* this check discards "nan" which was used
 			   prior to MPD 0.18 */
 			if (IsDigitASCII(*p))
-				pc.SetMixRampDelay(atof(p));
+				pc.SetMixRampDelay(ParseFloat(p));
 		} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_RANDOM))) {
 			random_mode = StringIsEqual(p, "1");
 		} else if ((p = StringAfterPrefix(line, PLAYLIST_STATE_FILE_CURRENT))) {
diff --git a/src/tag/ReplayGain.cxx b/src/tag/ReplayGain.cxx
index 10f2f8a32..ccdcf3fc8 100644
--- a/src/tag/ReplayGain.cxx
+++ b/src/tag/ReplayGain.cxx
@@ -22,6 +22,7 @@
 #include "VorbisComment.hxx"
 #include "ReplayGainInfo.hxx"
 #include "util/ASCII.hxx"
+#include "util/NumberParser.hxx"
 
 #include <assert.h>
 #include <stdlib.h>
@@ -33,16 +34,16 @@ ParseReplayGainTagTemplate(ReplayGainInfo &info, const T t)
 	const char *value;
 
 	if ((value = t["replaygain_track_gain"]) != nullptr) {
-		info.track.gain = atof(value);
+		info.track.gain = ParseFloat(value);
 		return true;
 	} else if ((value = t["replaygain_album_gain"]) != nullptr) {
-		info.album.gain = atof(value);
+		info.album.gain = ParseFloat(value);
 		return true;
 	} else if ((value = t["replaygain_track_peak"]) != nullptr) {
-		info.track.peak = atof(value);
+		info.track.peak = ParseFloat(value);
 		return true;
 	} else if ((value = t["replaygain_album_peak"]) != nullptr) {
-		info.album.peak = atof(value);
+		info.album.peak = ParseFloat(value);
 		return true;
 	} else
 		return false;

From 6ba918b20346056294c15bdf1abd207f1dca84b4 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 22:37:12 +0100
Subject: [PATCH 14/21] input/file: don't use posix_fadvise() on Android

Requires Android API 21, but we want to support older versions as
well.
---
 src/input/plugins/FileInputPlugin.cxx | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/input/plugins/FileInputPlugin.cxx b/src/input/plugins/FileInputPlugin.cxx
index 2d2850c74..518de233a 100644
--- a/src/input/plugins/FileInputPlugin.cxx
+++ b/src/input/plugins/FileInputPlugin.cxx
@@ -65,9 +65,12 @@ OpenFileInputStream(Path path,
 		throw FormatRuntimeError("Not a regular file: %s",
 					 path.c_str());
 
+#if !defined(__BIONIC__) || __ANDROID_API__ >= 21
+	/* posix_fadvise() requires Android API 21 */
 #ifdef POSIX_FADV_SEQUENTIAL
 	posix_fadvise(reader.GetFD().Get(), (off_t)0, info.GetSize(),
 		      POSIX_FADV_SEQUENTIAL);
+#endif
 #endif
 
 	return InputStreamPtr(new FileInputStream(path.ToUTF8().c_str(),

From 927071e08511442604e7514aac8399229f722b63 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 22:57:31 +0100
Subject: [PATCH 15/21] python/build/project.py: add quilt support

---
 python/build/project.py | 9 +++++++++
 python/build/quilt.py   | 9 +++++++++
 2 files changed, 18 insertions(+)
 create mode 100644 python/build/quilt.py

diff --git a/python/build/project.py b/python/build/project.py
index 352806fcb..b78c89238 100644
--- a/python/build/project.py
+++ b/python/build/project.py
@@ -3,10 +3,12 @@ import re
 
 from build.download import download_and_verify
 from build.tar import untar
+from build.quilt import push_all
 
 class Project:
     def __init__(self, url, md5, installed, name=None, version=None,
                  base=None,
+                 patches=None,
                  edits=None,
                  use_cxx=False):
         if base is None:
@@ -29,6 +31,10 @@ class Project:
         self.md5 = md5
         self.installed = installed
 
+        if patches is not None:
+            srcdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+            patches = os.path.join(srcdir, patches)
+        self.patches = patches
         self.edits = edits
         self.use_cxx = use_cxx
 
@@ -51,6 +57,9 @@ class Project:
             parent_path = toolchain.build_path
         path = untar(self.download(toolchain), parent_path, self.base)
 
+        if self.patches is not None:
+            push_all(toolchain, path, self.patches)
+
         if self.edits is not None:
             for filename, function in self.edits.items():
                 with open(os.path.join(path, filename), 'r+t') as f:
diff --git a/python/build/quilt.py b/python/build/quilt.py
new file mode 100644
index 000000000..876453d2b
--- /dev/null
+++ b/python/build/quilt.py
@@ -0,0 +1,9 @@
+import subprocess
+
+def run_quilt(toolchain, cwd, patches_path, *args):
+    env = dict(toolchain.env)
+    env['QUILT_PATCHES'] = patches_path
+    subprocess.check_call(['quilt'] + list(args), cwd=cwd, env=env)
+
+def push_all(toolchain, src_path, patches_path):
+    run_quilt(toolchain, src_path, patches_path, 'push', '-a')

From 3859a504662fdf829e432b8b8cd39ccc277a852b Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 23:02:35 +0100
Subject: [PATCH 16/21] python/build/libs.py: convert CURL edit to quilt patch

---
 python/build/libs.py                |  8 +++-----
 src/lib/curl/patches/only_lib.patch | 15 +++++++++++++++
 src/lib/curl/patches/series         |  1 +
 3 files changed, 19 insertions(+), 5 deletions(-)
 create mode 100644 src/lib/curl/patches/only_lib.patch
 create mode 100644 src/lib/curl/patches/series

diff --git a/python/build/libs.py b/python/build/libs.py
index 569159f34..7d93799d8 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -1,4 +1,6 @@
 import re
+from os.path import abspath
+
 from build.project import Project
 from build.zlib import ZlibProject
 from build.autotools import AutotoolsProject
@@ -359,11 +361,7 @@ curl = AutotoolsProject(
         '--without-ssl', '--without-gnutls', '--without-nss', '--without-libssh2',
     ],
 
-    edits={
-        # build only the library, not the "curl" command-line tool
-        'Makefile.in': lambda data: re.sub(r'^SUBDIRS = lib src$', r'SUBDIRS = lib',
-                                           data, count=1, flags=re.MULTILINE),
-    }
+    patches='src/lib/curl/patches',
 )
 
 boost = BoostProject(
diff --git a/src/lib/curl/patches/only_lib.patch b/src/lib/curl/patches/only_lib.patch
new file mode 100644
index 000000000..2bc374983
--- /dev/null
+++ b/src/lib/curl/patches/only_lib.patch
@@ -0,0 +1,15 @@
+Index: curl-7.58.0/Makefile.in
+===================================================================
+--- curl-7.58.0.orig/Makefile.in
++++ curl-7.58.0/Makefile.in
+@@ -641,8 +641,8 @@ CLEANFILES = $(VC6_LIBDSP) $(VC6_SRCDSP)
+  $(VC14_LIBVCXPROJ) $(VC14_SRCVCXPROJ) $(VC15_LIBVCXPROJ) $(VC15_SRCVCXPROJ)
+ 
+ bin_SCRIPTS = curl-config
+-SUBDIRS = lib src
+-DIST_SUBDIRS = $(SUBDIRS) tests packages scripts include docs
++SUBDIRS = lib
++DIST_SUBDIRS = $(SUBDIRS) include
+ pkgconfigdir = $(libdir)/pkgconfig
+ pkgconfig_DATA = libcurl.pc
+ LIB_VAUTH_CFILES = vauth/vauth.c vauth/cleartext.c vauth/cram.c         \
diff --git a/src/lib/curl/patches/series b/src/lib/curl/patches/series
new file mode 100644
index 000000000..5d0e89439
--- /dev/null
+++ b/src/lib/curl/patches/series
@@ -0,0 +1 @@
+only_lib.patch

From 9187a0810620a597ff1e583f55f47be4de387e96 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 23:07:49 +0100
Subject: [PATCH 17/21] lib/curl: remove .netrc support on Android

Not needed on Android, and the implementation uses getpwuid_r() which
is unavailable on old Android versions.
---
 src/lib/curl/patches/no_netrc.patch | 20 ++++++++++++++++++++
 src/lib/curl/patches/series         |  1 +
 2 files changed, 21 insertions(+)
 create mode 100644 src/lib/curl/patches/no_netrc.patch

diff --git a/src/lib/curl/patches/no_netrc.patch b/src/lib/curl/patches/no_netrc.patch
new file mode 100644
index 000000000..22557d162
--- /dev/null
+++ b/src/lib/curl/patches/no_netrc.patch
@@ -0,0 +1,20 @@
+Index: curl-7.58.0/lib/url.c
+===================================================================
+--- curl-7.58.0.orig/lib/url.c
++++ curl-7.58.0/lib/url.c
+@@ -3503,6 +3503,7 @@ static CURLcode override_login(struct Cu
+   }
+ 
+   conn->bits.netrc = FALSE;
++#ifndef __BIONIC__
+   if(data->set.use_netrc != CURL_NETRC_IGNORED) {
+     int ret = Curl_parsenetrc(conn->host.name,
+                               userp, passwdp,
+@@ -3524,6 +3525,7 @@ static CURLcode override_login(struct Cu
+       conn->bits.user_passwd = TRUE; /* enable user+password */
+     }
+   }
++#endif
+ 
+   return CURLE_OK;
+ }
diff --git a/src/lib/curl/patches/series b/src/lib/curl/patches/series
index 5d0e89439..f9bd7bfe3 100644
--- a/src/lib/curl/patches/series
+++ b/src/lib/curl/patches/series
@@ -1 +1,2 @@
 only_lib.patch
+no_netrc.patch

From 31c206bf80b85a033aa9dfbc6e56ac738c611e19 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 10 Feb 2018 00:00:53 +0100
Subject: [PATCH 18/21] android/build.py: add -mfpu=vfp, explicitly disabling
 NEON

Apparently, clang defaults to NEON when ARMv7 is used.  Not all ARMv7
CPUs we target have NEON, so we need to disable that.
---
 android/build.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/android/build.py b/android/build.py
index 0b001e7da..328b02edf 100755
--- a/android/build.py
+++ b/android/build.py
@@ -67,7 +67,7 @@ class AndroidNdkToolchain:
 
         common_flags = '-Os -g'
         common_flags += ' -fPIC'
-        common_flags += ' -march=armv7-a -mfloat-abi=softfp'
+        common_flags += ' -march=armv7-a -mfpu=vfp -mfloat-abi=softfp'
 
         toolchain_bin = os.path.join(toolchain_path, 'bin')
         llvm_bin = os.path.join(llvm_path, 'bin')

From e0ca4b865a2dcb581c90cff66e919b30312b45da Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 9 Feb 2018 22:22:44 +0100
Subject: [PATCH 19/21] android: require SDK version 14

Closes #213.
---
 NEWS                        | 2 ++
 android/AndroidManifest.xml | 2 +-
 android/build.py            | 4 ++--
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index b9057693d..9ee17962a 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,8 @@ ver 0.20.17 (not yet released)
 * output
   - alsa: fix crash bug with 8 channels
 * fix real-time and idle scheduling with Musl
+* Android
+  - fix compatibility with Android 4.0
 
 ver 0.20.16 (2018/02/03)
 * output
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index e5bc6a87f..713c136bf 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -5,7 +5,7 @@
           android:versionCode="16"
           android:versionName="0.20.17">
 
-  <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17"/>
+  <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17"/>
 
   <application android:icon="@drawable/icon" android:label="@string/app_name">
     <activity android:name=".Main"
diff --git a/android/build.py b/android/build.py
index 328b02edf..3e2e71dcc 100755
--- a/android/build.py
+++ b/android/build.py
@@ -46,7 +46,7 @@ class AndroidNdkToolchain:
 
         self.ndk_arch = 'arm'
         android_abi = 'armeabi-v7a'
-        ndk_platform = 'android-21'
+        ndk_platform = 'android-14'
 
         # select the NDK compiler
         gcc_version = '4.9'
@@ -87,7 +87,7 @@ class AndroidNdkToolchain:
         self.cppflags = '--sysroot=' + sysroot + \
             ' -isystem ' + os.path.join(install_prefix, 'include') + \
             ' -isystem ' + os.path.join(sysroot, 'usr', 'include', arch) + \
-            ' -D__ANDROID_API__=21'
+            ' -D__ANDROID_API__=14'
         self.ldflags = '--sysroot=' + sysroot + \
             ' -L' + os.path.join(install_prefix, 'lib') + \
             ' -L' + os.path.join(target_root, 'usr', 'lib') + \

From bede564618e78d2a4eee5af329178be89ae34412 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 10 Feb 2018 09:07:51 +0100
Subject: [PATCH 20/21] mixer/alsa: work around rounding error at volume 0

Due to rounding errors, a slightly negative value can be passed to
set_normalized_volume(), which will make the log10() call fail.
Actually, volume 0 is already failing because log10(0) is illegal.  So
let's fix this by implementing two corner cases: <=0 and >=100.

Closes #212
---
 NEWS                               | 2 ++
 src/mixer/plugins/volume_mapping.c | 7 +++++++
 2 files changed, 9 insertions(+)

diff --git a/NEWS b/NEWS
index 9ee17962a..190ef1015 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,8 @@
 ver 0.20.17 (not yet released)
 * output
   - alsa: fix crash bug with 8 channels
+* mixer
+  - alsa: fix rounding error at volume 0
 * fix real-time and idle scheduling with Musl
 * Android
   - fix compatibility with Android 4.0
diff --git a/src/mixer/plugins/volume_mapping.c b/src/mixer/plugins/volume_mapping.c
index 4e559cf54..2078d346d 100644
--- a/src/mixer/plugins/volume_mapping.c
+++ b/src/mixer/plugins/volume_mapping.c
@@ -139,6 +139,13 @@ static int set_normalized_volume(snd_mixer_elem_t *elem,
 		return set_raw[ctl_dir](elem, value);
 	}
 
+	/* two special cases to avoid rounding errors at 0% and
+	   100% */
+	if (volume <= 0)
+		return set_dB[ctl_dir](elem, min, dir);
+	else if (volume >= 100)
+		return set_dB[ctl_dir](elem, max, dir);
+
 	if (use_linear_dB_scale(min, max)) {
 		value = lrint_dir(volume * (max - min), dir) + min;
 		return set_dB[ctl_dir](elem, value, dir);

From c2940a8385201b6501a43aefa6f9bee983950c01 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sun, 11 Feb 2018 13:02:53 +0100
Subject: [PATCH 21/21] release v0.20.17

---
 NEWS | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 190ef1015..fd91dea42 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,4 @@
-ver 0.20.17 (not yet released)
+ver 0.20.17 (2018/02/11)
 * output
   - alsa: fix crash bug with 8 channels
 * mixer