Revert "remove macOS support"

This reverts commit 518ce0187a.
This commit is contained in:
Camille Scholtz
2025-01-30 16:37:01 +01:00
committed by Max Kellermann
parent f15b6a43d3
commit 509786cbf1
33 changed files with 1659 additions and 40 deletions

14
src/apple/AudioObject.cxx Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#include "AudioObject.hxx"
#include "StringRef.hxx"
Apple::StringRef
AudioObjectGetStringProperty(AudioObjectID inObjectID,
const AudioObjectPropertyAddress &inAddress)
{
auto s = AudioObjectGetPropertyDataT<CFStringRef>(inObjectID,
inAddress);
return Apple::StringRef(s);
}

79
src/apple/AudioObject.hxx Normal file
View File

@@ -0,0 +1,79 @@
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#ifndef APPLE_AUDIO_OBJECT_HXX
#define APPLE_AUDIO_OBJECT_HXX
#include "Throw.hxx"
#include "util/AllocatedArray.hxx"
#include <CoreAudio/AudioHardware.h>
#include <cstddef>
namespace Apple {
class StringRef;
}
inline std::size_t
AudioObjectGetPropertyDataSize(AudioObjectID inObjectID,
const AudioObjectPropertyAddress &inAddress)
{
UInt32 size;
OSStatus status = AudioObjectGetPropertyDataSize(inObjectID,
&inAddress,
0, nullptr, &size);
if (status != noErr)
Apple::ThrowOSStatus(status);
return size;
}
template<typename T>
T
AudioObjectGetPropertyDataT(AudioObjectID inObjectID,
const AudioObjectPropertyAddress &inAddress)
{
OSStatus status;
UInt32 size = sizeof(T);
T value;
status = AudioObjectGetPropertyData(inObjectID, &inAddress,
0, nullptr,
&size, &value);
if (status != noErr)
Apple::ThrowOSStatus(status);
return value;
}
Apple::StringRef
AudioObjectGetStringProperty(AudioObjectID inObjectID,
const AudioObjectPropertyAddress &inAddress);
template<typename T>
AllocatedArray<T>
AudioObjectGetPropertyDataArray(AudioObjectID inObjectID,
const AudioObjectPropertyAddress &inAddress)
{
OSStatus status;
UInt32 size;
status = AudioObjectGetPropertyDataSize(inObjectID,
&inAddress,
0, nullptr, &size);
if (status != noErr)
Apple::ThrowOSStatus(status);
AllocatedArray<T> result(size / sizeof(T));
status = AudioObjectGetPropertyData(inObjectID, &inAddress,
0, nullptr,
&size, result.data());
if (status != noErr)
Apple::ThrowOSStatus(status);
return result;
}
#endif

85
src/apple/AudioUnit.hxx Normal file
View File

