Android: Improve detection of optimal settings for QAudioOutput.

This change will try to detect the optimal buffer size and sample-rate
on Android (requires API level 17+). If the device supports low-latency
playback, then it's recommended that application developers check the
preferred sample-rate through QAudioDeviceInfo::preferredFormat().
On most devices the preferred sample rate seems to be 48 KHz, so this
is now the default instead of 44.1 KHz.

Note that not all devices supports "proper" low-latency playback, and
there are no APIs to retrieve information about how many devices that
can be active at the same time; The only remotely quantitative value
I've found is "a few"...

Change-Id: I0708738b4a31f6bf9e88e9a816679cb688e023f3
Reviewed-by: Yoann Lopes <yoann.lopes@theqtcompany.com>
This commit is contained in:
Christian Strømme
2015-11-17 16:05:09 +01:00
committed by Christian Stromme
parent ddaacc147e
commit 3bd9da9aba
5 changed files with 184 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
TARGET = qtaudio_opensles TARGET = qtaudio_opensles
QT += multimedia-private QT += multimedia-private core-private
PLUGIN_TYPE = audio PLUGIN_TYPE = audio
PLUGIN_CLASS_NAME = QOpenSLESPlugin PLUGIN_CLASS_NAME = QOpenSLESPlugin

View File

