Compare commits

..

34 Commits

Author SHA1 Message Date
Max Kellermann
938728820b release v0.22.6 2021-02-16 13:56:14 +01:00
Max Kellermann
80531ef8d8 db/simple: fix ExportedSong move constructor for non-owning sources
If the constructor moves from an ExportedSong instance which refers to
somebody else's "Tag" instance, the newly constructed instance will
instead refer to its own empty "tag_buffer" field.  This broke
SimpleDatabase::GetSong(), i.e. all songs on the queue restored from
the state file or added using the "addid" command.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1089
2021-02-16 13:52:25 +01:00
Max Kellermann
a91fba6a3d increment version number to 0.22.6 2021-02-16 13:47:33 +01:00
Max Kellermann
f8be403c34 release v0.22.5 2021-02-15 21:18:18 +01:00
Max Kellermann
28a5cdf319 android/meson.build: update the SDK platform to 29
Needed for `requestLegacyExternalStorage` (commit ca02fb7782).
2021-02-15 21:17:26 +01:00
Max Kellermann
6b1d264b35 command/queue: better error message for open-ended range with "move"
The "move" command doesn't allow open-ended ranges because they don't
make a lot of sense; moving an open-ended range is only possible if
the destination index is before the range, and in that case, the
client should be well aware how many songs there are.

Closes https://github.com/MusicPlayerDaemon/MPD/pull/1057
2021-02-15 20:57:22 +01:00
Max Kellermann
a6c10e9a1c protocol/ArgParser: check for invalid ranges
Catch errors like that early, before invalid ranges get passed to
internal MPD subsystems.
2021-02-15 20:55:30 +01:00
Max Kellermann
19a46064e9 protocol/RangeArg: add methods IsWellFormed(), IsEmpty(), HasAtLeast(), Count() 2021-02-15 20:54:51 +01:00
Max Kellermann
b57eeaa720 protocol/RangeArg: add static method Single() 2021-02-15 20:29:37 +01:00
Max Kellermann
ad059d5804 protocol/RangeArg: add method IsOpenEnded() 2021-02-15 20:29:35 +01:00
Max Kellermann
6e1940e930 protocol/RangeArg: add static method OpenEnded() 2021-02-15 20:29:34 +01:00
Max Kellermann
103194e32d protocol/RangeArg: add missing noexcept 2021-02-15 19:56:02 +01:00
Shen-Ta Hsieh
481c330c17 src/output: Set thread name for Wasapi output thread 2021-02-15 17:51:49 +01:00
Shen-Ta Hsieh
7ef489e057 src/win32: run clang-format 2021-02-15 17:50:51 +01:00
Shen-Ta Hsieh
d9e5d5ff5b src/win32: Add error message for NO_ERROR 2021-02-15 17:45:25 +01:00
Max Kellermann
ca02fb7782 android/AndroidManifest.xml: enable requestLegacyExternalStorage
This is a workaround for the new scoped storage design in Android 11:

 https://developer.android.com/about/versions/11/privacy/storage

This needs a proper solution eventually, but this quick fix will do
until we change "targetSdkVersion" to 30.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1061
2021-02-15 17:43:05 +01:00
Max Kellermann
d4d06da2f8 db/simple: fix dangling LightSong::tag reference in moved ExportedSong
After commit 1afa33c3c7, an old bug was revealed:
SimpleDatabase::GetSong() constructs an ExportedSong instance by
moving the return value of Song::Export(), which causes the
LightSong::tag field to be dangling on the moved-from
ExportedSong::tag_buffer.  This broke tags from CUE sheets.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1070
2021-02-15 17:38:37 +01:00
Max Kellermann
efde78db77 output/Thread: skip drain calls if there is no data to be played
Keep track of whether there is data being played, and don't call
AudioOutput::Drain() after Cancel() has been called already.
2021-02-15 16:39:13 +01:00
Max Kellermann
f1b8bcd6b2 output/pulse: don't drain if stream is suspended or corked
In this state, we can't make any progress.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1084
2021-02-15 16:07:16 +01:00
Max Kellermann
c2bc3704e1 output/pulse: move code to virtual method Drain()
Drain only if it was requested explicitly.
2021-02-15 15:59:54 +01:00
Max Kellermann
def120aca4 output/pulse: eliminate the pause field
It is useless, because we're always checking pa_stream_is_corked().
2021-02-15 15:59:46 +01:00
Max Kellermann
6d2b09ac2b doc/developer.rst: update branch names 2021-02-15 13:41:46 +01:00
Max Kellermann
78b43a9930 doc/protocol.rst: document add on local socket
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1022
2021-02-15 13:00:18 +01:00
Max Kellermann
da5ff779c6 python/build/libs.py: enable CURL/schannel support on Windows
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1031
2021-02-07 21:58:08 +01:00
Max Kellermann
e7da5b104d archive/iso9660: another fix for unaligned reads
Commit 79b2366387 added the field `skip`
to support unaligned reads, but set the `offset` field to a wrong
value.  This resulted in miscalculation of `remaining`, causing
an assertion failure.

