| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qopenslesengine.h" |
| |
| #include "qopenslesaudioinput.h" |
| #include <qdebug.h> |
| |
| #ifdef ANDROID |
| #include <SLES/OpenSLES_Android.h> |
| #include <QtCore/private/qjnihelpers_p.h> |
| #include <QtCore/private/qjni_p.h> |
| #endif |
| |
| #define MINIMUM_PERIOD_TIME_MS 5 |
| #define DEFAULT_PERIOD_TIME_MS 50 |
| |
| #define CheckError(message) if (result != SL_RESULT_SUCCESS) { qWarning(message); return; } |
| |
| Q_GLOBAL_STATIC(QOpenSLESEngine, openslesEngine); |
| |
| QOpenSLESEngine::QOpenSLESEngine() |
| : m_engineObject(0) |
| , m_engine(0) |
| , m_checkedInputFormats(false) |
| { |
| SLresult result; |
| |
| result = slCreateEngine(&m_engineObject, 0, 0, 0, 0, 0); |
| CheckError("Failed to create engine"); |
| |
| result = (*m_engineObject)->Realize(m_engineObject, SL_BOOLEAN_FALSE); |
| CheckError("Failed to realize engine"); |
| |
| result = (*m_engineObject)->GetInterface(m_engineObject, SL_IID_ENGINE, &m_engine); |
| CheckError("Failed to get engine interface"); |
| } |
| |
| QOpenSLESEngine::~QOpenSLESEngine() |
| { |
| if (m_engineObject) |
| (*m_engineObject)->Destroy(m_engineObject); |
| } |
| |
| QOpenSLESEngine *QOpenSLESEngine::instance() |
| { |
| return openslesEngine(); |
| } |
| |
| SLDataFormat_PCM QOpenSLESEngine::audioFormatToSLFormatPCM(const QAudioFormat &format) |
| { |
| SLDataFormat_PCM format_pcm; |
| format_pcm.formatType = SL_DATAFORMAT_PCM; |
| format_pcm.numChannels = format.channelCount(); |
| format_pcm.samplesPerSec = format.sampleRate() * 1000; |
| format_pcm.bitsPerSample = format.sampleSize(); |
| format_pcm.containerSize = format.sampleSize(); |
| format_pcm.channelMask = (format.channelCount() == 1 ? |
| SL_SPEAKER_FRONT_CENTER : |
| SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT); |
| format_pcm.endianness = (format.byteOrder() == QAudioFormat::LittleEndian ? |
| SL_BYTEORDER_LITTLEENDIAN : |
| SL_BYTEORDER_BIGENDIAN); |
| return format_pcm; |
| |
| } |
| |
| QByteArray QOpenSLESEngine::defaultDevice(QAudio::Mode mode) const |
| { |
| const auto &devices = availableDevices(mode); |
| return !devices.isEmpty() ? devices.first() : QByteArray(); |
| } |
| |
| QList<QByteArray> QOpenSLESEngine::availableDevices(QAudio::Mode mode) const |
| { |
| QList<QByteArray> devices; |
| if (mode == QAudio::AudioInput) { |
| #ifdef ANDROID |
| devices << QT_ANDROID_PRESET_MIC |
| << QT_ANDROID_PRESET_CAMCORDER |
| << QT_ANDROID_PRESET_VOICE_RECOGNITION |
| << QT_ANDROID_PRESET_VOICE_COMMUNICATION; |
| #else |
| devices << "default"; |
| #endif |
| } else { |
| devices << "default"; |
| } |
| return devices; |
| } |
| |
| QList<int> QOpenSLESEngine::supportedChannelCounts(QAudio::Mode mode) const |
| { |
| if (mode == QAudio::AudioInput) { |
| if (!m_checkedInputFormats) |
| const_cast<QOpenSLESEngine *>(this)->checkSupportedInputFormats(); |
| return m_supportedInputChannelCounts; |
| } else { |
| return QList<int>() << 1 << 2; |
| } |
| } |
| |
| QList<int> QOpenSLESEngine::supportedSampleRates(QAudio::Mode mode) const |
| { |
| if (mode == QAudio::AudioInput) { |
| if (!m_checkedInputFormats) |
| const_cast<QOpenSLESEngine *>(this)->checkSupportedInputFormats(); |
| return m_supportedInputSampleRates; |
| } else { |
| return QList<int>() << 8000 << 11025 << 12000 << 16000 << 22050 |
| << 24000 << 32000 << 44100 << 48000; |
| } |
| } |
| |
| int QOpenSLESEngine::getOutputValue(QOpenSLESEngine::OutputValue type, int defaultValue) |
| { |
| #if defined(Q_OS_ANDROID) |
| static int sampleRate = 0; |
| static int framesPerBuffer = 0; |
| static const int sdkVersion = QtAndroidPrivate::androidSdkVersion(); |
| |
| if (sdkVersion < 17) // getProperty() was added in API level 17... |
| return defaultValue; |
| |
| if (type == FramesPerBuffer && framesPerBuffer != 0) |
| return framesPerBuffer; |
| |
| if (type == SampleRate && sampleRate != 0) |
| return sampleRate; |
| |
| QJNIObjectPrivate ctx(QtAndroidPrivate::activity()); |
| if (!ctx.isValid()) |
| return defaultValue; |
| |
| |
| QJNIObjectPrivate audioServiceString = ctx.getStaticObjectField("android/content/Context", |
| "AUDIO_SERVICE", |
| "Ljava/lang/String;"); |
| QJNIObjectPrivate am = ctx.callObjectMethod("getSystemService", |
| "(Ljava/lang/String;)Ljava/lang/Object;", |
| audioServiceString.object()); |
| if (!am.isValid()) |
| return defaultValue; |
| |
| QJNIObjectPrivate sampleRateField = QJNIObjectPrivate::getStaticObjectField("android/media/AudioManager", |
| "PROPERTY_OUTPUT_SAMPLE_RATE", |
| "Ljava/lang/String;"); |
| QJNIObjectPrivate framesPerBufferField = QJNIObjectPrivate::getStaticObjectField("android/media/AudioManager", |
| "PROPERTY_OUTPUT_FRAMES_PER_BUFFER", |
| "Ljava/lang/String;"); |
| |
| QJNIObjectPrivate sampleRateString = am.callObjectMethod("getProperty", |
| "(Ljava/lang/String;)Ljava/lang/String;", |
| sampleRateField.object()); |
| QJNIObjectPrivate framesPerBufferString = am.callObjectMethod("getProperty", |
| "(Ljava/lang/String;)Ljava/lang/String;", |
| framesPerBufferField.object()); |
| |
| if (!sampleRateString.isValid() || !framesPerBufferString.isValid()) |
| return defaultValue; |
| |
| framesPerBuffer = framesPerBufferString.toString().toInt(); |
| sampleRate = sampleRateString.toString().toInt(); |
| |
| if (type == FramesPerBuffer) |
| return framesPerBuffer; |
| |
| if (type == SampleRate) |
| return sampleRate; |
| |
| #endif // Q_OS_ANDROID |
| |
| return defaultValue; |
| } |
| |
| int QOpenSLESEngine::getDefaultBufferSize(const QAudioFormat &format) |
| { |
| #if defined(Q_OS_ANDROID) |
| if (!format.isValid()) |
| return 0; |
| |
| const int channelConfig = [&format]() -> int |
| { |
| if (format.channelCount() == 1) |
| return 4; /* MONO */ |
| else if (format.channelCount() == 2) |
| return 12; /* STEREO */ |
| else if (format.channelCount() > 2) |
| return 1052; /* SURROUND */ |
| else |
| return 1; /* DEFAULT */ |
| }(); |
| |
| const int audioFormat = [&format]() -> int |
| { |
| if (format.sampleType() == QAudioFormat::Float && QtAndroidPrivate::androidSdkVersion() >= 21) |
| return 4; /* PCM_FLOAT */ |
| else if (format.sampleSize() == 8) |
| return 3; /* PCM_8BIT */ |
| else if (format.sampleSize() == 16) |
| return 2; /* PCM_16BIT*/ |
| else |
| return 1; /* DEFAULT */ |
| }(); |
| |
| const int sampleRate = format.sampleRate(); |
| const int minBufferSize = QJNIObjectPrivate::callStaticMethod<jint>("android/media/AudioTrack", |
| "getMinBufferSize", |
| "(III)I", |
| sampleRate, |
| channelConfig, |
| audioFormat); |
| return minBufferSize > 0 ? minBufferSize : format.bytesForDuration(DEFAULT_PERIOD_TIME_MS); |
| #else |
| return format.bytesForDuration(DEFAULT_PERIOD_TIME_MS); |
| #endif // Q_OS_ANDROID |
| } |
| |
| int QOpenSLESEngine::getLowLatencyBufferSize(const QAudioFormat &format) |
| { |
| return format.bytesForFrames(QOpenSLESEngine::getOutputValue(QOpenSLESEngine::FramesPerBuffer, |
| format.framesForDuration(MINIMUM_PERIOD_TIME_MS))); |
| } |
| |
| bool QOpenSLESEngine::supportsLowLatency() |
| { |
| #if defined(Q_OS_ANDROID) |
| static int isSupported = -1; |
| |
| if (isSupported != -1) |
| return (isSupported == 1); |
| |
| QJNIObjectPrivate ctx(QtAndroidPrivate::activity()); |
| if (!ctx.isValid()) |
| return false; |
| |
| QJNIObjectPrivate pm = ctx.callObjectMethod("getPackageManager", "()Landroid/content/pm/PackageManager;"); |
| if (!pm.isValid()) |
| return false; |
| |
| QJNIObjectPrivate audioFeatureField = QJNIObjectPrivate::getStaticObjectField("android/content/pm/PackageManager", |
| "FEATURE_AUDIO_LOW_LATENCY", |
| "Ljava/lang/String;"); |
| if (!audioFeatureField.isValid()) |
| return false; |
| |
| isSupported = pm.callMethod<jboolean>("hasSystemFeature", |
| "(Ljava/lang/String;)Z", |
| audioFeatureField.object()); |
| return (isSupported == 1); |
| #else |
| return true; |
| #endif // Q_OS_ANDROID |
| } |
| |
| bool QOpenSLESEngine::printDebugInfo() |
| { |
| return qEnvironmentVariableIsSet("QT_OPENSL_INFO"); |
| } |
| |
| void QOpenSLESEngine::checkSupportedInputFormats() |
| { |
| m_supportedInputChannelCounts = QList<int>() << 1; |
| m_supportedInputSampleRates.clear(); |
| |
| SLDataFormat_PCM defaultFormat; |
| defaultFormat.formatType = SL_DATAFORMAT_PCM; |
| defaultFormat.numChannels = 1; |
| defaultFormat.samplesPerSec = SL_SAMPLINGRATE_44_1; |
| defaultFormat.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; |
| defaultFormat.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16; |
| defaultFormat.channelMask = SL_SPEAKER_FRONT_CENTER; |
| defaultFormat.endianness = SL_BYTEORDER_LITTLEENDIAN; |
| |
| const SLuint32 rates[9] = { SL_SAMPLINGRATE_8, |
| SL_SAMPLINGRATE_11_025, |
| SL_SAMPLINGRATE_12, |
| SL_SAMPLINGRATE_16, |
| SL_SAMPLINGRATE_22_05, |
| SL_SAMPLINGRATE_24, |
| SL_SAMPLINGRATE_32, |
| SL_SAMPLINGRATE_44_1, |
| SL_SAMPLINGRATE_48 }; |
| |
| |
| // Test sampling rates |
| for (int i = 0 ; i < 9; ++i) { |
| SLDataFormat_PCM format = defaultFormat; |
| format.samplesPerSec = rates[i]; |
| |
| if (inputFormatIsSupported(format)) |
| m_supportedInputSampleRates.append(rates[i] / 1000); |
| |
| } |
| |
| // Test if stereo is supported |
| { |
| SLDataFormat_PCM format = defaultFormat; |
| format.numChannels = 2; |
| format.channelMask = 0; |
| if (inputFormatIsSupported(format)) |
| m_supportedInputChannelCounts.append(2); |
| } |
| |
| m_checkedInputFormats = true; |
| } |
| |
| bool QOpenSLESEngine::inputFormatIsSupported(SLDataFormat_PCM format) |
| { |
| SLresult result; |
| SLObjectItf recorder = 0; |
| SLDataLocator_IODevice loc_dev = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, |
| SL_DEFAULTDEVICEID_AUDIOINPUT, NULL }; |
| SLDataSource audioSrc = { &loc_dev, NULL }; |
| |
| #ifdef ANDROID |
| SLDataLocator_AndroidSimpleBufferQueue loc_bq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 }; |
| #else |
| SLDataLocator_BufferQueue loc_bq = { SL_DATALOCATOR_BUFFERQUEUE, 1 }; |
| #endif |
| SLDataSink audioSnk = { &loc_bq, &format }; |
| |
| result = (*m_engine)->CreateAudioRecorder(m_engine, &recorder, &audioSrc, &audioSnk, 0, 0, 0); |
| if (result == SL_RESULT_SUCCESS) |
| result = (*recorder)->Realize(recorder, false); |
| |
| if (result == SL_RESULT_SUCCESS) { |
| (*recorder)->Destroy(recorder); |
| return true; |
| } |
| |
| return false; |
| } |