@@ -0,0 +1,85 @@
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#ifndef APPLE_AUDIO_UNIT_HXX
#define APPLE_AUDIO_UNIT_HXX
#include "Throw.hxx"
#include <AudioUnit/AudioUnit.h>
template<typename T>
T
AudioUnitGetPropertyT(AudioUnit inUnit, AudioUnitPropertyID inID,
AudioUnitScope inScope,
AudioUnitElement inElement)
{
UInt32 size = sizeof(T);
T value;
OSStatus status = AudioUnitGetProperty(inUnit, inID, inScope,
inElement,
&value, &size);
if (status != noErr)
Apple::ThrowOSStatus(status);
return value;
}
template<typename T>
void
AudioUnitSetPropertyT(AudioUnit inUnit, AudioUnitPropertyID inID,
AudioUnitScope inScope,
AudioUnitElement inElement,
const T &value)
{
OSStatus status = AudioUnitSetProperty(inUnit, inID, inScope,
inElement,
&value, sizeof(value));
if (status != noErr)
Apple::ThrowOSStatus(status);
}
inline void
AudioUnitSetCurrentDevice(AudioUnit inUnit, const AudioDeviceID &value)
{
AudioUnitSetPropertyT(inUnit, kAudioOutputUnitProperty_CurrentDevice,
kAudioUnitScope_Global, 0,
value);
}
inline void
AudioUnitSetInputStreamFormat(AudioUnit inUnit,
const AudioStreamBasicDescription &value)
{
AudioUnitSetPropertyT(inUnit, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, 0,
value);
}
inline void
AudioUnitSetInputRenderCallback(AudioUnit inUnit,
const AURenderCallbackStruct &value)
{
AudioUnitSetPropertyT(inUnit, kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input, 0,
value);
}
inline UInt32
AudioUnitGetBufferFrameSize(AudioUnit inUnit)
{
return AudioUnitGetPropertyT<UInt32>(inUnit,
kAudioDevicePropertyBufferFrameSize,
kAudioUnitScope_Global, 0);
}
inline void
AudioUnitSetBufferFrameSize(AudioUnit inUnit, const UInt32 &value)
{
AudioUnitSetPropertyT(inUnit, kAudioDevicePropertyBufferFrameSize,
kAudioUnitScope_Global, 0,
value);
}
#endif

49
src/apple/ErrorRef.hxx Normal file
View File

@@ -0,0 +1,49 @@
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#ifndef APPLE_ERROR_REF_HXX
#define APPLE_ERROR_REF_HXX
#include <CoreFoundation/CFError.h>
#include <utility>
namespace Apple {
class ErrorRef {
CFErrorRef ref = nullptr;
public:
explicit ErrorRef(CFErrorRef _ref) noexcept
:ref(_ref) {}
ErrorRef(CFAllocatorRef allocator, CFErrorDomain domain,
CFIndex code, CFDictionaryRef userInfo) noexcept
:ref(CFErrorCreate(allocator, domain, code, userInfo)) {}
ErrorRef(ErrorRef &&src) noexcept
:ref(std::exchange(src.ref, nullptr)) {}
~ErrorRef() noexcept {
if (ref)
CFRelease(ref);
}
ErrorRef &operator=(ErrorRef &&src) noexcept {
using std::swap;
swap(ref, src.ref);
return *this;
}
operator bool() const noexcept {
return ref != nullptr;
}
CFStringRef CopyDescription() const noexcept {
return CFErrorCopyDescription(ref);
}
};
} // namespace Apple
#endif

47
src/apple/StringRef.hxx Normal file
View File

@@ -0,0 +1,47 @@
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#ifndef APPLE_STRING_REF_HXX
#define APPLE_STRING_REF_HXX
#include <CoreFoundation/CFString.h>
#include <utility>
namespace Apple {
class StringRef {
CFStringRef ref = nullptr;
public:
explicit StringRef(CFStringRef _ref) noexcept
:ref(_ref) {}
StringRef(StringRef &&src) noexcept
:ref(std::exchange(src.ref, nullptr)) {}
~StringRef() noexcept {
if (ref)
CFRelease(ref);
}
StringRef &operator=(StringRef &&src) noexcept {
using std::swap;
swap(ref, src.ref);
return *this;
}
operator bool() const noexcept {
return ref != nullptr;
}
bool GetCString(char *buffer, std::size_t size,
CFStringEncoding encoding=kCFStringEncodingUTF8) const noexcept
{
return CFStringGetCString(ref, buffer, size, encoding);
}
};
} // namespace Apple
#endif

42
src/apple/Throw.cxx Normal file
View File

