diff --git a/NEWS b/NEWS index dc1f06be9..4cb424a35 100644 --- a/NEWS +++ b/NEWS @@ -14,11 +14,25 @@ ver 0.24 (not yet released) * switch to C++20 - GCC 10 or clang 11 (or newer) recommended -ver 0.23.8 (not yet released) +ver 0.23.8 (2022/07/09) * storage - curl: fix crash if web server does not understand WebDAV -* output - - pipewire: fix crash with PipeWire 0.3.53 +* input + - cdio_paranoia: fix crash if no drive was found + - cdio_paranoia: faster cancellation + - cdio_paranoia: don't scan for replay gain tags + - pipewire: fix playback of very short tracks + - pipewire: drop all buffers before manual song change + - pipewire: fix stuttering after manual song change + - snapcast: fix busy loop while paused + - snapcast: fix stuttering after resuming playback +* mixer + - better error messages + - alsa: fix setting volume before playback starts + - pipewire: fix crash bug + - pipewire: fix volume change events with PipeWire 0.3.53 + - pipewire: don't force initial volume=100% +* support libfmt 9 ver 0.23.7 (2022/05/09) * database diff --git a/doc/user.rst b/doc/user.rst index 1311503ab..1b01c64e3 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -1095,7 +1095,19 @@ The "music directory" is where you store your music files. :program:`MPD` stores Depending on the size of your music collection and the speed of the storage, this can take a while. -To exclude a file from the update, create a file called :file:`.mpdignore` in its parent directory. Each line of that file may contain a list of shell wildcards. Matching files in the current directory and all subdirectories are excluded. +To exclude a file from the update, create a file called +:file:`.mpdignore` in its parent directory. Each line of that file +may contain a list of shell wildcards. Matching files (or +directories) in the current directory and all subdirectories are +excluded. Example:: + + *.opus + 99* + +Subject to pattern matching is the file/directory name. It is (not +yet) possible to match nested path names, e.g. something like +``foo/*.flac`` is not possible. + Mounting other storages into the music directory ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/python/build/libs.py b/python/build/libs.py index afad33c06..0fca3daca 100644 --- a/python/build/libs.py +++ b/python/build/libs.py @@ -382,14 +382,14 @@ ffmpeg = FfmpegProject( ) openssl = OpenSSLProject( - 'https://www.openssl.org/source/openssl-3.0.3.tar.gz', - 'ee0078adcef1de5f003c62c80cc96527721609c6f3bb42b7795df31f8b558c0b', + 'https://www.openssl.org/source/openssl-3.0.5.tar.gz', + 'aa7d8d9bef71ad6525c55ba11e5f4397889ce49c2c9349dcea6d3e4f0b024a7a', 'include/openssl/ossl_typ.h', ) curl = CmakeProject( - 'https://curl.se/download/curl-7.83.1.tar.xz', - '2cb9c2356e7263a1272fd1435ef7cdebf2cd21400ec287b068396deb705c22c4', + 'https://curl.se/download/curl-7.84.0.tar.xz', + '2d118b43f547bfe5bae806d8d47b4e596ea5b25a6c1f080aef49fbcd817c5db8', 'lib/libcurl.a', [ '-DBUILD_CURL_EXE=OFF', diff --git a/src/Log.hxx b/src/Log.hxx index 186131087..3ca8b95b2 100644 --- a/src/Log.hxx +++ b/src/Log.hxx @@ -45,7 +45,10 @@ void LogFmt(LogLevel level, const Domain &domain, const S &format_str, Args&&... args) noexcept { -#if FMT_VERSION >= 70000 +#if FMT_VERSION >= 90000 + return LogVFmt(level, domain, format_str, + fmt::make_format_args(args...)); +#elif FMT_VERSION >= 70000 return LogVFmt(level, domain, fmt::to_string_view(format_str), fmt::make_args_checked(format_str, args...)); diff --git a/src/client/Response.hxx b/src/client/Response.hxx index c77bfe136..bed5ba6e7 100644 --- a/src/client/Response.hxx +++ b/src/client/Response.hxx @@ -82,7 +82,10 @@ public: template bool Fmt(const S &format_str, Args&&... args) noexcept { -#if FMT_VERSION >= 70000 +#if FMT_VERSION >= 90000 + return VFmt(format_str, + fmt::make_format_args(args...)); +#elif FMT_VERSION >= 70000 return VFmt(fmt::to_string_view(format_str), fmt::make_args_checked(format_str, args...)); @@ -109,7 +112,10 @@ public: template void FmtError(enum ack code, const S &format_str, Args&&... args) noexcept { -#if FMT_VERSION >= 70000 +#if FMT_VERSION >= 90000 + return VFmtError(code, format_str, + fmt::make_format_args(args...)); +#elif FMT_VERSION >= 70000 return VFmtError(code, fmt::to_string_view(format_str), fmt::make_args_checked(format_str, args...)); diff --git a/src/command/OtherCommands.cxx b/src/command/OtherCommands.cxx index 6ed43398d..0bcc3be7d 100644 --- a/src/command/OtherCommands.cxx +++ b/src/command/OtherCommands.cxx @@ -332,15 +332,11 @@ handle_getvol(Client &client, Request, Response &r) } CommandResult -handle_setvol(Client &client, Request args, Response &r) +handle_setvol(Client &client, Request args, Response &) { unsigned level = args.ParseUnsigned(0, 100); - if (!volume_level_change(client.GetPartition().outputs, level)) { - r.Error(ACK_ERROR_SYSTEM, "problems setting volume"); - return CommandResult::ERROR; - } - + volume_level_change(client.GetPartition().outputs, level); return CommandResult::OK; } @@ -363,11 +359,8 @@ handle_volume(Client &client, Request args, Response &r) else if (new_volume > 100) new_volume = 100; - if (new_volume != old_volume && - !volume_level_change(outputs, new_volume)) { - r.Error(ACK_ERROR_SYSTEM, "problems setting volume"); - return CommandResult::ERROR; - } + if (new_volume != old_volume) + volume_level_change(outputs, new_volume); return CommandResult::OK; } diff --git a/src/decoder/Control.hxx b/src/decoder/Control.hxx index 085070ba5..3f82a9a8d 100644 --- a/src/decoder/Control.hxx +++ b/src/decoder/Control.hxx @@ -257,6 +257,12 @@ public: return HasFailed(); } + [[gnu::pure]] + bool LockIsReplayGainEnabled() const noexcept { + const std::scoped_lock protect(mutex); + return replay_gain_mode != ReplayGainMode::OFF; + } + /** * Transition this obejct from DecoderState::START to * DecoderState::DECODE. diff --git a/src/decoder/Thread.cxx b/src/decoder/Thread.cxx index b302dab72..c9635e70b 100644 --- a/src/decoder/Thread.cxx +++ b/src/decoder/Thread.cxx @@ -36,6 +36,7 @@ #include "util/RuntimeError.hxx" #include "util/Domain.hxx" #include "util/ScopeExit.hxx" +#include "util/StringCompare.hxx" #include "thread/Name.hxx" #include "tag/ApeReplayGain.hxx" #include "Log.hxx" @@ -261,12 +262,16 @@ LoadReplayGain(DecoderClient &client, InputStream &is) static void MaybeLoadReplayGain(DecoderBridge &bridge, InputStream &is) { - { - const std::scoped_lock protect(bridge.dc.mutex); - if (bridge.dc.replay_gain_mode == ReplayGainMode::OFF) - /* ReplayGain is disabled */ - return; - } + if (!bridge.dc.LockIsReplayGainEnabled()) + /* ReplayGain is disabled */ + return; + + if (is.HasMimeType() && + StringStartsWith(is.GetMimeType(), "audio/x-mpd-")) + /* skip for (virtual) files (e.g. from the + cdio_paranoia input plugin) which cannot possibly + contain tags */ + return; LoadReplayGain(bridge, is); } diff --git a/src/input/plugins/CdioParanoiaInputPlugin.cxx b/src/input/plugins/CdioParanoiaInputPlugin.cxx index 0b2d52ab2..160531f64 100644 --- a/src/input/plugins/CdioParanoiaInputPlugin.cxx +++ b/src/input/plugins/CdioParanoiaInputPlugin.cxx @@ -30,10 +30,12 @@ #include "util/RuntimeError.hxx" #include "util/Domain.hxx" #include "util/ByteOrder.hxx" +#include "util/ScopeExit.hxx" #include "fs/AllocatedPath.hxx" #include "Log.hxx" #include "config/Block.hxx" +#include #include #include @@ -48,21 +50,19 @@ class CdioParanoiaInputStream final : public InputStream { CdIo_t *const cdio; CdromParanoia para; - const lsn_t lsn_from, lsn_to; - int lsn_relofs; + const lsn_t lsn_from; char buffer[CDIO_CD_FRAMESIZE_RAW]; - int buffer_lsn; + lsn_t buffer_lsn; public: CdioParanoiaInputStream(const char *_uri, Mutex &_mutex, cdrom_drive_t *_drv, CdIo_t *_cdio, bool reverse_endian, - lsn_t _lsn_from, lsn_t _lsn_to) + lsn_t _lsn_from, lsn_t lsn_to) :InputStream(_uri, _mutex), drv(_drv), cdio(_cdio), para(drv), - lsn_from(_lsn_from), lsn_to(_lsn_to), - lsn_relofs(0), + lsn_from(_lsn_from), buffer_lsn(-1) { /* Set reading mode for full paranoia, but allow @@ -173,9 +173,12 @@ cdio_detect_device() if (devices == nullptr) return nullptr; - AllocatedPath path = AllocatedPath::FromFS(devices[0]); - cdio_free_device_list(devices); - return path; + AtScopeExit(devices) { cdio_free_device_list(devices); }; + + if (devices[0] == nullptr) + return nullptr; + + return AllocatedPath::FromFS(devices[0]); } static InputStreamPtr @@ -271,81 +274,70 @@ CdioParanoiaInputStream::Seek(std::unique_lock &, return; /* calculate current LSN */ - lsn_relofs = new_offset / CDIO_CD_FRAMESIZE_RAW; - offset = new_offset; + const lsn_t lsn_relofs = new_offset / CDIO_CD_FRAMESIZE_RAW; - { + if (lsn_relofs != buffer_lsn) { const ScopeUnlock unlock(mutex); para.Seek(lsn_from + lsn_relofs); } + + offset = new_offset; } size_t CdioParanoiaInputStream::Read(std::unique_lock &, void *ptr, size_t length) { - size_t nbytes = 0; - char *wptr = (char *) ptr; + /* end of track ? */ + if (IsEOF()) + return 0; - while (length > 0) { - /* end of track ? */ - if (lsn_from + lsn_relofs > lsn_to) - break; + //current sector was changed ? + const int16_t *rbuf; - //current sector was changed ? - const int16_t *rbuf; - if (lsn_relofs != buffer_lsn) { - const ScopeUnlock unlock(mutex); + const lsn_t lsn_relofs = offset / CDIO_CD_FRAMESIZE_RAW; + const std::size_t diff = offset % CDIO_CD_FRAMESIZE_RAW; - try { - rbuf = para.Read().data(); - } catch (...) { - char *s_err = cdio_cddap_errors(drv); - if (s_err) { - FmtError(cdio_domain, - "paranoia_read: {}", s_err); - cdio_cddap_free_messages(s_err); - } + if (lsn_relofs != buffer_lsn) { + const ScopeUnlock unlock(mutex); - throw; + try { + rbuf = para.Read().data(); + } catch (...) { + char *s_err = cdio_cddap_errors(drv); + if (s_err) { + FmtError(cdio_domain, + "paranoia_read: {}", s_err); + cdio_cddap_free_messages(s_err); } - //store current buffer - memcpy(buffer, rbuf, CDIO_CD_FRAMESIZE_RAW); - buffer_lsn = lsn_relofs; - } else { - //use cached sector - rbuf = (const int16_t *)buffer; + throw; } - //correct offset - const int diff = offset - lsn_relofs * CDIO_CD_FRAMESIZE_RAW; - - assert(diff >= 0 && diff < CDIO_CD_FRAMESIZE_RAW); - - const size_t maxwrite = CDIO_CD_FRAMESIZE_RAW - diff; //# of bytes pending in current buffer - const size_t len = (length < maxwrite? length : maxwrite); - - //skip diff bytes from this lsn - memcpy(wptr, ((const char *)rbuf) + diff, len); - //update pointer - wptr += len; - nbytes += len; - - //update offset - offset += len; - lsn_relofs = offset / CDIO_CD_FRAMESIZE_RAW; - //update length - length -= len; + //store current buffer + memcpy(buffer, rbuf, CDIO_CD_FRAMESIZE_RAW); + buffer_lsn = lsn_relofs; + } else { + //use cached sector + rbuf = (const int16_t *)buffer; } + const size_t maxwrite = CDIO_CD_FRAMESIZE_RAW - diff; //# of bytes pending in current buffer + const std::size_t nbytes = std::min(length, maxwrite); + + //skip diff bytes from this lsn + memcpy(ptr, ((const char *)rbuf) + diff, nbytes); + + //update offset + offset += nbytes; + return nbytes; } bool CdioParanoiaInputStream::IsEOF() const noexcept { - return lsn_from + lsn_relofs > lsn_to; + return offset >= size; } static constexpr const char *cdio_paranoia_prefixes[] = { diff --git a/src/lib/curl/patches/no_CMAKE_C_IMPLICIT_LINK_LIBRARIES.patch b/src/lib/curl/patches/no_CMAKE_C_IMPLICIT_LINK_LIBRARIES.patch index d7d0ed678..dfa126374 100644 --- a/src/lib/curl/patches/no_CMAKE_C_IMPLICIT_LINK_LIBRARIES.patch +++ b/src/lib/curl/patches/no_CMAKE_C_IMPLICIT_LINK_LIBRARIES.patch @@ -1,6 +1,8 @@ ---- curl-7.75.0.orig/CMakeLists.txt 2021-02-02 09:26:24.000000000 +0100 -+++ curl-7.75.0/CMakeLists.txt 2021-03-25 20:17:25.445684029 +0100 -@@ -1453,7 +1453,7 @@ +Index: curl-7.84.0/CMakeLists.txt +=================================================================== +--- curl-7.84.0.orig/CMakeLists.txt ++++ curl-7.84.0/CMakeLists.txt +@@ -1536,7 +1536,7 @@ set(includedir "\${prefix}/ set(LDFLAGS "${CMAKE_SHARED_LINKER_FLAGS}") set(LIBCURL_LIBS "") set(libdir "${CMAKE_INSTALL_PREFIX}/lib") @@ -8,4 +10,4 @@ +foreach(_lib ${CURL_LIBS}) if(TARGET "${_lib}") set(_libname "${_lib}") - get_target_property(_libtype "${_libname}" TYPE) + get_target_property(_imported "${_libname}" IMPORTED) diff --git a/src/lib/curl/patches/no_netrc.patch b/src/lib/curl/patches/no_netrc.patch index c825ea36a..35b83b621 100644 --- a/src/lib/curl/patches/no_netrc.patch +++ b/src/lib/curl/patches/no_netrc.patch @@ -1,20 +1,20 @@ -Index: curl-7.71.1/lib/url.c +Index: curl-7.84.0/lib/url.c =================================================================== ---- curl-7.71.1.orig/lib/url.c -+++ curl-7.71.1/lib/url.c -@@ -2871,6 +2871,7 @@ - } +--- curl-7.84.0.orig/lib/url.c ++++ curl-7.84.0/lib/url.c +@@ -3003,6 +3003,7 @@ static CURLcode override_login(struct Cu + #ifndef CURL_DISABLE_NETRC conn->bits.netrc = FALSE; +#ifndef __BIONIC__ if(data->set.use_netrc && !data->set.str[STRING_USERNAME]) { bool netrc_user_changed = FALSE; bool netrc_passwd_changed = FALSE; -@@ -2895,6 +2896,7 @@ - conn->bits.user_passwd = TRUE; /* enable user+password */ +@@ -3079,6 +3080,7 @@ static CURLcode override_login(struct Cu + return CURLE_OUT_OF_MEMORY; } } +#endif - /* for updated strings, we update them in the URL */ - if(*userp) { + return CURLE_OK; + } diff --git a/src/mixer/MixerAll.cxx b/src/mixer/MixerAll.cxx index 2d9c9080e..302a06160 100644 --- a/src/mixer/MixerAll.cxx +++ b/src/mixer/MixerAll.cxx @@ -73,42 +73,77 @@ MultipleOutputs::GetVolume() const noexcept return total / ok; } -static bool -output_mixer_set_volume(AudioOutputControl &ao, unsigned volume) noexcept +enum class SetVolumeResult { + NO_MIXER, + DISABLED, + ERROR, + OK, +}; + +static SetVolumeResult +output_mixer_set_volume(AudioOutputControl &ao, unsigned volume) { assert(volume <= 100); auto *mixer = ao.GetMixer(); if (mixer == nullptr) - return false; + return SetVolumeResult::NO_MIXER; /* software mixers are always updated, even if they are disabled */ - if (!ao.IsReallyEnabled() && !mixer->IsPlugin(software_mixer_plugin)) - return false; + if (!mixer->IsPlugin(software_mixer_plugin) && + /* "global" mixers can be used even if the output hasn't + been used yet */ + !(mixer->IsGlobal() ? ao.IsEnabled() : ao.IsReallyEnabled())) + return SetVolumeResult::DISABLED; try { mixer_set_volume(mixer, volume); - return true; + return SetVolumeResult::OK; } catch (...) { FmtError(mixer_domain, "Failed to set mixer for '{}': {}", ao.GetName(), std::current_exception()); - return false; + std::throw_with_nested(std::runtime_error(fmt::format("Failed to set mixer for '{}'", + ao.GetName()))); } } -bool -MultipleOutputs::SetVolume(unsigned volume) noexcept +void +MultipleOutputs::SetVolume(unsigned volume) { assert(volume <= 100); - bool success = false; - for (const auto &ao : outputs) - success = output_mixer_set_volume(*ao, volume) - || success; + SetVolumeResult result = SetVolumeResult::NO_MIXER; + std::exception_ptr error; - return success; + for (const auto &ao : outputs) { + try { + auto r = output_mixer_set_volume(*ao, volume); + if (r > result) + result = r; + } catch (...) { + /* remember the first error */ + if (!error) { + error = std::current_exception(); + result = SetVolumeResult::ERROR; + } + } + } + + switch (result) { + case SetVolumeResult::NO_MIXER: + throw std::runtime_error{"No mixer"}; + + case SetVolumeResult::DISABLED: + throw std::runtime_error{"All outputs are disabled"}; + + case SetVolumeResult::ERROR: + std::rethrow_exception(error); + + case SetVolumeResult::OK: + break; + } } static int diff --git a/src/mixer/MixerControl.cxx b/src/mixer/MixerControl.cxx index 8ed050482..51ea759c1 100644 --- a/src/mixer/MixerControl.cxx +++ b/src/mixer/MixerControl.cxx @@ -60,9 +60,9 @@ mixer_open(Mixer *mixer) try { mixer->Open(); mixer->open = true; - mixer->failed = false; + mixer->failure = {}; } catch (...) { - mixer->failed = true; + mixer->failure = std::current_exception(); throw; } } @@ -75,6 +75,7 @@ mixer_close_internal(Mixer *mixer) mixer->Close(); mixer->open = false; + mixer->failure = {}; } void @@ -95,20 +96,6 @@ mixer_auto_close(Mixer *mixer) mixer_close(mixer); } -/* - * Close the mixer due to failure. The mutex must be locked before - * calling this function. - */ -static void -mixer_failed(Mixer *mixer) -{ - assert(mixer->open); - - mixer_close_internal(mixer); - - mixer->failed = true; -} - int mixer_get_volume(Mixer *mixer) { @@ -116,7 +103,7 @@ mixer_get_volume(Mixer *mixer) assert(mixer != nullptr); - if (mixer->plugin.global && !mixer->failed) + if (mixer->plugin.global && !mixer->failure) mixer_open(mixer); const std::scoped_lock protect(mixer->mutex); @@ -125,7 +112,8 @@ mixer_get_volume(Mixer *mixer) try { volume = mixer->GetVolume(); } catch (...) { - mixer_failed(mixer); + mixer_close_internal(mixer); + mixer->failure = std::current_exception(); throw; } } else @@ -140,11 +128,13 @@ mixer_set_volume(Mixer *mixer, unsigned volume) assert(mixer != nullptr); assert(volume <= 100); - if (mixer->plugin.global && !mixer->failed) + if (mixer->plugin.global && !mixer->failure) mixer_open(mixer); const std::scoped_lock protect(mixer->mutex); if (mixer->open) mixer->SetVolume(volume); + else if (mixer->failure) + std::rethrow_exception(mixer->failure); } diff --git a/src/mixer/MixerInternal.hxx b/src/mixer/MixerInternal.hxx index 9fc92d2bd..fd075000f 100644 --- a/src/mixer/MixerInternal.hxx +++ b/src/mixer/MixerInternal.hxx @@ -25,6 +25,8 @@ #include "thread/Mutex.hxx" #include "util/Compiler.h" +#include + class MixerListener; class Mixer { @@ -39,17 +41,17 @@ public: */ Mutex mutex; + /** + * Contains error details if this mixer has failed. If set, + * it should not be reopened automatically. + */ + std::exception_ptr failure; + /** * Is the mixer device currently open? */ bool open = false; - /** - * Has this mixer failed, and should not be reopened - * automatically? - */ - bool failed = false; - public: explicit Mixer(const MixerPlugin &_plugin, MixerListener &_listener) noexcept @@ -63,6 +65,10 @@ public: return &plugin == &other; } + bool IsGlobal() const noexcept { + return plugin.global; + } + /** * Open mixer device * diff --git a/src/mixer/Volume.cxx b/src/mixer/Volume.cxx index 5f37e48fc..4afb97e5d 100644 --- a/src/mixer/Volume.cxx +++ b/src/mixer/Volume.cxx @@ -71,16 +71,16 @@ software_volume_change(MultipleOutputs &outputs, unsigned volume) return true; } -static bool +static void hardware_volume_change(MultipleOutputs &outputs, unsigned volume) { /* reset the cache */ last_hardware_volume = -1; - return outputs.SetVolume(volume); + outputs.SetVolume(volume); } -bool +void volume_level_change(MultipleOutputs &outputs, unsigned volume) { assert(volume <= 100); @@ -89,7 +89,7 @@ volume_level_change(MultipleOutputs &outputs, unsigned volume) idle_add(IDLE_MIXER); - return hardware_volume_change(outputs, volume); + hardware_volume_change(outputs, volume); } bool diff --git a/src/mixer/Volume.hxx b/src/mixer/Volume.hxx index ae38ce6f0..b245fe7fe 100644 --- a/src/mixer/Volume.hxx +++ b/src/mixer/Volume.hxx @@ -30,7 +30,10 @@ InvalidateHardwareVolume() noexcept; int volume_level_get(const MultipleOutputs &outputs) noexcept; -bool +/** + * Throws on error. + */ +void volume_level_change(MultipleOutputs &outputs, unsigned volume); bool diff --git a/src/output/MultipleOutputs.hxx b/src/output/MultipleOutputs.hxx index 761111ae3..d32c2c7e9 100644 --- a/src/output/MultipleOutputs.hxx +++ b/src/output/MultipleOutputs.hxx @@ -141,10 +141,11 @@ public: /** * Sets the volume on all available mixers. * + * Throws on error. + * * @param volume the volume (range 0..100) - * @return true on success, false on failure */ - bool SetVolume(unsigned volume) noexcept; + void SetVolume(unsigned volume); /** * Similar to GetVolume(), but gets the volume only for diff --git a/src/output/plugins/PipeWireOutputPlugin.cxx b/src/output/plugins/PipeWireOutputPlugin.cxx index 12b78428b..ff0177f89 100644 --- a/src/output/plugins/PipeWireOutputPlugin.cxx +++ b/src/output/plugins/PipeWireOutputPlugin.cxx @@ -24,6 +24,7 @@ #include "../Error.hxx" #include "mixer/plugins/PipeWireMixerPlugin.hxx" #include "pcm/Silence.hxx" +#include "lib/fmt/ExceptionFormatter.hxx" #include "system/Error.hxx" #include "util/BitReverse.hxx" #include "util/Domain.hxx" @@ -55,6 +56,7 @@ #include #include +#include #include #include @@ -84,7 +86,14 @@ class PipeWireOutput final : AudioOutput { uint32_t target_id = PW_ID_ANY; - float volume = 1.0; + /** + * The current volume level (0.0 .. 1.0). + * + * This get initialized to -1 which means "unknown", so + * restore_volume will not attempt to override PipeWire's + * initial volume level. + */ + float volume = -1; PipeWireMixer *mixer = nullptr; unsigned channels; @@ -216,27 +225,34 @@ private: o.Drained(); } - void ControlInfo(const struct pw_stream_control *control) noexcept { - float sum = 0; - unsigned c; - for (c = 0; c < control->n_values; c++) - sum += control->values[c]; + void OnChannelVolumes(const struct pw_stream_control &control) noexcept { + if (control.n_values < 1) + return; - sum /= control->n_values; + float sum = std::accumulate(control.values, + control.values + control.n_values, + 0.0f); + volume = std::cbrt(sum / control.n_values); if (mixer != nullptr) - pipewire_mixer_on_change(*mixer, std::cbrt(sum)); + pipewire_mixer_on_change(*mixer, volume); pw_thread_loop_signal(thread_loop, false); } - static void ControlInfo(void *data, - [[maybe_unused]] uint32_t id, + void ControlInfo([[maybe_unused]] uint32_t id, + const struct pw_stream_control &control) noexcept { + switch (id) { + case SPA_PROP_channelVolumes: + OnChannelVolumes(control); + break; + } + } + + static void ControlInfo(void *data, uint32_t id, const struct pw_stream_control *control) noexcept { auto &o = *(PipeWireOutput *)data; - if (control->name != nullptr && - StringIsEqual(control->name, "Channel Volumes")) - o.ControlInfo(control); + o.ControlInfo(id, *control); } #if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) @@ -308,22 +324,38 @@ PipeWireOutput::PipeWireOutput(const ConfigBlock &block) } } +/** + * Throws on error. + * + * @param volume a volume level between 0.0 and 1.0 + */ +static void +SetVolume(struct pw_stream &stream, unsigned channels, float volume) +{ + float value[MAX_CHANNELS]; + std::fill_n(value, channels, volume * volume * volume); + + if (pw_stream_set_control(&stream, + SPA_PROP_channelVolumes, channels, value, + 0) != 0) + throw std::runtime_error("pw_stream_set_control() failed"); +} + void PipeWireOutput::SetVolume(float _volume) { + if (thread_loop == nullptr) { + /* the mixer is open (because it is a "global" mixer), + but Enable() on this output has not yet been + called */ + volume = _volume; + return; + } + const PipeWire::ThreadLoopLock lock(thread_loop); - float newvol = _volume*_volume*_volume; - - if (stream != nullptr && !restore_volume) { - float vol[MAX_CHANNELS]; - std::fill_n(vol, channels, newvol); - - if (pw_stream_set_control(stream, - SPA_PROP_channelVolumes, channels, vol, - 0) != 0) - throw std::runtime_error("pw_stream_set_control() failed"); - } + if (stream != nullptr && !restore_volume) + ::SetVolume(*stream, channels, _volume); volume = _volume; } @@ -639,7 +671,16 @@ PipeWireOutput::ParamChanged([[maybe_unused]] uint32_t id, { if (restore_volume) { restore_volume = false; - SetVolume(volume); + + if (volume >= 0) { + try { + ::SetVolume(*stream, channels, volume); + } catch (...) { + FmtError(pipewire_output_domain, + FMT_STRING("Failed to restore volume: {}"), + std::current_exception()); + } + } } #if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE) @@ -824,6 +865,17 @@ PipeWireOutput::Drain() { const PipeWire::ThreadLoopLock lock(thread_loop); + if (drained) + return; + + if (!active) { + /* there is data in the ring_buffer, but the stream is + not yet active; activate it now to ensure it is + played before this method returns */ + active = true; + pw_stream_set_active(stream, true); + } + drain_requested = true; AtScopeExit(this) { drain_requested = false; }; @@ -839,7 +891,24 @@ PipeWireOutput::Cancel() noexcept const PipeWire::ThreadLoopLock lock(thread_loop); interrupted = false; + if (drained) + return; + + /* clear MPD's ring buffer */ ring_buffer->reset(); + + /* clear libpipewire's buffer */ + pw_stream_flush(stream, false); + drained = true; + + /* pause the PipeWire stream so libpipewire ceases invoking + the "process" callback (we have no data until our Play() + method gets called again); the stream will be resume by + Play() after the ring_buffer has been refilled */ + if (active) { + active = false; + pw_stream_set_active(stream, false); + } } bool diff --git a/src/output/plugins/snapcast/SnapcastOutputPlugin.cxx b/src/output/plugins/snapcast/SnapcastOutputPlugin.cxx index d6e883b35..768d5e1a2 100644 --- a/src/output/plugins/snapcast/SnapcastOutputPlugin.cxx +++ b/src/output/plugins/snapcast/SnapcastOutputPlugin.cxx @@ -215,9 +215,8 @@ SnapcastOutput::RemoveClient(SnapcastClient &client) noexcept std::chrono::steady_clock::duration SnapcastOutput::Delay() const noexcept { - if (!LockHasClients() && pause) { - /* if there's no client and this output is paused, - then Pause() will not do anything, it will not fill + if (pause) { + /* Pause() will not do anything, it will not fill the buffer and it will not update the timer; therefore, we reset the timer here */ timer->Reset(); diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap index 63869be17..366e7bb85 100644 --- a/subprojects/fmt.wrap +++ b/subprojects/fmt.wrap @@ -3,10 +3,10 @@ directory = fmt-8.1.1 source_url = https://github.com/fmtlib/fmt/archive/8.1.1.tar.gz source_filename = fmt-8.1.1.tar.gz source_hash = 3d794d3cf67633b34b2771eb9f073bde87e846e0d395d254df7b211ef1ec7346 -patch_filename = fmt_8.1.1-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/fmt_8.1.1-1/get_patch -patch_hash = 6035a67c7a8c90bed74c293c7265c769f47a69816125f7566bccb8e2543cee5e +patch_filename = fmt_8.1.1-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fmt_8.1.1-2/get_patch +patch_hash = cd001046281330a8862591780a9ea71a1fa594edd0d015deb24e44680c9ea33b +wrapdb_version = 8.1.1-2 [provide] fmt = fmt_dep - diff --git a/subprojects/vorbis.wrap b/subprojects/vorbis.wrap index dfca897c9..6c1a9a94d 100644 --- a/subprojects/vorbis.wrap +++ b/subprojects/vorbis.wrap @@ -3,9 +3,10 @@ directory = libvorbis-1.3.7 source_url = https://downloads.xiph.org/releases/vorbis/libvorbis-1.3.7.tar.xz source_filename = libvorbis-1.3.7.tar.xz source_hash = b33cc4934322bcbf6efcbacf49e3ca01aadbea4114ec9589d1b1e9d20f72954b -patch_filename = vorbis_1.3.7-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/vorbis_1.3.7-2/get_patch -patch_hash = fe302576cbf8408754b332b539ea1b83f0f96fa9aae50a5d1fea911713d5f21c +patch_filename = vorbis_1.3.7-3_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/vorbis_1.3.7-3/get_patch +patch_hash = 6cb90a61ede8c64d3e8e379b96dcc800c9dd69e925122b3d73d8f59a563c3afa +wrapdb_version = 1.3.7-3 [provide] vorbis = vorbis_dep