diff --git a/src/output/Control.cxx b/src/output/Control.cxx index e4be4dfa7..fd4a1aa50 100644 --- a/src/output/Control.cxx +++ b/src/output/Control.cxx @@ -359,6 +359,9 @@ AudioOutputControl::LockPauseAsync() noexcept mixer_auto_close()) */ mixer_auto_close(output->mixer); + if (output) + output->Interrupt(); + const std::lock_guard protect(mutex); assert(allow_play); @@ -379,6 +382,9 @@ AudioOutputControl::LockDrainAsync() noexcept void AudioOutputControl::LockCancelAsync() noexcept { + if (output) + output->Interrupt(); + const std::lock_guard protect(mutex); if (IsOpen()) { @@ -403,6 +409,8 @@ 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, @@ -426,6 +434,9 @@ AudioOutputControl::LockCloseWait() noexcept { assert(!open || !fail_timer.IsDefined()); + if (output) + output->Interrupt(); + std::unique_lock lock(mutex); CloseWait(lock); } @@ -434,6 +445,9 @@ void AudioOutputControl::BeginDestroy() noexcept { if (thread.IsDefined()) { + if (output) + output->Interrupt(); + const std::lock_guard protect(mutex); if (!killed) { killed = true; diff --git a/src/output/Control.hxx b/src/output/Control.hxx index c87dc133b..809c65101 100644 --- a/src/output/Control.hxx +++ b/src/output/Control.hxx @@ -196,6 +196,15 @@ class AudioOutputControl { */ bool allow_play = true; + /** + * Was an #AudioOutputInterrupted caught? In this case, + * playback is suspended, and the output thread waits for a + * command. + * + * This field is only valid while the output is open. + */ + bool caught_interrupted; + /** * True while the OutputThread is inside ao_play(). This * means the PlayerThread does not need to wake up the diff --git a/src/output/Error.hxx b/src/output/Error.hxx new file mode 100644 index 000000000..86fc1c297 --- /dev/null +++ b/src/output/Error.hxx @@ -0,0 +1,29 @@ +/* + * Copyright 2003-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_AUDIO_OUTPUT_ERROR_HXX +#define MPD_AUDIO_OUTPUT_ERROR_HXX + +/** + * An exception class that will be thrown by various #AudioOutput + * methods after AudioOutput::Interrupt() has been called. + */ +class AudioOutputInterrupted {}; + +#endif diff --git a/src/output/Filtered.cxx b/src/output/Filtered.cxx index a38add5c4..b3833953e 100644 --- a/src/output/Filtered.cxx +++ b/src/output/Filtered.cxx @@ -183,6 +183,12 @@ FilteredAudioOutput::Drain() output->Drain(); } +void +FilteredAudioOutput::Interrupt() noexcept +{ + output->Interrupt(); +} + void FilteredAudioOutput::Cancel() noexcept { diff --git a/src/output/Filtered.hxx b/src/output/Filtered.hxx index 11a0ac742..7e2413cc9 100644 --- a/src/output/Filtered.hxx +++ b/src/output/Filtered.hxx @@ -216,6 +216,8 @@ public: */ void CloseSoftwareMixer() noexcept; + void Interrupt() noexcept; + gcc_pure std::chrono::steady_clock::duration Delay() noexcept; diff --git a/src/output/Interface.hxx b/src/output/Interface.hxx index a54e23a0b..e39dac0b0 100644 --- a/src/output/Interface.hxx +++ b/src/output/Interface.hxx @@ -126,6 +126,24 @@ public: return false; } + /** + * Interrupt a blocking operation inside the plugin. This + * method will be called from outside the output thread (and + * therefore the method must be thread-safe), to make the + * output thread ready for receiving a command. For example, + * it will be called to prepare for an upcoming Close(), + * Cancel() or Pause() call. + * + * This method can be called any time, even if the output is + * not open or disabled. + * + * Implementations usually send some kind of message/signal to + * the output thread to wake it up and return to the output + * thread loop (e.g. by throwing #AudioOutputInterrupted), + * where the incoming command will be handled and dispatched. + */ + virtual void Interrupt() noexcept {} + /** * Returns a positive number if the output thread shall further * delay the next call to Play() or Pause(), which will happen @@ -142,6 +160,11 @@ public: /** * Display metadata for the next chunk. Optional method, * because not all devices can display metadata. + * + * Throws on error. + * + * May throw #AudioOutputInterrupted after Interrupt() has + * been called. */ virtual void SendTag(const Tag &) {} @@ -151,6 +174,9 @@ public: * * Throws on error. * + * May throw #AudioOutputInterrupted after Interrupt() has + * been called. + * * @return the number of bytes played (must be a multiple of * the frame size) */ @@ -177,6 +203,9 @@ public: * disconnected. Plugins which do not support pausing will * simply be closed, and have to be reopened when unpaused. * + * May throw #AudioOutputInterrupted after Interrupt() has + * been called. + * * @return false on error (output will be closed by caller), * true for continue to pause * diff --git a/src/output/Thread.cxx b/src/output/Thread.cxx index 23b6acaa0..1427e0d32 100644 --- a/src/output/Thread.cxx +++ b/src/output/Thread.cxx @@ -18,6 +18,7 @@ */ #include "Control.hxx" +#include "Error.hxx" #include "Filtered.hxx" #include "Client.hxx" #include "Domain.hxx" @@ -135,6 +136,7 @@ AudioOutputControl::InternalOpen(const AudioFormat in_audio_format, last_error = nullptr; fail_timer.Reset(); + caught_interrupted = false; skip_delay = true; AudioFormat f; @@ -243,6 +245,9 @@ AudioOutputControl::PlayChunk(std::unique_lock &lock) noexcept const ScopeUnlock unlock(mutex); try { output->SendTag(*tag); + } catch (AudioOutputInterrupted) { + caught_interrupted = true; + return false; } catch (...) { FormatError(std::current_exception(), "Failed to send tag to %s", @@ -267,6 +272,9 @@ AudioOutputControl::PlayChunk(std::unique_lock &lock) noexcept nbytes = output->Play(data.data, data.size); assert(nbytes > 0); assert(nbytes <= data.size); + } catch (AudioOutputInterrupted) { + caught_interrupted = true; + return false; } catch (...) { FormatError(std::current_exception(), "Failed to play on %s", GetLogName()); @@ -342,6 +350,7 @@ AudioOutputControl::InternalPause(std::unique_lock &lock) noexcept try { const ScopeUnlock unlock(mutex); success = output->IteratePause(); + } catch (AudioOutputInterrupted) { } catch (...) { FormatError(std::current_exception(), "Failed to pause %s", @@ -425,7 +434,8 @@ AudioOutputControl::Task() noexcept /* no pending command: play (or wait for a command) */ - if (open && allow_play && InternalPlay(lock)) + if (open && allow_play && !caught_interrupted && + InternalPlay(lock)) /* don't wait for an event if there are more chunks in the pipe */ continue; @@ -463,6 +473,8 @@ AudioOutputControl::Task() noexcept break; } + caught_interrupted = false; + InternalPause(lock); break; @@ -475,6 +487,8 @@ AudioOutputControl::Task() noexcept break; } + caught_interrupted = false; + if (always_on) { /* in "always_on" mode, the output is paused instead of being closed; @@ -499,6 +513,8 @@ AudioOutputControl::Task() noexcept break; case Command::CANCEL: + caught_interrupted = false; + source.Cancel(); if (open) {