@@ -0,0 +1,42 @@
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#include "Throw.hxx"
#include "ErrorRef.hxx"
#include "StringRef.hxx"
#include <cstring>
#include <stdexcept>
namespace Apple {
void
ThrowOSStatus(OSStatus status)
{
const Apple::ErrorRef cferr(nullptr, kCFErrorDomainOSStatus,
status, nullptr);
const Apple::StringRef cfstr(cferr.CopyDescription());
char msg[1024];
if (!cfstr.GetCString(msg, sizeof(msg)))
throw std::runtime_error("Unknown OSStatus");
throw std::runtime_error(msg);
}
void
ThrowOSStatus(OSStatus status, const char *_msg)
{
const Apple::ErrorRef cferr(nullptr, kCFErrorDomainOSStatus,
status, nullptr);
const Apple::StringRef cfstr(cferr.CopyDescription());
char msg[1024];
std::strcpy(msg, _msg);
size_t length = std::strlen(msg);
cfstr.GetCString(msg + length, sizeof(msg) - length);
throw std::runtime_error(msg);
}
} // namespace Apple

19
src/apple/Throw.hxx Normal file
View File

@@ -0,0 +1,19 @@
// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>
#ifndef APPLE_THROW_HXX
#define APPLE_THROW_HXX
#include <CoreFoundation/CFBase.h>
namespace Apple {
void
ThrowOSStatus(OSStatus status);
void
ThrowOSStatus(OSStatus status, const char *msg);
} // namespace Apple
#endif

25
src/apple/meson.build Normal file
View File

@@ -0,0 +1,25 @@
if not is_darwin
apple_dep = dependency('', required: false)
subdir_done()
endif
audiounit_dep = declare_dependency(
link_args: ['-framework', 'AudioUnit', '-framework', 'CoreAudio', '-framework', 'CoreServices'],
)
apple = static_library(
'apple',
'AudioObject.cxx',
'Throw.cxx',
include_directories: inc,
dependencies: [
audiounit_dep,
],
)
apple_dep = declare_dependency(
link_with: apple,
dependencies: [
audiounit_dep,
],
)

View File

@@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#include "OSXMixerPlugin.hxx"
#include "mixer/Mixer.hxx"
#include "output/plugins/OSXOutputPlugin.hxx"
class OSXMixer final : public Mixer {
OSXOutput &output;
public:
OSXMixer(OSXOutput &_output, MixerListener &_listener)
:Mixer(osx_mixer_plugin, _listener),
output(_output)
{
}
/* virtual methods from class Mixer */
void Open() noexcept override {
}
void Close() noexcept override {
}
int GetVolume() override;
void SetVolume(unsigned volume) override;
};
int
OSXMixer::GetVolume()
{
return osx_output_get_volume(output);
}
void
OSXMixer::SetVolume(unsigned new_volume)
{
osx_output_set_volume(output, new_volume);
}
static Mixer *
osx_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao,
MixerListener &listener,
[[maybe_unused]] const ConfigBlock &block)
{
OSXOutput &osxo = (OSXOutput &)ao;
return new OSXMixer(osxo, listener);
}
const MixerPlugin osx_mixer_plugin = {
osx_mixer_init,
true,
};

View File

@@ -0,0 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#pragma once
struct MixerPlugin;
extern const MixerPlugin osx_mixer_plugin;

View File

@@ -14,6 +14,10 @@ if enable_oss
mixer_plugins_sources += 'OssMixerPlugin.cxx'
endif
if is_darwin
mixer_plugins_sources += 'OSXMixerPlugin.cxx'
endif
if pipewire_dep.found()
mixer_plugins_sources += 'PipeWireMixerPlugin.cxx'
endif

View File

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

View File

@@ -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,
};

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,11 @@ SetThreadName(const char *name) noexcept
requires a non-const pointer argument, which we don't have
here */
#ifdef __APPLE__
pthread_setname_np(name);
#else
pthread_setname_np(pthread_self(), name);
#endif
#elif defined(HAVE_PRCTL) && defined(PR_SET_NAME)
prctl(PR_SET_NAME, (unsigned long)name, 0, 0, 0);
#else

