diff --git a/NEWS b/NEWS
index 05f0c4eae..fdc2a9b92 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
ver 0.23.3 (not yet released)
+* output
+ - alsa: add option "stop_dsd_silence" to work around DSD DAC noise
* macOS: fix libfmt related build failure
ver 0.23.2 (2021/10/22)
diff --git a/doc/plugins.rst b/doc/plugins.rst
index 18b176fd2..497fda069 100644
--- a/doc/plugins.rst
+++ b/doc/plugins.rst
@@ -836,6 +836,11 @@ The `Advanced Linux Sound Architecture (ALSA) `_ p
- If set to no, then libasound will not attempt to convert between different sample formats (16 bit, 24 bit, floating point, ...).
* - **dop yes|no**
- If set to yes, then DSD over PCM according to the `DoP standard `_ is enabled. This wraps DSD samples in fake 24 bit PCM, and is understood by some DSD capable products, but may be harmful to other hardware. Therefore, the default is no and you can enable the option at your own risk.
+ * - **stop_dsd_silence yes|no**
+ - If enabled, silence is played before manually stopping playback
+ ("stop" or "pause") in DSD mode (native DSD or DoP). This is a
+ workaround for some DACs which emit noise when stopping DSD
+ playback.
* - **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.
diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx
index 2a81eadb0..8b93a464f 100644
--- a/src/output/plugins/AlsaOutputPlugin.cxx
+++ b/src/output/plugins/AlsaOutputPlugin.cxx
@@ -84,6 +84,23 @@ class AlsaOutput final
* @see http://dsd-guide.com/dop-open-standard
*/
bool dop_setting;
+
+ /**
+ * Are we currently playing DSD? (Native DSD or DoP)
+ */
+ bool use_dsd;
+
+ /**
+ * Play some silence before closing the output in DSD mode?
+ * This is a workaround for some DACs which emit noise when
+ * stopping DSD playback.
+ */
+ const bool stop_dsd_silence;
+
+ /**
+ * Are we currently draining with #stop_dsd_silence?
+ */
+ bool in_stop_dsd_silence;
#endif
/** libasound's buffer_time setting (in microseconds) */
@@ -355,6 +372,9 @@ private:
error = std::current_exception();
active = false;
waiting = false;
+#ifdef ENABLE_DSD
+ in_stop_dsd_silence = false;
+#endif
cond.notify_one();
}
@@ -411,6 +431,7 @@ AlsaOutput::AlsaOutput(EventLoop &_loop, const ConfigBlock &block)
dop_setting(block.GetBlockValue("dop", false) ||
/* legacy name from MPD 0.18 and older: */
block.GetBlockValue("dsd_usb", false)),
+ stop_dsd_silence(block.GetBlockValue("stop_dsd_silence", false)),
#endif
buffer_time(block.GetPositiveValue("buffer_time",
MPD_ALSA_BUFFER_TIME_US)),
@@ -711,6 +732,9 @@ AlsaOutput::Open(AudioFormat &audio_format)
snd_pcm_nonblock(pcm, 1);
#ifdef ENABLE_DSD
+ use_dsd = audio_format.format == SampleFormat::DSD;
+ in_stop_dsd_silence = false;
+
if (params.dsd_mode == PcmExport::DsdMode::DOP)
LogDebug(alsa_output_domain, "DoP (DSD over PCM) enabled");
#endif
@@ -844,6 +868,18 @@ AlsaOutput::WriteFromPeriodBuffer() noexcept
inline bool
AlsaOutput::DrainInternal()
{
+#ifdef ENABLE_DSD
+ if (in_stop_dsd_silence) {
+ /* "stop_dsd_silence" is in progress: clear internal
+ buffers and instead, fill the period buffer with
+ silence */
+ in_stop_dsd_silence = false;
+ ring_buffer->reset();
+ period_buffer.Clear();
+ period_buffer.FillWithSilence(silence, out_frame_size);
+ }
+#endif
+
/* drain ring_buffer */
CopyRingToPeriodBuffer();
@@ -974,6 +1010,17 @@ AlsaOutput::Cancel() noexcept
return;
}
+#ifdef ENABLE_DSD
+ if (stop_dsd_silence && use_dsd) {
+ /* play some DSD silence instead of snd_pcm_drop() */
+ std::unique_lock lock(mutex);
+ in_stop_dsd_silence = true;
+ drain = true;
+ cond.wait(lock, [this]{ return !drain || !active; });
+ return;
+ }
+#endif
+
BlockingCall(GetEventLoop(), [this](){
CancelInternal();
});