Friday, December 25, 2015

OpenSL ESでActivityが非アクティブになった時に音を停止する簡単な方法

前回OpenSL ESでサウンドを再生しましたが、Activityが非アクティブになっても音が鳴り続ける問題がありました。それに対しての簡単な対処法として、バッファを細かく分けてEnqueueしてみました。Enqueueは32768バイト単位でしています。(サイズは適当です)

void Voice::Play(bool loop)
{
if (!IsReady()) {
return;
}
auto playback = [](SLAndroidSimpleBufferQueueItf q, void* context_) {
double now = GetTime();
if (now - systemMisc.GetLastUpdateTime() >= 0.5) {
return;
}
WaveContext* context = (WaveContext*)context_;
int totalSize;
const void* buf = RiffFindChunk(context->fileImg, "data", &totalSize);
int enqueued = context->enqueuedSize;
if (enqueued >= totalSize) {
if (!context->loop) {
return;
}
enqueued = 0;
}
int toEnqueue = std::min(totalSize - enqueued, 32768);
enqueued += toEnqueue;
context->enqueuedSize = enqueued;
SLCall(q, Enqueue, (char*)buf + enqueued - toEnqueue, toEnqueue);
};
SLCall(context->playerPlay, SetPlayState, SL_PLAYSTATE_STOPPED);
SLCall(context->playerBufferQueue, RegisterCallback, playback, context);
context->enqueuedSize = 0;
context->loop = loop;
SLCall(context->playerPlay, SetPlayState, SL_PLAYSTATE_PLAYING);
playback(context->playerBufferQueue, context);
}
最終アップデートからの経過時間が0.5秒を超えたらコールバック無視します。この方法の長所は、コールバック一つでループ再生と非アクティブになった時の音停止を同時に実装でき、コードが簡潔になります。Activityから非アクティブの通知を受け取る必要もありません。短所としては、アップデートループで長い処理をすると音が切れてしまうことです。

また、停止した音を再開する処理を盛り込んでいません。ここは、ゲーム毎の実装依存が多いと思いあえてしませんでした。例えば多くの場合は再開すべき音はBGMだけであったりするでしょう。

AndroidのOpenSL ESはコールバックは別スレッドから来るので特に気をつける必要があります。Android特有の実装についてはNDKのdocs/Additional_library_docs/opensles/index.html に説明があるので、一度目を通しておくと良さそうです。

Saturday, December 12, 2015

実践OpenSL ES、AndroidでWAVファイル再生

OpenSL ESによるサウンドエンジンを使い物になるように書いてみます。

まず、気づくのはDirectXを彷彿させるインターフェース型のAPIであることです。多くのメソッドはSLresultで結果を返し、Destroyでリソースを解放して終了します。素直に書くと冗長なコードになりそうですが、まず最初にDirectX開発者にはおなじみの手法を導入します。

template <class T> void SafeDestroy(T& p)
{
if (p) {
(*p)->Destroy(p);
p = nullptr;
}
}
SLresult _slHandleError(const char* func, int line, const char* command, SLresult r)
{
if (r != SL_RESULT_SUCCESS) {
const char *err = nullptr;
switch (r) {
#define E(er) case er: err = #er; break
E(SL_RESULT_PRECONDITIONS_VIOLATED);
E(SL_RESULT_PARAMETER_INVALID);
E(SL_RESULT_MEMORY_FAILURE);
E(SL_RESULT_RESOURCE_ERROR);
E(SL_RESULT_RESOURCE_LOST);
E(SL_RESULT_BUFFER_INSUFFICIENT);
E(SL_RESULT_CONTENT_CORRUPTED);
E(SL_RESULT_CONTENT_UNSUPPORTED);
#undef E
default:
aflog("%s(%d): err=%d %s\n", func, line, r, command);
return r;
}
aflog("%s(%d): %s %s\n", func, line, err, command);
}
return r;
}
#define SLHandleError(command) _afHandleSLError(__FUNCTION__, __LINE__, #command, command)
#define SLCall(obj,func,...) afHandleSLError((*obj)->func(obj, __VA_ARGS__))
SafeDestroyはDestroy呼び出しと同時にnullptrを代入してダングリングポインタにならないようにします。

