xref: /aosp_15_r20/external/oboe/samples/RhythmGame/README.md (revision 05767d913155b055644481607e6fa1e35e2fe72c)
1*05767d91SRobert WuRhythm Game sample
2*05767d91SRobert Wu==================
3*05767d91SRobert Wu
4*05767d91SRobert WuThis sample demonstrates how to build a simple musical game. The objective of the game is to clap in time to a song by copying what you hear. You do this by listening to the clap sounds, then tapping on the screen to copy those claps.
5*05767d91SRobert Wu
6*05767d91SRobert WuFor a step-by-step guide on how this game works and how to build it check out this codelab: [Build a Musical Game using Oboe](https://developer.android.com/codelabs/musicalgame-using-oboe).
7*05767d91SRobert Wu
8*05767d91SRobert Wu
9*05767d91SRobert WuScreenshots
10*05767d91SRobert Wu-----------
11*05767d91SRobert WuThe UI is deliberately very simple - just tap anywhere in the grey area after hearing the claps. The UI will change color to indicate the game state. The colors are:
12*05767d91SRobert Wu
13*05767d91SRobert Wu- Yellow: Game is loading (assets are being decompressed)
14*05767d91SRobert Wu- Grey: Game is being played
15*05767d91SRobert Wu- Orange: You tapped too early
16*05767d91SRobert Wu- Green: You tapped on time
17*05767d91SRobert Wu- Purple: You tapped too late
18*05767d91SRobert Wu- Red: There was a problem loading the game (check logcat output)
19*05767d91SRobert Wu
20*05767d91SRobert Wu![RhythmGame Screenshot](images/RhythmGame-screenshot.png)
21*05767d91SRobert Wu
22*05767d91SRobert Wu
23*05767d91SRobert Wu### Audio timeline
24*05767d91SRobert Wu![Game timeline](images/1-timeline.png "Game timeline")
25*05767d91SRobert Wu
26*05767d91SRobert WuThe game plays the clap sounds on the first 3 beats of the bar. These are played in time with the backing track.
27*05767d91SRobert Wu
28*05767d91SRobert Wu When the user taps on the screen, a clap sound is played and the game checks whether the tap occurred within an acceptable time window.
29*05767d91SRobert Wu
30*05767d91SRobert Wu### Architecture
31*05767d91SRobert Wu
32*05767d91SRobert Wu![Game architecture](images/2-architecture.png "Game architecture")
33*05767d91SRobert Wu
34*05767d91SRobert WuOboe provides the [`AudioStream`](https://github.com/google/oboe/blob/main/include/oboe/AudioStream.h) class and associated objects to allow the sample to output audio data to the audio device. All other objects are provided by the sample.
35*05767d91SRobert Wu
36*05767d91SRobert WuEach time the `AudioStream` needs more audio data it calls [`AudioDataCallback::onAudioReady`](https://github.com/google/oboe/blob/main/include/oboe/AudioStreamCallback.h). This passes a container array named `audioData` to the `Game` object which must then fill the array with `numFrames` of audio frames.
37*05767d91SRobert Wu
38*05767d91SRobert Wu
39*05767d91SRobert Wu![onAudioReady signature](images/3-audioData.png "onAudioReady signature")
40*05767d91SRobert Wu
41*05767d91SRobert Wu### Latency optimizations
42*05767d91SRobert WuThe sample uses the following optimizations to obtain a low latency audio stream:
43*05767d91SRobert Wu
44*05767d91SRobert Wu- Performance mode set to [Low Latency](https://github.com/google/oboe/blob/main/FullGuide.md#setting-performance-mode)
45*05767d91SRobert Wu- Sharing mode set to [Exclusive](https://github.com/google/oboe/blob/main/FullGuide.md#sharing-mode)
46*05767d91SRobert Wu- Buffer size set to twice the number of frames in a burst (double buffering)
47*05767d91SRobert Wu
48*05767d91SRobert Wu### Audio rendering
49*05767d91SRobert Wu
50*05767d91SRobert WuThe `IRenderableAudio` interface (abstract class) represents objects which can produce frames of audio data. The `Player` and `Mixer` objects both implement this interface.
51*05767d91SRobert Wu
52*05767d91SRobert WuBoth the clap sound and backing tracks are represented by `Player` objects which are then mixed together using a `Mixer`.
53*05767d91SRobert Wu
54*05767d91SRobert Wu![Audio rendering](images/4-audio-rendering.png "Audio rendering")
55*05767d91SRobert Wu
56*05767d91SRobert Wu### Sharing objects with the audio thread
57*05767d91SRobert Wu
58*05767d91SRobert WuIt is very important that the audio thread (which calls the `onAudioReady` method) is never blocked. Blocking can cause underruns and audio glitches. To avoid blocking we use a `LockFreeQueue` to share information between the audio thread and other threads. The following diagram shows how claps are enqueued by pushing the clap times (in milliseconds) onto the queue, then dequeuing the clap time when the clap is played.
59*05767d91SRobert Wu
60*05767d91SRobert Wu![Lock free queue](images/5-lockfreequeue.png "Lock free queue")
61*05767d91SRobert Wu
62*05767d91SRobert WuWe also use [atomics](http://en.cppreference.com/w/cpp/atomic/atomic) to ensure that threads see a consistent view of any shared primitives.
63*05767d91SRobert Wu
64*05767d91SRobert Wu### Keeping UI events and audio in sync
65*05767d91SRobert Wu
66*05767d91SRobert WuWhen a tap event arrives on the UI thread it only contains the time (milliseconds since boot) that the event occurred. We need to figure out what the song position was when the tap occurred.
67*05767d91SRobert Wu
68*05767d91SRobert WuTo do this we keep track of the song position and the time it was last updated. These values are updated each time the `onAudioReady` method is called. This enables us to keep the UI in sync with the audio timeline.
69*05767d91SRobert Wu
70*05767d91SRobert Wu![Audio/UI synchronization](images/6-audio-ui-sync.png "Audio/UI synchronization")
71*05767d91SRobert Wu
72*05767d91SRobert Wu### Calculating whether a tap was successful
73*05767d91SRobert WuOnce we know when the user tapped in the song, we can calculate whether that tap was successful i.e whether it fell within an acceptable time range. This range is known as the "tap window".
74*05767d91SRobert Wu
75*05767d91SRobert Wu![Tap window calculation](images/7-tap-window.png "Tap window calculation")
76*05767d91SRobert Wu
77*05767d91SRobert WuOnce we know the result of the tap the UI is updated with a color to give the user visual feedback. This is done in `getTapResult`.
78*05767d91SRobert Wu
79*05767d91SRobert WuNote that once a tap has been received the tap window is removed from the queue - the user only gets one chance to get their tap right!
80*05767d91SRobert Wu
81*05767d91SRobert Wu### Use of compressed audio assets
82*05767d91SRobert WuIn order to reduce APK size this game uses MP3 files for its audio assets. These are extracted on game startup in `AAssetDataSource::newFromCompressedAsset`. A yellow screen will be shown during this process.
83*05767d91SRobert Wu
84*05767d91SRobert WuBy default the game uses `NDKExtractor` for asset extraction and decoding. Under the hood this uses the [NDK Media APIs](https://developer.android.com/ndk/reference/group/media).
85*05767d91SRobert Wu
86*05767d91SRobert WuThere are some limitations with this approach:
87*05767d91SRobert Wu
88*05767d91SRobert Wu- Only available on API 21 and above
89*05767d91SRobert Wu- No resampling: The extracted output format will match the input format of the MP3. In this case a sample rate of 48000. If your audio stream's sample rate doesn't match the assets will not be extracted and an error will be displayed in logcat.
90*05767d91SRobert Wu- 16-bit output only.
91*05767d91SRobert Wu
92*05767d91SRobert WuA faster, more versatile solution is to use [FFmpeg](https://www.ffmpeg.org/). To do this follow [the instructions here](https://medium.com/@donturner/using-ffmpeg-for-faster-audio-decoding-967894e94e71) and use the `ffmpegExtractor` build variant found in `app.gradle`. The extraction will then be done by `FFmpegExtractor`.
93