/* Copyright (c) 2019 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "nativeaudio/AudioOut.h" #include "nativeaudio/AudioOut_private.h" #include #include #include #define TRANSPORT_BUFFER_SIZE 4096 static bool initialized; static AudioObjectID device; static AudioDeviceIOProcID ioProcID; static void * transportBuffer; static bool hostFormatSet; static OSStatus ioProc(AudioObjectID inDevice, const AudioTimeStamp * inNow, const AudioBufferList * inInputData, const AudioTimeStamp * inInputTime, AudioBufferList * outOutputData, const AudioTimeStamp * inOutputTime, void * __nullable inClientData) { AudioOut_transferToOutput(outOutputData->mBuffers[0].mData, g_AudioOut_outputCallback, g_AudioOut_outputContext, outOutputData->mBuffers[0].mDataByteSize / (g_AudioOut_hostFormat.bytesPerSample * g_AudioOut_hostFormat.channelCount), transportBuffer, TRANSPORT_BUFFER_SIZE, g_AudioOut_hostFormat, g_AudioOut_transportFormat, &g_AudioOut_resampleState); return 0; } static AudioOut_sampleFormat getCoreAudioSampleFormat(void) { AudioObjectPropertyAddress descriptionAddress = {kAudioStreamPropertyVirtualFormat, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMaster}; AudioStreamBasicDescription streamDescription = {0}; UInt32 size = sizeof(streamDescription); OSStatus status = AudioObjectGetPropertyData(device, &descriptionAddress, 0, NULL, &size, &streamDescription); if (status != 0) { #ifdef DEBUG fprintf(stderr, "AudioObjectGetPropertyData (kAudioStreamPropertyVirtualFormat) returned %d\n", status); #endif return g_AudioOut_hostFormat; } #ifdef DEBUG if (streamDescription.mChannelsPerFrame != 1 && streamDescription.mChannelsPerFrame != 2) { fprintf(stderr, "Warning: Unexpected channel count: %u\n", streamDescription.mChannelsPerFrame); } if (!(streamDescription.mFormatFlags & kAudioFormatFlagIsFloat) && (streamDescription.mBitsPerChannel != 8 && streamDescription.mBitsPerChannel != 16)) { fprintf(stderr, "Warning: Unexpected bit depth for integer output: %u\n", streamDescription.mBitsPerChannel); } else if ((streamDescription.mFormatFlags & kAudioFormatFlagIsFloat) && streamDescription.mBitsPerChannel != 32) { fprintf(stderr, "Warning: Unexpected bit depth for float output: %u\n", streamDescription.mBitsPerChannel); } if (streamDescription.mBytesPerFrame != streamDescription.mChannelsPerFrame * streamDescription.mBitsPerChannel / 8) { fprintf(stderr, "Warning: Unexpected bytes per frame for %u channels, %u bits per channel: %u\n", streamDescription.mChannelsPerFrame, streamDescription.mBitsPerChannel, streamDescription.mBytesPerFrame); } #endif AudioOut_sampleFormat sampleFormat; sampleFormat.channelCount = streamDescription.mChannelsPerFrame; sampleFormat.sampleRate = streamDescription.mSampleRate; sampleFormat.bytesPerSample = streamDescription.mBitsPerChannel / 8; return sampleFormat; } static void setCoreAudioSampleFormat(AudioOut_sampleFormat sampleFormat) { AudioStreamBasicDescription streamDescription = {0}; streamDescription.mBitsPerChannel = g_AudioOut_hostFormat.bytesPerSample * 8; streamDescription.mBytesPerFrame = g_AudioOut_hostFormat.bytesPerSample * g_AudioOut_hostFormat.channelCount; streamDescription.mChannelsPerFrame = g_AudioOut_hostFormat.channelCount; streamDescription.mBytesPerPacket = streamDescription.mBytesPerFrame; if (g_AudioOut_hostFormat.bytesPerSample == 4) { streamDescription.mFormatFlags = kAudioFormatFlagIsFloat; } else { streamDescription.mFormatFlags = kAudioFormatFlagIsSignedInteger; } streamDescription.mFormatID = kAudioFormatLinearPCM; streamDescription.mFramesPerPacket = 1; streamDescription.mSampleRate = g_AudioOut_hostFormat.sampleRate; AudioObjectPropertyAddress descriptionAddress = {kAudioStreamPropertyVirtualFormat, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMaster}; UInt32 size = sizeof(streamDescription); OSStatus status = AudioObjectSetPropertyData(device, &descriptionAddress, 0, NULL, size, &streamDescription); if (status != 0) { #ifdef DEBUG fprintf(stderr, "AudioObjectSetPropertyData (kAudioStreamPropertyVirtualFormat) returned %d\n", status); #endif return; } } static OSStatus propertyListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress * inAddresses, void * __nullable inClientData) { g_AudioOut_hostFormat = getCoreAudioSampleFormat(); return 0; } void AudioOut_init(const char * processName) { if (initialized) { return; } AudioObjectPropertyAddress objectAddress = {kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; AudioObjectPropertyAddress descriptionAddress = {kAudioStreamPropertyVirtualFormat, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMaster}; UInt32 size = sizeof(device); OSStatus status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &objectAddress, 0, NULL, &size, &device); if (status != 0) { #ifdef DEBUG fprintf(stderr, "AudioObjectGetPropertyData (kAudioHardwarePropertyDefaultOutputDevice) returned %d\n", status); #endif return; } status = AudioObjectAddPropertyListener(device, &descriptionAddress, propertyListenerProc, NULL); if (status != 0) { #ifdef DEBUG fprintf(stderr, "AudioObjectAddPropertyListener returned %d\n", status); #endif return; } if (hostFormatSet) { setCoreAudioSampleFormat(g_AudioOut_hostFormat); } g_AudioOut_transportFormat = g_AudioOut_hostFormat = getCoreAudioSampleFormat(); transportBuffer = malloc(TRANSPORT_BUFFER_SIZE); initialized = true; } void AudioOut_shutdown(void) { } void AudioOut_setHostFormat(AudioOut_sampleFormat format) { if (format.channelCount == g_AudioOut_hostFormat.channelCount && format.bytesPerSample == g_AudioOut_hostFormat.bytesPerSample && format.sampleRate == g_AudioOut_hostFormat.sampleRate) { return; } if (initialized) { bool wasPlaying = g_AudioOut_outputActive; if (wasPlaying) { AudioOut_stopOutput(); } setCoreAudioSampleFormat(format); if (wasPlaying) { AudioOut_startOutput(g_AudioOut_outputCallback, g_AudioOut_outputContext); } } else { g_AudioOut_hostFormat = format; hostFormatSet = true; } } void AudioOut_startOutput(AudioOutCallback callback, void * context) { g_AudioOut_outputCallback = callback; g_AudioOut_outputContext = context; AudioDeviceCreateIOProcID(device, ioProc, NULL, &ioProcID); AudioDeviceStart(device, ioProcID); g_AudioOut_outputActive = true; } void AudioOut_stopOutput(void) { AudioDeviceStop(device, ioProcID); AudioDeviceDestroyIOProcID(device, ioProcID); g_AudioOut_outputActive = false; }