1160 lines
41 KiB
Plaintext
1160 lines
41 KiB
Plaintext
// Copyright 2021-2023 The Mumble Developers. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license
|
|
// that can be found in the LICENSE file at the root of the
|
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
|
|
|
#include <AVFoundation/AVFoundation.h>
|
|
#include <IOKit/audio/IOAudioTypes.h>
|
|
#include <CoreAudio/AudioHardware.h>
|
|
#include "MainWindow.h"
|
|
#include "Global.h"
|
|
|
|
#include <exception>
|
|
#include <sstream>
|
|
#include "CoreAudio.h"
|
|
|
|
// Ignore deprecation warnings for the whole file, for now.
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
|
|
namespace {
|
|
extern "C" {
|
|
// The dirty hack used to disable the "compulsory" ducking offered by VoiceProcessingAU.
|
|
// A popular variant of this hack is `printf "p *(char*)(void(*)())AudioDeviceDuck=0xc3\nq" | lldb -n FaceTime`
|
|
// See https://github.com/chromium/chromium/blob/6acb61fb1f335a720c51ed20acec5b3a4a19f308/media/audio/mac/audio_low_latency_input_mac.cc#L38
|
|
// for reference.
|
|
OSStatus AudioDeviceDuck(AudioDeviceID inDevice,
|
|
Float32 inDuckedLevel,
|
|
const AudioTimeStamp* __nullable inStartTime,
|
|
Float32 inRampDuration) __attribute__((weak_import));
|
|
}
|
|
|
|
void UndoDucking(AudioDeviceID output_device_id) {
|
|
if (AudioDeviceDuck != nullptr) {
|
|
// Ramp the volume back up over half a second.
|
|
qDebug("CoreAudioInput: Undo Ducking caused by VoIP AU.");
|
|
AudioDeviceDuck(output_device_id, 1.0, nullptr, 0.5);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
namespace core_audio_utils {
|
|
|
|
class CoreAudioException : public std::exception {
|
|
private:
|
|
QString msg_;
|
|
public:
|
|
explicit CoreAudioException(const QString& msg) : msg_(msg) {}
|
|
~CoreAudioException() = default;
|
|
|
|
const char* what() const noexcept { return msg_.toUtf8().constData(); }
|
|
QString getMessage() const noexcept { return msg_; }
|
|
};
|
|
|
|
|
|
|
|
CFStringRef QStringToCFString(const QString &str) {
|
|
return CFStringCreateWithCharacters(kCFAllocatorDefault, reinterpret_cast< const UniChar * >(str.unicode()),
|
|
str.length());
|
|
}
|
|
|
|
QString GetDeviceStringProperty(AudioObjectID device_id, AudioObjectPropertySelector property_selector) {
|
|
CFStringRef property_value = nullptr;
|
|
UInt32 size = sizeof(property_value);
|
|
AudioObjectPropertyAddress property_address = {
|
|
property_selector,
|
|
kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
OSStatus result = AudioObjectGetPropertyData(
|
|
device_id, &property_address, 0,
|
|
nullptr, &size, &property_value);
|
|
if (result != noErr) {
|
|
throw CoreAudioException(QString("Unable to get string property %1 of %2.").arg(property_selector).arg(static_cast<int>(device_id)));
|
|
}
|
|
|
|
char buf[4096];
|
|
CFStringGetCString(property_value, buf, 4096, kCFStringEncodingUTF8);
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
UInt32 GetDeviceUint32Property(AudioObjectID device_id, AudioObjectPropertySelector property_selector, AudioObjectPropertyScope property_scope) {
|
|
AudioObjectPropertyAddress property_address = {
|
|
property_selector,
|
|
property_scope,
|
|
kAudioObjectPropertyElementMaster};
|
|
UInt32 property_value;
|
|
UInt32 size = sizeof(property_value);
|
|
OSStatus result = AudioObjectGetPropertyData(device_id, &property_address, 0, nullptr, &size, &property_value);
|
|
if (result != noErr) {
|
|
throw CoreAudioException(QString("Unable to get uint32 property %1 of %2.").arg(property_selector).arg(static_cast<int>(device_id)));
|
|
}
|
|
|
|
return property_value;
|
|
}
|
|
|
|
UInt32 GetDevicePropertySize(AudioObjectID device_id, AudioObjectPropertySelector property_selector, AudioObjectPropertyScope property_scope) {
|
|
AudioObjectPropertyAddress property_address = {
|
|
property_selector,
|
|
property_scope,
|
|
kAudioObjectPropertyElementMaster};
|
|
UInt32 size = 0;
|
|
OSStatus result = AudioObjectGetPropertyDataSize(device_id, &property_address, 0, nullptr, &size);
|
|
if (result != noErr) {
|
|
throw CoreAudioException(QString("Unable to get property size of %1 of %2.").arg(property_selector).arg(static_cast<int>(device_id)));
|
|
}
|
|
return size;
|
|
}
|
|
|
|
QVector<AudioObjectID> GetAudioObjectIDs(AudioObjectID audio_object_id, AudioObjectPropertySelector property_selector) {
|
|
AudioObjectPropertyAddress property_address = {
|
|
property_selector,
|
|
kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
UInt32 size = GetDevicePropertySize(audio_object_id, property_selector, kAudioObjectPropertyScopeGlobal);
|
|
|
|
if (size == 0)
|
|
return {};
|
|
|
|
int device_count = size / sizeof(AudioObjectID);
|
|
|
|
QVector<AudioObjectID> device_ids(device_count);
|
|
OSStatus result = AudioObjectGetPropertyData(audio_object_id, &property_address, 0,
|
|
nullptr , &size, device_ids.data());
|
|
if (result != noErr) {
|
|
throw CoreAudioException(QString("Unable to get object ids from %1.").arg(audio_object_id));
|
|
return {};
|
|
}
|
|
|
|
return device_ids;
|
|
}
|
|
|
|
AudioBufferList* GetDeviceStreamConfiguration(AudioObjectID device_id, AUDirection type) {
|
|
AudioBufferList *bufs = nullptr;
|
|
OSStatus err;
|
|
auto scope = (type == AUDirection::INPUT) ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput;
|
|
unsigned int len = GetDevicePropertySize(
|
|
device_id,
|
|
kAudioDevicePropertyStreamConfiguration,
|
|
scope);
|
|
|
|
AudioObjectPropertyAddress propertyAddress = { kAudioHardwarePropertyDevices, scope,
|
|
kAudioObjectPropertyElementMaster };
|
|
|
|
bufs = reinterpret_cast< AudioBufferList * >(malloc(len));
|
|
propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
|
|
err = AudioObjectGetPropertyData(device_id, &propertyAddress, 0, nullptr, &len, bufs);
|
|
if (!bufs || err != noErr) {
|
|
free(bufs);
|
|
throw CoreAudioException(QString("Failed to get AudioStreamConfiguration from device %1.").arg(device_id));
|
|
}
|
|
|
|
return bufs;
|
|
}
|
|
|
|
AudioDeviceID GetDeviceID(const QString& devUid, AUDirection type) {
|
|
OSStatus err;
|
|
UInt32 len;
|
|
CFStringRef csDevUid = QStringToCFString(devUid);
|
|
AudioDeviceID devId;
|
|
|
|
// Struct used to query information of the system audio setup
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
.mSelector = 0, // this attribute will be specified later
|
|
.mScope = (type == AUDirection::INPUT) ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
|
|
.mElement = kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
AudioValueTranslation avt;
|
|
avt.mInputData = const_cast< struct __CFString ** >(&csDevUid);
|
|
avt.mInputDataSize = sizeof(CFStringRef *);
|
|
avt.mOutputData = &devId;
|
|
avt.mOutputDataSize = sizeof(AudioDeviceID);
|
|
|
|
len = sizeof(AudioValueTranslation);
|
|
propertyAddress.mSelector = kAudioHardwarePropertyDeviceForUID;
|
|
err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, &len, &avt);
|
|
if (err != noErr) {
|
|
throw CoreAudioException(QString("Unable to query AudioDeviceID of %1.").arg(devUid));
|
|
}
|
|
|
|
return devId;
|
|
}
|
|
|
|
AudioDeviceID GetDefaultDeviceID(AUDirection type) {
|
|
OSStatus err;
|
|
UInt32 len;
|
|
AudioDeviceID devId;
|
|
|
|
// Struct used to query information of the system audio setup
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
.mSelector = 0, // this attribute will be specified later
|
|
.mScope = (type == AUDirection::INPUT) ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
|
|
.mElement = kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
len = sizeof(AudioDeviceID);
|
|
propertyAddress.mSelector = (type == AUDirection::INPUT) ? kAudioHardwarePropertyDefaultInputDevice : kAudioHardwarePropertyDefaultOutputDevice;
|
|
err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, &len, &devId);
|
|
if (err != noErr) {
|
|
throw CoreAudioException("Unable to query for default AudioDeviceID.");
|
|
}
|
|
|
|
return devId;
|
|
}
|
|
|
|
UInt32 GetDeviceTransportType(AudioObjectID devId){
|
|
return core_audio_utils::GetDeviceUint32Property(devId,
|
|
kAudioDevicePropertyTransportType,
|
|
kAudioObjectPropertyScopeGlobal);
|
|
}
|
|
|
|
bool IsInputDevice(AudioObjectID device_id) {
|
|
// This part of the code is modified from Chromium. Original comments are kept.
|
|
// See https://github.com/chromium/chromium/blob/6acb61fb1f335a720c51ed20acec5b3a4a19f308/media/audio/mac/core_audio_util_mac.cc
|
|
// for reference.
|
|
QVector<AudioObjectID> streams = GetAudioObjectIDs(device_id, kAudioDevicePropertyStreams);
|
|
|
|
int num_undefined_input_streams = 0;
|
|
int num_defined_input_streams = 0;
|
|
int num_output_streams = 0;
|
|
|
|
for (auto stream_id : streams) {
|
|
auto direction = GetDeviceUint32Property(stream_id, kAudioStreamPropertyDirection,
|
|
kAudioObjectPropertyScopeGlobal);
|
|
const UInt32 kDirectionOutput = 0;
|
|
const UInt32 kDirectionInput = 1;
|
|
if (direction == kDirectionOutput) {
|
|
++num_output_streams;
|
|
} else if (direction == kDirectionInput) {
|
|
// Filter input streams based on what terminal it claims to be attached
|
|
// to. Note that INPUT_UNDEFINED comes from a set of terminals declared
|
|
// in IOKit. CoreAudio defines a number of terminals in
|
|
// AudioHardwareBase.h but none of them match any of the values I've
|
|
// seen used in practice, though I've only tested a few devices.
|
|
auto terminal = GetDeviceUint32Property(stream_id, kAudioStreamPropertyTerminalType,
|
|
kAudioObjectPropertyScopeGlobal);
|
|
if (terminal == INPUT_UNDEFINED) {
|
|
++num_undefined_input_streams;
|
|
} else {
|
|
++num_defined_input_streams;
|
|
}
|
|
}
|
|
}
|
|
|
|
// I've only seen INPUT_UNDEFINED introduced by the VoiceProcessing AudioUnit,
|
|
// but to err on the side of caution, let's allow a device with only undefined
|
|
// input streams and no output streams as well.
|
|
return num_defined_input_streams > 0 ||
|
|
(num_undefined_input_streams > 0 && num_output_streams == 0);
|
|
}
|
|
|
|
bool IsOutputDevice(AudioObjectID device_id) {
|
|
QVector<AudioObjectID> streams = GetAudioObjectIDs(device_id, kAudioDevicePropertyStreams);
|
|
|
|
for (auto stream_id : streams) {
|
|
auto direction = GetDeviceUint32Property(stream_id, kAudioStreamPropertyDirection,
|
|
kAudioObjectPropertyScopeGlobal);
|
|
const UInt32 kDirectionOutput = 0;
|
|
if (direction == kDirectionOutput) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
static void LogStreamDescription(AudioStreamBasicDescription format) {
|
|
std::stringstream ss;
|
|
unsigned char formatID[5];
|
|
*(UInt32 *) formatID = OSSwapHostToBigInt32(format.mFormatID);
|
|
formatID[4] = '\0';
|
|
|
|
// General description
|
|
ss << format.mChannelsPerFrame << " ch, " << format.mSampleRate << " Hz, '" << formatID << "' (0x"
|
|
<< std::hex << std::setw(8) << std::setfill('0') << format.mFormatFlags << std::dec << ") ";
|
|
|
|
if (kAudioFormatLinearPCM == format.mFormatID) {
|
|
// Bit depth
|
|
UInt32 fractionalBits =
|
|
((0x3f << 7)/*kLinearPCMFormatFlagsSampleFractionMask*/ & format.mFormatFlags)
|
|
>> 7/*kLinearPCMFormatFlagsSampleFractionShift*/;
|
|
if (0 < fractionalBits)
|
|
ss << (format.mBitsPerChannel - fractionalBits) << "." << fractionalBits;
|
|
else
|
|
ss << format.mBitsPerChannel;
|
|
|
|
ss << "-bit";
|
|
|
|
// Endianness
|
|
bool isInterleaved = !(kAudioFormatFlagIsNonInterleaved & format.mFormatFlags);
|
|
UInt32 interleavedChannelCount = (isInterleaved ? format.mChannelsPerFrame : 1);
|
|
UInt32 sampleSize = (0 < format.mBytesPerFrame && 0 < interleavedChannelCount ?
|
|
format.mBytesPerFrame / interleavedChannelCount : 0);
|
|
if (1 < sampleSize)
|
|
ss << ((kLinearPCMFormatFlagIsBigEndian & format.mFormatFlags) ? " big-endian"
|
|
: " little-endian");
|
|
|
|
// Sign
|
|
bool isInteger = !(kLinearPCMFormatFlagIsFloat & format.mFormatFlags);
|
|
if (isInteger)
|
|
ss << ((kLinearPCMFormatFlagIsSignedInteger & format.mFormatFlags) ? " signed"
|
|
: " unsigned");
|
|
|
|
// Integer or floating
|
|
ss << (isInteger ? " integer" : " float");
|
|
|
|
// Packedness
|
|
if (0 < sampleSize && ((sampleSize << 3) != format.mBitsPerChannel))
|
|
ss << ((kLinearPCMFormatFlagIsPacked & format.mFormatFlags) ? ", packed in "
|
|
: ", unpacked in ")
|
|
<< sampleSize << " bytes";
|
|
|
|
// Alignment
|
|
if ((0 < sampleSize && ((sampleSize << 3) != format.mBitsPerChannel)) ||
|
|
(0 != (format.mBitsPerChannel & 7)))
|
|
ss << ((kLinearPCMFormatFlagIsAlignedHigh & format.mFormatFlags) ? " high-aligned"
|
|
: " low-aligned");
|
|
|
|
if (!isInterleaved)
|
|
ss << ", deinterleaved";
|
|
} else if (kAudioFormatAppleLossless == format.mFormatID) {
|
|
UInt32 sourceBitDepth = 0;
|
|
switch (format.mFormatFlags) {
|
|
case kAppleLosslessFormatFlag_16BitSourceData:
|
|
sourceBitDepth = 16;
|
|
break;
|
|
case kAppleLosslessFormatFlag_20BitSourceData:
|
|
sourceBitDepth = 20;
|
|
break;
|
|
case kAppleLosslessFormatFlag_24BitSourceData:
|
|
sourceBitDepth = 24;
|
|
break;
|
|
case kAppleLosslessFormatFlag_32BitSourceData:
|
|
sourceBitDepth = 32;
|
|
break;
|
|
}
|
|
|
|
if (0 != sourceBitDepth)
|
|
ss << "from " << sourceBitDepth << "-bit source, ";
|
|
else
|
|
ss << "from UNKNOWN source bit depth, ";
|
|
|
|
ss << format.mFramesPerPacket << " frames/packet";
|
|
} else
|
|
ss << format.mBitsPerChannel << " bits/channel, " << format.mBytesPerPacket << " bytes/packet, "
|
|
<< format.mFramesPerPacket << " frames/packet, " << format.mBytesPerFrame << " bytes/frame";
|
|
|
|
qDebug("CoreAudioStreamDescription: %s", ss.str().c_str());
|
|
}
|
|
|
|
static void LogAUStreamDescription(AudioUnit au) {
|
|
AudioStreamBasicDescription description;
|
|
UInt32 len = sizeof(AudioStreamBasicDescription);
|
|
OSStatus err;
|
|
err = AudioUnitGetProperty(au, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &description,
|
|
&len);
|
|
if (err == noErr) {
|
|
LogStreamDescription(description);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
};
|
|
|
|
#define CHECK_RETURN_FALSE(statement, warning_msg) \
|
|
{ \
|
|
OSStatus _err = statement; \
|
|
if (_err != noErr) { \
|
|
qWarning(warning_msg); \
|
|
return false; \
|
|
} \
|
|
} \
|
|
|
|
#define CHECK_RETURN(statement, warning_msg) \
|
|
{ \
|
|
OSStatus _err = statement; \
|
|
if (_err != noErr) { \
|
|
qWarning(warning_msg); \
|
|
return; \
|
|
} \
|
|
} \
|
|
|
|
#define CHECK_WARN(statement, warning_msg) \
|
|
{ \
|
|
OSStatus _err = statement; \
|
|
if (_err != noErr) { \
|
|
qWarning(warning_msg); \
|
|
} \
|
|
} \
|
|
|
|
class CoreAudioInit : public DeferInit {
|
|
CoreAudioInputRegistrar *cairReg;
|
|
CoreAudioOutputRegistrar *caorReg;
|
|
|
|
public:
|
|
CoreAudioInit() : cairReg(nullptr), caorReg(nullptr) {}
|
|
void initialize();
|
|
void destroy();
|
|
};
|
|
|
|
static CoreAudioInit cainit;
|
|
|
|
void CoreAudioInit::initialize() {
|
|
cairReg = new CoreAudioInputRegistrar();
|
|
caorReg = new CoreAudioOutputRegistrar();
|
|
}
|
|
|
|
void CoreAudioInit::destroy() {
|
|
delete cairReg;
|
|
delete caorReg;
|
|
}
|
|
|
|
const QList< audioDevice > CoreAudioSystem::getDeviceChoices(bool input) {
|
|
const bool doEcho = (Global::get().s.echoOption == EchoCancelOptionID::APPLE_AEC);
|
|
const QHash< QString, QString > devices = CoreAudioSystem::getDevices(input, doEcho);
|
|
|
|
QList< audioDevice > choices = {
|
|
audioDevice(QObject::tr("Default Device"), QString())
|
|
};
|
|
|
|
for (auto &key : devices.keys()) {
|
|
choices << audioDevice(devices.value(key), key);
|
|
}
|
|
|
|
return choices;
|
|
}
|
|
|
|
const QHash< QString, QString > CoreAudioSystem::getDevices(bool input, bool echo) {
|
|
QHash< QString, QString > qhReturn;
|
|
try {
|
|
QVector<AudioObjectID> devs = core_audio_utils::GetAudioObjectIDs(kAudioObjectSystemObject,
|
|
kAudioHardwarePropertyDevices);
|
|
|
|
for (AudioObjectID devid : devs) {
|
|
|
|
// Mac's native echo cancellation doesn't support aggregate device
|
|
// since it will create aggregate devices itself.
|
|
if (echo && core_audio_utils::GetDeviceTransportType(devid) == kAudioDeviceTransportTypeAggregate) {
|
|
continue;
|
|
}
|
|
|
|
if ((input && !core_audio_utils::IsInputDevice(devid))
|
|
|| (!input && !core_audio_utils::IsOutputDevice(devid))) continue;
|
|
|
|
QString qsDeviceName = core_audio_utils::GetDeviceStringProperty(devid, kAudioDevicePropertyDeviceNameCFString);
|
|
QString qsDeviceIdentifier = core_audio_utils::GetDeviceStringProperty(devid, kAudioDevicePropertyDeviceUID);
|
|
|
|
qhReturn.insert(qsDeviceIdentifier, qsDeviceName);
|
|
}
|
|
} catch (core_audio_utils::CoreAudioException& e) {
|
|
qWarning() << "CoreAudioSystem: " << e.what();
|
|
}
|
|
|
|
return qhReturn;
|
|
}
|
|
|
|
CoreAudioInputRegistrar::CoreAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("CoreAudio"), 10) {
|
|
echoOptions.push_back(EchoCancelOptionID::APPLE_AEC);
|
|
}
|
|
|
|
AudioInput *CoreAudioInputRegistrar::create() {
|
|
if (!isMicrophoneAccessDeniedByOS()) {
|
|
return new CoreAudioInput();
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
const QVariant CoreAudioInputRegistrar::getDeviceChoice() {
|
|
return Global::get().s.qsCoreAudioInput;
|
|
}
|
|
|
|
const QList< audioDevice > CoreAudioInputRegistrar::getDeviceChoices() {
|
|
return CoreAudioSystem::getDeviceChoices(true);
|
|
}
|
|
|
|
void CoreAudioInputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
|
|
s.qsCoreAudioInput = choice.toString();
|
|
}
|
|
|
|
bool CoreAudioInputRegistrar::canEcho(EchoCancelOptionID echoCancelID, const QString &outputSystem) const {
|
|
Q_UNUSED(outputSystem)
|
|
|
|
if (@available(macOS 10.14, *)) {
|
|
if (echoCancelID == EchoCancelOptionID::APPLE_AEC) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CoreAudioInputRegistrar::isMicrophoneAccessDeniedByOS() {
|
|
// Only available after macOS 10.14
|
|
// See https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/
|
|
// requesting_authorization_for_media_capture_on_macos?language=objc
|
|
if (@available(macOS 10.14, *)){
|
|
qDebug("CoreAudioInput: Checking microphone permission....");
|
|
// Request permission to access the camera and microphone.
|
|
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio])
|
|
{
|
|
case AVAuthorizationStatusAuthorized: {
|
|
// The user has previously granted access to the camera.
|
|
qDebug("CoreAudioInput: Checking microphone permission passed.");
|
|
return false;
|
|
}
|
|
case AVAuthorizationStatusNotDetermined: {
|
|
// The app hasn't yet asked the user for microphone access.
|
|
qWarning("CoreAudioInput: Mumble hasn't asked the user for microphone access. Asking for it now.");
|
|
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler: ^(BOOL _granted) {
|
|
if (_granted) {
|
|
Audio::stopInput();
|
|
Audio::startInput();
|
|
} else {
|
|
qWarning("CoreAudioInput: Microphone access denied by user.");
|
|
}
|
|
}];
|
|
return true;
|
|
}
|
|
case AVAuthorizationStatusDenied: {
|
|
// The user has previously denied access.
|
|
qWarning("CoreAudioInput: Microphone access has been previously denied by user.");
|
|
Global::get().mw->msgBox(QObject::tr("Access to the microphone was denied. Please allow Mumble to use the microphone "
|
|
"by changing the settings in System Preferences -> Security & Privacy -> Privacy -> "
|
|
"Microphone."));
|
|
return true;
|
|
}
|
|
case AVAuthorizationStatusRestricted: {
|
|
// The user can't grant access due to restrictions.
|
|
qWarning("CoreAudioInput: Microphone access denied due to system restrictions.");
|
|
Global::get().mw->msgBox(QObject::tr("Access to the microphone was denied due to system restrictions. You will not be able "
|
|
"to use the microphone in this session."));
|
|
return true;
|
|
}
|
|
default: {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AudioOutput *CoreAudioOutputRegistrar::create() {
|
|
return new CoreAudioOutput();
|
|
}
|
|
|
|
const QVariant CoreAudioOutputRegistrar::getDeviceChoice() {
|
|
return Global::get().s.qsCoreAudioOutput;
|
|
}
|
|
|
|
const QList< audioDevice > CoreAudioOutputRegistrar::getDeviceChoices() {
|
|
return CoreAudioSystem::getDeviceChoices(false);
|
|
}
|
|
|
|
void CoreAudioOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
|
|
s.qsCoreAudioOutput = choice.toString();
|
|
}
|
|
|
|
bool CoreAudioOutputRegistrar::canMuteOthers() const {
|
|
return false;
|
|
}
|
|
|
|
CoreAudioInput::CoreAudioInput() {
|
|
}
|
|
|
|
bool CoreAudioInput::openAUHAL(AudioStreamBasicDescription &streamDescription){
|
|
AudioComponent comp;
|
|
AudioComponentDescription desc;
|
|
UInt32 val, len;
|
|
|
|
desc.componentType = kAudioUnitType_Output;
|
|
desc.componentSubType = kAudioUnitSubType_HALOutput;
|
|
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
|
|
desc.componentFlags = 0;
|
|
desc.componentFlagsMask = 0;
|
|
|
|
qDebug("CoreAudioInput: Use AUHAL as IO AudioUnit.");
|
|
|
|
comp = AudioComponentFindNext(nullptr, &desc);
|
|
if (!comp) {
|
|
qWarning("CoreAudioInput: Unable to find AUHAL.");
|
|
return false;
|
|
}
|
|
|
|
CHECK_RETURN_FALSE(AudioComponentInstanceNew(comp, &auHAL), "CoreAudioInput: Unable to open AUHAL component.");
|
|
|
|
val = 1;
|
|
CHECK_RETURN_FALSE(AudioUnitSetProperty(auHAL, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input,
|
|
AUDirection::INPUT, &val, sizeof(UInt32)),
|
|
"CoreAudioInput: Unable to configure input scope on AUHAL.");
|
|
|
|
val = 0;
|
|
CHECK_RETURN_FALSE(AudioUnitSetProperty(auHAL, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output,
|
|
AUDirection::OUTPUT, &val, sizeof(UInt32)),
|
|
"CoreAudioInput: Unable to configure output scope on AUHAL.");
|
|
|
|
len = sizeof(AudioDeviceID);
|
|
CHECK_RETURN_FALSE(AudioUnitSetProperty(auHAL, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global,
|
|
AUDirection::OUTPUT, &inputDevId, len),
|
|
"CoreAudioInput: Unable to set device of AUHAL.");
|
|
|
|
len = sizeof(AudioStreamBasicDescription);
|
|
CHECK_RETURN_FALSE(AudioUnitGetProperty(auHAL, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
|
|
AUDirection::INPUT, &streamDescription, &len),
|
|
"CoreAudioInput: Unable to query device for stream info.");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CoreAudioInput::openAUVoip(AudioStreamBasicDescription &streamDescription) {
|
|
// Initialize VoiceProcessingIO AU, utilizing macOS's builtin echo cancellation.
|
|
// This AU is poorly documented by Apple. See Chromium's code for more information:
|
|
// https://github.com/chromium/chromium/blob/master/media/audio/mac/audio_low_latency_input_mac.cc
|
|
if(@available(macOS 10.12, *)) {
|
|
UInt32 len, val;
|
|
AudioComponentDescription desc = {
|
|
.componentType = kAudioUnitType_Output,
|
|
.componentSubType = kAudioUnitSubType_VoiceProcessingIO,
|
|
.componentManufacturer = kAudioUnitManufacturer_Apple,
|
|
.componentFlags = 0,
|
|
.componentFlagsMask = 0
|
|
};
|
|
|
|
qDebug("CoreAudioInput: Use VoiceProcessingIO as IO AudioUnit.");
|
|
|
|
AudioComponent inputComponent = AudioComponentFindNext(nullptr, &desc);
|
|
|
|
CHECK_RETURN_FALSE(AudioComponentInstanceNew(inputComponent, &auVoip),
|
|
"CoreAudioInput: Unable to create VoiceProcessingIO AudioUnit.");
|
|
|
|
try {
|
|
if (core_audio_utils::GetDeviceTransportType(inputDevId) == kAudioDeviceTransportTypeAggregate) {
|
|
qWarning("CoreAudioInput: Aggregated devices are not supported by VoiceProcessingIO AudioUnit.");
|
|
return false;
|
|
}
|
|
} catch (core_audio_utils::CoreAudioException& e) {
|
|
qWarning() << "CoreAudioInput: " << e.what();
|
|
return false;
|
|
}
|
|
|
|
len = sizeof(AudioDeviceID);
|
|
CHECK_RETURN_FALSE(AudioUnitSetProperty(auVoip, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global,
|
|
AUDirection::INPUT, &inputDevId, len),
|
|
"CoreAudioInput: Unable to set device of VoiceProcessingIO AudioUnit.");
|
|
|
|
// It is reported that the echo source need to be specified as the output device.
|
|
// If no output device is specified, MacOS would take the default output device as echo source.
|
|
CHECK_RETURN_FALSE(AudioUnitSetProperty(auVoip, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global,
|
|
AUDirection::OUTPUT, &echoOutputDevId, len),
|
|
"CoreAudioInput: Unable to set device of VoiceProcessingIO AudioUnit.");
|
|
|
|
|
|
len = sizeof(AudioStreamBasicDescription);
|
|
CHECK_RETURN_FALSE(AudioUnitGetProperty(auVoip, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
|
|
AUDirection::INPUT, &streamDescription, &len),
|
|
"CoreAudioInput: Unable to query device for stream info from VoiceProcessingIO AudioUnit.");
|
|
|
|
#ifdef DEBUG
|
|
qDebug("CoreAudioInput: VOIP Input stream description:");
|
|
core_audio_utils::LogAUStreamDescription(auVoip);
|
|
#endif
|
|
|
|
// Mute the VoiceProcessing AU (Value 0 stands for "mute")
|
|
// VoiceProcessing AU is a output node and has the ability of playing things out. We simply don't want that.
|
|
val = 0;
|
|
len = sizeof(val);
|
|
AudioUnitSetProperty(auVoip, kAUVoiceIOProperty_MuteOutput, kAudioUnitScope_Global, 1, &val, len);
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
bool CoreAudioInput::initializeInputAU(AudioUnit au, AudioStreamBasicDescription &streamDescription, int &actualBufferLength) {
|
|
OSStatus err;
|
|
UInt32 len, val;
|
|
|
|
len = sizeof(AudioStreamBasicDescription);
|
|
CHECK_RETURN_FALSE(AudioUnitSetProperty(au, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output,
|
|
AUDirection::INPUT, &streamDescription, len),
|
|
"CoreAudioInput: Unable to set stream format for Input AU - 1.")
|
|
|
|
CHECK_RETURN_FALSE(AudioUnitSetProperty(au, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
|
|
AUDirection::OUTPUT, &streamDescription, len),
|
|
"CoreAudioInput: Unable to set stream format for Input AU - 2.");
|
|
#ifdef DEBUG
|
|
qDebug("CoreAudioInput: Input stream description:");
|
|
core_audio_utils::LogAUStreamDescription(au);
|
|
#endif
|
|
|
|
// Struct used to query information of the system audio setup
|
|
AudioObjectPropertyAddress propertyAddress = {
|
|
.mSelector = 0, // this attribute will be specified later
|
|
.mScope = kAudioDevicePropertyScopeInput,
|
|
.mElement = kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
AudioValueRange range;
|
|
len = sizeof(AudioValueRange);
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferFrameSizeRange;
|
|
CHECK_WARN(AudioObjectGetPropertyData(inputDevId, &propertyAddress, 0, nullptr, &len, &range),
|
|
"CoreAudioInput: Unable to query for allowed buffer size ranges of input device.");
|
|
|
|
qWarning("CoreAudioInput: BufferFrameSizeRange = (%.2f, %.2f)", range.mMinimum, range.mMaximum);
|
|
|
|
actualBufferLength = static_cast<int>(iMicLength);
|
|
val = iMicLength;
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferFrameSize;
|
|
err = AudioObjectSetPropertyData(inputDevId, &propertyAddress, 0, nullptr, sizeof(UInt32), &val);
|
|
if (err != noErr) {
|
|
qWarning("CoreAudioInput: Unable to set preferred buffer size on device. Querying for device default.");
|
|
len = sizeof(UInt32);
|
|
CHECK_RETURN_FALSE(AudioDeviceGetProperty(inputDevId, 0, true, kAudioDevicePropertyBufferFrameSize, &len, &val),
|
|
"CoreAudioInput: Unable to query for device's buffer size.");
|
|
|
|
actualBufferLength = (int) val;
|
|
}
|
|
|
|
CHECK_RETURN_FALSE(AudioUnitInitialize(au), "CoreAudioInput: Unable to initialize VoiceProcessingIO AudioUnit.");
|
|
|
|
return true;
|
|
}
|
|
|
|
void CoreAudioInput::run() {
|
|
OSStatus err;
|
|
UInt32 len;
|
|
AudioStreamBasicDescription fmt;
|
|
inputDevId = 0;
|
|
echoOutputDevId = 0;
|
|
bool doEcho = (Global::get().s.echoOption == EchoCancelOptionID::APPLE_AEC);
|
|
|
|
auHAL = nullptr;
|
|
auVoip = nullptr;
|
|
|
|
memset(&buflist, 0, sizeof(AudioBufferList));
|
|
|
|
try {
|
|
if (!Global::get().s.qsCoreAudioInput.isEmpty()) {
|
|
qWarning("CoreAudioInput: Set device to '%s'.", qPrintable(Global::get().s.qsCoreAudioInput));
|
|
inputDevId = core_audio_utils::GetDeviceID(Global::get().s.qsCoreAudioInput, AUDirection::INPUT);
|
|
} else {
|
|
qWarning("CoreAudioInput: Set device to 'Default Device'.");
|
|
inputDevId = core_audio_utils::GetDefaultDeviceID(AUDirection::INPUT);
|
|
}
|
|
|
|
if (doEcho) {
|
|
echoOutputDevId = core_audio_utils::GetDeviceID(Global::get().s.qsCoreAudioOutput, AUDirection::OUTPUT);
|
|
if (!openAUVoip(fmt)) { return; };
|
|
} else {
|
|
if (!openAUHAL(fmt)) { return; };
|
|
}
|
|
} catch (core_audio_utils::CoreAudioException& e) {
|
|
qWarning() << "CoreAudioInput: " << e.getMessage();
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
qDebug("CoreAudioInput: Original input stream description:");
|
|
core_audio_utils::LogStreamDescription(fmt);
|
|
#endif
|
|
|
|
if (fmt.mFormatFlags & kAudioFormatFlagIsFloat) {
|
|
eMicFormat = SampleFloat;
|
|
} else if (fmt.mFormatFlags & kAudioFormatFlagIsSignedInteger) {
|
|
eMicFormat = SampleShort;
|
|
}
|
|
|
|
if (fmt.mChannelsPerFrame > 1) {
|
|
qWarning("CoreAudioInput: Input device with more than one channel detected. Defaulting to 1.");
|
|
}
|
|
|
|
iMicFreq = static_cast<unsigned int>(fmt.mSampleRate);
|
|
iMicChannels = 1;
|
|
initializeMixer();
|
|
|
|
if (eMicFormat == SampleFloat) {
|
|
fmt.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
|
|
fmt.mBitsPerChannel = sizeof(float) * 8;
|
|
} else if (eMicFormat == SampleShort) {
|
|
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
|
fmt.mBitsPerChannel = sizeof(short) * 8;
|
|
}
|
|
|
|
AudioUnit auFinal = auHAL;
|
|
|
|
fmt.mFormatID = kAudioFormatLinearPCM;
|
|
fmt.mSampleRate = iMicFreq;
|
|
fmt.mChannelsPerFrame = iMicChannels;
|
|
fmt.mBytesPerFrame = iMicSampleSize;
|
|
fmt.mBytesPerPacket = iMicSampleSize;
|
|
fmt.mFramesPerPacket = 1;
|
|
|
|
int actualBufferLength;
|
|
|
|
if (doEcho) {
|
|
// Initialize macOS's builtin echo cancellation AU.
|
|
if(initializeInputAU(auVoip, fmt, actualBufferLength)) {
|
|
auFinal = auVoip;
|
|
} else {
|
|
qWarning("CoreAudioInput: Unable to initialize VoiceProcessing AU for echo cancellation.");
|
|
return;
|
|
}
|
|
} else {
|
|
if(!initializeInputAU(auHAL, fmt, actualBufferLength)) {
|
|
return;
|
|
};
|
|
}
|
|
|
|
AURenderCallbackStruct cb;
|
|
cb.inputProc = CoreAudioInput::inputCallback;
|
|
cb.inputProcRefCon = this;
|
|
len = sizeof(AURenderCallbackStruct);
|
|
CHECK_RETURN(AudioUnitSetProperty(auFinal, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &cb, len),
|
|
"CoreAudioInput: Unable to setup input callback.");
|
|
|
|
err = AudioUnitAddPropertyListener(auFinal, kAudioUnitProperty_StreamFormat, CoreAudioInput::propertyChange, this);
|
|
if (err != noErr) {
|
|
qWarning("CoreAudioInput: Unable to create input property change listener for AUHAL. Unable to listen to property change "
|
|
"events.");
|
|
}
|
|
|
|
AudioObjectPropertyAddress inputDeviceAddress = {
|
|
kAudioHardwarePropertyDefaultInputDevice,
|
|
kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster
|
|
};
|
|
CHECK_WARN(AudioObjectAddPropertyListener(kAudioObjectSystemObject, &inputDeviceAddress, CoreAudioInput::deviceChange, this),
|
|
"CoreAudioInput: Unable to create input device change listener. Unable to listen to device changes.");
|
|
|
|
buflist.mNumberBuffers = 1;
|
|
AudioBuffer *b = buflist.mBuffers;
|
|
b->mNumberChannels = iMicChannels;
|
|
b->mDataByteSize = iMicSampleSize * static_cast<unsigned int>(actualBufferLength);
|
|
b->mData = calloc(1, b->mDataByteSize);
|
|
|
|
// Start!
|
|
CHECK_RETURN(AudioOutputUnitStart(auFinal), "CoreAudioInput: Unable to start AudioUnit.");
|
|
|
|
if (doEcho) {
|
|
UndoDucking(echoOutputDevId);
|
|
}
|
|
|
|
bRunning = true;
|
|
}
|
|
|
|
void CoreAudioInput::stop() {
|
|
bRunning = false;
|
|
|
|
if (auHAL) {
|
|
CHECK_WARN(AudioOutputUnitStop(auHAL),
|
|
"CoreAudioInput: Unable to stop AudioUnit.");
|
|
CHECK_WARN(AudioUnitUninitialize(auHAL),
|
|
"CoreAudioInput: Unable to uninitialize AudioUnit.");
|
|
auHAL = nullptr;
|
|
}
|
|
|
|
if (auVoip) {
|
|
CHECK_WARN(AudioOutputUnitStop(auVoip),
|
|
"CoreAudioInput: Unable to stop AudioUnit.");
|
|
CHECK_WARN(AudioUnitUninitialize(auVoip),
|
|
"CoreAudioInput: Unable to uninitialize AudioUnit.");
|
|
CHECK_WARN(AudioComponentInstanceDispose(auVoip),
|
|
"CoreAudioInput: Unable to dispose AudioUnit.");
|
|
auVoip = nullptr;
|
|
}
|
|
|
|
AudioBuffer *b = buflist.mBuffers;
|
|
if (b && b->mData)
|
|
free(b->mData);
|
|
|
|
qWarning("CoreAudioInput: Shutting down.");
|
|
}
|
|
|
|
CoreAudioInput::~CoreAudioInput() {
|
|
bRunning = false;
|
|
wait();
|
|
stop();
|
|
}
|
|
|
|
OSStatus CoreAudioInput::inputCallback(void *udata, AudioUnitRenderActionFlags *flags, const AudioTimeStamp *ts,
|
|
UInt32 busnum, UInt32 nframes, AudioBufferList *buflist) {
|
|
Q_UNUSED(udata);
|
|
Q_UNUSED(buflist);
|
|
|
|
CoreAudioInput *i = reinterpret_cast< CoreAudioInput * >(udata);
|
|
OSStatus err;
|
|
|
|
if (i->auVoip) {
|
|
err = AudioUnitRender(i->auVoip, flags, ts, busnum, nframes, &i->buflist);
|
|
} else {
|
|
err = AudioUnitRender(i->auHAL, flags, ts, busnum, nframes, &i->buflist);
|
|
}
|
|
if (err != noErr) {
|
|
qWarning("CoreAudioInput: AudioUnitRender failed.");
|
|
return err;
|
|
}
|
|
|
|
i->addMic(i->buflist.mBuffers->mData, nframes);
|
|
|
|
return noErr;
|
|
}
|
|
|
|
void CoreAudioInput::propertyChange(void *udata, AudioUnit auHAL, AudioUnitPropertyID prop, AudioUnitScope scope,
|
|
AudioUnitElement element) {
|
|
Q_UNUSED(udata);
|
|
Q_UNUSED(auHAL);
|
|
Q_UNUSED(scope);
|
|
Q_UNUSED(element);
|
|
|
|
CoreAudioInput *o = reinterpret_cast< CoreAudioInput * >(udata);
|
|
if (!o->bRunning) { return; }
|
|
if (prop == kAudioUnitProperty_StreamFormat) {
|
|
qWarning("CoreAudioInput: Stream format change detected. Restarting AudioInput.");
|
|
Audio::stopInput();
|
|
Audio::startInput();
|
|
} else {
|
|
qWarning("CoreAudioInput: Unexpected property changed event received.");
|
|
}
|
|
}
|
|
|
|
OSStatus CoreAudioInput::deviceChange(AudioObjectID inObjectID, UInt32 inNumberAddresses,
|
|
const AudioObjectPropertyAddress inAddresses[], void *udata) {
|
|
Q_UNUSED(inObjectID);
|
|
Q_UNUSED(inNumberAddresses);
|
|
Q_UNUSED(inAddresses);
|
|
|
|
CoreAudioInput *o = reinterpret_cast< CoreAudioInput * >(udata);
|
|
if (!o->bRunning) return noErr;
|
|
|
|
qWarning("CoreAudioInput: Input device change detected. Restarting AudioInput.");
|
|
Audio::stopInput();
|
|
Audio::startInput();
|
|
|
|
return noErr;
|
|
}
|
|
|
|
|
|
CoreAudioOutput::CoreAudioOutput() {
|
|
}
|
|
|
|
|
|
void CoreAudioOutput::run() {
|
|
OSStatus err;
|
|
AudioStreamBasicDescription fmt;
|
|
unsigned int chanmasks[32];
|
|
AudioDeviceID devId = 0;
|
|
UInt32 len;
|
|
AudioObjectPropertyAddress propertyAddress = { 0, kAudioDevicePropertyScopeOutput,
|
|
kAudioObjectPropertyElementMaster };
|
|
|
|
try {
|
|
if (!Global::get().s.qsCoreAudioOutput.isEmpty()) {
|
|
qWarning("CoreAudioOutput: Set device to '%s'.", qPrintable(Global::get().s.qsCoreAudioOutput));
|
|
|
|
devId = core_audio_utils::GetDeviceID(Global::get().s.qsCoreAudioOutput, AUDirection::OUTPUT);
|
|
} else {
|
|
qWarning("CoreAudioOutput: Set device to 'Default Device'.");
|
|
|
|
devId = core_audio_utils::GetDefaultDeviceID(AUDirection::OUTPUT);
|
|
}
|
|
} catch (core_audio_utils::CoreAudioException& e) {
|
|
qWarning() << "CoreAudioOutput: " << e.what();
|
|
}
|
|
|
|
AudioComponent comp;
|
|
AudioComponentDescription desc;
|
|
|
|
desc.componentType = kAudioUnitType_Output;
|
|
desc.componentSubType = kAudioUnitSubType_HALOutput;
|
|
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
|
|
desc.componentFlags = 0;
|
|
desc.componentFlagsMask = 0;
|
|
|
|
comp = AudioComponentFindNext(nullptr, &desc);
|
|
if (!comp) {
|
|
qWarning("CoreAudioOuput: Unable to find AudioUnit.");
|
|
return;
|
|
}
|
|
|
|
CHECK_RETURN(AudioComponentInstanceNew(comp, &auHAL),
|
|
"CoreAudioOutput: Unable to open AudioUnit component.");
|
|
|
|
CHECK_RETURN(AudioUnitInitialize(auHAL),
|
|
"CoreAudioOutput: Unable to initialize output AudioUnit");
|
|
|
|
len = sizeof(AudioDeviceID);
|
|
CHECK_RETURN(AudioUnitSetProperty(auHAL, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &devId, len),
|
|
"CoreAudioOutput: Unable to set CurrentDevice property on AudioUnit.");
|
|
|
|
len = sizeof(AudioStreamBasicDescription);
|
|
CHECK_RETURN(AudioUnitGetProperty(auHAL, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &fmt, &len),
|
|
"CoreAudioOuptut: Unable to query device for stream info.");
|
|
|
|
#ifdef DEBUG
|
|
qDebug("CoreAudioOutput: Original output stream description:");
|
|
core_audio_utils::LogAUStreamDescription(auHAL);
|
|
#endif
|
|
|
|
iMixerFreq = static_cast< unsigned int >(fmt.mSampleRate);
|
|
iChannels = static_cast< unsigned int >(fmt.mChannelsPerFrame);
|
|
|
|
if (fmt.mFormatFlags & kAudioFormatFlagIsFloat) {
|
|
eSampleFormat = SampleFloat;
|
|
} else if (fmt.mFormatFlags & kAudioFormatFlagIsSignedInteger) {
|
|
eSampleFormat = SampleShort;
|
|
}
|
|
|
|
chanmasks[0] = SPEAKER_FRONT_LEFT;
|
|
chanmasks[1] = SPEAKER_FRONT_RIGHT;
|
|
initializeMixer(chanmasks);
|
|
|
|
if (eSampleFormat == SampleFloat) {
|
|
fmt.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
|
|
fmt.mBitsPerChannel = sizeof(float) * 8;
|
|
} else if (eSampleFormat == SampleShort) {
|
|
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
|
fmt.mBitsPerChannel = sizeof(short) * 8;
|
|
}
|
|
|
|
fmt.mFormatID = kAudioFormatLinearPCM;
|
|
fmt.mSampleRate = iMixerFreq;
|
|
fmt.mChannelsPerFrame = iChannels;
|
|
fmt.mBytesPerFrame = iSampleSize;
|
|
fmt.mBytesPerPacket = iSampleSize;
|
|
fmt.mFramesPerPacket = 1;
|
|
|
|
len = sizeof(AudioStreamBasicDescription);
|
|
CHECK_RETURN(AudioUnitSetProperty(auHAL, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &fmt, len),
|
|
"CoreAudioOutput: Unable to set stream format for output device.");
|
|
|
|
#ifdef DEBUG
|
|
qDebug("CoreAudioOutput: Finalized output stream description:");
|
|
core_audio_utils::LogAUStreamDescription(auHAL);
|
|
#endif
|
|
|
|
CHECK_WARN(AudioUnitAddPropertyListener(auHAL, kAudioUnitProperty_StreamFormat, CoreAudioOutput::propertyChange, this),
|
|
"CoreAudioOutput: Unable to create output property change listener. Unable to listen to property changes.");
|
|
|
|
AudioObjectPropertyAddress outputDeviceAddress = {
|
|
kAudioHardwarePropertyDefaultOutputDevice,
|
|
kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster
|
|
};
|
|
CHECK_WARN(AudioObjectAddPropertyListener(kAudioObjectSystemObject, &outputDeviceAddress, CoreAudioOutput::deviceChange, this),
|
|
"CoreAudioOutput: Unable to create output device change listener. Unable to listen to device changes.");
|
|
|
|
AURenderCallbackStruct cb;
|
|
cb.inputProc = CoreAudioOutput::outputCallback;
|
|
cb.inputProcRefCon = this;
|
|
len = sizeof(AURenderCallbackStruct);
|
|
CHECK_RETURN(AudioUnitSetProperty(auHAL, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, 0, &cb, len),
|
|
"CoreAudioOutput: Unable to setup callback.");
|
|
|
|
AudioValueRange range;
|
|
len = sizeof(AudioValueRange);
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferFrameSizeRange;
|
|
err = AudioObjectGetPropertyData(devId, &propertyAddress, 0, nullptr, &len, &range);
|
|
if (err != noErr) {
|
|
qWarning("CoreAudioOutput: Unable to query for allowed buffer size ranges.");
|
|
} else {
|
|
setBufferSize(static_cast<unsigned int>(range.mMaximum));
|
|
qWarning("CoreAudioOutput: BufferFrameSizeRange = (%.2f, %.2f)", range.mMinimum, range.mMaximum);
|
|
}
|
|
|
|
UInt32 val = (iFrameSize * iMixerFreq) / SAMPLE_RATE;
|
|
propertyAddress.mSelector = kAudioDevicePropertyBufferFrameSize;
|
|
CHECK_WARN(AudioObjectSetPropertyData(devId, &propertyAddress, 0, nullptr, sizeof(UInt32), &val),
|
|
"CoreAudioOutput: Could not set requested buffer size for device. Continuing with default.");
|
|
|
|
CHECK_RETURN(AudioOutputUnitStart(auHAL),
|
|
"CoreAudioOutput: Unable to start AudioUnit");
|
|
|
|
bRunning = true;
|
|
}
|
|
|
|
void CoreAudioOutput::stop() {
|
|
bRunning = false;
|
|
|
|
if (auHAL) {
|
|
CHECK_WARN(AudioOutputUnitStop(auHAL),
|
|
"CoreAudioOutput: Unable to stop AudioUnit.");
|
|
CHECK_WARN(AudioUnitUninitialize(auHAL),
|
|
"CoreAudioOutput: Unable to uninitialize AudioUnit.");
|
|
auHAL = nullptr;
|
|
}
|
|
|
|
qWarning("CoreAudioOutput: Shutting down.");
|
|
}
|
|
|
|
CoreAudioOutput::~CoreAudioOutput() {
|
|
bRunning = false;
|
|
|
|
wait();
|
|
stop();
|
|
}
|
|
|
|
OSStatus CoreAudioOutput::outputCallback(void *udata, AudioUnitRenderActionFlags *flags, const AudioTimeStamp *ts,
|
|
UInt32 busnum, UInt32 nframes, AudioBufferList *buflist) {
|
|
Q_UNUSED(flags);
|
|
Q_UNUSED(ts);
|
|
Q_UNUSED(busnum);
|
|
|
|
CoreAudioOutput *o = reinterpret_cast< CoreAudioOutput * >(udata);
|
|
AudioBuffer *buf = buflist->mBuffers;
|
|
|
|
if (o->bRunning) {
|
|
bool done = o->mix(buf->mData, nframes);
|
|
if (!done) {
|
|
buf->mDataByteSize = 0;
|
|
return -1;
|
|
}
|
|
} else {
|
|
buf->mDataByteSize = 0;
|
|
}
|
|
|
|
return noErr;
|
|
}
|
|
|
|
void CoreAudioOutput::propertyChange(void *udata, AudioUnit auHAL, AudioUnitPropertyID prop, AudioUnitScope scope,
|
|
AudioUnitElement element) {
|
|
Q_UNUSED(udata);
|
|
Q_UNUSED(auHAL);
|
|
Q_UNUSED(scope);
|
|
Q_UNUSED(element);
|
|
|
|
CoreAudioOutput *o = reinterpret_cast< CoreAudioOutput * >(udata);
|
|
if (!o->bRunning) return;
|
|
|
|
if (prop == kAudioUnitProperty_StreamFormat) {
|
|
qWarning("CoreAudioOutput: Stream format change detected. Restarting AudioOutput.");
|
|
o->stop();
|
|
Audio::stopOutput();
|
|
Audio::startOutput();
|
|
} else {
|
|
qWarning("CoreAudioOutput: Unexpected property changed event received.");
|
|
}
|
|
}
|
|
|
|
OSStatus CoreAudioOutput::deviceChange(AudioObjectID inObjectID, UInt32 inNumberAddresses,
|
|
const AudioObjectPropertyAddress inAddresses[], void *udata) {
|
|
Q_UNUSED(inObjectID);
|
|
Q_UNUSED(inNumberAddresses);
|
|
Q_UNUSED(inAddresses);
|
|
|
|
CoreAudioOutput *o = reinterpret_cast< CoreAudioOutput * >(udata);
|
|
if (!o->bRunning) return noErr;
|
|
|
|
qWarning("CoreAudioOutput: Output device change detected. Restarting AudioOutput.");
|
|
Audio::stopOutput();
|
|
Audio::startOutput();
|
|
|
|
return noErr;
|
|
}
|