OpenSL ES プログラミング メモ

警告: OpenSL ES は非推奨です。デベロッパーは、GitHub で入手可能なオープンソース Oboe ライブラリを使用する必要があります。Oboe は、AAudio によく似た API を提供する C++ ラッパーです。AAudio を利用できる場合は AAudio を呼び出し、AAudio を利用できない場合は OpenSL ES にフォールバックします。

このセクションのメモは、OpenSL ES 1.0.1 仕様を補足するものです。

オブジェクトとインターフェースの初期化

OpenSL ES プログラミング モデルには、デベロッパーになりたての方にはなじみがないと思われる 2 つの側面があります。それは、オブジェクトとインターフェースの違い、および初期化シーケンスです。

簡潔に言うと、OpenSL ES オブジェクトは Java や C++ などのプログラミング言語のオブジェクトの概念に似ていますが、OpenSL ES オブジェクトは関連付けられたインターフェースを介してのみ表示できるという点が異なります。これには、すべてのオブジェクトの初期インターフェース(SLObjectItf)が含まれます。オブジェクト自体にハンドルはありません。あるのは、オブジェクトの SLObjectItf インターフェースへのハンドルのみです。

OpenSL ES オブジェクトはまず「作成」されて、それにより SLObjectItf が返されて「認識」されます。これは、まずオブジェクトを構築してから初期化を完了する、一般的なプログラミング パターンに似ています(オブジェクトの構築はメモリ不足や無効なパラメータ以外で失敗することはなく、初期化はリソース不足が原因で失敗することがあります)。認識ステップでは、論理的な場所に実装を提供し、必要に応じて追加のリソースを割り当てます。

アプリは、オブジェクトを作成する API の一部として、後から取得する予定のインターフェースの配列を指定します。この配列がインターフェースを自動的に取得するわけではないことに注意してください。後でインターフェースを取得する意図があることを示すだけです。インターフェースは「暗黙的」または「明示的」として区別されます。明示的なインターフェースは、後で取得される場合、配列に列挙されている必要があります。暗黙的なインターフェースはオブジェクト作成配列に列挙されている必要はありませんが、列挙されていても問題はありません。OpenSL ES には、もう 1 つ「動的」という種類のインターフェースがあります。このインターフェースはオブジェクト作成配列で指定されている必要はなく、オブジェクトの作成後に追加できます。Android 実装は、こうした複雑さを回避するための便利な機能を提供します。詳しくは、オブジェクト作成時の動的インターフェースをご覧ください。

オブジェクトが作成、認識された後、アプリは最初の SLObjectItfGetInterface を使用して、必要な機能ごとにインターフェースを取得する必要があります。

最終的に、オブジェクトはそのインターフェースを介して使用できるようになりますが、一部のオブジェクトではさらに設定が必要になります。特に、URI データソースが含まれたオーディオ プレーヤーでは、接続エラーを検出するための準備がもう少し必要になります。詳しくは、オーディオ プレーヤーのプリフェッチのセクションをご覧ください。

アプリでオブジェクトが不要になったら、オブジェクトを明示的に破棄する必要があります。詳しくは、以下の破棄のセクションをご覧ください。

オーディオ プレーヤーのプリフェッチ

URI データソースが含まれたオーディオ プレーヤーの場合、Object::Realize がリソースを割り当てますが、データソースへの接続(「準備」)を実行することも、データのプリフェッチを開始することもありません。これらの処理は、プレーヤーの状態が SL_PLAYSTATE_PAUSED または SL_PLAYSTATE_PLAYING のいずれかに設定されたときに行われます。

一部の情報は、このシーケンスの比較的遅い時点までわからない場合があります。具体的に言うと、最初、Player::GetDurationSL_TIME_UNKNOWN を返し、MuteSolo::GetChannelCount はチャンネル数「0」またはエラー結果 SL_RESULT_PRECONDITIONS_VIOLATED を正常に返します。これらの API は、認識されると適切な値を返します。

