diff options
Diffstat (limited to 'externals/oboe/samples/RhythmGame/src/main/cpp')
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 |
