aboutsummaryrefslogtreecommitdiff
path: root/externals/oboe/samples/RhythmGame/src/main/cpp
diff options
context:
space:
mode:
authorswurl <swurl@swurl.xyz>2025-05-31 02:33:02 -0400
committerswurl <swurl@swurl.xyz>2025-05-31 02:33:02 -0400
commit6c655321e6b04b8648802e296bb5ca11d290ae68 (patch)
tree5c906511e4a2910ce8b806bb9d763b524c28619e /externals/oboe/samples/RhythmGame/src/main/cpp
parentc0cceff365f754f8859f4c9a0362ebdba7ea388b (diff)
Move dead submodules in-tree
Signed-off-by: swurl <swurl@swurl.xyz>
Diffstat (limited to 'externals/oboe/samples/RhythmGame/src/main/cpp')
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/Game.cpp241
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/Game.h85
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/GameConstants.h60
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp86
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h50
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/DataSource.h32
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.cpp318
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.h55
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.cpp199
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.h33
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.cpp59
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.h60
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/native-lib.cpp93
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.cpp33
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.h58
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/utils/LockFreeQueue.h154
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.cpp40
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.h42
-rw-r--r--externals/oboe/samples/RhythmGame/src/main/cpp/utils/logging.h32
19 files changed, 1730 insertions, 0 deletions
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/Game.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/Game.cpp
new file mode 100644
index 0000000000..199f4cce14
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/Game.cpp
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <utils/logging.h>
+#include <thread>
+#include <cinttypes>
+
+#include "Game.h"
+
+Game::Game(AAssetManager &assetManager): mAssetManager(assetManager) {
+}
+
+void Game::load() {
+
+ if (!openStream()) {
+ mGameState = GameState::FailedToLoad;
+ return;
+ }
+
+ if (!setupAudioSources()) {
+ mGameState = GameState::FailedToLoad;
+ return;
+ }
+
+ scheduleSongEvents();
+
+ Result result = mAudioStream->requestStart();
+ if (result != Result::OK){
+ LOGE("Failed to start stream. Error: %s", convertToText(result));
+ mGameState = GameState::FailedToLoad;
+ return;
+ }
+
+ mGameState = GameState::Playing;
+}
+
+void Game::start() {
+
+ // async returns a future, we must store this future to avoid blocking. It's not sufficient
+ // to store this in a local variable as its destructor will block until Game::load completes.
+ mLoadingResult = std::async(&Game::load, this);
+}
+
+void Game::stop(){
+
+ if (mAudioStream){
+ mAudioStream->stop();
+ mAudioStream->close();
+ mAudioStream.reset();
+ }
+}
+
+void Game::tap(int64_t eventTimeAsUptime) {
+
+ if (mGameState != GameState::Playing){
+ LOGW("Game not in playing state, ignoring tap event");
+ } else {
+ mClap->setPlaying(true);
+
+ int64_t nextClapWindowTimeMs;
+ if (mClapWindows.pop(nextClapWindowTimeMs)){
+
+ // Convert the tap time to a song position
+ int64_t tapTimeInSongMs = mSongPositionMs + (eventTimeAsUptime - mLastUpdateTime);
+ TapResult result = getTapResult(tapTimeInSongMs, nextClapWindowTimeMs);
+ mUiEvents.push(result);
+ }
+ }
+}
+
+void Game::tick(){
+
+ switch (mGameState){
+ case GameState::Playing:
+ TapResult r;
+ if (mUiEvents.pop(r)) {
+ renderEvent(r);
+ } else {
+ SetGLScreenColor(kPlayingColor);
+ }
+ break;
+
+ case GameState::Loading:
+ SetGLScreenColor(kLoadingColor);
+ break;
+
+ case GameState::FailedToLoad:
+ SetGLScreenColor(kLoadingFailedColor);
+ break;
+ }
+}
+
+void Game::onSurfaceCreated() {
+ SetGLScreenColor(kLoadingColor);
+}
+
+void Game::onSurfaceChanged(int widthInPixels, int heightInPixels) {
+}
+
+void Game::onSurfaceDestroyed() {
+}
+
+DataCallbackResult Game::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) {
+
+ auto *outputBuffer = static_cast<float *>(audioData);
+
+ int64_t nextClapEventMs;
+
+ for (int i = 0; i < numFrames; ++i) {
+
+ mSongPositionMs = convertFramesToMillis(
+ mCurrentFrame,
+ mAudioStream->getSampleRate());
+
+ if (mClapEvents.peek(nextClapEventMs) && mSongPositionMs >= nextClapEventMs){
+ mClap->setPlaying(true);
+ mClapEvents.pop(nextClapEventMs);
+ }
+ mMixer.renderAudio(outputBuffer+(oboeStream->getChannelCount()*i), 1);
+ mCurrentFrame++;
+ }
+
+ mLastUpdateTime = nowUptimeMillis();
+
+ return DataCallbackResult::Continue;
+}
+
+void Game::onErrorAfterClose(AudioStream *audioStream, Result error) {
+ if (error == Result::ErrorDisconnected){
+ mGameState = GameState::Loading;
+ mAudioStream.reset();
+ mMixer.removeAllTracks();
+ mCurrentFrame = 0;
+ mSongPositionMs = 0;
+ mLastUpdateTime = 0;
+ start();
+ } else {
+ LOGE("Stream error: %s", convertToText(error));
+ }
+}
+
+/**
+ * Get the result of a tap
+ *
+ * @param tapTimeInMillis - The time the tap occurred in milliseconds
+ * @param tapWindowInMillis - The time at the middle of the "tap window" in milliseconds
+ * @return TapResult can be Early, Late or Success
+ */
+TapResult Game::getTapResult(int64_t tapTimeInMillis, int64_t tapWindowInMillis){
+ LOGD("Tap time %" PRId64 ", tap window time: %" PRId64, tapTimeInMillis, tapWindowInMillis);
+ if (tapTimeInMillis <= tapWindowInMillis + kWindowCenterOffsetMs) {
+ if (tapTimeInMillis >= tapWindowInMillis - kWindowCenterOffsetMs) {
+ return TapResult::Success;
+ } else {
+ return TapResult::Early;
+ }
+ } else {
+ return TapResult::Late;
+ }
+}
+
+bool Game::openStream() {
+
+ // Create an audio stream
+ AudioStreamBuilder builder;
+ builder.setFormat(AudioFormat::Float);
+ builder.setFormatConversionAllowed(true);
+ builder.setPerformanceMode(PerformanceMode::LowLatency);
+ builder.setSharingMode(SharingMode::Exclusive);
+ builder.setSampleRate(48000);
+ builder.setSampleRateConversionQuality(
+ SampleRateConversionQuality::Medium);
+ builder.setChannelCount(2);
+ builder.setDataCallback(this);
+ builder.setErrorCallback(this);
+ Result result = builder.openStream(mAudioStream);
+ if (result != Result::OK){
+ LOGE("Failed to open stream. Error: %s", convertToText(result));
+ return false;
+ }
+
+ mMixer.setChannelCount(mAudioStream->getChannelCount());
+
+ return true;
+}
+
+bool Game::setupAudioSources() {
+
+ // Set the properties of our audio source(s) to match that of our audio stream
+ AudioProperties targetProperties {
+ .channelCount = mAudioStream->getChannelCount(),
+ .sampleRate = mAudioStream->getSampleRate()
+ };
+
+ // Create a data source and player for the clap sound
+ std::shared_ptr<AAssetDataSource> mClapSource {
+ AAssetDataSource::newFromCompressedAsset(mAssetManager, kClapFilename, targetProperties)
+ };
+ if (mClapSource == nullptr){
+ LOGE("Could not load source data for clap sound");
+ return false;
+ }
+ mClap = std::make_unique<Player>(mClapSource);
+
+ // Create a data source and player for our backing track
+ std::shared_ptr<AAssetDataSource> backingTrackSource {
+ AAssetDataSource::newFromCompressedAsset(mAssetManager, kBackingTrackFilename, targetProperties)
+ };
+ if (backingTrackSource == nullptr){
+ LOGE("Could not load source data for backing track");
+ return false;
+ }
+ mBackingTrack = std::make_unique<Player>(backingTrackSource);
+ mBackingTrack->setPlaying(true);
+ mBackingTrack->setLooping(true);
+
+ // Add both players to a mixer
+ mMixer.addTrack(mClap.get());
+ mMixer.addTrack(mBackingTrack.get());
+
+ return true;
+}
+
+void Game::scheduleSongEvents() {
+
+ for (auto t : kClapEvents) mClapEvents.push(t);
+ for (auto t : kClapWindows) mClapWindows.push(t);
+}
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/Game.h b/externals/oboe/samples/RhythmGame/src/main/cpp/Game.h
new file mode 100644
index 0000000000..d9cb2ef7f0
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/Game.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RHYTHMGAME_GAME_H
+#define RHYTHMGAME_GAME_H
+
+#include <future>
+
+#include <android/asset_manager.h>
+#include <oboe/Oboe.h>
+
+#include "shared/Mixer.h"
+
+#include "audio/Player.h"
+#include "audio/AAssetDataSource.h"
+#include "ui/OpenGLFunctions.h"
+#include "utils/LockFreeQueue.h"
+#include "utils/UtilityFunctions.h"
+#include "GameConstants.h"
+
+using namespace oboe;
+
+
+enum class GameState {
+ Loading,
+ Playing,
+ FailedToLoad
+};
+
+class Game : public AudioStreamDataCallback, AudioStreamErrorCallback {
+public:
+ explicit Game(AAssetManager&);
+ void start();
+ void stop();
+ void onSurfaceCreated();
+ void onSurfaceDestroyed();
+ void onSurfaceChanged(int widthInPixels, int heightInPixels);
+ void tick();
+ void tap(int64_t eventTimeAsUptime);
+
+ // Inherited from oboe::AudioStreamDataCallback.
+ DataCallbackResult
+ onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override;
+
+ // Inherited from oboe::AudioStreamErrorCallback.
+ void onErrorAfterClose(AudioStream *oboeStream, Result error) override;
+
+private:
+ AAssetManager& mAssetManager;
+ std::shared_ptr<AudioStream> mAudioStream;
+ std::unique_ptr<Player> mClap;
+ std::unique_ptr<Player> mBackingTrack;
+ Mixer mMixer;
+
+ LockFreeQueue<int64_t, kMaxQueueItems> mClapEvents;
+ std::atomic<int64_t> mCurrentFrame { 0 };
+ std::atomic<int64_t> mSongPositionMs { 0 };
+ LockFreeQueue<int64_t, kMaxQueueItems> mClapWindows;
+ LockFreeQueue<TapResult, kMaxQueueItems> mUiEvents;
+ std::atomic<int64_t> mLastUpdateTime { 0 };
+ std::atomic<GameState> mGameState { GameState::Loading };
+ std::future<void> mLoadingResult;
+
+ void load();
+ TapResult getTapResult(int64_t tapTimeInMillis, int64_t tapWindowInMillis);
+ bool openStream();
+ bool setupAudioSources();
+ void scheduleSongEvents();
+};
+
+
+#endif //RHYTHMGAME_GAME_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/GameConstants.h b/externals/oboe/samples/RhythmGame/src/main/cpp/GameConstants.h
new file mode 100644
index 0000000000..288159279e
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/GameConstants.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SAMPLES_GAMECONSTANTS_H
+#define SAMPLES_GAMECONSTANTS_H
+
+#include "ui/OpenGLFunctions.h"
+
+constexpr int kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer)
+constexpr int kMaxQueueItems = 4; // Must be power of 2
+
+// Colors for game states and visual feedback for taps
+constexpr ScreenColor kPlayingColor = GREY;
+constexpr ScreenColor kLoadingColor = YELLOW;
+constexpr ScreenColor kLoadingFailedColor = RED;
+constexpr ScreenColor kTapSuccessColor = GREEN;
+constexpr ScreenColor kTapEarlyColor = ORANGE;
+constexpr ScreenColor kTapLateColor = PURPLE;
+
+// This defines the size of the tap window in milliseconds. For example, if defined at 100ms the
+// player will have 100ms before and after the centre of the tap window to tap on the screen and
+// be successful
+constexpr int kWindowCenterOffsetMs = 100;
+
+// Filename for clap sound asset (in assets folder)
+constexpr char kClapFilename[] { "CLAP.mp3" };
+
+// Filename for the backing track asset (in assets folder)
+constexpr char kBackingTrackFilename[] { "FUNKY_HOUSE.mp3" };
+
+// The game will first demonstrate the pattern which the user should copy. It does this by
+// "clapping" (playing a clap sound) at certain times during the song. We can specify these times
+// here in milliseconds. Our backing track has a tempo of 120 beats per minute, which is 2 beats per
+// second. This means a pattern of 3 claps starting on the first beat of the first bar would mean
+// playing claps at 0ms, 500ms and 1000ms
+constexpr int64_t kClapEvents[] { 0, 500, 1000 };
+
+// We then want the user to tap on the screen exactly 4 beats after the first clap so we add clap
+// windows at 2000ms, 2500ms and 3000ms (or 2, 2.5 and 3 seconds). @see getTapResult for more info.
+constexpr int64_t kClapWindows[] { 2000, 2500, 3000 };
+
+struct AudioProperties {
+ int32_t channelCount;
+ int32_t sampleRate;
+};
+
+#endif //SAMPLES_GAMECONSTANTS_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp
new file mode 100644
index 0000000000..1c697878c2
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include <utils/logging.h>
+#include <oboe/Oboe.h>
+
+#include "AAssetDataSource.h"
+
+#if !defined(USE_FFMPEG)
+#error USE_FFMPEG should be defined in app.gradle
+#endif
+
+#if USE_FFMPEG==1
+#include "FFMpegExtractor.h"
+#else
+#include "NDKExtractor.h"
+#endif
+
+
+constexpr int kMaxCompressionRatio { 12 };
+
+AAssetDataSource* AAssetDataSource::newFromCompressedAsset(
+ AAssetManager &assetManager,
+ const char *filename,
+ const AudioProperties targetProperties) {
+
+ AAsset *asset = AAssetManager_open(&assetManager, filename, AASSET_MODE_UNKNOWN);
+ if (!asset) {
+ LOGE("Failed to open asset %s", filename);
+ return nullptr;
+ }
+
+ off_t assetSize = AAsset_getLength(asset);
+ LOGD("Opened %s, size %ld", filename, assetSize);
+
+ // Allocate memory to store the decompressed audio. We don't know the exact
+ // size of the decoded data until after decoding so we make an assumption about the
+ // maximum compression ratio and the decoded sample format (float for FFmpeg, int16 for NDK).
+#if USE_FFMPEG==true
+ const long maximumDataSizeInBytes = kMaxCompressionRatio * assetSize * sizeof(float);
+ auto decodedData = new uint8_t[maximumDataSizeInBytes];
+
+ int64_t bytesDecoded = FFMpegExtractor::decode(asset, decodedData, targetProperties);
+ auto numSamples = bytesDecoded / sizeof(float);
+#else
+ const long maximumDataSizeInBytes = kMaxCompressionRatio * assetSize * sizeof(int16_t);
+ auto decodedData = new uint8_t[maximumDataSizeInBytes];
+
+ int64_t bytesDecoded = NDKExtractor::decode(asset, decodedData, targetProperties);
+ auto numSamples = bytesDecoded / sizeof(int16_t);
+#endif
+
+ // Now we know the exact number of samples we can create a float array to hold the audio data
+ auto outputBuffer = std::make_unique<float[]>(numSamples);
+
+#if USE_FFMPEG==1
+ memcpy(outputBuffer.get(), decodedData, (size_t)bytesDecoded);
+#else
+ // The NDK decoder can only decode to int16, we need to convert to floats
+ oboe::convertPcm16ToFloat(
+ reinterpret_cast<int16_t*>(decodedData),
+ outputBuffer.get(),
+ bytesDecoded / sizeof(int16_t));
+#endif
+
+ delete[] decodedData;
+ AAsset_close(asset);
+
+ return new AAssetDataSource(std::move(outputBuffer),
+ numSamples,
+ targetProperties);
+}
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h
new file mode 100644
index 0000000000..cbcb433a34
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RHYTHMGAME_AASSETDATASOURCE_H
+#define RHYTHMGAME_AASSETDATASOURCE_H
+
+#include <android/asset_manager.h>
+#include <GameConstants.h>
+#include "DataSource.h"
+
+class AAssetDataSource : public DataSource {
+
+public:
+ int64_t getSize() const override { return mBufferSize; }
+ AudioProperties getProperties() const override { return mProperties; }
+ const float* getData() const override { return mBuffer.get(); }
+
+ static AAssetDataSource* newFromCompressedAsset(
+ AAssetManager &assetManager,
+ const char *filename,
+ AudioProperties targetProperties);
+
+private:
+
+ AAssetDataSource(std::unique_ptr<float[]> data, size_t size,
+ const AudioProperties properties)
+ : mBuffer(std::move(data))
+ , mBufferSize(size)
+ , mProperties(properties) {
+ }
+
+ const std::unique_ptr<float[]> mBuffer;
+ const int64_t mBufferSize;
+ const AudioProperties mProperties;
+
+};
+#endif //RHYTHMGAME_AASSETDATASOURCE_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/DataSource.h b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/DataSource.h
new file mode 100644
index 0000000000..90284536bb
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/DataSource.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RHYTHMGAME_AUDIOSOURCE_H
+#define RHYTHMGAME_AUDIOSOURCE_H
+
+#include <cstdint>
+#include <GameConstants.h>
+
+class DataSource {
+public:
+ virtual ~DataSource(){};
+ virtual int64_t getSize() const = 0;
+ virtual AudioProperties getProperties() const = 0;
+ virtual const float* getData() const = 0;
+};
+
+
+#endif //RHYTHMGAME_AUDIOSOURCE_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.cpp
new file mode 100644
index 0000000000..97d212f369
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.cpp
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <memory>
+#include <oboe/Definitions.h>
+#include "FFMpegExtractor.h"
+#include "utils/logging.h"
+
+constexpr int kInternalBufferSize = 1152; // Use MP3 block size. https://wiki.hydrogenaud.io/index.php?title=MP3
+
+int read(void *opaque, uint8_t *buf, int buf_size) {
+
+ auto asset = (AAsset *) opaque;
+ int bytesRead = AAsset_read(asset, buf, (size_t)buf_size);
+ return bytesRead;
+}
+
+int64_t seek(void *opaque, int64_t offset, int whence){
+
+ auto asset = (AAsset*)opaque;
+
+ // See https://www.ffmpeg.org/doxygen/3.0/avio_8h.html#a427ff2a881637b47ee7d7f9e368be63f
+ if (whence == AVSEEK_SIZE) return AAsset_getLength(asset);
+ if (AAsset_seek(asset, offset, whence) == -1){
+ return -1;
+ } else {
+ return 0;
+ }
+}
+
+bool FFMpegExtractor::createAVIOContext(AAsset *asset, uint8_t *buffer, uint32_t bufferSize,
+ AVIOContext **avioContext) {
+
+ constexpr int isBufferWriteable = 0;
+
+ *avioContext = avio_alloc_context(
+ buffer, // internal buffer for FFmpeg to use
+ bufferSize, // For optimal decoding speed this should be the protocol block size
+ isBufferWriteable,
+ asset, // Will be passed to our callback functions as a (void *)
+ read, // Read callback function
+ nullptr, // Write callback function (not used)
+ seek); // Seek callback function
+
+ if (*avioContext == nullptr){
+ LOGE("Failed to create AVIO context");
+ return false;
+ } else {
+ return true;
+ }
+}
+
+bool
+FFMpegExtractor::createAVFormatContext(AVIOContext *avioContext, AVFormatContext **avFormatContext) {
+
+ *avFormatContext = avformat_alloc_context();
+ (*avFormatContext)->pb = avioContext;
+
+ if (*avFormatContext == nullptr){
+ LOGE("Failed to create AVFormatContext");
+ return false;
+ } else {
+ return true;
+ }
+}
+
+bool FFMpegExtractor::openAVFormatContext(AVFormatContext *avFormatContext) {
+
+ int result = avformat_open_input(&avFormatContext,
+ "", /* URL is left empty because we're providing our own I/O */
+ nullptr /* AVInputFormat *fmt */,
+ nullptr /* AVDictionary **options */
+ );
+
+ if (result == 0) {
+ return true;
+ } else {
+ LOGE("Failed to open file. Error code %s", av_err2str(result));
+ return false;
+ }
+}
+
+bool FFMpegExtractor::getStreamInfo(AVFormatContext *avFormatContext) {
+
+ int result = avformat_find_stream_info(avFormatContext, nullptr);
+ if (result == 0 ){
+ return true;
+ } else {
+ LOGE("Failed to find stream info. Error code %s", av_err2str(result));
+ return false;
+ }
+}
+
+AVStream *FFMpegExtractor::getBestAudioStream(AVFormatContext *avFormatContext) {
+
+ int streamIndex = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
+
+ if (streamIndex < 0){
+ LOGE("Could not find stream");
+ return nullptr;
+ } else {
+ return avFormatContext->streams[streamIndex];
+ }
+}
+
+int64_t FFMpegExtractor::decode(
+ AAsset *asset,
+ uint8_t *targetData,
+ AudioProperties targetProperties) {
+
+ LOGI("Decoder: FFMpeg");
+
+ int returnValue = -1; // -1 indicates error
+
+ // Create a buffer for FFmpeg to use for decoding (freed in the custom deleter below)
+ auto buffer = reinterpret_cast<uint8_t*>(av_malloc(kInternalBufferSize));
+
+ // Create an AVIOContext with a custom deleter
+ std::unique_ptr<AVIOContext, void(*)(AVIOContext *)> ioContext {
+ nullptr,
+ [](AVIOContext *c) {
+ av_free(c->buffer);
+ avio_context_free(&c);
+ }
+ };
+ {
+ AVIOContext *tmp = nullptr;
+ if (!createAVIOContext(asset, buffer, kInternalBufferSize, &tmp)){
+ LOGE("Could not create an AVIOContext");
+ return returnValue;
+ }
+ ioContext.reset(tmp);
+ }
+
+ // Create an AVFormatContext using the avformat_free_context as the deleter function
+ std::unique_ptr<AVFormatContext, decltype(&avformat_free_context)> formatContext {
+ nullptr,
+ &avformat_free_context
+ };
+ {
+ AVFormatContext *tmp;
+ if (!createAVFormatContext(ioContext.get(), &tmp)) return returnValue;
+ formatContext.reset(tmp);
+ }
+
+ if (!openAVFormatContext(formatContext.get())) return returnValue;
+
+ if (!getStreamInfo(formatContext.get())) return returnValue;
+
+ // Obtain the best audio stream to decode
+ AVStream *stream = getBestAudioStream(formatContext.get());
+ if (stream == nullptr || stream->codecpar == nullptr){
+ LOGE("Could not find a suitable audio stream to decode");
+ return returnValue;
+ }
+
+ printCodecParameters(stream->codecpar);
+
+ // Find the codec to decode this stream
+ AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
+ if (!codec){
+ LOGE("Could not find codec with ID: %d", stream->codecpar->codec_id);
+ return returnValue;
+ }
+
+ // Create the codec context, specifying the deleter function
+ std::unique_ptr<AVCodecContext, void(*)(AVCodecContext *)> codecContext {
+ nullptr,
+ [](AVCodecContext *c) { avcodec_free_context(&c); }
+ };
+ {
+ AVCodecContext *tmp = avcodec_alloc_context3(codec);
+ if (!tmp){
+ LOGE("Failed to allocate codec context");
+ return returnValue;
+ }
+ codecContext.reset(tmp);
+ }
+
+ // Copy the codec parameters into the context
+ if (avcodec_parameters_to_context(codecContext.get(), stream->codecpar) < 0){
+ LOGE("Failed to copy codec parameters to codec context");
+ return returnValue;
+ }
+
+ // Open the codec
+ if (avcodec_open2(codecContext.get(), codec, nullptr) < 0){
+ LOGE("Could not open codec");
+ return returnValue;
+ }
+
+ // prepare resampler
+ int32_t outChannelLayout = (1 << targetProperties.channelCount) - 1;
+ LOGD("Channel layout %d", outChannelLayout);
+
+ SwrContext *swr = swr_alloc();
+ av_opt_set_int(swr, "in_channel_count", stream->codecpar->channels, 0);
+ av_opt_set_int(swr, "out_channel_count", targetProperties.channelCount, 0);
+ av_opt_set_int(swr, "in_channel_layout", stream->codecpar->channel_layout, 0);
+ av_opt_set_int(swr, "out_channel_layout", outChannelLayout, 0);
+ av_opt_set_int(swr, "in_sample_rate", stream->codecpar->sample_rate, 0);
+ av_opt_set_int(swr, "out_sample_rate", targetProperties.sampleRate, 0);
+ av_opt_set_int(swr, "in_sample_fmt", stream->codecpar->format, 0);
+ av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
+ av_opt_set_int(swr, "force_resampling", 1, 0);
+
+ // Check that resampler has been inited
+ int result = swr_init(swr);
+ if (result != 0){
+ LOGE("swr_init failed. Error: %s", av_err2str(result));
+ return returnValue;
+ };
+ if (!swr_is_initialized(swr)) {
+ LOGE("swr_is_initialized is false\n");
+ return returnValue;
+ }
+
+ // Prepare to read data
+ int bytesWritten = 0;
+ AVPacket avPacket; // Stores compressed audio data
+ av_init_packet(&avPacket);
+ AVFrame *decodedFrame = av_frame_alloc(); // Stores raw audio data
+ int bytesPerSample = av_get_bytes_per_sample((AVSampleFormat)stream->codecpar->format);
+
+ LOGD("Bytes per sample %d", bytesPerSample);
+
+ LOGD("DECODE START");
+
+ // While there is more data to read, read it into the avPacket
+ while (av_read_frame(formatContext.get(), &avPacket) == 0){
+
+ if (avPacket.stream_index == stream->index && avPacket.size > 0) {
+
+ // Pass our compressed data into the codec
+ result = avcodec_send_packet(codecContext.get(), &avPacket);
+ if (result != 0) {
+ LOGE("avcodec_send_packet error: %s", av_err2str(result));
+ goto cleanup;
+ }
+
+ // Retrieve our raw data from the codec
+ result = avcodec_receive_frame(codecContext.get(), decodedFrame);
+ if (result == AVERROR(EAGAIN)) {
+ // The codec needs more data before it can decode
+ LOGI("avcodec_receive_frame returned EAGAIN");
+ av_packet_unref(&avPacket);
+ continue;
+ } else if (result != 0) {
+ LOGE("avcodec_receive_frame error: %s", av_err2str(result));
+ goto cleanup;
+ }
+
+ // DO RESAMPLING
+ auto dst_nb_samples = (int32_t) av_rescale_rnd(
+ swr_get_delay(swr, decodedFrame->sample_rate) + decodedFrame->nb_samples,
+ targetProperties.sampleRate,
+ decodedFrame->sample_rate,
+ AV_ROUND_UP);
+
+ short *buffer1;
+ av_samples_alloc(
+ (uint8_t **) &buffer1,
+ nullptr,
+ targetProperties.channelCount,
+ dst_nb_samples,
+ AV_SAMPLE_FMT_FLT,
+ 0);
+ int frame_count = swr_convert(
+ swr,
+ (uint8_t **) &buffer1,
+ dst_nb_samples,
+ (const uint8_t **) decodedFrame->data,
+ decodedFrame->nb_samples);
+
+ int64_t bytesToWrite = frame_count * sizeof(float) * targetProperties.channelCount;
+ memcpy(targetData + bytesWritten, buffer1, (size_t)bytesToWrite);
+ bytesWritten += bytesToWrite;
+ av_freep(&buffer1);
+
+ av_packet_unref(&avPacket);
+ }
+ }
+
+ av_frame_free(&decodedFrame);
+ LOGD("DECODE END");
+
+ returnValue = bytesWritten;
+
+ cleanup:
+ return returnValue;
+}
+
+void FFMpegExtractor::printCodecParameters(AVCodecParameters *params) {
+
+ LOGD("Stream properties");
+ LOGD("Channels: %d", params->channels);
+ LOGD("Channel layout: %" PRId64, params->channel_layout);
+ LOGD("Sample rate: %d", params->sample_rate);
+ LOGD("Format: %s", av_get_sample_fmt_name((AVSampleFormat)params->format));
+ LOGD("Frame size: %d", params->frame_size);
+}
+
+
+
+
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.h b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.h
new file mode 100644
index 0000000000..a439b37c0a
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/FFMpegExtractor.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FFMPEG_FFMPEGEXTRACTOR_H
+#define FFMPEG_FFMPEGEXTRACTOR_H
+
+
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libswresample/swresample.h>
+#include <libavutil/opt.h>
+}
+
+#include <cstdint>
+#include <android/asset_manager.h>
+#include <GameConstants.h>
+
+class FFMpegExtractor {
+public:
+ static int64_t decode(AAsset *asset, uint8_t *targetData, AudioProperties targetProperties);
+
+private:
+ static bool createAVIOContext(AAsset *asset, uint8_t *buffer, uint32_t bufferSize,
+ AVIOContext **avioContext);
+
+ static bool createAVFormatContext(AVIOContext *avioContext, AVFormatContext **avFormatContext);
+
+ static bool openAVFormatContext(AVFormatContext *avFormatContext);
+
+ static int32_t cleanup(AVIOContext *avioContext, AVFormatContext *avFormatContext);
+
+ static bool getStreamInfo(AVFormatContext *avFormatContext);
+
+ static AVStream *getBestAudioStream(AVFormatContext *avFormatContext);
+
+ static AVCodec *findCodec(AVCodecID id);
+
+ static void printCodecParameters(AVCodecParameters *params);
+};
+
+
+#endif //FFMPEG_FFMPEGEXTRACTOR_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.cpp
new file mode 100644
index 0000000000..ac50940d91
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <sys/types.h>
+
+#include <cstring>
+
+#include <media/NdkMediaExtractor.h>
+#include <utils/logging.h>
+#include <cinttypes>
+
+#include "NDKExtractor.h"
+
+int32_t NDKExtractor::decode(AAsset *asset, uint8_t *targetData, AudioProperties targetProperties) {
+
+ LOGD("Using NDK decoder");
+
+ // open asset as file descriptor
+ off_t start, length;
+ int fd = AAsset_openFileDescriptor(asset, &start, &length);
+
+ // Extract the audio frames
+ AMediaExtractor *extractor = AMediaExtractor_new();
+ media_status_t amresult = AMediaExtractor_setDataSourceFd(extractor, fd,
+ static_cast<off64_t>(start),
+ static_cast<off64_t>(length));
+ if (amresult != AMEDIA_OK){
+ LOGE("Error setting extractor data source, err %d", amresult);
+ return 0;
+ }
+
+ // Specify our desired output format by creating it from our source
+ AMediaFormat *format = AMediaExtractor_getTrackFormat(extractor, 0);
+
+ int32_t sampleRate;
+ if (AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &sampleRate)){
+ LOGD("Source sample rate %d", sampleRate);
+ if (sampleRate != targetProperties.sampleRate){
+ LOGE("Input (%d) and output (%d) sample rates do not match. "
+ "NDK decoder does not support resampling.",
+ sampleRate,
+ targetProperties.sampleRate);
+ return 0;
+ }
+ } else {
+ LOGE("Failed to get sample rate");
+ return 0;
+ };
+
+ int32_t channelCount;
+ if (AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &channelCount)){
+ LOGD("Got channel count %d", channelCount);
+ if (channelCount != targetProperties.channelCount){
+ LOGE("NDK decoder does not support different "
+ "input (%d) and output (%d) channel counts",
+ channelCount,
+ targetProperties.channelCount);
+ }
+ } else {
+ LOGE("Failed to get channel count");
+ return 0;
+ }
+
+ const char *formatStr = AMediaFormat_toString(format);
+ LOGD("Output format %s", formatStr);
+
+ const char *mimeType;
+ if (AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mimeType)) {
+ LOGD("Got mime type %s", mimeType);
+ } else {
+ LOGE("Failed to get mime type");
+ return 0;
+ }
+
+ // Obtain the correct decoder
+ AMediaCodec *codec = nullptr;
+ AMediaExtractor_selectTrack(extractor, 0);
+ codec = AMediaCodec_createDecoderByType(mimeType);
+ AMediaCodec_configure(codec, format, nullptr, nullptr, 0);
+ AMediaCodec_start(codec);
+
+ // DECODE
+
+ bool isExtracting = true;
+ bool isDecoding = true;
+ int32_t bytesWritten = 0;
+
+ while(isExtracting || isDecoding){
+
+ if (isExtracting){
+
+ // Obtain the index of the next available input buffer
+ ssize_t inputIndex = AMediaCodec_dequeueInputBuffer(codec, 2000);
+ //LOGV("Got input buffer %d", inputIndex);
+
+ // The input index acts as a status if its negative
+ if (inputIndex < 0){
+ if (inputIndex == AMEDIACODEC_INFO_TRY_AGAIN_LATER){
+ // LOGV("Codec.dequeueInputBuffer try again later");
+ } else {
+ LOGE("Codec.dequeueInputBuffer unknown error status");
+ }
+ } else {
+
+ // Obtain the actual buffer and read the encoded data into it
+ size_t inputSize;
+ uint8_t *inputBuffer = AMediaCodec_getInputBuffer(codec, inputIndex, &inputSize);
+ //LOGV("Sample size is: %d", inputSize);
+
+ ssize_t sampleSize = AMediaExtractor_readSampleData(extractor, inputBuffer, inputSize);
+ auto presentationTimeUs = AMediaExtractor_getSampleTime(extractor);
+
+ if (sampleSize > 0){
+
+ // Enqueue the encoded data
+ AMediaCodec_queueInputBuffer(codec, inputIndex, 0, sampleSize,
+ presentationTimeUs,
+ 0);
+ AMediaExtractor_advance(extractor);
+
+ } else {
+ LOGD("End of extractor data stream");
+ isExtracting = false;
+
+ // We need to tell the codec that we've reached the end of the stream
+ AMediaCodec_queueInputBuffer(codec, inputIndex, 0, 0,
+ presentationTimeUs,
+ AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
+ }
+ }
+ }
+
+ if (isDecoding){
+ // Dequeue the decoded data
+ AMediaCodecBufferInfo info;
+ ssize_t outputIndex = AMediaCodec_dequeueOutputBuffer(codec, &info, 0);
+
+ if (outputIndex >= 0){
+
+ // Check whether this is set earlier
+ if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM){
+ LOGD("Reached end of decoding stream");
+ isDecoding = false;
+ }
+
+ // Valid index, acquire buffer
+ size_t outputSize;
+ uint8_t *outputBuffer = AMediaCodec_getOutputBuffer(codec, outputIndex, &outputSize);
+
+ /*LOGV("Got output buffer index %d, buffer size: %d, info size: %d writing to pcm index %d",
+ outputIndex,
+ outputSize,
+ info.size,
+ m_writeIndex);*/
+
+ // copy the data out of the buffer
+ memcpy(targetData + bytesWritten, outputBuffer, info.size);
+ bytesWritten+=info.size;
+ AMediaCodec_releaseOutputBuffer(codec, outputIndex, false);
+ } else {
+
+ // The outputIndex doubles as a status return if its value is < 0
+ switch(outputIndex){
+ case AMEDIACODEC_INFO_TRY_AGAIN_LATER:
+ LOGD("dequeueOutputBuffer: try again later");
+ break;
+ case AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED:
+ LOGD("dequeueOutputBuffer: output buffers changed");
+ break;
+ case AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED:
+ LOGD("dequeueOutputBuffer: output outputFormat changed");
+ format = AMediaCodec_getOutputFormat(codec);
+ LOGD("outputFormat changed to: %s", AMediaFormat_toString(format));
+ break;
+ }
+ }
+ }
+ }
+
+ // Clean up
+ AMediaFormat_delete(format);
+ AMediaCodec_delete(codec);
+ AMediaExtractor_delete(extractor);
+
+ return bytesWritten;
+}
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.h b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.h
new file mode 100644
index 0000000000..0073f7262d
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/NDKExtractor.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FFMPEG_NDKMEDIAEXTRACTOR_H
+#define FFMPEG_NDKMEDIAEXTRACTOR_H
+
+
+#include <cstdint>
+#include <android/asset_manager.h>
+#include <GameConstants.h>
+
+
+class NDKExtractor {
+
+public:
+ static int32_t decode(AAsset *asset, uint8_t *targetData, AudioProperties targetProperties);
+};
+
+
+#endif //FFMPEG_NDKMEDIAEXTRACTOR_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.cpp
new file mode 100644
index 0000000000..e73a9c351a
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Player.h"
+#include "utils/logging.h"
+
+void Player::renderAudio(float *targetData, int32_t numFrames){
+
+ const AudioProperties properties = mSource->getProperties();
+
+ if (mIsPlaying){
+
+ int64_t framesToRenderFromData = numFrames;
+ int64_t totalSourceFrames = mSource->getSize() / properties.channelCount;
+ const float *data = mSource->getData();
+
+ // Check whether we're about to reach the end of the recording
+ if (!mIsLooping && mReadFrameIndex + numFrames >= totalSourceFrames){
+ framesToRenderFromData = totalSourceFrames - mReadFrameIndex;
+ mIsPlaying = false;
+ }
+
+ for (int i = 0; i < framesToRenderFromData; ++i) {
+ for (int j = 0; j < properties.channelCount; ++j) {
+ targetData[(i*properties.channelCount)+j] = data[(mReadFrameIndex*properties.channelCount)+j];
+ }
+
+ // Increment and handle wraparound
+ if (++mReadFrameIndex >= totalSourceFrames) mReadFrameIndex = 0;
+ }
+
+ if (framesToRenderFromData < numFrames){
+ // fill the rest of the buffer with silence
+ renderSilence(&targetData[framesToRenderFromData], numFrames * properties.channelCount);
+ }
+
+ } else {
+ renderSilence(targetData, numFrames * properties.channelCount);
+ }
+}
+
+void Player::renderSilence(float *start, int32_t numSamples){
+ for (int i = 0; i < numSamples; ++i) {
+ start[i] = 0;
+ }
+}
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.h b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.h
new file mode 100644
index 0000000000..9c7eebace2
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/audio/Player.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RHYTHMGAME_SOUNDRECORDING_H
+#define RHYTHMGAME_SOUNDRECORDING_H
+
+#include <cstdint>
+#include <array>
+
+#include <chrono>
+#include <memory>
+#include <atomic>
+
+#include <android/asset_manager.h>
+
+#include "shared/IRenderableAudio.h"
+#include "DataSource.h"
+
+class Player : public IRenderableAudio{
+
+public:
+ /**
+ * Construct a new Player from the given DataSource. Players can share the same data source.
+ * For example, you could play two identical sounds concurrently by creating 2 Players with the
+ * same data source.
+ *
+ * @param source
+ */
+ Player(std::shared_ptr<DataSource> source)
+ : mSource(source)
+ {};
+
+ void renderAudio(float *targetData, int32_t numFrames);
+ void resetPlayHead() { mReadFrameIndex = 0; };
+ void setPlaying(bool isPlaying) { mIsPlaying = isPlaying; resetPlayHead(); };
+ void setLooping(bool isLooping) { mIsLooping = isLooping; };
+
+private:
+ int32_t mReadFrameIndex = 0;
+ std::atomic<bool> mIsPlaying { false };
+ std::atomic<bool> mIsLooping { false };
+ std::shared_ptr<DataSource> mSource;
+
+ void renderSilence(float*, int32_t);
+};
+
+#endif //RHYTHMGAME_SOUNDRECORDING_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/native-lib.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/native-lib.cpp
new file mode 100644
index 0000000000..456b64837d
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/native-lib.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <jni.h>
+#include <memory>
+
+#include <android/asset_manager_jni.h>
+
+#include "utils/logging.h"
+#include "Game.h"
+
+
+extern "C" {
+
+std::unique_ptr<Game> game;
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_MainActivity_native_1onStart(JNIEnv *env, jobject instance,
+ jobject jAssetManager) {
+
+ AAssetManager *assetManager = AAssetManager_fromJava(env, jAssetManager);
+ if (assetManager == nullptr) {
+ LOGE("Could not obtain the AAssetManager");
+ return;
+ }
+
+ game = std::make_unique<Game>(*assetManager);
+ game->start();
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_RendererWrapper_native_1onSurfaceCreated(JNIEnv *env,
+ jobject instance) {
+ game->onSurfaceCreated();
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_RendererWrapper_native_1onSurfaceChanged(JNIEnv *env,
+ jclass type,
+ jint width,
+ jint height) {
+ game->onSurfaceChanged(width, height);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_RendererWrapper_native_1onDrawFrame(JNIEnv *env,
+ jclass type) {
+ game->tick();
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_GameSurfaceView_native_1onTouchInput(JNIEnv *env,
+ jclass type,
+ jint event_type,
+ jlong time_since_boot_ms,
+ jint pixel_x,
+ jint pixel_y) {
+ game->tap(time_since_boot_ms);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_GameSurfaceView_native_1surfaceDestroyed__(JNIEnv *env,
+ jclass type) {
+ game->onSurfaceDestroyed();
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_MainActivity_native_1onStop(JNIEnv *env, jobject instance) {
+
+ game->stop();
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_oboe_samples_rhythmgame_MainActivity_native_1setDefaultStreamValues(JNIEnv *env,
+ jclass type,
+ jint sampleRate,
+ jint framesPerBurst) {
+ oboe::DefaultStreamValues::SampleRate = (int32_t) sampleRate;
+ oboe::DefaultStreamValues::FramesPerBurst = (int32_t) framesPerBurst;
+}
+} // extern "C"
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.cpp
new file mode 100644
index 0000000000..3662f6b217
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.cpp
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "OpenGLFunctions.h"
+#include "utils/logging.h"
+
+void CheckOpenGLError(const char* stmt, const char* fname, int line)
+{
+ GLenum err = glGetError();
+ if (err != GL_NO_ERROR)
+ {
+ LOGW("OpenGL error %08x, at %s:%i - for %s\n", err, fname, line, stmt);
+ assert(false);
+ }
+}
+
+void SetGLScreenColor(ScreenColor color){
+ glClearColor(color.red, color.green, color.blue, color.alpha);
+ glClear(GL_COLOR_BUFFER_BIT);
+}
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.h b/externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.h
new file mode 100644
index 0000000000..cd6ef2050f
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/ui/OpenGLFunctions.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_OGL_H
+#define ANDROID_OGL_H
+
+#include <assert.h>
+
+struct ScreenColor {
+ float red;
+ float green;
+ float blue;
+ float alpha;
+};
+
+constexpr ScreenColor RED { 1.0f, 0.0f, 0.0f, 1.0f };
+constexpr ScreenColor GREEN { 0.0f, 1.0f, 0.0f, 1.0f };
+constexpr ScreenColor BLUE { 0.0f, 0.0f, 1.0f, 1.0f };
+constexpr ScreenColor PURPLE { 1.0f, 0.0f, 1.0f, 1.0f };
+constexpr ScreenColor ORANGE { 1.0f, 0.5f, 0.0f, 1.0f };
+constexpr ScreenColor GREY { 0.3f, 0.3f, 0.3f, 0.3f };
+constexpr ScreenColor YELLOW { 1.0f, 1.0f, 0.0f, 1.0f };
+
+#ifdef GL3
+#include <GLES3/gl3.h>
+#elif GL3_2
+#include <GLES3/gl32.h>
+#else
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#endif
+
+void CheckOpenGLError(const char* stmt, const char* fname, int line);
+
+#ifndef NDEBUG
+#define GL_CHECK(stmt) \
+ stmt;\
+ CheckOpenGLError(#stmt, __FILE__, __LINE__);
+#else
+#define GL_CHECK(stmt) stmt
+#endif
+
+void SetGLScreenColor(ScreenColor color);
+
+#endif //ANDROID_OGL_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/utils/LockFreeQueue.h b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/LockFreeQueue.h
new file mode 100644
index 0000000000..fa9f7168bb
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/LockFreeQueue.h
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#ifndef RHYTHMGAME_LOCKFREEQUEUE_H
+#define RHYTHMGAME_LOCKFREEQUEUE_H
+
+#include <cstdint>
+#include <atomic>
+
+/**
+ * A lock-free queue for single consumer, single producer. Not thread-safe when using multiple
+ * consumers or producers.
+ *
+ * Example code:
+ *
+ * LockFreeQueue<int, 1024> myQueue;
+ * int value = 123;
+ * myQueue.push(value);
+ * myQueue.pop(value);
+ *
+ * @tparam T - The item type
+ * @tparam CAPACITY - Maximum number of items which can be held in the queue. Must be a power of 2.
+ * Must be less than the maximum value permissible in INDEX_TYPE
+ * @tparam INDEX_TYPE - The internal index type, defaults to uint32_t. Changing this will affect
+ * the maximum capacity. Included for ease of unit testing because testing queue lengths of
+ * UINT32_MAX can be time consuming and is not always possible.
+ */
+
+template <typename T, uint32_t CAPACITY, typename INDEX_TYPE = uint32_t>
+class LockFreeQueue {
+public:
+
+ /**
+ * Implementation details:
+ *
+ * We have 2 counters: readCounter and writeCounter. Each will increment until it reaches
+ * INDEX_TYPE_MAX, then wrap to zero. Unsigned integer overflow is defined behaviour in C++.
+ *
+ * Each time we need to access our data array we call mask() which gives us the index into the
+ * array. This approach avoids having a "dead item" in the buffer to distinguish between full
+ * and empty states. It also allows us to have a size() method which is easily calculated.
+ *
+ * IMPORTANT: This implementation is only thread-safe with a single reader thread and a single
+ * writer thread. Have more than one of either will result in Bad Thingsā„¢.
+ */
+
+ static constexpr bool isPowerOfTwo(uint32_t n) { return (n & (n - 1)) == 0; }
+ static_assert(isPowerOfTwo(CAPACITY), "Capacity must be a power of 2");
+ static_assert(std::is_unsigned<INDEX_TYPE>::value, "Index type must be unsigned");
+
+ /**
+ * Pop a value off the head of the queue
+ *
+ * @param val - element will be stored in this variable
+ * @return true if value was popped successfully, false if the queue is empty
+ */
+ bool pop(T &val) {
+ if (isEmpty()){
+ return false;
+ } else {
+ val = buffer[mask(readCounter)];
+ ++readCounter;
+ return true;
+ }
+ }
+
+ /**
+ * Add an item to the back of the queue
+ *
+ * @param item - The item to add
+ * @return true if item was added, false if the queue was full
+ */
+ bool push(const T& item) {
+ if (isFull()){
+ return false;
+ } else {
+ buffer[mask(writeCounter)] = item;
+ ++writeCounter;
+ return true;
+ }
+ }
+
+ /**
+ * Get the item at the front of the queue but do not remove it
+ *
+ * @param item - item will be stored in this variable
+ * @return true if item was stored, false if the queue was empty
+ */
+ bool peek(T &item) const {
+ if (isEmpty()){
+ return false;
+ } else {
+ item = buffer[mask(readCounter)];
+ return true;
+ }
+ }
+
+ /**
+ * Get the number of items in the queue
+ *
+ * @return number of items in the queue
+ */
+ INDEX_TYPE size() const {
+
+ /**
+ * This is worth some explanation:
+ *
+ * Whilst writeCounter is greater than readCounter the result of (write - read) will always
+ * be positive. Simple.
+ *
+ * But when writeCounter is equal to INDEX_TYPE_MAX (e.g. UINT32_MAX) the next push will
+ * wrap it around to zero, the start of the buffer, making writeCounter less than
+ * readCounter so the result of (write - read) will be negative.
+ *
+ * But because we're returning an unsigned type return value will be as follows:
+ *
+ * returnValue = INDEX_TYPE_MAX - (write - read)
+ *
+ * e.g. if write is 0, read is 150 and the INDEX_TYPE is uint8_t where the max value is
+ * 255 the return value will be (255 - (0 - 150)) = 105.
+ *
+ */
+ return writeCounter - readCounter;
+ };
+
+private:
+
+ bool isEmpty() const { return readCounter == writeCounter; }
+
+ bool isFull() const { return size() == CAPACITY; }
+
+ INDEX_TYPE mask(INDEX_TYPE n) const { return static_cast<INDEX_TYPE>(n & (CAPACITY - 1)); }
+
+ T buffer[CAPACITY];
+ std::atomic<INDEX_TYPE> writeCounter { 0 };
+ std::atomic<INDEX_TYPE> readCounter { 0 };
+
+};
+
+#endif //RHYTHMGAME_LOCKFREEQUEUE_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.cpp b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.cpp
new file mode 100644
index 0000000000..431f607374
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <chrono>
+#include <ui/OpenGLFunctions.h>
+#include <GameConstants.h>
+#include "UtilityFunctions.h"
+#include "logging.h"
+
+int64_t nowUptimeMillis() {
+ using namespace std::chrono;
+ return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
+}
+
+void renderEvent(TapResult r){
+ switch (r) {
+ case TapResult::Success:
+ SetGLScreenColor(kTapSuccessColor);
+ break;
+ case TapResult::Early:
+ SetGLScreenColor(kTapEarlyColor);
+ break;
+ case TapResult::Late:
+ SetGLScreenColor(kTapLateColor);
+ break;
+ }
+}
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.h b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.h
new file mode 100644
index 0000000000..659cd109b9
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/UtilityFunctions.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef RHYTHMGAME_UTILITYFUNCTIONS_H
+#define RHYTHMGAME_UTILITYFUNCTIONS_H
+
+#include <stdint.h>
+
+constexpr int64_t kMillisecondsInSecond = 1000;
+constexpr int64_t kNanosecondsInMillisecond = 1000000;
+
+enum class TapResult {
+ Early,
+ Late,
+ Success
+};
+
+int64_t nowUptimeMillis();
+
+constexpr int64_t convertFramesToMillis(const int64_t frames, const int sampleRate){
+ return static_cast<int64_t>((static_cast<double>(frames)/ sampleRate) * kMillisecondsInSecond);
+}
+
+TapResult getTapResult(int64_t tapTimeInMillis, int64_t tapWindowInMillis);
+
+void renderEvent(TapResult r);
+
+
+#endif //RHYTHMGAME_UTILITYFUNCTIONS_H
diff --git a/externals/oboe/samples/RhythmGame/src/main/cpp/utils/logging.h b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/logging.h
new file mode 100644
index 0000000000..cc04619805
--- /dev/null
+++ b/externals/oboe/samples/RhythmGame/src/main/cpp/utils/logging.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_LOGGING_H
+#define ANDROID_LOGGING_H
+
+#include <stdio.h>
+#include <android/log.h>
+#include <vector>
+
+#define APP_NAME "RhythmGame"
+#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, APP_NAME, __VA_ARGS__))
+#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, APP_NAME, __VA_ARGS__))
+#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, APP_NAME, __VA_ARGS__))
+#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, APP_NAME, __VA_ARGS__))
+
+
+
+#endif //ANDROID_LOGGING_H