/** * OpenAL cross platform audio library * Copyright (C) 1999-2007 by authors. * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * Or go to http://www.gnu.org/copyleft/lgpl.html */ #include "config.h" #include "coreaudio.h" #include #include #include #include #include #include #include #include #include #include #include #include "alnumeric.h" #include "core/converter.h" #include "core/device.h" #include "core/logging.h" #include "ringbuffer.h" #include "alc/events.h" #include #include #if TARGET_OS_IOS || TARGET_OS_TV #define CAN_ENUMERATE 0 #else #include #define CAN_ENUMERATE 1 #endif namespace { constexpr auto OutputElement = 0; constexpr auto InputElement = 1; struct FourCCPrinter { char mString[sizeof(UInt32) + 1]{}; constexpr FourCCPrinter(UInt32 code) noexcept { for(size_t i{0};i < sizeof(UInt32);++i) { const auto ch = static_cast(code & 0xff); /* If this breaks early it'll leave the first byte null, to get * read as a 0-length string. */ if(ch <= 0x1f || ch >= 0x7f) break; mString[sizeof(UInt32)-1-i] = ch; code >>= 8; } } constexpr FourCCPrinter(int code) noexcept : FourCCPrinter{static_cast(code)} { } constexpr const char *c_str() const noexcept { return mString; } }; #if CAN_ENUMERATE struct DeviceEntry { AudioDeviceID mId; std::string mName; }; std::vector PlaybackList; std::vector CaptureList; OSStatus GetHwProperty(AudioHardwarePropertyID propId, UInt32 dataSize, void *propData) { const AudioObjectPropertyAddress addr{propId, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; return AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, nullptr, &dataSize, propData); } OSStatus GetHwPropertySize(AudioHardwarePropertyID propId, UInt32 *outSize) { const AudioObjectPropertyAddress addr{propId, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; return AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &addr, 0, nullptr, outSize); } OSStatus GetDevProperty(AudioDeviceID devId, AudioDevicePropertyID propId, bool isCapture, UInt32 elem, UInt32 dataSize, void *propData) { static const AudioObjectPropertyScope scopes[2]{kAudioDevicePropertyScopeOutput, kAudioDevicePropertyScopeInput}; const AudioObjectPropertyAddress addr{propId, scopes[isCapture], elem}; return AudioObjectGetPropertyData(devId, &addr, 0, nullptr, &dataSize, propData); } OSStatus GetDevPropertySize(AudioDeviceID devId, AudioDevicePropertyID inPropertyID, bool isCapture, UInt32 elem, UInt32 *outSize) { static const AudioObjectPropertyScope scopes[2]{kAudioDevicePropertyScopeOutput, kAudioDevicePropertyScopeInput}; const AudioObjectPropertyAddress addr{inPropertyID, scopes[isCapture], elem}; return AudioObjectGetPropertyDataSize(devId, &addr, 0, nullptr, outSize); } std::string GetDeviceName(AudioDeviceID devId) { std::string devname; CFStringRef nameRef; /* Try to get the device name as a CFString, for Unicode name support. */ OSStatus err{GetDevProperty(devId, kAudioDevicePropertyDeviceNameCFString, false, 0, sizeof(nameRef), &nameRef)}; if(err == noErr) { const CFIndex propSize{CFStringGetMaximumSizeForEncoding(CFStringGetLength(nameRef), kCFStringEncodingUTF8)}; devname.resize(static_cast(propSize)+1, '\0'); CFStringGetCString(nameRef, &devname[0], propSize+1, kCFStringEncodingUTF8); CFRelease(nameRef); } else { /* If that failed, just get the C string. Hopefully there's nothing bad * with this. */ UInt32 propSize{}; if(GetDevPropertySize(devId, kAudioDevicePropertyDeviceName, false, 0, &propSize)) return devname; devname.resize(propSize+1, '\0'); if(GetDevProperty(devId, kAudioDevicePropertyDeviceName, false, 0, propSize, &devname[0])) { devname.clear(); return devname; } } /* Clear extraneous nul chars that may have been written with the name * string, and return it. */ while(!devname.back()) devname.pop_back(); return devname; } UInt32 GetDeviceChannelCount(AudioDeviceID devId, bool isCapture) { UInt32 propSize{}; auto err = GetDevPropertySize(devId, kAudioDevicePropertyStreamConfiguration, isCapture, 0, &propSize); if(err) { ERR("kAudioDevicePropertyStreamConfiguration size query failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return 0; } auto buflist_data = std::make_unique(propSize); auto *buflist = reinterpret_cast(buflist_data.get()); err = GetDevProperty(devId, kAudioDevicePropertyStreamConfiguration, isCapture, 0, propSize, buflist); if(err) { ERR("kAudioDevicePropertyStreamConfiguration query failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return 0; } UInt32 numChannels{0}; for(size_t i{0};i < buflist->mNumberBuffers;++i) numChannels += buflist->mBuffers[i].mNumberChannels; return numChannels; } void EnumerateDevices(std::vector &list, bool isCapture) { UInt32 propSize{}; if(auto err = GetHwPropertySize(kAudioHardwarePropertyDevices, &propSize)) { ERR("Failed to get device list size: %u\n", err); return; } auto devIds = std::vector(propSize/sizeof(AudioDeviceID), kAudioDeviceUnknown); if(auto err = GetHwProperty(kAudioHardwarePropertyDevices, propSize, devIds.data())) { ERR("Failed to get device list: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return; } std::vector newdevs; newdevs.reserve(devIds.size()); AudioDeviceID defaultId{kAudioDeviceUnknown}; GetHwProperty(isCapture ? kAudioHardwarePropertyDefaultInputDevice : kAudioHardwarePropertyDefaultOutputDevice, sizeof(defaultId), &defaultId); if(defaultId != kAudioDeviceUnknown) { newdevs.emplace_back(DeviceEntry{defaultId, GetDeviceName(defaultId)}); const auto &entry = newdevs.back(); TRACE("Got device: %s = ID %u\n", entry.mName.c_str(), entry.mId); } for(const AudioDeviceID devId : devIds) { if(devId == kAudioDeviceUnknown) continue; auto match_devid = [devId](const DeviceEntry &entry) noexcept -> bool { return entry.mId == devId; }; auto match = std::find_if(newdevs.cbegin(), newdevs.cend(), match_devid); if(match != newdevs.cend()) continue; auto numChannels = GetDeviceChannelCount(devId, isCapture); if(numChannels > 0) { newdevs.emplace_back(DeviceEntry{devId, GetDeviceName(devId)}); const auto &entry = newdevs.back(); TRACE("Got device: %s = ID %u\n", entry.mName.c_str(), entry.mId); } } if(newdevs.size() > 1) { /* Rename entries that have matching names, by appending '#2', '#3', * etc, as needed. */ for(auto curitem = newdevs.begin()+1;curitem != newdevs.end();++curitem) { auto check_match = [curitem](const DeviceEntry &entry) -> bool { return entry.mName == curitem->mName; }; if(std::find_if(newdevs.begin(), curitem, check_match) != curitem) { std::string name{curitem->mName}; size_t count{1}; auto check_name = [&name](const DeviceEntry &entry) -> bool { return entry.mName == name; }; do { name = curitem->mName; name += " #"; name += std::to_string(++count); } while(std::find_if(newdevs.begin(), curitem, check_name) != curitem); curitem->mName = std::move(name); } } } newdevs.shrink_to_fit(); newdevs.swap(list); } struct DeviceHelper { DeviceHelper() { AudioObjectPropertyAddress addr{kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}; OSStatus status = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &addr, DeviceListenerProc, nil); if (status != noErr) ERR("AudioObjectAddPropertyListener fail: %d", status); } ~DeviceHelper() { AudioObjectPropertyAddress addr{kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}; OSStatus status = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &addr, DeviceListenerProc, nil); if (status != noErr) ERR("AudioObjectRemovePropertyListener fail: %d", status); } static OSStatus DeviceListenerProc(AudioObjectID /*inObjectID*/, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void* /*inClientData*/) { for(UInt32 i = 0; i < inNumberAddresses; ++i) { switch(inAddresses[i].mSelector) { case kAudioHardwarePropertyDefaultOutputDevice: case kAudioHardwarePropertyDefaultSystemOutputDevice: alc::Event(alc::EventType::DefaultDeviceChanged, alc::DeviceType::Playback, "Default playback device changed: "+std::to_string(inAddresses[i].mSelector)); break; case kAudioHardwarePropertyDefaultInputDevice: alc::Event(alc::EventType::DefaultDeviceChanged, alc::DeviceType::Capture, "Default capture device changed: "+std::to_string(inAddresses[i].mSelector)); break; } } return noErr; } }; static std::optional sDeviceHelper; #else static constexpr char ca_device[] = "CoreAudio Default"; #endif struct CoreAudioPlayback final : public BackendBase { CoreAudioPlayback(DeviceBase *device) noexcept : BackendBase{device} { } ~CoreAudioPlayback() override; OSStatus MixerProc(AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) noexcept; void open(std::string_view name) override; bool reset() override; void start() override; void stop() override; AudioUnit mAudioUnit{}; uint mFrameSize{0u}; AudioStreamBasicDescription mFormat{}; // This is the OpenAL format as a CoreAudio ASBD DEF_NEWDEL(CoreAudioPlayback) }; CoreAudioPlayback::~CoreAudioPlayback() { AudioUnitUninitialize(mAudioUnit); AudioComponentInstanceDispose(mAudioUnit); } OSStatus CoreAudioPlayback::MixerProc(AudioUnitRenderActionFlags*, const AudioTimeStamp*, UInt32, UInt32, AudioBufferList *ioData) noexcept { for(size_t i{0};i < ioData->mNumberBuffers;++i) { auto &buffer = ioData->mBuffers[i]; mDevice->renderSamples(buffer.mData, buffer.mDataByteSize/mFrameSize, buffer.mNumberChannels); } return noErr; } void CoreAudioPlayback::open(std::string_view name) { #if CAN_ENUMERATE AudioDeviceID audioDevice{kAudioDeviceUnknown}; if(name.empty()) GetHwProperty(kAudioHardwarePropertyDefaultOutputDevice, sizeof(audioDevice), &audioDevice); else { if(PlaybackList.empty()) EnumerateDevices(PlaybackList, false); auto find_name = [name](const DeviceEntry &entry) -> bool { return entry.mName == name; }; auto devmatch = std::find_if(PlaybackList.cbegin(), PlaybackList.cend(), find_name); if(devmatch == PlaybackList.cend()) throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%.*s\" not found", static_cast(name.length()), name.data()}; audioDevice = devmatch->mId; } #else if(name.empty()) name = ca_device; else if(name != ca_device) throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%.*s\" not found", static_cast(name.length()), name.data()}; #endif /* open the default output unit */ AudioComponentDescription desc{}; desc.componentType = kAudioUnitType_Output; #if CAN_ENUMERATE desc.componentSubType = (audioDevice == kAudioDeviceUnknown) ? kAudioUnitSubType_DefaultOutput : kAudioUnitSubType_HALOutput; #else desc.componentSubType = kAudioUnitSubType_RemoteIO; #endif desc.componentManufacturer = kAudioUnitManufacturer_Apple; desc.componentFlags = 0; desc.componentFlagsMask = 0; AudioComponent comp{AudioComponentFindNext(NULL, &desc)}; if(comp == nullptr) throw al::backend_exception{al::backend_error::NoDevice, "Could not find audio component"}; AudioUnit audioUnit{}; OSStatus err{AudioComponentInstanceNew(comp, &audioUnit)}; if(err != noErr) throw al::backend_exception{al::backend_error::NoDevice, "Could not create component instance: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; #if CAN_ENUMERATE if(audioDevice != kAudioDeviceUnknown) AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, OutputElement, &audioDevice, sizeof(AudioDeviceID)); #endif err = AudioUnitInitialize(audioUnit); if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "Could not initialize audio unit: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; /* WARNING: I don't know if "valid" audio unit values are guaranteed to be * non-0. If not, this logic is broken. */ if(mAudioUnit) { AudioUnitUninitialize(mAudioUnit); AudioComponentInstanceDispose(mAudioUnit); } mAudioUnit = audioUnit; #if CAN_ENUMERATE if(!name.empty()) mDevice->DeviceName = name; else { UInt32 propSize{sizeof(audioDevice)}; audioDevice = kAudioDeviceUnknown; AudioUnitGetProperty(audioUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, OutputElement, &audioDevice, &propSize); std::string devname{GetDeviceName(audioDevice)}; if(!devname.empty()) mDevice->DeviceName = std::move(devname); else mDevice->DeviceName = "Unknown Device Name"; } if(audioDevice != kAudioDeviceUnknown) { UInt32 type{}; err = GetDevProperty(audioDevice, kAudioDevicePropertyDataSource, false, kAudioObjectPropertyElementMaster, sizeof(type), &type); if(err != noErr) ERR("Failed to get audio device type: %u\n", err); else { TRACE("Got device type '%s'\n", FourCCPrinter{type}.c_str()); mDevice->Flags.set(DirectEar, (type == kIOAudioOutputPortSubTypeHeadphones)); } } #else mDevice->DeviceName = name; #endif } bool CoreAudioPlayback::reset() { OSStatus err{AudioUnitUninitialize(mAudioUnit)}; if(err != noErr) ERR("AudioUnitUninitialize failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); /* retrieve default output unit's properties (output side) */ AudioStreamBasicDescription streamFormat{}; UInt32 size{sizeof(streamFormat)}; err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, OutputElement, &streamFormat, &size); if(err != noErr || size != sizeof(streamFormat)) { ERR("AudioUnitGetProperty(StreamFormat) failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return false; } #if 0 TRACE("Output streamFormat of default output unit -\n"); TRACE(" streamFormat.mFramesPerPacket = %d\n", streamFormat.mFramesPerPacket); TRACE(" streamFormat.mChannelsPerFrame = %d\n", streamFormat.mChannelsPerFrame); TRACE(" streamFormat.mBitsPerChannel = %d\n", streamFormat.mBitsPerChannel); TRACE(" streamFormat.mBytesPerPacket = %d\n", streamFormat.mBytesPerPacket); TRACE(" streamFormat.mBytesPerFrame = %d\n", streamFormat.mBytesPerFrame); TRACE(" streamFormat.mSampleRate = %5.0f\n", streamFormat.mSampleRate); #endif /* Use the sample rate from the output unit's current parameters, but reset * everything else. */ if(mDevice->Frequency != streamFormat.mSampleRate) { mDevice->BufferSize = static_cast(mDevice->BufferSize*streamFormat.mSampleRate/ mDevice->Frequency + 0.5); mDevice->Frequency = static_cast(streamFormat.mSampleRate); } /* FIXME: How to tell what channels are what in the output device, and how * to specify what we're giving? e.g. 6.0 vs 5.1 */ streamFormat.mChannelsPerFrame = mDevice->channelsFromFmt(); streamFormat.mFramesPerPacket = 1; streamFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kLinearPCMFormatFlagIsPacked; streamFormat.mFormatID = kAudioFormatLinearPCM; switch(mDevice->FmtType) { case DevFmtUByte: mDevice->FmtType = DevFmtByte; /* fall-through */ case DevFmtByte: streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger; streamFormat.mBitsPerChannel = 8; break; case DevFmtUShort: mDevice->FmtType = DevFmtShort; /* fall-through */ case DevFmtShort: streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger; streamFormat.mBitsPerChannel = 16; break; case DevFmtUInt: mDevice->FmtType = DevFmtInt; /* fall-through */ case DevFmtInt: streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger; streamFormat.mBitsPerChannel = 32; break; case DevFmtFloat: streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsFloat; streamFormat.mBitsPerChannel = 32; break; } streamFormat.mBytesPerFrame = streamFormat.mChannelsPerFrame*streamFormat.mBitsPerChannel/8; streamFormat.mBytesPerPacket = streamFormat.mBytesPerFrame*streamFormat.mFramesPerPacket; err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, OutputElement, &streamFormat, sizeof(streamFormat)); if(err != noErr) { ERR("AudioUnitSetProperty(StreamFormat) failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return false; } setDefaultWFXChannelOrder(); /* setup callback */ mFrameSize = mDevice->frameSizeFromFmt(); AURenderCallbackStruct input{}; input.inputProc = [](void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) noexcept { return static_cast(inRefCon)->MixerProc(ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData); }; input.inputProcRefCon = this; err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, OutputElement, &input, sizeof(AURenderCallbackStruct)); if(err != noErr) { ERR("AudioUnitSetProperty(SetRenderCallback) failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return false; } /* init the default audio unit... */ err = AudioUnitInitialize(mAudioUnit); if(err != noErr) { ERR("AudioUnitInitialize failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return false; } return true; } void CoreAudioPlayback::start() { const OSStatus err{AudioOutputUnitStart(mAudioUnit)}; if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "AudioOutputUnitStart failed: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; } void CoreAudioPlayback::stop() { OSStatus err{AudioOutputUnitStop(mAudioUnit)}; if(err != noErr) ERR("AudioOutputUnitStop failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); } struct CoreAudioCapture final : public BackendBase { CoreAudioCapture(DeviceBase *device) noexcept : BackendBase{device} { } ~CoreAudioCapture() override; OSStatus RecordProc(AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) noexcept; void open(std::string_view name) override; void start() override; void stop() override; void captureSamples(std::byte *buffer, uint samples) override; uint availableSamples() override; AudioUnit mAudioUnit{0}; uint mFrameSize{0u}; AudioStreamBasicDescription mFormat{}; // This is the OpenAL format as a CoreAudio ASBD SampleConverterPtr mConverter; std::vector mCaptureData; RingBufferPtr mRing{nullptr}; DEF_NEWDEL(CoreAudioCapture) }; CoreAudioCapture::~CoreAudioCapture() { if(mAudioUnit) AudioComponentInstanceDispose(mAudioUnit); mAudioUnit = 0; } OSStatus CoreAudioCapture::RecordProc(AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList*) noexcept { union { std::byte _[maxz(sizeof(AudioBufferList), offsetof(AudioBufferList, mBuffers[1]))]; AudioBufferList list; } audiobuf{}; audiobuf.list.mNumberBuffers = 1; audiobuf.list.mBuffers[0].mNumberChannels = mFormat.mChannelsPerFrame; audiobuf.list.mBuffers[0].mData = mCaptureData.data(); audiobuf.list.mBuffers[0].mDataByteSize = static_cast(mCaptureData.size()); OSStatus err{AudioUnitRender(mAudioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &audiobuf.list)}; if(err != noErr) { ERR("AudioUnitRender capture error: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); return err; } mRing->write(mCaptureData.data(), inNumberFrames); return noErr; } void CoreAudioCapture::open(std::string_view name) { #if CAN_ENUMERATE AudioDeviceID audioDevice{kAudioDeviceUnknown}; if(name.empty()) GetHwProperty(kAudioHardwarePropertyDefaultInputDevice, sizeof(audioDevice), &audioDevice); else { if(CaptureList.empty()) EnumerateDevices(CaptureList, true); auto find_name = [name](const DeviceEntry &entry) -> bool { return entry.mName == name; }; auto devmatch = std::find_if(CaptureList.cbegin(), CaptureList.cend(), find_name); if(devmatch == CaptureList.cend()) throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%.*s\" not found", static_cast(name.length()), name.data()}; audioDevice = devmatch->mId; } #else if(name.empty()) name = ca_device; else if(name != ca_device) throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%.*s\" not found", static_cast(name.length()), name.data()}; #endif AudioComponentDescription desc{}; desc.componentType = kAudioUnitType_Output; #if CAN_ENUMERATE desc.componentSubType = (audioDevice == kAudioDeviceUnknown) ? kAudioUnitSubType_DefaultOutput : kAudioUnitSubType_HALOutput; #else desc.componentSubType = kAudioUnitSubType_RemoteIO; #endif desc.componentManufacturer = kAudioUnitManufacturer_Apple; desc.componentFlags = 0; desc.componentFlagsMask = 0; // Search for component with given description AudioComponent comp{AudioComponentFindNext(NULL, &desc)}; if(comp == NULL) throw al::backend_exception{al::backend_error::NoDevice, "Could not find audio component"}; // Open the component OSStatus err{AudioComponentInstanceNew(comp, &mAudioUnit)}; if(err != noErr) throw al::backend_exception{al::backend_error::NoDevice, "Could not create component instance: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; // Turn off AudioUnit output UInt32 enableIO{0}; err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, OutputElement, &enableIO, sizeof(enableIO)); if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "Could not disable audio unit output property: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; // Turn on AudioUnit input enableIO = 1; err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, InputElement, &enableIO, sizeof(enableIO)); if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "Could not enable audio unit input property: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; #if CAN_ENUMERATE if(audioDevice != kAudioDeviceUnknown) AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, InputElement, &audioDevice, sizeof(AudioDeviceID)); #endif // set capture callback AURenderCallbackStruct input{}; input.inputProc = [](void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) noexcept { return static_cast(inRefCon)->RecordProc(ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData); }; input.inputProcRefCon = this; err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, InputElement, &input, sizeof(AURenderCallbackStruct)); if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "Could not set capture callback: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; // Disable buffer allocation for capture UInt32 flag{0}; err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, InputElement, &flag, sizeof(flag)); if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "Could not disable buffer allocation property: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; // Initialize the device err = AudioUnitInitialize(mAudioUnit); if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "Could not initialize audio unit: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; // Get the hardware format AudioStreamBasicDescription hardwareFormat{}; UInt32 propertySize{sizeof(hardwareFormat)}; err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, InputElement, &hardwareFormat, &propertySize); if(err != noErr || propertySize != sizeof(hardwareFormat)) throw al::backend_exception{al::backend_error::DeviceError, "Could not get input format: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; // Set up the requested format description AudioStreamBasicDescription requestedFormat{}; switch(mDevice->FmtType) { case DevFmtByte: requestedFormat.mBitsPerChannel = 8; requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; break; case DevFmtUByte: requestedFormat.mBitsPerChannel = 8; requestedFormat.mFormatFlags = kAudioFormatFlagIsPacked; break; case DevFmtShort: requestedFormat.mBitsPerChannel = 16; requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; break; case DevFmtUShort: requestedFormat.mBitsPerChannel = 16; requestedFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; break; case DevFmtInt: requestedFormat.mBitsPerChannel = 32; requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; break; case DevFmtUInt: requestedFormat.mBitsPerChannel = 32; requestedFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; break; case DevFmtFloat: requestedFormat.mBitsPerChannel = 32; requestedFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; break; } switch(mDevice->FmtChans) { case DevFmtMono: requestedFormat.mChannelsPerFrame = 1; break; case DevFmtStereo: requestedFormat.mChannelsPerFrame = 2; break; case DevFmtQuad: case DevFmtX51: case DevFmtX61: case DevFmtX71: case DevFmtX714: case DevFmtX3D71: case DevFmtAmbi3D: throw al::backend_exception{al::backend_error::DeviceError, "%s not supported", DevFmtChannelsString(mDevice->FmtChans)}; } requestedFormat.mBytesPerFrame = requestedFormat.mChannelsPerFrame * requestedFormat.mBitsPerChannel / 8; requestedFormat.mBytesPerPacket = requestedFormat.mBytesPerFrame; requestedFormat.mSampleRate = mDevice->Frequency; requestedFormat.mFormatID = kAudioFormatLinearPCM; requestedFormat.mReserved = 0; requestedFormat.mFramesPerPacket = 1; // save requested format description for later use mFormat = requestedFormat; mFrameSize = mDevice->frameSizeFromFmt(); // Use intermediate format for sample rate conversion (outputFormat) // Set sample rate to the same as hardware for resampling later AudioStreamBasicDescription outputFormat{requestedFormat}; outputFormat.mSampleRate = hardwareFormat.mSampleRate; // The output format should be the requested format, but using the hardware sample rate // This is because the AudioUnit will automatically scale other properties, except for sample rate err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, InputElement, &outputFormat, sizeof(outputFormat)); if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "Could not set input format: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; /* Calculate the minimum AudioUnit output format frame count for the pre- * conversion ring buffer. Ensure at least 100ms for the total buffer. */ double srateScale{outputFormat.mSampleRate / mDevice->Frequency}; auto FrameCount64 = maxu64(static_cast(std::ceil(mDevice->BufferSize*srateScale)), static_cast(outputFormat.mSampleRate)/10); FrameCount64 += MaxResamplerPadding; if(FrameCount64 > std::numeric_limits::max()) throw al::backend_exception{al::backend_error::DeviceError, "Calculated frame count is too large: %" PRIu64, FrameCount64}; UInt32 outputFrameCount{}; propertySize = sizeof(outputFrameCount); err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, OutputElement, &outputFrameCount, &propertySize); if(err != noErr || propertySize != sizeof(outputFrameCount)) throw al::backend_exception{al::backend_error::DeviceError, "Could not get input frame count: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; mCaptureData.resize(outputFrameCount * mFrameSize); outputFrameCount = static_cast(maxu64(outputFrameCount, FrameCount64)); mRing = RingBuffer::Create(outputFrameCount, mFrameSize, false); /* Set up sample converter if needed */ if(outputFormat.mSampleRate != mDevice->Frequency) mConverter = SampleConverter::Create(mDevice->FmtType, mDevice->FmtType, mFormat.mChannelsPerFrame, static_cast(hardwareFormat.mSampleRate), mDevice->Frequency, Resampler::FastBSinc24); #if CAN_ENUMERATE if(!name.empty()) mDevice->DeviceName = name; else { UInt32 propSize{sizeof(audioDevice)}; audioDevice = kAudioDeviceUnknown; AudioUnitGetProperty(mAudioUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, InputElement, &audioDevice, &propSize); std::string devname{GetDeviceName(audioDevice)}; if(!devname.empty()) mDevice->DeviceName = std::move(devname); else mDevice->DeviceName = "Unknown Device Name"; } #else mDevice->DeviceName = name; #endif } void CoreAudioCapture::start() { OSStatus err{AudioOutputUnitStart(mAudioUnit)}; if(err != noErr) throw al::backend_exception{al::backend_error::DeviceError, "AudioOutputUnitStart failed: '%s' (%u)", FourCCPrinter{err}.c_str(), err}; } void CoreAudioCapture::stop() { OSStatus err{AudioOutputUnitStop(mAudioUnit)}; if(err != noErr) ERR("AudioOutputUnitStop failed: '%s' (%u)\n", FourCCPrinter{err}.c_str(), err); } void CoreAudioCapture::captureSamples(std::byte *buffer, uint samples) { if(!mConverter) { mRing->read(buffer, samples); return; } auto rec_vec = mRing->getReadVector(); const void *src0{rec_vec.first.buf}; auto src0len = static_cast(rec_vec.first.len); uint got{mConverter->convert(&src0, &src0len, buffer, samples)}; size_t total_read{rec_vec.first.len - src0len}; if(got < samples && !src0len && rec_vec.second.len > 0) { const void *src1{rec_vec.second.buf}; auto src1len = static_cast(rec_vec.second.len); got += mConverter->convert(&src1, &src1len, buffer + got*mFrameSize, samples-got); total_read += rec_vec.second.len - src1len; } mRing->readAdvance(total_read); } uint CoreAudioCapture::availableSamples() { if(!mConverter) return static_cast(mRing->readSpace()); return mConverter->availableOut(static_cast(mRing->readSpace())); } } // namespace BackendFactory &CoreAudioBackendFactory::getFactory() { static CoreAudioBackendFactory factory{}; return factory; } bool CoreAudioBackendFactory::init() { #if CAN_ENUMERATE sDeviceHelper.emplace(); #endif return true; } bool CoreAudioBackendFactory::querySupport(BackendType type) { return type == BackendType::Playback || type == BackendType::Capture; } std::string CoreAudioBackendFactory::probe(BackendType type) { std::string outnames; #if CAN_ENUMERATE auto append_name = [&outnames](const DeviceEntry &entry) -> void { /* Includes null char. */ outnames.append(entry.mName.c_str(), entry.mName.length()+1); }; switch(type) { case BackendType::Playback: EnumerateDevices(PlaybackList, false); std::for_each(PlaybackList.cbegin(), PlaybackList.cend(), append_name); break; case BackendType::Capture: EnumerateDevices(CaptureList, true); std::for_each(CaptureList.cbegin(), CaptureList.cend(), append_name); break; } #else switch(type) { case BackendType::Playback: case BackendType::Capture: /* Includes null char. */ outnames.append(ca_device, sizeof(ca_device)); break; } #endif return outnames; } BackendPtr CoreAudioBackendFactory::createBackend(DeviceBase *device, BackendType type) { if(type == BackendType::Playback) return BackendPtr{new CoreAudioPlayback{device}}; if(type == BackendType::Capture) return BackendPtr{new CoreAudioCapture{device}}; return nullptr; }