
committed by
Max Kellermann

parent
f15b6a43d3
commit
509786cbf1
14
src/apple/AudioObject.cxx
Normal file
14
src/apple/AudioObject.cxx
Normal 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
79
src/apple/AudioObject.hxx
Normal 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
85
src/apple/AudioUnit.hxx
Normal 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
49
src/apple/ErrorRef.hxx
Normal 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
47
src/apple/StringRef.hxx
Normal 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
42
src/apple/Throw.cxx
Normal 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
19
src/apple/Throw.hxx
Normal 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
25
src/apple/meson.build
Normal 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,
|
||||
],
|
||||
)
|
53
src/mixer/plugins/OSXMixerPlugin.cxx
Normal file
53
src/mixer/plugins/OSXMixerPlugin.cxx
Normal 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,
|
||||
};
|
8
src/mixer/plugins/OSXMixerPlugin.hxx
Normal file
8
src/mixer/plugins/OSXMixerPlugin.hxx
Normal 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;
|
@@ -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
|
||||
|
@@ -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
|
||||
|
851
src/output/plugins/OSXOutputPlugin.cxx
Normal file
851
src/output/plugins/OSXOutputPlugin.cxx
Normal 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,
|
||||
};
|
17
src/output/plugins/OSXOutputPlugin.hxx
Normal file
17
src/output/plugins/OSXOutputPlugin.hxx
Normal 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
|
@@ -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,
|
||||
|
@@ -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
76
src/zeroconf/Bonjour.cxx
Normal 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
55
src/zeroconf/Bonjour.hxx
Normal 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
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user