最初に不明なプロパティとしては他にも、サンプルレートや、(アプリが指定した MIME タイプやコンテナタイプとは対称的な)コンテンツのヘッダーの調査に基づく実際のメディア コンテンツ タイプなどが挙げられます。これらも準備またはプリフェッチの後半で特定されますが、取得するための API はありません。

すべての情報をいつ入手できるかを特定するには、プリフェッチ状態インターフェースが役立ちます。または、アプリで定期的にポーリングすることもできます。ただし、MP3 のストリーミングの期間などの一部の情報は把握できない場合があります。

プリフェッチ状態インターフェースはエラー検出にも役立ちます。コールバックを登録し、少なくとも SL_PREFETCHEVENT_FILLLEVELCHANGE イベントと SL_PREFETCHEVENT_STATUSCHANGE イベントを有効にします。この両方のイベントが同時に配信され、PrefetchStatus::GetFillLevel によってゼロレベルが報告され、PrefetchStatus::GetPrefetchStatus によって SL_PREFETCHSTATUS_UNDERFLOW が報告された場合は、データソースに修復不可能なエラーがあることを示しています。このエラーには、ローカル ファイル名が存在しないかネットワーク URI が無効であるために、データソースに接続できないことなどが挙げられます。

次のバージョンの OpenSL ES では、データソース内のエラーの処理に対するより明示的なサポートが追加される予定です。ただし、バイナリの互換性を将来にわたって維持するために、修復不可能なエラーの現在の報告方法は今後もサポートする予定です。

まとめると、推奨されるコード シーケンスは次のようになります。

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterfaceSL_IID_PREFETCHSTATUS
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. Object::GetInterfaceSL_IID_PLAY
  8. Play::SetPlayStateSL_PLAYSTATE_PAUSED または SL_PLAYSTATE_PLAYING に設定)

注: 準備とプリフェッチはここで行われます。この間に、定期的なステータス更新によってコールバックが呼び出されます。

破棄

アプリでオブジェクトが不要になったときは、すべてのオブジェクトを必ず破棄してください。 オブジェクトは作成したときと逆の順番で破棄する必要があります。これは、依存オブジェクトを持つオブジェクトを破棄するのは安全でないためです。たとえば、オーディオ プレーヤーとオーディオ レコーダー、出力ミックス、エンジンという順番で破棄します。

OpenSL ES では、インターフェースの自動ガベージ コレクションや参照カウントをサポートしていません。Object::Destroy を呼び出すと、関連付けられているオブジェクトから派生するすべての現存のインターフェースが未定義になります。

Android OpenSL ES 実装では、そのようなインターフェースの誤使用は検出されません。 オブジェクトが破棄された後もそれらのインターフェースを使用し続けると、アプリがクラッシュしたり、予測不能な動作になったりすることがあります。

オブジェクト破棄シーケンスの一環として、プライマリ オブジェクト インターフェースと関連付けられているすべてのインターフェースを明示的に NULL に設定することをおすすめします。これにより、古いインターフェース ハンドルの意図しない誤用を防ぐことができます。

ステレオ パンニング

Volume::EnableStereoPosition を使用してモノソースのステレオ パンニングを有効にすると、総音響出力レベルが 3 dB 減少します。これは、ソースが一方のチャンネルから他方のチャンネルにパンニングされたときに、総音響出力レベルを一定に保つために必要です。そのため、必要な場合だけステレオ ポジショニングを有効にしてください。詳しくは、オーディオ パンニングに関する Wikipedia の記事をご覧ください。

コールバックとスレッド

コールバック ハンドラは通常、実装がイベントを検出したときに同期的に呼び出されます。アプリはこの時点では非同期なので、非ブロック同期メカニズムを使用して、アプリとコールバック ハンドラの間で共有される変数へのアクセスを制御する必要があります。バッファキューなどのサンプルコードでは、簡潔さを考慮して、この同期を省略したり、ブロック同期を使用したりしています。ただし、運用コードには適切な非ブロック同期が不可欠です。

