diff options
author | Atneya Nair <atneya.nair@gmail.com> | 2019-08-13 09:07:06 -0700 |
---|---|---|
committer | Don Turner <dturner@users.noreply.github.com> | 2019-08-13 17:07:06 +0100 |
commit | 738663903f73ecb2aec6c62cb1bafdf03657d7ed (patch) | |
tree | badd4dfb41ab9f8377f1976885bdb65662b47b21 | |
parent | e09fd98e1482a1eacda2bc77427e9403717722f6 (diff) | |
download | oboe-738663903f73ecb2aec6c62cb1bafdf03657d7ed.tar.gz |
Refactor samples (#541)
Refactoring the hello-oboe and MegaDrone samples. Changes include:
- Each sample's audio engine now owns a separate callback object, rather than it _being_ a callback. This decouples the callback from the owning class.
- A new `DefaultAudioStreamCallback` object implements common useful callback functionality.
- In hello-oboe a customised `LatencyTuningCallback` is used which automatically adjusts the buffer size of the audio stream based on the number of underruns (more underruns = bigger buffer).
- `ManagedStream` is used in both samples which simplifies the ownership of an `AudioStream` object.
- Both samples now have a `TappableAudioSource` which is an object which can be tapped and renders audio data.
- Duplicated code has been refactored into shared objects, these can be found in the `samples/shared` folder.
Thanks to @atneya who implemented the vast majority of these changes.
24 files changed, 748 insertions, 603 deletions
diff --git a/samples/MegaDrone/build.gradle b/samples/MegaDrone/build.gradle index 564993f3..02ab26ea 100644 --- a/samples/MegaDrone/build.gradle +++ b/samples/MegaDrone/build.gradle @@ -35,7 +35,7 @@ android { } externalNativeBuild { cmake { - path "CMakeLists.txt" + path "src/main/cpp/CMakeLists.txt" } } } diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp b/samples/MegaDrone/src/main/cpp/AudioEngine.cpp deleted file mode 100644 index 94a330f4..00000000 --- a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 "AudioEngine.h" -#include "../../../../../src/common/OboeDebug.h" - -void AudioEngine::start(std::vector<int> cpuIds) { - - //LOGD("In start()"); - - mCpuIds = cpuIds; - AudioStreamBuilder builder; - - mStabilizedCallback = new StabilizedCallback(this); - builder.setCallback(mStabilizedCallback); - builder.setPerformanceMode(PerformanceMode::LowLatency); - builder.setSharingMode(SharingMode::Exclusive); - - Result result = builder.openStream(&mStream); - if (result != Result::OK){ - LOGE("Failed to open stream. Error: %s", convertToText(result)); - return; - } - - // If the output is 16-bit ints then create a separate float buffer to render into. - // This buffer will then be converted to 16-bit ints in onAudioReady - if (mStream->getFormat() == AudioFormat::I16){ - - // We use the audio stream's capacity as the maximum size since this is feasibly the - // largest number of frames we'd be required to render inside the audio callback - mConversionBuffer = std::make_unique<float[]>(mStream->getBufferCapacityInFrames() * - mStream->getChannelCount()); - } - - mSynth = std::make_unique<Synth>(mStream->getSampleRate(), mStream->getChannelCount()); - mStream->setBufferSizeInFrames(mStream->getFramesPerBurst() * 2); - mStream->requestStart(); - - //LOGD("Finished start()"); -} - -void AudioEngine::stop() { - - //LOGD("In stop()"); - - if (mStream != nullptr){ - mStream->close(); - } - //LOGD("Finished stop()"); -} - -void AudioEngine::tap(bool isOn) { - mSynth->setWaveOn(isOn); -} - -DataCallbackResult -AudioEngine::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) { - - if (!mIsThreadAffinitySet) setThreadAffinity(); - - bool is16Bit = (oboeStream->getFormat() == AudioFormat::I16); - float *outputBuffer = (is16Bit) ? mConversionBuffer.get() : static_cast<float*>(audioData); - mSynth->renderAudio(outputBuffer, numFrames); - - if (is16Bit) { - oboe::convertFloatToPcm16(outputBuffer, - static_cast<int16_t *>(audioData), - numFrames * oboeStream->getChannelCount()); - } - - return DataCallbackResult::Continue; -} - -/** - * Set the thread affinity for the current thread to mCpuIds. This can be useful to call on the - * audio thread to avoid underruns caused by CPU core migrations to slower CPU cores. - */ -void AudioEngine::setThreadAffinity() { - - pid_t current_thread_id = gettid(); - cpu_set_t cpu_set; - CPU_ZERO(&cpu_set); - - // If the callback cpu ids aren't specified then bind to the current cpu - if (mCpuIds.empty()) { - int current_cpu_id = sched_getcpu(); - LOGV("Current CPU ID is %d", current_cpu_id); - CPU_SET(current_cpu_id, &cpu_set); - } else { - - for (size_t i = 0; i < mCpuIds.size(); i++) { - int cpu_id = mCpuIds.at(i); - LOGV("CPU ID %d added to cores set", cpu_id); - CPU_SET(cpu_id, &cpu_set); - } - } - - int result = sched_setaffinity(current_thread_id, sizeof(cpu_set_t), &cpu_set); - if (result == 0) { - LOGV("Thread affinity set"); - } else { - LOGW("Error setting thread affinity. Error no: %d", result); - } - - mIsThreadAffinitySet = true; -} diff --git a/samples/MegaDrone/CMakeLists.txt b/samples/MegaDrone/src/main/cpp/CMakeLists.txt index fd94781d..ca6c2b39 100644 --- a/samples/MegaDrone/CMakeLists.txt +++ b/samples/MegaDrone/src/main/cpp/CMakeLists.txt @@ -4,20 +4,21 @@ cmake_minimum_required(VERSION 3.4.1) ### INCLUDE OBOE LIBRARY ### # Set the path to the Oboe library directory -set (OBOE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../..) +set (OBOE_DIR ../../../../..) # Add the Oboe library as a subproject. Since Oboe is an out-of-tree source library we must also # specify a binary directory add_subdirectory(${OBOE_DIR} ./oboe-bin) # Include the Oboe headers -include_directories(${OBOE_DIR}/include ${OBOE_DIR}/samples) +include_directories(${OBOE_DIR}/include ${OBOE_DIR}/samples/shared ${OBOE_DIR}/samples/debug-utils) + ### END OBOE INCLUDE SECTION ### add_library( megadrone SHARED - src/main/cpp/native-lib.cpp - src/main/cpp/AudioEngine.cpp + native-lib.cpp + MegaDroneEngine.cpp ) target_link_libraries( megadrone log oboe ) diff --git a/samples/MegaDrone/src/main/cpp/MegaDroneEngine.cpp b/samples/MegaDrone/src/main/cpp/MegaDroneEngine.cpp new file mode 100644 index 00000000..8e569f94 --- /dev/null +++ b/samples/MegaDrone/src/main/cpp/MegaDroneEngine.cpp @@ -0,0 +1,79 @@ +/* + * 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 "MegaDroneEngine.h" + +/** + * Main audio engine for the MegaDrone sample. It is responsible for: + * + * - Creating the callback object which will be supplied when constructing the audio stream + * - Setting the CPU core IDs to which the callback thread should bind to + * - Creating the playback stream, including setting the callback object + * - Creating `Synth` which will render the audio inside the callback + * - Starting the playback stream + * - Restarting the playback stream when `restart()` is called by the callback object + * + * @param cpuIds + */ +MegaDroneEngine::MegaDroneEngine(std::vector<int> cpuIds) { + + createCallback(cpuIds); + start(); +} + +void MegaDroneEngine::tap(bool isDown) { + mAudioSource->tap(isDown); +} + +void MegaDroneEngine::restart() { + start(); +} + +// Create the playback stream +oboe::Result MegaDroneEngine::createPlaybackStream() { + oboe::AudioStreamBuilder builder; + return builder.setSharingMode(oboe::SharingMode::Exclusive) + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setFormat(oboe::AudioFormat::Float) + ->setCallback(mCallback.get()) + ->openManagedStream(mStream); +} + +// Create the callback and set its thread affinity to the supplied CPU core IDs +void MegaDroneEngine::createCallback(std::vector<int> cpuIds){ + // Create the callback, we supply ourselves as the parent so that we can restart the stream + // when it's disconnected + mCallback = std::make_unique<DefaultAudioStreamCallback>(*this); + + // Bind the audio callback to specific CPU cores as this can help avoid underruns caused by + // core migrations + mCallback->setCpuIds(cpuIds); + mCallback->setThreadAffinityEnabled(true); +} + +void MegaDroneEngine::start(){ + auto result = createPlaybackStream(); + if (result == Result::OK){ + // Create our synthesizer audio source using the properties of the stream + mAudioSource = std::make_shared<Synth>(mStream->getSampleRate(), mStream->getChannelCount()); + mCallback->setSource(std::dynamic_pointer_cast<IRenderableAudio>(mAudioSource)); + mStream->start(); + } else { + LOGE("Failed to create the playback stream. Error: %s", convertToText(result)); + } +} diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.h b/samples/MegaDrone/src/main/cpp/MegaDroneEngine.h index d3a9630a..dbe1ae1a 100644 --- a/samples/MegaDrone/src/main/cpp/AudioEngine.h +++ b/samples/MegaDrone/src/main/cpp/MegaDroneEngine.h @@ -14,39 +14,41 @@ * limitations under the License. */ -#ifndef MEGADRONE_AUDIOENGINE_H -#define MEGADRONE_AUDIOENGINE_H +#ifndef MEGADRONE_ENGINE_H +#define MEGADRONE_ENGINE_H #include <oboe/Oboe.h> #include <vector> #include "Synth.h" +#include <DefaultAudioStreamCallback.h> +#include <TappableAudioSource.h> +#include <IRestartable.h> using namespace oboe; -class AudioEngine : public AudioStreamCallback { +class MegaDroneEngine : public IRestartable { public: - void start(std::vector<int> cpuIds); - void tap(bool isOn); + MegaDroneEngine(std::vector<int> cpuIds); - DataCallbackResult - onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override; + virtual ~MegaDroneEngine() = default; - void stop(); + void tap(bool isDown); -private: + // from IRestartable + virtual void restart() override; - StabilizedCallback *mStabilizedCallback = nullptr; - AudioStream *mStream = nullptr; - std::unique_ptr<Synth> mSynth; - std::vector<int> mCpuIds; // IDs of CPU cores which the audio callback should be bound to - bool mIsThreadAffinitySet = false; - std::unique_ptr<float[]> mConversionBuffer; // Used for float->int16 conversion +private: + oboe::ManagedStream mStream; + std::shared_ptr<TappableAudioSource> mAudioSource; + std::unique_ptr<DefaultAudioStreamCallback> mCallback; - void setThreadAffinity(); + oboe::Result createPlaybackStream(); + void createCallback(std::vector<int> cpuIds); + void start(); }; -#endif //MEGADRONE_AUDIOENGINE_H +#endif //MEGADRONE_ENGINE_H diff --git a/samples/MegaDrone/src/main/cpp/Synth.h b/samples/MegaDrone/src/main/cpp/Synth.h index 4d935a9b..93e723cc 100644 --- a/samples/MegaDrone/src/main/cpp/Synth.h +++ b/samples/MegaDrone/src/main/cpp/Synth.h @@ -18,10 +18,11 @@ #define MEGADRONE_SYNTH_H #include <array> +#include <TappableAudioSource.h> -#include "shared/Oscillator.h" -#include "shared/Mixer.h" -#include "shared/MonoToStereo.h" +#include <Oscillator.h> +#include <Mixer.h> +#include <MonoToStereo.h> constexpr int kNumOscillators = 100; constexpr float kOscBaseFrequency = 116.0; @@ -29,25 +30,26 @@ constexpr float kOscDivisor = 33; constexpr float kOscAmplitude = 0.009; -class Synth : public IRenderableAudio { +class Synth : public TappableAudioSource { public: - Synth(int32_t sampleRate, int32_t channelCount) { + Synth(int32_t sampleRate, int32_t channelCount) : + TappableAudioSource(sampleRate, channelCount) { for (int i = 0; i < kNumOscillators; ++i) { - mOscs[i].setSampleRate(sampleRate); - mOscs[i].setFrequency(kOscBaseFrequency+(static_cast<float>(i)/kOscDivisor)); + mOscs[i].setSampleRate(mSampleRate); + mOscs[i].setFrequency(kOscBaseFrequency + (static_cast<float>(i) / kOscDivisor)); mOscs[i].setAmplitude(kOscAmplitude); mMixer.addTrack(&mOscs[i]); } - if (channelCount == oboe::ChannelCount::Stereo) { + if (mChannelCount == oboe::ChannelCount::Stereo) { mOutputStage = &mConverter; } else { mOutputStage = &mMixer; } } - void setWaveOn(bool isEnabled) { - for (auto &osc : mOscs) osc.setWaveOn(isEnabled); + void tap(bool isOn) override { + for (auto &osc : mOscs) osc.setWaveOn(isOn); }; // From IRenderableAudio diff --git a/samples/MegaDrone/src/main/cpp/native-lib.cpp b/samples/MegaDrone/src/main/cpp/native-lib.cpp index 7567af4e..b29861f9 100644 --- a/samples/MegaDrone/src/main/cpp/native-lib.cpp +++ b/samples/MegaDrone/src/main/cpp/native-lib.cpp @@ -18,8 +18,8 @@ #include <string> #include <vector> -#include "AudioEngine.h" -#include "../../../../../src/common/OboeDebug.h" +#include "MegaDroneEngine.h" + std::vector<int> convertJavaArrayToVector(JNIEnv *env, jintArray intArray){ @@ -35,7 +35,6 @@ std::vector<int> convertJavaArrayToVector(JNIEnv *env, jintArray intArray){ } return v; } - extern "C" { /** * Start the audio engine @@ -48,53 +47,31 @@ extern "C" { JNIEXPORT jlong JNICALL Java_com_example_oboe_megadrone_MainActivity_startEngine(JNIEnv *env, jobject /*unused*/, jintArray jCpuIds) { - // We use std::nothrow so `new` returns a nullptr if the engine creation fails - AudioEngine *engine = new(std::nothrow) AudioEngine(); - if (engine) { - std::vector<int> cpuIds = convertJavaArrayToVector(env, jCpuIds); - engine->start(cpuIds); - LOGD("Engine started"); - } else { - LOGE("Failed to create audio engine"); - } + std::vector<int> cpuIds = convertJavaArrayToVector(env, jCpuIds); + LOGD("cpu ids size: %d", static_cast<int>(cpuIds.size())); + MegaDroneEngine *engine = new MegaDroneEngine(std::move(cpuIds)); + LOGD("Engine Started"); return reinterpret_cast<jlong>(engine); } -/** - * Stop the audio engine - * - * @param env - * @param instance - * @param jEngineHandle - pointer to the audio engine - */ JNIEXPORT void JNICALL -Java_com_example_oboe_megadrone_MainActivity_stopEngine( JNIEnv * /*unused*/, jobject /*unused*/, - jlong jEngineHandle) { - auto *engine = reinterpret_cast<AudioEngine *>(jEngineHandle); +Java_com_example_oboe_megadrone_MainActivity_stopEngine(JNIEnv *env, jobject instance, + jlong jEngineHandle) { + auto engine = reinterpret_cast<MegaDroneEngine*>(jEngineHandle); if (engine) { - engine->stop(); delete engine; - LOGD("Engine stopped"); } else { - LOGE("Engine handle is invalid, call startEngine() to create a new one"); - return; + LOGD("Engine invalid, call startEngine() to create"); } } -/** - * Send a tap event to the audio engine - * - * @param env - * @param instance - * @param jEngineHandle - pointer to audio engine - * @param isDown - true if user is tapping down on screen, false user is lifting finger off screen - */ + JNIEXPORT void JNICALL -Java_com_example_oboe_megadrone_MainActivity_tap(JNIEnv * /*unused*/, jobject /*unused*/, - jlong jEngineHandle, - jboolean isDown) { - auto *engine = reinterpret_cast<AudioEngine *>(jEngineHandle); - if (engine){ +Java_com_example_oboe_megadrone_MainActivity_tap(JNIEnv *env, jobject instance, + jlong jEngineHandle, jboolean isDown) { + + auto *engine = reinterpret_cast<MegaDroneEngine*>(jEngineHandle); + if (engine) { engine->tap(isDown); } else { LOGE("Engine handle is invalid, call createEngine() to create a new one"); diff --git a/samples/hello-oboe/build.gradle b/samples/hello-oboe/build.gradle index cdd975de..26d3ccd3 100644 --- a/samples/hello-oboe/build.gradle +++ b/samples/hello-oboe/build.gradle @@ -27,7 +27,7 @@ android { } externalNativeBuild { cmake { - path 'CMakeLists.txt' + path 'src/main/cpp/CMakeLists.txt' } } } diff --git a/samples/hello-oboe/CMakeLists.txt b/samples/hello-oboe/src/main/cpp/CMakeLists.txt index 5541dcef..77e898e1 100644 --- a/samples/hello-oboe/CMakeLists.txt +++ b/samples/hello-oboe/src/main/cpp/CMakeLists.txt @@ -19,28 +19,30 @@ cmake_minimum_required(VERSION 3.4.1) ### INCLUDE OBOE LIBRARY ### # Set the path to the Oboe library directory -set (OBOE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../..) +set (OBOE_DIR ../../../../../) # Add the Oboe library as a subproject. Since Oboe is an out-of-tree source library we must also # specify a binary directory add_subdirectory(${OBOE_DIR} ./oboe-bin) # Include the Oboe headers and shared sample code -include_directories(${OBOE_DIR}/include ${OBOE_DIR}/samples) - -### END OBOE INCLUDE SECTION ### +include_directories(${OBOE_DIR}/include ${OBOE_DIR}/samples/shared) # Debug utilities -set (DEBUG_UTILS_PATH "../debug-utils") +set (DEBUG_UTILS_PATH "${OBOE_DIR}/samples/debug-utils") set (DEBUG_UTILS_SOURCES ${DEBUG_UTILS_PATH}/trace.cpp) include_directories(${DEBUG_UTILS_PATH}) + +### END OBOE INCLUDE SECTION ### + + # App specific sources -set (APP_DIR src/main/cpp) -file (GLOB_RECURSE APP_SOURCES - ${APP_DIR}/jni_bridge.cpp - ${APP_DIR}/PlayAudioEngine.cpp - ${APP_DIR}/SoundGenerator.cpp +set (APP_SOURCES + jni_bridge.cpp + HelloOboeEngine.cpp + SoundGenerator.cpp + LatencyTuningCallback.cpp ) # Build the libhello-oboe library diff --git a/samples/hello-oboe/src/main/cpp/HelloOboeEngine.cpp b/samples/hello-oboe/src/main/cpp/HelloOboeEngine.cpp new file mode 100644 index 00000000..de3e3bac --- /dev/null +++ b/samples/hello-oboe/src/main/cpp/HelloOboeEngine.cpp @@ -0,0 +1,148 @@ +/** + * Copyright 2017 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 <inttypes.h> +#include <memory> + +#include <Oscillator.h> + +#include "HelloOboeEngine.h" +#include "SoundGenerator.h" + + +/** + * Main audio engine for the HelloOboe sample. It is responsible for: + * + * - Creating a callback object which is supplied when constructing the audio stream, and will be + * called when the stream starts + * - Restarting the stream when user-controllable properties (Audio API, channel count etc) are + * changed, and when the stream is disconnected (e.g. when headphones are attached) + * - Calculating the audio latency of the stream + * + */ +HelloOboeEngine::HelloOboeEngine(): mLatencyCallback(std::make_unique<LatencyTuningCallback>(*this)) { + start(); + updateLatencyDetection(); +} + +double HelloOboeEngine::getCurrentOutputLatencyMillis() { + if (!mIsLatencyDetectionSupported) return -1; + // Get the time that a known audio frame was presented for playing + auto result = mStream->getTimestamp(CLOCK_MONOTONIC); + double outputLatencyMillis = -1; + const int64_t kNanosPerMillisecond = 1000000; + if (result == oboe::Result::OK) { + oboe::FrameTimestamp playedFrame = result.value(); + // Get the write index for the next audio frame + int64_t writeIndex = mStream->getFramesWritten(); + // Calculate the number of frames between our known frame and the write index + int64_t frameIndexDelta = writeIndex - playedFrame.position; + // Calculate the time which the next frame will be presented + int64_t frameTimeDelta = (frameIndexDelta * oboe::kNanosPerSecond) / (mStream->getSampleRate()); + int64_t nextFramePresentationTime = playedFrame.timestamp + frameTimeDelta; + // Assume that the next frame will be written at the current time + using namespace std::chrono; + int64_t nextFrameWriteTime = + duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count(); + // Calculate the latency + outputLatencyMillis = static_cast<double>(nextFramePresentationTime - nextFrameWriteTime) + / kNanosPerMillisecond; + } else { + LOGE("Error calculating latency: %s", oboe::convertToText(result.error())); + } + return outputLatencyMillis; +} + +void HelloOboeEngine::setBufferSizeInBursts(int32_t numBursts) { + mIsLatencyDetectionSupported = false; + mLatencyCallback->setBufferTuneEnabled(numBursts == kBufferSizeAutomatic); + auto result = mStream->setBufferSizeInFrames( + numBursts * mStream->getFramesPerBurst()); + updateLatencyDetection(); + if (result) { + LOGD("Buffer size successfully changed to %d", result.value()); + } else { + LOGW("Buffer size could not be changed, %d", result.error()); + } +} + +void HelloOboeEngine::setAudioApi(oboe::AudioApi audioApi) { + mIsLatencyDetectionSupported = false; + createPlaybackStream(*oboe::AudioStreamBuilder(*mStream) + .setAudioApi(audioApi)); + updateAudioSource(); + LOGD("AudioAPI is now %d", mStream->getAudioApi()); +} + +void HelloOboeEngine::setChannelCount(int channelCount) { + mIsLatencyDetectionSupported = false; + createPlaybackStream(*oboe::AudioStreamBuilder(*mStream) + .setChannelCount(channelCount)); + updateAudioSource(); + LOGD("Channel count is now %d", mStream->getChannelCount()); +} + +void HelloOboeEngine::setDeviceId(int32_t deviceId) { + mIsLatencyDetectionSupported = false; + createPlaybackStream(*oboe::AudioStreamBuilder(*mStream). + setDeviceId(deviceId)); + updateAudioSource(); + LOGD("Device ID is now %d", mStream->getDeviceId()); +} + +bool HelloOboeEngine::isLatencyDetectionSupported() { + return mIsLatencyDetectionSupported; +} + +void HelloOboeEngine::updateLatencyDetection() { + mIsLatencyDetectionSupported = (mStream->getTimestamp((CLOCK_MONOTONIC)) != + oboe::Result::ErrorUnimplemented); +} + +void HelloOboeEngine::tap(bool isDown) { + mAudioSource->tap(isDown); +} + +void HelloOboeEngine::updateAudioSource() { + *mAudioSource = SoundGenerator(mStream->getSampleRate(), mStream->getChannelCount()); + mStream->start(); + updateLatencyDetection(); +} + +oboe::Result HelloOboeEngine::createPlaybackStream(oboe::AudioStreamBuilder builder) { + return builder.setSharingMode(oboe::SharingMode::Exclusive) + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setFormat(oboe::AudioFormat::Float) + ->setCallback(mLatencyCallback.get()) + ->openManagedStream(mStream); +} + +void HelloOboeEngine::restart() { + start(); +} + +void HelloOboeEngine::start() { + auto result = createPlaybackStream(oboe::AudioStreamBuilder()); + if (result == oboe::Result::OK){ + mAudioSource = std::make_shared<SoundGenerator>(mStream->getSampleRate(), mStream->getChannelCount()); + mLatencyCallback->setSource(std::dynamic_pointer_cast<IRenderableAudio>(mAudioSource)); + mStream->start(); + } else { + LOGE("Error creating playback stream. Error: %s", oboe::convertToText(result)); + } +} + diff --git a/samples/hello-oboe/src/main/cpp/HelloOboeEngine.h b/samples/hello-oboe/src/main/cpp/HelloOboeEngine.h new file mode 100644 index 00000000..bf1d92b8 --- /dev/null +++ b/samples/hello-oboe/src/main/cpp/HelloOboeEngine.h @@ -0,0 +1,89 @@ +/* + * Copyright 2017 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 OBOE_HELLO_OBOE_ENGINE_H +#define OBOE_HELLO_OBOE_ENGINE_H + +#include <oboe/Oboe.h> + +#include "SoundGenerator.h" +#include "LatencyTuningCallback.h" +#include "IRestartable.h" + +constexpr int32_t kBufferSizeAutomatic = 0; + +class HelloOboeEngine : public IRestartable { + +public: + HelloOboeEngine(); + + virtual ~HelloOboeEngine() = default; + + void tap(bool isDown); + + // From IRestartable + void restart() override; + + // These methods reset the underlying stream with new properties + + /** + * Set the audio device which should be used for playback. Can be set to oboe::kUnspecified if + * you want to use the default playback device (which is usually the built-in speaker if + * no other audio devices, such as headphones, are attached). + * + * @param deviceId the audio device id, can be obtained through an {@link AudioDeviceInfo} object + * using Java/JNI. + */ + void setDeviceId(int32_t deviceId); + + void setChannelCount(int channelCount); + + void setAudioApi(oboe::AudioApi audioApi); + + void setBufferSizeInBursts(int32_t numBursts); + + /** + * Calculate the current latency between writing a frame to the output stream and + * the same frame being presented to the audio hardware. + * + * Here's how the calculation works: + * + * 1) Get the time a particular frame was presented to the audio hardware + * @see AudioStream::getTimestamp + * 2) From this extrapolate the time which the *next* audio frame written to the stream + * will be presented + * 3) Assume that the next audio frame is written at the current time + * 4) currentLatency = nextFramePresentationTime - nextFrameWriteTime + * + * @return Output Latency in Milliseconds + */ + double getCurrentOutputLatencyMillis(); + + bool isLatencyDetectionSupported(); + +private: + oboe::ManagedStream mStream; + std::unique_ptr<LatencyTuningCallback> mLatencyCallback; + std::shared_ptr<SoundGenerator> mAudioSource; + bool mIsLatencyDetectionSupported = false; + + oboe::Result createPlaybackStream(oboe::AudioStreamBuilder builder); + void updateLatencyDetection(); + void updateAudioSource(); + void start(); +}; + +#endif //OBOE_HELLO_OBOE_ENGINE_H diff --git a/samples/hello-oboe/src/main/cpp/LatencyTuningCallback.cpp b/samples/hello-oboe/src/main/cpp/LatencyTuningCallback.cpp new file mode 100644 index 00000000..0cc4a1d9 --- /dev/null +++ b/samples/hello-oboe/src/main/cpp/LatencyTuningCallback.cpp @@ -0,0 +1,40 @@ +/** + * Copyright 2017 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 "LatencyTuningCallback.h" + +oboe::DataCallbackResult LatencyTuningCallback::onAudioReady( + oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) { + if (!mLatencyTuner) mLatencyTuner = std::make_unique<oboe::LatencyTuner>(*oboeStream); + if (mBufferTuneEnabled && oboeStream->getAudioApi() == oboe::AudioApi::AAudio) { + mLatencyTuner->tune(); + } + auto underrunCountResult = oboeStream->getXRunCount(); + int bufferSize = oboeStream->getBufferSizeInFrames(); + /** + * The following output can be seen by running a systrace. Tracing is preferable to logging + * inside the callback since tracing does not block. + * + * See https://developer.android.com/studio/profile/systrace-commandline.html + */ + if (Trace::isEnabled()) Trace::beginSection("numFrames %d, Underruns %d, buffer size %d", + numFrames, underrunCountResult.value(), bufferSize); + auto result = DefaultAudioStreamCallback::onAudioReady(oboeStream, audioData, numFrames); + if (Trace::isEnabled()) Trace::endSection(); + return result; +} + + diff --git a/samples/hello-oboe/src/main/cpp/LatencyTuningCallback.h b/samples/hello-oboe/src/main/cpp/LatencyTuningCallback.h new file mode 100644 index 00000000..42cbe4c2 --- /dev/null +++ b/samples/hello-oboe/src/main/cpp/LatencyTuningCallback.h @@ -0,0 +1,64 @@ +/** + * Copyright 2017 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_LATENCY_TUNING_CALLBACK_H +#define SAMPLES_LATENCY_TUNING_CALLBACK_H + +#include <oboe/Oboe.h> +#include <oboe/LatencyTuner.h> + +#include <TappableAudioSource.h> +#include <DefaultAudioStreamCallback.h> +#include <trace.h> + +/** + * This callback object extends the functionality of `DefaultAudioStreamCallback` by automatically + * tuning the latency of the audio stream. @see onAudioReady for more details on this. + * + * It also demonstrates how to use tracing functions for logging inside the audio callback without + * blocking. + */ +class LatencyTuningCallback: public DefaultAudioStreamCallback { +public: + LatencyTuningCallback(IRestartable &mParent) : DefaultAudioStreamCallback(mParent) { + + // Initialize the trace functions, this enables you to output trace statements without + // blocking. See https://developer.android.com/studio/profile/systrace-commandline.html + Trace::initialize(); + } + + /** + * Every time the playback stream requires data this method will be called. + * + * @param audioStream the audio stream which is requesting data, this is the mPlayStream object + * @param audioData an empty buffer into which we can write our audio data + * @param numFrames the number of audio frames which are required + * @return Either oboe::DataCallbackResult::Continue if the stream should continue requesting data + * or oboe::DataCallbackResult::Stop if the stream should stop. + */ + oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override; + + void setBufferTuneEnabled(bool enabled) {mBufferTuneEnabled = enabled;} + +private: + bool mBufferTuneEnabled = true; + + // This will be used to automatically tune the buffer size of the stream, obtaining optimal latency + std::unique_ptr<oboe::LatencyTuner> mLatencyTuner; +}; + +#endif //SAMPLES_LATENCY_TUNING_CALLBACK_H diff --git a/samples/hello-oboe/src/main/cpp/PlayAudioEngine.cpp b/samples/hello-oboe/src/main/cpp/PlayAudioEngine.cpp deleted file mode 100644 index a03a9317..00000000 --- a/samples/hello-oboe/src/main/cpp/PlayAudioEngine.cpp +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Copyright 2017 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 <trace.h> -#include <inttypes.h> -#include <memory> - -#include "shared/Oscillator.h" - -#include "PlayAudioEngine.h" -#include "logging_macros.h" -#include "SoundGenerator.h" - -constexpr int64_t kNanosPerMillisecond = 1000000; // Use int64_t to avoid overflows in calculations - - -PlayAudioEngine::PlayAudioEngine() { - - // Initialize the trace functions, this enables you to output trace statements without - // blocking. See https://developer.android.com/studio/profile/systrace-commandline.html - Trace::initialize(); - oboe::AudioStreamBuilder builder = oboe::AudioStreamBuilder(); - createPlaybackStream(&builder); -} - -/** - * Creates an audio stream for playback. Takes in a builder pointer which contains stream params - */ -void PlayAudioEngine::createPlaybackStream(oboe::AudioStreamBuilder *builder) { - oboe::Result result = builder->setSharingMode(oboe::SharingMode::Exclusive) - ->setPerformanceMode(oboe::PerformanceMode::LowLatency) - ->setCallback(this) - ->openManagedStream(mPlayStream); - if (result == oboe::Result::OK && mPlayStream.get() != nullptr) { - // Set the buffer size to the burst size - this will give us the minimum possible latency - mPlayStream->setBufferSizeInFrames(mPlayStream->getFramesPerBurst()); - - // TODO: Implement Oboe_convertStreamToText - // PrintAudioStreamInfo(mPlayStream); - if (mPlayStream->getFormat() == oboe::AudioFormat::I16){ - // create a buffer of floats which we can render our audio data into - int conversionBufferSamples = mPlayStream->getBufferCapacityInFrames() * mPlayStream->getChannelCount(); - LOGD("Stream format is 16-bit integers, creating a temporary buffer of %d samples" - " for float->int16 conversion", conversionBufferSamples); - mConversionBuffer = std::make_unique<float[]>(conversionBufferSamples); - } - - mSoundGenerator = std::make_unique<SoundGenerator>( - mPlayStream->getSampleRate(), - mPlayStream->getBufferCapacityInFrames(), - mPlayStream->getChannelCount() - ); - - // Create a latency tuner which will automatically tune our buffer size. - mLatencyTuner = std::make_unique<oboe::LatencyTuner>(*mPlayStream); - // Start the stream - the dataCallback function will start being called - result = mPlayStream->requestStart(); - if (result != oboe::Result::OK) { - LOGE("Error starting stream. %s", oboe::convertToText(result)); - } - - mIsLatencyDetectionSupported = (mPlayStream->getTimestamp(CLOCK_MONOTONIC, 0, 0) != - oboe::Result::ErrorUnimplemented); - - } else { - LOGE("Failed to create stream. Error: %s", oboe::convertToText(result)); - } -} - -void PlayAudioEngine::setToneOn(bool isToneOn) { - mSoundGenerator->setTonesOn(isToneOn); -} - -/** - * Every time the playback stream requires data this method will be called. - * - * @param audioStream the audio stream which is requesting data, this is the mPlayStream object - * @param audioData an empty buffer into which we can write our audio data - * @param numFrames the number of audio frames which are required - * @return Either oboe::DataCallbackResult::Continue if the stream should continue requesting data - * or oboe::DataCallbackResult::Stop if the stream should stop. - */ -oboe::DataCallbackResult -PlayAudioEngine::onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) { - - if (mBufferSizeSelection == kBufferSizeAutomatic) mLatencyTuner->tune(); - int32_t bufferSize = audioStream->getBufferSizeInFrames(); - - /** - * The following output can be seen by running a systrace. Tracing is preferable to logging - * inside the callback since tracing does not block. - * - * See https://developer.android.com/studio/profile/systrace-commandline.html - */ - auto underrunCountResult = audioStream->getXRunCount(); - - if (Trace::isEnabled()) Trace::beginSection("numFrames %d, Underruns %d, buffer size %d", - numFrames, underrunCountResult.value(), bufferSize); - - bool is16BitFormat = (audioStream->getFormat() == oboe::AudioFormat::I16); - int32_t channelCount = audioStream->getChannelCount(); - - // If the stream is 16-bit render into a float buffer then convert that buffer to 16-bit ints - float *outputBuffer = (is16BitFormat) ? mConversionBuffer.get() : static_cast<float *>(audioData); - mSoundGenerator->renderAudio(outputBuffer, numFrames); - - if (is16BitFormat){ - oboe::convertFloatToPcm16(outputBuffer, - static_cast<int16_t *>(audioData), - numFrames * channelCount); - } - - if (mIsLatencyDetectionSupported) { - calculateCurrentOutputLatencyMillis(audioStream, &mCurrentOutputLatencyMillis); - } - - if (Trace::isEnabled()) Trace::endSection(); - return oboe::DataCallbackResult::Continue; -} - -/** - * Calculate the current latency between writing a frame to the output stream and - * the same frame being presented to the audio hardware. - * - * Here's how the calculation works: - * - * 1) Get the time a particular frame was presented to the audio hardware - * @see AudioStream::getTimestamp - * 2) From this extrapolate the time which the *next* audio frame written to the stream - * will be presented - * 3) Assume that the next audio frame is written at the current time - * 4) currentLatency = nextFramePresentationTime - nextFrameWriteTime - * - * @param stream The stream being written to - * @param latencyMillis pointer to a variable to receive the latency in milliseconds between - * writing a frame to the stream and that frame being presented to the audio hardware. - * @return oboe::Result::OK or a oboe::Result::Error* value. It is normal to receive an error soon - * after a stream has started because the timestamps are not yet available. - */ -oboe::Result -PlayAudioEngine::calculateCurrentOutputLatencyMillis(oboe::AudioStream *stream, - double *latencyMillis) { - - // Get the time that a known audio frame was presented for playing - auto result = stream->getTimestamp(CLOCK_MONOTONIC); - - if (result == oboe::Result::OK) { - - oboe::FrameTimestamp playedFrame = result.value(); - - // Get the write index for the next audio frame - int64_t writeIndex = stream->getFramesWritten(); - - // Calculate the number of frames between our known frame and the write index - int64_t frameIndexDelta = writeIndex - playedFrame.position; - - // Calculate the time which the next frame will be presented - int64_t frameTimeDelta = (frameIndexDelta * oboe::kNanosPerSecond) / (stream->getSampleRate()); - int64_t nextFramePresentationTime = playedFrame.timestamp + frameTimeDelta; - - // Assume that the next frame will be written at the current time - using namespace std::chrono; - int64_t nextFrameWriteTime = - duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count(); - - // Calculate the latency - *latencyMillis = static_cast<double>(nextFramePresentationTime - nextFrameWriteTime) - / kNanosPerMillisecond; - } else { - LOGE("Error calculating latency: %s", oboe::convertToText(result.error())); - } - - return result; -} - -/** - * If there is an error with a stream this function will be called. A common example of an error - * is when an audio device (such as headphones) is disconnected. It is safe to restart the stream - * in this method. There is no need to create a new thread. - * - * @param audioStream the stream with the error - * @param error the error which occured, a human readable string can be obtained using - * oboe::convertToText(error); - * - * @see oboe::StreamCallback - */ -void PlayAudioEngine::onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) { - if (error == oboe::Result::ErrorDisconnected) { - oboe::AudioStreamBuilder builder_ = oboe::AudioStreamBuilder(*oboeStream); - restartStream(&builder_); - } -} - -void PlayAudioEngine::restartStream(oboe::AudioStreamBuilder *builder) { - LOGI("Restarting stream"); - createPlaybackStream(builder); -} - -double PlayAudioEngine::getCurrentOutputLatencyMillis() { - return mCurrentOutputLatencyMillis; -} - -void PlayAudioEngine::setBufferSizeInBursts(int32_t numBursts) { - mBufferSizeSelection = numBursts; - auto result = mPlayStream->setBufferSizeInFrames( - mBufferSizeSelection * mPlayStream->getFramesPerBurst()); - if (result) { - LOGD("Buffer size successfully changed to %d", result.value()); - } else { - LOGW("Buffer size could not be changed, %d", result.error()); - } -} - -bool PlayAudioEngine::isLatencyDetectionSupported() { - return mIsLatencyDetectionSupported; -} -void PlayAudioEngine::setAudioApi(oboe::AudioApi audioApi) { - oboe::AudioStreamBuilder *builder = oboe::AudioStreamBuilder(*mPlayStream).setAudioApi(audioApi); - restartStream(builder); - LOGD("AudioAPI is now %d", mPlayStream->getAudioApi()); -} - -void PlayAudioEngine::setChannelCount(int channelCount) { - oboe::AudioStreamBuilder *builder = oboe::AudioStreamBuilder(*mPlayStream).setChannelCount(channelCount); - restartStream(builder); - LOGD("Channel count is now %d", mPlayStream->getChannelCount()); -} - -/** - * Set the audio device which should be used for playback. Can be set to oboe::kUnspecified if - * you want to use the default playback device (which is usually the built-in speaker if - * no other audio devices, such as headphones, are attached). - * - * @param deviceId the audio device id, can be obtained through an {@link AudioDeviceInfo} object - * using Java/JNI. - */ -void PlayAudioEngine::setDeviceId(int32_t deviceId) { - oboe::AudioStreamBuilder *builder = oboe::AudioStreamBuilder(*mPlayStream).setDeviceId(deviceId); - restartStream(builder); - LOGD("Device ID is now %d", mPlayStream->getDeviceId()); -} - diff --git a/samples/hello-oboe/src/main/cpp/PlayAudioEngine.h b/samples/hello-oboe/src/main/cpp/PlayAudioEngine.h deleted file mode 100644 index 4f1ec098..00000000 --- a/samples/hello-oboe/src/main/cpp/PlayAudioEngine.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2017 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 OBOE_HELLOOBOE_PLAYAUDIOENGINE_H -#define OBOE_HELLOOBOE_PLAYAUDIOENGINE_H - -#include <oboe/Oboe.h> - -#include "SoundGenerator.h" - -constexpr int32_t kBufferSizeAutomatic = 0; - -class PlayAudioEngine : oboe::AudioStreamCallback { - -public: - PlayAudioEngine(); - - - void setAudioApi(oboe::AudioApi audioApi); - - void setDeviceId(int32_t deviceId); - - void setChannelCount(int channelCount); - - void setBufferSizeInBursts(int32_t numBursts); - - void setToneOn(bool isToneOn); - - double getCurrentOutputLatencyMillis(); - - bool isLatencyDetectionSupported(); - - // oboe::StreamCallback methods - oboe::DataCallbackResult - onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32_t numFrames); - - void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error); - - - -private: - oboe::ManagedStream mPlayStream; - double mCurrentOutputLatencyMillis = 0; - int32_t mBufferSizeSelection = kBufferSizeAutomatic; // Used to keep track if we are auto tuning - bool mIsLatencyDetectionSupported = false; - std::unique_ptr<oboe::LatencyTuner> mLatencyTuner; - std::unique_ptr<SoundGenerator> mSoundGenerator; - std::unique_ptr<float[]> mConversionBuffer { nullptr }; - // We will handle conversion to avoid getting kicked off the fast track as penalty - - void createPlaybackStream(oboe::AudioStreamBuilder *builder); - - void restartStream(oboe::AudioStreamBuilder *builder); - - oboe::Result calculateCurrentOutputLatencyMillis(oboe::AudioStream *stream, double *latencyMillis); - -}; - -#endif //OBOE_HELLOOBOE_PLAYAUDIOENGINE_H diff --git a/samples/hello-oboe/src/main/cpp/SoundGenerator.cpp b/samples/hello-oboe/src/main/cpp/SoundGenerator.cpp index 690bf37e..1de9030f 100644 --- a/samples/hello-oboe/src/main/cpp/SoundGenerator.cpp +++ b/samples/hello-oboe/src/main/cpp/SoundGenerator.cpp @@ -14,14 +14,11 @@ * limitations under the License. */ -#include <memory> #include "SoundGenerator.h" -SoundGenerator::SoundGenerator(int32_t sampleRate, int32_t maxFrames, int32_t channelCount) - : mSampleRate(sampleRate) - , mChannelCount(channelCount) - , mOscillators(std::make_unique<Oscillator[]>(channelCount)) - , mBuffer(std::make_unique<float[]>(maxFrames)){ +SoundGenerator::SoundGenerator(int32_t sampleRate, int32_t channelCount) : + TappableAudioSource(sampleRate, channelCount) + , mOscillators(std::make_unique<Oscillator[]>(channelCount)){ double frequency = 440.0; constexpr double interval = 110.0; @@ -37,18 +34,17 @@ SoundGenerator::SoundGenerator(int32_t sampleRate, int32_t maxFrames, int32_t ch } void SoundGenerator::renderAudio(float *audioData, int32_t numFrames) { - // Render each oscillator into its own channel + std::fill_n(mBuffer.get(), kSharedBufferSize, 0); for (int i = 0; i < mChannelCount; ++i) { - mOscillators[i].renderAudio(mBuffer.get(), numFrames); for (int j = 0; j < numFrames; ++j) { - audioData[(j*mChannelCount)+i] = mBuffer[j]; + audioData[(j * mChannelCount) + i] = mBuffer[j]; } } } -void SoundGenerator::setTonesOn(bool isOn) { +void SoundGenerator::tap(bool isOn) { for (int i = 0; i < mChannelCount; ++i) { mOscillators[i].setWaveOn(isOn); } diff --git a/samples/hello-oboe/src/main/cpp/SoundGenerator.h b/samples/hello-oboe/src/main/cpp/SoundGenerator.h index 7577cdca..1ffae37f 100644 --- a/samples/hello-oboe/src/main/cpp/SoundGenerator.h +++ b/samples/hello-oboe/src/main/cpp/SoundGenerator.h @@ -18,13 +18,15 @@ #define SAMPLES_SOUNDGENERATOR_H -#include <shared/IRenderableAudio.h> -#include <shared/Oscillator.h> +#include <Oscillator.h> +#include <TappableAudioSource.h> /** * Generates a fixed frequency tone for each channel. + * Implements RenderableTap (sound source with toggle) which is required for AudioEngines. */ -class SoundGenerator : public IRenderableAudio { +class SoundGenerator : public TappableAudioSource { + static constexpr size_t kSharedBufferSize = 1024; public: /** * Create a new SoundGenerator object. @@ -36,20 +38,20 @@ public: * channel, the output will be interlaced. * */ - SoundGenerator(int32_t sampleRate, int32_t maxFrames, int32_t channelCount); + SoundGenerator(int32_t sampleRate, int32_t channelCount); ~SoundGenerator() = default; + SoundGenerator(SoundGenerator&& other) = default; + SoundGenerator& operator= (SoundGenerator&& other) = default; + // Switch the tones on - void setTonesOn(bool isOn); + void tap(bool isOn) override; - // From IRenderableAudio void renderAudio(float *audioData, int32_t numFrames) override; private: - const int32_t mSampleRate; - const int32_t mChannelCount; - const std::unique_ptr<Oscillator[]> mOscillators; - const std::unique_ptr<float[]> mBuffer; + std::unique_ptr<Oscillator[]> mOscillators; + std::unique_ptr<float[]> mBuffer = std::make_unique<float[]>(kSharedBufferSize); }; diff --git a/samples/hello-oboe/src/main/cpp/jni_bridge.cpp b/samples/hello-oboe/src/main/cpp/jni_bridge.cpp index 512ed306..b248ff03 100644 --- a/samples/hello-oboe/src/main/cpp/jni_bridge.cpp +++ b/samples/hello-oboe/src/main/cpp/jni_bridge.cpp @@ -16,7 +16,7 @@ #include <jni.h> #include <oboe/Oboe.h> -#include "PlayAudioEngine.h" +#include "HelloOboeEngine.h" #include "logging_macros.h" extern "C" { @@ -31,7 +31,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1createEngine( JNIEnv *env, jclass /*unused*/) { // We use std::nothrow so `new` returns a nullptr if the engine creation fails - PlayAudioEngine *engine = new(std::nothrow) PlayAudioEngine(); + HelloOboeEngine *engine = new(std::nothrow) HelloOboeEngine(); return reinterpret_cast<jlong>(engine); } @@ -41,7 +41,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1deleteEngine( jclass, jlong engineHandle) { - delete reinterpret_cast<PlayAudioEngine *>(engineHandle); + delete reinterpret_cast<HelloOboeEngine *>(engineHandle); } JNIEXPORT void JNICALL @@ -51,12 +51,12 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1setToneOn( jlong engineHandle, jboolean isToneOn) { - PlayAudioEngine *engine = reinterpret_cast<PlayAudioEngine *>(engineHandle); + HelloOboeEngine *engine = reinterpret_cast<HelloOboeEngine *>(engineHandle); if (engine == nullptr) { LOGE("Engine handle is invalid, call createHandle() to create a new one"); return; } - engine->setToneOn(isToneOn); + engine->tap(isToneOn); } JNIEXPORT void JNICALL @@ -66,7 +66,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1setAudioApi( jlong engineHandle, jint audioApi) { - PlayAudioEngine *engine = reinterpret_cast<PlayAudioEngine*>(engineHandle); + HelloOboeEngine *engine = reinterpret_cast<HelloOboeEngine*>(engineHandle); if (engine == nullptr) { LOGE("Engine handle is invalid, call createHandle() to create a new one"); return; @@ -83,7 +83,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1setAudioDeviceId( jlong engineHandle, jint deviceId) { - PlayAudioEngine *engine = reinterpret_cast<PlayAudioEngine*>(engineHandle); + HelloOboeEngine *engine = reinterpret_cast<HelloOboeEngine*>(engineHandle); if (engine == nullptr) { LOGE("Engine handle is invalid, call createHandle() to create a new one"); return; @@ -98,7 +98,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1setChannelCount( jlong engineHandle, jint channelCount) { - PlayAudioEngine *engine = reinterpret_cast<PlayAudioEngine*>(engineHandle); + HelloOboeEngine *engine = reinterpret_cast<HelloOboeEngine*>(engineHandle); if (engine == nullptr) { LOGE("Engine handle is invalid, call createHandle() to create a new one"); return; @@ -113,7 +113,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1setBufferSizeInBurs jlong engineHandle, jint bufferSizeInBursts) { - PlayAudioEngine *engine = reinterpret_cast<PlayAudioEngine*>(engineHandle); + HelloOboeEngine *engine = reinterpret_cast<HelloOboeEngine*>(engineHandle); if (engine == nullptr) { LOGE("Engine handle is invalid, call createHandle() to create a new one"); return; @@ -128,7 +128,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1getCurrentOutputLat jclass, jlong engineHandle) { - PlayAudioEngine *engine = reinterpret_cast<PlayAudioEngine*>(engineHandle); + HelloOboeEngine *engine = reinterpret_cast<HelloOboeEngine*>(engineHandle); if (engine == nullptr) { LOGE("Engine is null, you must call createEngine before calling this method"); return static_cast<jdouble>(-1.0); @@ -142,7 +142,7 @@ Java_com_google_sample_oboe_hellooboe_PlaybackEngine_native_1isLatencyDetectionS jclass type, jlong engineHandle) { - PlayAudioEngine *engine = reinterpret_cast<PlayAudioEngine*>(engineHandle); + HelloOboeEngine *engine = reinterpret_cast<HelloOboeEngine*>(engineHandle); if (engine == nullptr) { LOGE("Engine is null, you must call createEngine before calling this method"); return JNI_FALSE; diff --git a/samples/hello-oboe/src/main/cpp/ndk-stl-config.cmake b/samples/hello-oboe/src/main/cpp/ndk-stl-config.cmake deleted file mode 100644 index 3133faf3..00000000 --- a/samples/hello-oboe/src/main/cpp/ndk-stl-config.cmake +++ /dev/null @@ -1,39 +0,0 @@ -# Copy shared STL files to Android Studio output directory so they can be -# packaged in the APK. -# Usage: -# -# find_package(ndk-stl REQUIRED) -# -# or -# -# find_package(ndk-stl REQUIRED PATHS ".") - -if(NOT ${ANDROID_STL} MATCHES "_shared") - return() -endif() - -function(configure_shared_stl lib_path so_base) - message("Configuring STL ${so_base} for ${ANDROID_ABI}") - configure_file( - "${ANDROID_NDK}/sources/cxx-stl/${lib_path}/libs/${ANDROID_ABI}/lib${so_base}.so" - "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${so_base}.so" - COPYONLY) -endfunction() - -if("${ANDROID_STL}" STREQUAL "libstdc++") - # The default minimal system C++ runtime library. -elseif("${ANDROID_STL}" STREQUAL "gabi++_shared") - # The GAbi++ runtime (shared). - message(FATAL_ERROR "gabi++_shared was not configured by ndk-stl package") -elseif("${ANDROID_STL}" STREQUAL "stlport_shared") - # The STLport runtime (shared). - configure_shared_stl("stlport" "stlport_shared") -elseif("${ANDROID_STL}" STREQUAL "gnustl_shared") - # The GNU STL (shared). - configure_shared_stl("gnu-libstdc++/4.9" "gnustl_shared") -elseif("${ANDROID_STL}" STREQUAL "c++_shared") - # The LLVM libc++ runtime (shared). - configure_shared_stl("llvm-libc++" "c++_shared") -else() - message(FATAL_ERROR "STL configuration ANDROID_STL=${ANDROID_STL} is not supported") -endif() diff --git a/samples/shared/DefaultAudioStreamCallback.h b/samples/shared/DefaultAudioStreamCallback.h new file mode 100644 index 00000000..64b84748 --- /dev/null +++ b/samples/shared/DefaultAudioStreamCallback.h @@ -0,0 +1,139 @@ +/* + * Copyright 2019 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_DEFAULT_AUDIO_STREAM_CALLBACK_H +#define SAMPLES_DEFAULT_AUDIO_STREAM_CALLBACK_H + + +#include <vector> +#include <oboe/AudioStreamCallback.h> +#include <logging_macros.h> + +#include "IRenderableAudio.h" +#include "IRestartable.h" + +/** + * This is a callback object which will render data from an `IRenderableAudio` source. It is + * constructed using an `IRestartable` which allows it to automatically restart the parent object + * if the stream is disconnected (for example, when headphones are attached). + * + * @param IRestartable - the object which should be restarted when the stream is disconnected + */ +class DefaultAudioStreamCallback : public oboe::AudioStreamCallback { +public: + DefaultAudioStreamCallback(IRestartable &parent): mParent(parent) {} + virtual ~DefaultAudioStreamCallback() = default; + + virtual oboe::DataCallbackResult + onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override { + + if (mIsThreadAffinityEnabled && !mIsThreadAffinitySet) { + setThreadAffinity(); + mIsThreadAffinitySet = true; + } + + float *outputBuffer = static_cast<float *>(audioData); + if (!mRenderable) { + LOGE("Renderable source not set!"); + return oboe::DataCallbackResult::Stop; + } + mRenderable->renderAudio(outputBuffer, numFrames); + return oboe::DataCallbackResult::Continue; + } + virtual void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override { + // Restart the stream when it errors out with disconnect + if (error == oboe::Result::ErrorDisconnected) { + LOGE("Restarting AudioStream after disconnect"); + mParent.restart(); + } else { + LOGE("Unknown error"); + } + mIsThreadAffinitySet = false; + } + + void setSource(std::shared_ptr<IRenderableAudio> renderable) { + mRenderable = renderable; + } + + std::shared_ptr<IRenderableAudio> getSource() { + return mRenderable; + } + + /** + * Set the CPU IDs to bind the audio callback thread to + * + * @param mCpuIds - the CPU IDs to bind to + */ + void setCpuIds(std::vector<int> cpuIds){ + mCpuIds = std::move(cpuIds); + } + + /** + * Enable or disable binding the audio callback thread to specific CPU cores. The CPU core IDs + * can be specified using @see setCpuIds. If no CPU IDs are specified the initial core which the + * audio thread is called on will be used. + * + * @param isEnabled - whether the audio callback thread should be bound to specific CPU core(s) + */ + void setThreadAffinityEnabled(bool isEnabled){ + mIsThreadAffinityEnabled = isEnabled; + LOGD("Thread affinity enabled: %s", (isEnabled) ? "true" : "false"); + } + +private: + std::shared_ptr<IRenderableAudio> mRenderable; + IRestartable &mParent; + std::vector<int> mCpuIds; // IDs of CPU cores which the audio callback should be bound to + std::atomic<bool> mIsThreadAffinityEnabled { false }; + std::atomic<bool> mIsThreadAffinitySet { false }; + + /** + * Set the thread affinity for the current thread to mCpuIds. This can be useful to call on the + * audio thread to avoid underruns caused by CPU core migrations to slower CPU cores. + */ + void setThreadAffinity() { + + pid_t current_thread_id = gettid(); + cpu_set_t cpu_set; + CPU_ZERO(&cpu_set); + + // If the callback cpu ids aren't specified then bind to the current cpu + if (mCpuIds.empty()) { + int current_cpu_id = sched_getcpu(); + LOGD("Binding to current CPU ID %d", current_cpu_id); + CPU_SET(current_cpu_id, &cpu_set); + } else { + LOGD("Binding to %d CPU IDs", static_cast<int>(mCpuIds.size())); + for (size_t i = 0; i < mCpuIds.size(); i++) { + int cpu_id = mCpuIds.at(i); + LOGD("CPU ID %d added to cores set", cpu_id); + CPU_SET(cpu_id, &cpu_set); + } + } + + int result = sched_setaffinity(current_thread_id, sizeof(cpu_set_t), &cpu_set); + if (result == 0) { + LOGV("Thread affinity set"); + } else { + LOGW("Error setting thread affinity. Error no: %d", result); + } + + mIsThreadAffinitySet = true; + } + +}; + +#endif //SAMPLES_DEFAULT_AUDIO_STREAM_CALLBACK_H diff --git a/samples/shared/IRenderableAudio.h b/samples/shared/IRenderableAudio.h index eee9872e..da0b8cbc 100644 --- a/samples/shared/IRenderableAudio.h +++ b/samples/shared/IRenderableAudio.h @@ -14,8 +14,8 @@ * limitations under the License. */ -#ifndef MEGADRONE_RENDERABLEAUDIO_H -#define MEGADRONE_RENDERABLEAUDIO_H +#ifndef SAMPLES_IRENDERABLEAUDIO_H +#define SAMPLES_IRENDERABLEAUDIO_H #include <cstdint> #include <string> @@ -28,4 +28,4 @@ public: }; -#endif //MEGADRONE_RENDERABLEAUDIO_H +#endif //SAMPLES_IRENDERABLEAUDIO_H diff --git a/samples/shared/IRestartable.h b/samples/shared/IRestartable.h new file mode 100644 index 00000000..855fff3a --- /dev/null +++ b/samples/shared/IRestartable.h @@ -0,0 +1,28 @@ +/* + * 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_IRESTARTABLE_H +#define SAMPLES_IRESTARTABLE_H + +/** + * Represents an object which can be restarted. For example an audio engine which has one or more + * streams which can be restarted following a change in audio device configuration. For example, + * headphones being connected. + */ +class IRestartable { +public: + virtual void restart() = 0; +}; +#endif //SAMPLES_IRESTARTABLE_H diff --git a/samples/shared/ITappable.h b/samples/shared/ITappable.h new file mode 100644 index 00000000..40cb7167 --- /dev/null +++ b/samples/shared/ITappable.h @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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_ITAPPABLE_H +#define SAMPLES_ITAPPABLE_H + +class ITappable { +public: + virtual ~ITappable() = default; + virtual void tap(bool isDown) = 0; +}; +#endif //SAMPLES_ITAPPABLE_H diff --git a/samples/shared/TappableAudioSource.h b/samples/shared/TappableAudioSource.h new file mode 100644 index 00000000..fde3917b --- /dev/null +++ b/samples/shared/TappableAudioSource.h @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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_RENDERABLE_TAP_H +#define SAMPLES_RENDERABLE_TAP_H + +#include <stdint.h> + +#include "IRenderableAudio.h" +#include "ITappable.h" + +/** + * This class renders Float audio, but can be tapped to control. + * It also contains members for sample rate and channel count + */ +class TappableAudioSource : public IRenderableAudio, public ITappable { +public: + TappableAudioSource(int32_t sampleRate, int32_t channelCount) : + mSampleRate(sampleRate), mChannelCount(channelCount) { } + + int32_t mSampleRate; + int32_t mChannelCount; +}; + +#endif //SAMPLES_RENDERABLE_TAP_H
\ No newline at end of file |