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 // If our audio stream is expecting 16-bit samples we need to render our floats into a separate
119 // buffer then convert them into 16-bit ints
120 bool is16Bit = (oboeStream->getFormat() == AudioFormat::I16);
121 float *outputBuffer = (is16Bit) ? mConversionBuffer.get() : static_cast<float *>(audioData);
122
123 int64_t nextClapEventMs;
124
125 for (int i = 0; i < numFrames; ++i) {
126
127 mSongPositionMs = convertFramesToMillis(
128 mCurrentFrame,
129 mAudioStream->getSampleRate());
130
131 if (mClapEvents.peek(nextClapEventMs) && mSongPositionMs >= nextClapEventMs){
132 mClap->setPlaying(true);
133 mClapEvents.pop(nextClapEventMs);
134 }
135 mMixer.renderAudio(outputBuffer+(oboeStream->getChannelCount()*i), 1);
136 mCurrentFrame++;
137 }
138
139 if (is16Bit){
140 oboe::convertFloatToPcm16(outputBuffer,
141 static_cast<int16_t*>(audioData),
142 numFrames * oboeStream->getChannelCount());
143 }
144
145 mLastUpdateTime = nowUptimeMillis();
146
147 return DataCallbackResult::Continue;
148 }
149
onErrorAfterClose(AudioStream * oboeStream,Result error)150 void Game::onErrorAfterClose(AudioStream *oboeStream, Result error){
151 LOGE("The audio stream was closed, please restart the game. Error: %s", convertToText(error));
152 };
153
154 /**
155 * Get the result of a tap
156 *
157 * @param tapTimeInMillis - The time the tap occurred in milliseconds
158 * @param tapWindowInMillis - The time at the middle of the "tap window" in milliseconds
159 * @return TapResult can be Early, Late or Success
160 */
getTapResult(int64_t tapTimeInMillis,int64_t tapWindowInMillis)161 TapResult Game::getTapResult(int64_t tapTimeInMillis, int64_t tapWindowInMillis){
162 LOGD("Tap time %" PRId64 ", tap window time: %" PRId64, tapTimeInMillis, tapWindowInMillis);
163 if (tapTimeInMillis <= tapWindowInMillis + kWindowCenterOffsetMs) {
164 if (tapTimeInMillis >= tapWindowInMillis - kWindowCenterOffsetMs) {
165 return TapResult::Success;
166 } else {
167 return TapResult::Early;
168 }
169 } else {
170 return TapResult::Late;
171 }
172 }
173
openStream()174 bool Game::openStream() {
175
176 // Create an audio stream
177 AudioStreamBuilder builder;
178 builder.setDataCallback(this);
179 builder.setErrorCallback(this);
180 builder.setPerformanceMode(PerformanceMode::LowLatency);
181 builder.setSharingMode(SharingMode::Exclusive);
182
183 Result result = builder.openStream(mAudioStream);
184 if (result != Result::OK){
185 LOGE("Failed to open stream. Error: %s", convertToText(result));
186 return false;
187 }
188
189 if (mAudioStream->getFormat() == AudioFormat::I16){
190 mConversionBuffer = std::make_unique<float[]>(
191 (size_t)mAudioStream->getBufferCapacityInFrames() *
192 mAudioStream->getChannelCount());
193 }
194
195 // Reduce stream latency by setting the buffer size to a multiple of the burst size
196 auto setBufferSizeResult = mAudioStream->setBufferSizeInFrames(
197 mAudioStream->getFramesPerBurst() * kBufferSizeInBursts);
198 if (setBufferSizeResult != Result::OK){
199 LOGW("Failed to set buffer size. Error: %s", convertToText(setBufferSizeResult.error()));
200 }
201
202 mMixer.setChannelCount(mAudioStream->getChannelCount());
203
204 return true;
205 }
206
setupAudioSources()207 bool Game::setupAudioSources() {
208
209 // Set the properties of our audio source(s) to match that of our audio stream
210 AudioProperties targetProperties {
211 .channelCount = mAudioStream->getChannelCount(),
212 .sampleRate = mAudioStream->getSampleRate()
213 };
214
215 // Create a data source and player for the clap sound
216 std::shared_ptr<AAssetDataSource> mClapSource {
217 AAssetDataSource::newFromCompressedAsset(mAssetManager, kClapFilename, targetProperties)
218 };
219 if (mClapSource == nullptr){
220 LOGE("Could not load source data for clap sound");
221 return false;
222 }
223 mClap = std::make_unique<Player>(mClapSource);
224
225 // Create a data source and player for our backing track
226 std::shared_ptr<AAssetDataSource> backingTrackSource {
227 AAssetDataSource::newFromCompressedAsset(mAssetManager, kBackingTrackFilename, targetProperties)
228 };
229 if (backingTrackSource == nullptr){
230 LOGE("Could not load source data for backing track");
231 return false;
232 }
233 mBackingTrack = std::make_unique<Player>(backingTrackSource);
234 mBackingTrack->setPlaying(true);
235 mBackingTrack->setLooping(true);
236
237 // Add both players to a mixer
238 mMixer.addTrack(mClap.get());
239 mMixer.addTrack(mBackingTrack.get());
240
241 return true;
242 }
243
scheduleSongEvents()244 void Game::scheduleSongEvents() {
245
246 for (auto t : kClapEvents) mClapEvents.push(t);
247 for (auto t : kClapWindows) mClapWindows.push(t);
248 }
249