From 00a520a4c35fa07552b2bf0965e41cde791ca1db Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 26 Feb 2021 00:58:49 +0100 Subject: [PATCH 01/24] doc/user.rst: update Windows&Android build dependencies Closes https://github.com/MusicPlayerDaemon/MPD/issues/1112 --- doc/user.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/user.rst b/doc/user.rst index e21b07d36..fe4825a4d 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -141,6 +141,15 @@ Basically, there are two ways to compile :program:`MPD` for Windows: This section is about the latter. +You need: + +* `mingw-w64 `__ +* `Meson 0.49.0 `__ and `Ninja + `__ +* cmake +* pkg-config +* quilt + Just like with the native build, unpack the :program:`MPD` source tarball and change into the directory. Then, instead of :program:`meson`, type: @@ -168,6 +177,11 @@ You need: * Android SDK * `Android NDK r22 `_ +* `Meson 0.49.0 `__ and `Ninja + `__ +* cmake +* pkg-config +* quilt Just like with the native build, unpack the :program:`MPD` source tarball and change into the directory. Then, instead of From c95e3dc065be71bc66dd3b7b3f7b208f541e9341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Rolim?= Date: Thu, 25 Feb 2021 14:21:36 -0300 Subject: [PATCH 02/24] storage/plugins/CurlStorage: don't use glibc extension in ParseTimePoint. %Z is a glibc extension to strptime, and is a no-op there, due to the mapping between timezone names and their definition (especially when the name comes from a different machine) being ambiguous / impossible. Time in HTTP headers is guaranteed to be UTC. Passing an unknown format to strptime() implementations that don't support it will generally cause them to return NULL, which will lead to ParseTimePoint throwing an exception and ParseTimeStamp using an unnecessary fallback. Since the timezone name goes at the end of the string, we don't need to use %Z to skip it (could be an issue in a different time stamp format), so simply removing %Z works best. --- NEWS | 2 ++ src/storage/plugins/CurlStorage.cxx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 47e7f1be4..35fba5b78 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,8 @@ ver 0.22.7 (not yet released) * decoder - ffmpeg: fix build problem with FFmpeg 3.4 +* storage + - curl: don't use glibc extension ver 0.22.6 (2021/02/16) * fix missing tags on songs in queue diff --git a/src/storage/plugins/CurlStorage.cxx b/src/storage/plugins/CurlStorage.cxx index c986a8a49..731f7e7b9 100644 --- a/src/storage/plugins/CurlStorage.cxx +++ b/src/storage/plugins/CurlStorage.cxx @@ -193,7 +193,7 @@ ParseTimeStamp(const char *s) { try { // TODO: make this more robust - return ParseTimePoint(s, "%a, %d %b %Y %T %Z"); + return ParseTimePoint(s, "%a, %d %b %Y %T"); } catch (...) { return std::chrono::system_clock::time_point::min(); } From 8d80280ab9ac9ec17abd981990771ea9927767a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Rolim?= Date: Thu, 25 Feb 2021 13:53:31 -0300 Subject: [PATCH 03/24] time/ISO8601: don't use glibc extension in strptime. Per the manual for strptime, %F is equivalent %Y-%m-%d, so use that directly. --- NEWS | 2 ++ src/time/ISO8601.cxx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 35fba5b78..dbd9cbe1a 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,6 @@ ver 0.22.7 (not yet released) +* protocol + - don't use glibc extension to parse time stamps * decoder - ffmpeg: fix build problem with FFmpeg 3.4 * storage diff --git a/src/time/ISO8601.cxx b/src/time/ISO8601.cxx index 067e4c4ee..edda7034e 100644 --- a/src/time/ISO8601.cxx +++ b/src/time/ISO8601.cxx @@ -185,7 +185,7 @@ ParseISO8601(const char *s) struct tm tm{}; /* parse the date */ - const char *end = strptime(s, "%F", &tm); + const char *end = strptime(s, "%Y-%m-%d", &tm); if (end == nullptr) { /* try without field separators */ end = strptime(s, "%Y%m%d", &tm); From cfb7f8ab84ca4c2d6d2f2826f893992c27a7f591 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 14 Jan 2021 12:39:45 +0100 Subject: [PATCH 04/24] util/AllocatedString: rename to BasicAllocatedString To make things simpler, AllocatedString is now a non-template class. --- src/decoder/plugins/SidplayDecoderPlugin.cxx | 16 ++++----- src/fs/NarrowPath.hxx | 2 +- src/lib/icu/CaseFold.cxx | 8 ++--- src/lib/icu/CaseFold.hxx | 4 +-- src/lib/icu/Collate.cxx | 2 +- src/lib/icu/Compare.cxx | 2 +- src/lib/icu/Compare.hxx | 4 +-- src/lib/icu/Converter.cxx | 10 +++--- src/lib/icu/Converter.hxx | 6 ++-- src/lib/icu/Util.cxx | 4 +-- src/lib/icu/Util.hxx | 4 +-- src/lib/icu/Win32.cxx | 8 ++--- src/lib/icu/Win32.hxx | 7 ++-- src/output/plugins/WasapiOutputPlugin.cxx | 2 +- src/output/plugins/httpd/HttpdClient.cxx | 2 +- .../plugins/httpd/IcyMetaDataServer.cxx | 4 +-- .../plugins/httpd/IcyMetaDataServer.hxx | 4 +-- src/util/AllocatedString.hxx | 36 +++++++++++-------- src/util/FormatString.cxx | 6 ++-- src/util/FormatString.hxx | 6 ++-- 20 files changed, 73 insertions(+), 64 deletions(-) diff --git a/src/decoder/plugins/SidplayDecoderPlugin.cxx b/src/decoder/plugins/SidplayDecoderPlugin.cxx index 207f2088c..edc0ab028 100644 --- a/src/decoder/plugins/SidplayDecoderPlugin.cxx +++ b/src/decoder/plugins/SidplayDecoderPlugin.cxx @@ -456,7 +456,7 @@ sidplay_file_decode(DecoderClient &client, Path path_fs) } while (cmd != DecoderCommand::STOP); } -static AllocatedString +static AllocatedString Windows1252ToUTF8(const char *s) noexcept { #ifdef HAVE_ICU_CONVERTER @@ -469,9 +469,9 @@ Windows1252ToUTF8(const char *s) noexcept * Fallback to not transcoding windows-1252 to utf-8, that may result * in invalid utf-8 unless nonprintable characters are replaced. */ - auto t = AllocatedString::Duplicate(s); + auto t = AllocatedString::Duplicate(s); - for (size_t i = 0; t[i] != AllocatedString::SENTINEL; i++) + for (size_t i = 0; t[i] != AllocatedString::SENTINEL; i++) if (!IsPrintableASCII(t[i])) t[i] = '?'; @@ -479,7 +479,7 @@ Windows1252ToUTF8(const char *s) noexcept } gcc_pure -static AllocatedString +static AllocatedString GetInfoString(const SidTuneInfo &info, unsigned i) noexcept { #ifdef HAVE_SIDPLAYFP @@ -496,7 +496,7 @@ GetInfoString(const SidTuneInfo &info, unsigned i) noexcept } gcc_pure -static AllocatedString +static AllocatedString GetDateString(const SidTuneInfo &info) noexcept { /* @@ -507,12 +507,12 @@ GetDateString(const SidTuneInfo &info) noexcept * author or group> may be for example Rob Hubbard. A full field * may be for example "1987 Rob Hubbard". */ - AllocatedString release = GetInfoString(info, 2); + AllocatedString release = GetInfoString(info, 2); /* Keep the part only for the date. */ - for (size_t i = 0; release[i] != AllocatedString::SENTINEL; i++) + for (size_t i = 0; release[i] != AllocatedString::SENTINEL; i++) if (std::isspace(release[i])) { - release[i] = AllocatedString::SENTINEL; + release[i] = AllocatedString::SENTINEL; break; } diff --git a/src/fs/NarrowPath.hxx b/src/fs/NarrowPath.hxx index c74999f44..740342d7d 100644 --- a/src/fs/NarrowPath.hxx +++ b/src/fs/NarrowPath.hxx @@ -36,7 +36,7 @@ */ class NarrowPath { #ifdef _UNICODE - using Value = AllocatedString<>; + using Value = AllocatedString; #else using Value = StringPointer<>; #endif diff --git a/src/lib/icu/CaseFold.cxx b/src/lib/icu/CaseFold.cxx index edc466a48..ebb75f324 100644 --- a/src/lib/icu/CaseFold.cxx +++ b/src/lib/icu/CaseFold.cxx @@ -38,13 +38,13 @@ #include -AllocatedString<> +AllocatedString IcuCaseFold(std::string_view src) noexcept try { #ifdef HAVE_ICU const auto u = UCharFromUTF8(src); if (u.IsNull()) - return AllocatedString<>::Duplicate(src); + return AllocatedString::Duplicate(src); AllocatedArray folded(u.size() * 2U); @@ -54,7 +54,7 @@ try { U_FOLD_CASE_DEFAULT, &error_code); if (folded_length == 0 || error_code != U_ZERO_ERROR) - return AllocatedString<>::Duplicate(src); + return AllocatedString::Duplicate(src); folded.SetSize(folded_length); return UCharToUTF8({folded.begin(), folded.size()}); @@ -63,7 +63,7 @@ try { #error not implemented #endif } catch (...) { - return AllocatedString<>::Duplicate(src); + return AllocatedString::Duplicate(src); } #endif /* HAVE_ICU_CASE_FOLD */ diff --git a/src/lib/icu/CaseFold.hxx b/src/lib/icu/CaseFold.hxx index 73d4e2289..3aed54af0 100644 --- a/src/lib/icu/CaseFold.hxx +++ b/src/lib/icu/CaseFold.hxx @@ -27,9 +27,9 @@ #include -template class AllocatedString; +class AllocatedString; -AllocatedString +AllocatedString IcuCaseFold(std::string_view src) noexcept; #endif diff --git a/src/lib/icu/Collate.cxx b/src/lib/icu/Collate.cxx index 859e60978..c31f46883 100644 --- a/src/lib/icu/Collate.cxx +++ b/src/lib/icu/Collate.cxx @@ -88,7 +88,7 @@ IcuCollate(std::string_view a, std::string_view b) noexcept b.data(), b.size(), &code); #elif defined(_WIN32) - AllocatedString wa = nullptr, wb = nullptr; + BasicAllocatedString wa = nullptr, wb = nullptr; try { wa = MultiByteToWideChar(CP_UTF8, a); diff --git a/src/lib/icu/Compare.cxx b/src/lib/icu/Compare.cxx index 14184752d..6bc82bdd5 100644 --- a/src/lib/icu/Compare.cxx +++ b/src/lib/icu/Compare.cxx @@ -46,7 +46,7 @@ IcuCompare::IcuCompare(std::string_view _needle) noexcept #else IcuCompare::IcuCompare(std::string_view _needle) noexcept - :needle(AllocatedString<>::Duplicate(_needle)) {} + :needle(AllocatedString::Duplicate(_needle)) {} #endif diff --git a/src/lib/icu/Compare.hxx b/src/lib/icu/Compare.hxx index 2751f1b4b..6f4841739 100644 --- a/src/lib/icu/Compare.hxx +++ b/src/lib/icu/Compare.hxx @@ -38,9 +38,9 @@ class IcuCompare { #ifdef _WIN32 /* Windows API functions work with wchar_t strings, so let's cache the MultiByteToWideChar() result for performance */ - AllocatedString needle; + BasicAllocatedString needle; #else - AllocatedString<> needle; + AllocatedString needle; #endif public: diff --git a/src/lib/icu/Converter.cxx b/src/lib/icu/Converter.cxx index 228010c80..a231668a8 100644 --- a/src/lib/icu/Converter.cxx +++ b/src/lib/icu/Converter.cxx @@ -77,7 +77,7 @@ IcuConverter::Create(const char *charset) #ifdef HAVE_ICU #elif defined(HAVE_ICONV) -static AllocatedString +static AllocatedString DoConvert(iconv_t conv, std::string_view src) { // TODO: dynamic buffer? @@ -95,12 +95,12 @@ DoConvert(iconv_t conv, std::string_view src) if (in_left > 0) throw std::runtime_error("Charset conversion failed"); - return AllocatedString<>::Duplicate({buffer, sizeof(buffer) - out_left}); + return AllocatedString::Duplicate({buffer, sizeof(buffer) - out_left}); } #endif -AllocatedString +AllocatedString IcuConverter::ToUTF8(std::string_view s) const { #ifdef HAVE_ICU @@ -128,7 +128,7 @@ IcuConverter::ToUTF8(std::string_view s) const #endif } -AllocatedString +AllocatedString IcuConverter::FromUTF8(std::string_view s) const { #ifdef HAVE_ICU @@ -151,7 +151,7 @@ IcuConverter::FromUTF8(std::string_view s) const throw std::runtime_error(FormatString("Failed to convert from Unicode: %s", u_errorName(code)).c_str()); - return AllocatedString<>::Duplicate({buffer, size_t(target - buffer)}); + return AllocatedString::Duplicate({buffer, size_t(target - buffer)}); #elif defined(HAVE_ICONV) return DoConvert(from_utf8, s); diff --git a/src/lib/icu/Converter.hxx b/src/lib/icu/Converter.hxx index 8368cc166..fc7d6ae40 100644 --- a/src/lib/icu/Converter.hxx +++ b/src/lib/icu/Converter.hxx @@ -40,7 +40,7 @@ struct UConverter; #endif -template class AllocatedString; +class AllocatedString; /** * This class can convert strings with a certain character set to and @@ -85,7 +85,7 @@ public: * Throws std::runtime_error on error. */ gcc_nonnull_all - AllocatedString ToUTF8(std::string_view s) const; + AllocatedString ToUTF8(std::string_view s) const; /** * Convert the string from UTF-8. @@ -93,7 +93,7 @@ public: * Throws std::runtime_error on error. */ gcc_nonnull_all - AllocatedString FromUTF8(std::string_view s) const; + AllocatedString FromUTF8(std::string_view s) const; }; #endif diff --git a/src/lib/icu/Util.cxx b/src/lib/icu/Util.cxx index 3087bed73..a473f987b 100644 --- a/src/lib/icu/Util.cxx +++ b/src/lib/icu/Util.cxx @@ -48,7 +48,7 @@ UCharFromUTF8(std::string_view src) return dest; } -AllocatedString<> +AllocatedString UCharToUTF8(std::basic_string_view src) { /* worst-case estimate */ @@ -65,5 +65,5 @@ UCharToUTF8(std::basic_string_view src) throw std::runtime_error(u_errorName(error_code)); dest[dest_length] = 0; - return AllocatedString<>::Donate(dest.release()); + return AllocatedString::Donate(dest.release()); } diff --git a/src/lib/icu/Util.hxx b/src/lib/icu/Util.hxx index 7b2958e22..835acb4f6 100644 --- a/src/lib/icu/Util.hxx +++ b/src/lib/icu/Util.hxx @@ -25,7 +25,7 @@ #include template class AllocatedArray; -template class AllocatedString; +class AllocatedString; /** * Wrapper for u_strFromUTF8(). @@ -40,7 +40,7 @@ UCharFromUTF8(std::string_view src); * * Throws std::runtime_error on error. */ -AllocatedString +AllocatedString UCharToUTF8(std::basic_string_view src); #endif diff --git a/src/lib/icu/Win32.cxx b/src/lib/icu/Win32.cxx index e556a3b2b..05da2ef49 100644 --- a/src/lib/icu/Win32.cxx +++ b/src/lib/icu/Win32.cxx @@ -25,7 +25,7 @@ #include -AllocatedString +AllocatedString WideCharToMultiByte(unsigned code_page, std::wstring_view src) { int length = WideCharToMultiByte(code_page, 0, src.data(), src.size(), @@ -42,10 +42,10 @@ WideCharToMultiByte(unsigned code_page, std::wstring_view src) throw MakeLastError("Failed to convert from Unicode"); buffer[length] = '\0'; - return AllocatedString::Donate(buffer.release()); + return AllocatedString::Donate(buffer.release()); } -AllocatedString +BasicAllocatedString MultiByteToWideChar(unsigned code_page, std::string_view src) { int length = MultiByteToWideChar(code_page, 0, src.data(), src.size(), @@ -60,5 +60,5 @@ MultiByteToWideChar(unsigned code_page, std::string_view src) throw MakeLastError("Failed to convert to Unicode"); buffer[length] = L'\0'; - return AllocatedString::Donate(buffer.release()); + return BasicAllocatedString::Donate(buffer.release()); } diff --git a/src/lib/icu/Win32.hxx b/src/lib/icu/Win32.hxx index ab804d82c..287495010 100644 --- a/src/lib/icu/Win32.hxx +++ b/src/lib/icu/Win32.hxx @@ -24,20 +24,21 @@ #include -template class AllocatedString; +class AllocatedString; +template class BasicAllocatedString; /** * Throws std::system_error on error. */ gcc_pure gcc_nonnull_all -AllocatedString +AllocatedString WideCharToMultiByte(unsigned code_page, std::wstring_view src); /** * Throws std::system_error on error. */ gcc_pure gcc_nonnull_all -AllocatedString +BasicAllocatedString MultiByteToWideChar(unsigned code_page, std::string_view src); #endif diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index ce91f4e5b..a6c1d483b 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -194,7 +194,7 @@ private: bool is_exclusive; bool enumerate_devices; std::string device_config; - std::vector>> device_desc; + std::vector> device_desc; std::shared_ptr event; std::optional com; ComPtr enumerator; diff --git a/src/output/plugins/httpd/HttpdClient.cxx b/src/output/plugins/httpd/HttpdClient.cxx index c5e2d819e..36e9b3abc 100644 --- a/src/output/plugins/httpd/HttpdClient.cxx +++ b/src/output/plugins/httpd/HttpdClient.cxx @@ -136,7 +136,7 @@ bool HttpdClient::SendResponse() noexcept { char buffer[1024]; - AllocatedString<> allocated = nullptr; + AllocatedString allocated = nullptr; const char *response; assert(state == State::RESPONSE); diff --git a/src/output/plugins/httpd/IcyMetaDataServer.cxx b/src/output/plugins/httpd/IcyMetaDataServer.cxx index d923835f3..6e4a0d24d 100644 --- a/src/output/plugins/httpd/IcyMetaDataServer.cxx +++ b/src/output/plugins/httpd/IcyMetaDataServer.cxx @@ -27,7 +27,7 @@ #include -AllocatedString<> +AllocatedString icy_server_metadata_header(const char *name, const char *genre, const char *url, const char *content_type, int metaint) noexcept @@ -54,7 +54,7 @@ icy_server_metadata_header(const char *name, content_type); } -static AllocatedString<> +static AllocatedString icy_server_metadata_string(const char *stream_title, const char* stream_url) noexcept { diff --git a/src/output/plugins/httpd/IcyMetaDataServer.hxx b/src/output/plugins/httpd/IcyMetaDataServer.hxx index ef7b32646..f3762dd59 100644 --- a/src/output/plugins/httpd/IcyMetaDataServer.hxx +++ b/src/output/plugins/httpd/IcyMetaDataServer.hxx @@ -24,9 +24,9 @@ #include "tag/Type.h" struct Tag; -template class AllocatedString; +class AllocatedString; -AllocatedString +AllocatedString icy_server_metadata_header(const char *name, const char *genre, const char *url, const char *content_type, int metaint) noexcept; diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index 162654e68..3c9a7b9b9 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 Max Kellermann + * Copyright 2015-2021 Max Kellermann * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -41,8 +41,8 @@ * * Unlike std::string, this object can hold a "nullptr" special value. */ -template -class AllocatedString { +template +class BasicAllocatedString { public: using value_type = typename StringPointer::value_type; using reference = typename StringPointer::reference; @@ -57,41 +57,41 @@ public: private: pointer value; - explicit AllocatedString(pointer _value) noexcept + explicit BasicAllocatedString(pointer _value) noexcept :value(_value) {} public: - AllocatedString(std::nullptr_t n) noexcept + BasicAllocatedString(std::nullptr_t n) noexcept :value(n) {} - AllocatedString(AllocatedString &&src) noexcept + BasicAllocatedString(BasicAllocatedString &&src) noexcept :value(src.Steal()) {} - ~AllocatedString() noexcept { + ~BasicAllocatedString() noexcept { delete[] value; } - static AllocatedString Donate(pointer value) noexcept { - return AllocatedString(value); + static BasicAllocatedString Donate(pointer value) noexcept { + return BasicAllocatedString(value); } - static AllocatedString Null() noexcept { + static BasicAllocatedString Null() noexcept { return nullptr; } - static AllocatedString Empty() { + static BasicAllocatedString Empty() { auto p = new value_type[1]; p[0] = SENTINEL; return Donate(p); } - static AllocatedString Duplicate(string_view src) { + static BasicAllocatedString Duplicate(string_view src) { auto p = new value_type[src.size() + 1]; *std::copy_n(src.data(), src.size(), p) = SENTINEL; return Donate(p); } - AllocatedString &operator=(AllocatedString &&src) noexcept { + BasicAllocatedString &operator=(BasicAllocatedString &&src) noexcept { std::swap(value, src.value); return *this; } @@ -136,9 +136,17 @@ public: return std::exchange(value, nullptr); } - AllocatedString Clone() const { + BasicAllocatedString Clone() const { return Duplicate(*this); } }; +class AllocatedString : public BasicAllocatedString { +public: + using BasicAllocatedString::BasicAllocatedString; + + AllocatedString(BasicAllocatedString &&src) noexcept + :BasicAllocatedString(std::move(src)) {} +}; + #endif diff --git a/src/util/FormatString.cxx b/src/util/FormatString.cxx index 7f6592e4f..7cf43192d 100644 --- a/src/util/FormatString.cxx +++ b/src/util/FormatString.cxx @@ -23,7 +23,7 @@ #include #include -AllocatedString<> +AllocatedString FormatStringV(const char *fmt, std::va_list args) noexcept { std::va_list tmp; @@ -37,10 +37,10 @@ FormatStringV(const char *fmt, std::va_list args) noexcept char *buffer = new char[length + 1]; vsnprintf(buffer, length + 1, fmt, args); - return AllocatedString<>::Donate(buffer); + return AllocatedString::Donate(buffer); } -AllocatedString<> +AllocatedString FormatString(const char *fmt, ...) noexcept { std::va_list args; diff --git a/src/util/FormatString.hxx b/src/util/FormatString.hxx index 54069abd5..83f112a91 100644 --- a/src/util/FormatString.hxx +++ b/src/util/FormatString.hxx @@ -24,20 +24,20 @@ #include -template class AllocatedString; +class AllocatedString; /** * Format into an #AllocatedString. */ gcc_nonnull_all -AllocatedString +AllocatedString FormatStringV(const char *fmt, std::va_list args) noexcept; /** * Format into an #AllocatedString. */ gcc_nonnull(1) gcc_printf(1,2) -AllocatedString +AllocatedString FormatString(const char *fmt, ...) noexcept; #endif From 32b7b2e2fa1d2acbadea0390b63872451591ff5b Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 14 Jan 2021 13:02:30 +0100 Subject: [PATCH 05/24] util/AllocatedString: add default constructor --- src/lib/icu/Collate.cxx | 2 +- src/output/plugins/httpd/HttpdClient.cxx | 2 +- src/util/AllocatedString.hxx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/icu/Collate.cxx b/src/lib/icu/Collate.cxx index c31f46883..54d226895 100644 --- a/src/lib/icu/Collate.cxx +++ b/src/lib/icu/Collate.cxx @@ -88,7 +88,7 @@ IcuCollate(std::string_view a, std::string_view b) noexcept b.data(), b.size(), &code); #elif defined(_WIN32) - BasicAllocatedString wa = nullptr, wb = nullptr; + BasicAllocatedString wa, wb; try { wa = MultiByteToWideChar(CP_UTF8, a); diff --git a/src/output/plugins/httpd/HttpdClient.cxx b/src/output/plugins/httpd/HttpdClient.cxx index 36e9b3abc..8a88b131d 100644 --- a/src/output/plugins/httpd/HttpdClient.cxx +++ b/src/output/plugins/httpd/HttpdClient.cxx @@ -136,7 +136,7 @@ bool HttpdClient::SendResponse() noexcept { char buffer[1024]; - AllocatedString allocated = nullptr; + AllocatedString allocated; const char *response; assert(state == State::RESPONSE); diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index 3c9a7b9b9..2a5aaa02d 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -55,12 +55,13 @@ public: static constexpr value_type SENTINEL = '\0'; private: - pointer value; + pointer value = nullptr; explicit BasicAllocatedString(pointer _value) noexcept :value(_value) {} public: + BasicAllocatedString() noexcept = default; BasicAllocatedString(std::nullptr_t n) noexcept :value(n) {} @@ -145,6 +146,7 @@ class AllocatedString : public BasicAllocatedString { public: using BasicAllocatedString::BasicAllocatedString; + AllocatedString() noexcept = default; AllocatedString(BasicAllocatedString &&src) noexcept :BasicAllocatedString(std::move(src)) {} }; From 6e1c8edf095bb89aa50db4bb54e4507320f14e76 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 14 Jan 2021 13:16:52 +0100 Subject: [PATCH 06/24] util/AllocatedString: add string_view constructor Replaces the static Duplicate() method. --- src/decoder/plugins/SidplayDecoderPlugin.cxx | 2 +- src/lib/icu/CaseFold.cxx | 6 +++--- src/lib/icu/Compare.cxx | 2 +- src/lib/icu/Converter.cxx | 4 ++-- src/util/AllocatedString.hxx | 18 +++++++++++------- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/decoder/plugins/SidplayDecoderPlugin.cxx b/src/decoder/plugins/SidplayDecoderPlugin.cxx index edc0ab028..ba13e6ff4 100644 --- a/src/decoder/plugins/SidplayDecoderPlugin.cxx +++ b/src/decoder/plugins/SidplayDecoderPlugin.cxx @@ -469,7 +469,7 @@ Windows1252ToUTF8(const char *s) noexcept * Fallback to not transcoding windows-1252 to utf-8, that may result * in invalid utf-8 unless nonprintable characters are replaced. */ - auto t = AllocatedString::Duplicate(s); + AllocatedString t(s); for (size_t i = 0; t[i] != AllocatedString::SENTINEL; i++) if (!IsPrintableASCII(t[i])) diff --git a/src/lib/icu/CaseFold.cxx b/src/lib/icu/CaseFold.cxx index ebb75f324..ec7ca80fb 100644 --- a/src/lib/icu/CaseFold.cxx +++ b/src/lib/icu/CaseFold.cxx @@ -44,7 +44,7 @@ try { #ifdef HAVE_ICU const auto u = UCharFromUTF8(src); if (u.IsNull()) - return AllocatedString::Duplicate(src); + return AllocatedString(src); AllocatedArray folded(u.size() * 2U); @@ -54,7 +54,7 @@ try { U_FOLD_CASE_DEFAULT, &error_code); if (folded_length == 0 || error_code != U_ZERO_ERROR) - return AllocatedString::Duplicate(src); + return AllocatedString(src); folded.SetSize(folded_length); return UCharToUTF8({folded.begin(), folded.size()}); @@ -63,7 +63,7 @@ try { #error not implemented #endif } catch (...) { - return AllocatedString::Duplicate(src); + return AllocatedString(src); } #endif /* HAVE_ICU_CASE_FOLD */ diff --git a/src/lib/icu/Compare.cxx b/src/lib/icu/Compare.cxx index 6bc82bdd5..dfc5d2210 100644 --- a/src/lib/icu/Compare.cxx +++ b/src/lib/icu/Compare.cxx @@ -46,7 +46,7 @@ IcuCompare::IcuCompare(std::string_view _needle) noexcept #else IcuCompare::IcuCompare(std::string_view _needle) noexcept - :needle(AllocatedString::Duplicate(_needle)) {} + :needle(_needle) {} #endif diff --git a/src/lib/icu/Converter.cxx b/src/lib/icu/Converter.cxx index a231668a8..6123c2490 100644 --- a/src/lib/icu/Converter.cxx +++ b/src/lib/icu/Converter.cxx @@ -95,7 +95,7 @@ DoConvert(iconv_t conv, std::string_view src) if (in_left > 0) throw std::runtime_error("Charset conversion failed"); - return AllocatedString::Duplicate({buffer, sizeof(buffer) - out_left}); + return AllocatedString({buffer, sizeof(buffer) - out_left}); } #endif @@ -151,7 +151,7 @@ IcuConverter::FromUTF8(std::string_view s) const throw std::runtime_error(FormatString("Failed to convert from Unicode: %s", u_errorName(code)).c_str()); - return AllocatedString::Duplicate({buffer, size_t(target - buffer)}); + return AllocatedString({buffer, size_t(target - buffer)}); #elif defined(HAVE_ICONV) return DoConvert(from_utf8, s); diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index 2a5aaa02d..6d02aa25e 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -65,6 +65,9 @@ public: BasicAllocatedString(std::nullptr_t n) noexcept :value(n) {} + explicit BasicAllocatedString(string_view src) noexcept + :value(Duplicate(src)) {} + BasicAllocatedString(BasicAllocatedString &&src) noexcept :value(src.Steal()) {} @@ -86,12 +89,6 @@ public: return Donate(p); } - static BasicAllocatedString Duplicate(string_view src) { - auto p = new value_type[src.size() + 1]; - *std::copy_n(src.data(), src.size(), p) = SENTINEL; - return Donate(p); - } - BasicAllocatedString &operator=(BasicAllocatedString &&src) noexcept { std::swap(value, src.value); return *this; @@ -138,7 +135,14 @@ public: } BasicAllocatedString Clone() const { - return Duplicate(*this); + return BasicAllocatedString(Duplicate(*this)); + } + +private: + static pointer Duplicate(string_view src) { + auto p = new value_type[src.size() + 1]; + *std::copy_n(src.data(), src.size(), p) = SENTINEL; + return p; } }; From bca5d79f88a63a79d964181cead4271ac76dc40f Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 14 Jan 2021 13:34:43 +0100 Subject: [PATCH 07/24] util/AllocatedString: add const_pointer constructor --- src/util/AllocatedString.hxx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index 6d02aa25e..e977712fc 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -65,7 +65,10 @@ public: BasicAllocatedString(std::nullptr_t n) noexcept :value(n) {} - explicit BasicAllocatedString(string_view src) noexcept + explicit BasicAllocatedString(string_view src) + :value(Duplicate(src)) {} + + explicit BasicAllocatedString(const_pointer src) :value(Duplicate(src)) {} BasicAllocatedString(BasicAllocatedString &&src) noexcept @@ -144,6 +147,12 @@ private: *std::copy_n(src.data(), src.size(), p) = SENTINEL; return p; } + + static pointer Duplicate(const_pointer src) { + return src != nullptr + ? Duplicate(std::string_view(src)) + : nullptr; + } }; class AllocatedString : public BasicAllocatedString { From b833c5d2c71090eb5130a8389505e0df59697d31 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 14 Jan 2021 13:43:42 +0100 Subject: [PATCH 08/24] util/AllocatedString: replace Clone() with copy constructor --- src/lib/icu/Compare.hxx | 10 +++++----- src/util/AllocatedString.hxx | 9 ++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/lib/icu/Compare.hxx b/src/lib/icu/Compare.hxx index 6f4841739..3110d16aa 100644 --- a/src/lib/icu/Compare.hxx +++ b/src/lib/icu/Compare.hxx @@ -38,11 +38,11 @@ class IcuCompare { #ifdef _WIN32 /* Windows API functions work with wchar_t strings, so let's cache the MultiByteToWideChar() result for performance */ - BasicAllocatedString needle; -#else - AllocatedString needle; + using AllocatedString = BasicAllocatedString; #endif + AllocatedString needle; + public: IcuCompare():needle(nullptr) {} @@ -50,12 +50,12 @@ public: IcuCompare(const IcuCompare &src) noexcept :needle(src - ? src.needle.Clone() + ? AllocatedString(src.needle) : nullptr) {} IcuCompare &operator=(const IcuCompare &src) noexcept { needle = src - ? src.needle.Clone() + ? AllocatedString(src.needle) : nullptr; return *this; } diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index e977712fc..9b2c5812a 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -71,6 +71,9 @@ public: explicit BasicAllocatedString(const_pointer src) :value(Duplicate(src)) {} + BasicAllocatedString(const BasicAllocatedString &src) noexcept + :BasicAllocatedString(Duplicate(src.value)) {} + BasicAllocatedString(BasicAllocatedString &&src) noexcept :value(src.Steal()) {} @@ -137,10 +140,6 @@ public: return std::exchange(value, nullptr); } - BasicAllocatedString Clone() const { - return BasicAllocatedString(Duplicate(*this)); - } - private: static pointer Duplicate(string_view src) { auto p = new value_type[src.size() + 1]; @@ -150,7 +149,7 @@ private: static pointer Duplicate(const_pointer src) { return src != nullptr - ? Duplicate(std::string_view(src)) + ? Duplicate(string_view(src)) : nullptr; } }; From 99405a4c93ab5ef3387e851ee5d452f838e2338c Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 14 Jan 2021 13:35:24 +0100 Subject: [PATCH 09/24] util/AllocatedString: add operator=() --- src/util/AllocatedString.hxx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index 9b2c5812a..19f17ee0e 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -100,6 +100,18 @@ public: return *this; } + BasicAllocatedString &operator=(string_view src) noexcept { + delete[] std::exchange(value, nullptr); + value = Duplicate(src); + return *this; + } + + BasicAllocatedString &operator=(const_pointer src) noexcept { + delete[] std::exchange(value, nullptr); + value = src != nullptr ? Duplicate(src) : nullptr; + return *this; + } + constexpr bool operator==(std::nullptr_t) const noexcept { return value == nullptr; } @@ -161,6 +173,8 @@ public: AllocatedString() noexcept = default; AllocatedString(BasicAllocatedString &&src) noexcept :BasicAllocatedString(std::move(src)) {} + + using BasicAllocatedString::operator=; }; #endif From 67760f52831cbfbb0014bc4744029c3e2c9840be Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Mon, 18 Jan 2021 22:32:29 +0100 Subject: [PATCH 10/24] util/AllocatedString: support casting a nulled instance to string_view --- src/util/AllocatedString.hxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index 19f17ee0e..2ca1218c9 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -125,7 +125,9 @@ public: } operator string_view() const noexcept { - return value; + return value != nullptr + ? string_view(value) + : string_view(); } constexpr const_pointer c_str() const noexcept { From dc9103befecf6056b0a75e569de030acc847f4a9 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 21 Jan 2021 20:07:14 +0100 Subject: [PATCH 11/24] util/AllocatedString: remove Null(), IsNull() --- src/fs/NarrowPath.cxx | 2 +- src/lib/icu/Compare.cxx | 4 ++-- src/lib/icu/Compare.hxx | 2 +- src/output/plugins/httpd/IcyMetaDataServer.cxx | 2 +- src/util/AllocatedString.hxx | 8 -------- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/fs/NarrowPath.cxx b/src/fs/NarrowPath.cxx index fec228f9d..e9655d7f8 100644 --- a/src/fs/NarrowPath.cxx +++ b/src/fs/NarrowPath.cxx @@ -29,7 +29,7 @@ NarrowPath::NarrowPath(Path _path) noexcept :value(WideCharToMultiByte(CP_ACP, _path.c_str())) { - if (value.IsNull()) + if (value == nullptr) /* fall back to empty string */ value = Value::Empty(); } diff --git a/src/lib/icu/Compare.cxx b/src/lib/icu/Compare.cxx index dfc5d2210..60265dd10 100644 --- a/src/lib/icu/Compare.cxx +++ b/src/lib/icu/Compare.cxx @@ -56,7 +56,7 @@ 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()) + if (needle == nullptr) /* the MultiByteToWideChar() call in the constructor has failed, so let's always fail the comparison */ return false; @@ -83,7 +83,7 @@ IcuCompare::IsIn(const char *haystack) const noexcept return StringFind(IcuCaseFold(haystack).c_str(), needle.c_str()) != nullptr; #elif defined(_WIN32) - if (needle.IsNull()) + if (needle == nullptr) /* the MultiByteToWideChar() call in the constructor has failed, so let's always fail the comparison */ return false; diff --git a/src/lib/icu/Compare.hxx b/src/lib/icu/Compare.hxx index 3110d16aa..88e2e0711 100644 --- a/src/lib/icu/Compare.hxx +++ b/src/lib/icu/Compare.hxx @@ -65,7 +65,7 @@ public: gcc_pure operator bool() const noexcept { - return !needle.IsNull(); + return needle != nullptr; } gcc_pure diff --git a/src/output/plugins/httpd/IcyMetaDataServer.cxx b/src/output/plugins/httpd/IcyMetaDataServer.cxx index 6e4a0d24d..d746d0a0a 100644 --- a/src/output/plugins/httpd/IcyMetaDataServer.cxx +++ b/src/output/plugins/httpd/IcyMetaDataServer.cxx @@ -110,7 +110,7 @@ icy_server_metadata_page(const Tag &tag, const TagType *types) noexcept const auto icy_string = icy_server_metadata_string(stream_title, ""); - if (icy_string.IsNull()) + if (icy_string == nullptr) return nullptr; return std::make_shared(icy_string.c_str(), diff --git a/src/util/AllocatedString.hxx b/src/util/AllocatedString.hxx index 2ca1218c9..27c047514 100644 --- a/src/util/AllocatedString.hxx +++ b/src/util/AllocatedString.hxx @@ -85,10 +85,6 @@ public: return BasicAllocatedString(value); } - static BasicAllocatedString Null() noexcept { - return nullptr; - } - static BasicAllocatedString Empty() { auto p = new value_type[1]; p[0] = SENTINEL; @@ -120,10 +116,6 @@ public: return value != nullptr; } - constexpr bool IsNull() const noexcept { - return value == nullptr; - } - operator string_view() const noexcept { return value != nullptr ? string_view(value) From 0f39dc1edb91b4126ceb2c4c5e21d82bae32ffb8 Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Thu, 4 Mar 2021 18:09:56 +0100 Subject: [PATCH 12/24] output/wasapi: use AUDCLNT_BUFFERFLAGS_SILENT for paused output --- src/output/plugins/WasapiOutputPlugin.cxx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index a6c1d483b..d9d16e97e 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -275,6 +275,7 @@ void WasapiOutputThread::Work() noexcept { } BYTE *data; + DWORD mode = 0; result = render_client->GetBuffer(write_in_frames, &data); if (FAILED(result)) { @@ -282,18 +283,20 @@ void WasapiOutputThread::Work() noexcept { } AtScopeExit(&) { - render_client->ReleaseBuffer(write_in_frames, 0); + render_client->ReleaseBuffer(write_in_frames, mode); }; - const UINT32 write_size = write_in_frames * frame_size; - UINT32 new_data_size = 0; if (current_state == Status::PLAY) { + const UINT32 write_size = write_in_frames * frame_size; + UINT32 new_data_size = 0; new_data_size = spsc_buffer.pop(data, write_size); + std::fill_n(data + new_data_size, + write_size - new_data_size, 0); } else { + mode = AUDCLNT_BUFFERFLAGS_SILENT; FormatDebug(wasapi_output_domain, "Working thread paused"); } - std::fill_n(data + new_data_size, write_size - new_data_size, 0); } catch (...) { std::unique_lock lock(error.mutex); error.error_ptr = std::current_exception(); From ed1a995bffb9dff8d6487c5fedc8e939f8648fd6 Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Sat, 27 Feb 2021 18:35:56 +0800 Subject: [PATCH 13/24] thread: Add `Future` implement for mingw32 without pthread --- src/thread/Future.hxx | 43 ++++ src/thread/WindowsFuture.hxx | 394 +++++++++++++++++++++++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 src/thread/Future.hxx create mode 100644 src/thread/WindowsFuture.hxx diff --git a/src/thread/Future.hxx b/src/thread/Future.hxx new file mode 100644 index 000000000..a277066f9 --- /dev/null +++ b/src/thread/Future.hxx @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * 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 + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef THREAD_FUTURE_HXX +#define THREAD_FUTURE_HXX + +#ifdef _WIN32 + +#include "WindowsFuture.hxx" + +template +using Future = WinFuture; +template +using Promise = WinPromise; + +#else + +#include + +template +using Future = std::future; +template +using Promise = std::promise; + +#endif + +#endif diff --git a/src/thread/WindowsFuture.hxx b/src/thread/WindowsFuture.hxx new file mode 100644 index 000000000..f5d304a1e --- /dev/null +++ b/src/thread/WindowsFuture.hxx @@ -0,0 +1,394 @@ +/* + * Copyright 2020-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * 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 + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef THREAD_WINDOWS_FUTURE_HXX +#define THREAD_WINDOWS_FUTURE_HXX + +#include "CriticalSection.hxx" +#include "WindowsCond.hxx" +#include +#include +#include + +enum class WinFutureErrc : int { + future_already_retrieved = 1, + promise_already_satisfied, + no_state, + broken_promise, +}; + +enum class WinFutureStatus { ready, timeout, deferred }; + +static inline const std::error_category &win_future_category() noexcept; +class WinFutureCategory : public std::error_category { +public: + const char *name() const noexcept override { return "win_future"; } + std::string message(int Errcode) const override { + using namespace std::literals; + switch (static_cast(Errcode)) { + case WinFutureErrc::broken_promise: + return "Broken promise"s; + case WinFutureErrc::future_already_retrieved: + return "Future already retrieved"s; + case WinFutureErrc::promise_already_satisfied: + return "Promise already satisfied"s; + case WinFutureErrc::no_state: + return "No associated state"s; + default: + return "Unknown error"s; + } + } + std::error_condition default_error_condition(int code) const noexcept override { + return std::error_condition(code, win_future_category()); + } +}; +static inline const std::error_category &win_future_category() noexcept { + static const WinFutureCategory win_future_category_instance{}; + return win_future_category_instance; +} + +class WinFutureError : public std::logic_error { +public: + WinFutureError(WinFutureErrc errcode) + : WinFutureError( + std::error_code(static_cast(errcode), win_future_category())) {} + +private: + explicit WinFutureError(std::error_code errcode) + : std::logic_error("WinFutureError: " + errcode.message()), code(errcode) {} + std::error_code code; +}; + +template +class WinFutureState { +private: + mutable CriticalSection mutex; + WindowsCond condition; + std::variant result; + bool retrieved = false; + bool ready = false; + +public: + bool is_ready() const noexcept { + std::unique_lock lock(mutex); + return ready; + } + + bool already_retrieved() const noexcept { + std::unique_lock lock(mutex); + return retrieved; + } + + void wait() { + std::unique_lock lock(mutex); + condition.wait(lock, [this]() { return ready; }); + } + + template + WinFutureStatus + wait_for(const std::chrono::duration &timeout_duration) const { + std::unique_lock lock(mutex); + // deferred function not support yet + if (condition.wait_for(lock, timeout_duration, + [this]() { return ready; })) { + return WinFutureStatus::ready; + } + return WinFutureStatus::timeout; + } + + virtual T &get_value() { + std::unique_lock lock(mutex); + if (retrieved) { + throw WinFutureError(WinFutureErrc::future_already_retrieved); + } + if (auto eptr = std::get_if(&result)) { + std::rethrow_exception(*eptr); + } + retrieved = true; + condition.wait(lock, [this]() { return ready; }); + if (auto eptr = std::get_if(&result)) { + std::rethrow_exception(*eptr); + } + return *std::get_if(&result); + } + + void set_value(const T &value) { + std::unique_lock lock(mutex); + if (!std::holds_alternative(&result)) { + throw WinFutureError(WinFutureErrc::promise_already_satisfied); + } + result.template emplace(value); + ready = true; + condition.notify_all(); + } + + void set_value(T &&value) { + std::unique_lock lock(mutex); + if (!std::holds_alternative(result)) { + throw WinFutureError(WinFutureErrc::promise_already_satisfied); + } + result.template emplace(std::move(value)); + ready = true; + condition.notify_all(); + } + + void set_exception(std::exception_ptr eptr) { + std::unique_lock lock(mutex); + if (!std::holds_alternative(result)) { + throw WinFutureError(WinFutureErrc::promise_already_satisfied); + } + result.template emplace(eptr); + ready = true; + condition.notify_all(); + } +}; + +template +class WinFutureStateManager { +public: + WinFutureStateManager() = default; + WinFutureStateManager(std::shared_ptr> new_state) + : state(std::move(new_state)) {} + WinFutureStateManager(const WinFutureStateManager &) = default; + WinFutureStateManager &operator=(const WinFutureStateManager &) = default; + WinFutureStateManager(WinFutureStateManager &&) = default; + WinFutureStateManager &operator=(WinFutureStateManager &&) = default; + + [[nodiscard]] bool valid() const noexcept { return static_cast(state); } + + void wait() const { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->wait(); + } + + template + WinFutureStatus + wait_for(const std::chrono::duration &timeout_duration) const { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + return state->wait_for(timeout_duration); + } + + T &get_value() const { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + return state->get_value(); + } + + void set_value(const T &value) { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->set_value(value); + } + + void set_value(T &&value) { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->set_value(std::move(value)); + } + + void set_exception(std::exception_ptr eptr) { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->set_exception(eptr); + } + +private: + std::shared_ptr> state; +}; + +template +class WinFuture : public WinFutureStateManager { + using Base = WinFutureStateManager; + static_assert(!std::is_array_v && std::is_object_v && + std::is_destructible_v, + "T in future must meet the Cpp17Destructible requirements " + "(N4878 [futures.unique.future]/4)."); + +public: + WinFuture() noexcept = default; + WinFuture(WinFuture &&) noexcept = default; + WinFuture &operator=(WinFuture &&) noexcept = default; + WinFuture(const WinFuture &) noexcept = delete; + WinFuture &operator=(const WinFuture &) noexcept = delete; + + WinFuture(const Base &base, std::monostate) : Base(base) {} + ~WinFuture() noexcept = default; + T get() { + WinFuture local(std::move(*this)); + return std::move(local.get_value()); + } + +private: + using Base::get_value; + using Base::set_exception; + using Base::set_value; +}; + +template +class WinFuture : public WinFutureStateManager { + using Base = WinFutureStateManager; + +public: + WinFuture() noexcept = default; + WinFuture(WinFuture &&) noexcept = default; + WinFuture &operator=(WinFuture &&) noexcept = default; + WinFuture(const WinFuture &) noexcept = delete; + WinFuture &operator=(const WinFuture &) noexcept = delete; + + WinFuture(const Base &base, std::monostate) : Base(base) {} + ~WinFuture() noexcept = default; + T &get() { + WinFuture local(std::move(*this)); + return *local.get_value(); + } + +private: + using Base::get_value; + using Base::set_exception; + using Base::set_value; +}; + +template <> +class WinFuture : public WinFutureStateManager { + using Base = WinFutureStateManager; + +public: + WinFuture() noexcept = default; + WinFuture(WinFuture &&) noexcept = default; + WinFuture &operator=(WinFuture &&) noexcept = default; + WinFuture(const WinFuture &) noexcept = delete; + WinFuture &operator=(const WinFuture &) noexcept = delete; + + WinFuture(const Base &base, std::monostate) : Base(base) {} + ~WinFuture() noexcept = default; + void get() { + WinFuture local(std::move(*this)); + local.get_value(); + } + +private: + using Base::get_value; + using Base::set_exception; + using Base::set_value; +}; + +template +class WinPromiseBase { +public: + WinPromiseBase(std::shared_ptr> new_state) + : state(std::move(new_state)) {} + WinPromiseBase(WinPromiseBase &&) = default; + WinPromiseBase &operator=(WinPromiseBase &&) = default; + WinPromiseBase(const WinPromiseBase &) = delete; + WinPromiseBase &operator=(const WinPromiseBase &) = delete; + + WinFutureStateManager &get_state_for_set() { + if (!state.valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + return state; + } + + WinFutureStateManager &get_state_for_future() { + if (!state.valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + if (future_retrieved) { + throw WinFutureError(WinFutureErrc::future_already_retrieved); + } + future_retrieved = true; + return state; + } + +private: + WinFutureStateManager state; + bool future_retrieved = false; +}; + +template +class WinPromise { +public: + WinPromise() : base(std::make_shared>()) {} + WinPromise(WinPromise &&) = default; + WinPromise(const WinPromise &) = delete; + ~WinPromise() noexcept {} + [[nodiscard]] WinFuture get_future() { + return WinFuture(base.get_state_for_future(), std::monostate()); + } + void set_value(const T &value) { base.get_state_for_set().set_value(value); } + void set_value(T &&value) { + base.get_state_for_set().set_value(std::forward(value)); + } + void set_exception(std::exception_ptr eptr) { + base.get_state_for_set().set_exception(eptr); + } + +private: + WinPromiseBase base; +}; + +template +class WinPromise { +public: + WinPromise() : base(std::make_shared>()) {} + WinPromise(WinPromise &&) = default; + WinPromise(const WinPromise &) = delete; + ~WinPromise() noexcept {} + [[nodiscard]] WinFuture get_future() { + return WinFuture(base.get_state_for_future(), std::monostate()); + } + void set_value(T &value) { + base.get_state_for_set().set_value(std::addressof(value)); + } + void set_exception(std::exception_ptr eptr) { + base.get_state_for_set().set_exception(eptr); + } + +private: + WinPromiseBase base; +}; + +template <> +class WinPromise { +public: + WinPromise() : base(std::make_shared>()) {} + WinPromise(WinPromise &&) = default; + WinPromise(const WinPromise &) = delete; + ~WinPromise() noexcept {} + [[nodiscard]] WinFuture get_future() { + return WinFuture(base.get_state_for_future(), std::monostate()); + } + void set_value() { base.get_state_for_set().set_value(0); } + void set_exception(std::exception_ptr eptr) { + base.get_state_for_set().set_exception(eptr); + } + +private: + WinPromiseBase base; +}; + +#endif From 22b840c2f18a225aa706b9d3fcce2f7b0f319975 Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Thu, 4 Mar 2021 18:37:22 +0100 Subject: [PATCH 14/24] win32/Com: use if with init-statement --- src/win32/Com.hxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/win32/Com.hxx b/src/win32/Com.hxx index ded264ee2..0c3c400fb 100644 --- a/src/win32/Com.hxx +++ b/src/win32/Com.hxx @@ -29,8 +29,8 @@ class COM { public: COM() { - HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (FAILED(result)) { + if (HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + FAILED(result)) { throw FormatHResultError(result, "Unable to initialize COM"); } } From 0cccdcf9b215a41f4f600207f326e93b3c11fdfe Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:19:36 +0800 Subject: [PATCH 15/24] src/win32: Add support for COINIT_APARTMENTTHREADED --- src/win32/Com.hxx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/win32/Com.hxx b/src/win32/Com.hxx index 0c3c400fb..a2aa062a1 100644 --- a/src/win32/Com.hxx +++ b/src/win32/Com.hxx @@ -31,7 +31,17 @@ public: COM() { if (HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); FAILED(result)) { - throw FormatHResultError(result, "Unable to initialize COM"); + throw FormatHResultError( + result, + "Unable to initialize COM with COINIT_MULTITHREADED"); + } + } + COM(bool) { + if (HRESULT result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + FAILED(result)) { + throw FormatHResultError( + result, + "Unable to initialize COM with COINIT_APARTMENTTHREADED"); } } ~COM() noexcept { CoUninitialize(); } From 5103eb303977ea79d8f7b687272737adf9ffe237 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 4 Mar 2021 18:42:51 +0100 Subject: [PATCH 16/24] meson.build: compile Win32Main.cxx only on Windows --- meson.build | 7 ++++++- src/win32/Win32Main.cxx | 5 ----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index 2dbbc9b4a..0b0c78921 100644 --- a/meson.build +++ b/meson.build @@ -272,7 +272,6 @@ sources = [ 'src/LogInit.cxx', 'src/ls.cxx', 'src/Instance.cxx', - 'src/win32/Win32Main.cxx', 'src/MusicBuffer.cxx', 'src/MusicPipe.cxx', 'src/MusicChunk.cxx', @@ -320,6 +319,12 @@ sources = [ 'src/PlaylistFile.cxx', ] +if is_windows + sources += [ + 'src/win32/Win32Main.cxx', + ] +endif + if not is_android sources += [ 'src/CommandLine.cxx', diff --git a/src/win32/Win32Main.cxx b/src/win32/Win32Main.cxx index f3b356bc0..81a2b274f 100644 --- a/src/win32/Win32Main.cxx +++ b/src/win32/Win32Main.cxx @@ -18,9 +18,6 @@ */ #include "Main.hxx" - -#ifdef _WIN32 - #include "util/Compiler.h" #include "Instance.hxx" #include "system/FatalError.hxx" @@ -155,5 +152,3 @@ void win32_app_stopping() else running.store(false); } - -#endif From b1d7567226ccb374abfb757031fa1972339cbe1a Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:14:51 +0800 Subject: [PATCH 17/24] win32: Add ComWorker to run all COM function on same thread --- meson.build | 1 + src/win32/ComWorker.cxx | 49 ++++++++++++++++++ src/win32/ComWorker.hxx | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/win32/ComWorker.cxx create mode 100644 src/win32/ComWorker.hxx diff --git a/meson.build b/meson.build index 0b0c78921..d72da267e 100644 --- a/meson.build +++ b/meson.build @@ -322,6 +322,7 @@ sources = [ if is_windows sources += [ 'src/win32/Win32Main.cxx', + 'src/win32/ComWorker.cxx', ] endif diff --git a/src/win32/ComWorker.cxx b/src/win32/ComWorker.cxx new file mode 100644 index 000000000..d708dffbe --- /dev/null +++ b/src/win32/ComWorker.cxx @@ -0,0 +1,49 @@ +/* + * Copyright 2020 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * 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 + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#include "ComWorker.hxx" +#include "Log.hxx" +#include "thread/Name.hxx" +#include "util/Domain.hxx" +#include "win32/Com.hxx" + +namespace { +static constexpr Domain com_worker_domain("com_worker"); +} + +Mutex COMWorker::mutex; +unsigned int COMWorker::reference_count = 0; +std::optional COMWorker::thread; + +void COMWorker::COMWorkerThread::Work() noexcept { + FormatDebug(com_worker_domain, "Working thread started"); + SetThreadName("COM Worker"); + COM com{true}; + while (true) { + if (!running_flag.test_and_set()) { + FormatDebug(com_worker_domain, "Working thread ended"); + return; + } + while (!spsc_buffer.empty()) { + std::function function; + spsc_buffer.pop(function); + function(); + } + event.Wait(200); + } +} diff --git a/src/win32/ComWorker.hxx b/src/win32/ComWorker.hxx new file mode 100644 index 000000000..cd3bd5db6 --- /dev/null +++ b/src/win32/ComWorker.hxx @@ -0,0 +1,110 @@ +/* + * Copyright 2020 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * 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 + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_WIN32_COM_WORKER_HXX +#define MPD_WIN32_COM_WORKER_HXX + +#include +#include +#include +#include + +#include "thread/Future.hxx" +#include "thread/Mutex.hxx" +#include "thread/Thread.hxx" +#include "win32/WinEvent.hxx" +#include +#include + +// Worker thread for all COM operation +class COMWorker { +private: + class COMWorkerThread : public Thread { + public: + COMWorkerThread() : Thread{BIND_THIS_METHOD(Work)} {} + + private: + friend class COMWorker; + void Work() noexcept; + void Finish() noexcept { + running_flag.clear(); + event.Set(); + } + void Push(const std::function &function) { + spsc_buffer.push(function); + event.Set(); + } + + boost::lockfree::spsc_queue> spsc_buffer{32}; + std::atomic_flag running_flag = true; + WinEvent event{}; + }; + +public: + static void Aquire() { + std::unique_lock locker(mutex); + if (reference_count == 0) { + thread.emplace(); + thread->Start(); + } + ++reference_count; + } + static void Release() noexcept { + std::unique_lock locker(mutex); + --reference_count; + if (reference_count == 0) { + thread->Finish(); + thread->Join(); + thread.reset(); + } + } + + template + static auto Async(Function &&function, Args &&...args) { + using R = std::invoke_result_t, + std::decay_t...>; + auto promise = std::make_shared>(); + auto future = promise->get_future(); + thread->Push([function = std::forward(function), + args = std::make_tuple(std::forward(args)...), + promise = std::move(promise)]() mutable { + try { + if constexpr (std::is_void_v) { + std::apply(std::forward(function), + std::move(args)); + promise->set_value(); + } else { + promise->set_value(std::apply( + std::forward(function), + std::move(args))); + } + } catch (...) { + promise->set_exception(std::current_exception()); + } + }); + return future; + } + +private: + static Mutex mutex; + static unsigned int reference_count; + static std::optional thread; +}; + +#endif From 29747377467168ac8928081d3d37aa5016acd778 Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:14:51 +0800 Subject: [PATCH 18/24] src/win32: Add ComWorker to run all COM function on same thread --- src/mixer/plugins/WasapiMixerPlugin.cxx | 149 ++++++++++++---------- src/output/plugins/WasapiOutputPlugin.cxx | 128 ++++++++++--------- 2 files changed, 149 insertions(+), 128 deletions(-) diff --git a/src/mixer/plugins/WasapiMixerPlugin.cxx b/src/mixer/plugins/WasapiMixerPlugin.cxx index d45af3dfa..bc2604633 100644 --- a/src/mixer/plugins/WasapiMixerPlugin.cxx +++ b/src/mixer/plugins/WasapiMixerPlugin.cxx @@ -19,7 +19,7 @@ #include "mixer/MixerInternal.hxx" #include "output/plugins/WasapiOutputPlugin.hxx" -#include "win32/Com.hxx" +#include "win32/ComWorker.hxx" #include "win32/HResult.hxx" #include @@ -28,92 +28,103 @@ class WasapiMixer final : public Mixer { WasapiOutput &output; - std::optional com; public: WasapiMixer(WasapiOutput &_output, MixerListener &_listener) : Mixer(wasapi_mixer_plugin, _listener), output(_output) {} - void Open() override { com.emplace(); } + void Open() override {} - void Close() noexcept override { com.reset(); } + void Close() noexcept override {} int GetVolume() override { - HRESULT result; - float volume_level; + auto future = COMWorker::Async([&]() -> int { + HRESULT result; + float volume_level; - if (wasapi_is_exclusive(output)) { - ComPtr endpoint_volume; - result = wasapi_output_get_device(output)->Activate( - __uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, - endpoint_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get device endpoint volume"); + if (wasapi_is_exclusive(output)) { + ComPtr endpoint_volume; + result = wasapi_output_get_device(output)->Activate( + __uuidof(IAudioEndpointVolume), CLSCTX_ALL, + nullptr, endpoint_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError(result, + "Unable to get device " + "endpoint volume"); + } + + result = endpoint_volume->GetMasterVolumeLevelScalar( + &volume_level); + if (FAILED(result)) { + throw FormatHResultError(result, + "Unable to get master " + "volume level"); + } + } else { + ComPtr session_volume; + result = wasapi_output_get_client(output)->GetService( + __uuidof(ISimpleAudioVolume), + session_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError(result, + "Unable to get client " + "session volume"); + } + + result = session_volume->GetMasterVolume(&volume_level); + if (FAILED(result)) { + throw FormatHResultError( + result, "Unable to get master volume"); + } } - result = endpoint_volume->GetMasterVolumeLevelScalar( - &volume_level); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get master volume level"); - } - } else { - ComPtr session_volume; - result = wasapi_output_get_client(output)->GetService( - __uuidof(ISimpleAudioVolume), - session_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get client session volume"); - } - - result = session_volume->GetMasterVolume(&volume_level); - if (FAILED(result)) { - throw FormatHResultError(result, - "Unable to get master volume"); - } - } - - return std::lround(volume_level * 100.0f); + return std::lround(volume_level * 100.0f); + }); + return future.get(); } void SetVolume(unsigned volume) override { - HRESULT result; - const float volume_level = volume / 100.0f; + COMWorker::Async([&]() { + HRESULT result; + const float volume_level = volume / 100.0f; - if (wasapi_is_exclusive(output)) { - ComPtr endpoint_volume; - result = wasapi_output_get_device(output)->Activate( - __uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, - endpoint_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get device endpoint volume"); - } + if (wasapi_is_exclusive(output)) { + ComPtr endpoint_volume; + result = wasapi_output_get_device(output)->Activate( + __uuidof(IAudioEndpointVolume), CLSCTX_ALL, + nullptr, endpoint_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to get device endpoint volume"); + } - result = endpoint_volume->SetMasterVolumeLevelScalar(volume_level, - nullptr); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to set master volume level"); - } - } else { - ComPtr session_volume; - result = wasapi_output_get_client(output)->GetService( - __uuidof(ISimpleAudioVolume), - session_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get client session volume"); - } + result = endpoint_volume->SetMasterVolumeLevelScalar( + volume_level, nullptr); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to set master volume level"); + } + } else { + ComPtr session_volume; + result = wasapi_output_get_client(output)->GetService( + __uuidof(ISimpleAudioVolume), + session_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to get client session volume"); + } - result = session_volume->SetMasterVolume(volume_level, nullptr); - if (FAILED(result)) { - throw FormatHResultError(result, - "Unable to set master volume"); + result = session_volume->SetMasterVolume(volume_level, + nullptr); + if (FAILED(result)) { + throw FormatHResultError( + result, "Unable to set master volume"); + } } - } + }).get(); } }; diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index d9d16e97e..8c52bf15f 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -32,6 +32,7 @@ #include "util/ScopeExit.hxx" #include "win32/Com.hxx" #include "win32/ComHeapPtr.hxx" +#include "win32/ComWorker.hxx" #include "win32/HResult.hxx" #include "win32/WinEvent.hxx" @@ -144,7 +145,6 @@ public: private: std::shared_ptr event; - std::optional com; ComPtr client; ComPtr render_client; const UINT32 frame_size; @@ -174,9 +174,17 @@ class WasapiOutput final : public AudioOutput { public: static AudioOutput *Create(EventLoop &, const ConfigBlock &block); WasapiOutput(const ConfigBlock &block); - void Enable() override; - void Disable() noexcept override; - void Open(AudioFormat &audio_format) override; + void Enable() override { + COMWorker::Aquire(); + COMWorker::Async([&]() { OpenDevice(); }).get(); + } + void Disable() noexcept override { + COMWorker::Async([&]() { DoDisable(); }).get(); + COMWorker::Release(); + } + void Open(AudioFormat &audio_format) override { + COMWorker::Async([&]() { DoOpen(audio_format); }).get(); + } void Close() noexcept override; std::chrono::steady_clock::duration Delay() const noexcept override; size_t Play(const void *chunk, size_t size) override; @@ -196,7 +204,6 @@ private: std::string device_config; std::vector> device_desc; std::shared_ptr event; - std::optional com; ComPtr enumerator; ComPtr device; ComPtr client; @@ -209,6 +216,10 @@ private: friend IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept; friend IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept; + void DoDisable() noexcept; + void DoOpen(AudioFormat &audio_format); + + void OpenDevice(); void FindExclusiveFormatSupported(AudioFormat &audio_format); void FindSharedFormatSupported(AudioFormat &audio_format); void EnumerateDevices(); @@ -234,15 +245,7 @@ IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept { void WasapiOutputThread::Work() noexcept { SetThreadName("Wasapi Output Worker"); FormatDebug(wasapi_output_domain, "Working thread started"); - try { - com.emplace(); - } catch (...) { - std::unique_lock lock(error.mutex); - error.error_ptr = std::current_exception(); - error.cond.wait(lock); - assert(error.error_ptr == nullptr); - return; - } + COM com{true}; while (true) { try { event->Wait(INFINITE); @@ -316,41 +319,8 @@ WasapiOutput::WasapiOutput(const ConfigBlock &block) enumerate_devices(block.GetBlockValue("enumerate", false)), device_config(block.GetBlockValue("device", "")) {} -void WasapiOutput::Enable() { - com.emplace(); - event = std::make_shared(); - enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, - CLSCTX_INPROC_SERVER); - - device_desc.clear(); - device.reset(); - - if (enumerate_devices && SafeTry([this]() { EnumerateDevices(); })) { - for (const auto &desc : device_desc) { - FormatNotice(wasapi_output_domain, "Device \"%u\" \"%s\"", - desc.first, desc.second.c_str()); - } - } - - unsigned int id = kErrorId; - if (!device_config.empty()) { - if (!SafeSilenceTry([this, &id]() { id = std::stoul(device_config); })) { - id = SearchDevice(device_config); - } - } - - if (id != kErrorId) { - SafeTry([this, id]() { GetDevice(id); }); - } - - if (!device) { - GetDefaultDevice(); - } - - device_desc.clear(); -} - -void WasapiOutput::Disable() noexcept { +/// run inside COMWorkerThread +void WasapiOutput::DoDisable() noexcept { if (thread) { try { thread->Finish(); @@ -369,7 +339,8 @@ void WasapiOutput::Disable() noexcept { event.reset(); } -void WasapiOutput::Open(AudioFormat &audio_format) { +/// run inside COMWorkerThread +void WasapiOutput::DoOpen(AudioFormat &audio_format) { if (audio_format.channels == 0) { throw FormatInvalidArgument("channels should > 0"); } @@ -497,9 +468,11 @@ void WasapiOutput::Close() noexcept { Pause(); thread->Finish(); thread->Join(); - thread.reset(); + COMWorker::Async([&]() { + thread.reset(); + client.reset(); + }).get(); spsc_buffer.reset(); - client.reset(); } std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { @@ -534,13 +507,14 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) { is_started = true; thread->Play(); - - HRESULT result; - result = client->Start(); - if (FAILED(result)) { - throw FormatHResultError(result, - "Failed to start client"); - } + COMWorker::Async([&]() { + HRESULT result; + result = client->Start(); + if (FAILED(result)) { + throw FormatHResultError( + result, "Failed to start client"); + } + }).wait(); } thread->CheckException(); @@ -575,6 +549,37 @@ bool WasapiOutput::Pause() { return true; } +/// run inside COMWorkerThread +void WasapiOutput::OpenDevice() { + enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, + CLSCTX_INPROC_SERVER); + + if (enumerate_devices && SafeTry([this]() { EnumerateDevices(); })) { + for (const auto &desc : device_desc) { + FormatNotice(wasapi_output_domain, "Device \"%u\" \"%s\"", + desc.first, desc.second.c_str()); + } + } + + unsigned int id = kErrorId; + if (!device_config.empty()) { + if (!SafeSilenceTry([this, &id]() { id = std::stoul(device_config); })) { + id = SearchDevice(device_config); + } + } + + if (id != kErrorId) { + SafeTry([this, id]() { GetDevice(id); }); + } + + if (!device) { + GetDefaultDevice(); + } + + device_desc.clear(); +} + +/// run inside COMWorkerThread void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) { SetFormat(device_format, audio_format); @@ -641,6 +646,7 @@ void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) { } while (true); } +/// run inside COMWorkerThread void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { HRESULT result; ComHeapPtr mixer_format; @@ -724,6 +730,7 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { } } +/// run inside COMWorkerThread void WasapiOutput::EnumerateDevices() { if (!device_desc.empty()) { return; @@ -776,6 +783,7 @@ void WasapiOutput::EnumerateDevices() { } } +/// run inside COMWorkerThread void WasapiOutput::GetDevice(unsigned int index) { HRESULT result; @@ -792,6 +800,7 @@ void WasapiOutput::GetDevice(unsigned int index) { } } +/// run inside COMWorkerThread unsigned int WasapiOutput::SearchDevice(std::string_view name) { if (!SafeTry([this]() { EnumerateDevices(); })) { return kErrorId; @@ -809,6 +818,7 @@ unsigned int WasapiOutput::SearchDevice(std::string_view name) { return iter->first; } +/// run inside COMWorkerThread void WasapiOutput::GetDefaultDevice() { HRESULT result; result = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, From db7caa2dac03ec114b99ec7d49d10d6f81b73115 Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:33:40 +0800 Subject: [PATCH 19/24] src/output: Move event and spsc_queue into thread object --- src/output/plugins/WasapiOutputPlugin.cxx | 72 +++++++++++------------ 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index 8c52bf15f..30eeb2c75 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -118,15 +118,14 @@ inline constexpr const unsigned int kErrorId = -1; class WasapiOutputThread : public Thread { public: enum class Status : uint32_t { FINISH, PLAY, PAUSE }; - WasapiOutputThread(std::shared_ptr _event, ComPtr _client, + WasapiOutputThread(IAudioClient *_client, ComPtr &&_render_client, const UINT32 _frame_size, const UINT32 _buffer_size_in_frames, - bool _is_exclusive, - boost::lockfree::spsc_queue &_spsc_buffer) - : Thread(BIND_THIS_METHOD(Work)), event(std::move(_event)), - client(std::move(_client)), render_client(std::move(_render_client)), - frame_size(_frame_size), buffer_size_in_frames(_buffer_size_in_frames), - is_exclusive(_is_exclusive), spsc_buffer(_spsc_buffer) {} + bool _is_exclusive) + : Thread(BIND_THIS_METHOD(Work)), client(_client), + render_client(std::move(_render_client)), frame_size(_frame_size), + buffer_size_in_frames(_buffer_size_in_frames), is_exclusive(_is_exclusive), + spsc_buffer(_buffer_size_in_frames * 4 * _frame_size) {} void Finish() noexcept { return SetStatus(Status::FINISH); } void Play() noexcept { return SetStatus(Status::PLAY); } void Pause() noexcept { return SetStatus(Status::PAUSE); } @@ -144,13 +143,13 @@ public: } private: - std::shared_ptr event; - ComPtr client; + friend class WasapiOutput; + WinEvent event; + IAudioClient *client; ComPtr render_client; const UINT32 frame_size; const UINT32 buffer_size_in_frames; bool is_exclusive; - boost::lockfree::spsc_queue &spsc_buffer; alignas(BOOST_LOCKFREE_CACHELINE_BYTES) std::atomic status = Status::PAUSE; alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct { @@ -162,10 +161,11 @@ private: Cond cond; std::exception_ptr error_ptr = nullptr; } error{}; + boost::lockfree::spsc_queue spsc_buffer; void SetStatus(Status s) noexcept { status.store(s); - event->Set(); + event.Set(); } void Work() noexcept; }; @@ -203,13 +203,11 @@ private: bool enumerate_devices; std::string device_config; std::vector> device_desc; - std::shared_ptr event; ComPtr enumerator; ComPtr device; ComPtr client; WAVEFORMATEXTENSIBLE device_format; - std::unique_ptr thread; - std::unique_ptr> spsc_buffer; + std::optional thread; std::size_t watermark; friend bool wasapi_is_exclusive(WasapiOutput &output) noexcept; @@ -248,7 +246,7 @@ void WasapiOutputThread::Work() noexcept { COM com{true}; while (true) { try { - event->Wait(INFINITE); + event.Wait(INFINITE); Status current_state = status.load(); if (current_state == Status::FINISH) { @@ -330,13 +328,10 @@ void WasapiOutput::DoDisable() noexcept { err.what()); } thread.reset(); - spsc_buffer.reset(); client.reset(); } device.reset(); enumerator.reset(); - com.reset(); - event.reset(); } /// run inside COMWorkerThread @@ -442,11 +437,6 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { throw FormatHResultError(result, "Unable to get new render client"); } - result = client->SetEventHandle(event->handle()); - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to set event handler"); - } - UINT32 buffer_size_in_frames; result = client->GetBufferSize(&buffer_size_in_frames); if (FAILED(result)) { @@ -455,11 +445,14 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { } watermark = buffer_size_in_frames * 3 * FrameSize(); - spsc_buffer = std::make_unique>( - buffer_size_in_frames * 4 * FrameSize()); - thread = std::make_unique( - event, client, std::move(render_client), FrameSize(), - buffer_size_in_frames, is_exclusive, *spsc_buffer); + thread.emplace(client.get(), std::move(render_client), FrameSize(), + buffer_size_in_frames, is_exclusive); + + result = client->SetEventHandle(thread->event.handle()); + if (FAILED(result)) { + throw FormatHResultError(result, "Unable to set event handler"); + } + thread->Start(); } @@ -472,7 +465,6 @@ void WasapiOutput::Close() noexcept { thread.reset(); client.reset(); }).get(); - spsc_buffer.reset(); } std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { @@ -480,7 +472,9 @@ std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { return std::chrono::steady_clock::duration::zero(); } - const size_t data_size = spsc_buffer->read_available(); + assert(thread); + + const size_t data_size = thread->spsc_buffer.read_available(); const size_t delay_size = std::max(data_size, watermark) - watermark; using s = std::chrono::seconds; @@ -490,13 +484,11 @@ std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { } size_t WasapiOutput::Play(const void *chunk, size_t size) { - if (!client || !thread) { - return 0; - } + assert(thread); do { const size_t consumed_size = - spsc_buffer->push(static_cast(chunk), size); + thread->spsc_buffer.push(static_cast(chunk), size); if (consumed_size == 0) { assert(is_started); thread->WaitWrite(); @@ -523,11 +515,6 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) { } while (true); } -void WasapiOutput::Drain() { - spsc_buffer->consume_all([](auto &&) {}); - thread->CheckException(); -} - bool WasapiOutput::Pause() { if (!client || !thread) { return false; @@ -549,6 +536,13 @@ bool WasapiOutput::Pause() { return true; } +void WasapiOutput::Drain() { + assert(thread); + + thread->spsc_buffer.consume_all([](auto &&) {}); + thread->CheckException(); +} + /// run inside COMWorkerThread void WasapiOutput::OpenDevice() { enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, From 844dbd2ec58109fddfb972e8ce0f03816b5e2547 Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:41:44 +0800 Subject: [PATCH 20/24] src/output: Use WinEvent for as a condition_variable without lock --- src/output/plugins/WasapiOutputPlugin.cxx | 37 +++++++++-------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index 30eeb2c75..b2833b2d9 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -129,15 +129,11 @@ public: void Finish() noexcept { return SetStatus(Status::FINISH); } void Play() noexcept { return SetStatus(Status::PLAY); } void Pause() noexcept { return SetStatus(Status::PAUSE); } - void WaitWrite() noexcept { - std::unique_lock lock(write.mutex); - write.cond.wait(lock); - } + void WaitDataPoped() noexcept { data_poped.Wait(200); } void CheckException() { - std::unique_lock lock(error.mutex); - if (error.error_ptr) { - std::exception_ptr err = std::exchange(error.error_ptr, nullptr); - error.cond.notify_all(); + if (error.occur.load()) { + auto err = std::exchange(error.ptr, nullptr); + error.thrown.Set(); std::rethrow_exception(err); } } @@ -145,6 +141,7 @@ public: private: friend class WasapiOutput; WinEvent event; + WinEvent data_poped; IAudioClient *client; ComPtr render_client; const UINT32 frame_size; @@ -153,14 +150,10 @@ private: alignas(BOOST_LOCKFREE_CACHELINE_BYTES) std::atomic status = Status::PAUSE; alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct { - Mutex mutex; - Cond cond; - } write{}; - alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct { - Mutex mutex; - Cond cond; - std::exception_ptr error_ptr = nullptr; - } error{}; + std::atomic_bool occur = false; + std::exception_ptr ptr = nullptr; + WinEvent thrown; + } error; boost::lockfree::spsc_queue spsc_buffer; void SetStatus(Status s) noexcept { @@ -255,8 +248,6 @@ void WasapiOutputThread::Work() noexcept { return; } - AtScopeExit(&) { write.cond.notify_all(); }; - HRESULT result; UINT32 data_in_frames; result = client->GetCurrentPadding(&data_in_frames); @@ -293,16 +284,16 @@ void WasapiOutputThread::Work() noexcept { new_data_size = spsc_buffer.pop(data, write_size); std::fill_n(data + new_data_size, write_size - new_data_size, 0); + data_poped.Set(); } else { mode = AUDCLNT_BUFFERFLAGS_SILENT; FormatDebug(wasapi_output_domain, "Working thread paused"); } } catch (...) { - std::unique_lock lock(error.mutex); - error.error_ptr = std::current_exception(); - error.cond.wait(lock); - assert(error.error_ptr == nullptr); + error.ptr = std::current_exception(); + error.occur.store(true); + error.thrown.Wait(INFINITE); } } } @@ -491,7 +482,7 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) { thread->spsc_buffer.push(static_cast(chunk), size); if (consumed_size == 0) { assert(is_started); - thread->WaitWrite(); + thread->WaitDataPoped(); continue; } From c46f97454af67014caf9444c6f8d68b5fdd4424b Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:57:05 +0800 Subject: [PATCH 21/24] src/output: Reopen device on error --- src/output/plugins/WasapiOutputPlugin.cxx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index b2833b2d9..b17c4ab7a 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -333,6 +333,15 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { client.reset(); + DWORD state; + if (HRESULT result = device->GetState(&state); FAILED(result)) { + throw FormatHResultError(result, "Unable to get device status"); + } + if (state != DEVICE_STATE_ACTIVE) { + device.reset(); + OpenDevice(); + } + HRESULT result; result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, client.AddressCast()); From 010f65a1d60ab76bfe0f3fb41475d8799784f9fe Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:57:24 +0800 Subject: [PATCH 22/24] src/output: Add Interrupt interface --- src/output/plugins/WasapiOutputPlugin.cxx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index b17c4ab7a..a9bfae83e 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -22,6 +22,7 @@ #include "WasapiOutputPlugin.hxx" #include "lib/icu/Win32.hxx" #include "mixer/MixerList.hxx" +#include "output/Error.hxx" #include "thread/Cond.hxx" #include "thread/Mutex.hxx" #include "thread/Name.hxx" @@ -30,6 +31,7 @@ #include "util/Domain.hxx" #include "util/RuntimeError.hxx" #include "util/ScopeExit.hxx" +#include "util/StringBuffer.hxx" #include "win32/Com.hxx" #include "win32/ComHeapPtr.hxx" #include "win32/ComWorker.hxx" @@ -129,7 +131,7 @@ public: void Finish() noexcept { return SetStatus(Status::FINISH); } void Play() noexcept { return SetStatus(Status::PLAY); } void Pause() noexcept { return SetStatus(Status::PAUSE); } - void WaitDataPoped() noexcept { data_poped.Wait(200); } + void WaitDataPoped() noexcept { data_poped.Wait(INFINITE); } void CheckException() { if (error.occur.load()) { auto err = std::exchange(error.ptr, nullptr); @@ -183,6 +185,7 @@ public: size_t Play(const void *chunk, size_t size) override; void Drain() override; bool Pause() override; + void Interrupt() noexcept override; constexpr bool Exclusive() const { return is_exclusive; } constexpr size_t FrameSize() const { return device_format.Format.nBlockAlign; } @@ -191,6 +194,7 @@ public: } private: + std::atomic_flag not_interrupted = true; bool is_started = false; bool is_exclusive; bool enumerate_devices; @@ -486,12 +490,17 @@ std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { size_t WasapiOutput::Play(const void *chunk, size_t size) { assert(thread); + not_interrupted.test_and_set(); + do { const size_t consumed_size = thread->spsc_buffer.push(static_cast(chunk), size); if (consumed_size == 0) { assert(is_started); thread->WaitDataPoped(); + if (!not_interrupted.test_and_set()) { + throw AudioOutputInterrupted{}; + } continue; } @@ -536,6 +545,13 @@ bool WasapiOutput::Pause() { return true; } +void WasapiOutput::Interrupt() noexcept { + if (thread) { + not_interrupted.clear(); + thread->data_poped.Set(); + } +} + void WasapiOutput::Drain() { assert(thread); From 6f77af20d0653dd747530078d0c5e7f1928f102b Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 08:05:41 +0800 Subject: [PATCH 23/24] src/output: Set fallback setting for DSD --- src/output/plugins/WasapiOutputPlugin.cxx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index a9bfae83e..e160c1f38 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -113,6 +113,13 @@ inline void SetFormat(WAVEFORMATEXTENSIBLE &device_format, } } +#ifdef ENABLE_DSD +void SetDSDFallback(AudioFormat &audio_format) noexcept { + audio_format.format = SampleFormat::FLOAT; + audio_format.sample_rate = 384000; +} +#endif + inline constexpr const unsigned int kErrorId = -1; } // namespace @@ -360,6 +367,12 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { audio_format.channels = 8; } +#ifdef ENABLE_DSD + if (audio_format.format == SampleFormat::DSD) { + SetDSDFallback(audio_format); + } +#endif + if (Exclusive()) { FindExclusiveFormatSupported(audio_format); } else { From da642b289049836f2d89b65726ef1aa5229b5f82 Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Wed, 2 Dec 2020 07:59:33 +0800 Subject: [PATCH 24/24] src/output: add algorithm for finding usable AudioFormat * Use PcmExport for 24bit packed output --- NEWS | 2 + src/output/plugins/WasapiOutputPlugin.cxx | 435 +++++++++++++--------- 2 files changed, 257 insertions(+), 180 deletions(-) diff --git a/NEWS b/NEWS index dbd9cbe1a..4c0e93818 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,8 @@ ver 0.22.7 (not yet released) - ffmpeg: fix build problem with FFmpeg 3.4 * storage - curl: don't use glibc extension +* output + - wasapi: add algorithm for finding usable audio format ver 0.22.6 (2021/02/16) * fix missing tags on songs in queue diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index e160c1f38..9cdf8e00e 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -23,11 +23,13 @@ #include "lib/icu/Win32.hxx" #include "mixer/MixerList.hxx" #include "output/Error.hxx" +#include "pcm/Export.hxx" #include "thread/Cond.hxx" #include "thread/Mutex.hxx" #include "thread/Name.hxx" #include "thread/Thread.hxx" #include "util/AllocatedString.hxx" +#include "util/ConstBuffer.hxx" #include "util/Domain.hxx" #include "util/RuntimeError.hxx" #include "util/ScopeExit.hxx" @@ -94,23 +96,38 @@ inline bool SafeSilenceTry(Functor &&functor) { } } -inline void SetFormat(WAVEFORMATEXTENSIBLE &device_format, - const AudioFormat &audio_format) noexcept { - device_format.dwChannelMask = GetChannelMask(audio_format.channels); - device_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; - device_format.Format.nChannels = audio_format.channels; - device_format.Format.nSamplesPerSec = audio_format.sample_rate; - device_format.Format.nBlockAlign = audio_format.GetFrameSize(); - device_format.Format.nAvgBytesPerSec = - audio_format.sample_rate * audio_format.GetFrameSize(); - device_format.Format.wBitsPerSample = audio_format.GetSampleSize() * 8; - device_format.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); - device_format.Samples.wValidBitsPerSample = audio_format.GetSampleSize() * 8; - if (audio_format.format == SampleFormat::FLOAT) { - device_format.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; +std::vector GetFormats(const AudioFormat &audio_format) noexcept { + std::vector Result; + if (audio_format.format == SampleFormat::S24_P32) { + Result.resize(2); + Result[0].Format.wBitsPerSample = 24; + Result[0].Samples.wValidBitsPerSample = 24; + Result[1].Format.wBitsPerSample = 32; + Result[1].Samples.wValidBitsPerSample = 24; } else { - device_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + Result.resize(1); + Result[0].Format.wBitsPerSample = audio_format.GetSampleSize() * 8; + Result[0].Samples.wValidBitsPerSample = audio_format.GetSampleSize() * 8; } + const DWORD mask = GetChannelMask(audio_format.channels); + const GUID guid = audio_format.format == SampleFormat::FLOAT + ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT + : KSDATAFORMAT_SUBTYPE_PCM; + for (auto &device_format : Result) { + device_format.dwChannelMask = mask; + device_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + device_format.Format.nChannels = audio_format.channels; + device_format.Format.nSamplesPerSec = audio_format.sample_rate; + device_format.Format.cbSize = + sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); + device_format.SubFormat = guid; + device_format.Format.nBlockAlign = device_format.Format.nChannels * + device_format.Format.wBitsPerSample / + 8; + device_format.Format.nAvgBytesPerSec = + audio_format.sample_rate * device_format.Format.nBlockAlign; + } + return Result; } #ifdef ENABLE_DSD @@ -213,6 +230,7 @@ private: WAVEFORMATEXTENSIBLE device_format; std::optional thread; std::size_t watermark; + std::optional pcm_export; friend bool wasapi_is_exclusive(WasapiOutput &output) noexcept; friend IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept; @@ -222,6 +240,7 @@ private: void DoOpen(AudioFormat &audio_format); void OpenDevice(); + bool TryFormatExclusive(const AudioFormat &audio_format); void FindExclusiveFormatSupported(AudioFormat &audio_format); void FindSharedFormatSupported(AudioFormat &audio_format); void EnumerateDevices(); @@ -259,29 +278,28 @@ void WasapiOutputThread::Work() noexcept { return; } - HRESULT result; - UINT32 data_in_frames; - result = client->GetCurrentPadding(&data_in_frames); - if (FAILED(result)) { - throw FormatHResultError(result, - "Failed to get current padding"); - } - UINT32 write_in_frames = buffer_size_in_frames; if (!is_exclusive) { + UINT32 data_in_frames; + if (HRESULT result = + client->GetCurrentPadding(&data_in_frames); + FAILED(result)) { + throw FormatHResultError( + result, "Failed to get current padding"); + } + if (data_in_frames >= buffer_size_in_frames) { continue; } write_in_frames -= data_in_frames; - } else if (data_in_frames >= buffer_size_in_frames * 2) { - continue; } BYTE *data; DWORD mode = 0; - result = render_client->GetBuffer(write_in_frames, &data); - if (FAILED(result)) { + if (HRESULT result = + render_client->GetBuffer(write_in_frames, &data); + FAILED(result)) { throw FormatHResultError(result, "Failed to get buffer"); } @@ -338,10 +356,6 @@ void WasapiOutput::DoDisable() noexcept { /// run inside COMWorkerThread void WasapiOutput::DoOpen(AudioFormat &audio_format) { - if (audio_format.channels == 0) { - throw FormatInvalidArgument("channels should > 0"); - } - client.reset(); DWORD state; @@ -353,16 +367,12 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { OpenDevice(); } - HRESULT result; - result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - client.AddressCast()); - if (FAILED(result)) { + if (HRESULT result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, + client.AddressCast()); + FAILED(result)) { throw FormatHResultError(result, "Unable to activate audio client"); } - if (audio_format.format == SampleFormat::S24_P32) { - audio_format.format = SampleFormat::S32; - } if (audio_format.channels > 8) { audio_format.channels = 8; } @@ -378,6 +388,24 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { } else { FindSharedFormatSupported(audio_format); } + bool require_export = audio_format.format == SampleFormat::S24_P32; + if (require_export) { + PcmExport::Params params; + params.dsd_mode = PcmExport::DsdMode::NONE; + params.shift8 = false; + params.pack24 = false; + if (device_format.Format.wBitsPerSample == 32 && + device_format.Samples.wValidBitsPerSample == 24) { + params.shift8 = true; + } + if (device_format.Format.wBitsPerSample == 24) { + params.pack24 = true; + } + FormatDebug(wasapi_output_domain, "Packing data: shift8=%d pack24=%d", + int(params.shift8), int(params.pack24)); + pcm_export.emplace(); + pcm_export->Open(audio_format.format, audio_format.channels, params); + } using s = std::chrono::seconds; using ms = std::chrono::milliseconds; @@ -385,78 +413,95 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { using hundred_ns = std::chrono::duration>; // The unit in REFERENCE_TIME is hundred nanoseconds - REFERENCE_TIME device_period; - result = client->GetDevicePeriod(&device_period, nullptr); - if (FAILED(result)) { + REFERENCE_TIME default_device_period, min_device_period; + + if (HRESULT result = + client->GetDevicePeriod(&default_device_period, &min_device_period); + FAILED(result)) { throw FormatHResultError(result, "Unable to get device period"); } - FormatDebug(wasapi_output_domain, "Device period: %I64u ns", - size_t(ns(hundred_ns(device_period)).count())); + FormatDebug(wasapi_output_domain, + "Default device period: %I64u ns, Minimum device period: " + "%I64u ns", + ns(hundred_ns(default_device_period)).count(), + ns(hundred_ns(min_device_period)).count()); - REFERENCE_TIME buffer_duration = device_period; - if (!Exclusive()) { + REFERENCE_TIME buffer_duration; + if (Exclusive()) { + buffer_duration = default_device_period; + } else { const REFERENCE_TIME align = hundred_ns(ms(50)).count(); - buffer_duration = (align / device_period) * device_period; + buffer_duration = (align / default_device_period) * default_device_period; } FormatDebug(wasapi_output_domain, "Buffer duration: %I64u ns", size_t(ns(hundred_ns(buffer_duration)).count())); if (Exclusive()) { - result = client->Initialize( - AUDCLNT_SHAREMODE_EXCLUSIVE, - AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - buffer_duration, buffer_duration, - reinterpret_cast(&device_format), nullptr); - if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) { - // https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize - UINT32 buffer_size_in_frames = 0; - result = client->GetBufferSize(&buffer_size_in_frames); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get audio client buffer size"); - } - buffer_duration = std::ceil( - double(buffer_size_in_frames * hundred_ns(s(1)).count()) / - SampleRate()); - FormatDebug(wasapi_output_domain, - "Aligned buffer duration: %I64u ns", - size_t(ns(hundred_ns(buffer_duration)).count())); - client.reset(); - result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, - nullptr, client.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to activate audio client"); - } - result = client->Initialize( - AUDCLNT_SHAREMODE_EXCLUSIVE, - AUDCLNT_STREAMFLAGS_NOPERSIST | + if (HRESULT result = client->Initialize( + AUDCLNT_SHAREMODE_EXCLUSIVE, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buffer_duration, + buffer_duration, + reinterpret_cast(&device_format), nullptr); + FAILED(result)) { + if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) { + // https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize + UINT32 buffer_size_in_frames = 0; + result = client->GetBufferSize(&buffer_size_in_frames); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to get audio client buffer size"); + } + buffer_duration = + std::ceil(double(buffer_size_in_frames * + hundred_ns(s(1)).count()) / + SampleRate()); + FormatDebug( + wasapi_output_domain, + "Aligned buffer duration: %I64u ns", + size_t(ns(hundred_ns(buffer_duration)).count())); + client.reset(); + result = device->Activate(__uuidof(IAudioClient), + CLSCTX_ALL, nullptr, + client.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to activate audio client"); + } + result = client->Initialize( + AUDCLNT_SHAREMODE_EXCLUSIVE, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - buffer_duration, buffer_duration, - reinterpret_cast(&device_format), - nullptr); + buffer_duration, buffer_duration, + reinterpret_cast(&device_format), + nullptr); + } + + if (FAILED(result)) { + throw FormatHResultError( + result, "Unable to initialize audio client"); + } } } else { - result = client->Initialize( - AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - buffer_duration, 0, - reinterpret_cast(&device_format), nullptr); - } - - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to initialize audio client"); + if (HRESULT result = client->Initialize( + AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + buffer_duration, 0, + reinterpret_cast(&device_format), nullptr); + FAILED(result)) { + throw FormatHResultError(result, + "Unable to initialize audio client"); + } } ComPtr render_client; - result = client->GetService(IID_PPV_ARGS(render_client.Address())); - if (FAILED(result)) { + if (HRESULT result = client->GetService(IID_PPV_ARGS(render_client.Address())); + FAILED(result)) { throw FormatHResultError(result, "Unable to get new render client"); } UINT32 buffer_size_in_frames; - result = client->GetBufferSize(&buffer_size_in_frames); - if (FAILED(result)) { + if (HRESULT result = client->GetBufferSize(&buffer_size_in_frames); + FAILED(result)) { throw FormatHResultError(result, "Unable to get audio client buffer size"); } @@ -465,8 +510,8 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { thread.emplace(client.get(), std::move(render_client), FrameSize(), buffer_size_in_frames, is_exclusive); - result = client->SetEventHandle(thread->event.handle()); - if (FAILED(result)) { + if (HRESULT result = client->SetEventHandle(thread->event.handle()); + FAILED(result)) { throw FormatHResultError(result, "Unable to set event handler"); } @@ -474,19 +519,33 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) { } void WasapiOutput::Close() noexcept { - assert(client && thread); - Pause(); + assert(thread); + + try { + COMWorker::Async([&]() { + if (HRESULT result = client->Stop(); FAILED(result)) { + throw FormatHResultError(result, "Failed to stop client"); + } + }).get(); + thread->CheckException(); + } catch (std::exception &err) { + FormatError(wasapi_output_domain, "exception while stoping: %s", + err.what()); + } + is_started = false; thread->Finish(); thread->Join(); COMWorker::Async([&]() { thread.reset(); client.reset(); }).get(); + pcm_export.reset(); } std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { - if (!client || !is_started) { - return std::chrono::steady_clock::duration::zero(); + if (!is_started) { + // idle while paused + return std::chrono::seconds(1); } assert(thread); @@ -505,9 +564,16 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) { not_interrupted.test_and_set(); + ConstBuffer input(chunk, size); + if (pcm_export) { + input = pcm_export->Export(input); + } + if (input.empty()) + return size; + do { - const size_t consumed_size = - thread->spsc_buffer.push(static_cast(chunk), size); + const size_t consumed_size = thread->spsc_buffer.push( + static_cast(input.data), input.size); if (consumed_size == 0) { assert(is_started); thread->WaitDataPoped(); @@ -519,12 +585,9 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) { if (!is_started) { is_started = true; - thread->Play(); COMWorker::Async([&]() { - HRESULT result; - result = client->Start(); - if (FAILED(result)) { + if (HRESULT result = client->Start(); FAILED(result)) { throw FormatHResultError( result, "Failed to start client"); } @@ -533,28 +596,19 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) { thread->CheckException(); + if (pcm_export) { + return pcm_export->CalcInputSize(consumed_size); + } return consumed_size; } while (true); } bool WasapiOutput::Pause() { - if (!client || !thread) { - return false; + if (is_started) { + thread->Pause(); + is_started = false; } - if (!is_started) { - return true; - } - - HRESULT result; - result = client->Stop(); - if (FAILED(result)) { - throw FormatHResultError(result, "Failed to stop client"); - } - - is_started = false; - thread->Pause(); thread->CheckException(); - return true; } @@ -603,70 +657,69 @@ void WasapiOutput::OpenDevice() { } /// run inside COMWorkerThread -void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) { - SetFormat(device_format, audio_format); - - do { - HRESULT result; - result = client->IsFormatSupported( +bool WasapiOutput::TryFormatExclusive(const AudioFormat &audio_format) { + for (auto test_format : GetFormats(audio_format)) { + HRESULT result = client->IsFormatSupported( AUDCLNT_SHAREMODE_EXCLUSIVE, - reinterpret_cast(&device_format), nullptr); - - switch (result) { - case S_OK: - return; - case AUDCLNT_E_UNSUPPORTED_FORMAT: - break; - default: - throw FormatHResultError(result, "IsFormatSupported failed"); + reinterpret_cast(&test_format), nullptr); + const auto format_string = ToString(audio_format); + const auto result_string = std::string(HRESULTToString(result)); + FormatDebug(wasapi_output_domain, "Trying %s %lu %u-%u (exclusive) -> %s", + format_string.c_str(), test_format.Format.nSamplesPerSec, + test_format.Format.wBitsPerSample, + test_format.Samples.wValidBitsPerSample, + result_string.c_str()); + if (SUCCEEDED(result)) { + device_format = test_format; + return true; } + } + return false; +} - // Trying PCM fallback. - if (audio_format.format == SampleFormat::FLOAT) { - audio_format.format = SampleFormat::S32; +/// run inside COMWorkerThread +void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) { + for (uint8_t channels : {0, 2, 6, 8, 7, 1, 4, 5, 3}) { + if (audio_format.channels == channels) { continue; } - - // Trying sample rate fallback. - if (audio_format.sample_rate > 96000) { - audio_format.sample_rate = 96000; - continue; + if (channels == 0) { + channels = audio_format.channels; } - - if (audio_format.sample_rate > 88200) { - audio_format.sample_rate = 88200; - continue; + auto old_channels = std::exchange(audio_format.channels, channels); + for (uint32_t rate : {0, 384000, 352800, 192000, 176400, 96000, 88200, + 48000, 44100, 32000, 22050, 16000, 11025, 8000}) { + if (audio_format.sample_rate <= rate) { + continue; + } + if (rate == 0) { + rate = audio_format.sample_rate; + } + auto old_rate = std::exchange(audio_format.sample_rate, rate); + for (SampleFormat format : { + SampleFormat::UNDEFINED, + SampleFormat::S32, + SampleFormat::S24_P32, + SampleFormat::S16, + SampleFormat::S8, + }) { + if (audio_format.format == format) { + continue; + } + if (format == SampleFormat::UNDEFINED) { + format = audio_format.format; + } + auto old_format = + std::exchange(audio_format.format, format); + if (TryFormatExclusive(audio_format)) { + return; + } + audio_format.format = old_format; + } + audio_format.sample_rate = old_rate; } - - if (audio_format.sample_rate > 64000) { - audio_format.sample_rate = 64000; - continue; - } - - if (audio_format.sample_rate > 48000) { - audio_format.sample_rate = 48000; - continue; - } - - // Trying 2 channels fallback. - if (audio_format.channels > 2) { - audio_format.channels = 2; - continue; - } - - // Trying S16 fallback. - if (audio_format.format == SampleFormat::S32) { - audio_format.format = SampleFormat::S16; - continue; - } - - if (audio_format.sample_rate > 41100) { - audio_format.sample_rate = 41100; - continue; - } - - throw FormatHResultError(result, "Format is not supported"); - } while (true); + audio_format.channels = old_channels; + } } /// run inside COMWorkerThread @@ -679,15 +732,23 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { if (FAILED(result)) { throw FormatHResultError(result, "GetMixFormat failed"); } - audio_format.sample_rate = device_format.Format.nSamplesPerSec; - - SetFormat(device_format, audio_format); + audio_format.sample_rate = mixer_format->nSamplesPerSec; + device_format = GetFormats(audio_format).front(); ComHeapPtr closest_format; result = client->IsFormatSupported( AUDCLNT_SHAREMODE_SHARED, reinterpret_cast(&device_format), closest_format.AddressCast()); + { + const auto format_string = ToString(audio_format); + const auto result_string = std::string(HRESULTToString(result)); + FormatDebug(wasapi_output_domain, "Trying %s %lu %u-%u (shared) -> %s", + format_string.c_str(), device_format.Format.nSamplesPerSec, + device_format.Format.wBitsPerSample, + device_format.Samples.wValidBitsPerSample, + result_string.c_str()); + } if (FAILED(result) && result != AUDCLNT_E_UNSUPPORTED_FORMAT) { throw FormatHResultError(result, "IsFormatSupported failed"); @@ -701,12 +762,23 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { // Trying channels fallback. audio_format.channels = mixer_format->nChannels; - SetFormat(device_format, audio_format); + device_format = GetFormats(audio_format).front(); result = client->IsFormatSupported( AUDCLNT_SHAREMODE_SHARED, reinterpret_cast(&device_format), closest_format.AddressCast()); + { + const auto format_string = ToString(audio_format); + const auto result_string = std::string(HRESULTToString(result)); + FormatDebug(wasapi_output_domain, + "Trying %s %lu %u-%u (shared) -> %s", + format_string.c_str(), + device_format.Format.nSamplesPerSec, + device_format.Format.wBitsPerSample, + device_format.Samples.wValidBitsPerSample, + result_string.c_str()); + } if (FAILED(result)) { throw FormatHResultError(result, "Format is not supported"); } @@ -745,7 +817,10 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { audio_format.format = SampleFormat::S16; break; case 32: - audio_format.format = SampleFormat::S32; + audio_format.format = + device_format.Samples.wValidBitsPerSample == 32 + ? SampleFormat::S32 + : SampleFormat::S24_P32; break; } } else if (device_format.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {