/* ============================================================================== This file is part of the JUCE library - "Jules' Utility Class Extensions" Copyright 2004-11 by Raw Material Software Ltd. ------------------------------------------------------------------------------ JUCE can be redistributed and/or modified under the terms of the GNU General Public License (Version 2), as published by the Free Software Foundation. A copy of the license is included in the JUCE distribution, or can be found online at www.gnu.org/licenses. JUCE 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 General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.rawmaterialsoftware.com/juce for more information. ============================================================================== */ class IPhoneAudioIODevice : public AudioIODevice { public: IPhoneAudioIODevice (const String& deviceName) : AudioIODevice (deviceName, "Audio"), actualBufferSize (0), isRunning (false), audioUnit (0), callback (nullptr), floatData (1, 2) { getSessionHolder().activeDevices.add (this); numInputChannels = 2; numOutputChannels = 2; preferredBufferSize = 0; updateDeviceInfo(); } ~IPhoneAudioIODevice() { getSessionHolder().activeDevices.removeFirstMatchingValue (this); close(); } StringArray getOutputChannelNames() { StringArray s; s.add ("Left"); s.add ("Right"); return s; } StringArray getInputChannelNames() { StringArray s; if (audioInputIsAvailable) { s.add ("Left"); s.add ("Right"); } return s; } int getNumSampleRates() { return 1; } double getSampleRate (int index) { return sampleRate; } int getNumBufferSizesAvailable() { return 6; } int getBufferSizeSamples (int index) { return 1 << (jlimit (0, 5, index) + 6); } int getDefaultBufferSize() { return 1024; } String open (const BigInteger& inputChannels, const BigInteger& outputChannels, double sampleRate, int bufferSize) { close(); lastError = String::empty; preferredBufferSize = (bufferSize <= 0) ? getDefaultBufferSize() : bufferSize; // xxx set up channel mapping activeOutputChans = outputChannels; activeOutputChans.setRange (2, activeOutputChans.getHighestBit(), false); numOutputChannels = activeOutputChans.countNumberOfSetBits(); monoOutputChannelNumber = activeOutputChans.findNextSetBit (0); activeInputChans = inputChannels; activeInputChans.setRange (2, activeInputChans.getHighestBit(), false); numInputChannels = activeInputChans.countNumberOfSetBits(); monoInputChannelNumber = activeInputChans.findNextSetBit (0); AudioSessionSetActive (true); UInt32 audioCategory = kAudioSessionCategory_MediaPlayback; if (numInputChannels > 0 && audioInputIsAvailable) { audioCategory = kAudioSessionCategory_PlayAndRecord; UInt32 allowBluetoothInput = 1; AudioSessionSetProperty (kAudioSessionProperty_OverrideCategoryEnableBluetoothInput, sizeof (allowBluetoothInput), &allowBluetoothInput); } AudioSessionSetProperty (kAudioSessionProperty_AudioCategory, sizeof (audioCategory), &audioCategory); AudioSessionAddPropertyListener (kAudioSessionProperty_AudioRouteChange, routingChangedStatic, this); fixAudioRouteIfSetToReceiver(); updateDeviceInfo(); Float32 bufferDuration = preferredBufferSize / sampleRate; AudioSessionSetProperty (kAudioSessionProperty_PreferredHardwareIOBufferDuration, sizeof (bufferDuration), &bufferDuration); actualBufferSize = preferredBufferSize; prepareFloatBuffers(); isRunning = true; routingChanged (nullptr); // creates and starts the AU lastError = audioUnit != 0 ? "" : "Couldn't open the device"; return lastError; } void close() { if (isRunning) { isRunning = false; AudioSessionRemovePropertyListenerWithUserData (kAudioSessionProperty_AudioRouteChange, routingChangedStatic, this); AudioSessionSetActive (false); if (audioUnit != 0) { AudioComponentInstanceDispose (audioUnit); audioUnit = 0; } } } bool isOpen() { return isRunning; } int getCurrentBufferSizeSamples() { return actualBufferSize; } double getCurrentSampleRate() { return sampleRate; } int getCurrentBitDepth() { return 16; } BigInteger getActiveOutputChannels() const { return activeOutputChans; } BigInteger getActiveInputChannels() const { return activeInputChans; } int getOutputLatencyInSamples() { return 0; } //xxx int getInputLatencyInSamples() { return 0; } //xxx void start (AudioIODeviceCallback* callback_) { if (isRunning && callback != callback_) { if (callback_ != nullptr) callback_->audioDeviceAboutToStart (this); const ScopedLock sl (callbackLock); callback = callback_; } } void stop() { if (isRunning) { AudioIODeviceCallback* lastCallback; { const ScopedLock sl (callbackLock); lastCallback = callback; callback = nullptr; } if (lastCallback != nullptr) lastCallback->audioDeviceStopped(); } } bool isPlaying() { return isRunning && callback != nullptr; } String getLastError() { return lastError; } private: //================================================================================================== CriticalSection callbackLock; Float64 sampleRate; int numInputChannels, numOutputChannels; int preferredBufferSize, actualBufferSize; bool isRunning; String lastError; AudioStreamBasicDescription format; AudioUnit audioUnit; UInt32 audioInputIsAvailable; AudioIODeviceCallback* callback; BigInteger activeOutputChans, activeInputChans; AudioSampleBuffer floatData; float* inputChannels[3]; float* outputChannels[3]; bool monoInputChannelNumber, monoOutputChannelNumber; void prepareFloatBuffers() { floatData.setSize (numInputChannels + numOutputChannels, actualBufferSize); zeromem (inputChannels, sizeof (inputChannels)); zeromem (outputChannels, sizeof (outputChannels)); for (int i = 0; i < numInputChannels; ++i) inputChannels[i] = floatData.getSampleData (i); for (int i = 0; i < numOutputChannels; ++i) outputChannels[i] = floatData.getSampleData (i + numInputChannels); } //================================================================================================== OSStatus process (AudioUnitRenderActionFlags* flags, const AudioTimeStamp* time, const UInt32 numFrames, AudioBufferList* data) { OSStatus err = noErr; if (audioInputIsAvailable && numInputChannels > 0) err = AudioUnitRender (audioUnit, flags, time, 1, numFrames, data); const ScopedLock sl (callbackLock); if (callback != nullptr) { if (audioInputIsAvailable && numInputChannels > 0) { short* shortData = (short*) data->mBuffers[0].mData; if (numInputChannels >= 2) { for (UInt32 i = 0; i < numFrames; ++i) { inputChannels[0][i] = *shortData++ * (1.0f / 32768.0f); inputChannels[1][i] = *shortData++ * (1.0f / 32768.0f); } } else { if (monoInputChannelNumber > 0) ++shortData; for (UInt32 i = 0; i < numFrames; ++i) { inputChannels[0][i] = *shortData++ * (1.0f / 32768.0f); ++shortData; } } } else { for (int i = numInputChannels; --i >= 0;) zeromem (inputChannels[i], sizeof (float) * numFrames); } callback->audioDeviceIOCallback ((const float**) inputChannels, numInputChannels, outputChannels, numOutputChannels, (int) numFrames); short* shortData = (short*) data->mBuffers[0].mData; int n = 0; if (numOutputChannels >= 2) { for (UInt32 i = 0; i < numFrames; ++i) { shortData [n++] = (short) (outputChannels[0][i] * 32767.0f); shortData [n++] = (short) (outputChannels[1][i] * 32767.0f); } } else if (numOutputChannels == 1) { for (UInt32 i = 0; i < numFrames; ++i) { const short s = (short) (outputChannels[monoOutputChannelNumber][i] * 32767.0f); shortData [n++] = s; shortData [n++] = s; } } else { zeromem (data->mBuffers[0].mData, 2 * sizeof (short) * numFrames); } } else { zeromem (data->mBuffers[0].mData, 2 * sizeof (short) * numFrames); } return err; } void updateDeviceInfo() { UInt32 size = sizeof (sampleRate); AudioSessionGetProperty (kAudioSessionProperty_CurrentHardwareSampleRate, &size, &sampleRate); size = sizeof (audioInputIsAvailable); AudioSessionGetProperty (kAudioSessionProperty_AudioInputAvailable, &size, &audioInputIsAvailable); } void routingChanged (const void* propertyValue) { if (! isRunning) return; if (propertyValue != nullptr) { CFDictionaryRef routeChangeDictionary = (CFDictionaryRef) propertyValue; CFNumberRef routeChangeReasonRef = (CFNumberRef) CFDictionaryGetValue (routeChangeDictionary, CFSTR (kAudioSession_AudioRouteChangeKey_Reason)); SInt32 routeChangeReason; CFNumberGetValue (routeChangeReasonRef, kCFNumberSInt32Type, &routeChangeReason); if (routeChangeReason == kAudioSessionRouteChangeReason_OldDeviceUnavailable) fixAudioRouteIfSetToReceiver(); } updateDeviceInfo(); createAudioUnit(); AudioSessionSetActive (true); if (audioUnit != 0) { UInt32 formatSize = sizeof (format); AudioUnitGetProperty (audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &format, &formatSize); Float32 bufferDuration = preferredBufferSize / sampleRate; UInt32 bufferDurationSize = sizeof (bufferDuration); AudioSessionGetProperty (kAudioSessionProperty_CurrentHardwareIOBufferDuration, &bufferDurationSize, &bufferDurationSize); actualBufferSize = (int) (sampleRate * bufferDuration + 0.5); AudioOutputUnitStart (audioUnit); } } //================================================================================================== struct AudioSessionHolder { AudioSessionHolder() { AudioSessionInitialize (0, 0, interruptionListenerCallback, this); } static void interruptionListenerCallback (void* client, UInt32 interruptionType) { const Array & activeDevices = static_cast (client)->activeDevices; for (int i = activeDevices.size(); --i >= 0;) activeDevices.getUnchecked(i)->interruptionListener (interruptionType); } Array activeDevices; }; static AudioSessionHolder& getSessionHolder() { static AudioSessionHolder audioSessionHolder; return audioSessionHolder; } void interruptionListener (const UInt32 interruptionType) { /*if (interruptionType == kAudioSessionBeginInterruption) { isRunning = false; AudioOutputUnitStop (audioUnit); if (juce_iPhoneShowModalAlert ("Audio Interrupted", "This could have been interrupted by another application or by unplugging a headset", @"Resume", @"Cancel")) { isRunning = true; routingChanged (nullptr); } }*/ if (interruptionType == kAudioSessionEndInterruption) { isRunning = true; AudioSessionSetActive (true); AudioOutputUnitStart (audioUnit); } } //================================================================================================== static OSStatus processStatic (void* client, AudioUnitRenderActionFlags* flags, const AudioTimeStamp* time, UInt32 /*busNumber*/, UInt32 numFrames, AudioBufferList* data) { return static_cast (client)->process (flags, time, numFrames, data); } static void routingChangedStatic (void* client, AudioSessionPropertyID, UInt32 /*inDataSize*/, const void* propertyValue) { static_cast (client)->routingChanged (propertyValue); } //================================================================================================== void resetFormat (const int numChannels) noexcept { zerostruct (format); format.mFormatID = kAudioFormatLinearPCM; format.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked | kAudioFormatFlagsNativeEndian; format.mBitsPerChannel = 8 * sizeof (short); format.mChannelsPerFrame = numChannels; format.mFramesPerPacket = 1; format.mBytesPerFrame = format.mBytesPerPacket = numChannels * sizeof (short); } bool createAudioUnit() { if (audioUnit != 0) { AudioComponentInstanceDispose (audioUnit); audioUnit = 0; } resetFormat (2); AudioComponentDescription desc; desc.componentType = kAudioUnitType_Output; desc.componentSubType = kAudioUnitSubType_RemoteIO; desc.componentManufacturer = kAudioUnitManufacturer_Apple; desc.componentFlags = 0; desc.componentFlagsMask = 0; AudioComponent comp = AudioComponentFindNext (0, &desc); AudioComponentInstanceNew (comp, &audioUnit); if (audioUnit == 0) return false; if (numInputChannels > 0) { const UInt32 one = 1; AudioUnitSetProperty (audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, sizeof (one)); } { AudioChannelLayout layout; layout.mChannelBitmap = 0; layout.mNumberChannelDescriptions = 0; layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo; AudioUnitSetProperty (audioUnit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Input, 0, &layout, sizeof (layout)); AudioUnitSetProperty (audioUnit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Output, 0, &layout, sizeof (layout)); } { AURenderCallbackStruct inputProc; inputProc.inputProc = processStatic; inputProc.inputProcRefCon = this; AudioUnitSetProperty (audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &inputProc, sizeof (inputProc)); } AudioUnitSetProperty (audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, sizeof (format)); AudioUnitSetProperty (audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &format, sizeof (format)); AudioUnitInitialize (audioUnit); return true; } // If the routing is set to go through the receiver (i.e. the speaker, but quiet), this re-routes it // to make it loud. Needed because by default when using an input + output, the output is kept quiet. static void fixAudioRouteIfSetToReceiver() { CFStringRef audioRoute = 0; UInt32 propertySize = sizeof (audioRoute); if (AudioSessionGetProperty (kAudioSessionProperty_AudioRoute, &propertySize, &audioRoute) == noErr) { NSString* route = (NSString*) audioRoute; //DBG ("audio route: " + nsStringToJuce (route)); if ([route hasPrefix: @"Receiver"]) { UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker; AudioSessionSetProperty (kAudioSessionProperty_OverrideAudioRoute, sizeof (audioRouteOverride), &audioRouteOverride); } CFRelease (audioRoute); } } JUCE_DECLARE_NON_COPYABLE (IPhoneAudioIODevice); }; //============================================================================== class IPhoneAudioIODeviceType : public AudioIODeviceType { public: //============================================================================== IPhoneAudioIODeviceType() : AudioIODeviceType ("iPhone Audio") { } void scanForDevices() {} StringArray getDeviceNames (bool wantInputNames) const { return StringArray ("iPhone Audio"); } int getDefaultDeviceIndex (bool forInput) const { return 0; } int getIndexOfDevice (AudioIODevice* device, bool asInput) const { return device != nullptr ? 0 : -1; } bool hasSeparateInputsAndOutputs() const { return false; } AudioIODevice* createDevice (const String& outputDeviceName, const String& inputDeviceName) { if (outputDeviceName.isNotEmpty() || inputDeviceName.isNotEmpty()) return new IPhoneAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName : inputDeviceName); return nullptr; } private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (IPhoneAudioIODeviceType); }; //============================================================================== AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_iOSAudio() { return new IPhoneAudioIODeviceType(); }