// Native Audio // 5argon - Exceed7 Experiments // Problems/suggestions : 5argon@exceed7.com // Special thanks to Con for written this wonderful OpenAL tutorial : http://ohno789.blogspot.com/2013/08/playing-audio-samples-using-openal-on.html #import "NativeAudio.h" //#define LOG_NATIVE_AUDIO @implementation NativeAudio static ALCdevice *openALDevice; static ALCcontext *openALContext; //OpenAL sources starts at number 2400 //Hard limit specified by the library is that we can use up to 32 sources. #define kMaxConcurrentSources 32 //We split stereo to separated sources, so we could do "balance panning" by adjusting each source's volume and without messing with OpenAL's positional audio to simulate panning. //Unfortunately this reduces the usable sources from 32 to 16. //For some reason, OpenAL prints this //2019-03-16 00:02:09.703867+0700 ProductName[239:4538] AUBase.cpp:832:DispatchSetProperty: ca_require: ValidFormat(inScope, inElement, newDesc) InvalidFormat //Only while playing an audio rapidly over a certain frequency, once every 16th play, and cause only left ear to be audible at that play O_o //I am not sure what is going on or is it OpenAL's bug, but reducing it to 15 works O_o //Something must be defective at that 32th source, and that belongs to the right ear by our stereo splitting gymnastic explained earlier. I don't know the reason, but for now please bear with 1 less source. //Wtf... #define kHalfMaxConcurrentSources 15 //OpenAL buffer index starts at number 2432. (Now you know then source limit is implicitly 32) //This number will be remembered in NativeAudioPointer at managed side. //As far as I know there is no limit. I can allocate way over 500 sounds and it does not seems to cause any bad things. //But of course every sound will cost memory and that should be your real limit. //This is limit for just in case someday we discover a real hard limit, then Native Audio could warn us. #define kMaxBuffers 1024 #define fixedMixingRate 24000 //Error when this goes to max static int bufferAllocationCount = 0; //Never reset static int runningBufferAllocationNumber = 0; static NativeAudioSourceIdPair* nasips; static NativeAudioBufferIdPair* nabips; +(void) naHandleInterruption: (NSNotification*) notification { id audioInterruption = [[notification userInfo] valueForKey:AVAudioSessionInterruptionTypeKey]; if(audioInterruption != NULL) { AVAudioSessionInterruptionType typeKey = (AVAudioSessionInterruptionType) [audioInterruption integerValue]; if(typeKey == AVAudioSessionInterruptionTypeBegan) { //NSLog(@"--- INTERRUPTION ---"); alcMakeContextCurrent(NULL); } else { //There is an iOS bug.. if you end the incoming call TOO FAST or unlucky (?) then end interruption won't be called lol //In that case we handle it with device becoming active notification instead. //NSLog(@"--- END INTERRUPTION ---"); alcMakeContextCurrent(openALContext); } } } +(void) naDidBecomeActive: (NSNotification*) notification { //NSLog(@"--- END INTERRUPTION (interruption end bugged) ---"); //If the context was set before on interruption, //I think it is fine to set it again. alcMakeContextCurrent(openALContext); } + (AudioFileID) openAudioFile:(NSString *)audioFilePathAsString { NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePathAsString]; AudioFileID afid; OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid); if (0 != openAudioFileResult) { NSLog(@"An error occurred when attempting to open the audio file %@: %d", audioFilePathAsString, (int)openAudioFileResult); } return afid; } + (UInt32) getSizeOfAudioComponent:(AudioFileID)afid { UInt64 audioDataSize = 0; UInt32 propertySize = sizeof(UInt64); OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &propertySize, &audioDataSize); if (0 != getSizeResult) { NSLog(@"An error occurred when attempting to determine the size of audio file."); } return (UInt32)audioDataSize; } + (AudioStreamBasicDescription) getDescription:(AudioFileID)afid { AudioStreamBasicDescription desc; UInt32 propertySize = sizeof(desc); OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyDataFormat, &propertySize, &desc); if (0 != getSizeResult) { NSLog(@"An error occurred when attempting to determine the property of audio file."); } return desc; } + (int) Initialize { openALDevice = alcOpenDevice(NULL); openALContext = alcCreateContext(openALDevice, NULL); /* ALCint attributes[] = { ALC_FREQUENCY, fixedMixingRate }; */ //openALContext = alcCreateContext(openALDevice, attributes); alcMakeContextCurrent(openALContext); NSNotificationCenter* notiCenter = [NSNotificationCenter defaultCenter]; //This is for handling phone calls, etc. Unity already handles AVAudioSession I think, we then handle OpenAL additionally. [notiCenter addObserver:self selector:@selector(naHandleInterruption:) name:AVAudioSessionInterruptionNotification object:NULL]; //This is to handle iOS bug where if you end the phone call too fast or unlucky the interruption ended will not be called. //So DidBecomeActive will be an another safety net for us. Interruption began can't be missed I think, so it is safe //not to register WillResignActive.. (?) //[notiCenter addObserver:self selector:@selector(naWillResignActive:) name:UIApplicationWillResignActiveNotification object:NULL]; [notiCenter addObserver:self selector:@selector(naDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:NULL]; nasips = (NativeAudioSourceIdPair*) malloc(sizeof(NativeAudioSourceIdPair) * kHalfMaxConcurrentSources); //"nabip" is for that just a single number can maps to 2 number (L and R buffer) //The upper limit of buffers is a whopping 1024, this will take 4096 bytes = 0.0041MB //I tried the realloc way, but it strangely realloc something related to text display Unity is using and crash the game (bug?) //Might be related to that the memory area is in the heap (static) nabips = (NativeAudioBufferIdPair*) malloc(sizeof(NativeAudioBufferIdPair*) * kMaxBuffers); ALuint sourceIDL; ALuint sourceIDR; for (int i = 0; i < kHalfMaxConcurrentSources; i++) { alGenSources(1, &sourceIDL); alSourcei(sourceIDL, AL_SOURCE_RELATIVE, AL_TRUE); alSourcef(sourceIDL, AL_REFERENCE_DISTANCE, 1.0f); alSourcef(sourceIDL, AL_MAX_DISTANCE, 2.0f); alGenSources(1, &sourceIDR); alSourcei(sourceIDR, AL_SOURCE_RELATIVE, AL_TRUE); alSourcef(sourceIDR, AL_REFERENCE_DISTANCE, 1.0f); alSourcef(sourceIDR, AL_MAX_DISTANCE, 2.0f); NativeAudioSourceIdPair nasip; nasip.left = sourceIDL; nasip.right = sourceIDR; nasips[i] = nasip; //roll off factor is default to 1.0 } alDistanceModel(AL_LINEAR_DISTANCE_CLAMPED); #ifdef LOG_NATIVE_AUDIO NSLog(@"Initialized OpenAL"); #endif return 0; //0 = success } + (void) UnloadAudio: (int) index { ALuint bufferIdL = (ALuint)nabips[index].left; ALuint bufferIdR = (ALuint)nabips[index].right; alDeleteBuffers(1, &bufferIdL); alDeleteBuffers(1, &bufferIdR); bufferAllocationCount -= 2; } + (int) LoadAudio:(char*) soundUrl resamplingQuality:(int) resamplingQuality { if (bufferAllocationCount > kMaxBuffers) { NSLog(@"Fail to load because OpenAL reaches the maximum sound buffers limit. Raise the limit or use unloading to free up the quota."); return -1; } if(openALDevice == nil) { [NativeAudio Initialize]; } NSString *audioFilePath = [NSString stringWithFormat:@"%@/Data/Raw/%@", [[NSBundle mainBundle] resourcePath], [NSString stringWithUTF8String:soundUrl] ]; AudioFileID afid = [NativeAudio openAudioFile:audioFilePath]; AudioStreamBasicDescription loadingAudioDescription = [NativeAudio getDescription:afid]; #ifdef LOG_NATIVE_AUDIO NSLog(@"Input description : Flags %u Bits/channel %u FormatID %u SampleRate %f Bytes/Frame %u Bytes/Packet %u Channels/Frame %u Frames/Packet %u", (unsigned int)loadingAudioDescription.mFormatFlags, (unsigned int)loadingAudioDescription.mBitsPerChannel, (unsigned int)loadingAudioDescription.mFormatID, loadingAudioDescription.mSampleRate, (unsigned int)loadingAudioDescription.mBytesPerFrame, (unsigned int)loadingAudioDescription.mBytesPerPacket, (unsigned int)loadingAudioDescription.mChannelsPerFrame, (unsigned int)loadingAudioDescription.mFramesPerPacket ); #endif //UInt32 bytesPerFrame = loadingAudioDescription.mBytesPerFrame; UInt32 channel = loadingAudioDescription.mChannelsPerFrame; //This is originally float? //NSLog(@"LOADED RATE : %f", loadingAudioDescription.mSampleRate); //NSLog(@"CHANN : %u", (unsigned int)loadingAudioDescription.mChannelsPerFrame); //NSLog(@"BPF : %u", (unsigned int)loadingAudioDescription.mBytesPerFrame); UInt32 samplingRate = (UInt32) loadingAudioDescription.mSampleRate; //Next, load the original audio UInt32 audioSize = [NativeAudio getSizeOfAudioComponent:afid]; char *audioData = (char*)malloc(audioSize); OSStatus readBytesResult = AudioFileReadBytes(afid, false, 0, &audioSize, audioData); if (0 != readBytesResult) { NSLog(@"ERROR : AudioFileReadBytes %@: %d", audioFilePath, (int)readBytesResult); } AudioFileClose(afid); int loadedIndex = [NativeAudio SendByteArray:audioData audioSize:audioSize channels:channel samplingRate:samplingRate resamplingQuality:resamplingQuality]; if (audioData) { free(audioData); audioData = NULL; } return loadedIndex; } //Can call from Unity to give Unity-loaded AudioClip!! + (int) SendByteArray:(char*) audioData audioSize:(int)audioSize channels:(int)channel samplingRate:(int)samplingRate resamplingQuality:(int)resamplingQuality { //I don't know if an "optimal rate" exist on Apple device or not. //Enable either one to make the 24000 that Unity choose matters, and resample our audio to match //This is always 24000 for all Unity games as far as I tried. (why?) //int rate = (int)[[AVAudioSession sharedInstance]sampleRate]; //int rate = fixedMixingRate; //Enable this to not care, and just put our audio to OpenAL without resampling. int rate = samplingRate; if(samplingRate != rate) { float ratio = rate / ((float) samplingRate); //byte -> short size_t shortLength = audioSize / 2; size_t resampledArrayShortLength = (size_t)floor(shortLength * ratio); resampledArrayShortLength += resampledArrayShortLength % 2; NSLog(@"Resampling! Ratio %f / Length %zu -> %zu", ratio, shortLength, resampledArrayShortLength); float *floatArrayForSRCIn = (float*)calloc(shortLength, sizeof(float *)); float *floatArrayForSRCOut = (float*)calloc(resampledArrayShortLength, sizeof(float *)); //SRC takes float data. src_short_to_float_array((short*)audioData, floatArrayForSRCIn, (int)shortLength); SRC_DATA dataForSRC; dataForSRC.data_in = floatArrayForSRCIn; dataForSRC.data_out = floatArrayForSRCOut; dataForSRC.input_frames = shortLength / channel; dataForSRC.output_frames = resampledArrayShortLength / channel; dataForSRC.src_ratio = ratio; //This is in/out and it is less than 1.0 in the case of upsampling. //Use the SRC library. Thank you Eric! int error = src_simple(&dataForSRC, resamplingQuality, channel); if(error != 0) { [NSException raise:@"Native Audio Error" format:@"Resampling error with code %s", src_strerror(error)]; } short* shortData = (short*)calloc(resampledArrayShortLength, sizeof(short *)); src_float_to_short_array(floatArrayForSRCOut, shortData, (int)resampledArrayShortLength); shortLength = resampledArrayShortLength; //Replace the input argument with a new calloc. //We don't release the input argument, but in the case of resample we need to release it too. audioData = (char*)shortData; audioSize = (int)(resampledArrayShortLength * 2); free(floatArrayForSRCIn); free(floatArrayForSRCOut); } //I have a failed attempt to use AudioConverterFillComplexBuffer, a method where an entire internet does not have a single understandable working example. //If you want to do the "elegant" conversion, this is a very important read. (terminology, etc.) //https://developer.apple.com/documentation/coreaudio/audiostreambasicdescription //The deinterleaving conversion below is super noob and ugly... but it works. UInt32 bytesPerFrame = 2 * channel; // We fixed to 16-bit audio so that's that. UInt32 step = bytesPerFrame / channel; char *audioDataL = (char*)malloc(audioSize/channel); char *audioDataR = (char*)malloc(audioSize/channel); //NSLog(@"LR Length %d AudioSize %d Channel %d" , audioSize/channel, audioSize, channel ); //This routine ensure no matter what the case `audioData` is completely migrated to the new L R separated buffer. if(channel == 2) { BOOL rightInterleave = false; // 0 1 2 3 4 5 6 7 8 9 101112131415 // 0 1 0 1 2 3 2 3 4 5 4 5 6 7 6 7 // L L R R L L R R L L R R L L R R for(int i = 0; i < audioSize; i += step) { int baseIndex = (i/bytesPerFrame) * step; //the divide will get rid of fractions first //NSLog(@"%d %d %u %d",i,baseIndex, (unsigned int)step, rightInterleave); for(int j = 0; j < step ; j++) { if(!rightInterleave) { audioDataL[baseIndex + j] = audioData[i + j]; } else { audioDataR[baseIndex + j] = audioData[i + j]; } } rightInterleave = !rightInterleave; } } else if(channel == 1) { for(int i = 0; i < audioSize; i++) { audioDataL[i] = audioData[i]; audioDataR[i] = audioData[i]; } } else { //throw? [NSException raise:@"Native Audio Error" format:@"Your audio is not either 1 or 2 channels!"]; } ALuint bufferIdL; alGenBuffers(1, &bufferIdL); bufferAllocationCount++; alBufferData(bufferIdL, AL_FORMAT_MONO16, audioDataL, audioSize/channel, rate); ALuint bufferIdR; alGenBuffers(1, &bufferIdR); bufferAllocationCount++; alBufferData(bufferIdR, AL_FORMAT_MONO16, audioDataR, audioSize/channel, rate); //alBufferData should be copying the audio to memory, so we can safely release them now. if (audioDataL) { free(audioDataL); audioDataL = NULL; } if (audioDataR) { free(audioDataR); audioDataR = NULL; } if(samplingRate != rate) { //This is now the new calloc-ed memory from the resampler. We can remove it. free(audioData); //Otherwise we cannot free the incoming data, since it is the same as it might be from C#. //We let C# GC handle it. } runningBufferAllocationNumber++; NativeAudioBufferIdPair nabip; nabip.left = bufferIdL; nabip.right = bufferIdR; //Calculate and cache other data nabip.channels = channel; nabip.bitDepth = 16; //This byte size is already stereo nabip.lengthSeconds = audioSize / (float)nabip.channels / (float)(nabip.bitDepth / 8) / (float)rate; nabips[runningBufferAllocationNumber - 1] = nabip; #ifdef LOG_NATIVE_AUDIO NSLog(@"Loaded OpenAL sound: %@ bufferId: L %d R %d size: %u",[NSString stringWithUTF8String:soundUrl], bufferIdL, bufferIdR, (unsigned int)audioSize); #endif return runningBufferAllocationNumber - 1; } static ALuint sourceCycleIndex = 0; //Sources are selected sequentially. //Searching for non-playing source might be a better idea to reduce sound cutoff chance //(For example, by the time we reach 33rd sound some sound earlier must have finished playing, and we can select that one safely) //But for performance concern I don't want to run a for...in loop everytime I play sounds. //The reason of "half" of total available sources is this is only for the left channel. The right channel will be the left's index *2 + (int) CycleThroughSources { sourceCycleIndex = (sourceCycleIndex + 1) % kHalfMaxConcurrentSources; return sourceCycleIndex; } + (float)LengthByAudioBuffer:(int)index { return nabips[index].lengthSeconds; } + (void)StopAudio:(int) nativeSourceIndex { alSourceStop(nasips[nativeSourceIndex].left); alSourceStop(nasips[nativeSourceIndex].right); } // Confirm an incoming index. If invalid, fallback to round-robin. // At C# side some methods sending in -1 to be intentionally invalid here so you get round-robin. + (int)GetNativeSource:(int) index { //-1 or invalid source cycle will get a round robin play. if(index >= kHalfMaxConcurrentSources || index < 0) { index = [NativeAudio CycleThroughSources]; } return index; } //Not only called from C# manual prepare, also internally from normal play as well. + (void)PrepareAudio:(int) audioBufferIndex IntoNativeSourceIndex:(int) nativeSourceIndex { NativeAudioSourceIdPair nasip = nasips[nativeSourceIndex]; NativeAudioBufferIdPair nabip = nabips[audioBufferIndex]; //We cannot change audio source if it is playing, it will fail silently. alSourceStop(nasip.left); alSourceStop(nasip.right); alSourcei(nasip.left, AL_BUFFER, nabip.left); alSourcei(nasip.right, AL_BUFFER, nabip.right); #ifdef LOG_NATIVE_AUDIO NSLog(@"Pairing OpenAL buffer: L %d R %d with source : L %d R %d", nabips[audioBufferIndex].left, nabips[audioBufferIndex].right, nasips[nativeSourceIndex].left, nasips[nativeSourceIndex].right); #endif } //This is playing "blindly", believing that the audio assigned is correct. Must be used immediately after prepare for reliable result. //It is separated to allow extreme micro optimization where you prepare in anticipation, then play later believing that //it is still valid. + (void)PlayAudioWithNativeSourceIndex:(int) nativeSourceIndex Adjustment:(NativeAudioPlayAdjustment) playAdjustment { NativeAudioSourceIdPair nasip = nasips[nativeSourceIndex]; //If we call play before the adjust, you might hear the pre-adjusted audio. //It is THAT fast, even in between lines of code you can hear the audio already. [NativeAudio SetVolume:playAdjustment.volume OnNativeSourceIndex:nativeSourceIndex]; [NativeAudio SetPan:playAdjustment.pan OnNativeSourceIndex:nativeSourceIndex]; alSourcef(nasips[nativeSourceIndex].left, AL_SEC_OFFSET, playAdjustment.offsetSeconds); alSourcef(nasips[nativeSourceIndex].right, AL_SEC_OFFSET, playAdjustment.offsetSeconds); alSourcei(nasips[nativeSourceIndex].left, AL_LOOPING, playAdjustment.trackLoop ? AL_TRUE : AL_FALSE); alSourcei(nasips[nativeSourceIndex].right, AL_LOOPING, playAdjustment.trackLoop ? AL_TRUE : AL_FALSE); ALint state; //alSourcePlay on a paused source results in RESUME, we need to stop it to start over. alGetSourcei(nasips[nativeSourceIndex].left, AL_SOURCE_STATE, &state); if(state == AL_PAUSED) { alSourceStop(nasip.left); alSourceStop(nasip.right); } alSourcePlay(nasip.left); alSourcePlay(nasip.right); #ifdef LOG_NATIVE_AUDIO NSLog(@"Played OpenAL at source index : L %d R %d", nasip.left, nasip.right); #endif } + (void)SetVolume:(float) volume OnNativeSourceIndex:(int) nativeSourceIndex { #ifdef LOG_NATIVE_AUDIO NSLog(@"Set Volume %f L %d R %d", volume, nasips[nativeSourceIndex].left, nasips[nativeSourceIndex].right); #endif alSourcef(nasips[nativeSourceIndex].left, AL_GAIN, volume); alSourcef(nasips[nativeSourceIndex].right, AL_GAIN, volume); } //With OpenAL's 3D design, to achieve 2D panning we have deinterleaved the stereo file //into 2 separated mono sources positioned left and right of the listener. This achieve the same stereo effect. //Gain is already used in SetVolume, we will use a linear attenuation for panning. + (void)SetPan:(float) pan OnNativeSourceIndex:(int) nativeSourceIndex { #ifdef LOG_NATIVE_AUDIO NSLog(@"Set Pan %f L %d R %d", pan, nasips[nativeSourceIndex].left, nasips[nativeSourceIndex].right); #endif //Left channel attenuate linearly on right pan alSource3f(nasips[nativeSourceIndex].left, AL_POSITION, -1 - (MAX(pan, 0)), 0, 0); //Right channel attenuate linearly on left pan alSource3f(nasips[nativeSourceIndex].right, AL_POSITION, 1 - (MIN(pan, 0)), 0, 0); } //Only one side is enough? + (float)GetPlaybackTimeOfNativeSourceIndex:(int) nativeSourceIndex { ALfloat returnValue; alGetSourcef(nasips[nativeSourceIndex].left, AL_SEC_OFFSET, &returnValue); #ifdef LOG_NATIVE_AUDIO NSLog(@"Get Playback Time %f", returnValue); #endif return returnValue; } +(void)SetPlaybackTimeOfNativeSourceIndex:(int) nativeSourceIndex Offset:(float)offsetSeconds { alSourcef(nasips[nativeSourceIndex].left, AL_SEC_OFFSET, offsetSeconds); alSourcef(nasips[nativeSourceIndex].right, AL_SEC_OFFSET, offsetSeconds); ALint state; alGetSourcei(nasips[nativeSourceIndex].left, AL_SOURCE_STATE, &state); if(state == AL_STOPPED) { alSourcePlay(nasips[nativeSourceIndex].left); alSourcePlay(nasips[nativeSourceIndex].right); } #ifdef LOG_NATIVE_AUDIO NSLog(@"Set Playback Time %f", offsetSeconds); #endif } +(void)Pause:(int)nativeSourceIndex { ALint state; alGetSourcei(nasips[nativeSourceIndex].left, AL_SOURCE_STATE, &state); if(state == AL_PLAYING) { alSourcePause(nasips[nativeSourceIndex].left); alSourcePause(nasips[nativeSourceIndex].right); } } +(void) Resume:(int)nativeSourceIndex { ALint state; alGetSourcei(nasips[nativeSourceIndex].left, AL_SOURCE_STATE, &state); if(state == AL_PAUSED) { alSourcePlay(nasips[nativeSourceIndex].left); alSourcePlay(nasips[nativeSourceIndex].right); } } //This is matched with what's defined at C#. typedef enum IosAudioPortType { //---Output--- /// /// Line-level output to the dock connector. /// LineOut = 0, /// /// Output to a wired headset. /// Headphones = 1, /// /// Output to a speaker intended to be held near the ear. /// BuiltInReceiver = 2, /// /// Output to the device's built-in speaker. /// BuiltInSpeaker = 3, /// /// Output to a device via the High-Definition Multimedia Interface (HDMI) specification. /// HDMI = 4, /// /// Output to a remote device over AirPlay. /// AirPlay = 5, /// /// Output to a Bluetooth Low Energy (LE) peripheral. /// BluetoothLE = 6, /// /// Output to a Bluetooth A2DP device. /// BluetoothA2DP = 7, //---Input--- /// /// Line-level input from the dock connector. /// LineIn = 8, /// /// The built-in microphone on a device. /// BuiltInMic = 9, /// /// A microphone that is built-in to a wired headset. /// HeadsetMic = 10, //---Input-Output--- /// /// Input or output on a Bluetooth Hands-Free Profile device. /// BluetoothHFP = 11, /// /// Input or output on a Universal Serial Bus device. /// UsbAudio = 12, /// /// Input or output via Car Audio. /// CarAudio = 13, } IosAudioPortType; //Interop array with C#, by having C# allocate large enough empty array and this one write to it.. +(void) GetDeviceAudioInformation: (double*)interopArray OutputDeviceEnumArray:(int*) outputDeviceEnumArray { //Various shared audio session properties.. AVAudioSession* sharedInstance = [AVAudioSession sharedInstance]; interopArray[0] = [sharedInstance outputLatency]; interopArray[1] = [sharedInstance sampleRate]; interopArray[2] = [sharedInstance preferredSampleRate]; interopArray[3] = [sharedInstance IOBufferDuration]; interopArray[4] = [sharedInstance preferredIOBufferDuration]; //Output devices AVAudioSessionRouteDescription *routeDescription = [sharedInstance currentRoute]; NSArray *outPorts = routeDescription.outputs; int i = 0; for (AVAudioSessionPortDescription *port in outPorts) { IosAudioPortType iapt; if([port.portType isEqualToString:AVAudioSessionPortLineOut]) iapt = LineOut; else if([port.portType isEqualToString:AVAudioSessionPortHeadphones]) iapt = Headphones; else if([port.portType isEqualToString:AVAudioSessionPortBuiltInReceiver]) iapt = BuiltInReceiver; else if([port.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) iapt = BuiltInSpeaker; else if([port.portType isEqualToString:AVAudioSessionPortHDMI]) iapt = HDMI; else if([port.portType isEqualToString:AVAudioSessionPortAirPlay]) iapt = AirPlay; else if([port.portType isEqualToString:AVAudioSessionPortBluetoothLE]) iapt = BluetoothLE; else if([port.portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) iapt = BluetoothA2DP; else if([port.portType isEqualToString:AVAudioSessionPortLineIn]) iapt = LineIn; else if([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) iapt = BuiltInMic; else if([port.portType isEqualToString:AVAudioSessionPortHeadsetMic]) iapt = HeadsetMic; else if([port.portType isEqualToString:AVAudioSessionPortBluetoothHFP]) iapt = BluetoothHFP; else if([port.portType isEqualToString:AVAudioSessionPortUSBAudio]) iapt = UsbAudio; else if([port.portType isEqualToString:AVAudioSessionPortCarAudio]) iapt = CarAudio; outputDeviceEnumArray[i] = iapt; i++; } } @end extern "C" { int _Initialize() { return [NativeAudio Initialize]; } void _GetDeviceAudioInformation(double* interopArray, int* outputDeviceEnumArray) { return [NativeAudio GetDeviceAudioInformation:interopArray OutputDeviceEnumArray: outputDeviceEnumArray]; } int _SendByteArray(char* byteArrayInput, int byteSize, int channels, int samplingRate, int resamplingQuality) { return [NativeAudio SendByteArray:byteArrayInput audioSize:byteSize channels:channels samplingRate:samplingRate resamplingQuality: resamplingQuality]; } int _LoadAudio(char* soundUrl, int resamplingQuality) { return [NativeAudio LoadAudio:soundUrl resamplingQuality: resamplingQuality]; } void _PrepareAudio(int bufferIndex, int nativeSourceIndex) { [NativeAudio PrepareAudio:bufferIndex IntoNativeSourceIndex:nativeSourceIndex]; } void _PlayAudioWithNativeSourceIndex(int nativeSourceIndex, NativeAudioPlayAdjustment playAdjustment) { [NativeAudio PlayAudioWithNativeSourceIndex:nativeSourceIndex Adjustment: playAdjustment]; } float _LengthByAudioBuffer(int bufferIndex) { return [NativeAudio LengthByAudioBuffer: bufferIndex]; } void _StopAudio(int nativeSourceIndex) { [NativeAudio StopAudio:nativeSourceIndex]; } void _SetVolume(int nativeSourceIndex, float volume){ [NativeAudio SetVolume:volume OnNativeSourceIndex:nativeSourceIndex]; } void _SetPan(int nativeSourceIndex, float pan){ [NativeAudio SetPan:pan OnNativeSourceIndex:nativeSourceIndex]; } float _GetPlaybackTime(int nativeSourceIndex){ return [NativeAudio GetPlaybackTimeOfNativeSourceIndex: nativeSourceIndex]; } void _SetPlaybackTime(int nativeSourceIndex, float offsetSeconds){ [NativeAudio SetPlaybackTimeOfNativeSourceIndex:nativeSourceIndex Offset: offsetSeconds]; } void _Pause(int nativeSourceIndex) { [NativeAudio Pause: nativeSourceIndex]; } void _Resume(int nativeSourceIndex) { [NativeAudio Resume: nativeSourceIndex]; } void _UnloadAudio(int index) { [NativeAudio UnloadAudio:index]; } int _GetNativeSource(int index) { [NativeAudio GetNativeSource:index]; } }