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 ccafe3f3cf in 0.21.3),
the silence generator didn't work reliably.
This commit is contained in:
Max Kellermann 2019-06-28 14:51:27 +02:00
parent 0c0a354753
commit 61a72a5d13
2 changed files with 40 additions and 0 deletions

1
NEWS
View File

@ -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

View File

@ -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;