The fix is to assign `offset` the correct value, but consider the
`skip` value in the assertion.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1067
2021-02-07 21:41:51 +01:00
Max Kellermann
4be76f3c8f archive/iso9660: check "skip==0" before doing optimized large read
After a Seek() to an odd offset, some data needs to be skipped from
the start of the block, and reading right into the given buffer
doesn't work.
2021-02-07 21:38:13 +01:00
Max Kellermann
c58c53293c test/run_input: add option --seek 2021-02-07 21:20:17 +01:00
Max Kellermann
8695a2806a test/run_input: document more options 2021-02-07 21:17:10 +01:00
vkostas
a59f1b21a6 Fix: Separate Conductor from Performer
Conductor was incorrectly saved to Performer tag in MPD database
2021-02-07 20:45:01 +01:00
Max Kellermann
9e2d09dabc net/SocketError: add syscall specific check functions
Fixes Windows compatibility.
2021-01-21 22:05:21 +01:00
Max Kellermann
2719f62feb net/SocketError: relicense to BSD-2 2021-01-21 21:31:02 +01:00
Max Kellermann
234cedd6c6 increment version number to 0.22.5 2021-01-21 17:43:25 +01:00
Max Kellermann
5b946e9d95 android/AndroidManifest.xml: android release 0.22.4 2021-01-21 17:36:00 +01:00
Max Kellermann
b46ca50dcc android/AndroidManifest.xml: raise targetSdkVersion to 29
The Google overlords require me to change to 29 or else I can't upload
new releases to Google Play.

 https://developer.android.com/distribute/best-practices/develop/target-sdk
2021-01-21 17:35:59 +01:00
27 changed files with 324 additions and 78 deletions

21
NEWS
View File

@@ -1,3 +1,24 @@
ver 0.22.6 (2021/02/16)
* fix missing tags on songs in queue
ver 0.22.5 (2021/02/15)
* protocol
- error for malformed ranges instead of ignoring silently
- better error message for open-ended range with "move"
* database
- simple: fix missing CUE sheet metadata in "addid" command
* tags
- id: translate TPE3 to Conductor, not Performer
* archive
- iso9660: another fix for unaligned reads
* output
- httpd: error handling on Windows improved
- pulse: fix deadlock with "always_on"
* Windows:
- enable https:// support (via Schannel)
* Android
- work around "Permission denied" on mpd.conf
ver 0.22.4 (2021/01/21) ver 0.22.4 (2021/01/21)
* protocol * protocol
- add command "binarylimit" to allow larger chunk sizes - add command "binarylimit" to allow larger chunk sizes

View File

@@ -2,10 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd" package="org.musicpd"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="51" android:versionCode="54"
android:versionName="0.22.1"> android:versionName="0.22.6">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28"/> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
<uses-feature android:name="android.software.leanback" <uses-feature android:name="android.software.leanback"
android:required="false" /> android:required="false" />
@@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application android:allowBackup="true" <application android:allowBackup="true"
android:requestLegacyExternalStorage="true"
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:banner="@drawable/icon" android:banner="@drawable/icon"
android:label="@string/app_name"> android:label="@string/app_name">

View File

@@ -5,8 +5,8 @@ android_ndk = get_option('android_ndk')
android_sdk = get_option('android_sdk') android_sdk = get_option('android_sdk')
android_abi = get_option('android_abi') android_abi = get_option('android_abi')
android_sdk_build_tools_version = '27.0.0' android_sdk_build_tools_version = '29.0.3'
android_sdk_platform = 'android-23' android_sdk_platform = 'android-29'
android_build_tools_dir = join_paths(android_sdk, 'build-tools', android_sdk_build_tools_version) android_build_tools_dir = join_paths(android_sdk, 'build-tools', android_sdk_build_tools_version)
android_sdk_platform_dir = join_paths(android_sdk, 'platforms', android_sdk_platform) android_sdk_platform_dir = join_paths(android_sdk, 'platforms', android_sdk_platform)

View File

@@ -38,7 +38,7 @@ author = 'Max Kellermann'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.22.4' version = '0.22.6'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version

View File

@@ -68,11 +68,11 @@ There are two active branches in the git repository:
- the "unstable" branch called ``master`` where new features are - the "unstable" branch called ``master`` where new features are
merged. This will become the next major release eventually. merged. This will become the next major release eventually.
- the "stable" branch (currently called ``v0.21.x``) where only bug - the "stable" branch (currently called ``v0.22.x``) where only bug
fixes are merged. fixes are merged.
Once :program:`MPD` 0.22 is released, a new branch called ``v0.22.x`` Once :program:`MPD` 0.23 is released, a new branch called ``v0.23.x``
will be created for 0.22 bug-fix releases; after that, ``v0.21.x`` will be created for 0.23 bug-fix releases; after that, ``v0.22.x``
will eventually cease to be maintained. will eventually cease to be maintained.
After bug fixes have been added to the "stable" branch, it will be After bug fixes have been added to the "stable" branch, it will be

View File

@@ -677,6 +677,11 @@ Whenever possible, ids should be used.
(directories add recursively). ``URI`` (directories add recursively). ``URI``
can also be a single file. can also be a single file.
Clients that are connected via local socket may add arbitrary
local files (URI is an absolute path). Exmaple::
add "/home/foo/Music/bar.ogg"
.. _command_addid: .. _command_addid:
:command:`addid {URI} [POSITION]` :command:`addid {URI} [POSITION]`

View File

@@ -1,7 +1,7 @@
project( project(
'mpd', 'mpd',
['c', 'cpp'], ['c', 'cpp'],
version: '0.22.4', version: '0.22.6',
meson_version: '>= 0.49.0', meson_version: '>= 0.49.0',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View File

@@ -407,6 +407,9 @@ curl = AutotoolsProject(
'--disable-progress-meter', '--disable-progress-meter',
'--disable-alt-svc', '--disable-alt-svc',
'--without-gnutls', '--without-nss', '--without-libssh2', '--without-gnutls', '--without-nss', '--without-libssh2',
# native Windows SSL/TLS support, option ignored on non-Windows builds
'--with-schannel',
], ],
patches='src/lib/curl/patches', patches='src/lib/curl/patches',

View File

@@ -221,8 +221,8 @@ public:
if (new_offset > size) if (new_offset > size)
throw std::runtime_error("Invalid seek offset"); throw std::runtime_error("Invalid seek offset");
offset = new_offset;
skip = new_offset % ISO_BLOCKSIZE; skip = new_offset % ISO_BLOCKSIZE;
offset = new_offset - skip;
buffer.Clear(); buffer.Clear();
} }
}; };
@@ -260,13 +260,13 @@ Iso9660InputStream::Read(std::unique_lock<Mutex> &,
if (r.empty()) { if (r.empty()) {
/* the buffer is empty - read more data from the ISO file */ /* the buffer is empty - read more data from the ISO file */
assert(offset % ISO_BLOCKSIZE == 0); assert((offset - skip) % ISO_BLOCKSIZE == 0);
const ScopeUnlock unlock(mutex); const ScopeUnlock unlock(mutex);
const lsn_t read_lsn = lsn + offset / ISO_BLOCKSIZE; const lsn_t read_lsn = lsn + offset / ISO_BLOCKSIZE;
if (read_size >= ISO_BLOCKSIZE) { if (read_size >= ISO_BLOCKSIZE && skip == 0) {
/* big read - read right into the caller's buffer */ /* big read - read right into the caller's buffer */
auto nbytes = iso->SeekRead(ptr, read_lsn, auto nbytes = iso->SeekRead(ptr, read_lsn,

View File

@@ -326,6 +326,11 @@ CommandResult
handle_move(Client &client, Request args, [[maybe_unused]] Response &r) handle_move(Client &client, Request args, [[maybe_unused]] Response &r)
{ {
RangeArg range = args.ParseRange(0); RangeArg range = args.ParseRange(0);
if (range.IsOpenEnded()) {
r.Error(ACK_ERROR_ARG, "Open-ended range not supported");
return CommandResult::ERROR;
}
int to = args.ParseInt(1); int to = args.ParseInt(1);
client.GetPartition().MoveRange(range.start, range.end, to); client.GetPartition().MoveRange(range.start, range.end, to);
return CommandResult::OK; return CommandResult::OK;

View File

@@ -29,6 +29,12 @@
* a #LightSong, e.g. a merged #Tag. * a #LightSong, e.g. a merged #Tag.
*/ */
class ExportedSong : public LightSong { class ExportedSong : public LightSong {
/**
* A reference target for LightSong::tag, but it is only used
* if this instance "owns" the #Tag. For instances referring
* to a foreign #Tag instance (e.g. a Song::tag), this field
* is not used (and empty).
*/
Tag tag_buffer; Tag tag_buffer;
public: public:
@@ -37,6 +43,25 @@ public:
ExportedSong(const char *_uri, Tag &&_tag) noexcept ExportedSong(const char *_uri, Tag &&_tag) noexcept
:LightSong(_uri, tag_buffer), :LightSong(_uri, tag_buffer),
tag_buffer(std::move(_tag)) {} tag_buffer(std::move(_tag)) {}
/* this custom move constructor is necessary so LightSong::tag
points to this instance's #Tag field instead of leaving a
dangling reference to the source object's #Tag field */
ExportedSong(ExportedSong &&src) noexcept
:LightSong(src,
/* refer to tag_buffer only if the
moved-from instance also owned the Tag
which its LightSong::tag field refers
to */
OwnsTag() ? tag_buffer : src.tag),
tag_buffer(std::move(src.tag_buffer)) {}
ExportedSong &operator=(ExportedSong &&) = delete;
private:
bool OwnsTag() const noexcept {
return &tag == &tag_buffer;
}
}; };
#endif #endif

View File

@@ -36,7 +36,7 @@ BufferedSocket::DirectRead(void *data, size_t length) noexcept
} }
const auto code = GetSocketError(); const auto code = GetSocketError();
if (IsSocketErrorAgain(code)) if (IsSocketErrorReceiveWouldBlock(code))
return 0; return 0;
if (IsSocketErrorClosed(code)) if (IsSocketErrorClosed(code))

View File

@@ -31,7 +31,7 @@ FullyBufferedSocket::DirectWrite(const void *data, size_t length) noexcept
const auto nbytes = GetSocket().Write((const char *)data, length); const auto nbytes = GetSocket().Write((const char *)data, length);
if (gcc_unlikely(nbytes < 0)) { if (gcc_unlikely(nbytes < 0)) {
const auto code = GetSocketError(); const auto code = GetSocketError();
if (IsSocketErrorAgain(code)) if (IsSocketErrorSendWouldBlock(code))
return 0; return 0;
IdleMonitor::Cancel(); IdleMonitor::Cancel();

View File

@@ -1,20 +1,30 @@
/* /*
* Copyright 2003-2021 The Music Player Daemon Project * Copyright 2015-2021 Max Kellermann <max.kellermann@gmail.com>
* http://www.musicpd.org
* *
* This program is free software; you can redistribute it and/or modify * Redistribution and use in source and binary forms, with or without
* it under the terms of the GNU General Public License as published by * modification, are permitted provided that the following conditions
* the Free Software Foundation; either version 2 of the License, or * are met:
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * - Redistributions of source code must retain the above copyright
* but WITHOUT ANY WARRANTY; without even the implied warranty of * notice, this list of conditions and the following disclaimer.
* 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 * - Redistributions in binary form must reproduce the above copyright
* with this program; if not, write to the Free Software Foundation, Inc., * notice, this list of conditions and the following disclaimer in the
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * 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 "SocketError.hxx" #include "SocketError.hxx"

View File

@@ -1,24 +1,34 @@
/* /*
* Copyright 2003-2021 The Music Player Daemon Project * Copyright 2015-2021 Max Kellermann <max.kellermann@gmail.com>
* http://www.musicpd.org
* *
* This program is free software; you can redistribute it and/or modify * Redistribution and use in source and binary forms, with or without
* it under the terms of the GNU General Public License as published by * modification, are permitted provided that the following conditions
* the Free Software Foundation; either version 2 of the License, or * are met:
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * - Redistributions of source code must retain the above copyright
* but WITHOUT ANY WARRANTY; without even the implied warranty of * notice, this list of conditions and the following disclaimer.
* 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 * - Redistributions in binary form must reproduce the above copyright
* with this program; if not, write to the Free Software Foundation, Inc., * notice, this list of conditions and the following disclaimer in the
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * 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 MPD_SOCKET_ERROR_HXX #ifndef SOCKET_ERROR_HXX
#define MPD_SOCKET_ERROR_HXX #define SOCKET_ERROR_HXX
#include "util/Compiler.h" #include "util/Compiler.h"
#include "system/Error.hxx" #include "system/Error.hxx"
@@ -42,14 +52,79 @@ GetSocketError() noexcept
#endif #endif
} }
gcc_const constexpr bool
static inline bool IsSocketErrorInProgress(socket_error_t code) noexcept
IsSocketErrorAgain(socket_error_t code) noexcept
{ {
#ifdef _WIN32 #ifdef _WIN32
return code == WSAEINPROGRESS; return code == WSAEINPROGRESS;
#else #else
return code == EAGAIN; return code == EINPROGRESS;
#endif
}
constexpr bool
IsSocketErrorWouldBlock(socket_error_t code) noexcept
{
#ifdef _WIN32
return code == WSAEWOULDBLOCK;
#else
return code == EWOULDBLOCK;
#endif
}
constexpr bool
IsSocketErrorConnectWouldBlock(socket_error_t code) noexcept
{
#if defined(_WIN32) || defined(__linux__)
/* on Windows, WSAEINPROGRESS is for blocking sockets and
WSAEWOULDBLOCK for non-blocking sockets */
/* on Linux, EAGAIN==EWOULDBLOCK is for local sockets and
EINPROGRESS is for all other sockets */
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
#else
/* on all other operating systems, there's just EINPROGRESS */
return IsSocketErrorInProgress(code);
#endif
}
constexpr bool
IsSocketErrorSendWouldBlock(socket_error_t code) noexcept
{
#ifdef _WIN32
/* on Windows, WSAEINPROGRESS is for blocking sockets and
WSAEWOULDBLOCK for non-blocking sockets */
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
#else
/* on all other operating systems, there's just EAGAIN==EWOULDBLOCK */
return IsSocketErrorWouldBlock(code);
#endif
}
constexpr bool
IsSocketErrorReceiveWouldBlock(socket_error_t code) noexcept
{
#ifdef _WIN32
/* on Windows, WSAEINPROGRESS is for blocking sockets and
WSAEWOULDBLOCK for non-blocking sockets */
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
#else
/* on all other operating systems, there's just
EAGAIN==EWOULDBLOCK */
return IsSocketErrorWouldBlock(code);
#endif
}
constexpr bool
IsSocketErrorAcceptWouldBlock(socket_error_t code) noexcept
{
#ifdef _WIN32
/* on Windows, WSAEINPROGRESS is for blocking sockets and
WSAEWOULDBLOCK for non-blocking sockets */
return IsSocketErrorInProgress(code) || IsSocketErrorWouldBlock(code);
#else
/* on all other operating systems, there's just
EAGAIN==EWOULDBLOCK */
return IsSocketErrorWouldBlock(code);
#endif #endif
} }

View File

@@ -181,6 +181,14 @@ class AudioOutputControl {
*/ */
bool open = false; bool open = false;
/**
* Is the device currently playing, i.e. is its buffer
* (likely) non-empty? If not, then it will never be drained.
*
* This field is only valid while the output is open.
*/
bool playing;
/** /**
* Is the device paused? i.e. the output thread is in the * Is the device paused? i.e. the output thread is in the
* ao_pause() loop. * ao_pause() loop.

View File

@@ -53,7 +53,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
if (open && cf != output->filter_audio_format) if (open && cf != output->filter_audio_format)
/* if the filter's output format changes, the output /* if the filter's output format changes, the output
must be reopened as well */ must be reopened as well */
InternalCloseOutput(true); InternalCloseOutput(playing);
output->filter_audio_format = cf; output->filter_audio_format = cf;
@@ -64,6 +64,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
} }
open = true; open = true;
playing = false;
} else if (in_audio_format != output->out_audio_format) { } else if (in_audio_format != output->out_audio_format) {
/* reconfigure the final ConvertFilter for its new /* reconfigure the final ConvertFilter for its new
input AudioFormat */ input AudioFormat */
@@ -285,6 +286,9 @@ AudioOutputControl::PlayChunk(std::unique_lock<Mutex> &lock) noexcept
assert(nbytes % output->out_audio_format.GetFrameSize() == 0); assert(nbytes % output->out_audio_format.GetFrameSize() == 0);
source.ConsumeData(nbytes); source.ConsumeData(nbytes);
/* there's data to be drained from now on */
playing = true;
} }
return true; return true;
@@ -371,6 +375,9 @@ AudioOutputControl::InternalPause(std::unique_lock<Mutex> &lock) noexcept
} }
skip_delay = true; skip_delay = true;
/* ignore drain commands until we got something new to play */
playing = false;
} }
static void static void
@@ -390,6 +397,10 @@ PlayFull(FilteredAudioOutput &output, ConstBuffer<void> _buffer)
inline void inline void
AudioOutputControl::InternalDrain() noexcept AudioOutputControl::InternalDrain() noexcept
{ {
/* after this method finishes, there's nothing left to be
drained */
playing = false;
try { try {
/* flush the filter and play its remaining output */ /* flush the filter and play its remaining output */
@@ -518,6 +529,7 @@ AudioOutputControl::Task() noexcept
source.Cancel(); source.Cancel();
if (open) { if (open) {
playing = false;
const ScopeUnlock unlock(mutex); const ScopeUnlock unlock(mutex);
output->Cancel(); output->Cancel();
} }

View File

@@ -56,8 +56,6 @@ class PulseOutput final : AudioOutput {
size_t writable; size_t writable;
bool pause;
/** /**
* Was Interrupt() called? This will unblock Play(). It will * Was Interrupt() called? This will unblock Play(). It will
* be reset by Cancel() and Pause(), as documented by the * be reset by Cancel() and Pause(), as documented by the
@@ -113,6 +111,7 @@ public:
[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override; [[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
size_t Play(const void *chunk, size_t size) override; size_t Play(const void *chunk, size_t size) override;
void Drain() override;
void Cancel() noexcept override; void Cancel() noexcept override;
bool Pause() override; bool Pause() override;
@@ -688,7 +687,6 @@ PulseOutput::Open(AudioFormat &audio_format)
"pa_stream_connect_playback() has failed"); "pa_stream_connect_playback() has failed");
} }
pause = false;
interrupted = false; interrupted = false;
} }
@@ -699,17 +697,6 @@ PulseOutput::Close() noexcept
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
if (pa_stream_get_state(stream) == PA_STREAM_READY) {
pa_operation *o =
pa_stream_drain(stream,
pulse_output_stream_success_cb, this);
if (o == nullptr) {
LogPulseError(context,
"pa_stream_drain() has failed");
} else
pulse_wait_for_operation(mainloop, o);
}
DeleteStream(); DeleteStream();
if (context != nullptr && if (context != nullptr &&
@@ -780,7 +767,7 @@ PulseOutput::Delay() const noexcept
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
auto result = std::chrono::steady_clock::duration::zero(); auto result = std::chrono::steady_clock::duration::zero();
if (pause && pa_stream_is_corked(stream) && if (pa_stream_is_corked(stream) &&
pa_stream_get_state(stream) == PA_STREAM_READY) pa_stream_get_state(stream) == PA_STREAM_READY)
/* idle while paused */ /* idle while paused */
result = std::chrono::seconds(1); result = std::chrono::seconds(1);
@@ -796,8 +783,6 @@ PulseOutput::Play(const void *chunk, size_t size)
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
pause = false;
/* check if the stream is (already) connected */ /* check if the stream is (already) connected */
WaitStream(); WaitStream();
@@ -840,6 +825,25 @@ PulseOutput::Play(const void *chunk, size_t size)
return size; return size;
} }
void
PulseOutput::Drain()
{
Pulse::LockGuard lock(mainloop);
if (pa_stream_get_state(stream) != PA_STREAM_READY ||
pa_stream_is_suspended(stream) ||
pa_stream_is_corked(stream))
return;
pa_operation *o =
pa_stream_drain(stream,
pulse_output_stream_success_cb, this);
if (o == nullptr)
throw MakePulseError(context, "pa_stream_drain() failed");
pulse_wait_for_operation(mainloop, o);
}
void void
PulseOutput::Cancel() noexcept PulseOutput::Cancel() noexcept
{ {
@@ -876,7 +880,6 @@ PulseOutput::Pause()
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
pause = true;
interrupted = false; interrupted = false;
/* check if the stream is (already/still) connected */ /* check if the stream is (already/still) connected */

View File

@@ -24,6 +24,7 @@
#include "mixer/MixerList.hxx" #include "mixer/MixerList.hxx"
#include "thread/Cond.hxx" #include "thread/Cond.hxx"
#include "thread/Mutex.hxx" #include "thread/Mutex.hxx"
#include "thread/Name.hxx"
#include "thread/Thread.hxx" #include "thread/Thread.hxx"
#include "util/AllocatedString.hxx" #include "util/AllocatedString.hxx"
#include "util/Domain.hxx" #include "util/Domain.hxx"
@@ -231,6 +232,7 @@ IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept {
} }
void WasapiOutputThread::Work() noexcept { void WasapiOutputThread::Work() noexcept {
SetThreadName("Wasapi Output Worker");
FormatDebug(wasapi_output_domain, "Working thread started"); FormatDebug(wasapi_output_domain, "Working thread started");
try { try {
com.emplace(); com.emplace();

View File

@@ -278,7 +278,7 @@ HttpdClient::TryWrite() noexcept
metadata_current_position); metadata_current_position);
if (nbytes < 0) { if (nbytes < 0) {
auto e = GetSocketError(); auto e = GetSocketError();
if (IsSocketErrorAgain(e)) if (IsSocketErrorSendWouldBlock(e))
return true; return true;
if (!IsSocketErrorClosed(e)) { if (!IsSocketErrorClosed(e)) {
@@ -305,7 +305,7 @@ HttpdClient::TryWrite() noexcept
ssize_t nbytes = GetSocket().Write(&empty_data, 1); ssize_t nbytes = GetSocket().Write(&empty_data, 1);
if (nbytes < 0) { if (nbytes < 0) {
auto e = GetSocketError(); auto e = GetSocketError();
if (IsSocketErrorAgain(e)) if (IsSocketErrorSendWouldBlock(e))
return true; return true;
if (!IsSocketErrorClosed(e)) { if (!IsSocketErrorClosed(e)) {
@@ -328,7 +328,7 @@ HttpdClient::TryWrite() noexcept
bytes_to_write); bytes_to_write);
if (nbytes < 0) { if (nbytes < 0) {
auto e = GetSocketError(); auto e = GetSocketError();
if (IsSocketErrorAgain(e)) if (IsSocketErrorSendWouldBlock(e))
return true; return true;
if (!IsSocketErrorClosed(e)) { if (!IsSocketErrorClosed(e)) {

View File

@@ -94,7 +94,7 @@ ParseCommandArgRange(const char *s)
s); s);
if (test == test2) if (test == test2)
value = std::numeric_limits<int>::max(); return RangeArg::OpenEnded(range.start);
if (value < 0) if (value < 0)
throw FormatProtocolError(ACK_ERROR_ARG, throw FormatProtocolError(ACK_ERROR_ARG,
@@ -107,9 +107,13 @@ ParseCommandArgRange(const char *s)
range.end = (unsigned)value; range.end = (unsigned)value;
} else { } else {
range.end = (unsigned)value + 1; return RangeArg::Single(range.start);
} }
if (!range.IsWellFormed())
throw FormatProtocolError(ACK_ERROR_ARG,
"Malformed range: %s", s);
return range; return range;
} }

View File

@@ -25,8 +25,22 @@
struct RangeArg { struct RangeArg {
unsigned start, end; unsigned start, end;
static constexpr RangeArg All() { /**
return { 0, std::numeric_limits<unsigned>::max() }; * Construct an open-ended range starting at the given index.
*/
static constexpr RangeArg OpenEnded(unsigned start) noexcept {
return { start, std::numeric_limits<unsigned>::max() };
}
static constexpr RangeArg All() noexcept {
return OpenEnded(0);
}
/**
* Construct an instance describing exactly one index.
*/
static constexpr RangeArg Single(unsigned i) noexcept {
return { i, i + 1 };
} }
constexpr bool operator==(RangeArg other) const noexcept { constexpr bool operator==(RangeArg other) const noexcept {
@@ -37,13 +51,45 @@ struct RangeArg {
return !(*this == other); return !(*this == other);
} }
constexpr bool IsOpenEnded() const noexcept {
return end == All().end;
}
constexpr bool IsAll() const noexcept { constexpr bool IsAll() const noexcept {
return *this == All(); return *this == All();
} }
constexpr bool IsWellFormed() const noexcept {
return start <= end;
}
/**
* Is this range empty? A malformed range also counts as
* "empty" for this method.
*/
constexpr bool IsEmpty() const noexcept {
return start >= end;
}
/**
* Check if the range contains at least this number of items.
* Unlike Count(), this allows the object to be malformed.
*/
constexpr bool HasAtLeast(unsigned n) const noexcept {
return start + n <= end;
}
constexpr bool Contains(unsigned i) const noexcept { constexpr bool Contains(unsigned i) const noexcept {
return i >= start && i < end; return i >= start && i < end;
} }
/**
* Count the number of items covered by this range. This requires the
* object to be well-formed.
*/
constexpr unsigned Count() const noexcept {
return end - start;
}
}; };
#endif #endif

View File

@@ -88,6 +88,19 @@ struct LightSong {
LightSong(const char *_uri, const Tag &_tag) noexcept LightSong(const char *_uri, const Tag &_tag) noexcept
:uri(_uri), tag(_tag) {} :uri(_uri), tag(_tag) {}
/**
* A copy constructor which copies all fields, but only sets
* the tag to a caller-provided reference. This is used by
* the #ExportedSong move constructor.
*/
LightSong(const LightSong &src, const Tag &_tag) noexcept
:directory(src.directory), uri(src.uri),
real_uri(src.real_uri),
tag(_tag),
mtime(src.mtime),
start_time(src.start_time), end_time(src.end_time),
audio_format(src.audio_format) {}
gcc_pure gcc_pure
std::string GetURI() const noexcept { std::string GetURI() const noexcept {
if (directory == nullptr) if (directory == nullptr)

View File

@@ -352,7 +352,7 @@ scan_id3_tag(const struct id3_tag *tag, TagHandler &handler) noexcept
handler); handler);
tag_id3_import_text(tag, ID3_FRAME_COMPOSER, TAG_COMPOSER, tag_id3_import_text(tag, ID3_FRAME_COMPOSER, TAG_COMPOSER,
handler); handler);
tag_id3_import_text(tag, "TPE3", TAG_PERFORMER, tag_id3_import_text(tag, "TPE3", TAG_CONDUCTOR,
handler); handler);
tag_id3_import_text(tag, "TPE4", TAG_PERFORMER, handler); tag_id3_import_text(tag, "TPE4", TAG_PERFORMER, handler);
tag_id3_import_text(tag, "TIT1", TAG_GROUPING, handler); tag_id3_import_text(tag, "TIT1", TAG_GROUPING, handler);

View File

@@ -112,6 +112,6 @@ template <typename T>
void swap(ComPtr<T> &lhs, ComPtr<T> &rhs) noexcept { void swap(ComPtr<T> &lhs, ComPtr<T> &rhs) noexcept {
lhs.swap(rhs); lhs.swap(rhs);
} }
} } // namespace std
#endif #endif

View File

@@ -59,6 +59,7 @@ case x:
C(E_INVALIDARG); C(E_INVALIDARG);
C(E_OUTOFMEMORY); C(E_OUTOFMEMORY);
C(E_POINTER); C(E_POINTER);
C(NO_ERROR);
#undef C #undef C
} }
return std::string_view(); return std::string_view();

View File

@@ -56,6 +56,8 @@ struct CommandLine {
FromNarrowPath config_path; FromNarrowPath config_path;
std::size_t seek = 0;
std::size_t chunk_size = MAX_CHUNK_SIZE; std::size_t chunk_size = MAX_CHUNK_SIZE;
bool verbose = false; bool verbose = false;
@@ -67,6 +69,7 @@ enum Option {
OPTION_CONFIG, OPTION_CONFIG,
OPTION_VERBOSE, OPTION_VERBOSE,
OPTION_SCAN, OPTION_SCAN,
OPTION_SEEK,
OPTION_CHUNK_SIZE, OPTION_CHUNK_SIZE,
}; };
@@ -74,6 +77,7 @@ static constexpr OptionDef option_defs[] = {
{"config", 0, true, "Load a MPD configuration file"}, {"config", 0, true, "Load a MPD configuration file"},
{"verbose", 'v', false, "Verbose logging"}, {"verbose", 'v', false, "Verbose logging"},
{"scan", 0, false, "Scan tags instead of reading raw data"}, {"scan", 0, false, "Scan tags instead of reading raw data"},
{"seek", 0, true, "Start reading at this position"},
{"chunk-size", 0, true, "Read this number of bytes at a time"}, {"chunk-size", 0, true, "Read this number of bytes at a time"},
}; };
@@ -108,6 +112,10 @@ ParseCommandLine(int argc, char **argv)
c.scan = true; c.scan = true;
break; break;
case OPTION_SEEK:
c.seek = ParseSize(o.value);
break;
case OPTION_CHUNK_SIZE: case OPTION_CHUNK_SIZE:
c.chunk_size = ParseSize(o.value); c.chunk_size = ParseSize(o.value);
if (c.chunk_size <= 0 || c.chunk_size > MAX_CHUNK_SIZE) if (c.chunk_size <= 0 || c.chunk_size > MAX_CHUNK_SIZE)
@@ -118,7 +126,7 @@ ParseCommandLine(int argc, char **argv)
auto args = option_parser.GetRemaining(); auto args = option_parser.GetRemaining();
if (args.size != 1) if (args.size != 1)
throw std::runtime_error("Usage: run_input [--verbose] [--config=FILE] URI"); throw std::runtime_error("Usage: run_input [--verbose] [--config=FILE] [--scan] [--chunk-size=BYTES] URI");
c.uri = args.front(); c.uri = args.front();
return c; return c;
@@ -153,10 +161,14 @@ tag_save(FILE *file, const Tag &tag)
} }
static int static int
dump_input_stream(InputStream &is, FileDescriptor out, size_t chunk_size) dump_input_stream(InputStream &is, FileDescriptor out,
offset_type seek, size_t chunk_size)
{ {
std::unique_lock<Mutex> lock(is.mutex); std::unique_lock<Mutex> lock(is.mutex);
if (seek > 0)
is.Seek(lock, seek);
/* print meta data */ /* print meta data */
if (is.HasMimeType()) if (is.HasMimeType())
@@ -256,7 +268,7 @@ try {
Mutex mutex; Mutex mutex;
auto is = InputStream::OpenReady(c.uri, mutex); auto is = InputStream::OpenReady(c.uri, mutex);
return dump_input_stream(*is, FileDescriptor(STDOUT_FILENO), return dump_input_stream(*is, FileDescriptor(STDOUT_FILENO),
c.chunk_size); c.seek, c.chunk_size);
} catch (...) { } catch (...) {
PrintException(std::current_exception()); PrintException(std::current_exception());
return EXIT_FAILURE; return EXIT_FAILURE;