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が非アクティブになっても音が鳴り続けるという問題があります。この解決は次回書きます。

No comments:

Post a Comment