// // libtgvoip is free and unencumbered public domain software. // For more information, see http://unlicense.org or the UNLICENSE file // you should have received with this source code distribution. // #include "AudioOutputAudioUnitOSX.h" #include "../../VoIPController.h" #include "../../logging.h" #include #include #include #define BUFFER_SIZE 960 #define CHECK_AU_ERROR(res, msg) \ if (res != noErr) \ { \ LOGE("output: " msg ": OSStatus=%d", (int)res); \ return; \ } #define kOutputBus 0 #define kInputBus 1 using namespace tgvoip; using namespace tgvoip::audio; AudioOutputAudioUnitLegacy::AudioOutputAudioUnitLegacy(std::string deviceID) { remainingDataSize = 0; isPlaying = false; sysDevID = 0; OSStatus status; AudioComponentDescription inputDesc = { .componentType = kAudioUnitType_Output, .componentSubType = kAudioUnitSubType_HALOutput, .componentFlags = 0, .componentFlagsMask = 0, .componentManufacturer = kAudioUnitManufacturer_Apple}; AudioComponent component = AudioComponentFindNext(nullptr, &inputDesc); status = AudioComponentInstanceNew(component, &unit); CHECK_AU_ERROR(status, "Error creating AudioUnit"); UInt32 flag = 1; status = AudioUnitSetProperty(unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &flag, sizeof(flag)); CHECK_AU_ERROR(status, "Error enabling AudioUnit output"); flag = 0; status = AudioUnitSetProperty(unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &flag, sizeof(flag)); CHECK_AU_ERROR(status, "Error enabling AudioUnit input"); char model[128]; std::memset(model, 0, sizeof(model)); std::size_t msize = sizeof(model); int mres = sysctlbyname("hw.model", model, &msize, nullptr, 0); if (mres == 0) { LOGV("Mac model: %s", model); isMacBookPro = (strncmp("MacBookPro", model, 10) == 0); } SetCurrentDevice(deviceID); CFRunLoopRef theRunLoop = nullptr; AudioObjectPropertyAddress propertyAddress = {kAudioHardwarePropertyRunLoop, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; status = AudioObjectSetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, sizeof(CFRunLoopRef), &theRunLoop); propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; propertyAddress.mScope = kAudioObjectPropertyScopeGlobal; propertyAddress.mElement = kAudioObjectPropertyElementMaster; AudioObjectAddPropertyListener(kAudioObjectSystemObject, &propertyAddress, AudioOutputAudioUnitLegacy::DefaultDeviceChangedCallback, this); AudioStreamBasicDescription desiredFormat = { .mSampleRate = /*hardwareFormat.mSampleRate*/ 48000, .mFormatID = kAudioFormatLinearPCM, .mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked | kAudioFormatFlagsNativeEndian, .mFramesPerPacket = 1, .mChannelsPerFrame = 1, .mBitsPerChannel = 16, .mBytesPerPacket = 2, .mBytesPerFrame = 2}; status = AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &desiredFormat, sizeof(desiredFormat)); CHECK_AU_ERROR(status, "Error setting format"); AURenderCallbackStruct callbackStruct; callbackStruct.inputProc = AudioOutputAudioUnitLegacy::BufferCallback; callbackStruct.inputProcRefCon = this; status = AudioUnitSetProperty(unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, kOutputBus, &callbackStruct, sizeof(callbackStruct)); CHECK_AU_ERROR(status, "Error setting input buffer callback"); status = AudioUnitInitialize(unit); CHECK_AU_ERROR(status, "Error initializing unit"); } AudioOutputAudioUnitLegacy::~AudioOutputAudioUnitLegacy() { AudioObjectPropertyAddress propertyAddress; propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; propertyAddress.mScope = kAudioObjectPropertyScopeGlobal; propertyAddress.mElement = kAudioObjectPropertyElementMaster; AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &propertyAddress, AudioOutputAudioUnitLegacy::DefaultDeviceChangedCallback, this); AudioObjectPropertyAddress dataSourceProp = { kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; if (isMacBookPro && sysDevID && AudioObjectHasProperty(sysDevID, &dataSourceProp)) { AudioObjectRemovePropertyListener(sysDevID, &dataSourceProp, AudioOutputAudioUnitLegacy::DefaultDeviceChangedCallback, this); } AudioUnitUninitialize(unit); AudioComponentInstanceDispose(unit); } void AudioOutputAudioUnitLegacy::Start() { isPlaying = true; OSStatus status = AudioOutputUnitStart(unit); CHECK_AU_ERROR(status, "Error starting AudioUnit"); } void AudioOutputAudioUnitLegacy::Stop() { isPlaying = false; OSStatus status = AudioOutputUnitStart(unit); CHECK_AU_ERROR(status, "Error stopping AudioUnit"); } OSStatus AudioOutputAudioUnitLegacy::BufferCallback(void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags, const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList* ioData) { AudioOutputAudioUnitLegacy* input = reinterpret_cast(inRefCon); input->HandleBufferCallback(ioData); return noErr; } bool AudioOutputAudioUnitLegacy::IsPlaying() { return isPlaying; } void AudioOutputAudioUnitLegacy::HandleBufferCallback(AudioBufferList* ioData) { int i; for (i = 0; i < ioData->mNumberBuffers; i++) { AudioBuffer buf = ioData->mBuffers[i]; if (!isPlaying) { std::memset(buf.mData, 0, buf.mDataByteSize); return; } while (remainingDataSize < buf.mDataByteSize) { assert(remainingDataSize + BUFFER_SIZE * 2 < 10240); InvokeCallback(remainingData + remainingDataSize, BUFFER_SIZE * 2); remainingDataSize += BUFFER_SIZE * 2; } std::memcpy(buf.mData, remainingData, buf.mDataByteSize); remainingDataSize -= buf.mDataByteSize; memmove(remainingData, remainingData + buf.mDataByteSize, remainingDataSize); } } void AudioOutputAudioUnitLegacy::EnumerateDevices(std::vector& devs) { AudioObjectPropertyAddress propertyAddress = { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; UInt32 dataSize = 0; OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, &dataSize); if (kAudioHardwareNoError != status) { LOGE("AudioObjectGetPropertyDataSize (kAudioHardwarePropertyDevices) failed: %i", status); return; } UInt32 deviceCount = (UInt32)(dataSize / sizeof(AudioDeviceID)); AudioDeviceID* audioDevices = (AudioDeviceID*)(std::malloc(dataSize)); status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, &dataSize, audioDevices); if (kAudioHardwareNoError != status) { LOGE("AudioObjectGetPropertyData (kAudioHardwarePropertyDevices) failed: %i", status); std::free(audioDevices); audioDevices = nullptr; return; } // Iterate through all the devices and determine which are input-capable propertyAddress.mScope = kAudioDevicePropertyScopeOutput; for (UInt32 i = 0; i < deviceCount; ++i) { // Query device UID CFStringRef deviceUID = nullptr; dataSize = sizeof(deviceUID); propertyAddress.mSelector = kAudioDevicePropertyDeviceUID; status = AudioObjectGetPropertyData(audioDevices[i], &propertyAddress, 0, nullptr, &dataSize, &deviceUID); if (kAudioHardwareNoError != status) { LOGE("AudioObjectGetPropertyData (kAudioDevicePropertyDeviceUID) failed: %i", status); continue; } // Query device name CFStringRef deviceName = nullptr; dataSize = sizeof(deviceName); propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString; status = AudioObjectGetPropertyData(audioDevices[i], &propertyAddress, 0, nullptr, &dataSize, &deviceName); if (kAudioHardwareNoError != status) { LOGE("AudioObjectGetPropertyData (kAudioDevicePropertyDeviceNameCFString) failed: %i", status); continue; } // Determine if the device is an input device (it is an input device if it has input channels) dataSize = 0; propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration; status = AudioObjectGetPropertyDataSize(audioDevices[i], &propertyAddress, 0, nullptr, &dataSize); if (kAudioHardwareNoError != status) { LOGE("AudioObjectGetPropertyDataSize (kAudioDevicePropertyStreamConfiguration) failed: %i", status); continue; } AudioBufferList* bufferList = (AudioBufferList*)(std::malloc(dataSize)); status = AudioObjectGetPropertyData(audioDevices[i], &propertyAddress, 0, nullptr, &dataSize, bufferList); if (kAudioHardwareNoError != status || 0 == bufferList->mNumberBuffers) { if (kAudioHardwareNoError != status) LOGE("AudioObjectGetPropertyData (kAudioDevicePropertyStreamConfiguration) failed: %i", status); std::free(bufferList); bufferList = nullptr; continue; } std::free(bufferList); bufferList = nullptr; AudioOutputDevice dev; char buf[1024]; CFStringGetCString(deviceName, buf, 1024, kCFStringEncodingUTF8); dev.displayName = std::string(buf); CFStringGetCString(deviceUID, buf, 1024, kCFStringEncodingUTF8); dev.id = std::string(buf); if (dev.id.rfind("VPAUAggregateAudioDevice-0x") == 0) continue; devs.push_back(dev); } std::free(audioDevices); audioDevices = nullptr; } void AudioOutputAudioUnitLegacy::SetCurrentDevice(std::string deviceID) { UInt32 size = sizeof(AudioDeviceID); AudioDeviceID outputDevice = 0; OSStatus status; AudioObjectPropertyAddress dataSourceProp = { kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; if (isMacBookPro && sysDevID && AudioObjectHasProperty(sysDevID, &dataSourceProp)) { AudioObjectRemovePropertyListener(sysDevID, &dataSourceProp, AudioOutputAudioUnitLegacy::DefaultDeviceChangedCallback, this); } if (deviceID == "default") { AudioObjectPropertyAddress propertyAddress; propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; propertyAddress.mScope = kAudioObjectPropertyScopeGlobal; propertyAddress.mElement = kAudioObjectPropertyElementMaster; UInt32 propsize = sizeof(AudioDeviceID); status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, &propsize, &outputDevice); CHECK_AU_ERROR(status, "Error getting default input device"); } else { AudioObjectPropertyAddress propertyAddress = { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; UInt32 dataSize = 0; status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, &dataSize); CHECK_AU_ERROR(status, "Error getting devices size"); UInt32 deviceCount = (UInt32)(dataSize / sizeof(AudioDeviceID)); AudioDeviceID audioDevices[deviceCount]; status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, nullptr, &dataSize, audioDevices); CHECK_AU_ERROR(status, "Error getting device list"); for (UInt32 i = 0; i < deviceCount; ++i) { // Query device UID CFStringRef deviceUID = nullptr; dataSize = sizeof(deviceUID); propertyAddress.mSelector = kAudioDevicePropertyDeviceUID; status = AudioObjectGetPropertyData(audioDevices[i], &propertyAddress, 0, nullptr, &dataSize, &deviceUID); CHECK_AU_ERROR(status, "Error getting device uid"); char buf[1024]; CFStringGetCString(deviceUID, buf, 1024, kCFStringEncodingUTF8); if (deviceID == buf) { LOGV("Found device for id %s", buf); outputDevice = audioDevices[i]; break; } } if (!outputDevice) { LOGW("Requested device not found, using default"); SetCurrentDevice("default"); return; } } status = AudioUnitSetProperty(unit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, kOutputBus, &outputDevice, size); CHECK_AU_ERROR(status, "Error setting output device"); AudioStreamBasicDescription hardwareFormat; size = sizeof(hardwareFormat); status = AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kOutputBus, &hardwareFormat, &size); CHECK_AU_ERROR(status, "Error getting hardware format"); hardwareSampleRate = hardwareFormat.mSampleRate; AudioStreamBasicDescription desiredFormat = { .mSampleRate = 48000, .mFormatID = kAudioFormatLinearPCM, .mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked | kAudioFormatFlagsNativeEndian, .mFramesPerPacket = 1, .mChannelsPerFrame = 1, .mBitsPerChannel = 16, .mBytesPerPacket = 2, .mBytesPerFrame = 2}; status = AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &desiredFormat, sizeof(desiredFormat)); CHECK_AU_ERROR(status, "Error setting format"); LOGD("Switched playback device, new sample rate %d", hardwareSampleRate); this->m_currentDevice = deviceID; sysDevID = outputDevice; AudioObjectPropertyAddress propertyAddress = { kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; size = 4; UInt32 bufferFrameSize; status = AudioObjectGetPropertyData(outputDevice, &propertyAddress, 0, nullptr, &size, &bufferFrameSize); if (status == noErr) { m_estimatedDelay = bufferFrameSize / 48; LOGD("CoreAudio buffer size for output device is %u frames (%u ms)", bufferFrameSize, m_estimatedDelay); } if (isMacBookPro) { if (AudioObjectHasProperty(outputDevice, &dataSourceProp)) { UInt32 dataSource; size = 4; AudioObjectGetPropertyData(outputDevice, &dataSourceProp, 0, nullptr, &size, &dataSource); SetPanRight(dataSource == 'ispk'); AudioObjectAddPropertyListener(outputDevice, &dataSourceProp, AudioOutputAudioUnitLegacy::DefaultDeviceChangedCallback, this); } else { SetPanRight(false); } } } void AudioOutputAudioUnitLegacy::SetPanRight(bool panRight) { LOGI("%sabling pan right on macbook pro", panRight ? "En" : "Dis"); std::int32_t channelMap[] = {panRight ? -1 : 0, 0}; OSStatus status = AudioUnitSetProperty(unit, kAudioOutputUnitProperty_ChannelMap, kAudioUnitScope_Global, kOutputBus, channelMap, sizeof(channelMap)); CHECK_AU_ERROR(status, "Error setting channel map"); } OSStatus AudioOutputAudioUnitLegacy::DefaultDeviceChangedCallback(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* inClientData) { AudioOutputAudioUnitLegacy* self = reinterpret_cast(inClientData); if (inAddresses[0].mSelector == kAudioHardwarePropertyDefaultOutputDevice) { LOGV("System default input device changed"); if (self->m_currentDevice == "default") { self->SetCurrentDevice(self->m_currentDevice); } } else if (inAddresses[0].mSelector == kAudioDevicePropertyDataSource) { UInt32 dataSource; UInt32 size = 4; AudioObjectGetPropertyData(inObjectID, inAddresses, 0, nullptr, &size, &dataSource); self->SetPanRight(dataSource == 'ispk'); } return noErr; }