オーディオ レイテンシ

レイテンシとは、信号がシステムを移動するのにかかる時間のことです。オーディオ アプリに関係するレイテンシの種類で一般的なものは、次のとおりです。

  • オーディオ出力レイテンシは、オーディオ サンプルが、アプリによって生成されてから、ヘッドフォン差込口や内蔵スピーカーで再生されるまでの時間です。
  • オーディオ入力レイテンシは、端末のオーディオ入力(マイクなど)によってオーディオ信号が受信されてから、そのオーディオ データがアプリで利用できるようになるまでの時間です。
  • ラウンドトリップ レイテンシは、入力レイテンシ、アプリ処理時間、出力レイテンシの合計です。

  • タッチ レイテンシは、ユーザーが画面をタッチしてから、アプリがタッチイベントを受信するまでの時間です。
  • ウォームアップ レイテンシは、データが最初にバッファ内のキューに登録されたときにオーディオ パイプラインを起動するのにかかる時間です。

このページでは、入出力のレイテンシが低いオーディオ アプリを開発する方法と、ウォームアップ レイテンシを回避する方法について説明します。

レイテンシを測定する

オーディオの入出力レイテンシを単独で測定するのは困難です。これは、最初のサンプルがオーディオパスに送信されたタイミングを正確に把握する必要があるためです(ただし、光のテスト回路とオシロスコープを使用すればそれが可能です)。ラウンドトリップのオーディオ レイテンシを知れば、「オーディオ入力(および出力)レイテンシは、信号処理を伴わないパスのラウンドトリップ オーディオ レイテンシの半分である」という大まかな経験則を使用できます。

ラウンドトリップのオーディオ レイテンシは、端末のモデルと Android のビルドによって大きく異なります。公開されている測定値を見れば、Nexus 端末のラウンドトリップ レイテンシの大まかな目安がわかります。

オーディオ信号を生成し、その信号をリッスンして、送受信間にかかった時間を測定するアプリを作成すれば、ラウンドトリップ オーディオ レイテンシを測定することができます。または、このレイテンシ テスト アプリをインストールすることもできます。このアプリは、Larsen テストを使用してラウンドトリップ レイテンシ テストを実行します。レイテンシ テスト アプリのソースコードを表示することも可能です。

信号処理が最小限であるオーディオパスによって、レイテンシを最小限に抑えることが可能になります。このため、ヘッドセット コネクタを介してテストを実行できるオーディオ ループバック ドングルの使用もお勧めします。

レイテンシを最小限に抑えるためのベスト プラクティス

オーディオ性能を検証する

Android Compatibility Definition Document (CDD)に、互換性のある Android 端末のハードウェアとソフトウェアの要件が列挙されています。全般的な互換性プログラムの詳細については Android Compatibility を、実際の CDD ドキュメントについては CDD をご覧ください。

CDD では、ラウンドトリップ レイテンシは 20 ミリ秒以下と指定されます(ミュージシャンは一般に 10 ミリ秒を必要とします)。これは、20 ミリ秒で実現できる重要なユースケースがあるためです。

現在、実行時に Android 端末のパスでオーディオ レイテンシを特定できる API はありません。ただし、次のハードウェア機能フラグを使用すれば、端末がレイテンシを保証しているかどうかを確認できます。

これらのフラグを報告する基準は、CDD のセクション 5.6 Audio Latency5.10 Professional Audio で定義されています。

Java にこれらの機能があるかどうかを確認する方法は次のとおりです。

Kotlin

val hasLowLatencyFeature: Boolean =
        packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)

val hasProFeature: Boolean =
        packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)

Java

boolean hasLowLatencyFeature =
    getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);

boolean hasProFeature =
    getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO);

オーディオ機能の関係については、android.hardware.audio.low_latency 機能が android.hardware.audio.pro の前提条件となります。端末は android.hardware.audio.low_latency を実装できますが、android.hardware.audio.pro を実装することはできません。また、その逆も不可です。

オーディオ性能に関する想定をしない

