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