コールバック ハンドラは、Android ランタイムにアタッチされていない、アプリ以外の内部スレッドから呼び出されるため、JNI を使用する資格がありません。これらの内部スレッドは OpenSL ES 実装の整合性に不可欠なため、コールバック ハンドラが作業を過度にブロックしたり実行したりしないようにする必要もあります。

コールバック ハンドラは、JNI を使用したり、コールバックに釣り合わない作業を実行したりする必要がある場合、代わりに別のスレッドのイベントをポストして処理する必要があります。許容できるコールバック ワークロードの例としては、次の出力バッファ(AudioPlayer 用)のレンダリングとキュー登録、いっぱいになったばかりの入力バッファの処理と次の空のバッファ(AudioRecorder 用)のキュー登録、Get ファミリーの大半のようなシンプルな API などが挙げられます。ワークロードについては、以下のパフォーマンスのセクションをご覧ください。

その逆は安全です。JNI を開始した Android アプリスレッドは、OpenSL ES API(ブロックするものを含む)を直接呼び出すことができます。ただし、メインスレッドからのブロッキング呼び出しは、「アプリケーション応答なし」(ANR)エラーとなる可能性があるため、推奨されません。

コールバック ハンドラを呼び出すスレッドに関する判断は、実装に大きく委ねられています。このように柔軟性が高いのは、特にマルチコア デバイスで継続的な最適化を図るためです。

コールバック ハンドラが実行されるスレッドの ID は、呼び出しごとに異なる場合があります。したがって、呼び出し間で一貫性を保つために、pthread_self() から返された pthread_t、または gettid() から返された pid_t に依存しないようにしてください。同じ理由により、pthread_setspecific()pthread_getspecific() などのスレッド ローカル ストレージ(TLS)API をコールバックで使用しないでください。

実装では、同じ種類のコールバックが同じオブジェクトに対して同時に発生することはありません。ただし、さまざまなスレッドで、さまざまな種類のコールバックが同じオブジェクトに対して同時に発生する可能性はあります。

パフォーマンス

OpenSL ES はネイティブ C API であるため、OpenSL ES を呼び出す非実行時のアプリスレッドには、ガベージ コレクションの一時停止など、ランタイムに関連するオーバーヘッドがありません。下記の例外が 1 つありますが、OpenSL ES を使用することによって得られるパフォーマンス上のメリットはこれだけです。具体的に言うと、OpenSL ES を使用しても、プラットフォームの通常レベルよりオーディオ レイテンシが低くなったりスケジューリング優先順位が高くなったりといった機能強化に必ずしもつながるわけではありません。しかし、Android プラットフォームと特定のデバイスの実装が進化を続けていく中で、OpenSL ES アプリが今後のシステム パフォーマンスの向上によるメリットを享受できる可能性はあります。

そうした進化の 1 つに、低オーディオ出力レイテンシのサポートがあります。低出力レイテンシの基礎ができたのは Android 4.1(API レベル 16)で、続いて Android 4.2(API レベル 17)で進化を遂げました。これらの機能強化を利用するには、android.hardware.audio.low_latency 機能を備えたデバイス実装で OpenSL ES を使用します。デバイスが Android 2.3(API レベル 9)以降をサポートしている場合は、この機能を備えていなくても OpenSL ES API を使用できます。ただし、出力レイテンシは高くなる可能性があります。より低い出力レイテンシパスが使用されるのは、デバイスのネイティブ出力構成と互換性のあるバッファサイズおよびサンプルレートをアプリが要求した場合だけです。これらのパラメータはデバイス固有であり、以下のように取得する必要があります。

Android 4.2(API レベル 17)以降では、アプリは、プラットフォーム ネイティブな出力サンプルレートとバッファサイズ、またはデバイスのプライマリ出力ストリームに最適な出力サンプルレートとバッファサイズをクエリすることができます。先ほど触れた機能テストと組み合わせると、アプリは自身を適切に構成して、サポート対象のデバイスでレイテンシ出力を低下させることができます。

