/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found 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.juce.com for more information. ============================================================================== */ #undef check const char* const openSLTypeName = "Android OpenSL"; bool isOpenSLAvailable() { DynamicLibrary library; return library.open ("libOpenSLES.so"); } //============================================================================== class OpenSLAudioIODevice : public AudioIODevice, private Thread { public: OpenSLAudioIODevice (const String& deviceName) : AudioIODevice (deviceName, openSLTypeName), Thread ("OpenSL"), callback (nullptr), sampleRate (0), deviceOpen (false), inputBuffer (2, 2), outputBuffer (2, 2) { // OpenSL has piss-poor support for determining latency, so the only way I can find to // get a number for this is by asking the AudioTrack/AudioRecord classes.. AndroidAudioIODevice javaDevice (deviceName); // this is a total guess about how to calculate the latency, but seems to vaguely agree // with the devices I've tested.. YMMV inputLatency = (javaDevice.minBufferSizeIn * 2) / 3; outputLatency = (javaDevice.minBufferSizeOut * 2) / 3; const int64 longestLatency = jmax (inputLatency, outputLatency); const int64 totalLatency = inputLatency + outputLatency; inputLatency = (int) ((longestLatency * inputLatency) / totalLatency) & ~15; outputLatency = (int) ((longestLatency * outputLatency) / totalLatency) & ~15; } ~OpenSLAudioIODevice() { close(); } bool openedOk() const { return engine.outputMixObject != nullptr; } StringArray getOutputChannelNames() override { StringArray s; s.add ("Left"); s.add ("Right"); return s; } StringArray getInputChannelNames() override { StringArray s; s.add ("Audio Input"); return s; } Array getAvailableSampleRates() override { static const double rates[] = { 8000.0, 16000.0, 32000.0, 44100.0, 48000.0 }; Array retval (rates, numElementsInArray (rates)); // make sure the native sample rate is pafrt of the list double native = getNativeSampleRate(); if (native != 0.0 && ! retval.contains (native)) retval.add (native); return retval; } Array getAvailableBufferSizes() override { // we need to offer the lowest possible buffer size which // is the native buffer size const int defaultNumMultiples = 8; const int nativeBufferSize = getNativeBufferSize(); Array retval; for (int i = 1; i < defaultNumMultiples; ++i) retval.add (i * nativeBufferSize); return retval; } String open (const BigInteger& inputChannels, const BigInteger& outputChannels, double requestedSampleRate, int bufferSize) override { close(); lastError.clear(); sampleRate = (int) requestedSampleRate; int preferredBufferSize = (bufferSize <= 0) ? getDefaultBufferSize() : bufferSize; activeOutputChans = outputChannels; activeOutputChans.setRange (2, activeOutputChans.getHighestBit(), false); numOutputChannels = activeOutputChans.countNumberOfSetBits(); activeInputChans = inputChannels; activeInputChans.setRange (1, activeInputChans.getHighestBit(), false); numInputChannels = activeInputChans.countNumberOfSetBits(); actualBufferSize = preferredBufferSize; inputBuffer.setSize (jmax (1, numInputChannels), actualBufferSize); outputBuffer.setSize (jmax (1, numOutputChannels), actualBufferSize); outputBuffer.clear(); const int audioBuffersToEnqueue = hasLowLatencyAudioPath() ? buffersToEnqueueForLowLatency : buffersToEnqueueSlowAudio; DBG ("OpenSL: numInputChannels = " << numInputChannels << ", numOutputChannels = " << numOutputChannels << ", nativeBufferSize = " << getNativeBufferSize() << ", nativeSampleRate = " << getNativeSampleRate() << ", actualBufferSize = " << actualBufferSize << ", audioBuffersToEnqueue = " << audioBuffersToEnqueue << ", sampleRate = " << sampleRate); if (numInputChannels > 0) { if (! RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) { // If you hit this assert, you probably forgot to get RuntimePermissions::recordAudio // before trying to open an audio input device. This is not going to work! jassertfalse; lastError = "Error opening OpenSL input device: the app was not granted android.permission.RECORD_AUDIO"; } else { recorder = engine.createRecorder (numInputChannels, sampleRate, audioBuffersToEnqueue, actualBufferSize); if (recorder == nullptr) lastError = "Error opening OpenSL input device: creating Recorder failed."; } } if (numOutputChannels > 0) { player = engine.createPlayer (numOutputChannels, sampleRate, audioBuffersToEnqueue, actualBufferSize); if (player == nullptr) lastError = "Error opening OpenSL input device: creating Player failed."; } // pre-fill buffers for (int i = 0; i < audioBuffersToEnqueue; ++i) processBuffers(); startThread (8); deviceOpen = true; return lastError; } void close() override { stop(); stopThread (6000); deviceOpen = false; recorder = nullptr; player = nullptr; } int getOutputLatencyInSamples() override { return outputLatency; } int getInputLatencyInSamples() override { return inputLatency; } bool isOpen() override { return deviceOpen; } int getCurrentBufferSizeSamples() override { return actualBufferSize; } int getCurrentBitDepth() override { return 16; } BigInteger getActiveOutputChannels() const override { return activeOutputChans; } BigInteger getActiveInputChannels() const override { return activeInputChans; } String getLastError() override { return lastError; } bool isPlaying() override { return callback != nullptr; } int getDefaultBufferSize() override { // Only on a Pro-Audio device will we set the lowest possible buffer size // by default. We need to be more conservative on other devices // as they may be low-latency, but still have a crappy CPU. return (isProAudioDevice() ? 1 : 6) * defaultBufferSizeIsMultipleOfNative * getNativeBufferSize(); } double getCurrentSampleRate() override { return (sampleRate == 0.0 ? getNativeSampleRate() : sampleRate); } void start (AudioIODeviceCallback* newCallback) override { stop(); if (deviceOpen && callback != newCallback) { if (newCallback != nullptr) newCallback->audioDeviceAboutToStart (this); setCallback (newCallback); } } void stop() override { if (AudioIODeviceCallback* const oldCallback = setCallback (nullptr)) oldCallback->audioDeviceStopped(); } bool setAudioPreprocessingEnabled (bool enable) override { return recorder != nullptr && recorder->setAudioPreprocessingEnabled (enable); } private: //============================================================================== CriticalSection callbackLock; AudioIODeviceCallback* callback; int actualBufferSize, sampleRate; int inputLatency, outputLatency; bool deviceOpen; String lastError; BigInteger activeOutputChans, activeInputChans; int numInputChannels, numOutputChannels; AudioSampleBuffer inputBuffer, outputBuffer; struct Player; struct Recorder; enum { // The number of buffers to enqueue needs to be at least two for the audio to use the low-latency // audio path (see "Performance" section in ndk/docs/Additional_library_docs/opensles/index.html) buffersToEnqueueForLowLatency = 2, buffersToEnqueueSlowAudio = 4, defaultBufferSizeIsMultipleOfNative = 1 }; //============================================================================== static String audioManagerGetProperty (const String& property) { const LocalRef jProperty (javaString (property)); const LocalRef text ((jstring) android.activity.callObjectMethod (JuceAppActivity.audioManagerGetProperty, jProperty.get())); if (text.get() != 0) return juceString (text); return String(); } static bool androidHasSystemFeature (const String& property) { const LocalRef jProperty (javaString (property)); return android.activity.callBooleanMethod (JuceAppActivity.hasSystemFeature, jProperty.get()); } static double getNativeSampleRate() { return audioManagerGetProperty ("android.media.property.OUTPUT_SAMPLE_RATE").getDoubleValue(); } static int getNativeBufferSize() { const int val = audioManagerGetProperty ("android.media.property.OUTPUT_FRAMES_PER_BUFFER").getIntValue(); return val > 0 ? val : 512; } static bool isProAudioDevice() { return androidHasSystemFeature ("android.hardware.audio.pro"); } static bool hasLowLatencyAudioPath() { return androidHasSystemFeature ("android.hardware.audio.low_latency"); } //============================================================================== AudioIODeviceCallback* setCallback (AudioIODeviceCallback* const newCallback) { const ScopedLock sl (callbackLock); AudioIODeviceCallback* const oldCallback = callback; callback = newCallback; return oldCallback; } void processBuffers() { if (recorder != nullptr) recorder->readNextBlock (inputBuffer, *this); { const ScopedLock sl (callbackLock); if (callback != nullptr) callback->audioDeviceIOCallback (numInputChannels > 0 ? inputBuffer.getArrayOfReadPointers() : nullptr, numInputChannels, numOutputChannels > 0 ? outputBuffer.getArrayOfWritePointers() : nullptr, numOutputChannels, actualBufferSize); else outputBuffer.clear(); } if (player != nullptr) player->writeBuffer (outputBuffer, *this); } void run() override { setThreadToAudioPriority (); if (recorder != nullptr) recorder->start(); if (player != nullptr) player->start(); while (! threadShouldExit()) processBuffers(); } void setThreadToAudioPriority () { // see android.os.Process.THREAD_PRIORITY_AUDIO const int THREAD_PRIORITY_AUDIO = -16; jint priority = THREAD_PRIORITY_AUDIO; if (priority != android.activity.callIntMethod (JuceAppActivity.setCurrentThreadPriority, (jint) priority)) DBG ("Unable to set audio thread priority: priority is still " << priority); } //============================================================================== struct Engine { Engine() : engineObject (nullptr), engineInterface (nullptr), outputMixObject (nullptr) { if (library.open ("libOpenSLES.so")) { typedef SLresult (*CreateEngineFunc) (SLObjectItf*, SLuint32, const SLEngineOption*, SLuint32, const SLInterfaceID*, const SLboolean*); if (CreateEngineFunc createEngine = (CreateEngineFunc) library.getFunction ("slCreateEngine")) { check (createEngine (&engineObject, 0, nullptr, 0, nullptr, nullptr)); SLInterfaceID* SL_IID_ENGINE = (SLInterfaceID*) library.getFunction ("SL_IID_ENGINE"); SL_IID_ANDROIDSIMPLEBUFFERQUEUE = (SLInterfaceID*) library.getFunction ("SL_IID_ANDROIDSIMPLEBUFFERQUEUE"); SL_IID_PLAY = (SLInterfaceID*) library.getFunction ("SL_IID_PLAY"); SL_IID_RECORD = (SLInterfaceID*) library.getFunction ("SL_IID_RECORD"); SL_IID_ANDROIDCONFIGURATION = (SLInterfaceID*) library.getFunction ("SL_IID_ANDROIDCONFIGURATION"); check ((*engineObject)->Realize (engineObject, SL_BOOLEAN_FALSE)); check ((*engineObject)->GetInterface (engineObject, *SL_IID_ENGINE, &engineInterface)); check ((*engineInterface)->CreateOutputMix (engineInterface, &outputMixObject, 0, nullptr, nullptr)); check ((*outputMixObject)->Realize (outputMixObject, SL_BOOLEAN_FALSE)); } } } ~Engine() { if (outputMixObject != nullptr) (*outputMixObject)->Destroy (outputMixObject); if (engineObject != nullptr) (*engineObject)->Destroy (engineObject); } Player* createPlayer (const int numChannels, const int sampleRate, const int numBuffers, const int bufferSize) { if (numChannels <= 0) return nullptr; ScopedPointer player (new Player (numChannels, sampleRate, *this, numBuffers, bufferSize)); return player->openedOk() ? player.release() : nullptr; } Recorder* createRecorder (const int numChannels, const int sampleRate, const int numBuffers, const int bufferSize) { if (numChannels <= 0) return nullptr; ScopedPointer recorder (new Recorder (numChannels, sampleRate, *this, numBuffers, bufferSize)); return recorder->openedOk() ? recorder.release() : nullptr; } SLObjectItf engineObject; SLEngineItf engineInterface; SLObjectItf outputMixObject; SLInterfaceID* SL_IID_ANDROIDSIMPLEBUFFERQUEUE; SLInterfaceID* SL_IID_PLAY; SLInterfaceID* SL_IID_RECORD; SLInterfaceID* SL_IID_ANDROIDCONFIGURATION; private: DynamicLibrary library; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Engine) }; //============================================================================== struct BufferList { BufferList (const int numChannels_, const int numBuffers_, const int numSamples_) : numChannels (numChannels_), numBuffers (numBuffers_), numSamples (numSamples_), bufferSpace (numChannels_ * numSamples * numBuffers), nextBlock (0) { } int16* waitForFreeBuffer (Thread& threadToCheck) noexcept { while (numBlocksOut.get() == numBuffers) { dataArrived.wait (1); if (threadToCheck.threadShouldExit()) return nullptr; } return getNextBuffer(); } int16* getNextBuffer() noexcept { if (++nextBlock == numBuffers) nextBlock = 0; return bufferSpace + nextBlock * numChannels * numSamples; } void bufferReturned() noexcept { --numBlocksOut; dataArrived.signal(); } void bufferSent() noexcept { ++numBlocksOut; dataArrived.signal(); } int getBufferSizeBytes() const noexcept { return numChannels * numSamples * sizeof (int16); } const int numChannels, numBuffers, numSamples; private: HeapBlock bufferSpace; int nextBlock; Atomic numBlocksOut; WaitableEvent dataArrived; }; //============================================================================== struct Player { Player (int numChannels, int sampleRate, Engine& engine, int playerNumBuffers, int playerBufferSize) : playerObject (nullptr), playerPlay (nullptr), playerBufferQueue (nullptr), bufferList (numChannels, playerNumBuffers, playerBufferSize) { SLDataFormat_PCM pcmFormat = { SL_DATAFORMAT_PCM, (SLuint32) numChannels, (SLuint32) (sampleRate * 1000), SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, (numChannels == 1) ? SL_SPEAKER_FRONT_CENTER : (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT), SL_BYTEORDER_LITTLEENDIAN }; SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, static_cast (bufferList.numBuffers) }; SLDataSource audioSrc = { &bufferQueue, &pcmFormat }; SLDataLocator_OutputMix outputMix = { SL_DATALOCATOR_OUTPUTMIX, engine.outputMixObject }; SLDataSink audioSink = { &outputMix, nullptr }; // (SL_IID_BUFFERQUEUE is not guaranteed to remain future-proof, so use SL_IID_ANDROIDSIMPLEBUFFERQUEUE) const SLInterfaceID interfaceIDs[] = { *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE }; const SLboolean flags[] = { SL_BOOLEAN_TRUE }; check ((*engine.engineInterface)->CreateAudioPlayer (engine.engineInterface, &playerObject, &audioSrc, &audioSink, 1, interfaceIDs, flags)); check ((*playerObject)->Realize (playerObject, SL_BOOLEAN_FALSE)); check ((*playerObject)->GetInterface (playerObject, *engine.SL_IID_PLAY, &playerPlay)); check ((*playerObject)->GetInterface (playerObject, *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &playerBufferQueue)); check ((*playerBufferQueue)->RegisterCallback (playerBufferQueue, staticCallback, this)); } ~Player() { if (playerPlay != nullptr) check ((*playerPlay)->SetPlayState (playerPlay, SL_PLAYSTATE_STOPPED)); if (playerBufferQueue != nullptr) check ((*playerBufferQueue)->Clear (playerBufferQueue)); if (playerObject != nullptr) (*playerObject)->Destroy (playerObject); } bool openedOk() const noexcept { return playerBufferQueue != nullptr; } void start() { jassert (openedOk()); check ((*playerPlay)->SetPlayState (playerPlay, SL_PLAYSTATE_PLAYING)); } void writeBuffer (const AudioSampleBuffer& buffer, Thread& thread) noexcept { jassert (buffer.getNumChannels() == bufferList.numChannels); jassert (buffer.getNumSamples() < bufferList.numSamples * bufferList.numBuffers); int offset = 0; int numSamples = buffer.getNumSamples(); while (numSamples > 0) { if (int16* const destBuffer = bufferList.waitForFreeBuffer (thread)) { for (int i = 0; i < bufferList.numChannels; ++i) { typedef AudioData::Pointer DstSampleType; typedef AudioData::Pointer SrcSampleType; DstSampleType dstData (destBuffer + i, bufferList.numChannels); SrcSampleType srcData (buffer.getReadPointer (i, offset)); dstData.convertSamples (srcData, bufferList.numSamples); } enqueueBuffer (destBuffer); numSamples -= bufferList.numSamples; offset += bufferList.numSamples; } else { break; } } } private: SLObjectItf playerObject; SLPlayItf playerPlay; SLAndroidSimpleBufferQueueItf playerBufferQueue; BufferList bufferList; void enqueueBuffer (int16* buffer) noexcept { check ((*playerBufferQueue)->Enqueue (playerBufferQueue, buffer, bufferList.getBufferSizeBytes())); bufferList.bufferSent(); } static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) noexcept { jassert (queue == static_cast (context)->playerBufferQueue); ignoreUnused (queue); static_cast (context)->bufferList.bufferReturned(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Player) }; //============================================================================== struct Recorder { Recorder (int numChannels, int sampleRate, Engine& engine, const int numBuffers, const int numSamples) : recorderObject (nullptr), recorderRecord (nullptr), recorderBufferQueue (nullptr), configObject (nullptr), bufferList (numChannels, numBuffers, numSamples) { SLDataFormat_PCM pcmFormat = { SL_DATAFORMAT_PCM, (SLuint32) numChannels, (SLuint32) (sampleRate * 1000), // (sample rate units are millihertz) SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, (numChannels == 1) ? SL_SPEAKER_FRONT_CENTER : (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT), SL_BYTEORDER_LITTLEENDIAN }; SLDataLocator_IODevice ioDevice = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr }; SLDataSource audioSrc = { &ioDevice, nullptr }; SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, static_cast (bufferList.numBuffers) }; SLDataSink audioSink = { &bufferQueue, &pcmFormat }; const SLInterfaceID interfaceIDs[] = { *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE }; const SLboolean flags[] = { SL_BOOLEAN_TRUE }; if (check ((*engine.engineInterface)->CreateAudioRecorder (engine.engineInterface, &recorderObject, &audioSrc, &audioSink, 1, interfaceIDs, flags))) { if (check ((*recorderObject)->Realize (recorderObject, SL_BOOLEAN_FALSE))) { check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_RECORD, &recorderRecord)); check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorderBufferQueue)); // not all android versions seem to have a config object SLresult result = (*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_ANDROIDCONFIGURATION, &configObject); if (result != SL_RESULT_SUCCESS) configObject = nullptr; check ((*recorderBufferQueue)->RegisterCallback (recorderBufferQueue, staticCallback, this)); check ((*recorderRecord)->SetRecordState (recorderRecord, SL_RECORDSTATE_STOPPED)); } } } ~Recorder() { if (recorderRecord != nullptr) check ((*recorderRecord)->SetRecordState (recorderRecord, SL_RECORDSTATE_STOPPED)); if (recorderBufferQueue != nullptr) check ((*recorderBufferQueue)->Clear (recorderBufferQueue)); if (recorderObject != nullptr) (*recorderObject)->Destroy (recorderObject); } bool openedOk() const noexcept { return recorderBufferQueue != nullptr; } void start() { jassert (openedOk()); check ((*recorderRecord)->SetRecordState (recorderRecord, SL_RECORDSTATE_RECORDING)); } void readNextBlock (AudioSampleBuffer& buffer, Thread& thread) { jassert (buffer.getNumChannels() == bufferList.numChannels); jassert (buffer.getNumSamples() < bufferList.numSamples * bufferList.numBuffers); jassert ((buffer.getNumSamples() % bufferList.numSamples) == 0); int offset = 0; int numSamples = buffer.getNumSamples(); while (numSamples > 0) { if (int16* const srcBuffer = bufferList.waitForFreeBuffer (thread)) { for (int i = 0; i < bufferList.numChannels; ++i) { typedef AudioData::Pointer DstSampleType; typedef AudioData::Pointer SrcSampleType; DstSampleType dstData (buffer.getWritePointer (i, offset)); SrcSampleType srcData (srcBuffer + i, bufferList.numChannels); dstData.convertSamples (srcData, bufferList.numSamples); } enqueueBuffer (srcBuffer); numSamples -= bufferList.numSamples; offset += bufferList.numSamples; } else { break; } } } bool setAudioPreprocessingEnabled (bool enable) { SLuint32 mode = enable ? SL_ANDROID_RECORDING_PRESET_GENERIC : SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; return configObject != nullptr && check ((*configObject)->SetConfiguration (configObject, SL_ANDROID_KEY_RECORDING_PRESET, &mode, sizeof (mode))); } private: SLObjectItf recorderObject; SLRecordItf recorderRecord; SLAndroidSimpleBufferQueueItf recorderBufferQueue; SLAndroidConfigurationItf configObject; BufferList bufferList; void enqueueBuffer (int16* buffer) noexcept { check ((*recorderBufferQueue)->Enqueue (recorderBufferQueue, buffer, bufferList.getBufferSizeBytes())); bufferList.bufferSent(); } static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) noexcept { jassert (queue == static_cast (context)->recorderBufferQueue); ignoreUnused (queue); static_cast (context)->bufferList.bufferReturned(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Recorder) }; //============================================================================== Engine engine; ScopedPointer player; ScopedPointer recorder; //============================================================================== static bool check (const SLresult result) noexcept { jassert (result == SL_RESULT_SUCCESS); return result == SL_RESULT_SUCCESS; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenSLAudioIODevice) }; //============================================================================== class OpenSLAudioDeviceType : public AudioIODeviceType { public: OpenSLAudioDeviceType() : AudioIODeviceType (openSLTypeName) {} //============================================================================== void scanForDevices() override {} StringArray getDeviceNames (bool wantInputNames) const override { return StringArray (openSLTypeName); } int getDefaultDeviceIndex (bool forInput) const override { return 0; } int getIndexOfDevice (AudioIODevice* device, bool asInput) const override { return device != nullptr ? 0 : -1; } bool hasSeparateInputsAndOutputs() const override { return false; } AudioIODevice* createDevice (const String& outputDeviceName, const String& inputDeviceName) override { ScopedPointer dev; if (outputDeviceName.isNotEmpty() || inputDeviceName.isNotEmpty()) { dev = new OpenSLAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName : inputDeviceName); if (! dev->openedOk()) dev = nullptr; } return dev.release(); } private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenSLAudioDeviceType) }; //============================================================================== AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_OpenSLES() { return isOpenSLAvailable() ? new OpenSLAudioDeviceType() : nullptr; }