Commit 0309db33 authored by Florian Schlichting's avatar Florian Schlichting

New upstream version 0.20.19

parent 3dc04775
......@@ -31,3 +31,8 @@ The following people have contributed code to MPD:
Jean-Francois Dockes <jf@dockes.org>
Yue Wang <yuleopen@gmail.com>
Matthew Leon Grinshpun <ml@matthewleon.com>
Dimitris Papastamos <sin@2f30.org>
Florian Schlichting <fsfs@debian.org>
François Revol <revol@free.fr>
Jacob Vosmaer <contact@jacobvosmaer.nl>
Thomas Guillem <thomas@gllm.fr>
......@@ -15615,8 +15615,8 @@ maintainer-clean-generic:
@echo "it deletes files that may require special tools to rebuild."
-test -z "$(BUILT_SOURCES)" || rm -f $(BUILT_SOURCES)
@ENABLE_DOCUMENTATION_FALSE@install-data-local:
@ANDROID_FALSE@@ENABLE_DOCUMENTATION_FALSE@@ENABLE_HAIKU_FALSE@clean-local:
@ENABLE_DOCUMENTATION_FALSE@uninstall-local:
@ANDROID_FALSE@@ENABLE_DOCUMENTATION_FALSE@@ENABLE_HAIKU_FALSE@clean-local:
clean: clean-am
clean-am: clean-binPROGRAMS clean-generic clean-local \
ver 0.20.19 (2018/04/26)
* protocol
- validate absolute seek time, reject negative values
* database
- proxy: fix "search already in progress" errors
- proxy: implement "list ... group"
* input
- mms: fix lockup bug and a crash bug
* decoder
- ffmpeg: fix av_register_all() deprecation warning (FFmpeg 4.0)
* player
- fix spurious "Not seekable" error when switching radio streams
* macOS: fix crash bug
ver 0.20.18 (2018/02/24)
* input
- curl: allow authentication methods other than "Basic"
......
......@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
android:versionCode="17"
android:versionName="0.20.18">
android:versionCode="18"
android:versionName="0.20.19">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17"/>
......
......@@ -3,13 +3,14 @@
import os, os.path
import sys, subprocess
if len(sys.argv) < 3:
print("Usage: build.py SDK_PATH NDK_PATH [configure_args...]", file=sys.stderr)
if len(sys.argv) < 4:
print("Usage: build.py SDK_PATH NDK_PATH ABI [configure_args...]", file=sys.stderr)
sys.exit(1)
sdk_path = sys.argv[1]
ndk_path = sys.argv[2]
configure_args = sys.argv[3:]
android_abi = sys.argv[3]
configure_args = sys.argv[4:]
if not os.path.isfile(os.path.join(sdk_path, 'tools', 'android')):
print("SDK not found in", ndk_path, file=sys.stderr)
......@@ -19,8 +20,27 @@ if not os.path.isdir(ndk_path):
print("NDK not found in", ndk_path, file=sys.stderr)
sys.exit(1)
android_abis = {
'armeabi-v7a': {
'arch': 'arm-linux-androideabi',
'ndk_arch': 'arm',
'toolchain_arch': 'arm-linux-androideabi',
'llvm_triple': 'armv7-none-linux-androideabi',
'cflags': '-march=armv7-a -mfpu=vfp -mfloat-abi=softfp',
},
'x86': {
'arch': 'i686-linux-android',
'ndk_arch': 'x86',
'toolchain_arch': 'x86',
'llvm_triple': 'i686-none-linux-android',
'cflags': '-march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32',
},
}
# select the NDK target
arch = 'arm-linux-androideabi'
abi_info = android_abis[android_abi]
arch = abi_info['arch']
# the path to the MPD sources
mpd_path = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]) or '.', '..'))
......@@ -44,8 +64,7 @@ class AndroidNdkToolchain:
self.src_path = src_path
self.build_path = build_path
self.ndk_arch = 'arm'
android_abi = 'armeabi-v7a'
ndk_arch = abi_info['ndk_arch']
ndk_platform = 'android-14'
# select the NDK compiler
......@@ -53,7 +72,7 @@ class AndroidNdkToolchain:
ndk_platform_path = os.path.join(ndk_path, 'platforms', ndk_platform)
sysroot = os.path.join(ndk_path, 'sysroot')
target_root = os.path.join(ndk_platform_path, 'arch-' + self.ndk_arch)
target_root = os.path.join(ndk_platform_path, 'arch-' + ndk_arch)
install_prefix = os.path.join(arch_path, 'root')
......@@ -61,13 +80,13 @@ class AndroidNdkToolchain:
self.install_prefix = install_prefix
self.sysroot = sysroot
toolchain_path = os.path.join(ndk_path, 'toolchains', arch + '-' + gcc_version, 'prebuilt', build_arch)
toolchain_path = os.path.join(ndk_path, 'toolchains', abi_info['toolchain_arch'] + '-' + gcc_version, 'prebuilt', build_arch)
llvm_path = os.path.join(ndk_path, 'toolchains', 'llvm', 'prebuilt', build_arch)
llvm_triple = 'armv7-none-linux-androideabi'
llvm_triple = abi_info['llvm_triple']
common_flags = '-Os -g'
common_flags += ' -fPIC'
common_flags += ' -march=armv7-a -mfpu=vfp -mfloat-abi=softfp'
common_flags += ' ' + abi_info['cflags']
toolchain_bin = os.path.join(toolchain_path, 'bin')
llvm_bin = os.path.join(llvm_path, 'bin')
......@@ -95,7 +114,7 @@ class AndroidNdkToolchain:
' ' + common_flags
self.libs = ''
self.is_arm = self.ndk_arch == 'arm'
self.is_arm = ndk_arch == 'arm'
self.is_armv7 = self.is_arm and 'armv7' in self.cflags
self.is_windows = False
......
#! /bin/sh
# Guess values for system-dependent variables and create Makefiles.
# Generated by GNU Autoconf 2.69 for mpd 0.20.18.
# Generated by GNU Autoconf 2.69 for mpd 0.20.19.
#
# Report bugs to <musicpd-dev-team@lists.sourceforge.net>.
#
......@@ -580,8 +580,8 @@ MAKEFLAGS=
# Identity of this package.
PACKAGE_NAME='mpd'
PACKAGE_TARNAME='mpd'
PACKAGE_VERSION='0.20.18'
PACKAGE_STRING='mpd 0.20.18'
PACKAGE_VERSION='0.20.19'
PACKAGE_STRING='mpd 0.20.19'
PACKAGE_BUGREPORT='musicpd-dev-team@lists.sourceforge.net'
PACKAGE_URL=''
......@@ -1785,7 +1785,7 @@ if test "$ac_init_help" = "long"; then
# Omit some internal or obsolete options to make the list less imposing.
# This message is too long to be a string in the A/UX 3.1 sh.
cat <<_ACEOF
\`configure' configures mpd 0.20.18 to adapt to many kinds of systems.
\`configure' configures mpd 0.20.19 to adapt to many kinds of systems.
Usage: $0 [OPTION]... [VAR=VALUE]...
......@@ -1856,7 +1856,7 @@ fi
if test -n "$ac_init_help"; then
case $ac_init_help in
short | recursive ) echo "Configuration of mpd 0.20.18:";;
short | recursive ) echo "Configuration of mpd 0.20.19:";;
esac
cat <<\_ACEOF
......@@ -2209,7 +2209,7 @@ fi
test -n "$ac_init_help" && exit $ac_status
if $ac_init_version; then
cat <<\_ACEOF
mpd configure 0.20.18
mpd configure 0.20.19
generated by GNU Autoconf 2.69
Copyright (C) 2012 Free Software Foundation, Inc.
......@@ -2616,7 +2616,7 @@ cat >config.log <<_ACEOF
This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.
It was created by mpd $as_me 0.20.18, which was
It was created by mpd $as_me 0.20.19, which was
generated by GNU Autoconf 2.69. Invocation command line was
$ $0 $@
......@@ -2967,7 +2967,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu
VERSION_MAJOR=0
VERSION_MINOR=20
VERSION_REVISION=18
VERSION_REVISION=19
VERSION_EXTRA=0
......@@ -3486,7 +3486,7 @@ fi
# Define the identity of the package.
PACKAGE='mpd'
VERSION='0.20.18'
VERSION='0.20.19'
cat >>confdefs.h <<_ACEOF
......@@ -9216,7 +9216,7 @@ fi
if test "x$want_boost" = "xyes"; then
boost_lib_version_req=1.46
boost_lib_version_req=1.54
boost_lib_version_req_shorten=`expr $boost_lib_version_req : '\([0-9]*\.[0-9]*\)'`
boost_lib_version_req_major=`expr $boost_lib_version_req : '\([0-9]*\)'`
boost_lib_version_req_minor=`expr $boost_lib_version_req : '[0-9]*\.\([0-9]*\)'`
......@@ -21779,7 +21779,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
# report actual input values of CONFIG_FILES etc. instead of their
# values after options handling.
ac_log="
This file was extended by mpd $as_me 0.20.18, which was
This file was extended by mpd $as_me 0.20.19, which was
generated by GNU Autoconf 2.69. Invocation command line was
CONFIG_FILES = $CONFIG_FILES
......@@ -21845,7 +21845,7 @@ _ACEOF
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
ac_cs_version="\\
mpd config.status 0.20.18
mpd config.status 0.20.19
configured by $0, generated by GNU Autoconf 2.69,
with options \\"\$ac_cs_config\\"
......
AC_PREREQ(2.60)
AC_INIT(mpd, 0.20.18, musicpd-dev-team@lists.sourceforge.net)
AC_INIT(mpd, 0.20.19, musicpd-dev-team@lists.sourceforge.net)
VERSION_MAJOR=0
VERSION_MINOR=20
VERSION_REVISION=18
VERSION_REVISION=19
VERSION_EXTRA=0
AC_CONFIG_SRCDIR([src/Main.cxx])
......@@ -454,7 +454,7 @@ dnl ---------------------------------------------------------------------------
dnl Mandatory Libraries
dnl ---------------------------------------------------------------------------
AX_BOOST_BASE([1.46],, [AC_MSG_ERROR([Boost not found])])
AX_BOOST_BASE([1.54],, [AC_MSG_ERROR([Boost not found])])
AC_ARG_ENABLE(icu,
AS_HELP_STRING([--enable-icu],
......
......@@ -38,7 +38,7 @@ PROJECT_NAME = MPD
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 0.20.18
PROJECT_NUMBER = 0.20.19
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
......
......@@ -79,7 +79,10 @@
<para>
If you need to tweak the configuration, you can create a file
called <filename>mpd.conf</filename> on the data partition.
called <filename>mpd.conf</filename> on the data partition
(the directory which is returned by Android's <ulink
url="https://developer.android.com/reference/android/os/Environment.html#getExternalStorageDirectory()">getExternalStorageDirectory()</ulink>
API function).
</para>
</section>
......@@ -111,7 +114,7 @@ cd mpd-version</programlisting>
<listitem>
<para>
<ulink url="http://www.boost.org/">Boost 1.46</ulink>
<ulink url="http://www.boost.org/">Boost 1.54</ulink>
</para>
</listitem>
......@@ -265,6 +268,59 @@ apt-get install g++ \
script.
</para>
</section>
<section id="android_build">
<title>Compiling for Android</title>
<para>
MPD can be compiled as an Android app. It can be installed
easily with <link linkend="install_android">Google
Play</link>, but if you want to build it from source, follow
this section.
</para>
<para>
You need:
</para>
<itemizedlist>
<listitem>
<para>
Android SDK
</para>
</listitem>
<listitem>
<para>
<ulink
url="https://developer.android.com/ndk/downloads/index.html">Android
NDK</ulink>
</para>
</listitem>
</itemizedlist>
<para>
Just like with the native build, unpack the
<application>MPD</application> source tarball and change
into the directory. Then, instead of
<command>./configure</command>, type:
</para>
<programlisting>./android/build.py SDK_PATH NDK_PATH ABI
make android/build/mpd-debug.apk</programlisting>
<para>
<varname>SDK_PATH</varname> is the absolute path where you
installed the Android SDK; <varname>NDK_PATH</varname> is
the Android NDK installation path; <varname>ABI</varname> is
the Android ABI to be built, e.g. "armeabi-v7a".
</para>
<para>
This downloads various library sources, and then configures
and builds <application>MPD</application>.
</para>
</section>
</section>
<section id="systemd_socket">
......
......@@ -17,12 +17,17 @@ libogg = AutotoolsProject(
)
libvorbis = AutotoolsProject(
'http://downloads.xiph.org/releases/vorbis/libvorbis-1.3.5.tar.xz',
'28cb28097c07a735d6af56e598e1c90f',
'http://downloads.xiph.org/releases/vorbis/libvorbis-1.3.6.tar.xz',
'af00bb5a784e7c9e69f56823de4637c350643deedaf333d0fa86ecdba6fcb415',
'lib/libvorbis.a',
[
'--disable-shared', '--enable-static',
],
edits={
# this option is not understood by clang
'configure': lambda data: data.replace('-mno-ieee-fp', ' '),
}
)
opus = AutotoolsProject(
......@@ -100,8 +105,8 @@ liblame = AutotoolsProject(
)
ffmpeg = FfmpegProject(
'http://ffmpeg.org/releases/ffmpeg-3.4.2.tar.xz',
'2b92e9578ef8b3e49eeab229e69305f5f4cbc1fdaa22e927fc7fca18acccd740',
'http://ffmpeg.org/releases/ffmpeg-4.0.tar.xz',
'ed945daf40b124e77a685893cc025d086f638bc703183460aff49508edb3a43f',
'lib/libavcodec.a',
[
'--disable-shared', '--enable-static',
......@@ -329,8 +334,8 @@ ffmpeg = FfmpegProject(
)
curl = AutotoolsProject(
'http://curl.haxx.se/download/curl-7.58.0.tar.xz',
'6a813875243609eb75f37fa72044e4ad618b55ec15a4eafdac2df6a7e800e3e3',
'http://curl.haxx.se/download/curl-7.59.0.tar.xz',
'e44eaabdf916407585bf5c7939ff1161e6242b6b015d3f2f5b758b2a330461fc',
'lib/libcurl.a',
[
'--disable-shared', '--enable-static',
......
......@@ -107,10 +107,6 @@
#include <locale.h>
#endif
#ifdef __BLOCKS__
#include <dispatch/dispatch.h>
#endif
#include <limits.h>
static constexpr size_t KILOBYTE = 1024;
......@@ -483,21 +479,8 @@ try {
daemonize_begin(options.daemon);
#endif
#ifdef __BLOCKS__
/* Runs the OS X native event loop in the main thread, and runs
the rest of mpd_main on a new thread. This lets CoreAudio receive
route change notifications (e.g. plugging or unplugging headphones).
All hardware output on OS X ultimately uses CoreAudio internally.
This must be run after forking; if dispatch is called before forking,
the child process will have a broken internal dispatch state. */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
exit(mpd_main_after_fork(config));
});
dispatch_main();
return EXIT_FAILURE; // unreachable, because dispatch_main never returns
#else
return mpd_main_after_fork(config);
#endif
} catch (const std::exception &e) {
LogError(e);
return EXIT_FAILURE;
......
......@@ -325,6 +325,34 @@ SendConstraints(mpd_connection *connection, const DatabaseSelection &selection)
return true;
}
static bool
SendGroupMask(mpd_connection *connection, tag_mask_t mask)
{
#if LIBMPDCLIENT_CHECK_VERSION(2,12,0)
for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) {
if ((mask & (tag_mask_t(1) << i)) == 0)
continue;
const auto tag = Convert(TagType(i));
if (tag == MPD_TAG_COUNT)
throw std::runtime_error("Unsupported tag");
if (!mpd_search_add_group_tag(connection, tag))
return false;
}
return true;
#else
(void)connection;
(void)mask;
if (mask != 0)
throw std::runtime_error("Grouping requires libmpdclient 2.12");
return true;
#endif
}
ProxyDatabase::ProxyDatabase(EventLoop &_loop, DatabaseListener &_listener,
const ConfigBlock &block)
:Database(proxy_db_plugin),
......@@ -682,7 +710,7 @@ static void
SearchSongs(struct mpd_connection *connection,
const DatabaseSelection &selection,
VisitSong visit_song)
{
try {
assert(selection.recursive);
assert(visit_song);
......@@ -709,6 +737,11 @@ SearchSongs(struct mpd_connection *connection,
if (!mpd_response_finish(connection))
ThrowError(connection);
} catch (...) {
if (connection != nullptr)
mpd_search_cancel(connection);
throw;
}
/**
......@@ -756,9 +789,9 @@ ProxyDatabase::Visit(const DatabaseSelection &selection,
void
ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
gcc_unused tag_mask_t group_mask,
tag_mask_t group_mask,
VisitTag visit_tag) const
{
try {
// TODO: eliminate the const_cast
const_cast<ProxyDatabase *>(this)->EnsureConnected();
......@@ -767,32 +800,47 @@ ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
throw std::runtime_error("Unsupported tag");
if (!mpd_search_db_tags(connection, tag_type2) ||
!SendConstraints(connection, selection))
!SendConstraints(connection, selection) ||
!SendGroupMask(connection, group_mask))
ThrowError(connection);
// TODO: use group_mask
if (!mpd_search_commit(connection))
ThrowError(connection);
while (auto *pair = mpd_recv_pair_tag(connection, tag_type2)) {
TagBuilder builder;
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
TagBuilder tag;
tag.AddItem(tag_type, pair->value);
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
if (tag.IsEmpty())
if (current_type == tag_type && !builder.IsEmpty()) {
try {
visit_tag(builder.Commit());
} catch (...) {
mpd_response_finish(connection);
throw;
}
}
builder.AddItem(current_type, pair->value);
if (!builder.HasType(current_type))
/* if no tag item has been added, then the
given value was not acceptable
(e.g. empty); forcefully insert an empty
tag in this case, as the caller expects the
given tag type to be present */
tag.AddEmptyItem(tag_type);
builder.AddEmptyItem(current_type);
}
if (!builder.IsEmpty()) {
try {
visit_tag(tag.Commit());
visit_tag(builder.Commit());
} catch (...) {
mpd_response_finish(connection);
throw;
......@@ -801,6 +849,11 @@ ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
if (!mpd_response_finish(connection))
ThrowError(connection);
} catch (...) {
if (connection != nullptr)
mpd_search_cancel(connection);
throw;
}
DatabaseStats
......
......@@ -308,9 +308,14 @@ struct DecoderControl {
bool IsCurrentSong(const DetachedSong &_song) const noexcept;
gcc_pure
bool LockIsCurrentSong(const DetachedSong &_song) const noexcept {
bool IsSeekableCurrentSong(const DetachedSong &_song) const noexcept {
return seekable && IsCurrentSong(_song);
}
gcc_pure
bool LockIsSeeakbleCurrentSong(const DetachedSong &_song) const noexcept {
const std::lock_guard<Mutex> protect(mutex);
return IsCurrentSong(_song);
return IsSeekableCurrentSong(_song);
}
private:
......
......@@ -26,8 +26,12 @@
#include <assert.h>
#include <string.h>
ThreadInputStream::~ThreadInputStream()
void
ThreadInputStream::Stop() noexcept
{
if (!thread.IsDefined())
return;
{
const std::lock_guard<Mutex> lock(mutex);
close = true;
......@@ -42,6 +46,7 @@ ThreadInputStream::~ThreadInputStream()
buffer->Clear();
HugeFree(buffer->Write().data, buffer_size);
delete buffer;
buffer = nullptr;
}
}
......@@ -68,7 +73,7 @@ ThreadInputStream::ThreadFunc()
Open();
} catch (...) {
postponed_exception = std::current_exception();
cond.broadcast();
SetReady();
return;
}
......
......@@ -27,6 +27,7 @@
#include <exception>
#include <assert.h>
#include <stdint.h>
template<typename T> class CircularBuffer;
......@@ -39,6 +40,11 @@ template<typename T> class CircularBuffer;
* manages the thread and the buffer.
*
* This works only for "streams": unknown length, no seeking, no tags.
*
* The implementation must call Stop() before its destruction
* completes. This cannot be done in ~ThreadInputStream() because at
* this point, the class has been morphed back to #ThreadInputStream
* and the still-running thread will crash due to pure method call.
*/
class ThreadInputStream : public InputStream {
const char *const plugin;
......@@ -76,7 +82,13 @@ public:
thread(BIND_THIS_METHOD(ThreadFunc)),
buffer_size(_buffer_size) {}
virtual ~ThreadInputStream();
#ifndef NDEBUG
~ThreadInputStream() override {
/* Stop() must have been called already */
assert(!thread.IsDefined());
assert(buffer == nullptr);
}
#endif
/**
* Initialize the object and start the thread.
......@@ -90,6 +102,12 @@ public:
size_t Read(void *ptr, size_t size) override final;
protected:
/**
* Stop the thread and free the buffer. This must be called
* before destruction of this object completes.
*/
void Stop() noexcept;
void SetMimeType(const char *_mime) {
assert(thread.IsInside());
......
......@@ -39,6 +39,10 @@ public:
MMS_BUFFER_SIZE) {
}
~MmsInputStream() noexcept override {
Stop();
}
protected:
virtual void Open() override;
virtual size_t ThreadRead(void *ptr, size_t size) override;
......
......@@ -33,6 +33,9 @@ FfmpegInit()
{
av_log_set_callback(FfmpegLogCallback);
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
/* deprecated as of FFmpeg 4.0 */
av_register_all();
#endif
}
......@@ -40,6 +40,9 @@ public:
~ScopeNetInit() noexcept {
WSACleanup();
}
#else
public:
ScopeNetInit() {}
#endif
};
......
......@@ -46,19 +46,20 @@ pcm_dsd_to_dop(PcmBuffer &buffer, unsigned channels,
assert(audio_valid_channel_count(channels));
assert(_src.size % channels == 0);
const unsigned num_src_samples = _src.size;
const unsigned num_src_frames = num_src_samples / channels;
const size_t num_src_samples = _src.size;
const size_t num_src_frames = num_src_samples / channels;
/* this rounds down and discards the last odd frame; not
/* this rounds down and discards up to 3 odd frames; not
elegant, but good enough for now */
const unsigned num_frames = num_src_frames / 2;
const unsigned num_samples = num_frames * channels;
const size_t num_dop_quads = num_src_frames / 4;
const size_t num_frames = num_dop_quads * 2;
const size_t num_samples = num_frames * channels;
uint32_t *const dest0 = (uint32_t *)buffer.GetT<uint32_t>(num_samples),
*dest = dest0;
auto src = _src.data;
for (unsigned i = num_frames / 2; i > 0; --i) {
for (size_t i = num_dop_quads; i > 0; --i) {
for (unsigned c = channels; c > 0; --c) {
/* each 24 bit sample has 16 DSD sample bits
plus the magic 0x05 marker */
......
......@@ -584,7 +584,7 @@ Player::SeekDecoder()
const SongTime start_time = pc.next_song->GetStartTime();
if (!dc.LockIsCurrentSong(*pc.next_song)) {
if (!dc.LockIsSeeakbleCurrentSong(*pc.next_song)) {
/* the decoder is already decoding the "next" song -
stop it and start the previous song again */
......
......@@ -164,6 +164,10 @@ SongTime
ParseCommandArgSongTime(const char *s)
{
auto value = ParseCommandArgFloat(s);
if (value < 0)
throw FormatProtocolError(ACK_ERROR_ARG,
"Negative value not allowed: %s", s);
return SongTime::FromS(value);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment