Commit ecc07e4e authored by Max Kellermann's avatar Max Kellermann

Merge tag 'v0.22.5'

release v0.22.5
parents cbc830fd f8be403c
...@@ -2,13 +2,23 @@ ver 0.23 (not yet released) ...@@ -2,13 +2,23 @@ ver 0.23 (not yet released)
* protocol * protocol
- new command "getvol" - new command "getvol"
ver 0.22.5 (not yet released) 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 * tags
- id: translate TPE3 to Conductor, not Performer - id: translate TPE3 to Conductor, not Performer
* archive * archive
- iso9660: another fix for unaligned reads - iso9660: another fix for unaligned reads
* output * output
- httpd: error handling on Windows improved - 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
......
...@@ -19,6 +19,7 @@ ...@@ -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">
......
...@@ -5,8 +5,8 @@ android_ndk = get_option('android_ndk') ...@@ -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)
......
...@@ -68,11 +68,11 @@ There are two active branches in the git repository: ...@@ -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
......
...@@ -689,6 +689,11 @@ Whenever possible, ids should be used. ...@@ -689,6 +689,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]`
......
...@@ -407,6 +407,9 @@ curl = AutotoolsProject( ...@@ -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',
......
...@@ -326,6 +326,11 @@ CommandResult ...@@ -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;
......
...@@ -37,6 +37,15 @@ public: ...@@ -37,6 +37,15 @@ 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, tag_buffer),
tag_buffer(std::move(src.tag_buffer)) {}
ExportedSong &operator=(ExportedSong &&) = delete;
}; };
#endif #endif
...@@ -182,6 +182,14 @@ class AudioOutputControl { ...@@ -182,6 +182,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.
*/ */
......
...@@ -53,7 +53,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format) ...@@ -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) ...@@ -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 ...@@ -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 ...@@ -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) ...@@ -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 ...@@ -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();
} }
......
...@@ -56,8 +56,6 @@ class PulseOutput final : AudioOutput { ...@@ -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: ...@@ -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) ...@@ -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 ...@@ -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 ...@@ -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) ...@@ -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();
...@@ -841,6 +826,25 @@ PulseOutput::Play(const void *chunk, size_t size) ...@@ -841,6 +826,25 @@ PulseOutput::Play(const void *chunk, size_t size)
} }
void 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
PulseOutput::Cancel() noexcept PulseOutput::Cancel() noexcept
{ {
assert(mainloop != nullptr); assert(mainloop != nullptr);
...@@ -876,7 +880,6 @@ PulseOutput::Pause() ...@@ -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 */
......
...@@ -24,6 +24,7 @@ ...@@ -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 { ...@@ -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();
......
...@@ -94,7 +94,7 @@ ParseCommandArgRange(const char *s) ...@@ -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) ...@@ -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;
} }
......
...@@ -25,8 +25,22 @@ ...@@ -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 { ...@@ -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
...@@ -88,6 +88,19 @@ struct LightSong { ...@@ -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)
......
...@@ -112,6 +112,6 @@ template <typename T> ...@@ -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
...@@ -59,6 +59,7 @@ case x: ...@@ -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();
......
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