前回OpenSL ESでサウンドを再生しましたが、Activityが非アクティブになっても音が鳴り続ける問題がありました。それに対しての簡単な対処法として、バッファを細かく分けてEnqueueしてみました。Enqueueは32768バイト単位でしています。(サイズは適当です)
最終アップデートからの経過時間が0.5秒を超えたらコールバック無視します。この方法の長所は、コールバック一つでループ再生と非アクティブになった時の音停止を同時に実装でき、コードが簡潔になります。Activityから非アクティブの通知を受け取る必要もありません。短所としては、アップデートループで長い処理をすると音が切れてしまうことです。
また、停止した音を再開する処理を盛り込んでいません。ここは、ゲーム毎の実装依存が多いと思いあえてしませんでした。例えば多くの場合は再開すべき音はBGMだけであったりするでしょう。
AndroidのOpenSL ESはコールバックは別スレッドから来るので特に気をつける必要があります。Android特有の実装についてはNDKのdocs/Additional_library_docs/opensles/index.html に説明があるので、一度目を通しておくと良さそうです。
Showing posts with label Android Studio. Show all posts
Showing posts with label Android Studio. Show all posts
Friday, December 25, 2015
Saturday, December 12, 2015
実践OpenSL ES、AndroidでWAVファイル再生
OpenSL ESによるサウンドエンジンを使い物になるように書いてみます。
まず、気づくのはDirectXを彷彿させるインターフェース型のAPIであることです。多くのメソッドはSLresultで結果を返し、Destroyでリソースを解放して終了します。素直に書くと冗長なコードになりそうですが、まず最初にDirectX開発者にはおなじみの手法を導入します。
SafeDestroyはDestroy呼び出しと同時にnullptrを代入してダングリングポインタにならないようにします。
SLHandleErrorというのは、各種メソッドがエラーコードを返したらソースコード上の行数と関数呼び出しの概要をログに出力するためのマクロです。Direct3D9のサンプルでは"V"という一文字のマクロが多用されていましたが、それです。
SLCallは、インターフェース呼び出しをすっきり書きたいがために書いたマクロです。これを使ってエンジンの初期化と終了を書いてみます。
エンジンの初期化と終了が書けました。
DirectXから来ると戸惑うのが、SLObjectItfとその他インターフェースに分かれていることです。OpenSLでは全てのインスタンスの実体はSLObjectItfであって、何らかのメソッド呼び出しが必要になったらSLObjectItfから追加のインターフェースを取得します。また、SLObjectItfをDestroyするとSLObjectItfから取得したインターフェースも一緒に無効になります。
engineEngineがSafeDestroyではなくnullptrを代入しているのは、engineObjectをSafeDestroyした時に一緒に消滅しているからです。
次は、WAVファイルをロードして"audio player"を作ってみます。
CreateAudioPlayerで、WAVファイルの特性(ステレオ/モノラル、サンプリングレート、ビット数)に合わせた"audio player"を生成します。WaveFormatExはWindows APIにあるWAVEFORMATEX構造体で、この情報からSLDataFormat_PCM構造体を構築します。リファレンス通りに書くとまたコードが長くなりそうですが、例えばSL_SAMPLINGRATE_44_1は44100000と定義されていたりします。samplesPerSecondを1000倍して代入すれば良いわけです。事前定義値は https://www.khronos.org/registry/sles/api/1.1/OpenSLES.h にあります。
次はWAVファイルの再生部です。
Enqueueで再生するバッファを指定します。ループ再生する場合はコールバックを登録して、コールバックからEnqueueを繰り返します。コールバック設定前にSL_PLAYSTATE_STOPPEDを明示するのはステートがSL_PLAYSTATE_STOPPEDではない場合にSL_RESULT_PRECONDITIONS_VIOLATEDエラーが発生するためです。これはPlayメンバ関数が二回目に呼ばれた場合に起こります。
これで、OpenSLの初期化からWAVファイルの再生までができました。ところで、この方法だとActivityが非アクティブになっても音が鳴り続けるという問題があります。この解決は次回書きます。
まず、気づくのはDirectXを彷彿させるインターフェース型のAPIであることです。多くのメソッドはSLresultで結果を返し、Destroyでリソースを解放して終了します。素直に書くと冗長なコードになりそうですが、まず最初にDirectX開発者にはおなじみの手法を導入します。
SafeDestroyはDestroy呼び出しと同時にnullptrを代入してダングリングポインタにならないようにします。
SLHandleErrorというのは、各種メソッドがエラーコードを返したらソースコード上の行数と関数呼び出しの概要をログに出力するためのマクロです。Direct3D9のサンプルでは"V"という一文字のマクロが多用されていましたが、それです。
SLCallは、インターフェース呼び出しをすっきり書きたいがために書いたマクロです。これを使ってエンジンの初期化と終了を書いてみます。
エンジンの初期化と終了が書けました。
DirectXから来ると戸惑うのが、SLObjectItfとその他インターフェースに分かれていることです。OpenSLでは全てのインスタンスの実体はSLObjectItfであって、何らかのメソッド呼び出しが必要になったらSLObjectItfから追加のインターフェースを取得します。また、SLObjectItfをDestroyするとSLObjectItfから取得したインターフェースも一緒に無効になります。
engineEngineがSafeDestroyではなくnullptrを代入しているのは、engineObjectをSafeDestroyした時に一緒に消滅しているからです。
次は、WAVファイルをロードして"audio player"を作ってみます。
CreateAudioPlayerで、WAVファイルの特性(ステレオ/モノラル、サンプリングレート、ビット数)に合わせた"audio player"を生成します。WaveFormatExはWindows APIにあるWAVEFORMATEX構造体で、この情報からSLDataFormat_PCM構造体を構築します。リファレンス通りに書くとまたコードが長くなりそうですが、例えばSL_SAMPLINGRATE_44_1は44100000と定義されていたりします。samplesPerSecondを1000倍して代入すれば良いわけです。事前定義値は https://www.khronos.org/registry/sles/api/1.1/OpenSLES.h にあります。
次はWAVファイルの再生部です。
Enqueueで再生するバッファを指定します。ループ再生する場合はコールバックを登録して、コールバックからEnqueueを繰り返します。コールバック設定前にSL_PLAYSTATE_STOPPEDを明示するのはステートがSL_PLAYSTATE_STOPPEDではない場合にSL_RESULT_PRECONDITIONS_VIOLATEDエラーが発生するためです。これはPlayメンバ関数が二回目に呼ばれた場合に起こります。
これで、OpenSLの初期化からWAVファイルの再生までができました。ところで、この方法だとActivityが非アクティブになっても音が鳴り続けるという問題があります。この解決は次回書きます。
Sunday, June 7, 2015
Android NDKでC++11のstd::chronoを使った時間計測
今まで以下のような秒単位の時間計測関数を、Android用とWindows用にそれぞれ作って使っていました。
今更ながら、C++11のstd::chronoでどちらでも動く関数を書けることに気づき、以下のように書きなおしました。
static auto startが妙ですが、こうするとGetTimeの初回呼び出し時の時間をstartに記録できます。あとは毎回のGetTimeの呼び出しでstartからの経過時間を秒単位で返します。
#include <chrono> で "fatal error: chrono: No such file or directory"というエラーが出てしまう場合、build.gradleでgnustlを使うよう設定する必要があります。build.gradleの一部を抜粋します。
"stlport_static"ではだめで、"gnustl_static"を使う必要があるようです。android-ndk-r10d\sources\cxx-stl\stlport\stlportにはchronoが入っていないのに対し、android-ndk-r10d\sources\cxx-stl\gnu-libstdc++\4.9\includeには入っていました。
Labels:
Android,
Android Studio,
C++,
C++11,
Japanese,
Visual Studio
Saturday, May 23, 2015
Android StudioのGUIを使わずにビルド→インストール→起動
以下のようなバッチファイルをgradlew.batと同じ位置に作っておくことで、Android Studioを起動せずともコマンドライン又はエクスプローラからビルド、インストール、起動ができるようになります。NDKを使用している場合のビルドまで問題なく出来ました。
※ パッケージ名は、com.pinotnoir.adamasを、アクティビティ名はcommon.pinotnoir.glactivity.PinotGLActivityを置き換えてください。
ところで、"ADB server didn't ACK" というエラーに悩まされていたのですが、原因はNsight Tegra Visual Studio Editionでした。これが内部でADBをつかっているため衝突するようです。Visual Stuidio起動中に上のバッチファイルが使えない場合、Visual Studioを終了すると使えるようになりました。
Nsight Tegra Visual Studio Editionを一時停止することはできないようです。Visual StudioとAndroid Studioを同時に使いつつ、Nsight Tegraも使いたい場合、面倒なことになりそうです。Nsight Tegraをアンインストールすると衝突は起こらなくなりました。
※ パッケージ名は、com.pinotnoir.adamasを、アクティビティ名はcommon.pinotnoir.glactivity.PinotGLActivityを置き換えてください。
ところで、"ADB server didn't ACK" というエラーに悩まされていたのですが、原因はNsight Tegra Visual Studio Editionでした。これが内部でADBをつかっているため衝突するようです。Visual Stuidio起動中に上のバッチファイルが使えない場合、Visual Studioを終了すると使えるようになりました。