レイテンシに関する問題を防ぐには、次の想定に注意してください。

  • モバイル端末で使用するスピーカーとマイクの音響エフェクトは、一般的に良いとは想定しない。スピーカーとマイクはサイズが小さく、音響エフェクトは基本的に悪いので、音質を向上させるために信号処理が追加されます。この信号処理によってレイテンシが発生します。
  • 入力と出力のコールバックが同期されると想定しない。入出力を同時に行うため、各側に別個のバッファキュー完了ハンドラが使用されます。これらのコールバックは、相対順序になっているとは限りません。また、オーディオ クロックが同期されているとも限りません。両側で同じサンプルレートを使用する場合も同様です。アプリは適切なバッファ同期によってデータをバッファする必要があります。
  • 実際のサンプルレートが名目上のサンプルレートと完全に一致すると想定しない。たとえば、名目上のサンプルレートが 48,000 Hz の場合、オーディオ クロックはオペレーティング システムの CLOCK_MONOTONIC とは若干異なるレートで進むのが一般的です。これは、オーディオ クロックとシステム クロックが別の水晶から派生する場合があるためです。
  • 特にエンドポイントが別のパスにある場合は、実際の再生サンプルレートが実際のキャプチャ サンプルレートと完全に一致すると想定しない。たとえば、端末上のマイクから名目上のサンプルレート 48,000 Hz でキャプチャし、USB オーディオを名目上のサンプルレート 48,000 Hz で再生している場合、実際のサンプルレートは互いに若干異なる可能性があります。

オーディオ クロックが独立している可能性がある場合、非同期サンプルレート コンバージョンが必要になります。非同期サンプルレート コンバージョンの(音質に関しては理想的ではないものの)シンプルな方法は、ゼロが交わる地点付近で必要に応じてサンプルを複製またはドロップすることです。これで、より洗練されたコンバージョンが可能になります。

入力レイテンシを最小限に抑える

このセクションでは、内蔵マイクまたは外付けヘッドセット マイクで録音するときに、オーディオ入力レイテンシを減少させるのに役立つアドバイスを提供します。

  • アプリが入力を監視している場合、ユーザーに(たとえば、初回実行時に「ヘッドフォンの使用を推奨します」というような画面を表示するなどして)ヘッドセットの使用を勧めるようにします。ヘッドセットを使うだけで、レイテンシが可能な限り低くなるとは限らないので、注意してください。オーディオ パスから不要な信号処理を削除するために、録音中に VOICE_RECOGNITION プリセットを使用するなど、他の手順を実行することが必要になる場合があります。
  • PROPERTY_OUTPUT_SAMPLE_RATEgetProperty(String) で報告される、名目上のサンプルレート 44,100 Hz と 48,000 Hz を処理できるようにします。他のサンプルレートになる可能性はめったにありません。
  • PROPERTY_OUTPUT_FRAMES_PER_BUFFERgetProperty(String) によって報告されるバッファサイズを処理できるようにします。通常のバッファサイズとしては 96、128、160、192、240、256、または 512 フレームが挙げられますが、他の値になる可能性もあります。

出力レイテンシを最小限に抑える

オーディオ プレーヤーの作成時に、最適なサンプルレートを使用する

レイテンシを最小限に抑えるには、端末の最適なサンプルレートとバッファサイズに一致するオーディオ データを提供する必要があります。詳細については、レイテンシを低下させる設計をご覧ください。

次のサンプルコードに示すように、Java では AudioManager から最適なサンプルレートを取得することができます。

Kotlin

val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val sampleRateStr: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
var sampleRate: Int = sampleRateStr?.let { str ->
    Integer.parseInt(str).takeUnless { it == 0 }
} ?: 44100 // Use a default value if property not found

Java

AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String sampleRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
int sampleRate = Integer.parseInt(sampleRateStr);
if (sampleRate == 0) sampleRate = 44100; // Use a default value if property not found

最適なサンプルレートを把握したら、プレーヤーを作成するときにそのサンプルレートを指定できます。この例では OpenSL ES を使用します。

// create buffer queue audio player
void Java_com_example_audio_generatetone_MainActivity_createBufferQueueAudioPlayer
        (JNIEnv* env, jclass clazz, jint sampleRate, jint framesPerBuffer)
{
   ...
   // specify the audio source format
   SLDataFormat_PCM format_pcm;
   format_pcm.numChannels = 2;
   format_pcm.samplesPerSec = (SLuint32) sampleRate * 1000;
   ...
}

