From 61a72a5d131e89f069d2d2de9411de8fe4c21f23 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 28 Jun 2019 14:51:27 +0200 Subject: [PATCH] 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 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 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(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;