Nsight Tegra Visual Studio Editionを一時停止することはできないようです。Visual StudioとAndroid Studioを同時に使いつつ、Nsight Tegraも使いたい場合、面倒なことになりそうです。Nsight Tegraをアンインストールすると衝突は起こらなくなりました。
Saturday, February 28, 2015
Visual StudioとAndroid StudioでOpenGLを使ったマルチプラットフォーム開発
実際に構築してみました。ここにコミットしています。
ソースを共用しながら、AndroidとWindowsの両方で動かせるようにしてみました。C++を主に使うのでNDKも入れておきます。
Androidのassetsフォルダにテクスチャやシェーダーを配置します。ただ、eclipse時代と違ってAndroid Studioはデフォルトで app/src/main/assetsとすごく深い場所にあります。これでは不便なので、build.gradle に以下のように書いて使いやすい場所を指定します。(同時にC++のソースも浅い場所に移動しています)
AndroidのassetsはC++から直接アクセスできないので、Javaを経由します。以下のようなヘルパークラスを作っておき、C++から使います。
Javaでは指定のファイル名でassetsフォルダから読みだしてバイト列にしてC++側に返します。C++側でJavaからバイト列をもらい、改めてcallocでメモリを割り当て直してコピーしています。callocでサイズを+1しているのは、テキストファイルの終端に'\0'を入れたいためです。
GDI+とOpenGLではRGBAの並びが違うのでビットシフトで置き換えています。
OpenGLは以前作ったWGLGrabberを使って関数ポインタを取得しています。
基本的に出来る限りC++で処理すると決めたので、onTouchEventを使ったタッチ等のイベントは全部JavaからC++に渡します。また、GLSurfaceViewを使うので、onDrawFrameからC++のレンダラーを呼び出すことになります。
注意すべきはJavaのスレッドが2つあることです。onTouchEventはUIスレッド、onDrawFrameはレンダースレッドです。
今回は簡単にするため、タッチの座標だけ保存しておいてonDrawFrameの中からタッチイベントもC++に渡します。こうすることでC++は常にレンダースレッドで実行されることが保証されます。
将来的にはマルチスレッドの同期取りはC++側に移動したほうが最適化しやすいかもしれません。
ソースを共用しながら、AndroidとWindowsの両方で動かせるようにしてみました。C++を主に使うのでNDKも入れておきます。
Android StudioでassetsフォルダやC++のソースを階層の浅い場所に移動
Androidのassetsフォルダにテクスチャやシェーダーを配置します。ただ、eclipse時代と違ってAndroid Studioはデフォルトで app/src/main/assetsとすごく深い場所にあります。これでは不便なので、build.gradle に以下のように書いて使いやすい場所を指定します。(同時にC++のソースも浅い場所に移動しています)
main.assets.srcDirsはassetsを置く場所、main.jni.srcDirsはC++のファイルを置く場所です。'../../assets'のようにAndroid Studioのプロジェクトファイルより上位のフォルダも指定できることがわかりました。
Visual Studioからも同じソースやファイルを使うので、この任意のフォルダ位置を指定する機能は重宝します。
assetsフォルダをC++から使う
Javaでは指定のファイル名でassetsフォルダから読みだしてバイト列にしてC++側に返します。C++側でJavaからバイト列をもらい、改めてcallocでメモリを割り当て直してコピーしています。callocでサイズを+1しているのは、テキストファイルの終端に'\0'を入れたいためです。
テクスチャをJava経由でC++から生成
Android機はGPUによって対応している圧縮テクスチャフォーマットが違うので面倒です。この対応は後で考える事とし、とりあえず手始めにjpgやpngなどをまず使えるようにしておきます。
AndroidではJavaから直接テクスチャを生成すると簡単です。assetsフォルダからBitmapFactory.decodeStreamでBitmapを生成し、更にBitmapからGLUtils.texImage2Dでテクスチャを生成します。JavaからC++に渡すのはGLuint型(Javaではint型)の生成済みのテクスチャを表すtexture nameだけなのでシンプルに実装できます。
それぞれAndroid版と同名のローダを作ります。
LoadFileはassetsフォルダのファイルをfopenでファイルを読み込むだけです。
LoadTextureViaOSではGDI+を使うことで、アルファチャンネル付きの32bitのrawデータが得られます。ここからテクスチャを生成しています。
AndroidではJavaから直接テクスチャを生成すると簡単です。assetsフォルダからBitmapFactory.decodeStreamでBitmapを生成し、更にBitmapからGLUtils.texImage2Dでテクスチャを生成します。JavaからC++に渡すのはGLuint型(Javaではint型)の生成済みのテクスチャを表すtexture nameだけなのでシンプルに実装できます。
Windows版の対応
それぞれAndroid版と同名のローダを作ります。
LoadFileはassetsフォルダのファイルをfopenでファイルを読み込むだけです。
LoadTextureViaOSではGDI+を使うことで、アルファチャンネル付きの32bitのrawデータが得られます。ここからテクスチャを生成しています。
GDI+とOpenGLではRGBAの並びが違うのでビットシフトで置き換えています。
OpenGLは以前作ったWGLGrabberを使って関数ポインタを取得しています。
ネイティブクラスの設計
基本的に出来る限りC++で処理すると決めたので、onTouchEventを使ったタッチ等のイベントは全部JavaからC++に渡します。また、GLSurfaceViewを使うので、onDrawFrameからC++のレンダラーを呼び出すことになります。
注意すべきはJavaのスレッドが2つあることです。onTouchEventはUIスレッド、onDrawFrameはレンダースレッドです。
今回は簡単にするため、タッチの座標だけ保存しておいてonDrawFrameの中からタッチイベントもC++に渡します。こうすることでC++は常にレンダースレッドで実行されることが保証されます。
将来的にはマルチスレッドの同期取りはC++側に移動したほうが最適化しやすいかもしれません。
まとめ
OpenGLを使った開発はOpenGL関連のコードが大多数を占めるので、このようにプラットフォーム依存する場所を開発の始めに括っておくと以後のマルチプラットフォーム化が楽になります。
Labels:
Android,
Android Studio,
Japanese,
OpenGL,
OpenGL ES,
Visual Studio,
WGL,
Windows
Subscribe to:
Posts (Atom)