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