Android 4.2(API レベル 17)以前では、低レイテンシを実現するには 2 以上のバッファ数が必要です。Android 4.3(API レベル 18)以降では、バッファ数 1 で低レイテンシを実現できるようになりました。

出力エフェクトに関する OpenSL ES インターフェースはすべて、低レイテンシパスを除外します。

推奨される手順は次のとおりです。

  1. API レベル 9 以上かどうかを確認して、OpenSL ES が使用されていることを確かめます。
  2. 次のようなコードを使用して android.hardware.audio.low_latency 機能を確認します。

    Kotlin

    import android.content.pm.PackageManager
    ...
    val pm: PackageManager = context.packageManager
    val claimsFeature: Boolean = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)

    Java

    import android.content.pm.PackageManager;
    ...
    PackageManager pm = getContext().getPackageManager();
    boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
  3. API レベル 17 以上かどうかを確認して、android.media.AudioManager.getProperty() が使用されていることを確かめます。
  4. 次のようなコードを使用して、ネイティブな出力サンプルレートとバッファサイズ、またはこのデバイスのプライマリ出力ストリームに最適な出力サンプルレートとバッファサイズを取得します。

    Kotlin

    import android.media.AudioManager
    ...
    val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    val sampleRate: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
    val framesPerBuffer: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)

    Java

    import android.media.AudioManager;
    ...
    AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
    String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
    sampleRateframesPerBuffer は「文字列」であることに注意してください。まず、null を確認してから、Integer.parseInt() を使用して整数に変換します。
  5. OpenSL ES を使用して、PCM バッファキュー データロケータを含む AudioPlayer を作成します。

注: Audio Buffer Size テストアプリを使用すると、オーディオ機器の OpenSL ES オーディオ アプリのネイティブなバッファサイズとサンプルレートを特定できます。また、GitHub で audio-buffer-size サンプルを表示することもできます。

低レイテンシ オーディオ プレーヤーの数には制限があります。アプリが 3、4 個以上のオーディオ ソースを必要とする場合、アプリレベルでオーディオをミキシングすることを検討します。オーディオ プレーヤーは他のアプリと共有されるグローバル リソースであるため、アクティビティが一時停止されたときはオーディオ プレーヤーを破棄するようにしてください。

音量異常を防ぐため、バッファキュー コールバック ハンドラは予測可能な短い時間枠内で実行する必要があります。これは通常、ミューテックス、条件、または I/O 操作に無限のブロックがないことを意味します。代わりに、try lock、タイムアウト付きのロックと待機、非ブロック アルゴリズムを検討してください。

次のバッファ(AudioPlayer 用)をレンダリングするため、または前のバッファ(AudioRecord 用)を使用するために必要な演算にかかる時間は、各コールバックでほぼ同じです。非決定的な時間で実行したり、演算で「バースト」状態になったりするようなアルゴリズムは避けてください。コールバックの演算は、特定のコールバックにかかった CPU 時間が平均よりも大幅に長い場合にバースト状態になります。つまり、ハンドラの CPU 実行時間がほとんど変動せず、ハンドラが時間制限なしにブロックすることがないのが理想的です。

低レイテンシ オーディオが可能なのは、次の出力のみです。

一部のデバイスでは、スピーカーの修正と保護のデジタル シグナル処理によって、スピーカー レイテンシが他のパスよりも高くなります。

Android 5.0(API レベル 21)以降では、一部のデバイスで低レイテンシのオーディオ入力がサポートされています。この機能を利用するには、上記のように、まず低レイテンシ出力が利用できることを確認します。低レイテンシ出力機能は、低レイテンシ入力機能の前提条件です。次に、出力に使用するのと同じサンプルレートとバッファサイズの AudioRecorder を作成します。入力エフェクト用の OpenSL ES インターフェースは、低レイテンシパスを除外します。録音プリセット SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION は低レイテンシに使用する必要があります。このプリセットは、入力パスにレイテンシを追加する可能性がある、デバイス固有のデジタル シグナル処理を無効にします。録音プリセットについて詳しくは、上記の Android 構成インターフェースをご覧ください。

