xref: /aosp_15_r20/external/oboe/samples/RhythmGame/src/main/cpp/Game.cpp (revision 05767d913155b055644481607e6fa1e35e2fe72c)
1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include <utils/logging.h>
18 #include <thread>
19 #include <cinttypes>
20 
21 #include "Game.h"
22 
Game(AAssetManager & assetManager)23 Game::Game(AAssetManager &assetManager): mAssetManager(assetManager) {
24 }
25 
load()26 void Game::load() {
27 
28     if (!openStream()) {
29         mGameState = GameState::FailedToLoad;
30         return;
31     }
32 
33     if (!setupAudioSources()) {
34         mGameState = GameState::FailedToLoad;
35         return;
36     }
37 
38     scheduleSongEvents();
39 
40     Result result = mAudioStream->requestStart();
41     if (result != Result::OK){
42         LOGE("Failed to start stream. Error: %s", convertToText(result));
43         mGameState = GameState::FailedToLoad;
44         return;
45     }
46 
47     mGameState = GameState::Playing;
48 }
49 
start()50 void Game::start() {
51 
52     // async returns a future, we must store this future to avoid blocking. It's not sufficient
53     // to store this in a local variable as its destructor will block until Game::load completes.
54     mLoadingResult = std::async(&Game::load, this);
55 }
56 
stop()57 void Game::stop(){
58 
59     if (mAudioStream){
60         mAudioStream->stop();
61         mAudioStream->close();
62         mAudioStream.reset();
63     }
64 }
65 
tap(int64_t eventTimeAsUptime)66 void Game::tap(int64_t eventTimeAsUptime) {
67 
68     if (mGameState != GameState::Playing){
69         LOGW("Game not in playing state, ignoring tap event");
70     } else {
71         mClap->setPlaying(true);
72 
73         int64_t nextClapWindowTimeMs;
74         if (mClapWindows.pop(nextClapWindowTimeMs)){
75 
76             // Convert the tap time to a song position
77             int64_t tapTimeInSongMs = mSongPositionMs + (eventTimeAsUptime - mLastUpdateTime);
78             TapResult result = getTapResult(tapTimeInSongMs, nextClapWindowTimeMs);
79             mUiEvents.push(result);
80         }
81     }
82 }
83 
tick()84 void Game::tick(){
85 
86     switch (mGameState){
87         case GameState::Playing:
88             TapResult r;
89             if (mUiEvents.pop(r)) {
90                 renderEvent(r);
91             } else {
92                 SetGLScreenColor(kPlayingColor);
93             }
94             break;
95 
96         case GameState::Loading:
97             SetGLScreenColor(kLoadingColor);
98             break;
99 
100         case GameState::FailedToLoad:
101             SetGLScreenColor(kLoadingFailedColor);
102             break;
103     }
104 }
105 
onSurfaceCreated()106 void Game::onSurfaceCreated() {
107     SetGLScreenColor(kLoadingColor);
108 }
109 
onSurfaceChanged(int widthInPixels,int heightInPixels)110 void Game::onSurfaceChanged(int widthInPixels, int heightInPixels) {
111 }
112 
onSurfaceDestroyed()113 void Game::onSurfaceDestroyed() {
114 }
115 
onAudioReady(AudioStream * oboeStream,void * audioData,int32_t numFrames)116 DataCallbackResult Game::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) {
117 
118     auto *outputBuffer = static_cast<float *>(audioData);
119 
120     int64_t nextClapEventMs;
121 
122     for (int i = 0; i < numFrames; ++i) {
123 
124         mSongPositionMs = convertFramesToMillis(
125                 mCurrentFrame,
126                 mAudioStream->getSampleRate());
127 
128         if (mClapEvents.peek(nextClapEventMs) && mSongPositionMs >= nextClapEventMs){
129             mClap->setPlaying(true);
130             mClapEvents.pop(nextClapEventMs);
131         }
132         mMixer.renderAudio(outputBuffer+(oboeStream->getChannelCount()*i), 1);
133         mCurrentFrame++;
134     }
135 
136     mLastUpdateTime = nowUptimeMillis();
137 
138     return DataCallbackResult::Continue;
139 }
140 
onErrorAfterClose(AudioStream * audioStream,Result error)141 void Game::onErrorAfterClose(AudioStream *audioStream, Result error) {
142     if (error == Result::ErrorDisconnected){
143         mGameState = GameState::Loading;
144         mAudioStream.reset();
145         mMixer.removeAllTracks();
146         mCurrentFrame = 0;
147         mSongPositionMs = 0;
148         mLastUpdateTime = 0;
149         start();
150     } else {
151         LOGE("Stream error: %s", convertToText(error));
152     }
153 }
154 
155 /**
156  * Get the result of a tap
157  *
158  * @param tapTimeInMillis - The time the tap occurred in milliseconds
159  * @param tapWindowInMillis - The time at the middle of the "tap window" in milliseconds
160  * @return TapResult can be Early, Late or Success
161  */
getTapResult(int64_t tapTimeInMillis,int64_t tapWindowInMillis)162 TapResult Game::getTapResult(int64_t tapTimeInMillis, int64_t tapWindowInMillis){
163     LOGD("Tap time %" PRId64 ", tap window time: %" PRId64, tapTimeInMillis, tapWindowInMillis);
164     if (tapTimeInMillis <= tapWindowInMillis + kWindowCenterOffsetMs) {
165         if (tapTimeInMillis >= tapWindowInMillis - kWindowCenterOffsetMs) {
166             return TapResult::Success;
167         } else {
168             return TapResult::Early;
169         }
170     } else {
171         return TapResult::Late;
172     }
173 }
174 
openStream()175 bool Game::openStream() {
176 
177     // Create an audio stream
178     AudioStreamBuilder builder;
179     builder.setFormat(AudioFormat::Float);
180     builder.setFormatConversionAllowed(true);
181     builder.setPerformanceMode(PerformanceMode::LowLatency);
182     builder.setSharingMode(SharingMode::Exclusive);
183     builder.setSampleRate(48000);
184     builder.setSampleRateConversionQuality(
185             SampleRateConversionQuality::Medium);
186     builder.setChannelCount(2);
187     builder.setDataCallback(this);
188     builder.setErrorCallback(this);
189     Result result = builder.openStream(mAudioStream);
190     if (result != Result::OK){
191         LOGE("Failed to open stream. Error: %s", convertToText(result));
192         return false;
193     }
194 
195     mMixer.setChannelCount(mAudioStream->getChannelCount());
196 
197     return true;
198 }
199 
setupAudioSources()200 bool Game::setupAudioSources() {
201 
202     // Set the properties of our audio source(s) to match that of our audio stream
203     AudioProperties targetProperties {
204             .channelCount = mAudioStream->getChannelCount(),
205             .sampleRate = mAudioStream->getSampleRate()
206     };
207 
208     // Create a data source and player for the clap sound
209     std::shared_ptr<AAssetDataSource> mClapSource {
210             AAssetDataSource::newFromCompressedAsset(mAssetManager, kClapFilename, targetProperties)
211     };
212     if (mClapSource == nullptr){
213         LOGE("Could not load source data for clap sound");
214         return false;
215     }
216     mClap = std::make_unique<Player>(mClapSource);
217 
218     // Create a data source and player for our backing track
219     std::shared_ptr<AAssetDataSource> backingTrackSource {
220             AAssetDataSource::newFromCompressedAsset(mAssetManager, kBackingTrackFilename, targetProperties)
221     };
222     if (backingTrackSource == nullptr){
223         LOGE("Could not load source data for backing track");
224         return false;
225     }
226     mBackingTrack = std::make_unique<Player>(backingTrackSource);
227     mBackingTrack->setPlaying(true);
228     mBackingTrack->setLooping(true);
229 
230     // Add both players to a mixer
231     mMixer.addTrack(mClap.get());
232     mMixer.addTrack(mBackingTrack.get());
233 
234     return true;
235 }
236 
scheduleSongEvents()237 void Game::scheduleSongEvents() {
238 
239     for (auto t : kClapEvents) mClapEvents.push(t);
240     for (auto t : kClapWindows) mClapWindows.push(t);
241 }
242