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::GetPrefetchStatusSL_PREFETCHSTATUS_UNDERFLOW を報告する場合は、データソースに修復不可能なエラーがあることを示しています。このエラーにはたとえば、ローカル ファイル名が存在しないかネットワーク URI が無効であるために、データソースに接続できないことなどが挙げられます。

次のバージョンの OpenSL ES には、データソース内のエラー処理に対してのさらなる明示的なサポートの追加が予定されています。ただし、バイナリの互換性を維持するため、Android では、修復不可能なエラーを報告する現在のメソッドを引き続きサポートする予定です。

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

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. SL_IID_PREFETCHSTATUSObject::GetInterface
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. SL_IID_PLAYObject::GetInterface
  8. SL_PLAYSTATE_PAUSED への Play::SetPlayState、または 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 を呼び出す非実行時のアプリ スレッドには、ガベージ コレクションの一時停止など、ランタイムに関連するオーバーヘッドがありません。OpenSL ES を使用して得られるパフォーマンス上のメリットはこれだけですが、例外の 1 つをこのセクションで紹介します。具体的に言うと、OpenSL ES を使用しても、プラットフォームの通常レベルよりオーディオ レイテンシが低くなったりスケジューリング優先順位が高くなったりするなど、必ずしも機能が拡張されるわけではありません。しかし、Android プラットフォームと特定の端末の実装が進化を続けていくと、OpenSL ES アプリは今後のシステム パフォーマンス向上からメリットを得られることが期待されます。

そのような進化の 1 つに、低オーディオ出力レイテンシのサポートがあります。低出力レイテンシの基礎ができたのは Android 4.1(API レベル 16)で、続いて Android 4.2(API レベル 17)で進化を遂げました。これらの拡張は、OpenSL ES を介せば、android.hardware.audio.low_latency 機能が必要な端末実装に利用することができます。この機能が必要なくても、端末が 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 サンプルを表示することもできます。

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

音量異常を防ぐため、バッファキュー コールバック ハンドラは予測可能な短い時間枠で実行する必要があります。これは通常、ミューテックス、条件、または 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 プログラミング言語コード以上のことは実行できません。2 つのコードでは、使用可能な API だけが異なります。

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

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