diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index 89c6111871..a3575883dd 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -103,7 +103,7 @@ public: ValueWithDefault androidJavaLibs, androidRepositories, androidDependencies, androidScreenOrientation, androidActivityClass, androidActivitySubClassName, androidActivityBaseClassName, androidManifestCustomXmlElements, androidVersionCode, androidMinimumSDK, androidTheme, androidSharedLibraries, androidStaticLibraries, androidExtraAssetsFolder, - androidInternetNeeded, androidMicNeeded, androidBluetoothNeeded, androidExternalReadPermission, + androidOboeRepositoryPath, androidInternetNeeded, androidMicNeeded, androidBluetoothNeeded, androidExternalReadPermission, androidExternalWritePermission, androidInAppBillingPermission, androidVibratePermission,androidOtherPermissions, androidEnableRemoteNotifications, androidRemoteNotificationsConfigFile, androidEnableContentSharing, androidKeyStore, androidKeyStorePass, androidKeyAlias, androidKeyAliasPass, gradleVersion, gradleToolchain, androidPluginVersion, buildToolsVersion; @@ -125,6 +125,7 @@ public: androidSharedLibraries (settings, Ids::androidSharedLibraries, getUndoManager()), androidStaticLibraries (settings, Ids::androidStaticLibraries, getUndoManager()), androidExtraAssetsFolder (settings, Ids::androidExtraAssetsFolder, getUndoManager()), + androidOboeRepositoryPath (settings, Ids::androidOboeRepositoryPath, getUndoManager()), androidInternetNeeded (settings, Ids::androidInternetNeeded, getUndoManager(), true), androidMicNeeded (settings, Ids::microphonePermissionNeeded, getUndoManager(), false), androidBluetoothNeeded (settings, Ids::androidBluetoothNeeded, getUndoManager(), true), @@ -402,6 +403,14 @@ private: if (! isLibrary()) mo << "SET(BINARY_NAME \"juce_jni\")" << newLine << newLine; + if (project.getConfigFlag ("JUCE_USE_ANDROID_OBOE").get()) + { + String oboePath (androidOboeRepositoryPath.get().toString().quoted()); + + mo << "SET(OBOE_DIR " << oboePath << ")" << newLine << newLine; + mo << "add_subdirectory (${OBOE_DIR} ./oboe)" << newLine << newLine; + } + String cpufeaturesPath ("${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c"); mo << "add_library(\"cpufeatures\" STATIC \"" << cpufeaturesPath << "\")" << newLine << "set_source_files_properties(\"" << cpufeaturesPath << "\" PROPERTIES COMPILE_FLAGS \"-Wno-sign-conversion -Wno-gnu-statement-expression\")" << newLine << newLine; @@ -419,6 +428,10 @@ private: mo << " \"" << escapeDirectoryForCmake (path) << "\"" << newLine; mo << " \"${ANDROID_NDK}/sources/android/cpufeatures\"" << newLine; + + if (project.getConfigFlag ("JUCE_USE_ANDROID_OBOE").get()) + mo << " \"${OBOE_DIR}/include\"" << newLine; + mo << ")" << newLine << newLine; } @@ -548,6 +561,10 @@ private: mo << " \"cpufeatures\"" << newLine; } + + if (project.getConfigFlag ("JUCE_USE_ANDROID_OBOE").get()) + mo << " \"oboe\"" << newLine; + mo << ")" << newLine; overwriteFileIfDifferentOrThrow (file, mo); @@ -892,6 +909,10 @@ private: //============================================================================== void createManifestExporterProperties (PropertyListBuilder& props) { + props.add (new TextPropertyComponent (androidOboeRepositoryPath, "Oboe repository path", 2048, false), + "Path to the root of Oboe repository. Make sure to point Oboe repository to " + "commit with SHA 44c6b6ea9c8fa9b5b74cbd60f355068b57b50b37 before building."); + props.add (new ChoicePropertyComponent (androidInternetNeeded, "Internet Access"), "If enabled, this will set the android.permission.INTERNET flag in the manifest."); diff --git a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h index a9e9f2c47d..a76b7cff16 100644 --- a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h +++ b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h @@ -191,6 +191,7 @@ namespace Ids DECLARE_ID (androidVersionCode); DECLARE_ID (androidSDKPath); DECLARE_ID (androidNDKPath); + DECLARE_ID (androidOboeRepositoryPath); DECLARE_ID (androidInternetNeeded); DECLARE_ID (androidArchitectures); DECLARE_ID (androidManifestCustomXmlElements); diff --git a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp index eb172f469c..a896536a08 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -158,6 +158,7 @@ void AudioDeviceManager::createAudioDeviceTypes (OwnedArray& addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_iOSAudio()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_ALSA()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_JACK()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_Oboe()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_OpenSLES()); addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_Android()); } diff --git a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp index 3e28df6fad..cb609d592f 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp +++ b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp @@ -78,4 +78,8 @@ AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Android() AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_OpenSLES() { return nullptr; } #endif +#if ! (JUCE_ANDROID && JUCE_USE_ANDROID_OBOE) +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Oboe() { return nullptr; } +#endif + } // namespace juce diff --git a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h index c11f711a41..74c420acde 100644 --- a/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h +++ b/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h @@ -161,6 +161,8 @@ public: static AudioIODeviceType* createAudioIODeviceType_Android(); /** Creates an Android OpenSLES device type if it's available on this platform, or returns null. */ static AudioIODeviceType* createAudioIODeviceType_OpenSLES(); + /** Creates an Oboe device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_Oboe(); protected: explicit AudioIODeviceType (const String& typeName); diff --git a/modules/juce_audio_devices/juce_audio_devices.cpp b/modules/juce_audio_devices/juce_audio_devices.cpp index 72bfbfb966..731a63ebeb 100644 --- a/modules/juce_audio_devices/juce_audio_devices.cpp +++ b/modules/juce_audio_devices/juce_audio_devices.cpp @@ -153,6 +153,10 @@ #include #endif + #if JUCE_USE_ANDROID_OBOE + #include + #endif + #endif #include "audio_io/juce_AudioDeviceManager.cpp" @@ -211,6 +215,10 @@ #if JUCE_USE_ANDROID_OPENSLES #include "native/juce_android_OpenSL.cpp" #endif + + #if JUCE_USE_ANDROID_OBOE + #include "native/juce_android_Oboe.cpp" + #endif #endif #if ! JUCE_SYSTEMAUDIOVOL_IMPLEMENTED diff --git a/modules/juce_audio_devices/juce_audio_devices.h b/modules/juce_audio_devices/juce_audio_devices.h index 5d193fabce..c520746725 100644 --- a/modules/juce_audio_devices/juce_audio_devices.h +++ b/modules/juce_audio_devices/juce_audio_devices.h @@ -108,11 +108,31 @@ #define JUCE_JACK 0 #endif +/** Config: JUCE_USE_ANDROID_OBOE + *** + DEVELOPER PREVIEW - Oboe is currently in developer preview and + is in active development. This preview allows for early access + and evaluation for developers targeting Android platform. + *** + + Enables Oboe devices (Android only, API 16 or above). Requires + Oboe repository path to be specified in Android exporter. +*/ + +#ifndef JUCE_USE_ANDROID_OBOE + #define JUCE_USE_ANDROID_OBOE 0 +#endif + +#if JUCE_USE_ANDROID_OBOE && JUCE_ANDROID_API_VERSION < 16 + #undef JUCE_USE_ANDROID_OBOE + #define JUCE_USE_ANDROID_OBOE 0 +#endif + /** Config: JUCE_USE_ANDROID_OPENSLES Enables OpenSLES devices (Android only). */ #ifndef JUCE_USE_ANDROID_OPENSLES - #if JUCE_ANDROID_API_VERSION > 8 + #if ! JUCE_USE_ANDROID_OBOE && JUCE_ANDROID_API_VERSION >= 9 #define JUCE_USE_ANDROID_OPENSLES 1 #else #define JUCE_USE_ANDROID_OPENSLES 0 diff --git a/modules/juce_audio_devices/native/juce_android_Audio.cpp b/modules/juce_audio_devices/native/juce_android_Audio.cpp index 4a5d6bfb30..40a9725b8a 100644 --- a/modules/juce_audio_devices/native/juce_android_Audio.cpp +++ b/modules/juce_audio_devices/native/juce_android_Audio.cpp @@ -482,10 +482,16 @@ private: //============================================================================== +extern bool isOboeAvailable(); extern bool isOpenSLAvailable(); AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Android() { + #if JUCE_USE_ANDROID_OBOE + if (isOboeAvailable()) + return nullptr; + #endif + #if JUCE_USE_ANDROID_OPENSLES if (isOpenSLAvailable()) return nullptr; diff --git a/modules/juce_audio_devices/native/juce_android_Oboe.cpp b/modules/juce_audio_devices/native/juce_android_Oboe.cpp new file mode 100644 index 0000000000..c242495a26 --- /dev/null +++ b/modules/juce_audio_devices/native/juce_android_Oboe.cpp @@ -0,0 +1,1490 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2018 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifndef JUCE_OBOE_LOG_ENABLED + #define JUCE_OBOE_LOG_ENABLED 1 +#endif + +#if JUCE_OBOE_LOG_ENABLED + #define JUCE_OBOE_LOG(x) DBG(x) +#else + #define JUCE_OBOE_LOG(x) {} +#endif + +namespace juce +{ + +template struct OboeAudioIODeviceBufferHelpers {}; + +template<> +struct OboeAudioIODeviceBufferHelpers +{ + static oboe::AudioFormat oboeAudioFormat() { return oboe::AudioFormat::I16; } + + static constexpr int bitDepth() { return 16; } + + static void referAudioBufferDirectlyToOboeIfPossible (int16*, AudioBuffer&, int) {} + + static void convertFromOboe (const int16* srcInterleaved, AudioBuffer& audioBuffer, int numSamples) + { + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (audioBuffer.getWritePointer (i)); + SrcSampleType srcData (srcInterleaved + i, audioBuffer.getNumChannels()); + dstData.convertSamples (srcData, numSamples); + } + } + + static void convertToOboe (const AudioBuffer& audioBuffer, int16* dstInterleaved, int numSamples) + { + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (dstInterleaved + i, audioBuffer.getNumChannels()); + SrcSampleType srcData (audioBuffer.getReadPointer (i)); + dstData.convertSamples (srcData, numSamples); + } + } +}; + +template<> +struct OboeAudioIODeviceBufferHelpers +{ + static oboe::AudioFormat oboeAudioFormat() { return oboe::AudioFormat::Float; } + + static constexpr int bitDepth() { return 32; } + + static void referAudioBufferDirectlyToOboeIfPossible (float* nativeBuffer, AudioBuffer& audioBuffer, int numSamples) + { + if (audioBuffer.getNumChannels() == 1) + audioBuffer.setDataToReferTo (&nativeBuffer, 1, numSamples); + } + + static void convertFromOboe (const float* srcInterleaved, AudioBuffer& audioBuffer, int numSamples) + { + // No need to convert, we instructed the buffer to point to the src data directly already + if (audioBuffer.getNumChannels() == 1) + { + jassert (audioBuffer.getWritePointer (0) == srcInterleaved); + return; + } + + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (audioBuffer.getWritePointer (i)); + SrcSampleType srcData (srcInterleaved + i, audioBuffer.getNumChannels()); + dstData.convertSamples (srcData, numSamples); + } + } + + static void convertToOboe (const AudioBuffer& audioBuffer, float* dstInterleaved, int numSamples) + { + // No need to convert, we instructed the buffer to point to the src data directly already + if (audioBuffer.getNumChannels() == 1) + { + jassert (audioBuffer.getReadPointer (0) == dstInterleaved); + return; + } + + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (dstInterleaved + i, audioBuffer.getNumChannels()); + SrcSampleType srcData (audioBuffer.getReadPointer (i)); + dstData.convertSamples (srcData, numSamples); + } + } +}; + +//============================================================================== +class OboeAudioIODevice : public AudioIODevice +{ +public: + //============================================================================== + OboeAudioIODevice (const String& deviceName, + int inputDeviceIdToUse, + const Array& supportedInputSampleRatesToUse, + int maxNumInputChannelsToUse, + int outputDeviceIdToUse, + const Array& supportedOutputSampleRatesToUse, + int maxNumOutputChannelsToUse) + : AudioIODevice (deviceName, oboeTypeName), + inputDeviceId (inputDeviceIdToUse), + supportedInputSampleRates (supportedInputSampleRatesToUse), + maxNumInputChannels (maxNumInputChannelsToUse), + outputDeviceId (outputDeviceIdToUse), + supportedOutputSampleRates (supportedOutputSampleRatesToUse), + maxNumOutputChannels (maxNumOutputChannelsToUse) + { + // At least an input or an output has to be supported by the device! + jassert (inputDeviceId != -1 || outputDeviceId != -1); + } + + ~OboeAudioIODevice() + { + close(); + } + + StringArray getOutputChannelNames() override { return getChannelNames (false); } + StringArray getInputChannelNames() override { return getChannelNames (true); } + + Array getAvailableSampleRates() override + { + Array result; + + auto inputSampleRates = getAvailableSampleRates (true); + auto outputSampleRates = getAvailableSampleRates (false); + + if (inputDeviceId == -1) + { + for (auto& sr : outputSampleRates) + result.add (sr); + } + else if (outputDeviceId == -1) + { + for (auto& sr : inputSampleRates) + result.add (sr); + } + else + { + // For best performance, the same sample rate should be used for input and output, + for (auto& inputSampleRate : inputSampleRates) + { + if (outputSampleRates.contains (inputSampleRate)) + result.add (inputSampleRate); + } + } + + // either invalid device was requested or its input&output don't have compatible sample rate + jassert (result.size() > 0); + return result; + } + + 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 bufferSizes; + + for (int i = 1; i < defaultNumMultiples; ++i) + bufferSizes.add (i * nativeBufferSize); + + return bufferSizes; + } + + String open (const BigInteger& inputChannels, const BigInteger& outputChannels, + double requestedSampleRate, int bufferSize) override + { + close(); + + lastError.clear(); + sampleRate = (int) requestedSampleRate; + + actualBufferSize = (bufferSize <= 0) ? getDefaultBufferSize() : bufferSize; + + // The device may report no max, claiming "no limits". Pick sensible defaults. + int maxOutChans = maxNumOutputChannels > 0 ? maxNumOutputChannels : 2; + int maxInChans = maxNumInputChannels > 0 ? maxNumInputChannels : 1; + + activeOutputChans = outputChannels; + activeOutputChans.setRange (maxOutChans, + activeOutputChans.getHighestBit() + 1 - maxOutChans, + false); + + activeInputChans = inputChannels; + activeInputChans.setRange (maxInChans, + activeInputChans.getHighestBit() + 1 - maxInChans, + false); + + int numOutputChans = activeOutputChans.countNumberOfSetBits(); + int numInputChans = activeInputChans.countNumberOfSetBits(); + + if (numInputChans > 0 && (! 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 Oboe input device: the app was not granted android.permission.RECORD_AUDIO"; + } + + // At least one output channel should be set! + jassert (numOutputChans >= 0); + + session.reset (OboeSessionBase::create (*this, + inputDeviceId, outputDeviceId, + numInputChans, numOutputChans, + sampleRate, actualBufferSize)); + + deviceOpen = session != nullptr; + + if (! deviceOpen) + lastError = "Failed to create audio session"; + + return lastError; + } + + void close() override { stop(); } + int getOutputLatencyInSamples() override { return session->getOutputLatencyInSamples(); } + int getInputLatencyInSamples() override { return session->getInputLatencyInSamples(); } + bool isOpen() override { return deviceOpen; } + int getCurrentBufferSizeSamples() override { return actualBufferSize; } + int getCurrentBitDepth() override { return session->getCurrentBitDepth(); } + BigInteger getActiveOutputChannels() const override { return activeOutputChans; } + BigInteger getActiveInputChannels() const override { return activeInputChans; } + String getLastError() override { return lastError; } + bool isPlaying() override { return callback.get() != nullptr; } + int getXRunCount() const noexcept override { return session->getXRunCount(); } + + 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 + { + if (callback.get() != newCallback) + { + if (newCallback != nullptr) + newCallback->audioDeviceAboutToStart (this); + + AudioIODeviceCallback* oldCallback = callback.get(); + + if (oldCallback != nullptr) + { + // already running + if (newCallback == nullptr) + stop(); + else + setCallback (newCallback); + + oldCallback->audioDeviceStopped(); + } + else + { + jassert (newCallback != nullptr); + + // session hasn't started yet + setCallback (newCallback); + running = true; + + session->start(); + } + + callback = newCallback; + } + } + + void stop() override + { + if (session != nullptr) + session->stop(); + + running = false; + setCallback (nullptr); + } + + bool setAudioPreprocessingEnabled (bool) override + { + // Oboe does not expose this setting, yet it may use preprocessing + // for older APIs running OpenSL + return false; + } + + static const char* const oboeTypeName; + +private: + StringArray getChannelNames (bool forInput) + { + auto& deviceId = forInput ? inputDeviceId : outputDeviceId; + auto& numChannels = forInput ? maxNumInputChannels : maxNumOutputChannels; + + // If the device id is unknown (on olders APIs) or if the device claims to + // support "any" channel count, use a sensible default + if (deviceId == -1 || numChannels == -1) + return forInput ? StringArray ("Input") : StringArray ("Left", "Right"); + + StringArray names; + + for (int i = 0; i < numChannels; ++i) + names.add ("Channel " + String (i + 1)); + + return names; + } + + Array getAvailableSampleRates (bool forInput) + { + auto& supportedSampleRates = forInput + ? supportedInputSampleRates + : supportedOutputSampleRates; + + if (! supportedSampleRates.isEmpty()) + return supportedSampleRates; + + // device claims that it supports "any" sample rate, use + // standard ones then + return getDefaultSampleRates(); + } + + static Array getDefaultSampleRates() + { + static const int standardRates[] = { 8000, 11025, 12000, 16000, + 22050, 24000, 32000, 44100, 48000 }; + Array rates (standardRates, numElementsInArray (standardRates)); + + // make sure the native sample rate is part of the list + int native = (int) getNativeSampleRate(); + + if (native != 0 && ! rates.contains (native)) + rates.add (native); + + return rates; + } + + void setCallback (AudioIODeviceCallback* callbackToUse) + { + if (! running) + { + callback.set (callbackToUse); + return; + } + + // Setting nullptr callback is allowed only when playback is stopped. + jassert (callbackToUse != nullptr); + + while (true) + { + AudioIODeviceCallback* old = callback.get(); + + if (old == callbackToUse) + break; + + // If old is nullptr, then it means that it's currently being used! + if (old != nullptr && callback.compareAndSetBool (callbackToUse, old)) + break; + + Thread::sleep (1); + } + } + + void process (const float** inputChannelData, int numInputChannels, + float** outputChannelData, int numOutputChannels, int32_t numFrames) + { + if (AudioIODeviceCallback* cb = callback.exchange (nullptr)) + { + cb->audioDeviceIOCallback (inputChannelData, numInputChannels, + outputChannelData, numOutputChannels, numFrames); + callback.set (cb); + } + else + { + for (int i = 0; i < numOutputChannels; ++i) + zeromem (outputChannelData[i], sizeof (float) * static_cast (numFrames)); + } + } + + //============================================================================== + class OboeStream + { + public: + OboeStream (int deviceId, oboe::Direction direction, + oboe::SharingMode sharingMode, + int channelCount, oboe::AudioFormat format, + int32 sampleRate, int32 bufferSize, + oboe::AudioStreamCallback* callback = nullptr) + { + open (deviceId, direction, sharingMode, channelCount, + format, sampleRate, bufferSize, callback); + } + + ~OboeStream() + { + // AudioStreamCallback can only be deleted when stream has been closed + close(); + } + + bool openedOk() const noexcept + { + return openResult == oboe::Result::OK; + } + + void start() + { + jassert (openedOk()); + + if (openedOk() && stream != nullptr) + { + auto expectedState = oboe::StreamState::Starting; + auto nextState = oboe::StreamState::Started; + int64 timeoutNanos = 1000 * oboe::kNanosPerMillisecond; + + auto startResult = stream->requestStart(); + JUCE_OBOE_LOG ("Requested Oboe stream start with result: " + String (oboe::convertToText (startResult))); + + startResult = stream->waitForStateChange (expectedState, &nextState, timeoutNanos); + JUCE_OBOE_LOG ("Starting Oboe stream with result: " + String (oboe::convertToText (startResult)); + + "\nUses AAudio = " + (stream != nullptr ? String ((int) stream->usesAAudio()) : String ("?")) + + "\nDirection = " + (stream != nullptr ? String (oboe::convertToText (stream->getDirection())) : String ("?")) + + "\nSharingMode = " + (stream != nullptr ? String (oboe::convertToText (stream->getSharingMode())) : String ("?")) + + "\nChannelCount = " + (stream != nullptr ? String (stream->getChannelCount()) : String ("?")) + + "\nFormat = " + (stream != nullptr ? String (oboe::convertToText (stream->getFormat())) : String ("?")) + + "\nSampleRate = " + (stream != nullptr ? String (stream->getSampleRate()) : String ("?")) + + "\nBufferSizeInFrames = " + (stream != nullptr ? String (stream->getBufferSizeInFrames()) : String ("?")) + + "\nBufferCapacityInFrames = " + (stream != nullptr ? String (stream->getBufferCapacityInFrames()) : String ("?")) + + "\nFramesPerBurst = " + (stream != nullptr ? String (stream->getFramesPerBurst()) : String ("?")) + + "\nFramesPerCallback = " + (stream != nullptr ? String (stream->getFramesPerCallback()) : String ("?")) + + "\nBytesPerFrame = " + (stream != nullptr ? String (stream->getBytesPerFrame()) : String ("?")) + + "\nBytesPerSample = " + (stream != nullptr ? String (stream->getBytesPerSample()) : String ("?")) + + "\nPerformanceMode = " + String (oboe::convertToText (oboe::PerformanceMode::LowLatency)) + + "\ngetDeviceId = " + (stream != nullptr ? String (stream->getDeviceId()) : String ("?"))); + } + } + + oboe::AudioStream* getNativeStream() + { + jassert (openedOk()); + + return stream; + } + + int getXRunCount() const + { + return stream != nullptr ? stream->getXRunCount() : 0; + } + + private: + void open (int deviceId, oboe::Direction direction, + oboe::SharingMode sharingMode, + int channelCount, oboe::AudioFormat format, + int32 sampleRate, int32 bufferSize, + oboe::AudioStreamCallback* callback = nullptr) + { + oboe::AudioStreamBuilder builder; + + if (deviceId != -1) + builder.setDeviceId (deviceId); + + static int defaultFramesPerBurst = getDefaultFramesPerBurst(); + + // Note: letting OS to choose the buffer capacity & frames per callback. + builder.setDirection (direction); + builder.setSharingMode (sharingMode); + builder.setChannelCount (channelCount); + builder.setFormat (format); + builder.setSampleRate (sampleRate); + builder.setDefaultFramesPerBurst ((int32) defaultFramesPerBurst); + builder.setPerformanceMode (oboe::PerformanceMode::LowLatency); + builder.setCallback (callback); + + JUCE_OBOE_LOG (String ("Preparing Oboe stream with params:") + + "\nAAudio supported = " + String (int (builder.isAAudioSupported())) + + "\nAPI = " + String (oboe::convertToText (builder.getAudioApi())) + + "\nDeviceId = " + String (deviceId) + + "\nDirection = " + String (oboe::convertToText (direction)) + + "\nSharingMode = " + String (oboe::convertToText (sharingMode)) + + "\nChannelCount = " + String (channelCount) + + "\nFormat = " + String (oboe::convertToText (format)) + + "\nSampleRate = " + String (sampleRate) + + "\nBufferSizeInFrames = " + String (bufferSize) + + "\nFramesPerBurst = " + String (defaultFramesPerBurst) + + "\nPerformanceMode = " + String (oboe::convertToText (oboe::PerformanceMode::LowLatency))); + + openResult = builder.openStream (&stream); + JUCE_OBOE_LOG ("Building Oboe stream with result: " + String (oboe::convertToText (openResult)) + + "\nStream state = " + (stream != nullptr ? String (oboe::convertToText (stream->getState())) : String ("?"))); + + if (stream != nullptr) + stream->setBufferSizeInFrames (bufferSize); + + JUCE_OBOE_LOG (String ("Stream details:") + + "\nUses AAudio = " + (stream != nullptr ? String ((int) stream->usesAAudio()) : String ("?")) + + "\nDeviceId = " + (stream != nullptr ? String (stream->getDeviceId()) : String ("?")) + + "\nDirection = " + (stream != nullptr ? String (oboe::convertToText (stream->getDirection())) : String ("?")) + + "\nSharingMode = " + (stream != nullptr ? String (oboe::convertToText (stream->getSharingMode())) : String ("?")) + + "\nChannelCount = " + (stream != nullptr ? String (stream->getChannelCount()) : String ("?")) + + "\nFormat = " + (stream != nullptr ? String (oboe::convertToText (stream->getFormat())) : String ("?")) + + "\nSampleRate = " + (stream != nullptr ? String (stream->getSampleRate()) : String ("?")) + + "\nBufferSizeInFrames = " + (stream != nullptr ? String (stream->getBufferSizeInFrames()) : String ("?")) + + "\nBufferCapacityInFrames = " + (stream != nullptr ? String (stream->getBufferCapacityInFrames()) : String ("?")) + + "\nFramesPerBurst = " + (stream != nullptr ? String (stream->getFramesPerBurst()) : String ("?")) + + "\nFramesPerCallback = " + (stream != nullptr ? String (stream->getFramesPerCallback()) : String ("?")) + + "\nBytesPerFrame = " + (stream != nullptr ? String (stream->getBytesPerFrame()) : String ("?")) + + "\nBytesPerSample = " + (stream != nullptr ? String (stream->getBytesPerSample()) : String ("?")) + + "\nPerformanceMode = " + String (oboe::convertToText (oboe::PerformanceMode::LowLatency))); + } + + void close() + { + if (stream != nullptr) + { + oboe::Result result = stream->close(); + JUCE_OBOE_LOG ("Requested Oboe stream close with result: " + String (oboe::convertToText (result))); + } + } + + int getDefaultFramesPerBurst() const + { + // NB: this function only works for inbuilt speakers and headphones + auto* env = getEnv(); + + auto audioManager = LocalRef (env->CallObjectMethod (android.activity, + JuceAppActivity.getSystemService, + javaString ("audio").get())); + + auto propertyJavaString = javaString ("android.media.property.OUTPUT_FRAMES_PER_BUFFER"); + + auto framesPerBurstString = LocalRef ((jstring) android.activity.callObjectMethod (JuceAppActivity.audioManagerGetProperty, + propertyJavaString.get())); + + return framesPerBurstString != 0 ? env->CallStaticIntMethod (JavaInteger, JavaInteger.parseInt, framesPerBurstString.get(), 10) : 192; + } + + oboe::AudioStream* stream = nullptr; + oboe::Result openResult; + }; + + //============================================================================== + class OboeSessionBase : protected oboe::AudioStreamCallback + { + public: + static OboeSessionBase* create (OboeAudioIODevice& owner, + int inputDeviceId, int outputDeviceId, + int numInputChannels, int numOutputChannels, + int sampleRate, int bufferSize); + + virtual void start() = 0; + virtual void stop() = 0; + virtual int getOutputLatencyInSamples() = 0; + virtual int getInputLatencyInSamples() = 0; + + bool openedOk() const noexcept + { + if (inputStream != nullptr && ! inputStream->openedOk()) + return false; + + return outputStream != nullptr && outputStream->openedOk(); + } + + int getCurrentBitDepth() const noexcept { return bitDepth; } + + int getXRunCount() const + { + int inputXRunCount = jmax (0, inputStream != nullptr ? inputStream->getXRunCount() : 0); + int outputXRunCount = jmax (0, outputStream != nullptr ? outputStream->getXRunCount() : 0); + + return inputXRunCount + outputXRunCount; + } + + protected: + OboeSessionBase (OboeAudioIODevice& ownerToUse, + int inputDeviceIdToUse, int outputDeviceIdToUse, + int numInputChannelsToUse, int numOutputChannelsToUse, + int sampleRateToUse, int bufferSizeToUse, + oboe::AudioFormat streamFormatToUse, + int bitDepthToUse) + : owner (ownerToUse), + inputDeviceId (inputDeviceIdToUse), + outputDeviceId (outputDeviceIdToUse), + numInputChannels (numInputChannelsToUse), + numOutputChannels (numOutputChannelsToUse), + sampleRate (sampleRateToUse), + bufferSize (bufferSizeToUse), + streamFormat (streamFormatToUse), + bitDepth (bitDepthToUse), + outputStream (new OboeStream (outputDeviceId, + oboe::Direction::Output, + oboe::SharingMode::Exclusive, + numOutputChannels, + streamFormatToUse, + sampleRateToUse, + bufferSizeToUse, + this)) + { + if (numInputChannels > 0) + { + inputStream.reset (new OboeStream (inputDeviceId, + oboe::Direction::Input, + oboe::SharingMode::Exclusive, + numInputChannels, + streamFormatToUse, + sampleRateToUse, + bufferSizeToUse, + nullptr)); + + if (inputStream->openedOk() && outputStream->openedOk()) + { + // Input & output sample rates should match! + jassert (inputStream->getNativeStream()->getSampleRate() + == outputStream->getNativeStream()->getSampleRate()); + } + + checkStreamSetup (inputStream.get(), inputDeviceId, numInputChannels, + sampleRate, bufferSize, streamFormat); + } + + checkStreamSetup (outputStream.get(), outputDeviceId, numOutputChannels, + sampleRate, bufferSize, streamFormat); + } + + // Not strictly required as these should not change, but recommended by Google anyway + void checkStreamSetup (OboeStream* stream, int deviceId, int numChannels, int sampleRate, + int bufferSize, oboe::AudioFormat format) + { + auto* nativeStream = stream != nullptr ? stream->getNativeStream() : nullptr; + + if (nativeStream != nullptr) + { + ignoreUnused (deviceId, numChannels, sampleRate, bufferSize); + ignoreUnused (streamFormat, bitDepth); + + jassert (deviceId = nativeStream->getDeviceId()); + jassert (numChannels = nativeStream->getChannelCount()); + jassert (sampleRate == nativeStream->getSampleRate()); + jassert (format == nativeStream->getFormat()); + + if (nativeStream->usesAAudio()) + jassert (bufferSize == nativeStream->getBufferSizeInFrames()); + } + } + + int getBufferCapacityInFrames (bool forInput) const + { + auto& ptr = forInput ? inputStream : outputStream; + + if (ptr == nullptr || ! ptr->openedOk()) + return 0; + + return ptr->getNativeStream()->getBufferCapacityInFrames(); + } + + OboeAudioIODevice& owner; + int inputDeviceId, outputDeviceId; + int numInputChannels, numOutputChannels; + int sampleRate; + int bufferSize; + oboe::AudioFormat streamFormat; + int bitDepth; + + std::unique_ptr inputStream, outputStream; + }; + + //============================================================================== + template + class OboeSessionImpl : public OboeSessionBase + { + public: + OboeSessionImpl (OboeAudioIODevice& ownerToUse, + int inputDeviceId, int outputDeviceId, + int numInputChannelsToUse, int numOutputChannelsToUse, + int sampleRateToUse, int bufferSizeToUse) + : OboeSessionBase (ownerToUse, + inputDeviceId, outputDeviceId, + numInputChannelsToUse, numOutputChannelsToUse, + sampleRateToUse, bufferSizeToUse, + OboeAudioIODeviceBufferHelpers::oboeAudioFormat(), + OboeAudioIODeviceBufferHelpers::bitDepth()), + inputStreamNativeBuffer (static_cast (numInputChannelsToUse * getBufferCapacityInFrames (true))), + inputStreamSampleBuffer (numInputChannels, getBufferCapacityInFrames (true)), + outputStreamSampleBuffer (numOutputChannels, getBufferCapacityInFrames (false)) + { + } + + void start() override + { + audioCallbackGuard.set (0); + + if (inputStream != nullptr) + inputStream->start(); + + outputStream->start(); + + checkIsOutputLatencyDetectionSupported(); + } + + void stop() override + { + while (! audioCallbackGuard.compareAndSetBool (1, 0)) + Thread::sleep (1); + + inputStream = nullptr; + outputStream = nullptr; + + audioCallbackGuard.set (0); + } + + int getOutputLatencyInSamples() override { return outputLatency; } + int getInputLatencyInSamples() override { return -1; } + + private: + void checkIsOutputLatencyDetectionSupported() + { + if (! openedOk()) + { + isOutputLatencyDetectionSupported = false; + return; + } + + auto result = outputStream->getNativeStream()->getTimestamp (CLOCK_MONOTONIC, 0, 0); + isOutputLatencyDetectionSupported = result != oboe::Result::ErrorUnimplemented; + } + + oboe::DataCallbackResult onAudioReady (oboe::AudioStream* stream, void* audioData, int32_t numFrames) override + { + attachAndroidJNI(); + + if (audioCallbackGuard.compareAndSetBool (1, 0)) + { + if (stream == nullptr) + return oboe::DataCallbackResult::Stop; + + // only output stream should be the master stream receiving callbacks + jassert (stream->getDirection() == oboe::Direction::Output && stream == outputStream->getNativeStream()); + + //----------------- + // Read input from Oboe + inputStreamSampleBuffer.clear(); + inputStreamNativeBuffer.calloc (static_cast (numInputChannels * bufferSize)); + + if (inputStream != nullptr) + { + auto* nativeInputStream = inputStream->getNativeStream(); + + if (nativeInputStream->getFormat() != oboe::AudioFormat::I16 && nativeInputStream->getFormat() != oboe::AudioFormat::Float) + { + JUCE_OBOE_LOG ("Unsupported input stream audio format: " + String (oboe::convertToText (nativeInputStream->getFormat()))); + jassertfalse; + return oboe::DataCallbackResult::Continue; + } + + auto result = inputStream->getNativeStream()->read (inputStreamNativeBuffer.getData(), numFrames, 0); + + if (result >= 0) + { + OboeAudioIODeviceBufferHelpers::referAudioBufferDirectlyToOboeIfPossible (inputStreamNativeBuffer.get(), + inputStreamSampleBuffer, + result); + + OboeAudioIODeviceBufferHelpers::convertFromOboe (inputStreamNativeBuffer.get(), inputStreamSampleBuffer, result); + } + else + { + // Failed to read from input stream. + jassertfalse; + } + } + + //----------------- + // Setup output buffer + outputStreamSampleBuffer.clear(); + + OboeAudioIODeviceBufferHelpers::referAudioBufferDirectlyToOboeIfPossible (static_cast (audioData), + outputStreamSampleBuffer, + numFrames); + + //----------------- + // Process + // NB: the number of samples read from the input can potentially differ from numFrames. + owner.process (inputStreamSampleBuffer.getArrayOfReadPointers(), numInputChannels, + outputStreamSampleBuffer.getArrayOfWritePointers(), numOutputChannels, + numFrames); + + //----------------- + // Write output to Oboe + OboeAudioIODeviceBufferHelpers::convertToOboe (outputStreamSampleBuffer, static_cast (audioData), numFrames); + + if (isOutputLatencyDetectionSupported) + calculateOutputLatency(); + + audioCallbackGuard.set (0); + } + + return oboe::DataCallbackResult::Continue; + } + + void printStreamDebugInfo (oboe::AudioStream* stream) + { + ignoreUnused (stream); + + JUCE_OBOE_LOG ("\nUses AAudio = " + (stream != nullptr ? String ((int) stream->usesAAudio()) : String ("?")) + + "\nDirection = " + (stream != nullptr ? String (oboe::convertToText (stream->getDirection())) : String ("?")) + + "\nSharingMode = " + (stream != nullptr ? String (oboe::convertToText (stream->getSharingMode())) : String ("?")) + + "\nChannelCount = " + (stream != nullptr ? String (stream->getChannelCount()) : String ("?")) + + "\nFormat = " + (stream != nullptr ? String (oboe::convertToText (stream->getFormat())) : String ("?")) + + "\nSampleRate = " + (stream != nullptr ? String (stream->getSampleRate()) : String ("?")) + + "\nBufferSizeInFrames = " + (stream != nullptr ? String (stream->getBufferSizeInFrames()) : String ("?")) + + "\nBufferCapacityInFrames = " + (stream != nullptr ? String (stream->getBufferCapacityInFrames()) : String ("?")) + + "\nFramesPerBurst = " + (stream != nullptr ? String (stream->getFramesPerBurst()) : String ("?")) + + "\nFramesPerCallback = " + (stream != nullptr ? String (stream->getFramesPerCallback()) : String ("?")) + + "\nBytesPerFrame = " + (stream != nullptr ? String (stream->getBytesPerFrame()) : String ("?")) + + "\nBytesPerSample = " + (stream != nullptr ? String (stream->getBytesPerSample()) : String ("?")) + + "\nPerformanceMode = " + String (oboe::convertToText (oboe::PerformanceMode::LowLatency)) + + "\ngetDeviceId = " + (stream != nullptr ? String (stream->getDeviceId()) : String ("?"))); + } + + void calculateOutputLatency() + { + // Sadly, Oboe uses non-portable int64_t (a.k.a. long on LP64 and long long on LLP64) + int64_t lastWrittenAndPresentedFrameIndex = 0; + int64_t lastFramePresentationTimeNanos = 0; + + auto result = outputStream->getNativeStream()->getTimestamp (CLOCK_MONOTONIC, + &lastWrittenAndPresentedFrameIndex, + &lastFramePresentationTimeNanos); + + if (result != oboe::Result::OK) + return; + + int64_t currentNumFramesWritten = outputStream->getNativeStream()->getFramesWritten(); + int64_t framesDelta = currentNumFramesWritten - lastWrittenAndPresentedFrameIndex; + int64_t timeDeltaNanos = framesDelta * oboe::kNanosPerSecond / sampleRate; + + int64_t nextPresentationTimeNanos = lastFramePresentationTimeNanos + timeDeltaNanos; + int64_t nextFrameWriteTimeNanos = getCurrentTimeNanos(); + + if (nextFrameWriteTimeNanos < 0) + return; + + outputLatency = (int) ((nextPresentationTimeNanos - nextFrameWriteTimeNanos) * sampleRate / oboe::kNanosPerSecond); + } + + int64_t getCurrentTimeNanos() + { + timespec time; + + if (clock_gettime (CLOCK_MONOTONIC, &time) < 0) + return -1; + + return time.tv_sec * oboe::kNanosPerSecond + time.tv_nsec; + } + + void onErrorBeforeClose (oboe::AudioStream* stream, oboe::Result error) override + { + // only output stream should be the master stream receiving callbacks + jassert (stream->getDirection() == oboe::Direction::Output); + + JUCE_OBOE_LOG ("Oboe stream onErrorBeforeClose(): " + String (oboe::convertToText (error))); + printStreamDebugInfo (stream); + } + + void onErrorAfterClose (oboe::AudioStream* stream, oboe::Result error) override + { + // only output stream should be the master stream receiving callbacks + jassert (stream->getDirection() == oboe::Direction::Output); + + JUCE_OBOE_LOG ("Oboe stream onErrorAfterClose(): " + String (oboe::convertToText (error))); + + if (error == oboe::Result::ErrorDisconnected) + { + if (streamRestartGuard.compareAndSetBool (1, 0)) + { + // Close, recreate, and start the stream, not much use in current one. + // Use default device id, to let the OS pick the best ID (since our was disconnected). + + while (! audioCallbackGuard.compareAndSetBool (1, 0)) + Thread::sleep (1); + + outputStream = nullptr; + outputStream.reset (new OboeStream (-1, + oboe::Direction::Output, + oboe::SharingMode::Exclusive, + numOutputChannels, + streamFormat, + sampleRate, + bufferSize, + this)); + + outputStream->start(); + + audioCallbackGuard.set (0); + streamRestartGuard.set (0); + } + } + } + + HeapBlock inputStreamNativeBuffer; + AudioBuffer inputStreamSampleBuffer, + outputStreamSampleBuffer; + Atomic audioCallbackGuard { 0 }, + streamRestartGuard { 0 }; + + bool isOutputLatencyDetectionSupported = true; + int outputLatency = -1; + }; + + //============================================================================== + friend class OboeAudioIODeviceType; + friend class OboeRealtimeThread; + + //============================================================================== + int actualBufferSize = 0, sampleRate = 0; + bool deviceOpen = false; + String lastError; + BigInteger activeOutputChans, activeInputChans; + Atomic callback { nullptr }; + + int inputDeviceId; + Array supportedInputSampleRates; + int maxNumInputChannels; + int outputDeviceId; + Array supportedOutputSampleRates; + int maxNumOutputChannels; + + std::unique_ptr session; + + bool running = false; + + enum + { + // These at the moment correspond to OpenSL settings. + bufferSizeMultForLowLatency = 4, + bufferSizeMultForSlowAudio = 8, + 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 {}; + } + + 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() + { + auto val = audioManagerGetProperty ("android.media.property.OUTPUT_FRAMES_PER_BUFFER").getIntValue(); + return val > 0 ? val : 512; + } + + static bool isProAudioDevice() + { + return androidHasSystemFeature ("android.hardware.audio.pro"); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OboeAudioIODevice) +}; + +//============================================================================== +OboeAudioIODevice::OboeSessionBase* OboeAudioIODevice::OboeSessionBase::create (OboeAudioIODevice& owner, + int inputDeviceId, + int outputDeviceId, + int numInputChannels, + int numOutputChannels, + int sampleRate, + int bufferSize) +{ + + std::unique_ptr session; + auto sdkVersion = getEnv()->GetStaticIntField (AndroidBuildVersion, AndroidBuildVersion.SDK_INT); + + // SDK versions 21 and higher should natively support floating point... + if (sdkVersion >= 21) + { + session.reset (new OboeSessionImpl (owner, inputDeviceId, outputDeviceId, + numInputChannels, numOutputChannels, sampleRate, bufferSize)); + + // ...however, some devices lie so re-try without floating point + if (session != nullptr && (! session->openedOk())) + session.reset(); + } + + if (session == nullptr) + { + session.reset (new OboeSessionImpl (owner, inputDeviceId, outputDeviceId, + numInputChannels, numOutputChannels, sampleRate, bufferSize)); + + if (session != nullptr && (! session->openedOk())) + session.reset(); + } + + return session.release(); +} + +//============================================================================== +class OboeAudioIODeviceType : public AudioIODeviceType +{ +public: + OboeAudioIODeviceType() + : AudioIODeviceType (OboeAudioIODevice::oboeTypeName) + { + // Not using scanForDevices() to maintain behaviour backwards compatible with older APIs + checkAvailableDevices(); + } + + //============================================================================== + void scanForDevices() override {} + + StringArray getDeviceNames (bool wantInputNames) const override + { + if (inputDevices.isEmpty() && outputDevices.isEmpty()) + return StringArray (OboeAudioIODevice::oboeTypeName); + + StringArray names; + + for (auto& device : wantInputNames ? inputDevices : outputDevices) + names.add (device.name); + + return names; + } + + int getDefaultDeviceIndex (bool forInput) const override + { + // No need to create a stream when only one default device is created. + if (! supportsDevicesInfo()) + return 0; + + if (forInput && (! RuntimePermissions::isGranted (RuntimePermissions::recordAudio))) + return 0; + + // Create stream with a default device ID and query the stream for its device ID + using OboeStream = OboeAudioIODevice::OboeStream; + + OboeStream tempStream (-1, + forInput ? oboe::Direction::Input : oboe::Direction::Output, + oboe::SharingMode::Shared, + forInput ? 1 : 2, + getSdkVersion() >= 21 ? oboe::AudioFormat::Float : oboe::AudioFormat::I16, + (int) OboeAudioIODevice::getNativeSampleRate(), + OboeAudioIODevice::getNativeBufferSize(), + nullptr); + + if (auto* nativeStream = tempStream.getNativeStream()) + { + auto& devices = forInput ? inputDevices : outputDevices; + + for (int i = 0; i < devices.size(); ++i) + if (devices.getReference (i).id == nativeStream->getDeviceId()) + return i; + } + + return 0; + } + + int getIndexOfDevice (AudioIODevice* device, bool asInput) const override + { + if (device == nullptr) + return -1; + + auto* oboeDevice = static_cast (device); + auto oboeDeviceId = asInput ? oboeDevice->inputDeviceId + : oboeDevice->outputDeviceId; + + auto& devices = asInput ? inputDevices : outputDevices; + + for (int i = 0; i < devices.size(); ++i) + if (devices.getReference (i).id == oboeDeviceId) + return i; + + return -1; + } + + bool hasSeparateInputsAndOutputs() const override { return true; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) override + { + auto outputDeviceInfo = getDeviceInfoForName (outputDeviceName, false); + auto inputDeviceInfo = getDeviceInfoForName (inputDeviceName, true); + + if (outputDeviceInfo.name.isEmpty() && inputDeviceInfo.name.isEmpty()) + { + // Invalid device name passed. It must be one of the names returned by getDeviceNames(). + jassertfalse; + return nullptr; + } + + auto& name = outputDeviceInfo.name.isNotEmpty() ? outputDeviceInfo.name + : inputDeviceInfo.name; + + return new OboeAudioIODevice (name, + inputDeviceInfo.id, inputDeviceInfo.sampleRates, + inputDeviceInfo.numChannels, + outputDeviceInfo.id, outputDeviceInfo.sampleRates, + outputDeviceInfo.numChannels); + } + + static bool isOboeAvailable() + { + #if JUCE_USE_ANDROID_OBOE + return true; + #else + return false; + #endif + } + + private: + void checkAvailableDevices() + { + if (! supportsDevicesInfo()) + { + auto sampleRates = OboeAudioIODevice::getDefaultSampleRates(); + + inputDevices .add ({ OboeAudioIODevice::oboeTypeName, -1, sampleRates, 1 }); + outputDevices.add ({ OboeAudioIODevice::oboeTypeName, -1, sampleRates, 2 }); + + return; + } + + auto* env = getEnv(); + + jclass audioManagerClass = env->FindClass ("android/media/AudioManager"); + + // We should be really entering here only if API supports it. + jassert (audioManagerClass != 0); + + if (audioManagerClass == 0) + return; + + auto audioManager = LocalRef (env->CallObjectMethod (android.activity, + JuceAppActivity.getSystemService, + javaString ("audio").get())); + + static jmethodID getDevicesMethod = env->GetMethodID (audioManagerClass, "getDevices", + "(I)[Landroid/media/AudioDeviceInfo;"); + + static constexpr int allDevices = 3; + auto devices = LocalRef ((jobjectArray) env->CallObjectMethod (audioManager, + getDevicesMethod, + allDevices)); + + const int numDevices = env->GetArrayLength (devices.get()); + + for (int i = 0; i < numDevices; ++i) + { + auto device = LocalRef ((jobject) env->GetObjectArrayElement (devices.get(), i)); + addDevice (device, env); + } + + JUCE_OBOE_LOG ("-----InputDevices:"); + + for (auto& device : inputDevices) + { + JUCE_OBOE_LOG ("name = " << device.name); + JUCE_OBOE_LOG ("id = " << String (device.id)); + JUCE_OBOE_LOG ("sample rates size = " << String (device.sampleRates.size())); + JUCE_OBOE_LOG ("num channels = " + String (device.numChannels)); + } + + JUCE_OBOE_LOG ("-----OutputDevices:"); + + for (auto& device : outputDevices) + { + JUCE_OBOE_LOG ("name = " << device.name); + JUCE_OBOE_LOG ("id = " << String (device.id)); + JUCE_OBOE_LOG ("sample rates size = " << String (device.sampleRates.size())); + JUCE_OBOE_LOG ("num channels = " + String (device.numChannels)); + } + } + + bool supportsDevicesInfo() const + { + static auto result = getSdkVersion() >= 23; + return result; + } + + int getSdkVersion() const + { + static auto sdkVersion = getEnv()->GetStaticIntField (AndroidBuildVersion, AndroidBuildVersion.SDK_INT); + return sdkVersion; + } + + void addDevice (const LocalRef& device, JNIEnv* env) + { + auto deviceClass = LocalRef ((jclass) env->FindClass ("android/media/AudioDeviceInfo")); + + jmethodID getProductNameMethod = env->GetMethodID (deviceClass, "getProductName", + "()Ljava/lang/CharSequence;"); + + jmethodID getTypeMethod = env->GetMethodID (deviceClass, "getType", "()I"); + jmethodID getIdMethod = env->GetMethodID (deviceClass, "getId", "()I"); + jmethodID getSampleRatesMethod = env->GetMethodID (deviceClass, "getSampleRates", "()[I"); + jmethodID getChannelCountsMethod = env->GetMethodID (deviceClass, "getChannelCounts", "()[I"); + jmethodID isSourceMethod = env->GetMethodID (deviceClass, "isSource", "()Z"); + + auto name = juceString ((jstring) env->CallObjectMethod (device, getProductNameMethod)); + name << deviceTypeToString (env->CallIntMethod (device, getTypeMethod)); + int id = env->CallIntMethod (device, getIdMethod); + + auto jSampleRates = LocalRef ((jintArray) env->CallObjectMethod (device, getSampleRatesMethod)); + auto sampleRates = jintArrayToJuceArray (jSampleRates); + + auto jChannelCounts = LocalRef ((jintArray) env->CallObjectMethod (device, getChannelCountsMethod)); + auto channelCounts = jintArrayToJuceArray (jChannelCounts); + int numChannels = channelCounts.isEmpty() ? -1 : channelCounts.getLast(); + + bool isInput = env->CallBooleanMethod (device, isSourceMethod); + auto& devices = isInput ? inputDevices : outputDevices; + + devices.add ({ name, id, sampleRates, numChannels }); + } + + static const char* deviceTypeToString (int type) + { + switch (type) + { + case 0: return ""; + case 1: return " built-in earphone speaker"; + case 2: return " built-in speaker"; + case 3: return " wired headset"; + case 4: return " wired headphones"; + case 5: return " line analog"; + case 6: return " line digital"; + case 7: return " Bluetooth device typically used for telephony"; + case 8: return " Bluetooth device supporting the A2DP profile"; + case 9: return " HDMI"; + case 10: return " HDMI audio return channel"; + case 11: return " USB device"; + case 12: return " USB accessory"; + case 13: return " DOCK"; + case 14: return " FM"; + case 15: return " built-in microphone"; + case 16: return " FM tuner"; + case 17: return " TV tuner"; + case 18: return " telephony"; + case 19: return " auxiliary line-level connectors"; + case 20: return " IP"; + case 21: return " BUS"; + case 22: return " USB headset"; + default: jassertfalse; return ""; // type not supported yet, needs to be added! + } + } + + static Array jintArrayToJuceArray (const LocalRef& jArray) + { + auto* env = getEnv(); + + jint* jArrayElems = env->GetIntArrayElements (jArray, 0); + int numElems = env->GetArrayLength (jArray); + + Array juceArray; + + for (int s = 0; s < numElems; ++s) + juceArray.add (jArrayElems[s]); + + env->ReleaseIntArrayElements (jArray, jArrayElems, 0); + return juceArray; + } + + struct DeviceInfo + { + String name; + int id; + Array sampleRates; + int numChannels; + }; + + DeviceInfo getDeviceInfoForName (const String& name, bool isInput) + { + if (name.isEmpty()) + return {}; + + for (auto& device : isInput ? inputDevices : outputDevices) + if (device.name == name) + return device; + + return {}; + } + + Array inputDevices, outputDevices; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OboeAudioIODeviceType) +}; + +const char* const OboeAudioIODevice::oboeTypeName = "Android Oboe"; + + +//============================================================================== +bool isOboeAvailable() { return OboeAudioIODeviceType::isOboeAvailable(); } + +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Oboe() +{ + return isOboeAvailable() ? new OboeAudioIODeviceType() : nullptr; +} + +//============================================================================== +class OboeRealtimeThread : private oboe::AudioStreamCallback +{ + using OboeStream = OboeAudioIODevice::OboeStream; + +public: + OboeRealtimeThread() + : testStream (new OboeStream (-1, + oboe::Direction::Output, + oboe::SharingMode::Exclusive, + 1, + oboe::AudioFormat::Float, + (int) OboeAudioIODevice::getNativeSampleRate(), + OboeAudioIODevice::getNativeBufferSize(), + this)), + formatUsed (oboe::AudioFormat::Float) + { + // Fallback to I16 stream format if Float has not worked + if (! testStream->openedOk()) + { + testStream.reset (new OboeStream (-1, + oboe::Direction::Output, + oboe::SharingMode::Exclusive, + 1, + oboe::AudioFormat::I16, + (int) OboeAudioIODevice::getNativeSampleRate(), + OboeAudioIODevice::getNativeBufferSize(), + this)); + + formatUsed = oboe::AudioFormat::I16; + } + + parentThreadID = pthread_self(); + + pthread_cond_init (&threadReady, nullptr); + pthread_mutex_init (&threadReadyMutex, nullptr); + } + + bool isOk() const + { + return testStream != nullptr && testStream->openedOk(); + } + + pthread_t startThread (void* (*entry) (void*), void* userPtr) + { + pthread_mutex_lock (&threadReadyMutex); + + threadEntryProc = entry; + threadUserPtr = userPtr; + + testStream->start(); + + pthread_cond_wait (&threadReady, &threadReadyMutex); + pthread_mutex_unlock (&threadReadyMutex); + + return realtimeThreadID; + } + + oboe::DataCallbackResult onAudioReady (oboe::AudioStream*, void*, int32_t) override + { + // When running with OpenSL, the first callback will come on the parent thread. + if (threadEntryProc != nullptr && ! pthread_equal (parentThreadID, pthread_self())) + { + pthread_mutex_lock (&threadReadyMutex); + + realtimeThreadID = pthread_self(); + + pthread_cond_signal (&threadReady); + pthread_mutex_unlock (&threadReadyMutex); + + threadEntryProc (threadUserPtr); + threadEntryProc = nullptr; + + MessageManager::callAsync ([this] () { delete this; }); + + return oboe::DataCallbackResult::Stop; + } + + return oboe::DataCallbackResult::Continue; + } + + void onErrorBeforeClose (oboe::AudioStream*, oboe::Result error) override + { + JUCE_OBOE_LOG ("OboeRealtimeThread: Oboe stream onErrorBeforeClose(): " + String (oboe::convertToText (error))); + } + + void onErrorAfterClose (oboe::AudioStream* stream, oboe::Result error) override + { + JUCE_OBOE_LOG ("OboeRealtimeThread: Oboe stream onErrorAfterClose(): " + String (oboe::convertToText (error))); + + if (error == oboe::Result::ErrorDisconnected) + { + testStream.reset(); + testStream.reset (new OboeStream (-1, + oboe::Direction::Output, + oboe::SharingMode::Exclusive, + 1, + formatUsed, + (int) OboeAudioIODevice::getNativeSampleRate(), + OboeAudioIODevice::getNativeBufferSize(), + this)); + testStream->start(); + } + } + +private: + //============================================================================= + void* (*threadEntryProc) (void*) = nullptr; + void* threadUserPtr = nullptr; + + pthread_cond_t threadReady; + pthread_mutex_t threadReadyMutex; + pthread_t parentThreadID, realtimeThreadID; + + std::unique_ptr testStream; + oboe::AudioFormat formatUsed; +}; + +pthread_t juce_createRealtimeAudioThread (void* (*entry) (void*), void* userPtr) +{ + std::unique_ptr thread (new OboeRealtimeThread()); + + if (! thread->isOk()) + return {}; + + auto threadID = thread->startThread (entry, userPtr); + + // the thread will de-allocate itself + thread.release(); + + return threadID; +} + +} // namespace juce diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index af9a942b36..1b050b9440 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -570,7 +570,8 @@ DECLARE_JNI_CLASS (JavaHashMap, "java/util/HashMap"); #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - STATICMETHOD (valueOf, "valueOf", "(I)Ljava/lang/Integer;") + STATICMETHOD (parseInt, "parseInt", "(Ljava/lang/String;I)I") \ + STATICMETHOD (valueOf, "valueOf", "(I)Ljava/lang/Integer;") DECLARE_JNI_CLASS (JavaInteger, "java/lang/Integer"); #undef JNI_CLASS_MEMBERS diff --git a/modules/juce_core/native/juce_posix_SharedCode.h b/modules/juce_core/native/juce_posix_SharedCode.h index 00c25ad164..7d4b91acf6 100644 --- a/modules/juce_core/native/juce_posix_SharedCode.h +++ b/modules/juce_core/native/juce_posix_SharedCode.h @@ -929,8 +929,11 @@ extern "C" void* threadEntryProc (void* userData) return nullptr; } -#if JUCE_ANDROID && JUCE_MODULE_AVAILABLE_juce_audio_devices && (JUCE_USE_ANDROID_OPENSLES || (! defined(JUCE_USE_ANDROID_OPENSLES) && JUCE_ANDROID_API_VERSION > 8)) -#define JUCE_ANDROID_REALTIME_THREAD_AVAILABLE 1 +#if JUCE_ANDROID && JUCE_MODULE_AVAILABLE_juce_audio_devices && \ + ((JUCE_USE_ANDROID_OPENSLES || (! defined(JUCE_USE_ANDROID_OPENSLES) && JUCE_ANDROID_API_VERSION > 8)) \ + || (JUCE_USE_ANDROID_OBOE || (! defined(JUCE_USE_ANDROID_OBOE) && JUCE_ANDROID_API_VERSION > 15))) + + #define JUCE_ANDROID_REALTIME_THREAD_AVAILABLE 1 #endif #if JUCE_ANDROID_REALTIME_THREAD_AVAILABLE diff --git a/modules/juce_core/threads/juce_Thread.h b/modules/juce_core/threads/juce_Thread.h index f759a3ee18..fc5e426d88 100644 --- a/modules/juce_core/threads/juce_Thread.h +++ b/modules/juce_core/threads/juce_Thread.h @@ -200,9 +200,9 @@ public: for realtime audio processing. Currently, this priority is identical to priority 9, except when building - for Android with OpenSL support. + for Android with OpenSL/Oboe support. - In this case, JUCE will ask OpenSL to construct a super high priority thread + In this case, JUCE will ask OpenSL/Oboe to construct a super high priority thread specifically for realtime audio processing. Note that this priority can only be set **before** the thread has @@ -210,7 +210,7 @@ public: priority, is not supported under Android and will assert. For best performance this thread should yield at regular intervals - and not call any blocking APIS. + and not call any blocking APIs. @see startThread, setPriority, sleep, WaitableEvent */