今日は、先日登壇した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