Initial support for DSD over PCM on macOS

From: Christian Kröner <ckroener@gmx.net>

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
This commit is contained in:
Christian Kröner 2018-02-26 13:11:45 +01:00 committed by Max Kellermann
parent 47d1d3c855
commit e89c421313
3 changed files with 95 additions and 7 deletions

1
NEWS
View File

@ -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

View File

@ -4332,6 +4332,26 @@ run</programlisting>
select the best possible for each file.
</entry>
</row>
<row>
<entry>
<varname>dop</varname>
<parameter>yes|no</parameter>
</entry>
<entry>
If set to <parameter>yes</parameter>, then DSD over
PCM according to the <ulink
url="http://dsd-guide.com/dop-open-standard">DoP
standard</ulink> 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
<parameter>no</parameter> 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.
</entry>
</row>
<row>
<entry>
<varname>channel_map</varname>

View File

@ -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<PcmExport> pcm_export;
boost::lockfree::spsc_queue<uint8_t> *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;
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<size_t>(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<uint8_t>(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