今日は、先日登壇したDroidKaigiにて紹介したOboeライブラリを使用したサンプルコードを読んでいきます。
OboeとはAndroid NDKの高性能オーディオAPIを使用するC++ライブラリです。
ちなみに、OboeはハイパフォーマンスなゲームオーディオとしてAndroid Game SDKに組み込まれています*1。
なぜOboeを使う必要があるのか
Oboe はAndroid デバイスの 99 % 以上で最低レイテンシを提供している実績があります。 VoicyのAndroidアプリでは現状、喫緊でOboeを採用する必要性は正直ないです。
ですが、収録前の音声読み込み時間やSEの再生までのレイテンシー, 端末依存による音質の違いなど、音質やパフォーマンスチューニングの余地はたくさんあり、ナレッジを蓄積していきた考えです。
C++で書く必要がありますが、コード量は少なく大変使いやすいコードベースです。また英語ではありますが、手厚いコミュニティサポートもあります。加えて、昨今のカラオケや音ゲーなど音を扱うアプリがOboeを採用する動きがあります。*2
基本的な実装
オーディオストリームを作成する
ストリームの構築を行います。 AAudioStream で表現される「オーディオ ストリーム」に対して読み書きすることにより、オーディオ データをやり取りします。

bool AudioEngine::start() {
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
// 浮動小数点数の音声フォーマット
AAudioStreamBuilder_setFormat(streamBuilder, AAUDIO_FORMAT_PCM_FLOAT);
// 出力はモノラル
AAudioStreamBuilder_setChannelCount(streamBuilder, 1);
// 低レイテンシのパフォーマンス モードを設定
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
// AAudioStream_writeよりデータ コールバック関数のアプローチのほうが低レイテンシ アプリに適している。
// データ コールバック関数は、ストリームに音声データが必要になるたびに、優先度の高いスレッドから呼び出されます。
// これで dataCallback 関数の準備ができたので、start() メソッドからそれを使用するようストリームに指示することも簡単に行えます
// (:: は、関数がグローバル名前空間にあることを示します)。
AAudioStreamBuilder_setErrorCallback(streamBuilder, ::errorCallback, this);
// AAUDIO_OK 以外の結果が出た場合は、出力を Android Studio の Android Monitor ウィンドウ
// に記録し、false を返します。
aaudio_result_t result = AAudioStreamBuilder_openStream(streamBuilder, &stream_);
if (result != AAUDIO_OK) {
return false;
}
// すべての設定が完了したので、ストリームを開始して、音声データの使用とデータ コールバックのトリガーを開始できます。
result = AAudioStream_requestStart(stream_);
if (result != AAUDIO_OK) {
__android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error starting stream %s",
AAudio_convertResultToText(result));
return false;
}
AAudioStreamBuilder_delete(streamBuilder);
return true;
}
音ゲーのミニマムをつくる
Oboeのgithubリポジトリにはいくつかサンプルコードが含まれていて、今回はその中でも簡単な音楽ゲーム [RhythmGame]のコードを参照しながらなにやってるかみていきます。
どんなゲーム?
Oboe codelab pic.twitter.com/qxOWr9MOlZ
— Miyuki Onuma (@numaMyk) 2022年11月22日
音に合わせてタイミングよく画面タップするゲームです。 4拍子のトラックが連続的にループします。 ゲーム開始時に、すぐに音楽が再生され、最初の小節で3拍で手拍子の音が流れます。
ユーザーは、2小節目が始まったら画面をタップして、最初に流れた1小節目と同じタイミングで3回拍手を繰り返します。

タップするたびに、拍手音が鳴ります。タイミングよくタップすると、画面が緑色に点滅します。 タップするタイミングが早すぎるとオレンジ色に、遅すぎると紫色に点滅します。
サウンドのミキシング
複数のサウンドを同時に再生するには、それらをミックスする必要があります。 このサンプルにはミキサーオブジェクトが提供されています。
// 拍手音のデータソースとプレーヤーを作成する
std::shared_ptr<AAssetDataSource> mClapSource {
AAssetDataSource::newFromCompressedAsset(mAssetManager, "CLAP.mp3")
};
if (mClapSource == nullptr){
LOGE("Could not load source data for clap sound");
return false;
}
mClap = std::make_unique<Player>(mClapSource);
// BGMのデータソースとプレーヤーを作成します。
std::shared_ptr<AAssetDataSource> backingTrackSource {
AAssetDataSource::newFromCompressedAsset(mAssetManager, "FUNKY_HOUSE.mp3")
};
if (backingTrackSource == nullptr){
LOGE("Could not load source data for backing track");
return false;
}
mBackingTrack = std::make_unique<Player>(backingTrackSource);
mBackingTrack->setPlaying(true);
mBackingTrack->setLooping(true);
// 両方のプレーヤーをミキサーに加える
mMixer.addTrack(mClap.get());
mMixer.addTrack(mBackingTrack.get());
return true;
音とタップイベントを正確に同期させる
AudioStreamクラスがあります。 onAudioReadyが呼ばれるたびに、BGMのオーディオフレームが(ミキサーを通して)オーディオストリームにレンダリングされます。 書き込まれたフレームの数を数えることで、正確な再生時間と、拍手音を再生するタイミングをえることができます。
これを踏まえて、拍手イベントを正確なタイミングで再生する方法を説明します。
現在再生しているオーディオフレームを曲の位置にミリ秒単位で変換します。 この曲の位置で拍手をする必要があるかどうかをチェックする。必要であれば、再生するといった制御を行います。
DataCallbackResult Game::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) {
float *outputBuffer = static_cast<float *>(audioData);
int64_t nextClapEventMs;
for (int i = 0; i < numFrames; ++i) {
mSongPositionMs = convertFramesToMillis(
mCurrentFrame,
mAudioStream->getSampleRate());
if (mClapEvents.peek(nextClapEventMs) && mSongPositionMs >= nextClapEventMs){
mClap->setPlaying(true);
mClapEvents.pop(nextClapEventMs);
}
mMixer.renderAudio(outputBuffer+(oboeStream->getChannelCount()*i), 1);
mCurrentFrame++;
}
mLastUpdateTime = nowUptimeMillis();
return DataCallbackResult::Continue;
}
画面上のUIとオーディオを同期させる
Game.cppの、scheduleSongEventsというメソッドで、拍手イベントを待ち受けます。
void Game::scheduleSongEvents() {
mClapEvents.push(0);
mClapEvents.push(500);
mClapEvents.push(1000);
}
その他
音声の再生や録音は以下のCodelabから学べます。 https://developer.android.com/codelabs/making-waves-2-sampler?hl=ja#6