オーディオ レイテンシ

レイテンシとは、信号がシステムを移動する際にかかる時間を指します。オーディオ アプリに関係するレイテンシの主なタイプは次のとおりです。

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

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

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

遅延(レイテンシ)を測定する

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

オーディオ ラウンドトリップ レイテンシは、デバイスの機種と Android のビルドによって大きく異なります。たとえば、Nexus デバイスに関して公開されている測定値を見れば、ラウンドトリップ レイテンシを大まかに把握することができます。

オーディオ信号を生成し、その信号をリッスンして、送受信間にかかった時間を測定するアプリを作成すれば、オーディオ ラウンドトリップ レイテンシを測定することができます。

レイテンシを最小限に抑えるには、オーディオパスの信号処理を最小限に抑える必要があります。そのため、ヘッドセット コネクタ経由でテストを実行できるオーディオ ループバック ドングルを使用することをおすすめします。

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

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

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

CDD では、ラウンドトリップ レイテンシを 20 ms 以下と指定しています(ただし、音楽家は通常 10 ms を要求します)。20 ms であれば、さまざまな重要なユースケースを実現することができます。

現在のところ、実行時に Android デバイス上のパスのオーディオ レイテンシを判定できる API はありません。ただし、以下のハードウェア機能フラグを使用すると、デバイスがレイテンシについてなんらかの保証を行っているかどうかを確認できます。

各フラグをレポートする基準は、CDD の「5.6 Audio Latency」と「5.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(ハードウェア抽象化レイヤ)バッファが保持できるオーディオ フレームの数を示します。この数の正確な倍数を格納するようにオーディオ バッファを構築する必要があります。正確なオーディオ フレーム数を使用することで、コールバックが一定の間隔で発生し、ジッターが減少します。

HAL バッファサイズはデバイスや Android ビルドによって異なるため、ハードコーディングした値を使用するのではなく、API を使用してバッファサイズを判定することをおすすめします。

シグナル処理を含む出力インターフェースを追加しない

高速ミキサーがサポートしているのは、以下のインターフェースに限られます。

  • 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. ラウンドトリップの遅延時間