// SPDX-License-Identifier: GPL-2.0-or-later // Copyright The Music Player Daemon Project #include "Control.hxx" #include "Filtered.hxx" #include "Client.hxx" #include "Domain.hxx" #include "lib/fmt/ExceptionFormatter.hxx" #include "mixer/Mixer.hxx" #include "config/Block.hxx" #include "Log.hxx" #include /** after a failure, wait this duration before automatically reopening the device */ static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10); AudioOutputControl::AudioOutputControl(std::unique_ptr _output, AudioOutputClient &_client, const ConfigBlock &block) :output(std::move(_output)), name(output->GetName()), client(_client), thread(BIND_THIS_METHOD(Task)), tags(block.GetBlockValue("tags", true)), always_on(block.GetBlockValue("always_on", false)), enabled(block.GetBlockValue("enabled", true)) { } AudioOutputControl::AudioOutputControl(AudioOutputControl &&src, AudioOutputClient &_client) noexcept :output(src.Steal()), name(output->GetName()), client(_client), thread(BIND_THIS_METHOD(Task)), tags(src.tags), always_on(src.always_on) { } AudioOutputControl::~AudioOutputControl() noexcept { StopThread(); } std::unique_ptr AudioOutputControl::Steal() noexcept { assert(!IsDummy()); /* close and disable the output */ { std::unique_lock lock(mutex); if (really_enabled && output->SupportsEnableDisable()) CommandWait(lock, Command::DISABLE); enabled = really_enabled = false; } /* stop the thread */ StopThread(); /* now we can finally remove it */ const std::scoped_lock protect(mutex); return std::exchange(output, nullptr); } void AudioOutputControl::ReplaceDummy(std::unique_ptr new_output, bool _enabled) noexcept { assert(IsDummy()); assert(new_output); { const std::scoped_lock protect(mutex); output = std::move(new_output); enabled = _enabled; } client.ApplyEnabled(); } const char * AudioOutputControl::GetPluginName() const noexcept { return output ? output->GetPluginName() : "dummy"; } const char * AudioOutputControl::GetLogName() const noexcept { assert(!IsDummy()); return output ? output->GetLogName() : name.c_str(); } Mixer * AudioOutputControl::GetMixer() const noexcept { return output ? output->mixer : nullptr; } std::map AudioOutputControl::GetAttributes() const noexcept { return output ? output->GetAttributes() : std::map{}; } void AudioOutputControl::SetAttribute(std::string &&attribute_name, std::string &&value) { if (!output) throw std::runtime_error("Cannot set attribute on dummy output"); output->SetAttribute(std::move(attribute_name), std::move(value)); } bool AudioOutputControl::LockSetEnabled(bool new_value) noexcept { const std::scoped_lock protect(mutex); if (new_value == enabled) return false; enabled = new_value; return true; } bool AudioOutputControl::LockToggleEnabled() noexcept { const std::scoped_lock protect(mutex); return enabled = !enabled; } void AudioOutputControl::WaitForCommand(std::unique_lock &lock) noexcept { client_cond.wait(lock, [this]{ return IsCommandFinished(); }); } void AudioOutputControl::CommandAsync(Command cmd) noexcept { assert(IsCommandFinished()); command = cmd; wake_cond.notify_one(); } void AudioOutputControl::CommandWait(std::unique_lock &lock, Command cmd) noexcept { CommandAsync(cmd); WaitForCommand(lock); } void AudioOutputControl::LockCommandWait(Command cmd) noexcept { std::unique_lock lock(mutex); CommandWait(lock, cmd); } void AudioOutputControl::EnableAsync() { if (!output) return; if (!thread.IsDefined()) { if (!output->SupportsEnableDisable()) { /* don't bother to start the thread now if the device doesn't even have a enable() method; just assign the variable and we're done */ really_enabled = true; return; } StartThread(); } CommandAsync(Command::ENABLE); } void AudioOutputControl::DisableAsync() noexcept { if (!output) return; if (!thread.IsDefined()) { if (!output->SupportsEnableDisable()) really_enabled = false; else /* if there's no thread yet, the device cannot be enabled */ assert(!really_enabled); return; } CommandAsync(Command::DISABLE); } void AudioOutputControl::EnableDisableAsync() { if (enabled == really_enabled) return; if (enabled) EnableAsync(); else DisableAsync(); } inline bool AudioOutputControl::Open(std::unique_lock &lock, const AudioFormat audio_format, const MusicPipe &mp) noexcept { assert(allow_play); assert(audio_format.IsValid()); fail_timer.Reset(); if (open && audio_format == request.audio_format) { assert(request.pipe == &mp || (always_on && pause)); if (!pause) /* already open, already the right parameters - nothing needs to be done */ return true; } request.audio_format = audio_format; request.pipe = ∓ if (!thread.IsDefined()) { try { StartThread(); } catch (...) { LogError(std::current_exception()); return false; } } CommandWait(lock, Command::OPEN); const bool open2 = open; if (open2 && output->mixer != nullptr) { const ScopeUnlock unlock(mutex); try { output->mixer->LockOpen(); } catch (...) { FmtError(output_domain, "Failed to open mixer for '{}': {}", GetName(), std::current_exception()); } } return open2; } void AudioOutputControl::CloseWait(std::unique_lock &lock) noexcept { assert(allow_play); if (IsDummy()) return; if (output->mixer != nullptr) output->mixer->LockAutoClose(); assert(!open || !fail_timer.IsDefined()); if (open) CommandWait(lock, Command::CLOSE); else fail_timer.Reset(); } bool AudioOutputControl::LockUpdate(const AudioFormat audio_format, const MusicPipe &mp, bool force) noexcept { std::unique_lock lock(mutex); if (enabled && really_enabled) { if (force || !fail_timer.IsDefined() || fail_timer.Check(REOPEN_AFTER * 1000)) { return Open(lock, audio_format, mp); } } else if (IsOpen()) CloseWait(lock); return false; } bool AudioOutputControl::IsChunkConsumed(const MusicChunk &chunk) const noexcept { if (!open) return true; return source.IsChunkConsumed(chunk); } bool AudioOutputControl::LockIsChunkConsumed(const MusicChunk &chunk) const noexcept { const std::scoped_lock protect(mutex); return IsChunkConsumed(chunk); } void AudioOutputControl::LockPlay() noexcept { const std::scoped_lock protect(mutex); assert(allow_play); if (IsOpen() && !in_playback_loop && !woken_for_play) { woken_for_play = true; wake_cond.notify_one(); } } void AudioOutputControl::LockPauseAsync() noexcept { if (output && output->mixer != nullptr && !output->SupportsPause()) /* the device has no pause mode: close the mixer, unless its "global" flag is set (checked by Mixer::LockAutoClose()) */ output->mixer->LockAutoClose(); if (output) output->Interrupt(); const std::scoped_lock protect(mutex); assert(allow_play); if (IsOpen()) CommandAsync(Command::PAUSE); } void AudioOutputControl::LockDrainAsync() noexcept { const std::scoped_lock protect(mutex); assert(allow_play); if (IsOpen()) CommandAsync(Command::DRAIN); } void AudioOutputControl::LockCancelAsync() noexcept { if (output) output->Interrupt(); const std::scoped_lock protect(mutex); if (IsOpen()) { allow_play = false; CommandAsync(Command::CANCEL); } } void AudioOutputControl::LockAllowPlay() noexcept { const std::scoped_lock protect(mutex); allow_play = true; if (IsOpen()) wake_cond.notify_one(); } void AudioOutputControl::LockRelease() noexcept { if (!output) return; output->Interrupt(); if (output->mixer != nullptr && (!always_on || !output->SupportsPause())) /* the device has no pause mode: close the mixer, unless its "global" flag is set (checked by Mixer::LockAutoClose()) */ output->mixer->LockAutoClose(); std::unique_lock lock(mutex); assert(!open || !fail_timer.IsDefined()); assert(allow_play); if (IsOpen()) CommandWait(lock, Command::RELEASE); else fail_timer.Reset(); } void AudioOutputControl::LockCloseWait() noexcept { assert(!open || !fail_timer.IsDefined()); if (output) output->Interrupt(); std::unique_lock lock(mutex); CloseWait(lock); } void AudioOutputControl::BeginDestroy() noexcept { if (thread.IsDefined()) { if (output) output->Interrupt(); const std::scoped_lock protect(mutex); if (!killed) { killed = true; CommandAsync(Command::KILL); } } } void AudioOutputControl::StopThread() noexcept { BeginDestroy(); if (thread.IsDefined()) thread.Join(); assert(IsCommandFinished()); }