61a72a5d13
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.
1107 lines
26 KiB
C++
1107 lines
26 KiB
C++
/*
|
|
* Copyright 2003-2018 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.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "AlsaOutputPlugin.hxx"
|
|
#include "lib/alsa/AllowedFormat.hxx"
|
|
#include "lib/alsa/HwSetup.hxx"
|
|
#include "lib/alsa/NonBlock.hxx"
|
|
#include "lib/alsa/PeriodBuffer.hxx"
|
|
#include "lib/alsa/Version.hxx"
|
|
#include "../OutputAPI.hxx"
|
|
#include "mixer/MixerList.hxx"
|
|
#include "pcm/PcmExport.hxx"
|
|
#include "thread/Mutex.hxx"
|
|
#include "thread/Cond.hxx"
|
|
#include "util/Manual.hxx"
|
|
#include "util/RuntimeError.hxx"
|
|
#include "util/Domain.hxx"
|
|
#include "util/ConstBuffer.hxx"
|
|
#include "util/ScopeExit.hxx"
|
|
#include "util/StringView.hxx"
|
|
#include "event/MultiSocketMonitor.hxx"
|
|
#include "event/DeferEvent.hxx"
|
|
#include "event/Call.hxx"
|
|
#include "Log.hxx"
|
|
|
|
#include <alsa/asoundlib.h>
|
|
|
|
#include <boost/lockfree/spsc_queue.hpp>
|
|
|
|
#include <string>
|
|
#include <forward_list>
|
|
|
|
static const char default_device[] = "default";
|
|
|
|
static constexpr unsigned MPD_ALSA_BUFFER_TIME_US = 500000;
|
|
|
|
class AlsaOutput final
|
|
: AudioOutput, MultiSocketMonitor {
|
|
|
|
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;
|
|
|
|
/**
|
|
* The configured name of the ALSA device; empty for the
|
|
* default device
|
|
*/
|
|
const std::string device;
|
|
|
|
#ifdef ENABLE_DSD
|
|
/**
|
|
* Enable DSD over PCM according to the DoP standard?
|
|
*
|
|
* @see http://dsd-guide.com/dop-open-standard
|
|
*/
|
|
bool dop_setting;
|
|
#endif
|
|
|
|
/** libasound's buffer_time setting (in microseconds) */
|
|
const unsigned buffer_time;
|
|
|
|
/** libasound's period_time setting (in microseconds) */
|
|
const unsigned period_time;
|
|
|
|
/** the mode flags passed to snd_pcm_open */
|
|
int mode = 0;
|
|
|
|
std::forward_list<Alsa::AllowedFormat> allowed_formats;
|
|
|
|
/**
|
|
* Protects #dop_setting and #allowed_formats.
|
|
*/
|
|
mutable Mutex attributes_mutex;
|
|
|
|
/** the libasound PCM device handle */
|
|
snd_pcm_t *pcm;
|
|
|
|
#ifndef NDEBUG
|
|
/**
|
|
* The size of one audio frame passed to method play().
|
|
*/
|
|
size_t in_frame_size;
|
|
#endif
|
|
|
|
/**
|
|
* The size of one audio frame passed to libasound.
|
|
*/
|
|
size_t out_frame_size;
|
|
|
|
/**
|
|
* The size of one period, in number of frames.
|
|
*/
|
|
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
|
|
* silence.
|
|
*/
|
|
snd_pcm_sframes_t max_avail_frames;
|
|
|
|
/**
|
|
* Is this a buggy alsa-lib version, which needs a workaround
|
|
* for the snd_pcm_drain() bug always returning -EAGAIN? See
|
|
* alsa-lib commits fdc898d41135 and e4377b16454f for details.
|
|
* This bug was fixed in alsa-lib version 1.1.4.
|
|
*
|
|
* The workaround is to re-enable blocking mode for the
|
|
* snd_pcm_drain() call.
|
|
*/
|
|
bool work_around_drain_bug;
|
|
|
|
/**
|
|
* After Open() or Cancel(), has this output been activated by
|
|
* a Play() command?
|
|
*
|
|
* Protected by #mutex.
|
|
*/
|
|
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
|
|
* calling snd_pcm_drop().
|
|
*
|
|
* Without this flag, we could easily recover after a failed
|
|
* optimistic write (returning -EBADFD), but the Raspberry Pi
|
|
* audio driver is infamous for generating ugly artefacts from
|
|
* this.
|
|
*/
|
|
bool must_prepare;
|
|
|
|
/**
|
|
* Has snd_pcm_writei() been called successfully at least once
|
|
* since the PCM was prepared?
|
|
*
|
|
* This is necessary to work around a kernel bug which causes
|
|
* snd_pcm_drain() to return -EAGAIN forever in non-blocking
|
|
* mode if snd_pcm_writei() was never called.
|
|
*/
|
|
bool written;
|
|
|
|
bool drain;
|
|
|
|
/**
|
|
* This buffer gets allocated after opening the ALSA device.
|
|
* It contains silence samples, enough to fill one period (see
|
|
* #period_frames).
|
|
*/
|
|
uint8_t *silence;
|
|
|
|
AlsaNonBlockPcm non_block;
|
|
|
|
/**
|
|
* For copying data from OutputThread to IOThread.
|
|
*/
|
|
boost::lockfree::spsc_queue<uint8_t> *ring_buffer;
|
|
|
|
Alsa::PeriodBuffer period_buffer;
|
|
|
|
/**
|
|
* Protects #cond, #error, #active, #waiting, #drain.
|
|
*/
|
|
mutable Mutex mutex;
|
|
|
|
/**
|
|
* Used to wait when #ring_buffer is full. It will be
|
|
* signalled each time data is popped from the #ring_buffer,
|
|
* making space for more data.
|
|
*/
|
|
Cond cond;
|
|
|
|
std::exception_ptr error;
|
|
|
|
public:
|
|
AlsaOutput(EventLoop &loop, const ConfigBlock &block);
|
|
|
|
~AlsaOutput() noexcept {
|
|
/* free libasound's config cache */
|
|
snd_config_update_free_global();
|
|
}
|
|
|
|
using MultiSocketMonitor::GetEventLoop;
|
|
|
|
gcc_pure
|
|
const char *GetDevice() const noexcept {
|
|
return device.empty() ? default_device : device.c_str();
|
|
}
|
|
|
|
static AudioOutput *Create(EventLoop &event_loop,
|
|
const ConfigBlock &block) {
|
|
return new AlsaOutput(event_loop, block);
|
|
}
|
|
|
|
private:
|
|
const std::map<std::string, std::string> GetAttributes() const noexcept override;
|
|
void SetAttribute(std::string &&name, std::string &&value) override;
|
|
|
|
void Enable() override;
|
|
void Disable() noexcept override;
|
|
|
|
void Open(AudioFormat &audio_format) override;
|
|
void Close() noexcept override;
|
|
|
|
size_t Play(const void *chunk, size_t size) override;
|
|
void Drain() override;
|
|
void Cancel() noexcept override;
|
|
|
|
/**
|
|
* Set up the snd_pcm_t object which was opened by the caller.
|
|
* Set up the configured settings and the audio format.
|
|
*
|
|
* Throws #std::runtime_error on error.
|
|
*/
|
|
void Setup(AudioFormat &audio_format, PcmExport::Params ¶ms);
|
|
|
|
#ifdef ENABLE_DSD
|
|
void SetupDop(AudioFormat audio_format,
|
|
PcmExport::Params ¶ms);
|
|
#endif
|
|
|
|
void SetupOrDop(AudioFormat &audio_format, PcmExport::Params ¶ms
|
|
#ifdef ENABLE_DSD
|
|
, bool dop
|
|
#endif
|
|
);
|
|
|
|
gcc_pure
|
|
bool LockIsActive() const noexcept {
|
|
const std::lock_guard<Mutex> lock(mutex);
|
|
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
|
|
* has no effect; nothing will be played, and no code will be
|
|
* run on #EventLoop's thread.
|
|
*
|
|
* Caller must hold the mutex.
|
|
*
|
|
* @return true if Activate() was called, false if the mutex
|
|
* was never unlocked
|
|
*/
|
|
bool Activate() noexcept {
|
|
if (active && !waiting)
|
|
return false;
|
|
|
|
active = true;
|
|
waiting = false;
|
|
|
|
const ScopeUnlock unlock(mutex);
|
|
defer_invalidate_sockets.Schedule();
|
|
return true;
|
|
}
|
|
|
|
int Recover(int err) noexcept;
|
|
|
|
/**
|
|
* Drain all buffers. To be run in #EventLoop's thread.
|
|
*
|
|
* Throws on error.
|
|
*
|
|
* @return true if draining is complete, false if this method
|
|
* needs to be called again later
|
|
*/
|
|
bool DrainInternal();
|
|
|
|
/**
|
|
* Stop playback immediately, dropping all buffers. To be run
|
|
* in #EventLoop's thread.
|
|
*/
|
|
void CancelInternal() noexcept;
|
|
|
|
/**
|
|
* @return false if no data was moved
|
|
*/
|
|
bool CopyRingToPeriodBuffer() noexcept {
|
|
if (period_buffer.IsFull())
|
|
return false;
|
|
|
|
size_t nbytes = ring_buffer->pop(period_buffer.GetTail(),
|
|
period_buffer.GetSpaceBytes());
|
|
if (nbytes == 0)
|
|
return false;
|
|
|
|
period_buffer.AppendBytes(nbytes);
|
|
|
|
const std::lock_guard<Mutex> lock(mutex);
|
|
/* notify the OutputThread that there is now
|
|
room in ring_buffer */
|
|
cond.signal();
|
|
|
|
return true;
|
|
}
|
|
|
|
snd_pcm_sframes_t WriteFromPeriodBuffer() noexcept {
|
|
assert(!period_buffer.IsEmpty());
|
|
|
|
auto frames_written = snd_pcm_writei(pcm, period_buffer.GetHead(),
|
|
period_buffer.GetFrames(out_frame_size));
|
|
if (frames_written > 0) {
|
|
written = true;
|
|
period_buffer.ConsumeFrames(frames_written,
|
|
out_frame_size);
|
|
}
|
|
|
|
return frames_written;
|
|
}
|
|
|
|
void LockCaughtError() noexcept {
|
|
period_buffer.Clear();
|
|
|
|
const std::lock_guard<Mutex> lock(mutex);
|
|
error = std::current_exception();
|
|
active = false;
|
|
waiting = false;
|
|
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;
|
|
};
|
|
|
|
static constexpr Domain alsa_output_domain("alsa_output");
|
|
|
|
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) ||
|
|
/* legacy name from MPD 0.18 and older: */
|
|
block.GetBlockValue("dsd_usb", false)),
|
|
#endif
|
|
buffer_time(block.GetPositiveValue("buffer_time",
|
|
MPD_ALSA_BUFFER_TIME_US)),
|
|
period_time(block.GetPositiveValue("period_time", 0u))
|
|
{
|
|
#ifdef SND_PCM_NO_AUTO_RESAMPLE
|
|
if (!block.GetBlockValue("auto_resample", true))
|
|
mode |= SND_PCM_NO_AUTO_RESAMPLE;
|
|
#endif
|
|
|
|
#ifdef SND_PCM_NO_AUTO_CHANNELS
|
|
if (!block.GetBlockValue("auto_channels", true))
|
|
mode |= SND_PCM_NO_AUTO_CHANNELS;
|
|
#endif
|
|
|
|
#ifdef SND_PCM_NO_AUTO_FORMAT
|
|
if (!block.GetBlockValue("auto_format", true))
|
|
mode |= SND_PCM_NO_AUTO_FORMAT;
|
|
#endif
|
|
|
|
const char *allowed_formats_string =
|
|
block.GetBlockValue("allowed_formats", nullptr);
|
|
if (allowed_formats_string != nullptr)
|
|
allowed_formats = Alsa::AllowedFormat::ParseList(allowed_formats_string);
|
|
}
|
|
|
|
const std::map<std::string, std::string>
|
|
AlsaOutput::GetAttributes() const noexcept
|
|
{
|
|
const std::lock_guard<Mutex> lock(attributes_mutex);
|
|
|
|
return {
|
|
std::make_pair("allowed_formats",
|
|
Alsa::ToString(allowed_formats)),
|
|
#ifdef ENABLE_DSD
|
|
std::make_pair("dop", dop_setting ? "1" : "0"),
|
|
#endif
|
|
};
|
|
}
|
|
|
|
void
|
|
AlsaOutput::SetAttribute(std::string &&name, std::string &&value)
|
|
{
|
|
if (name == "allowed_formats") {
|
|
const std::lock_guard<Mutex> lock(attributes_mutex);
|
|
allowed_formats = Alsa::AllowedFormat::ParseList({value.data(), value.length()});
|
|
#ifdef ENABLE_DSD
|
|
} else if (name == "dop") {
|
|
const std::lock_guard<Mutex> lock(attributes_mutex);
|
|
if (value == "0")
|
|
dop_setting = false;
|
|
else if (value == "1")
|
|
dop_setting = true;
|
|
else
|
|
throw std::invalid_argument("Bad 'dop' value");
|
|
#endif
|
|
} else
|
|
AudioOutput::SetAttribute(std::move(name), std::move(value));
|
|
}
|
|
|
|
void
|
|
AlsaOutput::Enable()
|
|
{
|
|
pcm_export.Construct();
|
|
}
|
|
|
|
void
|
|
AlsaOutput::Disable() noexcept
|
|
{
|
|
pcm_export.Destruct();
|
|
}
|
|
|
|
static bool
|
|
alsa_test_default_device()
|
|
{
|
|
snd_pcm_t *handle;
|
|
|
|
int ret = snd_pcm_open(&handle, default_device,
|
|
SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
|
if (ret) {
|
|
FormatError(alsa_output_domain,
|
|
"Error opening default ALSA device: %s",
|
|
snd_strerror(-ret));
|
|
return false;
|
|
} else
|
|
snd_pcm_close(handle);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Wrapper for snd_pcm_sw_params().
|
|
*/
|
|
static void
|
|
AlsaSetupSw(snd_pcm_t *pcm, snd_pcm_uframes_t start_threshold,
|
|
snd_pcm_uframes_t avail_min)
|
|
{
|
|
snd_pcm_sw_params_t *swparams;
|
|
snd_pcm_sw_params_alloca(&swparams);
|
|
|
|
int err = snd_pcm_sw_params_current(pcm, swparams);
|
|
if (err < 0)
|
|
throw FormatRuntimeError("snd_pcm_sw_params_current() failed: %s",
|
|
snd_strerror(-err));
|
|
|
|
err = snd_pcm_sw_params_set_start_threshold(pcm, swparams,
|
|
start_threshold);
|
|
if (err < 0)
|
|
throw FormatRuntimeError("snd_pcm_sw_params_set_start_threshold() failed: %s",
|
|
snd_strerror(-err));
|
|
|
|
err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min);
|
|
if (err < 0)
|
|
throw FormatRuntimeError("snd_pcm_sw_params_set_avail_min() failed: %s",
|
|
snd_strerror(-err));
|
|
|
|
err = snd_pcm_sw_params(pcm, swparams);
|
|
if (err < 0)
|
|
throw FormatRuntimeError("snd_pcm_sw_params() failed: %s",
|
|
snd_strerror(-err));
|
|
}
|
|
|
|
inline void
|
|
AlsaOutput::Setup(AudioFormat &audio_format,
|
|
PcmExport::Params ¶ms)
|
|
{
|
|
const auto hw_result = Alsa::SetupHw(pcm,
|
|
buffer_time, period_time,
|
|
audio_format, params);
|
|
|
|
FormatDebug(alsa_output_domain, "format=%s (%s)",
|
|
snd_pcm_format_name(hw_result.format),
|
|
snd_pcm_format_description(hw_result.format));
|
|
|
|
FormatDebug(alsa_output_domain, "buffer_size=%u period_size=%u",
|
|
(unsigned)hw_result.buffer_size,
|
|
(unsigned)hw_result.period_size);
|
|
|
|
AlsaSetupSw(pcm, hw_result.buffer_size - hw_result.period_size,
|
|
hw_result.period_size);
|
|
|
|
auto alsa_period_size = hw_result.period_size;
|
|
if (alsa_period_size == 0)
|
|
/* this works around a SIGFPE bug that occurred when
|
|
an ALSA driver indicated period_size==0; this
|
|
caused a division by zero in alsa_play(). By using
|
|
the fallback "1", we make sure that this won't
|
|
happen again. */
|
|
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 */
|
|
max_avail_frames = hw_result.buffer_size - hw_result.period_size;
|
|
|
|
silence = new uint8_t[snd_pcm_frames_to_bytes(pcm, alsa_period_size)];
|
|
snd_pcm_format_set_silence(hw_result.format, silence,
|
|
alsa_period_size * audio_format.channels);
|
|
|
|
}
|
|
|
|
#ifdef ENABLE_DSD
|
|
|
|
inline void
|
|
AlsaOutput::SetupDop(const AudioFormat audio_format,
|
|
PcmExport::Params ¶ms)
|
|
{
|
|
assert(audio_format.format == SampleFormat::DSD);
|
|
|
|
/* pass 24 bit to AlsaSetup() */
|
|
|
|
AudioFormat dop_format = audio_format;
|
|
dop_format.format = SampleFormat::S24_P32;
|
|
|
|
const AudioFormat check = dop_format;
|
|
|
|
Setup(dop_format, params);
|
|
|
|
/* if the device allows only 32 bit, shift all DoP
|
|
samples left by 8 bit and leave the lower 8 bit cleared;
|
|
the DSD-over-USB documentation does not specify whether
|
|
this is legal, but there is anecdotical evidence that this
|
|
is possible (and the only option for some devices) */
|
|
params.shift8 = dop_format.format == SampleFormat::S32;
|
|
if (dop_format.format == SampleFormat::S32)
|
|
dop_format.format = SampleFormat::S24_P32;
|
|
|
|
if (dop_format != check) {
|
|
/* no bit-perfect playback, which is required
|
|
for DSD over USB */
|
|
delete[] silence;
|
|
throw std::runtime_error("Failed to configure DSD-over-PCM");
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
inline void
|
|
AlsaOutput::SetupOrDop(AudioFormat &audio_format, PcmExport::Params ¶ms
|
|
#ifdef ENABLE_DSD
|
|
, bool dop
|
|
#endif
|
|
)
|
|
{
|
|
#ifdef ENABLE_DSD
|
|
std::exception_ptr dop_error;
|
|
if (dop && audio_format.format == SampleFormat::DSD) {
|
|
try {
|
|
params.dop = true;
|
|
SetupDop(audio_format, params);
|
|
return;
|
|
} catch (...) {
|
|
dop_error = std::current_exception();
|
|
params.dop = false;
|
|
}
|
|
}
|
|
|
|
try {
|
|
#endif
|
|
Setup(audio_format, params);
|
|
#ifdef ENABLE_DSD
|
|
} catch (...) {
|
|
if (dop_error)
|
|
/* if DoP was attempted, prefer returning the
|
|
original DoP error instead of the fallback
|
|
error */
|
|
std::rethrow_exception(dop_error);
|
|
else
|
|
throw;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static constexpr bool
|
|
MaybeDmix(snd_pcm_type_t type)
|
|
{
|
|
return type == SND_PCM_TYPE_DMIX || type == SND_PCM_TYPE_PLUG;
|
|
}
|
|
|
|
gcc_pure
|
|
static bool
|
|
MaybeDmix(snd_pcm_t *pcm) noexcept
|
|
{
|
|
return MaybeDmix(snd_pcm_type(pcm));
|
|
}
|
|
|
|
static const Alsa::AllowedFormat &
|
|
BestMatch(const std::forward_list<Alsa::AllowedFormat> &haystack,
|
|
const AudioFormat &needle)
|
|
{
|
|
assert(!haystack.empty());
|
|
|
|
for (const auto &i : haystack)
|
|
if (needle.MatchMask(i.format))
|
|
return i;
|
|
|
|
return haystack.front();
|
|
}
|
|
|
|
void
|
|
AlsaOutput::Open(AudioFormat &audio_format)
|
|
{
|
|
#ifdef ENABLE_DSD
|
|
bool dop;
|
|
#endif
|
|
|
|
{
|
|
const std::lock_guard<Mutex> lock(attributes_mutex);
|
|
#ifdef ENABLE_DSD
|
|
dop = dop_setting;
|
|
#endif
|
|
|
|
if (!allowed_formats.empty()) {
|
|
const auto &a = BestMatch(allowed_formats,
|
|
audio_format);
|
|
audio_format.ApplyMask(a.format);
|
|
#ifdef ENABLE_DSD
|
|
dop = a.dop;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
int err = snd_pcm_open(&pcm, GetDevice(),
|
|
SND_PCM_STREAM_PLAYBACK, mode);
|
|
if (err < 0)
|
|
throw FormatRuntimeError("Failed to open ALSA device \"%s\": %s",
|
|
GetDevice(), snd_strerror(err));
|
|
|
|
FormatDebug(alsa_output_domain, "opened %s type=%s",
|
|
snd_pcm_name(pcm),
|
|
snd_pcm_type_name(snd_pcm_type(pcm)));
|
|
|
|
PcmExport::Params params;
|
|
params.alsa_channel_order = true;
|
|
|
|
try {
|
|
SetupOrDop(audio_format, params
|
|
#ifdef ENABLE_DSD
|
|
, dop
|
|
#endif
|
|
);
|
|
} catch (...) {
|
|
snd_pcm_close(pcm);
|
|
std::throw_with_nested(FormatRuntimeError("Error opening ALSA device \"%s\"",
|
|
GetDevice()));
|
|
}
|
|
|
|
work_around_drain_bug = MaybeDmix(pcm) &&
|
|
GetRuntimeAlsaVersion() < MakeAlsaVersion(1, 1, 4);
|
|
|
|
snd_pcm_nonblock(pcm, 1);
|
|
|
|
#ifdef ENABLE_DSD
|
|
if (params.dop)
|
|
FormatDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
|
|
#endif
|
|
|
|
pcm_export->Open(audio_format.format,
|
|
audio_format.channels,
|
|
params);
|
|
|
|
#ifndef NDEBUG
|
|
in_frame_size = audio_format.GetFrameSize();
|
|
#endif
|
|
out_frame_size = pcm_export->GetFrameSize(audio_format);
|
|
|
|
drain = false;
|
|
|
|
size_t period_size = period_frames * out_frame_size;
|
|
ring_buffer = new boost::lockfree::spsc_queue<uint8_t>(period_size * 4);
|
|
|
|
period_buffer.Allocate(period_frames, out_frame_size);
|
|
|
|
active = false;
|
|
waiting = false;
|
|
must_prepare = false;
|
|
written = false;
|
|
error = {};
|
|
}
|
|
|
|
inline int
|
|
AlsaOutput::Recover(int err) noexcept
|
|
{
|
|
if (err == -EPIPE) {
|
|
FormatDebug(alsa_output_domain,
|
|
"Underrun on ALSA device \"%s\"",
|
|
GetDevice());
|
|
} else if (err == -ESTRPIPE) {
|
|
FormatDebug(alsa_output_domain,
|
|
"ALSA device \"%s\" was suspended",
|
|
GetDevice());
|
|
}
|
|
|
|
switch (snd_pcm_state(pcm)) {
|
|
case SND_PCM_STATE_PAUSED:
|
|
err = snd_pcm_pause(pcm, /* disable */ 0);
|
|
break;
|
|
case SND_PCM_STATE_SUSPENDED:
|
|
err = snd_pcm_resume(pcm);
|
|
if (err == -EAGAIN)
|
|
return 0;
|
|
/* fall-through to snd_pcm_prepare: */
|
|
#if GCC_CHECK_VERSION(7,0)
|
|
[[fallthrough]];
|
|
#endif
|
|
case SND_PCM_STATE_OPEN:
|
|
case SND_PCM_STATE_SETUP:
|
|
case SND_PCM_STATE_XRUN:
|
|
period_buffer.Rewind();
|
|
written = false;
|
|
err = snd_pcm_prepare(pcm);
|
|
break;
|
|
case SND_PCM_STATE_DISCONNECTED:
|
|
break;
|
|
/* this is no error, so just keep running */
|
|
case SND_PCM_STATE_PREPARED:
|
|
case SND_PCM_STATE_RUNNING:
|
|
case SND_PCM_STATE_DRAINING:
|
|
err = 0;
|
|
break;
|
|
|
|
default:
|
|
/* this default case is just here to work around
|
|
-Wswitch due to SND_PCM_STATE_PRIVATE1 (libasound
|
|
1.1.6) */
|
|
break;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
inline bool
|
|
AlsaOutput::DrainInternal()
|
|
{
|
|
/* drain ring_buffer */
|
|
CopyRingToPeriodBuffer();
|
|
|
|
auto period_position = period_buffer.GetPeriodPosition(out_frame_size);
|
|
if (period_position > 0)
|
|
/* generate some silence to finish the partial
|
|
period */
|
|
period_buffer.FillWithSilence(silence, out_frame_size);
|
|
|
|
/* drain period_buffer */
|
|
if (!period_buffer.IsEmpty()) {
|
|
auto frames_written = WriteFromPeriodBuffer();
|
|
if (frames_written < 0) {
|
|
if (frames_written == -EAGAIN)
|
|
return false;
|
|
|
|
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
|
|
snd_strerror(-frames_written));
|
|
}
|
|
|
|
/* need to call CopyRingToPeriodBuffer() and
|
|
WriteFromPeriodBuffer() again in the next
|
|
iteration, so don't finish the drain just yet */
|
|
return false;
|
|
}
|
|
|
|
if (!written)
|
|
/* if nothing has ever been written to the PCM, we
|
|
don't need to drain it */
|
|
return true;
|
|
|
|
switch (snd_pcm_state(pcm)) {
|
|
case SND_PCM_STATE_PREPARED:
|
|
case SND_PCM_STATE_RUNNING:
|
|
/* these states require a call to snd_pcm_drain() */
|
|
break;
|
|
|
|
case SND_PCM_STATE_DRAINING:
|
|
/* already draining, but not yet finished; this is
|
|
probably a spurious epoll event, and we should wait
|
|
for the next one */
|
|
return false;
|
|
|
|
default:
|
|
/* all other states cannot be drained, and we're
|
|
done */
|
|
return true;
|
|
}
|
|
|
|
/* .. and finally drain the ALSA hardware buffer */
|
|
|
|
int result;
|
|
if (work_around_drain_bug) {
|
|
snd_pcm_nonblock(pcm, 0);
|
|
result = snd_pcm_drain(pcm);
|
|
snd_pcm_nonblock(pcm, 1);
|
|
} else
|
|
result = snd_pcm_drain(pcm);
|
|
|
|
if (result == 0)
|
|
return true;
|
|
else if (result == -EAGAIN)
|
|
return false;
|
|
else
|
|
throw FormatRuntimeError("snd_pcm_drain() failed: %s",
|
|
snd_strerror(-result));
|
|
}
|
|
|
|
void
|
|
AlsaOutput::Drain()
|
|
{
|
|
const std::lock_guard<Mutex> lock(mutex);
|
|
|
|
if (error)
|
|
std::rethrow_exception(error);
|
|
|
|
drain = true;
|
|
|
|
Activate();
|
|
|
|
while (drain && active)
|
|
cond.wait(mutex);
|
|
|
|
if (error)
|
|
std::rethrow_exception(error);
|
|
}
|
|
|
|
inline void
|
|
AlsaOutput::CancelInternal() noexcept
|
|
{
|
|
/* this method doesn't need to lock the mutex because while it
|
|
runs, the calling thread is blocked inside Cancel() */
|
|
|
|
must_prepare = true;
|
|
|
|
snd_pcm_drop(pcm);
|
|
|
|
pcm_export->Reset();
|
|
period_buffer.Clear();
|
|
ring_buffer->reset();
|
|
|
|
active = false;
|
|
waiting = false;
|
|
|
|
MultiSocketMonitor::Reset();
|
|
defer_invalidate_sockets.Cancel();
|
|
silence_timer.Cancel();
|
|
}
|
|
|
|
void
|
|
AlsaOutput::Cancel() noexcept
|
|
{
|
|
if (!LockIsActive()) {
|
|
/* early cancel, quick code path without thread
|
|
synchronization */
|
|
|
|
pcm_export->Reset();
|
|
assert(period_buffer.IsEmpty());
|
|
ring_buffer->reset();
|
|
|
|
return;
|
|
}
|
|
|
|
BlockingCall(GetEventLoop(), [this](){
|
|
CancelInternal();
|
|
});
|
|
}
|
|
|
|
void
|
|
AlsaOutput::Close() noexcept
|
|
{
|
|
/* make sure the I/O thread isn't inside DispatchSockets() */
|
|
BlockingCall(GetEventLoop(), [this](){
|
|
MultiSocketMonitor::Reset();
|
|
defer_invalidate_sockets.Cancel();
|
|
silence_timer.Cancel();
|
|
});
|
|
|
|
period_buffer.Free();
|
|
delete ring_buffer;
|
|
snd_pcm_close(pcm);
|
|
delete[] silence;
|
|
}
|
|
|
|
size_t
|
|
AlsaOutput::Play(const void *chunk, size_t size)
|
|
{
|
|
assert(size > 0);
|
|
assert(size % in_frame_size == 0);
|
|
|
|
const auto e = pcm_export->Export({chunk, size});
|
|
if (e.size == 0)
|
|
/* the DoP (DSD over PCM) filter converts two frames
|
|
at a time and ignores the last odd frame; if there
|
|
was only one frame (e.g. the last frame in the
|
|
file), the result is empty; to avoid an endless
|
|
loop, bail out here, and pretend the one frame has
|
|
been played */
|
|
return size;
|
|
|
|
const std::lock_guard<Mutex> lock(mutex);
|
|
|
|
while (true) {
|
|
if (error)
|
|
std::rethrow_exception(error);
|
|
|
|
size_t bytes_written = ring_buffer->push((const uint8_t *)e.data,
|
|
e.size);
|
|
if (bytes_written > 0)
|
|
return pcm_export->CalcSourceSize(bytes_written);
|
|
|
|
/* now that the ring_buffer is full, we can activate
|
|
the socket handlers to trigger the first
|
|
snd_pcm_writei() */
|
|
if (Activate())
|
|
/* since everything may have changed while the
|
|
mutex was unlocked, we need to skip the
|
|
cond.wait() call below and check the new
|
|
status */
|
|
continue;
|
|
|
|
/* wait for the DispatchSockets() to make room in the
|
|
ring_buffer */
|
|
cond.wait(mutex);
|
|
}
|
|
}
|
|
|
|
std::chrono::steady_clock::duration
|
|
AlsaOutput::PrepareSockets() noexcept
|
|
{
|
|
if (!LockIsActiveAndNotWaiting()) {
|
|
ClearSocketList();
|
|
return std::chrono::steady_clock::duration(-1);
|
|
}
|
|
|
|
try {
|
|
return non_block.PrepareSockets(*this, pcm);
|
|
} catch (...) {
|
|
ClearSocketList();
|
|
LockCaughtError();
|
|
return std::chrono::steady_clock::duration(-1);
|
|
}
|
|
}
|
|
|
|
void
|
|
AlsaOutput::DispatchSockets() noexcept
|
|
try {
|
|
non_block.DispatchSockets(*this, pcm);
|
|
|
|
if (must_prepare) {
|
|
must_prepare = false;
|
|
written = false;
|
|
|
|
int err = snd_pcm_prepare(pcm);
|
|
if (err < 0)
|
|
throw FormatRuntimeError("snd_pcm_prepare() failed: %s",
|
|
snd_strerror(-err));
|
|
}
|
|
|
|
{
|
|
const std::lock_guard<Mutex> lock(mutex);
|
|
|
|
assert(active);
|
|
|
|
if (drain) {
|
|
{
|
|
ScopeUnlock unlock(mutex);
|
|
if (!DrainInternal())
|
|
return;
|
|
|
|
MultiSocketMonitor::InvalidateSockets();
|
|
}
|
|
|
|
drain = false;
|
|
cond.signal();
|
|
return;
|
|
}
|
|
}
|
|
|
|
CopyRingToPeriodBuffer();
|
|
|
|
if (period_buffer.IsEmpty()) {
|
|
if (snd_pcm_state(pcm) == SND_PCM_STATE_PREPARED ||
|
|
snd_pcm_avail(pcm) <= max_avail_frames) {
|
|
/* at SND_PCM_STATE_PREPARED (not yet switched
|
|
to SND_PCM_STATE_RUNNING), we have no
|
|
pressure to fill the ALSA buffer, because
|
|
no xrun can possibly occur; and if no data
|
|
is available right now, we can easily wait
|
|
until some is available; so we just stop
|
|
monitoring the ALSA file descriptor, and
|
|
let it be reactivated by Play()/Activate()
|
|
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 happen at the
|
|
start of playback, when our ring_buffer is
|
|
smaller than the ALSA-PCM buffer */
|
|
|
|
{
|
|
const std::lock_guard<Mutex> lock(mutex);
|
|
waiting = true;
|
|
cond.signal();
|
|
}
|
|
|
|
/* avoid race condition: see if data has
|
|
arrived meanwhile before disabling the
|
|
event (but after setting the "waiting"
|
|
flag) */
|
|
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;
|
|
}
|
|
|
|
/* insert some silence if the buffer has not enough
|
|
data yet, to avoid ALSA xrun */
|
|
period_buffer.FillWithSilence(silence, out_frame_size);
|
|
}
|
|
|
|
auto frames_written = WriteFromPeriodBuffer();
|
|
if (frames_written < 0) {
|
|
if (frames_written == -EAGAIN || frames_written == -EINTR)
|
|
/* try again in the next DispatchSockets()
|
|
call which is still scheduled */
|
|
return;
|
|
|
|
if (Recover(frames_written) < 0)
|
|
throw FormatRuntimeError("snd_pcm_writei() failed: %s",
|
|
snd_strerror(-frames_written));
|
|
|
|
/* recovered; try again in the next DispatchSockets()
|
|
call */
|
|
return;
|
|
}
|
|
} catch (...) {
|
|
MultiSocketMonitor::Reset();
|
|
LockCaughtError();
|
|
}
|
|
|
|
const struct AudioOutputPlugin alsa_output_plugin = {
|
|
"alsa",
|
|
alsa_test_default_device,
|
|
&AlsaOutput::Create,
|
|
&alsa_mixer_plugin,
|
|
};
|