76
src/zeroconf/Bonjour.cxx Normal file
View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#include "Bonjour.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
#include <dns_sd.h>
#include <stdexcept>
#include <arpa/inet.h>
static constexpr Domain bonjour_domain("bonjour");
/**
* A wrapper for DNSServiceRegister() which returns the DNSServiceRef
* and throws on error.
*/
static DNSServiceRef
RegisterBonjour(const char *name, const char *type, unsigned port,
DNSServiceRegisterReply callback, void *ctx)
{
DNSServiceRef ref;
DNSServiceErrorType error = DNSServiceRegister(&ref,
0, 0, name, type,
nullptr, nullptr,
htons(port), 0,
nullptr,
callback, ctx);
if (error != kDNSServiceErr_NoError)
throw std::runtime_error("DNSServiceRegister() failed");
return ref;
}
BonjourHelper::BonjourHelper(EventLoop &_loop, const char *name,
const char *service_type, unsigned port)
:service_ref(RegisterBonjour(name, service_type, port,
Callback, this)),
socket_event(_loop,
BIND_THIS_METHOD(OnSocketReady),
SocketDescriptor(DNSServiceRefSockFD(service_ref)))
{
socket_event.ScheduleRead();
}
void
BonjourHelper::Callback([[maybe_unused]] DNSServiceRef sdRef,
[[maybe_unused]] DNSServiceFlags flags,
DNSServiceErrorType errorCode, const char *name,
[[maybe_unused]] const char *regtype,
[[maybe_unused]] const char *domain,
[[maybe_unused]] void *context) noexcept
{
auto &helper = *(BonjourHelper *)context;
if (errorCode != kDNSServiceErr_NoError) {
LogError(bonjour_domain,
"Failed to register zeroconf service");
helper.Cancel();
} else {
FmtDebug(bonjour_domain,
"Registered zeroconf service with name {:?}",
name);
}
}
std::unique_ptr<BonjourHelper>
BonjourInit(EventLoop &loop, const char *name,
const char *service_type, unsigned port)
{
return std::make_unique<BonjourHelper>(loop, name, service_type, port);
}

55
src/zeroconf/Bonjour.hxx Normal file
View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#ifndef MPD_ZEROCONF_BONJOUR_HXX
#define MPD_ZEROCONF_BONJOUR_HXX
#include "event/SocketEvent.hxx"
#include <dns_sd.h>
#include <memory>
class EventLoop;
class BonjourHelper final {
const DNSServiceRef service_ref;
SocketEvent socket_event;
public:
BonjourHelper(EventLoop &_loop, const char *name,
const char *service_name, unsigned port);
~BonjourHelper() noexcept {
DNSServiceRefDeallocate(service_ref);
}
BonjourHelper(const BonjourHelper &) = delete;
BonjourHelper &operator=(const BonjourHelper &) = delete;
private:
void Cancel() noexcept {
socket_event.Cancel();
}
static void Callback(DNSServiceRef sdRef, DNSServiceFlags flags,
DNSServiceErrorType errorCode, const char *name,
const char *regtype,
const char *domain,
void *context) noexcept;
/* virtual methods from class SocketMonitor */
void OnSocketReady([[maybe_unused]] unsigned flags) noexcept {
DNSServiceProcessResult(service_ref);
}
};
/**
* Throws on error.
*/
std::unique_ptr<BonjourHelper>
BonjourInit(EventLoop &loop, const char *name,
const char *service_type, unsigned port);
#endif

View File

@@ -3,13 +3,20 @@
#include "Glue.hxx"
#include "Helper.hxx"
#include "avahi/Helper.hxx"
#include "config/Data.hxx"
#include "config/Option.hxx"
#include "Listen.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
#ifdef HAVE_AVAHI
#include "avahi/Helper.hxx"
#endif
#ifdef HAVE_BONJOUR
#include "Bonjour.hxx"
#endif
#include <climits>
#include <string.h>

View File

