From fac15aaffbc2a9016c013188fb062407e3cebe20 Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Fri, 28 Jun 2019 14:39:52 +0200 Subject: [PATCH 1/7] output/alsa: fix comment typo --- src/output/plugins/AlsaOutputPlugin.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index 16832a1bc..e9e68c0ce 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -995,7 +995,7 @@ try { whenever more data arrives */ /* the same applies when there is still enough data in the ALSA-PCM buffer (determined by - snd_pcm_avail()); this can happend at the + snd_pcm_avail()); this can happen at the start of playback, when our ring_buffer is smaller than the ALSA-PCM buffer */ From 3da1fa88d0688c32f5db4948c3d86f3b0933bd4d Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Fri, 28 Jun 2019 18:04:45 +0200 Subject: [PATCH 2/7] output/alsa: fix comment typo --- src/output/plugins/AlsaOutputPlugin.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index e9e68c0ce..eac424622 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -501,7 +501,7 @@ AlsaOutput::Setup(AudioFormat &audio_format, period_frames = alsa_period_size; - /* generate silence if there's less than once period of data + /* generate silence if there's less than one period of data in the ALSA-PCM buffer */ max_avail_frames = hw_result.buffer_size - hw_result.period_size; From 3c5f860fb8164ccc4bb714cc39f3e6a3e982862a Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Fri, 28 Jun 2019 17:03:26 +0200 Subject: [PATCH 3/7] output/alsa: Cancel() also affects "active" (documentation) --- src/output/plugins/AlsaOutputPlugin.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index eac424622..54aa6d9e0 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -128,8 +128,8 @@ class AlsaOutput final bool work_around_drain_bug; /** - * After Open(), has this output been activated by a Play() - * command? + * After Open() or Cancel(), has this output been activated by + * a Play() command? * * Protected by #mutex. */ From 0c0a35475313c6bb96c7cdf6059d245746f3843d Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Fri, 28 Jun 2019 15:30:26 +0200 Subject: [PATCH 4/7] output/alsa: add a new flag "waiting" for xrun management In DispatchSockets(), when there was not enough data, but enough for current playback, the method would disable the "active" flag so the next Play() call would re-enable the MultiSocketMonitor. This was an abuse of the flag which could result in a crash in Cancel(), because that method asserts that the period_buffer is empty, which it may be not. The solution is to add anther flag called "waiting" which shares some behavior with the old flag. --- src/output/plugins/AlsaOutputPlugin.cxx | 27 ++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index 54aa6d9e0..96daac192 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -135,6 +135,13 @@ class AlsaOutput final */ bool active; + /** + * Is this output waiting for more data? + * + * Protected by #mutex. + */ + bool waiting; + /** * Do we need to call snd_pcm_prepare() before the next write? * It means that we put the device to SND_PCM_STATE_SETUP by @@ -176,7 +183,7 @@ class AlsaOutput final Alsa::PeriodBuffer period_buffer; /** - * Protects #cond, #error, #active, #drain. + * Protects #cond, #error, #active, #waiting, #drain. */ mutable Mutex mutex; @@ -248,6 +255,12 @@ private: return active; } + gcc_pure + bool LockIsActiveAndNotWaiting() const noexcept { + const std::lock_guard<Mutex> lock(mutex); + return active && !waiting; + } + /** * Activate the output by registering the sockets in the * #EventLoop. Before calling this, filling the ring buffer @@ -260,10 +273,11 @@ private: * was never unlocked */ bool Activate() noexcept { - if (active) + if (active && !waiting) return false; active = true; + waiting = false; const ScopeUnlock unlock(mutex); defer_invalidate_sockets.Schedule(); @@ -330,6 +344,7 @@ private: const std::lock_guard<Mutex> lock(mutex); error = std::current_exception(); active = false; + waiting = false; cond.signal(); } @@ -684,6 +699,7 @@ AlsaOutput::Open(AudioFormat &audio_format) period_buffer.Allocate(period_frames, out_frame_size); active = false; + waiting = false; must_prepare = false; written = false; error = {}; @@ -845,6 +861,7 @@ AlsaOutput::CancelInternal() noexcept ring_buffer->reset(); active = false; + waiting = false; MultiSocketMonitor::Reset(); defer_invalidate_sockets.Cancel(); @@ -930,7 +947,7 @@ AlsaOutput::Play(const void *chunk, size_t size) std::chrono::steady_clock::duration AlsaOutput::PrepareSockets() noexcept { - if (!LockIsActive()) { + if (!LockIsActiveAndNotWaiting()) { ClearSocketList(); return std::chrono::steady_clock::duration(-1); } @@ -1001,13 +1018,13 @@ try { { const std::lock_guard<Mutex> lock(mutex); - active = false; + waiting = true; cond.signal(); } /* avoid race condition: see if data has arrived meanwhile before disabling the - event (but after clearing the "active" + event (but after setting the "waiting" flag) */ if (!CopyRingToPeriodBuffer()) { MultiSocketMonitor::Reset(); From 61a72a5d131e89f069d2d2de9411de8fe4c21f23 Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Fri, 28 Jun 2019 14:51:27 +0200 Subject: [PATCH 5/7] output/alsa: schedule a timer to generate silence Without this timer, DispatchSockets() may disable the MultiSocketMonitor and if Play() doesn't get called soon, it never gets a chance to generate silence. However if Play() gets called, generating silence isn't necessary anymore... Resulting from this misdesign (added by commit ccafe3f3cf3 in 0.21.3), the silence generator didn't work reliably. --- NEWS | 1 + src/output/plugins/AlsaOutputPlugin.cxx | 39 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/NEWS b/NEWS index 542cd5d04..354491ea5 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,7 @@ ver 0.21.11 (not yet released) * output - alsa: fix busy loop while draining - alsa: fix missing drain call + - alsa: improve xrun-avoiding silence generator - alsa, osx: fix distortions with DSD_U32 and DoP on 32 bit CPUs * protocol - fix "list" with multiple "group" levels diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index 96daac192..7e1919e43 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -56,6 +56,15 @@ class AlsaOutput final DeferEvent defer_invalidate_sockets; + /** + * This timer is used to re-schedule the #MultiSocketMonitor + * after it had been disabled to wait for the next Play() call + * to deliver more data. This timer is necessary to start + * generating silence if Play() doesn't get called soon enough + * to avoid the xrun. + */ + TimerEvent silence_timer; + Manual<PcmExport> pcm_export; /** @@ -109,6 +118,8 @@ class AlsaOutput final */ snd_pcm_uframes_t period_frames; + std::chrono::steady_clock::duration effective_period_duration; + /** * If snd_pcm_avail() goes above this value and no more data * is available in the #ring_buffer, we need to play some @@ -348,6 +359,19 @@ private: cond.signal(); } + /** + * Callback for @silence_timer + */ + void OnSilenceTimer() noexcept { + { + const std::lock_guard<Mutex> lock(mutex); + assert(active); + waiting = false; + } + + MultiSocketMonitor::InvalidateSockets(); + } + /* virtual methods from class MultiSocketMonitor */ std::chrono::steady_clock::duration PrepareSockets() noexcept override; void DispatchSockets() noexcept override; @@ -359,6 +383,7 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block) :AudioOutput(FLAG_ENABLE_DISABLE), MultiSocketMonitor(_loop), defer_invalidate_sockets(_loop, BIND_THIS_METHOD(InvalidateSockets)), + silence_timer(_loop, BIND_THIS_METHOD(OnSilenceTimer)), device(block.GetBlockValue("device", "")), #ifdef ENABLE_DSD dop_setting(block.GetBlockValue("dop", false) || @@ -515,6 +540,7 @@ AlsaOutput::Setup(AudioFormat &audio_format, alsa_period_size = 1; period_frames = alsa_period_size; + effective_period_duration = audio_format.FramesToTime<decltype(effective_period_duration)>(period_frames); /* generate silence if there's less than one period of data in the ALSA-PCM buffer */ @@ -865,6 +891,7 @@ AlsaOutput::CancelInternal() noexcept MultiSocketMonitor::Reset(); defer_invalidate_sockets.Cancel(); + silence_timer.Cancel(); } void @@ -893,6 +920,7 @@ AlsaOutput::Close() noexcept BlockingCall(GetEventLoop(), [this](){ MultiSocketMonitor::Reset(); defer_invalidate_sockets.Cancel(); + silence_timer.Cancel(); }); period_buffer.Free(); @@ -1029,6 +1057,17 @@ try { if (!CopyRingToPeriodBuffer()) { MultiSocketMonitor::Reset(); defer_invalidate_sockets.Cancel(); + + /* just in case Play() doesn't get + called soon enough, schedule a + timer which generates silence + before the xrun occurs */ + /* the timer fires in half of a + period; this short duration may + produce a few more wakeups than + necessary, but should be small + enough to avoid the xrun */ + silence_timer.Schedule(effective_period_duration / 2); } return; From f780ac418a38a0a86ffcead364a27367b9210710 Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Fri, 28 Jun 2019 14:35:56 +0200 Subject: [PATCH 6/7] output/alsa: log when generating silence due to slow decoder MPD used to do that when this code lived in the player thread, but it was removed by commit 98a7c62d7a4f716d90af6d78e18d1a3b10bc54b3; and the replacement code in the ALSA output plugin didn't have it. --- NEWS | 1 + src/output/plugins/AlsaOutputPlugin.cxx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/NEWS b/NEWS index 354491ea5..8c8f514a5 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,7 @@ ver 0.21.11 (not yet released) - alsa: fix busy loop while draining - alsa: fix missing drain call - alsa: improve xrun-avoiding silence generator + - alsa: log when generating silence due to slow decoder - alsa, osx: fix distortions with DSD_U32 and DoP on 32 bit CPUs * protocol - fix "list" with multiple "group" levels diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index 7e1919e43..8ccdd2121 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -27,6 +27,7 @@ #include "../OutputAPI.hxx" #include "mixer/MixerList.hxx" #include "pcm/PcmExport.hxx" +#include "system/PeriodClock.hxx" #include "thread/Mutex.hxx" #include "thread/Cond.hxx" #include "util/Manual.hxx" @@ -65,6 +66,8 @@ class AlsaOutput final */ TimerEvent silence_timer; + PeriodClock throttle_silence_log; + Manual<PcmExport> pcm_export; /** @@ -1073,6 +1076,9 @@ try { return; } + if (throttle_silence_log.CheckUpdate(std::chrono::seconds(5))) + FormatWarning(alsa_output_domain, "Decoder is too slow; playing silence to avoid xrun"); + /* insert some silence if the buffer has not enough data yet, to avoid ALSA xrun */ period_buffer.FillWithSilence(silence, out_frame_size); From f6125f0c354491c47c854cd061af211bd8716b09 Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Wed, 3 Jul 2019 15:16:27 +0200 Subject: [PATCH 7/7] release v0.21.11 --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 8c8f514a5..b5972e4b5 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,4 @@ -ver 0.21.11 (not yet released) +ver 0.21.11 (2019/07/03) * input - tidal: deprecated because Tidal has changed the protocol * decoder