入出力を同時に行うため、各側でそれぞれ別個のバッファキュー完了ハンドラが使用されます。両側で同じサンプルレートが使用されていたとしても、コールバックの相対順序が維持されるとは限らず、オーディオ クロックが同期されているとも限りません。アプリレベルで、適切なバッファ同期を使用してデータをバッファする必要があります。

オーディオ クロックが独立している可能性がある場合、非同期サンプルレート変換が必要になります。非同期サンプルレート変換のシンプルな方法は、ゼロクロス ポイント付近で必要に応じてサンプルを複製するかドロップする方法です(ただし、音質は犠牲になります)。これにより、高度な変換が可能になります。

パフォーマンス モード

Android 7.1(API レベル 25)から、OpenSL ES でオーディオパスのパフォーマンス モードを指定する方法が導入されました。オプションは次のとおりです。

  • SL_ANDROID_PERFORMANCE_NONE: 特定のパフォーマンス要件はありません。 ハードウェアおよびソフトウェア エフェクトを許可します。
  • SL_ANDROID_PERFORMANCE_LATENCY: レイテンシを優先します。ハードウェアおよびソフトウェア エフェクトはありません。これがデフォルト モードです。
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: ハードウェアおよびソフトウェア エフェクトを許可しながら、レイテンシを優先します。
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: 節電を優先します。 ハードウェアおよびソフトウェア エフェクトを許可します。

注: 低レイテンシパスを必要とせず、デバイスの内蔵オーディオ エフェクトを利用したい場合(たとえば、動画再生の音響品質を向上させるため)、パフォーマンス モードを明示的に SL_ANDROID_PERFORMANCE_NONE に設定する必要があります。

パフォーマンス モードを設定するには、以下に示すように、Android 構成インターフェースを使用して SetConfiguration を呼び出す必要があります。

  // Obtain the Android configuration interface using a previously configured SLObjectItf.
  SLAndroidConfigurationItf configItf = nullptr;
  (*objItf)->GetInterface(objItf, SL_IID_ANDROIDCONFIGURATION, &configItf);

  // Set the performance mode.
  SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_NONE;
    result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE,
                                                     &performanceMode, sizeof(performanceMode));

セキュリティと権限

Android のセキュリティは、誰が何を行えるかという点に関してはプロセスレベルで実行されます。Java プログラミング言語コードは、ネイティブ コード以上のことは実行できません。また、ネイティブ コードも Java プログラミング言語コード以上のことは実行できません。これらのコードでは、使用可能な API だけが異なります。

OpenSL ES を使用するアプリは、類似の非ネイティブ API に必要なパーミッションを要求する必要があります。たとえば、音声を記録するアプリでは android.permission.RECORD_AUDIO パーミッションが、オーディオ エフェクトを使用するアプリでは android.permission.MODIFY_AUDIO_SETTINGS が、ネットワーク URI リソースを再生するアプリでは android.permission.NETWORK が必要です。詳しくは、システム パーミッションの使用をご覧ください。

プラットフォームのバージョンと実装によって異なりますが、メディア コンテンツ パーサーとソフトウェア コーデックは、OpenSL ES を呼び出す Android アプリのコンテキスト内で実行される可能性があります(ハードウェア コーデックは抽出されますが、デバイスに依存します)。パーサーとコーデックの脆弱性を悪用するよう設計された不正な形式のコンテンツは、既知の攻撃ベクトルです。したがって、信頼できるソースのメディアのみを再生することをおすすめします。また、信頼できないソースのメディアを処理するコードが比較的「サンドボックス化」された環境で実行されるよう、アプリを分離することをおすすめします。たとえば、信頼できないソースのメディアの処理を別のプロセスで行うことができます。両方のプロセスは同じ UID で実行されますが、この分離により攻撃を受けにくくなります。