@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#pragma once
#ifndef MPD_ZEROCONF_GLUE_HXX
#define MPD_ZEROCONF_GLUE_HXX
#include "Helper.hxx"
#include "config.h"
#include <memory>
@@ -11,8 +13,14 @@ struct ConfigData;
class EventLoop;
class ZeroconfHelper;
#ifdef HAVE_ZEROCONF
/**
* Throws on error.
*/
std::unique_ptr<ZeroconfHelper>
ZeroconfInit(const ConfigData &config, EventLoop &loop);
#endif /* ! HAVE_ZEROCONF */
#endif

View File

@@ -2,10 +2,19 @@
// Copyright The Music Player Daemon Project
#include "Helper.hxx"
#ifdef HAVE_AVAHI
#include "avahi/Helper.hxx"
#define CreateHelper AvahiInit
#endif
#ifdef HAVE_BONJOUR
#include "Bonjour.hxx"
#define CreateHelper BonjourInit
#endif
ZeroconfHelper::ZeroconfHelper(EventLoop &event_loop, const char *name,
const char *service_type, unsigned port)
:helper(AvahiInit(event_loop, name, service_type, port)) {}
:helper(CreateHelper(event_loop, name, service_type, port)) {}
ZeroconfHelper::~ZeroconfHelper() noexcept = default;

View File

@@ -1,15 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project
#pragma once
#ifndef MPD_ZEROCONF_HELPER_HXX
#define MPD_ZEROCONF_HELPER_HXX
#include "config.h"
#include <memory>
class EventLoop;
class AvahiHelper;
class BonjourHelper;
class ZeroconfHelper final {
#ifdef HAVE_AVAHI
std::unique_ptr<AvahiHelper> helper;
#endif
#ifdef HAVE_BONJOUR
std::unique_ptr<BonjourHelper> helper;
#endif
public:
ZeroconfHelper(EventLoop &event_loop, const char *name,
@@ -17,3 +27,5 @@ public:
~ZeroconfHelper() noexcept;
};
#endif

View File

@@ -3,7 +3,9 @@ zeroconf_option = get_option('zeroconf')
avahi_dep = dependency('', required: false)
if zeroconf_option == 'auto'
if is_android or is_windows
if is_darwin
zeroconf_option = 'bonjour'
elif is_android or is_windows
zeroconf_option = 'disabled'
elif dbus_dep.found()
zeroconf_option = 'avahi'
@@ -17,30 +19,61 @@ if zeroconf_option == 'disabled'
subdir_done()
endif
subdir('avahi')
if zeroconf_option == 'bonjour'
if not compiler.has_header('dns_sd.h')
error('dns_sd.h not found')
endif
if not avahi_dep.found()
zeroconf_dep = dependency('', required: false)
subdir_done()
bonjour_deps = [
]
if not is_darwin
bonjour_deps += declare_dependency(link_args: ['-ldns_sd'])
endif
conf.set('HAVE_BONJOUR', true)
zeroconf = static_library(
'zeroconf_bonjour',
'Glue.cxx',
'Helper.cxx',
'Bonjour.cxx',
include_directories: inc,
dependencies: [
event_dep,
log_dep,
],
)
zeroconf_dep = declare_dependency(
link_with: zeroconf,
dependencies: bonjour_deps,
)
else
subdir('avahi')
if not avahi_dep.found()
zeroconf_dep = dependency('', required: false)
subdir_done()
endif
conf.set('HAVE_AVAHI', true)
zeroconf = static_library(
'zeroconf_bonjour',
'Glue.cxx',
'Helper.cxx',
include_directories: inc,
dependencies: [
avahi_dep,
dbus_dep,
time_dep,
log_dep,
],
)
zeroconf_dep = declare_dependency(
link_with: zeroconf,
)
endif
conf.set('HAVE_AVAHI', true)
zeroconf = static_library(
'zeroconf',
'Glue.cxx',
'Helper.cxx',
include_directories: inc,
dependencies: [
avahi_dep,
dbus_dep,
time_dep,
log_dep,
],
)
zeroconf_dep = declare_dependency(
link_with: zeroconf,
)
conf.set('HAVE_ZEROCONF', true)