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