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:
parent
47d1d3c855
commit
e89c421313
1
NEWS
1
NEWS
@ -24,6 +24,7 @@ ver 0.21 (not yet released)
|
|||||||
- ao: fix crash bug due to partial frames
|
- ao: fix crash bug due to partial frames
|
||||||
- shout: support the Shine encoder plugin
|
- shout: support the Shine encoder plugin
|
||||||
- sndio: remove support for the broken RoarAudio sndio emulation
|
- sndio: remove support for the broken RoarAudio sndio emulation
|
||||||
|
- osx: initial support for DSD over PCM
|
||||||
* mixer
|
* mixer
|
||||||
- sndio: new mixer plugin
|
- sndio: new mixer plugin
|
||||||
* encoder
|
* encoder
|
||||||
|
20
doc/user.xml
20
doc/user.xml
@ -4332,6 +4332,26 @@ run</programlisting>
|
|||||||
select the best possible for each file.
|
select the best possible for each file.
|
||||||
</entry>
|
</entry>
|
||||||
</row>
|
</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>
|
<row>
|
||||||
<entry>
|
<entry>
|
||||||
<varname>channel_map</varname>
|
<varname>channel_map</varname>
|
||||||
|
@ -24,6 +24,9 @@
|
|||||||
#include "util/ScopeExit.hxx"
|
#include "util/ScopeExit.hxx"
|
||||||
#include "util/RuntimeError.hxx"
|
#include "util/RuntimeError.hxx"
|
||||||
#include "util/Domain.hxx"
|
#include "util/Domain.hxx"
|
||||||
|
#include "util/Manual.hxx"
|
||||||
|
#include "util/ConstBuffer.hxx"
|
||||||
|
#include "pcm/PcmExport.hxx"
|
||||||
#include "thread/Mutex.hxx"
|
#include "thread/Mutex.hxx"
|
||||||
#include "thread/Cond.hxx"
|
#include "thread/Cond.hxx"
|
||||||
#include "system/ByteOrder.hxx"
|
#include "system/ByteOrder.hxx"
|
||||||
@ -46,10 +49,20 @@ struct OSXOutput final : AudioOutput {
|
|||||||
const char *channel_map;
|
const char *channel_map;
|
||||||
bool hog_device;
|
bool hog_device;
|
||||||
bool sync_sample_rate;
|
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;
|
AudioDeviceID dev_id;
|
||||||
AudioComponentInstance au;
|
AudioComponentInstance au;
|
||||||
AudioStreamBasicDescription asbd;
|
AudioStreamBasicDescription asbd;
|
||||||
|
Float64 sample_rate;
|
||||||
|
Manual<PcmExport> pcm_export;
|
||||||
|
|
||||||
boost::lockfree::spsc_queue<uint8_t> *ring_buffer;
|
boost::lockfree::spsc_queue<uint8_t> *ring_buffer;
|
||||||
|
|
||||||
@ -116,6 +129,9 @@ OSXOutput::OSXOutput(const ConfigBlock &block)
|
|||||||
channel_map = block.GetBlockValue("channel_map");
|
channel_map = block.GetBlockValue("channel_map");
|
||||||
hog_device = block.GetBlockValue("hog_device", false);
|
hog_device = block.GetBlockValue("hog_device", false);
|
||||||
sync_sample_rate = block.GetBlockValue("sync_sample_rate", false);
|
sync_sample_rate = block.GetBlockValue("sync_sample_rate", false);
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
dop_setting = block.GetBlockValue("dop", false);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioOutput *
|
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)
|
osx_output_sync_device_sample_rate(AudioDeviceID dev_id, AudioStreamBasicDescription desc)
|
||||||
{
|
{
|
||||||
FormatDebug(osx_output_domain, "Syncing sample rate.");
|
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 synced to %f Hz.",
|
||||||
sample_rate);
|
sample_rate);
|
||||||
}
|
}
|
||||||
|
return sample_rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
static OSStatus
|
static OSStatus
|
||||||
@ -581,11 +598,13 @@ OSXOutput::Enable()
|
|||||||
throw FormatRuntimeError("Unable to open OS X component: %s",
|
throw FormatRuntimeError("Unable to open OS X component: %s",
|
||||||
errormsg);
|
errormsg);
|
||||||
}
|
}
|
||||||
|
pcm_export.Construct();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
osx_output_set_device(this);
|
osx_output_set_device(this);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
AudioComponentInstanceDispose(au);
|
AudioComponentInstanceDispose(au);
|
||||||
|
pcm_export.Destruct();
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,6 +616,7 @@ void
|
|||||||
OSXOutput::Disable() noexcept
|
OSXOutput::Disable() noexcept
|
||||||
{
|
{
|
||||||
AudioComponentInstanceDispose(au);
|
AudioComponentInstanceDispose(au);
|
||||||
|
pcm_export.Destruct();
|
||||||
|
|
||||||
if (hog_device)
|
if (hog_device)
|
||||||
osx_output_hog_device(dev_id, false);
|
osx_output_hog_device(dev_id, false);
|
||||||
@ -615,9 +635,14 @@ void
|
|||||||
OSXOutput::Open(AudioFormat &audio_format)
|
OSXOutput::Open(AudioFormat &audio_format)
|
||||||
{
|
{
|
||||||
char errormsg[1024];
|
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));
|
memset(&asbd, 0, sizeof(asbd));
|
||||||
asbd.mSampleRate = audio_format.sample_rate;
|
|
||||||
asbd.mFormatID = kAudioFormatLinearPCM;
|
asbd.mFormatID = kAudioFormatLinearPCM;
|
||||||
asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
|
asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
|
||||||
|
|
||||||
@ -634,22 +659,50 @@ OSXOutput::Open(AudioFormat &audio_format)
|
|||||||
asbd.mBitsPerChannel = 32;
|
asbd.mBitsPerChannel = 32;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
#ifdef ENABLE_DSD
|
||||||
|
case SampleFormat::DSD:
|
||||||
|
if(dop) {
|
||||||
|
asbd.mBitsPerChannel = 24;
|
||||||
|
params.dop = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
default:
|
default:
|
||||||
audio_format.format = SampleFormat::S32;
|
audio_format.format = SampleFormat::S32;
|
||||||
asbd.mBitsPerChannel = 32;
|
asbd.mBitsPerChannel = 32;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate);
|
||||||
|
|
||||||
if (IsBigEndian())
|
if (IsBigEndian())
|
||||||
asbd.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;
|
asbd.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;
|
||||||
|
|
||||||
|
if (audio_format.format == SampleFormat::DSD)
|
||||||
|
asbd.mBytesPerPacket = 4 * audio_format.channels;
|
||||||
|
else
|
||||||
asbd.mBytesPerPacket = audio_format.GetFrameSize();
|
asbd.mBytesPerPacket = audio_format.GetFrameSize();
|
||||||
asbd.mFramesPerPacket = 1;
|
asbd.mFramesPerPacket = 1;
|
||||||
asbd.mBytesPerFrame = asbd.mBytesPerPacket;
|
asbd.mBytesPerFrame = asbd.mBytesPerPacket;
|
||||||
asbd.mChannelsPerFrame = audio_format.channels;
|
asbd.mChannelsPerFrame = audio_format.channels;
|
||||||
|
|
||||||
if (sync_sample_rate)
|
if (sync_sample_rate
|
||||||
osx_output_sync_device_sample_rate(dev_id, asbd);
|
#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 =
|
OSStatus status =
|
||||||
AudioUnitSetProperty(au, kAudioUnitProperty_StreamFormat,
|
AudioUnitSetProperty(au, kAudioUnitProperty_StreamFormat,
|
||||||
@ -687,9 +740,10 @@ OSXOutput::Open(AudioFormat &audio_format)
|
|||||||
throw FormatRuntimeError("Unable to set frame size: %s",
|
throw FormatRuntimeError("Unable to set frame size: %s",
|
||||||
errormsg);
|
errormsg);
|
||||||
}
|
}
|
||||||
|
pcm_export->Open(audio_format.format, audio_format.channels, params);
|
||||||
|
|
||||||
size_t ring_buffer_size = std::max<size_t>(buffer_frame_size,
|
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);
|
ring_buffer = new boost::lockfree::spsc_queue<uint8_t>(ring_buffer_size);
|
||||||
|
|
||||||
status = AudioOutputUnitStart(au);
|
status = AudioOutputUnitStart(au);
|
||||||
@ -704,7 +758,20 @@ OSXOutput::Open(AudioFormat &audio_format)
|
|||||||
size_t
|
size_t
|
||||||
OSXOutput::Play(const void *chunk, size_t size)
|
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
|
std::chrono::steady_clock::duration
|
||||||
|
Loading…
Reference in New Issue
Block a user