committed by
Max Kellermann
parent
f15b6a43d3
commit
509786cbf1
@@ -15,6 +15,7 @@
|
||||
#include "plugins/NullOutputPlugin.hxx"
|
||||
#include "plugins/OpenALOutputPlugin.hxx"
|
||||
#include "plugins/OssOutputPlugin.hxx"
|
||||
#include "plugins/OSXOutputPlugin.hxx"
|
||||
#include "plugins/PipeOutputPlugin.hxx"
|
||||
#include "plugins/PipeWireOutputPlugin.hxx"
|
||||
#include "plugins/PulseOutputPlugin.hxx"
|
||||
@@ -59,6 +60,9 @@ constinit const AudioOutputPlugin *const audio_output_plugins[] = {
|
||||
#ifdef HAVE_OPENAL
|
||||
&openal_output_plugin,
|
||||
#endif
|
||||
#ifdef HAVE_OSX
|
||||
&osx_output_plugin,
|
||||
#endif
|
||||
#ifdef ENABLE_SOLARIS_OUTPUT
|
||||
&solaris_output_plugin,
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,851 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright The Music Player Daemon Project
|
||||
|
||||
#include "config.h"
|
||||
#include "OSXOutputPlugin.hxx"
|
||||
#include "apple/AudioObject.hxx"
|
||||
#include "apple/AudioUnit.hxx"
|
||||
#include "apple/StringRef.hxx"
|
||||
#include "apple/Throw.hxx"
|
||||
#include "../OutputAPI.hxx"
|
||||
#include "mixer/plugins/OSXMixerPlugin.hxx"
|
||||
#include "lib/fmt/RuntimeError.hxx"
|
||||
#include "lib/fmt/ToBuffer.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#include "util/Manual.hxx"
|
||||
#include "pcm/Export.hxx"
|
||||
#include "thread/Mutex.hxx"
|
||||
#include "thread/Cond.hxx"
|
||||
#include "util/ByteOrder.hxx"
|
||||
#include "util/CharUtil.hxx"
|
||||
#include "util/RingBuffer.hxx"
|
||||
#include "util/StringAPI.hxx"
|
||||
#include "util/StringBuffer.hxx"
|
||||
#include "Log.hxx"
|
||||
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
#include <AudioUnit/AudioUnit.h>
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <CoreServices/CoreServices.h>
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
// Backward compatibility from OSX 12.0 API change
|
||||
#if (__MAC_OS_X_VERSION_MAX_ALLOWED >= 120000)
|
||||
#define KAUDIO_OBJECT_PROPERTY_ELEMENT_MM kAudioObjectPropertyElementMain
|
||||
#define KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV kAudioHardwareServiceDeviceProperty_VirtualMainVolume
|
||||
#else
|
||||
#define KAUDIO_OBJECT_PROPERTY_ELEMENT_MM kAudioObjectPropertyElementMaster
|
||||
#define KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV kAudioHardwareServiceDeviceProperty_VirtualMasterVolume
|
||||
#endif
|
||||
|
||||
static constexpr unsigned MPD_OSX_BUFFER_TIME_MS = 100;
|
||||
|
||||
static auto
|
||||
StreamDescriptionToString(const AudioStreamBasicDescription desc) noexcept
|
||||
{
|
||||
// Only convert the lpcm formats (nothing else supported / used by MPD)
|
||||
assert(desc.mFormatID == kAudioFormatLinearPCM);
|
||||
|
||||
return FmtBuffer<256>("{} channel {} {}interleaved {}-bit {} {} ({}Hz)",
|
||||
desc.mChannelsPerFrame,
|
||||
(desc.mFormatFlags & kAudioFormatFlagIsNonMixable) ? "" : "mixable",
|
||||
(desc.mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? "non-" : "",
|
||||
desc.mBitsPerChannel,
|
||||
(desc.mFormatFlags & kAudioFormatFlagIsFloat) ? "Float" : "SInt",
|
||||
(desc.mFormatFlags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE",
|
||||
desc.mSampleRate);
|
||||
}
|
||||
|
||||
|
||||
struct OSXOutput final : AudioOutput {
|
||||
/* configuration settings */
|
||||
OSType component_subtype;
|
||||
/* only applicable with kAudioUnitSubType_HALOutput */
|
||||
const char *device_name;
|
||||
const char *const channel_map;
|
||||
const bool hog_device;
|
||||
|
||||
bool pause;
|
||||
|
||||
/**
|
||||
* Is the audio unit "started", i.e. was AudioOutputUnitStart() called?
|
||||
*/
|
||||
bool started;
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
/**
|
||||
* Enable DSD over PCM according to the DoP standard?
|
||||
*
|
||||
* @see http://dsd-guide.com/dop-open-standard
|
||||
*/
|
||||
const bool dop_setting;
|
||||
bool dop_enabled;
|
||||
Manual<PcmExport> pcm_export;
|
||||
#endif
|
||||
|
||||
AudioDeviceID dev_id;
|
||||
AudioComponentInstance au;
|
||||
AudioStreamBasicDescription asbd;
|
||||
|
||||
using RingBuffer = ::RingBuffer<std::byte>;
|
||||
RingBuffer ring_buffer;
|
||||
|
||||
OSXOutput(const ConfigBlock &block);
|
||||
|
||||
static AudioOutput *Create(EventLoop &, const ConfigBlock &block);
|
||||
int GetVolume();
|
||||
void SetVolume(unsigned new_volume);
|
||||
|
||||
private:
|
||||
void Enable() override;
|
||||
void Disable() noexcept override;
|
||||
|
||||
void Open(AudioFormat &audio_format) override;
|
||||
void Close() noexcept override;
|
||||
|
||||
std::chrono::steady_clock::duration Delay() const noexcept override;
|
||||
std::size_t Play(std::span<const std::byte> src) override;
|
||||
bool Pause() override;
|
||||
void Cancel() noexcept override;
|
||||
};
|
||||
|
||||
static constexpr Domain osx_output_domain("osx_output");
|
||||
|
||||
static bool
|
||||
osx_output_test_default_device()
|
||||
{
|
||||
/* on a Mac, this is always the default plugin, if nothing
|
||||
else is configured */
|
||||
return true;
|
||||
}
|
||||
|
||||
OSXOutput::OSXOutput(const ConfigBlock &block)
|
||||
:AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE),
|
||||
channel_map(block.GetBlockValue("channel_map")),
|
||||
hog_device(block.GetBlockValue("hog_device", false))
|
||||
#ifdef ENABLE_DSD
|
||||
, dop_setting(block.GetBlockValue("dop", false))
|
||||
#endif
|
||||
{
|
||||
const char *device = block.GetBlockValue("device");
|
||||
|
||||
if (device == nullptr || StringIsEqual(device, "default")) {
|
||||
component_subtype = kAudioUnitSubType_DefaultOutput;
|
||||
device_name = nullptr;
|
||||
}
|
||||
else if (StringIsEqual(device, "system")) {
|
||||
component_subtype = kAudioUnitSubType_SystemOutput;
|
||||
device_name = nullptr;
|
||||
}
|
||||
else {
|
||||
component_subtype = kAudioUnitSubType_HALOutput;
|
||||
/* XXX am I supposed to strdup() this? */
|
||||
device_name = device;
|
||||
}
|
||||
}
|
||||
|
||||
AudioOutput *
|
||||
OSXOutput::Create(EventLoop &, const ConfigBlock &block)
|
||||
{
|
||||
OSXOutput *oo = new OSXOutput(block);
|
||||
|
||||
static constexpr AudioObjectPropertyAddress default_system_output_device{
|
||||
kAudioHardwarePropertyDefaultSystemOutputDevice,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
|
||||
};
|
||||
|
||||
static constexpr AudioObjectPropertyAddress default_output_device{
|
||||
kAudioHardwarePropertyDefaultOutputDevice,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
|
||||
};
|
||||
|
||||
const auto &aopa =
|
||||
oo->component_subtype == kAudioUnitSubType_SystemOutput
|
||||
// get system output dev_id if configured
|
||||
? default_system_output_device
|
||||
/* fallback to default device initially (can still be
|
||||
changed by osx_output_set_device) */
|
||||
: default_output_device;
|
||||
|
||||
AudioDeviceID dev_id = kAudioDeviceUnknown;
|
||||
UInt32 dev_id_size = sizeof(dev_id);
|
||||
AudioObjectGetPropertyData(kAudioObjectSystemObject,
|
||||
&aopa,
|
||||
0,
|
||||
NULL,
|
||||
&dev_id_size,
|
||||
&dev_id);
|
||||
oo->dev_id = dev_id;
|
||||
|
||||
return oo;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
OSXOutput::GetVolume()
|
||||
{
|
||||
static constexpr AudioObjectPropertyAddress aopa = {
|
||||
KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
|
||||
};
|
||||
|
||||
const auto vol = AudioObjectGetPropertyDataT<Float32>(dev_id,
|
||||
aopa);
|
||||
|
||||
return static_cast<int>(vol * 100.0f);
|
||||
}
|
||||
|
||||
void
|
||||
OSXOutput::SetVolume(unsigned new_volume)
|
||||
{
|
||||
Float32 vol = new_volume / 100.0;
|
||||
static constexpr AudioObjectPropertyAddress aopa = {
|
||||
KAUDIO_HARDWARE_SERVICE_DEVICE_PROPERTY_VV,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
|
||||
};
|
||||
UInt32 size = sizeof(vol);
|
||||
OSStatus status = AudioObjectSetPropertyData(dev_id,
|
||||
&aopa,
|
||||
0,
|
||||
NULL,
|
||||
size,
|
||||
&vol);
|
||||
|
||||
if (status != noErr)
|
||||
Apple::ThrowOSStatus(status);
|
||||
}
|
||||
|
||||
static void
|
||||
osx_output_parse_channel_map(const char *device_name,
|
||||
const char *channel_map_str,
|
||||
SInt32 channel_map[],
|
||||
UInt32 num_channels)
|
||||
{
|
||||
unsigned int inserted_channels = 0;
|
||||
bool want_number = true;
|
||||
|
||||
while (*channel_map_str) {
|
||||
if (inserted_channels >= num_channels)
|
||||
throw FmtRuntimeError("{}: channel map contains more than {} entries or trailing garbage",
|
||||
device_name, num_channels);
|
||||
|
||||
if (!want_number && *channel_map_str == ',') {
|
||||
++channel_map_str;
|
||||
want_number = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (want_number &&
|
||||
(IsDigitASCII(*channel_map_str) || *channel_map_str == '-')
|
||||
) {
|
||||
char *endptr;
|
||||
channel_map[inserted_channels] = strtol(channel_map_str, &endptr, 10);
|
||||
if (channel_map[inserted_channels] < -1)
|
||||
throw FmtRuntimeError("{}: channel map value {} not allowed (must be -1 or greater)",
|
||||
device_name, channel_map[inserted_channels]);
|
||||
|
||||
channel_map_str = endptr;
|
||||
want_number = false;
|
||||
FmtDebug(osx_output_domain,
|
||||
"{}: channel_map[{}] = {}",
|
||||
device_name, inserted_channels,
|
||||
channel_map[inserted_channels]);
|
||||
++inserted_channels;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw FmtRuntimeError("{}: invalid character {:?} in channel map",
|
||||
device_name, *channel_map_str);
|
||||
}
|
||||
|
||||
if (inserted_channels < num_channels)
|
||||
throw FmtRuntimeError("{}: channel map contains less than {} entries",
|
||||
device_name, num_channels);
|
||||
}
|
||||
|
||||
static UInt32
|
||||
AudioUnitGetChannelsPerFrame(AudioUnit inUnit)
|
||||
{
|
||||
const auto desc = AudioUnitGetPropertyT<AudioStreamBasicDescription>(inUnit,
|
||||
kAudioUnitProperty_StreamFormat,
|
||||
kAudioUnitScope_Output,
|
||||
0);
|
||||
return desc.mChannelsPerFrame;
|
||||
}
|
||||
|
||||
static void
|
||||
osx_output_set_channel_map(OSXOutput *oo)
|
||||
{
|
||||
OSStatus status;
|
||||
|
||||
const UInt32 num_channels = AudioUnitGetChannelsPerFrame(oo->au);
|
||||
auto channel_map = std::make_unique<SInt32[]>(num_channels);
|
||||
osx_output_parse_channel_map(oo->device_name,
|
||||
oo->channel_map,
|
||||
channel_map.get(),
|
||||
num_channels);
|
||||
|
||||
UInt32 size = num_channels * sizeof(SInt32);
|
||||
status = AudioUnitSetProperty(oo->au,
|
||||
kAudioOutputUnitProperty_ChannelMap,
|
||||
kAudioUnitScope_Input,
|
||||
0,
|
||||
channel_map.get(),
|
||||
size);
|
||||
if (status != noErr)
|
||||
Apple::ThrowOSStatus(status, "unable to set channel map");
|
||||
}
|
||||
|
||||
|
||||
static float
|
||||
osx_output_score_sample_rate(Float64 destination_rate, unsigned source_rate)
|
||||
{
|
||||
float score = 0;
|
||||
double int_portion;
|
||||
double frac_portion = modf(source_rate / destination_rate, &int_portion);
|
||||
// prefer sample rates that are multiples of the source sample rate
|
||||
if (frac_portion < 0.01 || frac_portion >= 0.99)
|
||||
score += 1000;
|
||||
// prefer exact matches over other multiples
|
||||
score += (int_portion == 1.0) ? 500 : 0;
|
||||
if (source_rate == destination_rate)
|
||||
score += 1000;
|
||||
else if (source_rate > destination_rate)
|
||||
score += (int_portion > 1 && int_portion < 100) ? (100 - int_portion) / 100 * 100 : 0;
|
||||
else
|
||||
score += (int_portion > 1 && int_portion < 100) ? (100 + int_portion) / 100 * 100 : 0;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
static float
|
||||
osx_output_score_format(const AudioStreamBasicDescription &format_desc,
|
||||
const AudioStreamBasicDescription &target_format)
|
||||
{
|
||||
float score = 0;
|
||||
// Score only linear PCM formats (everything else MPD cannot use)
|
||||
if (format_desc.mFormatID == kAudioFormatLinearPCM) {
|
||||
score += osx_output_score_sample_rate(format_desc.mSampleRate,
|
||||
target_format.mSampleRate);
|
||||
|
||||
// Just choose the stream / format with the highest number of output channels
|
||||
score += format_desc.mChannelsPerFrame * 5;
|
||||
|
||||
if (target_format.mFormatFlags == kLinearPCMFormatFlagIsFloat) {
|
||||
// for float, prefer the highest bitdepth we have
|
||||
if (format_desc.mBitsPerChannel >= 16)
|
||||
score += (format_desc.mBitsPerChannel / 8);
|
||||
} else {
|
||||
if (format_desc.mBitsPerChannel == target_format.mBitsPerChannel)
|
||||
score += 5;
|
||||
else if (format_desc.mBitsPerChannel > target_format.mBitsPerChannel)
|
||||
score += 1;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
static Float64
|
||||
osx_output_set_device_format(AudioDeviceID dev_id,
|
||||
const AudioStreamBasicDescription &target_format)
|
||||
{
|
||||
static constexpr AudioObjectPropertyAddress aopa_device_streams = {
|
||||
kAudioDevicePropertyStreams,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
|
||||
};
|
||||
|
||||
static constexpr AudioObjectPropertyAddress aopa_stream_direction = {
|
||||
kAudioStreamPropertyDirection,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
|
||||
};
|
||||
|
||||
static constexpr AudioObjectPropertyAddress aopa_stream_phys_formats = {
|
||||
kAudioStreamPropertyAvailablePhysicalFormats,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
|
||||
};
|
||||
|
||||
static constexpr AudioObjectPropertyAddress aopa_stream_phys_format = {
|
||||
kAudioStreamPropertyPhysicalFormat,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
|
||||
};
|
||||
|
||||
OSStatus err;
|
||||
|
||||
const auto streams =
|
||||
AudioObjectGetPropertyDataArray<AudioStreamID>(dev_id,
|
||||
aopa_device_streams);
|
||||
|
||||
bool format_found = false;
|
||||
int output_stream;
|
||||
AudioStreamBasicDescription output_format;
|
||||
|
||||
for (const auto stream : streams) {
|
||||
const auto direction =
|
||||
AudioObjectGetPropertyDataT<UInt32>(stream,
|
||||
aopa_stream_direction);
|
||||
if (direction != 0)
|
||||
continue;
|
||||
|
||||
const auto format_list =
|
||||
AudioObjectGetPropertyDataArray<AudioStreamRangedDescription>(stream,
|
||||
aopa_stream_phys_formats);
|
||||
|
||||
float output_score = 0;
|
||||
|
||||
for (const auto &format : format_list) {
|
||||
AudioStreamBasicDescription format_desc = format.mFormat;
|
||||
std::string format_string;
|
||||
|
||||
// for devices with kAudioStreamAnyRate
|
||||
// we use the requested samplerate here
|
||||
if (format_desc.mSampleRate == kAudioStreamAnyRate)
|
||||
format_desc.mSampleRate = target_format.mSampleRate;
|
||||
float score = osx_output_score_format(format_desc, target_format);
|
||||
|
||||
// print all (linear pcm) formats and their rating
|
||||
if (score > 0.0f)
|
||||
FmtDebug(osx_output_domain,
|
||||
"Format: {} rated {}",
|
||||
StreamDescriptionToString(format_desc).c_str(),
|
||||
score);
|
||||
|
||||
if (score > output_score) {
|
||||
output_score = score;
|
||||
output_format = format_desc;
|
||||
output_stream = stream; // set the idx of the stream in the device
|
||||
format_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (format_found) {
|
||||
err = AudioObjectSetPropertyData(output_stream,
|
||||
&aopa_stream_phys_format,
|
||||
0,
|
||||
NULL,
|
||||
sizeof(output_format),
|
||||
&output_format);
|
||||
if (err != noErr)
|
||||
throw FmtRuntimeError("Failed to change the stream format: {}",
|
||||
err);
|
||||
}
|
||||
|
||||
return output_format.mSampleRate;
|
||||
}
|
||||
|
||||
static UInt32
|
||||
osx_output_set_buffer_size(AudioUnit au, AudioStreamBasicDescription desc)
|
||||
{
|
||||
const auto value_range = AudioUnitGetPropertyT<AudioValueRange>(au,
|
||||
kAudioDevicePropertyBufferFrameSizeRange,
|
||||
kAudioUnitScope_Global,
|
||||
0);
|
||||
|
||||
try {
|
||||
AudioUnitSetBufferFrameSize(au, value_range.mMaximum);
|
||||
} catch (...) {
|
||||
LogError(std::current_exception(),
|
||||
"Failed to set maximum buffer size");
|
||||
}
|
||||
|
||||
auto buffer_frame_size = AudioUnitGetBufferFrameSize(au);
|
||||
buffer_frame_size *= desc.mBytesPerFrame;
|
||||
|
||||
// We set the frame size to a power of two integer that
|
||||
// is larger than buffer_frame_size.
|
||||
UInt32 frame_size = 1;
|
||||
while (frame_size < buffer_frame_size + 1)
|
||||
frame_size <<= 1;
|
||||
|
||||
return frame_size;
|
||||
}
|
||||
|
||||
static void
|
||||
osx_output_hog_device(AudioDeviceID dev_id, bool hog) noexcept
|
||||
{
|
||||
static constexpr AudioObjectPropertyAddress aopa = {
|
||||
kAudioDevicePropertyHogMode,
|
||||
kAudioObjectPropertyScopeOutput,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM
|
||||
};
|
||||
|
||||
pid_t hog_pid;
|
||||
|
||||
try {
|
||||
hog_pid = AudioObjectGetPropertyDataT<pid_t>(dev_id, aopa);
|
||||
} catch (...) {
|
||||
Log(LogLevel::DEBUG, std::current_exception(),
|
||||
"Failed to query HogMode");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hog) {
|
||||
if (hog_pid != -1) {
|
||||
LogDebug(osx_output_domain,
|
||||
"Device is already hogged");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (hog_pid != getpid()) {
|
||||
FmtDebug(osx_output_domain,
|
||||
"Device is not owned by this process");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
hog_pid = hog ? getpid() : -1;
|
||||
UInt32 size = sizeof(hog_pid);
|
||||
OSStatus err;
|
||||
err = AudioObjectSetPropertyData(dev_id,
|
||||
&aopa,
|
||||
0,
|
||||
NULL,
|
||||
size,
|
||||
&hog_pid);
|
||||
if (err != noErr) {
|
||||
FmtDebug(osx_output_domain,
|
||||
"Cannot hog the device: {}", err);
|
||||
} else {
|
||||
LogDebug(osx_output_domain,
|
||||
hog_pid == -1
|
||||
? "Device is unhogged"
|
||||
: "Device is hogged");
|
||||
}
|
||||
}
|
||||
|
||||
[[gnu::pure]]
|
||||
static bool
|
||||
IsAudioDeviceName(AudioDeviceID id, const char *expected_name) noexcept
|
||||
{
|
||||
static constexpr AudioObjectPropertyAddress aopa_name{
|
||||
kAudioObjectPropertyName,
|
||||
kAudioObjectPropertyScopeGlobal,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
|
||||
};
|
||||
|
||||
char actual_name[256];
|
||||
|
||||
try {
|
||||
auto cfname = AudioObjectGetStringProperty(id, aopa_name);
|
||||
if (!cfname.GetCString(actual_name, sizeof(actual_name)))
|
||||
return false;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringIsEqual(actual_name, expected_name);
|
||||
}
|
||||
|
||||
static AudioDeviceID
|
||||
FindAudioDeviceByName(const char *name)
|
||||
{
|
||||
/* what are the available audio device IDs? */
|
||||
static constexpr AudioObjectPropertyAddress aopa_hw_devices{
|
||||
kAudioHardwarePropertyDevices,
|
||||
kAudioObjectPropertyScopeGlobal,
|
||||
KAUDIO_OBJECT_PROPERTY_ELEMENT_MM,
|
||||
};
|
||||
|
||||
const auto ids =
|
||||
AudioObjectGetPropertyDataArray<AudioDeviceID>(kAudioObjectSystemObject,
|
||||
aopa_hw_devices);
|
||||
|
||||
for (const auto id : ids) {
|
||||
if (IsAudioDeviceName(id, name))
|
||||
return id;
|
||||
}
|
||||
|
||||
throw FmtRuntimeError("Found no audio device names {:?}", name);
|
||||
}
|
||||
|
||||
static void
|
||||
osx_output_set_device(OSXOutput *oo)
|
||||
{
|
||||
if (oo->component_subtype != kAudioUnitSubType_HALOutput)
|
||||
return;
|
||||
|
||||
const auto id = FindAudioDeviceByName(oo->device_name);
|
||||
|
||||
FmtDebug(osx_output_domain,
|
||||
"found matching device: ID={}, name={}",
|
||||
id, oo->device_name);
|
||||
|
||||
AudioUnitSetCurrentDevice(oo->au, id);
|
||||
|
||||
oo->dev_id = id;
|
||||
FmtDebug(osx_output_domain,
|
||||
"set OS X audio output device ID={}, name={}",
|
||||
id, oo->device_name);
|
||||
|
||||
if (oo->channel_map)
|
||||
osx_output_set_channel_map(oo);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function (the 'render callback' osx_render) is called by the
|
||||
* OS X audio subsystem (CoreAudio) to request audio data that will be
|
||||
* played by the audio hardware. This function has hard time
|
||||
* constraints so it cannot do IO (debug statements) or memory
|
||||
* allocations.
|
||||
*/
|
||||
static OSStatus
|
||||
osx_render(void *vdata,
|
||||
[[maybe_unused]] AudioUnitRenderActionFlags *io_action_flags,
|
||||
[[maybe_unused]] const AudioTimeStamp *in_timestamp,
|
||||
[[maybe_unused]] UInt32 in_bus_number,
|
||||
UInt32 in_number_frames,
|
||||
AudioBufferList *buffer_list)
|
||||
{
|
||||
OSXOutput *od = (OSXOutput *) vdata;
|
||||
|
||||
std::size_t count = in_number_frames * od->asbd.mBytesPerFrame;
|
||||
buffer_list->mBuffers[0].mDataByteSize =
|
||||
od->ring_buffer.ReadTo({(std::byte *)buffer_list->mBuffers[0].mData, count});
|
||||
return noErr;
|
||||
}
|
||||
|
||||
void
|
||||
OSXOutput::Enable()
|
||||
{
|
||||
AudioComponentDescription desc;
|
||||
desc.componentType = kAudioUnitType_Output;
|
||||
desc.componentSubType = component_subtype;
|
||||
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
|
||||
desc.componentFlags = 0;
|
||||
desc.componentFlagsMask = 0;
|
||||
|
||||
AudioComponent comp = AudioComponentFindNext(nullptr, &desc);
|
||||
if (comp == 0)
|
||||
throw std::runtime_error("Error finding OS X component");
|
||||
|
||||
OSStatus status = AudioComponentInstanceNew(comp, &au);
|
||||
if (status != noErr)
|
||||
Apple::ThrowOSStatus(status, "Unable to open OS X component");
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
pcm_export.Construct();
|
||||
#endif
|
||||
|
||||
try {
|
||||
osx_output_set_device(this);
|
||||
} catch (...) {
|
||||
AudioComponentInstanceDispose(au);
|
||||
#ifdef ENABLE_DSD
|
||||
pcm_export.Destruct();
|
||||
#endif
|
||||
throw;
|
||||
}
|
||||
|
||||
if (hog_device)
|
||||
osx_output_hog_device(dev_id, true);
|
||||
}
|
||||
|
||||
void
|
||||
OSXOutput::Disable() noexcept
|
||||
{
|
||||
AudioComponentInstanceDispose(au);
|
||||
#ifdef ENABLE_DSD
|
||||
pcm_export.Destruct();
|
||||
#endif
|
||||
|
||||
if (hog_device)
|
||||
osx_output_hog_device(dev_id, false);
|
||||
}
|
||||
|
||||
void
|
||||
OSXOutput::Close() noexcept
|
||||
{
|
||||
if (started)
|
||||
AudioOutputUnitStop(au);
|
||||
AudioUnitUninitialize(au);
|
||||
ring_buffer = {};
|
||||
}
|
||||
|
||||
void
|
||||
OSXOutput::Open(AudioFormat &audio_format)
|
||||
{
|
||||
#ifdef ENABLE_DSD
|
||||
PcmExport::Params params;
|
||||
params.alsa_channel_order = true;
|
||||
bool dop = dop_setting;
|
||||
#endif
|
||||
|
||||
memset(&asbd, 0, sizeof(asbd));
|
||||
asbd.mFormatID = kAudioFormatLinearPCM;
|
||||
if (audio_format.format == SampleFormat::FLOAT) {
|
||||
asbd.mFormatFlags = kLinearPCMFormatFlagIsFloat;
|
||||
} else {
|
||||
asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
|
||||
}
|
||||
|
||||
if (IsBigEndian())
|
||||
asbd.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;
|
||||
|
||||
if (audio_format.format == SampleFormat::S24_P32) {
|
||||
asbd.mBitsPerChannel = 24;
|
||||
} else {
|
||||
asbd.mBitsPerChannel = audio_format.GetSampleSize() * 8;
|
||||
}
|
||||
asbd.mBytesPerPacket = audio_format.GetFrameSize();
|
||||
asbd.mSampleRate = audio_format.sample_rate;
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (dop && audio_format.format == SampleFormat::DSD) {
|
||||
asbd.mBitsPerChannel = 24;
|
||||
params.dsd_mode = PcmExport::DsdMode::DOP;
|
||||
asbd.mSampleRate = params.CalcOutputSampleRate(audio_format.sample_rate);
|
||||
asbd.mBytesPerPacket = 4 * audio_format.channels;
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
asbd.mFramesPerPacket = 1;
|
||||
asbd.mBytesPerFrame = asbd.mBytesPerPacket;
|
||||
asbd.mChannelsPerFrame = audio_format.channels;
|
||||
|
||||
Float64 sample_rate = osx_output_set_device_format(dev_id, asbd);
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (audio_format.format == SampleFormat::DSD &&
|
||||
sample_rate != asbd.mSampleRate) {
|
||||
// fall back to PCM in case sample_rate cannot be synchronized
|
||||
params.dsd_mode = PcmExport::DsdMode::NONE;
|
||||
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;
|
||||
}
|
||||
dop_enabled = params.dsd_mode == PcmExport::DsdMode::DOP;
|
||||
#endif
|
||||
|
||||
AudioUnitSetInputStreamFormat(au, asbd);
|
||||
|
||||
AURenderCallbackStruct callback;
|
||||
callback.inputProc = osx_render;
|
||||
callback.inputProcRefCon = this;
|
||||
|
||||
AudioUnitSetInputRenderCallback(au, callback);
|
||||
|
||||
OSStatus status = AudioUnitInitialize(au);
|
||||
if (status != noErr)
|
||||
Apple::ThrowOSStatus(status, "Unable to initialize OS X audio unit");
|
||||
|
||||
UInt32 buffer_frame_size = osx_output_set_buffer_size(au, asbd);
|
||||
|
||||
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);
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (dop_enabled) {
|
||||
pcm_export->Open(audio_format.format, audio_format.channels, params);
|
||||
ring_buffer_size = std::max<size_t>(buffer_frame_size,
|
||||
MPD_OSX_BUFFER_TIME_MS * pcm_export->GetOutputFrameSize() * asbd.mSampleRate / 1000);
|
||||
}
|
||||
#endif
|
||||
ring_buffer = RingBuffer{ring_buffer_size};
|
||||
|
||||
pause = false;
|
||||
started = false;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
OSXOutput::Play(std::span<const std::byte> input)
|
||||
{
|
||||
assert(!input.empty());
|
||||
|
||||
pause = false;
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (dop_enabled) {
|
||||
input = pcm_export->Export(input);
|
||||
if (input.empty())
|
||||
return input.size();
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t bytes_written = ring_buffer.WriteFrom(input);
|
||||
|
||||
if (!started) {
|
||||
OSStatus status = AudioOutputUnitStart(au);
|
||||
if (status != noErr)
|
||||
throw std::runtime_error("Unable to restart audio output after pause");
|
||||
|
||||
started = true;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
if (dop_enabled)
|
||||
bytes_written = pcm_export->CalcInputSize(bytes_written);
|
||||
#endif
|
||||
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::duration
|
||||
OSXOutput::Delay() const noexcept
|
||||
{
|
||||
return !ring_buffer.IsFull() && !pause
|
||||
? std::chrono::steady_clock::duration::zero()
|
||||
: std::chrono::milliseconds(MPD_OSX_BUFFER_TIME_MS / 4);
|
||||
}
|
||||
|
||||
bool OSXOutput::Pause()
|
||||
{
|
||||
pause = true;
|
||||
|
||||
if (started) {
|
||||
AudioOutputUnitStop(au);
|
||||
started = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
OSXOutput::Cancel() noexcept
|
||||
{
|
||||
if (started) {
|
||||
AudioOutputUnitStop(au);
|
||||
started = false;
|
||||
}
|
||||
|
||||
ring_buffer.Clear();
|
||||
#ifdef ENABLE_DSD
|
||||
pcm_export->Reset();
|
||||
#endif
|
||||
|
||||
/* the AudioUnit will be restarted by the next Play() call */
|
||||
}
|
||||
|
||||
int
|
||||
osx_output_get_volume(OSXOutput &output)
|
||||
{
|
||||
return output.GetVolume();
|
||||
}
|
||||
|
||||
void
|
||||
osx_output_set_volume(OSXOutput &output, unsigned new_volume)
|
||||
{
|
||||
return output.SetVolume(new_volume);
|
||||
}
|
||||
|
||||
const struct AudioOutputPlugin osx_output_plugin = {
|
||||
"osx",
|
||||
osx_output_test_default_device,
|
||||
&OSXOutput::Create,
|
||||
&osx_mixer_plugin,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright The Music Player Daemon Project
|
||||
|
||||
#ifndef MPD_OSX_OUTPUT_PLUGIN_HXX
|
||||
#define MPD_OSX_OUTPUT_PLUGIN_HXX
|
||||
|
||||
struct OSXOutput;
|
||||
|
||||
extern const struct AudioOutputPlugin osx_output_plugin;
|
||||
|
||||
int
|
||||
osx_output_get_volume(OSXOutput &output);
|
||||
|
||||
void
|
||||
osx_output_set_volume(OSXOutput &output, unsigned new_volume);
|
||||
|
||||
#endif
|
||||
@@ -7,8 +7,16 @@
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef __APPLE__
|
||||
#include <AL/al.h>
|
||||
#include <AL/alc.h>
|
||||
#else
|
||||
#include <OpenAL/al.h>
|
||||
#include <OpenAL/alc.h>
|
||||
/* on macOS, OpenAL is deprecated, but since the user asked to enable
|
||||
this plugin, let's ignore the compiler warnings */
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
|
||||
class OpenALOutput final : AudioOutput {
|
||||
/* should be enough for buffer size = 2048 */
|
||||
|
||||
@@ -49,7 +49,13 @@ endif
|
||||
|
||||
openal_dep = dependency('', required: false)
|
||||
if not get_option('openal').disabled()
|
||||
openal_dep = dependency('openal', required: false)
|
||||
if is_darwin
|
||||
if compiler.has_header('OpenAL/al.h')
|
||||
openal_dep = declare_dependency(link_args: ['-framework', 'OpenAL'])
|
||||
endif
|
||||
else
|
||||
openal_dep = dependency('openal', required: false)
|
||||
endif
|
||||
|
||||
if openal_dep.found()
|
||||
output_plugins_sources += 'OpenALOutputPlugin.cxx'
|
||||
@@ -63,6 +69,13 @@ if enable_oss
|
||||
output_plugins_sources += 'OssOutputPlugin.cxx'
|
||||
endif
|
||||
|
||||
if is_darwin
|
||||
output_plugins_sources += [
|
||||
'OSXOutputPlugin.cxx',
|
||||
]
|
||||
endif
|
||||
output_features.set('HAVE_OSX', is_darwin)
|
||||
|
||||
enable_pipe_output = get_option('pipe') and not is_windows
|
||||
output_features.set('ENABLE_PIPE_OUTPUT', enable_pipe_output)
|
||||
if enable_pipe_output
|
||||
@@ -153,6 +166,7 @@ output_plugins = static_library(
|
||||
include_directories: inc,
|
||||
dependencies: [
|
||||
alsa_dep,
|
||||
apple_dep,
|
||||
libao_dep,
|
||||
libjack_dep,
|
||||
pipewire_dep,
|
||||
|
||||
Reference in New Issue
Block a user