output/alsa: add option thesycon_dsd_workaround

This commit is contained in:
Max Kellermann 2021-11-04 14:21:02 +01:00
parent 356d13e9dd
commit 3413b1aeb4
2 changed files with 133 additions and 0 deletions

View File

@ -841,6 +841,11 @@ The `Advanced Linux Sound Architecture (ALSA) <http://www.alsa-project.org/>`_ p
("stop" or "pause") in DSD mode (native DSD or DoP). This is a
workaround for some DACs which emit noise when stopping DSD
playback.
* - **thesycon_dsd_workaround yes|no**
- If enabled, enables a workaround for a bug in Thesycon USB
audio receivers. On these devices, playing DSD512 or PCM
causes all subsequent attempts to play other DSD rates to fail,
which can be fixed by briefly playing PCM at 44.1 kHz.
* - **allowed_formats F1 F2 ...**
- Specifies a list of allowed audio formats, separated by a space. All items may contain asterisks as a wild card, and may be followed by "=dop" to enable DoP (DSD over PCM) for this particular format. The first matching format is used, and if none matches, MPD chooses the best fallback of this list.

View File

@ -43,6 +43,10 @@
#include "event/Call.hxx"
#include "Log.hxx"
#ifdef ENABLE_DSD
#include "util/AllocatedArray.hxx"
#endif
#include <alsa/asoundlib.h>
#include <boost/lockfree/spsc_queue.hpp>
@ -102,6 +106,16 @@ class AlsaOutput final
* Are we currently draining with #stop_dsd_silence?
*/
bool in_stop_dsd_silence;
/**
* Enable the DSD sync workaround for Thesycon USB audio
* receivers? On this device, playing DSD512 or PCM causes
* all subsequent attempts to play other DSD rates to fail,
* which can be fixed by briefly playing PCM at 44.1 kHz.
*/
const bool thesycon_dsd_workaround;
bool need_thesycon_dsd_workaround = thesycon_dsd_workaround;
#endif
/** libasound's buffer_time setting (in microseconds) */
@ -433,6 +447,8 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
/* legacy name from MPD 0.18 and older: */
block.GetBlockValue("dsd_usb", false)),
stop_dsd_silence(block.GetBlockValue("stop_dsd_silence", false)),
thesycon_dsd_workaround(block.GetBlockValue("thesycon_dsd_workaround",
false)),
#endif
buffer_time(block.GetPositiveValue("buffer_time",
MPD_ALSA_BUFFER_TIME_US)),
@ -675,6 +691,97 @@ BestMatch(const std::forward_list<Alsa::AllowedFormat> &haystack,
return haystack.front();
}
#ifdef ENABLE_DSD
static void
Play_44_1_Silence(snd_pcm_t *pcm)
{
snd_pcm_hw_params_t *hw;
snd_pcm_hw_params_alloca(&hw);
int err;
err = snd_pcm_hw_params_any(pcm, hw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_any() failed");
err = snd_pcm_hw_params_set_access(pcm, hw,
SND_PCM_ACCESS_RW_INTERLEAVED);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_access() failed");
err = snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S16);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_format() failed");
unsigned channels = 1;
err = snd_pcm_hw_params_set_channels_near(pcm, hw, &channels);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_channels_near() failed");
constexpr snd_pcm_uframes_t rate = 44100;
err = snd_pcm_hw_params_set_rate(pcm, hw, rate, 0);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_rate() failed");
snd_pcm_uframes_t buffer_size = 1;
err = snd_pcm_hw_params_set_buffer_size_near(pcm, hw, &buffer_size);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_buffer_size_near() failed");
snd_pcm_uframes_t period_size = 1;
int dir = 0;
err = snd_pcm_hw_params_set_period_size_near(pcm, hw, &period_size,
&dir);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params_set_period_size_near() failed");
err = snd_pcm_hw_params(pcm, hw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_hw_params() failed");
snd_pcm_sw_params_t *sw;
snd_pcm_sw_params_alloca(&sw);
err = snd_pcm_sw_params_current(pcm, sw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_sw_params_current() failed");
err = snd_pcm_sw_params_set_start_threshold(pcm, sw, period_size);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_sw_params_set_start_threshold() failed");
err = snd_pcm_sw_params(pcm, sw);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_sw_params() failed");
err = snd_pcm_prepare(pcm);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_prepare() failed");
AllocatedArray<int16_t> buffer{channels * period_size};
std::fill(buffer.begin(), buffer.end(), 0);
/* play at least 250ms of silence */
for (snd_pcm_uframes_t remaining_frames = rate / 4;;) {
auto n = snd_pcm_writei(pcm, buffer.data(),
period_size);
if (n < 0)
throw Alsa::MakeError(err, "snd_pcm_writei() failed");
if (snd_pcm_uframes_t(n) >= remaining_frames)
break;
remaining_frames -= snd_pcm_uframes_t(n);
}
err = snd_pcm_drain(pcm);
if (err < 0)
throw Alsa::MakeError(err, "snd_pcm_drain() failed");
}
#endif
void
AlsaOutput::Open(AudioFormat &audio_format)
{
@ -709,6 +816,22 @@ AlsaOutput::Open(AudioFormat &audio_format)
snd_pcm_name(pcm),
snd_pcm_type_name(snd_pcm_type(pcm)));
#ifdef ENABLE_DSD
if (need_thesycon_dsd_workaround &&
audio_format.format == SampleFormat::DSD &&
audio_format.sample_rate <= 256 * 44100 / 8) {
LogDebug(alsa_output_domain, "Playing some 44.1 kHz silence");
try {
Play_44_1_Silence(pcm);
} catch (...) {
LogError(std::current_exception());
}
need_thesycon_dsd_workaround = false;
}
#endif
PcmExport::Params params;
params.alsa_channel_order = true;
@ -733,6 +856,11 @@ AlsaOutput::Open(AudioFormat &audio_format)
use_dsd = audio_format.format == SampleFormat::DSD;
in_stop_dsd_silence = false;
if (thesycon_dsd_workaround &&
(!use_dsd ||
audio_format.sample_rate > 256 * 44100 / 8))
need_thesycon_dsd_workaround = true;
if (params.dsd_mode == PcmExport::DsdMode::DOP)
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
#endif