@@ -42,13 +42,23 @@
#endif // ANDROID #endif // ANDROID
#define BUFFER_COUNT 2 #define BUFFER_COUNT 2
#define DEFAULT_PERIOD_TIME_MS 50
#define MINIMUM_PERIOD_TIME_MS 5
#define EBASE 2.302585093 #define EBASE 2.302585093
#define LOG10(x) qLn(x)/qreal(EBASE) #define LOG10(x) qLn(x)/qreal(EBASE)
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
static inline void openSlDebugInfo()
{
const QAudioFormat &format = QAudioDeviceInfo::defaultOutputDevice().preferredFormat();
qDebug() << "======= OpenSL ES Device info ======="
<< "\nSupports low-latency playback: " << (QOpenSLESEngine::supportsLowLatency() ? "YES" : "NO")
<< "\nPreferred sample rate: " << QOpenSLESEngine::getOutputValue(QOpenSLESEngine::SampleRate, -1)
<< "\nFrames per buffer: " << QOpenSLESEngine::getOutputValue(QOpenSLESEngine::FramesPerBuffer, -1)
<< "\nPreferred Format: " << format
<< "\nLow-latency buffer size: " << QOpenSLESEngine::getLowLatencyBufferSize(format)
<< "\nDefault buffer size: " << QOpenSLESEngine::getDefaultBufferSize(format);
}
QMap<QString, qint32> QOpenSLESAudioOutput::m_categories; QMap<QString, qint32> QOpenSLESAudioOutput::m_categories;
QOpenSLESAudioOutput::QOpenSLESAudioOutput(const QByteArray &device) QOpenSLESAudioOutput::QOpenSLESAudioOutput(const QByteArray &device)
@@ -531,13 +541,17 @@ bool QOpenSLESAudioOutput::preparePlayer()
setVolume(m_volume); setVolume(m_volume);
const int lowLatencyBufferSize = QOpenSLESEngine::getLowLatencyBufferSize(m_format);
const int defaultBufferSize = QOpenSLESEngine::getDefaultBufferSize(m_format);
// Buffer size // Buffer size
if (m_bufferSize <= 0) { if (m_bufferSize <= 0) {
m_bufferSize = m_format.bytesForDuration(DEFAULT_PERIOD_TIME_MS * 1000); m_bufferSize = defaultBufferSize;
} else { } else if (QOpenSLESEngine::supportsLowLatency()) {
const int minimumBufSize = m_format.bytesForDuration(MINIMUM_PERIOD_TIME_MS * 1000); if (m_bufferSize < lowLatencyBufferSize)
if (m_bufferSize < minimumBufSize) m_bufferSize = lowLatencyBufferSize;
m_bufferSize = minimumBufSize; } else if (m_bufferSize < defaultBufferSize) {
m_bufferSize = defaultBufferSize;
} }
m_periodSize = m_bufferSize; m_periodSize = m_bufferSize;
@@ -598,6 +612,9 @@ void QOpenSLESAudioOutput::stopPlayer()
void QOpenSLESAudioOutput::startPlayer() void QOpenSLESAudioOutput::startPlayer()
{ {
if (QOpenSLESEngine::printDebugInfo())
openSlDebugInfo();
if (SL_RESULT_SUCCESS != (*m_playItf)->SetPlayState(m_playItf, SL_PLAYSTATE_PLAYING)) { if (SL_RESULT_SUCCESS != (*m_playItf)->SetPlayState(m_playItf, SL_PLAYSTATE_PLAYING)) {
setError(QAudio::FatalError); setError(QAudio::FatalError);
destroyPlayer(); destroyPlayer();

View File

@@ -61,7 +61,7 @@ QAudioFormat QOpenSLESDeviceInfo::preferredFormat() const
format.setCodec(QStringLiteral("audio/pcm")); format.setCodec(QStringLiteral("audio/pcm"));
format.setSampleSize(16); format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt); format.setSampleType(QAudioFormat::SignedInt);
format.setSampleRate(44100); format.setSampleRate(QOpenSLESEngine::getOutputValue(QOpenSLESEngine::SampleRate, 48000));
format.setChannelCount(m_mode == QAudio::AudioInput ? 1 : 2); format.setChannelCount(m_mode == QAudio::AudioInput ? 1 : 2);
return format; return format;
} }

View File

@@ -38,8 +38,13 @@
#ifdef ANDROID #ifdef ANDROID
#include <SLES/OpenSLES_Android.h> #include <SLES/OpenSLES_Android.h>
#include <QtCore/private/qjnihelpers_p.h>
#include <QtCore/private/qjni_p.h>
#endif #endif
#define MINIMUM_PERIOD_TIME_MS 5
#define DEFAULT_PERIOD_TIME_MS 50
#define CheckError(message) if (result != SL_RESULT_SUCCESS) { qWarning(message); return; } #define CheckError(message) if (result != SL_RESULT_SUCCESS) { qWarning(message); return; }
Q_GLOBAL_STATIC(QOpenSLESEngine, openslesEngine); Q_GLOBAL_STATIC(QOpenSLESEngine, openslesEngine);
@@ -130,6 +135,151 @@ QList<int> QOpenSLESEngine::supportedSampleRates(QAudio::Mode mode) const
} }
} }
int QOpenSLESEngine::getOutputValue(QOpenSLESEngine::OutputValue type, int defaultValue)
{
#if defined(Q_OS_ANDROID) && !defined(Q_OS_ANDROID_NO_SDK)
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) && !defined(Q_OS_ANDROID_NO_SDK)
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();
return QJNIObjectPrivate::callStaticMethod<jint>("android/media/AudioTrack",
"getMinBufferSize",
"(III)I",
sampleRate,
channelConfig,
audioFormat);
#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) && !defined(Q_OS_ANDROID_NO_SDK)
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() void QOpenSLESEngine::checkSupportedInputFormats()
{ {
m_supportedInputChannelCounts = QList<int>() << 1; m_supportedInputChannelCounts = QList<int>() << 1;

View File

@@ -45,6 +45,8 @@ QT_BEGIN_NAMESPACE
class QOpenSLESEngine class QOpenSLESEngine
{ {
public: public:
enum OutputValue { FramesPerBuffer, SampleRate };
QOpenSLESEngine(); QOpenSLESEngine();
~QOpenSLESEngine(); ~QOpenSLESEngine();
@@ -58,6 +60,12 @@ public:
QList<int> supportedChannelCounts(QAudio::Mode mode) const; QList<int> supportedChannelCounts(QAudio::Mode mode) const;
QList<int> supportedSampleRates(QAudio::Mode mode) const; QList<int> supportedSampleRates(QAudio::Mode mode) const;
static int getOutputValue(OutputValue type, int defaultValue = 0);
static int getDefaultBufferSize(const QAudioFormat &format);
static int getLowLatencyBufferSize(const QAudioFormat &format);
static bool supportsLowLatency();
static bool printDebugInfo();
private: private:
void checkSupportedInputFormats(); void checkSupportedInputFormats();
bool inputFormatIsSupported(SLDataFormat_PCM format); bool inputFormatIsSupported(SLDataFormat_PCM format);