SLHandleErrorというのは、各種メソッドがエラーコードを返したらソースコード上の行数と関数呼び出しの概要をログに出力するためのマクロです。Direct3D9のサンプルでは"V"という一文字のマクロが多用されていましたが、それです。

SLCallは、インターフェース呼び出しをすっきり書きたいがために書いたマクロです。これを使ってエンジンの初期化と終了を書いてみます。

class SL {
SLObjectItf engineObject = nullptr;
SLEngineItf engineEngine = nullptr;
SLObjectItf outputMixObject = nullptr;
public:
SL() {
SLHandleError(slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr));
SLCall(engineObject, Realize, SL_BOOLEAN_FALSE);
SLCall(engineObject, GetInterface, SL_IID_ENGINE, &engineEngine);
SLInterfaceID ids = SL_IID_ENVIRONMENTALREVERB;
SLboolean req = SL_BOOLEAN_FALSE;
SLCall(engineEngine, CreateOutputMix, &outputMixObject, 1, &ids, &req);
SLCall(outputMixObject, Realize, SL_BOOLEAN_FALSE);
}
~SL() {
SafeDestroy(outputMixObject);
SafeDestroy(engineObject);
engineEngine = nullptr;
}
SLEngineItf GetEngine(){ return engineEngine; }
SLObjectItf GetOutputMixObject() { return outputMixObject; }
};
static SL sl;
エンジンの初期化と終了が書けました。

DirectXから来ると戸惑うのが、SLObjectItfとその他インターフェースに分かれていることです。OpenSLでは全てのインスタンスの実体はSLObjectItfであって、何らかのメソッド呼び出しが必要になったらSLObjectItfから追加のインターフェースを取得します。また、SLObjectItfをDestroyするとSLObjectItfから取得したインターフェースも一緒に無効になります。

engineEngineがSafeDestroyではなくnullptrを代入しているのは、engineObjectをSafeDestroyした時に一緒に消滅しているからです。

次は、WAVファイルをロードして"audio player"を作ってみます。

struct WaveFormatEx {
uint16_t tag, channels;
uint32_t samplesPerSecond, averageBytesPerSecond;
uint16_t blockAlign, bitsPerSample;
};
struct WaveContext
{
SLObjectItf playerObject;
SLPlayItf playerPlay;
SLAndroidSimpleBufferQueueItf playerBufferQueue;
void *fileImg;
int enqueuedSize;
bool loop;
};
void Voice::Create(const char* fileName)
{
context = new WaveContext;
memset(context, 0, sizeof(*context));
bool result = false;
result = !!(context->fileImg = LoadFile(fileName));
assert(result);
const WaveFormatEx* wfx = (WaveFormatEx*)RiffFindChunk(context->fileImg, "fmt ");
assert(wfx);
SLDataLocator_AndroidSimpleBufferQueue q = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
SLDataFormat_PCM f = {SL_DATAFORMAT_PCM, wfx->channels, wfx->samplesPerSecond * 1000, wfx->bitsPerSample, wfx->bitsPerSample, wfx->channels == 2 ? SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT : SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};
SLDataSource src = {&q, &f};
SLDataLocator_OutputMix m = {SL_DATALOCATOR_OUTPUTMIX, sl.GetOutputMixObject()};
SLDataSink sink = {&m, nullptr};
SLInterfaceID ids = SL_IID_ANDROIDSIMPLEBUFFERQUEUE;
SLboolean req = SL_BOOLEAN_TRUE;
SLCall(sl.GetEngine(), CreateAudioPlayer, &context->playerObject, &src, &sink, 1, &ids, &req);
SLCall(context->playerObject, Realize, SL_BOOLEAN_FALSE);
SLCall(context->playerObject, GetInterface, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &context->playerBufferQueue);
SLCall(context->playerObject, GetInterface, SL_IID_PLAY, &context->playerPlay);
}
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ファイルの再生部です。

