OpenSL ES と NDK を使って Android オーディオストリーミング
この文章は、Victor Lazzariniのブログを翻訳したものです。彼は音楽ライブラリPySndObjなどの開発者です。さて、内容はAndroidで音楽プログラミングする場合、NDKを使って処理を高速化してもJavaのAPIを叩く限り不満は残ります。ここでNDKとOpenSL ESを使うことで不満を一掃しています。最後に、翻訳の精度は毎度低いので心配な方は原文と一緒に読まれることを願います。
>>>それでは本文
Androidのドキュメントやサンプルプログラムであまり触れられない話題にオーディオストリーミングがある。そのズレを埋めるのに、Android Native Development Kit(NDK)でもってOpenSL ES APIの使用検討してみたい。Androidプログラミングに不慣れな皆さんには、様々な開発コンポーネントをどのようにあつかうかの補足説明がいるだろう。
手始めに、トップレベルのアプリケーションプログラム環境がなければはじまらない、それはAndroid SDK、Javaベースだ。これは、AudioTrack APIでオーディオストリーミングをサポートしていて、SDKの一部。巷には、Androidプロジェクトのpd-androidとSupercolliderを含んだ、AudioTrackアプリケーションの様々な例がある。
SDKのほかに、Androidはまた、NDKと呼ばれるちょい低級のプログラミング環境を提供し、開発者に、Java Native Interface(JNI)でもってアプリケーションで使用できるCまたはC++のコードを書けるようにしている。Android2.3以降、OpenSL ES APIを加えたNDKは、執筆時点において広く使われていない。こいつを採用しているプロジェクトにCsound for Androidがある。ここではオーディオストリーミングのアプリ開発のためにOpenSL APIとNDK環境の使い方について説明していく。
開発環境のセットアップ
開発環境のセットアップには、GoogleのAndroid開発のサイトに行き、すべてのツールをダウンロードする必要があるだろう。ダウンロードしたものには、SDKとNDKとeclipseプラグインが含まれている。またEclipse IDEを取得しなければならない、"Classic"バージョンは、もしかすると本作業に最も適している。これらのパッケージをインストールする手順はとても明瞭であり、うまくいかなかったとしても、インターネットのたくさんの情報によってあなたは助けられる。
SWIGは、Android開発の別の役立つツールで、我々が書いたC言語の関数をラップするJavaのコードを作るのに役立つ。これは、かならずしも必要ではないが、直接JNIをつかえる。しかしながら、これはとても便利だ、JNIはソフトウェア開発周辺を埋める最も簡単なピースとはならない(ある人は、JNIは悪夢だと)。SWIGはCコードをとてもうまくラップし、作業をとても簡単にする。ここで説明する例でSWIGを使っていく。
サンプルプロジェクト
これから説明していくサンプルは、以下のコマンドgitで手に入れられる。
$git clone https://bitbucket.org/victorlazzarini/android-audiotest
また、これらのソースはウェブページのインターフェイスで、アーカイブと同じ場所から手に入れられる。
プロジェクトは、OpenSLのストリーミングIOモジュールのためのNDKプロジェクトとアプリケーション例のEclipseプロジェクトで構成される。NDKプロジェクトは最初、トップレベルのスクリプトを実行させることによってビルドされる。
$sh build.sh
この単純なスクリプトは最初、ダウンロードしたNDKの場所をセットアップする(あなたのシステムの場所に合うように、これを設定する必要があるだろう)。
export ANDROID_NDK_ROOT=$HOME/work/android-ndk-r7
その後、C言語のOpenSLのサンプルモジュールをアプリにリンクするJavaインターフェイスのコード、それをビルドするのにSWIGを呼ぶ手続きをする。C言語のコードをラップするC++ファイルとJavaのクラスの両方を作り、我々は実行するためにそれを使わないといけない。
swig -java -package opensl_example -includeall -verbose -outdir src/opensl_example -c++ -I/usr/local/include -I/System/Library/Frameworks/JavaVM.framework/Headers -I./jni -o jni/java_interface_wrap.cpp opensl_example_interface.i
これがなされるとき、NDKのビルドパスを呼び出す。
$ANDROID_NDK_ROOT/ndk-build TARGET_PLATFORM=android-9 V=1
これが、我々のネイティブコードを含む自動的に読み込み可能なモジュール(.so)をビルドする。このスクリプトは、ディレクトリ内でAndroid.mkファイルを使って組み込まれる。
一旦、NDKの作業パートがビルドされると、Eclipseに戻ることができる。Eclipseが起動したあと、File->Importを使ってプロジェクトを、その際に'Import into existing workspace'のオプションを選択し、インポートする必要がある。Eclipseはプロジェクトのディレクトリを要求し、あなたはブラウズしトップレベルのそれ(android-audiotest)を選択する。もしすべてが計画通りに進んだら、あなたのデバイスを接続し、build(Android app)を選択する。アプリケーションはビルドされ、デバイスで実行される。この時点で、マイクに向かって話すと、スピーカーを介して自分の声を聞くことができるだろう。
ネイティブインターフェイスのコード
2つのソースファイルは、このプロジェクトのネイティブのパートを構成する。つまりopensl_io.cが、すべてのオーディオストリーミング関数を持っていて、そしてopensl_example.cはシンプルなオーディオ処理の例を実装するのにこれらを使用する。OpenSL APIのリファレンスはOpenSL ES 1.0.1スペックで見つけられる、そしてリファレンスはまたAndroid NDK docs/openslディレクトリで配布されている。そこで、APIのAndroid実装に特化したドキュメントがあり、またオンラインでも利用可能である。
オーディオ出力のためにデバイスを解放する
OpenSLへのエントリポイントは、オーディオエンジンの作成を通して、以下のように。
result = slCreateEngine(&(p->engineObject), 0, NULL, 0, NULL, NULL);
SLObjectItf型のエンジンオブジェクトを初期化する(上の例では、SLObjectItfはポインタpが指すデータ構造に置かれる)。一旦、エンジンが作られると、リアライズしなくてはならない(これは、リアライズ後に作られる、OpenSLオブジェクトでもって共通の処理をするようになる)。エンジンインターフェイスはその時に取得され、インターフェイスは入力と出力デバイス(ソースとシンクをともなって)を解放するに続いて初期化するのに使われる。
result = (*p->engineObject)->Realize(p->engineObject, SL_BOOLEAN_FALSE); ... result = (*p->engineObject)->GetInterface(p->engineObject, SL_IID_ENGINE, &(p->engineEngine));
一旦、エンジンオブジェクトへのインターフェイスが取得したら、他のAPIオブジェクトを作成するのにインターフェイスを使うことができる。一般に、すべてのAPIオブジェクトのために。
1.オブジェクトを作る(インスタンス)
2.それをリアライズ(初期化)
3.GetInterface()メソッドで、(必要な機能にアクセスするために)オブジェクトのインターフェイスを取得する。
再生の際に、生成する最初のオブジェクトはOutput Mix(またはSLObjectItf)であり、その後、リアライズされる。
const SLInterfaceID ids[] = {SL_IID_VOLUME}; const SLboolean req[] = {SL_BOOLEAN_FALSE} result = (*p->engineEngine)->CreateOutputMix(p->engineEngine, &(p->outputMixObject), 1, ids, req); ... result = (*p->outputMixObject)->Realize(p->outputMixObject, SL_BOOLEAN_FALSE);
オブジェクトを操作する必要がないなら、オブジェクトのインターフェイスを取得する必要はない。ここで、生成しないといけない再生オブジェクトのソースとシンクを設定する。出力の場合、ソースはバッファ・キューになるだろう、そしてそれはオーディオデータのサンプルを送るところです。通常、パラメータをともなってバッファ・キューを設定する、データフォーマット、チャンネル数、サンプリングレート(sr)、などなど。
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM,channels,sr, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, speakers, SL_BYTEORDER_LITTLEENDIAN}; SLDataSource audioSrc = {&loc_bufq, &format_pcm};
そしてOutput Mixとシンク、上記で作成した、
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, p->outputMixObject}; SLDataSink audioSnk = {&loc_outmix, NULL};
オーディオプレイヤーのオブジェクトは、このソースとシンクで作成され、そしてリアライズされた。
const SLInterfaceID ids1[] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; const SLboolean req1[] = {SL_BOOLEAN_TRUE}; result = (*p->engineEngine)->CreateAudioPlayer(p->engineEngine, &(p->bqPlayerObject), &audioSrc, &audioSnk, 1, ids1, req1); ... result = (*p->bqPlayerObject)->Realize(p->bqPlayerObject, SL_BOOLEAN_FALSE)
その後、再生オブジェクトのインターフェイスを取得、
result = (*p->bqPlayerObject)->GetInterface(p->bqPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &(p->bqPlayerBufferQueue));
そして、バッファ・キューのインターフェイス(SLBufferQueueItf))
result = (*p->bqPlayerObject)->GetInterface(p->bqPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &(p->bqPlayerBufferQueue));
OpenSL APIはオーディオIOためのコールバックのメカニズムを提供する。しかし、CoreAudioまたはJackのような、他の非同期オーディオIOの実装とは異なって、コールバックは、処理のためにオーディオバッファを、引数の1つとして渡さない。代わりに、コールバックはアプリケーションの通知に使われるのみで、その通知は、バッファ・キューがデータを受信する準備ができたことを示す。
上記で得られたバッファ・キューのインターフェイスを使って、コールバックを設定する(bqPlayerCallbackは、コンテストとしてpに渡される)。
result = (*p->bqPlayerBufferQueue)->RegisterCallback(p->bqPlayerBufferQueue, bqPlayerCallback, p);
最終的に、再生のインターフェイスを使って、オーディオ再生を開始する。
result = (*p->bqPlayerPlay)->SetPlayState(p->bqPlayerPlay, SL_PLAYSTATE_PLAYING);
オーディオ入力のためにデバイスを解放する
オーディオデータの記録をはじめるプロセスは再生のプロセスにとても良く似ている。まず、ソースとシンクを設定して、それぞれ、オーディオ入力とバッファ・キューにする。
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL}; SLDataSource audioSrc = {&loc_dev, NULL}; ... SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, channels, sr, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, speakers, SL_BYTEORDER_LITTLEENDIAN}; SLDataSink audioSnk = {&loc_bq, &format_pcm};
その後、オーディオレコーダーを作成し、レコーダーをリアライズし、レコーダーのインターフェイスを取得する。
const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; const SLboolean req[1] = {SL_BOOLEAN_TRUE}; result = (*p->engineEngine)->CreateAudioRecorder(p->engineEngine, &(p->recorderObject), &audioSrc, &audioSnk, 1, id, req); ... result = (*p->recorderObject)->Realize(p->recorderObject, SL_BOOLEAN_FALSE); ... result = (*p->recorderObject)->GetInterface(p->recorderObject, SL_IID_RECORD, &(p->recorderRecord));
バッファ・キューのインターフェイスが取得され、コールバックを設定する。
result = (*p->recorderObject)->GetInterface(p->recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &(p->recorderBufferQueue)); ... result = (*p->recorderBufferQueue)->RegisterCallback(p->recorderBufferQueue, bqRecorderCallback,p);
これで、オーディオの録音を開始できる。
result = (*p->recorderRecord)->SetRecordState(p->recorderRecord, SL_RECORDSTATE_RECORDING);
オーディオIO
デバイスから(デバイスまで)オーディオをストリーミングするために、SLBufferQueueItfのEnqueue()メソッドを実行する。
SLresult (*Enqueue) (SLBufferQueueItf self, const void *pBuffer, SLuint32 size);
バッファー・キュー(入力または出力)が、新しいデータバッファの用意をするときはいつも、Enqueue()メソッドが呼ばれる必要がある。プレイヤーまたはレコーダーが再生または録音状態にセットされるや否や、バッファ・キューはデータの用意をする。この後、コールバックメカニズムは、バッファ・キューが他のデータ・ブロックを準備したことをアプリケーションに通知することについて責任を負っている。コールバックそれ自体の中または他の所でEnqueue()メソッドを呼ぶことができる。前者を採用した場合、実行しているコールバックメカニズムを実行させるのに、レコーディングまたは再生をはじめるときにバッファをエンキューする必要があり、しなければコールバックは決して呼ばれない。
後者は、渡すためのバッファが満たされるまで待ちながら、通知するためだけにコールバックを使用することになる。今回は、ダブルバッファを採用したので、半分がエンキューされると、もう一方はいっぱいになっていて、アプリケーションで消費される。これは、バッファに書き込む、またはバッファからサンプルを受け取るのに使えるオーディオのブロックを受け取れるシンプルなインタフェイスを作成できる。
ここにあるのは入力の場合です。コールバックはとてもミニマルで、バッファ・キューが用意できたことをすぐにメインの処理のスレッドに通知する。
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) { OPENSL_STREAM *p = (OPENSL_STREAM *) context; notifyThreadLock(p->inlock); }
その間、ループ処理は、サンプルのブロックを取得するためにオーディオ入力関数を呼び出す。バッファが空になったら、デバイスとバッファ切り替えによって満たされたバッファのエンキューの通知を待つ。
int android_AudioIn(OPENSL_STREAM *p,float *buffer,int size){ short *inBuffer; int i, bufsamps = p->inBufSamples, index = p->currentInputIndex; if(p == NULL || bufsamps == 0) return 0; inBuffer = p->inputBuffer[p->currentInputBuffer]; for(i=0; i < size; i++){ if (index >= bufsamps) { waitThreadLock(p->inlock); (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, inBuffer,bufsamps*sizeof(short)); p->currentInputBuffer = (p->currentInputBuffer ? 0 : 1); index = 0; inBuffer = p->inputBuffer[p->currentInputBuffer]; } buffer[i] = (float) inBuffer[index++]*CONVMYFLT; } p->currentInputIndex = index; if(p->outchannels == 0) p->time += (double) size/(p->sr*p->inchannels); return i; }
出力の場合は、反対の操作をする。コールバックは全く一緒なのだが、こっちでは、デバイスがバッファを消費したことを通知します。だからループ処理の中で、デバイスに渡たすブロックで出力バッファをいっぱいにするこの関数を呼ぶ。バッファがいっぱいになるとき、データをエンキューし、バッファを切り替えられるような通知を待つ。
int android_AudioOut(OPENSL_STREAM *p, float *buffer,int size){ short *outBuffer, *inBuffer; int i, bufsamps = p->outBufSamples, index = p->currentOutputIndex; if(p == NULL || bufsamps == 0) return 0; outBuffer = p->outputBuffer[p->currentOutputBuffer]; for(i=0; i < size; i++){ outBuffer[index++] = (short) (buffer[i]*CONV16BIT); if (index >= p->outBufSamples) { waitThreadLock(p->outlock); (*p->bqPlayerBufferQueue)->Enqueue(p->bqPlayerBufferQueue, outBuffer,bufsamps*sizeof(short)); p->currentOutputBuffer = (p->currentOutputBuffer ? 0 : 1); index = 0; outBuffer = p->outputBuffer[p->currentOutputBuffer]; } } p->currentOutputIndex = index; p->time += (double) size/(p->sr*p->outchannels); return i; }
OpenSLでオーディオストリーミングのために、上で説明したコードで最小限のAPIを構築する。5つの関数(そして1つの不透明なデータ構造)が含まれている。
/* 前もって与えられたサンプリングレート(sr)、入出力チャンネル、フレーム単位の IOバッファサイズをともなってオーディオデバイスを解放する。 OpenSLストリームのハンドルを返す。 */ OPENSL_STREAM* android_OpenAudioDevice(int sr, int inchannels, int outchannels, int bufferframes); /* オーディオデバイスを閉じる */ void android_CloseAudioDevice(OPENSL_STREAM *p); /* OpenSLストリーム *p から size サンプルのバッファーを読み込む 読み込んだサンプル数を返す。 */ int android_AudioIn(OPENSL_STREAM *p, float *buffer,int size); /* sizeサンプルのバッファをOpenSLストリーム*pへ書き込む。 書き込むサンプル数を返す。 */ int android_AudioOut(OPENSL_STREAM *p, float *buffer,int size); /* 現在のIOブロックの時間を毎秒単位で返す。 */ double android_GetTimestamp(OPENSL_STREAM *p);
プロセッシング
簡単な処理関数start_processing()でもって、この例は完成する、そしてアプリケーションから呼び出せるようにJavaでラップする。上記のAPIを採用している。
p = android_OpenAudioDevice(SR,1,2,BUFFERFRAMES); ... while(on) { samps = android_AudioIn(p,inbuffer,VECSAMPS_MONO); for(i = 0, j=0; i < samps; i++, j+=2) outbuffer[j] = outbuffer[j+1] = inbuffer[i]; android_AudioOut(p,outbuffer,VECSAMPS_STEREO); } android_CloseAudioDevice(p);
stop_processing()関数によって、アプリケーションを閉じる際にストリーミングを停止できるようになる。
アプリケーションのコード
最後に、プロジェクトを完了するのに、ちいさなJavaクラスをつくる、そのクラスは、Eclipseの自動生成されたJavaアプリケーションのコードをベースにしていて、そのコードにはセカンダリスレッドの追加がなされ、2つのラップしたネイティブ関数を呼ぶ。
public class AudiotestActivity extends Activity { /** Called when the activity is first created. */ Thread thread; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); thread = new Thread() { public void run() { setPriority(Thread.MAX_PRIORITY); opensl_example.start_process(); } }; thread.start(); } public void onDestroy(){ super.onDestroy(); opensl_example.stop_process(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } thread = null; } }
最語
私がこの文章を書いたのは、OpenSLとネイティブコードを使うオーディオ処理アプリケーション開発に光があたることを願ってのことだ。AudioTrack SDKに低レイテンツのオプションを(いまだ)提供する計画はないようだが、ネイティブコードは、ガベージコレクションによって停止するような仮想マシンのオーバーヘッドの対象にはならないので、よりよいパフォーマンスを提供できる。Androidのオーディオ開発の進むべき道ではないかとおもっている。Android OpenSLの実装のNDKドキュメントには、以下のように書いている。
Androidプラットフォームと特定のデバイスの実装は進化し続けているので、OpenSL ES アプリケーションにとって、将来のシステムパフォーマンス向上からの恩恵が期待できる。
これが、Androidのオーディオ開発者は、おそらくOpenSLに注意を払う必要があることを明示している。例が少ないので、悲観的になってしまうが、このブログが転換への一歩になることを期待する。