From e89c421313b53ed98b249cccb9aee3a0d0344a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kr=C3=B6ner?= Date: Mon, 26 Feb 2018 13:11:45 +0100 Subject: [PATCH] Initial support for DSD over PCM on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From: Christian Kröner This just copies the necessary bits and pieces from the ALSA plugin and applies them to OSXOutput based on dop config setting. It only changes the OSXOutput plugin as needed for DoP (further changes to support additionally e.g. integer mode or setting the physical device mode require rather a complete rewrite of the output plugin). Fortunately the Core Audio API is by default bit perfect and supports DoP with minimal changes (setting the sampling rate accordingly after ensuring that the physical mode supports at least 24 bits per channel seems to be enough). This was tested on an Amanero Combo384 device hooked up to a ES9018 DAC. USAGE (try only on DACs that support DoP): - Add dop "yes" option to mpdconf - Be sure to set at least 24bits per channel before playing some DSD file (using Audio-MIDI-Setup) - Based on the dop setting, MPD will change the sample rate as required and output DoP signal to the DAC - Hog mode is recommended to ensure that no other program will try to mix some output with the DoP stream (resulting in bad noise) - Alternatively set the default output device to another device (e.g. the built-in output) to avoid having other audio interfere with DSD playback --- NEWS | 1 + doc/user.xml | 20 +++++++ src/output/plugins/OSXOutputPlugin.cxx | 81 +++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/NEWS b/NEWS index f2991a53f..4f8f2a0d0 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,7 @@ ver 0.21 (not yet released) - ao: fix crash bug due to partial frames - shout: support the Shine encoder plugin - sndio: remove support for the broken RoarAudio sndio emulation + - osx: initial support for DSD over PCM * mixer - sndio: new mixer plugin * encoder diff --git a/doc/user.xml b/doc/user.xml index e547f1279..2447dce45 100644 --- a/doc/user.xml +++ b/doc/user.xml @@ -4332,6 +4332,26 @@ run select the best possible for each file. + + + 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. Under macOS you must + make sure to select a physical mode on the output + device which supports at least 24 bits per channel + as the Mac OS X plugin only changes the sample rate. + + channel_map diff --git a/src/output/plugins/OSXOutputPlugin.cxx b/src/output/plugins/OSXOutputPlugin.cxx index c3d3966e2..b7baf785e 100644 --- a/src/output/plugins/OSXOutputPlugin.cxx +++ b/src/output/plugins/OSXOutputPlugin.cxx @@ -24,6 +24,9 @@ #include "util/ScopeExit.hxx" #include "util/RuntimeError.hxx" #include "util/Domain.hxx" +#include "util/Manual.hxx" +#include "util/ConstBuffer.hxx" +#include "pcm/PcmExport.hxx" #include "thread/Mutex.hxx" #include "thread/Cond.hxx" #include "system/ByteOrder.hxx" @@ -46,10 +49,20 @@ struct OSXOutput final : AudioOutput { const char *channel_map; bool hog_device; bool sync_sample_rate; +#ifdef ENABLE_DSD + /** + * Enable DSD over PCM according to the DoP standard? + * + * @see http://dsd-guide.com/dop-open-standard + */ + bool dop_setting; +#endif AudioDeviceID dev_id; AudioComponentInstance au; AudioStreamBasicDescription asbd; + Float64 sample_rate; + Manual pcm_export; boost::lockfree::spsc_queue *ring_buffer; @@ -116,6 +129,9 @@ OSXOutput::OSXOutput(const ConfigBlock &block) channel_map = block.GetBlockValue("channel_map"); hog_device = block.GetBlockValue("hog_device", false); sync_sample_rate = block.GetBlockValue("sync_sample_rate", false); +#ifdef ENABLE_DSD + dop_setting = block.GetBlockValue("dop", false); +#endif } AudioOutput * @@ -271,7 +287,7 @@ osx_output_set_channel_map(OSXOutput *oo) } } -static void +static Float64 osx_output_sync_device_sample_rate(AudioDeviceID dev_id, AudioStreamBasicDescription desc) { FormatDebug(osx_output_domain, "Syncing sample rate."); @@ -336,6 +352,7 @@ osx_output_sync_device_sample_rate(AudioDeviceID dev_id, AudioStreamBasicDescrip "Sample rate synced to %f Hz.", sample_rate); } + return sample_rate; } static OSStatus @@ -581,11 +598,13 @@ OSXOutput::Enable() throw FormatRuntimeError("Unable to open OS X component: %s", errormsg); } + pcm_export.Construct(); try { osx_output_set_device(this); } catch (...) { AudioComponentInstanceDispose(au); + pcm_export.Destruct(); throw; } @@ -597,6 +616,7 @@ void OSXOutput::Disable() noexcept { AudioComponentInstanceDispose(au); + pcm_export.Destruct(); if (hog_device) osx_output_hog_device(dev_id, false); @@ -615,9 +635,14 @@ void OSXOutput::Open(AudioFormat &audio_format) { char errormsg[1024]; +#ifdef ENABLE_DSD + bool dop = dop_setting; +#endif + PcmExport::Params params; + params.alsa_channel_order = true; + params.dop = false; memset(&asbd, 0, sizeof(asbd)); - asbd.mSampleRate = audio_format.sample_rate; asbd.mFormatID = kAudioFormatLinearPCM; asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger; @@ -634,22 +659,50 @@ OSXOutput::Open(AudioFormat &audio_format) asbd.mBitsPerChannel = 32; break; +#ifdef ENABLE_DSD + case SampleFormat::DSD: + if(dop) { + asbd.mBitsPerChannel = 24; + params.dop = true; + break; + } +#endif + default: audio_format.format = SampleFormat::S32; asbd.mBitsPerChannel = 32; break; } + asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate); if (IsBigEndian()) asbd.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian; - asbd.mBytesPerPacket = audio_format.GetFrameSize(); + if (audio_format.format == SampleFormat::DSD) + asbd.mBytesPerPacket = 4 * audio_format.channels; + else + asbd.mBytesPerPacket = audio_format.GetFrameSize(); asbd.mFramesPerPacket = 1; asbd.mBytesPerFrame = asbd.mBytesPerPacket; asbd.mChannelsPerFrame = audio_format.channels; - if (sync_sample_rate) - osx_output_sync_device_sample_rate(dev_id, asbd); + if (sync_sample_rate +#ifdef ENABLE_DSD + || params.dop // sample rate needs to be synchronized for DoP +#endif + ) + sample_rate = osx_output_sync_device_sample_rate(dev_id, asbd); + +#ifdef ENABLE_DSD + if(params.dop && (sample_rate != asbd.mSampleRate)) { // fall back to PCM in case sample_rate cannot be synchronized + params.dop = false; + audio_format.format = SampleFormat::S32; + asbd.mBitsPerChannel = 32; + asbd.mBytesPerPacket = audio_format.GetFrameSize(); + asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate); + asbd.mBytesPerFrame = asbd.mBytesPerPacket; + } +#endif OSStatus status = AudioUnitSetProperty(au, kAudioUnitProperty_StreamFormat, @@ -687,9 +740,10 @@ OSXOutput::Open(AudioFormat &audio_format) throw FormatRuntimeError("Unable to set frame size: %s", errormsg); } + pcm_export->Open(audio_format.format, audio_format.channels, params); size_t ring_buffer_size = std::max(buffer_frame_size, - MPD_OSX_BUFFER_TIME_MS * audio_format.GetFrameSize() * audio_format.sample_rate / 1000); + MPD_OSX_BUFFER_TIME_MS * pcm_export->GetFrameSize(audio_format) * asbd.mSampleRate / 1000); ring_buffer = new boost::lockfree::spsc_queue(ring_buffer_size); status = AudioOutputUnitStart(au); @@ -704,7 +758,20 @@ OSXOutput::Open(AudioFormat &audio_format) size_t OSXOutput::Play(const void *chunk, size_t size) { - return ring_buffer->push((uint8_t *)chunk, size); + assert(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; + + size_t bytes_written = ring_buffer->push((const uint8_t *)e.data, + e.size); + return pcm_export->CalcSourceSize(bytes_written); } std::chrono::steady_clock::duration