diff --git a/NEWS b/NEWS index 14e2c93b2..2de064501 100644 --- a/NEWS +++ b/NEWS @@ -36,6 +36,24 @@ ver 0.22 (not yet released) * switch to C++17 - GCC 7 or clang 4 (or newer) recommended +ver 0.21.23 (2020/04/23) +* protocol + - add tag fallback for AlbumSort +* storage + - curl: fix corrupt "href" values in the presence of XML entities + - curl: unescape "href" values +* input + - nfs: fix crash bug + - nfs: fix freeze bug on reconnect +* decoder + - gme: adapt to API change in the upcoming version 0.7.0 +* output + - alsa: implement channel mapping for 5.0 and 7.0 +* player + - drain outputs at end of song in "single" mode +* Windows + - fix case insensitive search + ver 0.21.22 (2020/04/02) * database - simple: optimize startup diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 4dcdba4fd..64d0802b1 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="46" + android:versionName="0.21.23"> diff --git a/src/decoder/plugins/GmeDecoderPlugin.cxx b/src/decoder/plugins/GmeDecoderPlugin.cxx index 4fb6af43e..ee8fc2a48 100644 --- a/src/decoder/plugins/GmeDecoderPlugin.cxx +++ b/src/decoder/plugins/GmeDecoderPlugin.cxx @@ -187,7 +187,11 @@ gme_file_decode(DecoderClient &client, Path path_fs) LogWarning(gme_domain, gme_err); if (length > 0) - gme_set_fade(emu, length); + gme_set_fade(emu, length +#if GME_VERSION >= 0x000700 + , 8000 +#endif + ); /* play */ DecoderCommand cmd; diff --git a/src/event/PollGroupWinSelect.cxx b/src/event/PollGroupWinSelect.cxx index b415298dc..a03c60cb7 100644 --- a/src/event/PollGroupWinSelect.cxx +++ b/src/event/PollGroupWinSelect.cxx @@ -23,8 +23,8 @@ #include "PollGroupWinSelect.hxx" -constexpr int EVENT_READ = 0; -constexpr int EVENT_WRITE = 1; +static constexpr int EVENT_READ = 0; +static constexpr int EVENT_WRITE = 1; static constexpr bool HasEvent(unsigned events, int event_id) noexcept diff --git a/src/event/SocketMonitor.cxx b/src/event/SocketMonitor.cxx index 28bef0afd..018f7330e 100644 --- a/src/event/SocketMonitor.cxx +++ b/src/event/SocketMonitor.cxx @@ -23,6 +23,10 @@ #include #include +#ifdef USE_EPOLL +#include +#endif + void SocketMonitor::Dispatch(unsigned flags) noexcept { @@ -81,6 +85,21 @@ SocketMonitor::Schedule(unsigned flags) noexcept if (success) scheduled_flags = flags; +#ifdef USE_EPOLL + else if (errno == EBADF || errno == ENOENT) + /* the socket was probably closed by somebody else + (EBADF) or a new file descriptor with the same + number was created but not registered already + (ENOENT) - we can assume that there are no + scheduled events */ + /* note that when this happens, we're actually lucky + that it has failed - imagine another thread may + meanwhile have created something on the same file + descriptor number, and has registered it; the + epoll_ctl() call above would then have succeeded, + but broke the other thread's epoll registration */ + scheduled_flags = 0; +#endif return success; } diff --git a/src/event/SocketMonitor.hxx b/src/event/SocketMonitor.hxx index 321b36b72..bb1e84354 100644 --- a/src/event/SocketMonitor.hxx +++ b/src/event/SocketMonitor.hxx @@ -108,7 +108,7 @@ public: } bool ScheduleRead() noexcept { - return Schedule(GetScheduledFlags() | READ | HANGUP | ERROR); + return Schedule(GetScheduledFlags() | READ); } bool ScheduleWrite() noexcept { @@ -116,7 +116,7 @@ public: } void CancelRead() noexcept { - Schedule(GetScheduledFlags() & ~(READ|HANGUP|ERROR)); + Schedule(GetScheduledFlags() & ~READ); } void CancelWrite() noexcept { diff --git a/src/fs/NarrowPath.hxx b/src/fs/NarrowPath.hxx index c0e15ea23..eab404111 100644 --- a/src/fs/NarrowPath.hxx +++ b/src/fs/NarrowPath.hxx @@ -90,6 +90,11 @@ public: constexpr #endif operator Path() const noexcept { +#ifdef _UNICODE + if (value.IsNull()) + return nullptr; +#endif + return value; } }; diff --git a/src/lib/icu/CaseFold.cxx b/src/lib/icu/CaseFold.cxx index 74a0462a2..a2640263b 100644 --- a/src/lib/icu/CaseFold.cxx +++ b/src/lib/icu/CaseFold.cxx @@ -34,12 +34,6 @@ #include #endif -#ifdef _WIN32 -#include "Win32.hxx" -#include -#endif - -#include #include #include @@ -65,27 +59,6 @@ try { folded.SetSize(folded_length); return UCharToUTF8({folded.begin(), folded.size()}); -#elif defined(_WIN32) - const auto u = MultiByteToWideChar(CP_UTF8, src); - - const int size = LCMapStringEx(LOCALE_NAME_INVARIANT, - LCMAP_SORTKEY|LINGUISTIC_IGNORECASE, - u.c_str(), -1, nullptr, 0, - nullptr, nullptr, 0); - if (size <= 0) - return AllocatedString<>::Duplicate(src); - - std::unique_ptr buffer(new wchar_t[size]); - int result = LCMapStringEx(LOCALE_NAME_INVARIANT, - LCMAP_SORTKEY|LINGUISTIC_IGNORECASE, - u.c_str(), -1, buffer.get(), size, - nullptr, nullptr, 0); - if (result <= 0) - return AllocatedString<>::Duplicate(src); - - return WideCharToMultiByte(CP_UTF8, - {buffer.get(), size_t(result - 1)}); - #else #error not implemented #endif diff --git a/src/lib/icu/CaseFold.hxx b/src/lib/icu/CaseFold.hxx index ba91262d8..99cb9ae0b 100644 --- a/src/lib/icu/CaseFold.hxx +++ b/src/lib/icu/CaseFold.hxx @@ -22,7 +22,7 @@ #include "config.h" -#if defined(HAVE_ICU) || defined(_WIN32) +#ifdef HAVE_ICU #define HAVE_ICU_CASE_FOLD #include diff --git a/src/lib/icu/Collate.cxx b/src/lib/icu/Collate.cxx index a91e4e122..72ba06336 100644 --- a/src/lib/icu/Collate.cxx +++ b/src/lib/icu/Collate.cxx @@ -108,7 +108,7 @@ IcuCollate(std::string_view a, std::string_view b) noexcept } auto result = CompareStringEx(LOCALE_NAME_INVARIANT, - LINGUISTIC_IGNORECASE, + NORM_IGNORECASE, wa.c_str(), -1, wb.c_str(), -1, nullptr, nullptr, 0); diff --git a/src/lib/icu/Compare.cxx b/src/lib/icu/Compare.cxx index 1fde0a637..f802f2d0a 100644 --- a/src/lib/icu/Compare.cxx +++ b/src/lib/icu/Compare.cxx @@ -22,11 +22,27 @@ #include "util/StringAPI.hxx" #include "config.h" +#ifdef _WIN32 +#include "Win32.hxx" +#include +#endif + #ifdef HAVE_ICU_CASE_FOLD IcuCompare::IcuCompare(std::string_view _needle) noexcept :needle(IcuCaseFold(_needle)) {} +#elif defined(_WIN32) + +IcuCompare::IcuCompare(std::string_view _needle) noexcept + :needle(nullptr) +{ + try { + needle = MultiByteToWideChar(CP_UTF8, _needle); + } catch (...) { + } +} + #else IcuCompare::IcuCompare(std::string_view _needle) noexcept @@ -39,6 +55,22 @@ IcuCompare::operator==(const char *haystack) const noexcept { #ifdef HAVE_ICU_CASE_FOLD return StringIsEqual(IcuCaseFold(haystack).c_str(), needle.c_str()); +#elif defined(_WIN32) + if (needle.IsNull()) + /* the MultiByteToWideChar() call in the constructor + has failed, so let's always fail the comparison */ + return false; + + try { + auto w_haystack = MultiByteToWideChar(CP_UTF8, haystack); + return CompareStringEx(LOCALE_NAME_INVARIANT, + NORM_IGNORECASE, + w_haystack.c_str(), -1, + needle.c_str(), -1, + nullptr, nullptr, 0) == CSTR_EQUAL; + } catch (...) { + return false; + } #else return StringIsEqualIgnoreCase(haystack, needle.c_str()); #endif @@ -50,6 +82,24 @@ IcuCompare::IsIn(const char *haystack) const noexcept #ifdef HAVE_ICU_CASE_FOLD return StringFind(IcuCaseFold(haystack).c_str(), needle.c_str()) != nullptr; +#elif defined(_WIN32) + if (needle.IsNull()) + /* the MultiByteToWideChar() call in the constructor + has failed, so let's always fail the comparison */ + return false; + + try { + auto w_haystack = MultiByteToWideChar(CP_UTF8, haystack); + return FindNLSStringEx(LOCALE_NAME_INVARIANT, + FIND_FROMSTART|NORM_IGNORECASE, + w_haystack.c_str(), -1, + needle.c_str(), -1, + nullptr, + nullptr, nullptr, 0) >= 0; + } catch (...) { + /* MultiByteToWideChar() has failed */ + return false; + } #elif defined(HAVE_STRCASESTR) return strcasestr(haystack, needle.c_str()) != nullptr; #else diff --git a/src/lib/icu/Compare.hxx b/src/lib/icu/Compare.hxx index 832e7949c..cec58ab70 100644 --- a/src/lib/icu/Compare.hxx +++ b/src/lib/icu/Compare.hxx @@ -25,13 +25,23 @@ #include +#ifdef _WIN32 +#include +#endif + /** * This class can compare one string ("needle") with lots of other * strings ("haystacks") efficiently, ignoring case. With some * configurations, it can prepare a case-folded version of the needle. */ class IcuCompare { +#ifdef _WIN32 + /* Windows API functions work with wchar_t strings, so let's + cache the MultiByteToWideChar() result for performance */ + AllocatedString needle; +#else AllocatedString<> needle; +#endif public: IcuCompare():needle(nullptr) {} @@ -40,12 +50,12 @@ public: IcuCompare(const IcuCompare &src) noexcept :needle(src - ? AllocatedString<>::Duplicate(src.needle) + ? src.needle.Clone() : nullptr) {} IcuCompare &operator=(const IcuCompare &src) noexcept { needle = src - ? AllocatedString<>::Duplicate(src.needle) + ? src.needle.Clone() : nullptr; return *this; } diff --git a/src/lib/nfs/Connection.cxx b/src/lib/nfs/Connection.cxx index c1dc8d18d..f853b40eb 100644 --- a/src/lib/nfs/Connection.cxx +++ b/src/lib/nfs/Connection.cxx @@ -191,7 +191,9 @@ static constexpr int events_to_libnfs(unsigned i) noexcept { return ((i & SocketMonitor::READ) ? POLLIN : 0) | - ((i & SocketMonitor::WRITE) ? POLLOUT : 0); + ((i & SocketMonitor::WRITE) ? POLLOUT : 0) | + ((i & SocketMonitor::HANGUP) ? POLLHUP : 0) | + ((i & SocketMonitor::ERROR) ? POLLERR : 0); } NfsConnection::~NfsConnection() noexcept @@ -450,8 +452,7 @@ NfsConnection::ScheduleSocket() noexcept SocketMonitor::Open(_fd); } - SocketMonitor::Schedule(libnfs_to_events(which_events) - | SocketMonitor::HANGUP); + SocketMonitor::Schedule(libnfs_to_events(which_events)); } inline int diff --git a/src/lib/nfs/FileReader.cxx b/src/lib/nfs/FileReader.cxx index 6e66bfa71..5307a33ff 100644 --- a/src/lib/nfs/FileReader.cxx +++ b/src/lib/nfs/FileReader.cxx @@ -180,7 +180,6 @@ NfsFileReader::OnNfsConnectionDisconnected(std::exception_ptr e) noexcept inline void NfsFileReader::OpenCallback(nfsfh *_fh) noexcept { - assert(state == State::OPEN); assert(connection != nullptr); assert(_fh != nullptr); @@ -197,27 +196,33 @@ NfsFileReader::OpenCallback(nfsfh *_fh) noexcept } inline void -NfsFileReader::StatCallback(const struct stat *st) noexcept +NfsFileReader::StatCallback(const struct stat *_st) noexcept { - assert(state == State::STAT); assert(connection != nullptr); assert(fh != nullptr); - assert(st != nullptr); + assert(_st != nullptr); + +#if defined(_WIN32) && !defined(_WIN64) + /* on 32-bit Windows, libnfs enables -D_FILE_OFFSET_BITS=64, + but MPD (Meson) doesn't - to work around this mismatch, we + cast explicitly to "struct stat64" */ + const auto *st = (const struct stat64 *)_st; +#else + const auto *st = _st; +#endif if (!S_ISREG(st->st_mode)) { OnNfsFileError(std::make_exception_ptr(std::runtime_error("Not a regular file"))); return; } - state = State::IDLE; - OnNfsFileOpen(st->st_size); } void NfsFileReader::OnNfsCallback(unsigned status, void *data) noexcept { - switch (state) { + switch (std::exchange(state, State::IDLE)) { case State::INITIAL: case State::DEFER: case State::MOUNT: @@ -234,7 +239,6 @@ NfsFileReader::OnNfsCallback(unsigned status, void *data) noexcept break; case State::READ: - state = State::IDLE; OnNfsFileRead(data, status); break; } diff --git a/src/pcm/Order.cxx b/src/pcm/Order.cxx index b181dd75c..abdf2d88d 100644 --- a/src/pcm/Order.cxx +++ b/src/pcm/Order.cxx @@ -21,6 +21,28 @@ #include "Buffer.hxx" #include "util/ConstBuffer.hxx" + +/* + * According to: + * - https://xiph.org/flac/format.html#frame_header + * - https://github.com/nu774/qaac/wiki/Multichannel--handling + * the source channel order (after decoding, e.g., flac, alac) is for + * - 1ch: mono + * - 2ch: left, right + * - 3ch: left, right, center + * - 4ch: front left, front right, back left, back right + * - 5ch: front left, front right, front center, back/surround left, back/surround right + * - 6ch (aka 5.1): front left, front right, front center, LFE, back/surround left, back/surround right + * - 7ch: front left, front right, front center, LFE, back center, side left, side right + * - 8ch: (aka 7.1): front left, front right, front center, LFE, back left, back right, side left, side right + * + * The ALSA default channel map is (see /usr/share/alsa/pcm/surround71.conf): + * - front left, front right, back left, back right, front center, LFE, side left, side right + * + * Hence, in case of the following source channel orders 3ch, 5ch, 6ch (aka + * 5.1), 7ch and 8ch the channel order has to be adapted + */ + template struct TwoPointers { V *dest; @@ -44,17 +66,57 @@ struct TwoPointers { return *this; } + TwoPointers &ToAlsa50() noexcept { + *dest++ = src[0]; // front left + *dest++ = src[1]; // front right + *dest++ = src[3]; // surround left + *dest++ = src[4]; // surround right + *dest++ = src[2]; // front center + src += 5; + return *this; + } + TwoPointers &ToAlsa51() noexcept { return CopyTwo() // left+right .SwapTwoPairs(); // center, LFE, surround left+right } + TwoPointers &ToAlsa70() noexcept { + *dest++ = src[0]; // front left + *dest++ = src[1]; // front right + *dest++ = src[5]; // side left + *dest++ = src[6]; // side right + *dest++ = src[2]; // front center + *dest++ = src[3]; // LFE + *dest++ = src[4]; // back center + src += 7; + return *this; + } + TwoPointers &ToAlsa71() noexcept { return ToAlsa51() .CopyTwo(); // side left+right } }; +template +static void +ToAlsaChannelOrder50(V *dest, const V *src, size_t n) noexcept +{ + TwoPointers p{dest, src}; + for (size_t i = 0; i != n; ++i) + p.ToAlsa50(); +} + +template +static inline ConstBuffer +ToAlsaChannelOrder50(PcmBuffer &buffer, ConstBuffer src) noexcept +{ + auto dest = buffer.GetT(src.size); + ToAlsaChannelOrder50(dest, src.data, src.size / 5); + return { dest, src.size }; +} + template static void ToAlsaChannelOrder51(V *dest, const V *src, size_t n) noexcept @@ -73,6 +135,24 @@ ToAlsaChannelOrder51(PcmBuffer &buffer, ConstBuffer src) noexcept return { dest, src.size }; } +template +static void +ToAlsaChannelOrder70(V *dest, const V *src, size_t n) noexcept +{ + TwoPointers p{dest, src}; + for (size_t i = 0; i != n; ++i) + p.ToAlsa70(); +} + +template +static inline ConstBuffer +ToAlsaChannelOrder70(PcmBuffer &buffer, ConstBuffer src) noexcept +{ + auto dest = buffer.GetT(src.size); + ToAlsaChannelOrder70(dest, src.data, src.size / 7); + return { dest, src.size }; +} + template static void ToAlsaChannelOrder71(V *dest, const V *src, size_t n) noexcept @@ -97,9 +177,15 @@ ToAlsaChannelOrderT(PcmBuffer &buffer, ConstBuffer src, unsigned channels) noexcept { switch (channels) { + case 5: // 5.0 + return ToAlsaChannelOrder50(buffer, src); + case 6: // 5.1 return ToAlsaChannelOrder51(buffer, src); + case 7: // 7.0 + return ToAlsaChannelOrder70(buffer, src); + case 8: // 7.1 return ToAlsaChannelOrder71(buffer, src); diff --git a/src/player/CrossFade.cxx b/src/player/CrossFade.cxx index e5611f97c..7c9ece160 100644 --- a/src/player/CrossFade.cxx +++ b/src/player/CrossFade.cxx @@ -62,7 +62,7 @@ mixramp_interpolate(const char *ramp_list, float required_db) noexcept ++ramp_list; /* Check for exact match. */ - if (db == required_db) { + if (db >= required_db) { return duration; } diff --git a/src/player/Thread.cxx b/src/player/Thread.cxx index d790fbb3f..704eee05e 100644 --- a/src/player/Thread.cxx +++ b/src/player/Thread.cxx @@ -967,6 +967,12 @@ Player::SongBorder() noexcept if (border_pause) { paused = true; pc.listener.OnBorderPause(); + + /* drain all outputs to guarantee the current song is + really being played to the end; without this, the + Pause() call would drop all ring buffers */ + pc.outputs.Drain(); + pc.outputs.Pause(); idle_add(IDLE_PLAYER); } diff --git a/src/storage/plugins/CurlStorage.cxx b/src/storage/plugins/CurlStorage.cxx index d2fc24150..356fa8b58 100644 --- a/src/storage/plugins/CurlStorage.cxx +++ b/src/storage/plugins/CurlStorage.cxx @@ -394,7 +394,7 @@ private: break; case State::HREF: - response.href.assign(s, len); + response.href.append(s, len); break; case State::STATUS: @@ -474,7 +474,7 @@ class HttpListDirectoryOperation final : public PropfindOperation { public: HttpListDirectoryOperation(CurlGlobal &curl, const char *uri) :PropfindOperation(curl, uri, 1), - base_path(UriPathOrSlash(uri)) {} + base_path(CurlUnescape(GetEasy(), UriPathOrSlash(uri))) {} std::unique_ptr Perform() { DeferStart(); @@ -499,8 +499,7 @@ private: /* kludge: ignoring case in this comparison to avoid false negatives if the web server uses a different - case in hex digits in escaped characters; TODO: - implement properly */ + case */ path = StringAfterPrefixIgnoreCase(path, base_path.c_str()); if (path == nullptr || path.empty()) return nullptr; @@ -523,11 +522,12 @@ protected: if (r.status != 200) return; - const auto escaped_name = HrefToEscapedName(r.href.c_str()); - if (escaped_name.IsNull()) + std::string href = CurlUnescape(GetEasy(), r.href.c_str()); + const auto name = HrefToEscapedName(href.c_str()); + if (name.IsNull()) return; - entries.emplace_front(CurlUnescape(GetEasy(), escaped_name)); + entries.emplace_front(std::string(name.data, name.size)); auto &info = entries.front().info; info = StorageFileInfo(r.collection diff --git a/src/tag/Fallback.hxx b/src/tag/Fallback.hxx index 9690c1684..a3ee48bd1 100644 --- a/src/tag/Fallback.hxx +++ b/src/tag/Fallback.hxx @@ -45,6 +45,10 @@ ApplyTagFallback(TagType type, F &&f) noexcept "AlbumArtist"/"ArtistSort" was found */ return f(TAG_ARTIST); + if (type == TAG_ALBUM_SORT) + /* fall back to "Album" if no "AlbumSort" was found */ + return f(TAG_ALBUM); + return false; } diff --git a/src/zeroconf/AvahiPoll.cxx b/src/zeroconf/AvahiPoll.cxx index 3c1111e49..2edec9da6 100644 --- a/src/zeroconf/AvahiPoll.cxx +++ b/src/zeroconf/AvahiPoll.cxx @@ -26,9 +26,7 @@ static unsigned FromAvahiWatchEvent(AvahiWatchEvent e) { return (e & AVAHI_WATCH_IN ? SocketMonitor::READ : 0) | - (e & AVAHI_WATCH_OUT ? SocketMonitor::WRITE : 0) | - (e & AVAHI_WATCH_ERR ? SocketMonitor::ERROR : 0) | - (e & AVAHI_WATCH_HUP ? SocketMonitor::HANGUP : 0); + (e & AVAHI_WATCH_OUT ? SocketMonitor::WRITE : 0); } static AvahiWatchEvent