void Voice::Play(bool loop)
{
if (!IsReady()) {
return;
}
auto playback = [](SLAndroidSimpleBufferQueueItf q, void* context_) {
WaveContext* context = (WaveContext*)context_;
int totalSize;
const void* buf = RiffFindChunk(context->fileImg, "data", &totalSize);
SLCall(q, Enqueue, (char*)buf, totalSize);
};
auto doNothing = [](SLAndroidSimpleBufferQueueItf, void*) {};
SLCall(context->playerPlay, SetPlayState, SL_PLAYSTATE_STOPPED);
SLCall(context->playerBufferQueue, RegisterCallback, loop ? playback : doNothing, context);
SLCall(context->playerPlay, SetPlayState, SL_PLAYSTATE_PLAYING);
playback(context->playerBufferQueue, context);
}
view raw opensl_play.cpp hosted with ❤ by GitHub
Enqueueで再生するバッファを指定します。ループ再生する場合はコールバックを登録して、コールバックからEnqueueを繰り返します。コールバック設定前にSL_PLAYSTATE_STOPPEDを明示するのはステートがSL_PLAYSTATE_STOPPEDではない場合にSL_RESULT_PRECONDITIONS_VIOLATEDエラーが発生するためです。これはPlayメンバ関数が二回目に呼ばれた場合に起こります。

これで、OpenSLの初期化からWAVファイルの再生までができました。ところで、この方法だとActivityが非アクティブになっても音が鳴り続けるという問題があります。この解決は次回書きます。

Saturday, December 5, 2015

Luaのrequireで独自ファイルシステム上のスクリプトをロード

requireでAndroidのAPKのassetsフォルダからスクリプトを読み込みたくて、方法を探ってみました。requireはCのincludeに似た使い方をしますが、requireの実体はモジュール名を引数に取るただの関数です。

dofileとrequireはよく似た関数ですが、いくつか違いがあります。まずrequireは通常拡張子".lua"無しでモジュールを指定します。これを実装してみます。

function require(m)
dofile(m..".lua")
end

前回、dofileのファイルシステムを置き換えているので置き換え版のdofileを流用することでLuaだけで書けました。厳密にはpackage.pathを考慮していないという問題がありますが、これだけで用足りることも多いと思います。

ところで、requireで比較的重要なdofileとの違いが、重複requireガードです。これはpackage.loadedテーブルにロード結果が記録される事で行われます。実際にテーブルの中身をprintで出力してみると、以下のようになりました。(左がキー、右が値)
debug   table: 00C9D770
vec4    true
coroutine       table: 00C98AA8
_G      table: 00C92760
package table: 00C98828
math    table: 00C9A2D0
my_class        userdata: 00C9C3B8
io      table: 00C98C38
os      table: 00C99880
bind_win        true
string  table: 00C99FB0
utf8    table: 00C9D6D0
table   table: 00C98BE8
キーを見ると、luaL_openlibsでロードされるLuaの標準ライブラリに混じってrequireでロードしたモジュール(ここではvec4, bind_win, my_classがそれです)の名前が見えます。対して値はそのモジュールをロードした時のreturn値が格納されています。(return値が無い場合はtrue)

また、この値は同じモジュールの二回目以降のrequireで返される値としても使われます。以下の例は、dofileが毎回math.random()を実行するのに対して、requireは初回呼び出し時の値をpackage.loadedにキャッシュする事を確かめるものです。

print("calling with dofile", dofile("random.lua"), dofile("random.lua"), dofile("random.lua"))
print("calling with require", require("random"), require("random"), require("random"))
assert(require("random") == package.loaded["random"])
view raw caller.lua hosted with ❤ by GitHub
return math.random()
view raw random.lua hosted with ❤ by GitHub

以上を踏まえたファイルシステム置き換え版のrequireは以下のようになります。

function require(m)
package.loaded[m] = package.loaded[m] or dofile(m..".lua") or true
return package.loaded[m]
end