注: samplesPerSec は、「ミリヘルツ単位のチャンネルごとのサンプルレート」(1 Hz = 1000 mHz)を指します。

オーディオ データをキューに登録するときに、最適なバッファサイズを使用する

AudioManager API を使用すれば、最適なサンプルレートと同様の方法で最適なバッファサイズを取得することができます。

Kotlin

val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val framesPerBuffer: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
var framesPerBufferInt: Int = framesPerBuffer?.let { str ->
    Integer.parseInt(str).takeUnless { it == 0 }
} ?: 256 // Use default

Java

AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int framesPerBufferInt = Integer.parseInt(framesPerBuffer);
if (framesPerBufferInt == 0) framesPerBufferInt = 256; // Use default

PROPERTY_OUTPUT_FRAMES_PER_BUFFER プロパティは、HAL(Hardware Abstraction Layer)バッファが保持できるオーディオ フレームの数を示します。この数のちょうど倍数を含むようにオーディオ バッファを構築する必要があります。オーディオ フレームの正確な数を使用すると、コールバックが一定の間隔で発生し、ジッターが減少します。

重要なのは、ハードコーディングされた値を使用するのではなく、API を使用してバッファサイズを特定することです。これは、HAL バッファサイズが端末や Android ビルドによって異なるためです。

信号処理を必要とする出力インターフェースを追加しない

高速ミキサーによってサポートされるのは、次のインターフェースのみです。

  • SL_IID_ANDROIDSIMPLEBUFFERQUEUE
  • SL_IID_VOLUME
  • SL_IID_MUTESOLO

次のインターフェースは使用できません。信号処理が含まれているのが原因で、高速トラックのリクエストが拒否されるためです。

  • SL_IID_BASSBOOST
  • SL_IID_EFFECTSEND
  • SL_IID_ENVIRONMENTALREVERB
  • SL_IID_EQUALIZER
  • SL_IID_PLAYBACKRATE
  • SL_IID_PRESETREVERB
  • SL_IID_VIRTUALIZER
  • SL_IID_ANDROIDEFFECT
  • SL_IID_ANDROIDEFFECTSEND

次の例に示すように、プレーヤーを作成するときは、高速インターフェースのみを追加するようにしてください。

const SLInterfaceID interface_ids[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME };

低レイテンシ トラックを使用していることを確認する

次の手順を実行して、低レイテンシ トラックを正常に取得したことを確認します。

  1. アプリを起動し、次のコマンドを実行します。
  2. adb shell ps | grep your_app_name
    
  3. アプリのプロセス ID をメモします。
  4. ここで、アプリから何らかのオーディオを再生します。ターミナルから次のコマンドを実行するのに約 3 秒かかります。
  5. adb shell dumpsys media.audio_flinger
    
  6. プロセス ID をスキャンします。[Name] 列に [F] と表示される場合は、低レイテンシ トラックです(F は高速トラックを表します)。

ウォームアップ レイテンシを最小限に抑える

最初にオーディオ データをキューに登録する際、端末のオーディオ回路のウォームアップには、短いけれども、ある程度の時間がかかります。このウォームアップ レイテンシを回避するには、次のサンプルコードに示すように、無音を含むオーディオ データのバッファをキューに登録します。

#define CHANNELS 1
static short* silenceBuffer;
int numSamples = frames * CHANNELS;
silenceBuffer = malloc(sizeof(*silenceBuffer) * numSamples);
    for (i = 0; i<numSamples; i++) {
        silenceBuffer[i] = 0;
    }

オーディオを生成する必要があるときに、実際のオーディオ データを含んでいるバッファをキューに登録するよう切り替えることができます。

: 絶えずオーディオを出力すると、消費電力が大幅に増加します。onPause() メソッドで、出力を停止するようにしてください。また、一定期間ユーザー操作が行われなかったら、無音出力を一時停止することも検討してください。

その他のサンプルコード

オーディオ レイテンシを示すサンプルアプリをダウンロードするには、NDK サンプルをご覧ください。

詳細情報

  1. アプリ デベロッパーのオーディオ レイテンシ
  2. オーディオ レイテンシを引き起こすもの
  3. オーディオ レイテンシの測定
  4. オーディオのウォームアップ
  5. レイテンシ(オーディオ)
  6. ラウンドトリップの遅延時間