There are two ways use Oboe in your Android Studio project:
- Use the Oboe pre-built library binaries and headers. Use this approach if you just want to use a stable version of the Oboe library in your project.
or
- Build Oboe from source. Use this approach if you would like to debug or make changes to the Oboe source code and contribute back to the project.
Oboe is distributed as a prefab package via Google Maven (search for "oboe"). Prefab support was added to Android Studio Preview 4.0 Canary 9 so you'll need to be using this version of Android Studio or above.
Add the oboe dependency to your app's build.gradle
file. Replace "1.4.3" with the latest stable version of Oboe:
dependencies {
implementation 'com.google.oboe:oboe:1.4.3'
}
Also enable prefab by adding:
android {
buildFeatures {
prefab true
}
}
Include and link to oboe by updating your CMakeLists.txt
:
find_package (oboe REQUIRED CONFIG)
target_link_libraries(app oboe::oboe) # You may have other libraries here such as `log`.
Here's a complete example CMakeLists.txt
file:
cmake_minimum_required(VERSION 3.4.1)
# Build our own native library
add_library (native-lib SHARED native-lib.cpp )
# Find the Oboe package
find_package (oboe REQUIRED CONFIG)
# Specify the libraries which our native library is dependent on, including Oboe
target_link_libraries(app log oboe::oboe)
Configure your app to use the shared STL by updating your app/build.gradle
:
android {
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
}
}
}
}
Start by cloning the latest stable release of the Oboe repository, for example:
git clone -b 1.4-stable https://github.com/google/oboe
Make a note of the path which you cloned oboe into - you will need it shortly
If you use git as your version control system, consider adding Oboe as a submodule (underneath your cpp directory)
git submodule add https://github.com/google/oboe
This makes it easier to integrate updates to Oboe into your app, as well as contribute to the Oboe project.
Open your app's CMakeLists.txt
. This can be found under External Build Files
in the Android project view. If you don't have a CMakeLists.txt
you will need to add C++ support to your project.
Now add the following commands to the end of CMakeLists.txt
. Remember to update **PATH TO OBOE**
with your local Oboe path from the previous step:
# Set the path to the Oboe directory.
set (OBOE_DIR ***PATH TO OBOE***)
# Add the Oboe library as a subdirectory in your project.
# add_subdirectory tells CMake to look in this directory to
# compile oboe source files using oboe's CMake file.
# ./oboe specifies where the compiled binaries will be stored
add_subdirectory (${OBOE_DIR} ./oboe)
# Specify the path to the Oboe header files.
# This allows targets compiled with this CMake (application code)
# to see public Oboe headers, in order to access its API.
include_directories (${OBOE_DIR}/include)
In the same file find the target_link_libraries
command.
Add oboe
to the list of libraries which your app's library depends on. For example:
target_link_libraries(native-lib oboe)
Here's a complete example CMakeLists.txt
file:
cmake_minimum_required(VERSION 3.4.1)
# Build our own native library
add_library (native-lib SHARED native-lib.cpp )
# Build the Oboe library
set (OBOE_DIR ./oboe)
add_subdirectory (${OBOE_DIR} ./oboe)
# Make the Oboe public headers available to our app
include_directories (${OBOE_DIR}/include)
# Specify the libraries which our native library is dependent on, including Oboe
target_link_libraries (native-lib log oboe)
Now go to Build->Refresh Linked C++ Projects
to have Android Studio index the Oboe library.
Verify that your project builds correctly. If you have any issues building please report them here.
Once you've added Oboe to your project you can start using Oboe's features. The simplest, and probably most common thing you'll do in Oboe is to create an audio stream.
Include the Oboe header:
#include <oboe/Oboe.h>
Streams are built using an AudioStreamBuilder
. Create one like this:
oboe::AudioStreamBuilder builder;
Use the builder's set methods to set properties on the stream (you can read more about these properties in the full guide):
builder.setDirection(oboe::Direction::Output);
builder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
builder.setSharingMode(oboe::SharingMode::Exclusive);
builder.setFormat(oboe::AudioFormat::Float);
builder.setChannelCount(oboe::ChannelCount::Mono);
The builder's set methods return a pointer to the builder. So they can be easily chained:
oboe::AudioStreamBuilder builder;
builder.setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setSharingMode(oboe::SharingMode::Exclusive)
->setDataCallback(myCallback)
->setFormat(oboe::AudioFormat::Float);
Define an AudioStreamDataCallback
class to receive callbacks whenever the stream requires new data.
class MyCallback : public oboe::AudioStreamDataCallback {
public:
oboe::DataCallbackResult
onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) {
// We requested AudioFormat::Float so we assume we got it.
// For production code always check what format
// the stream has and cast to the appropriate type.
auto *outputData = static_cast<float *>(audioData);
// Generate random numbers (white noise) centered around zero.
const float amplitude = 0.2f;
for (int i = 0; i < numFrames; ++i){
outputData[i] = ((float)drand48() - 0.5f) * 2 * amplitude;
}
return oboe::DataCallbackResult::Continue;
}
};
You can find examples of how to play sound using digital synthesis and pre-recorded audio in the code samples.
Declare your callback somewhere that it won't get deleted while you are using it.
MyCallback myCallback;
Supply this callback class to the builder:
builder.setDataCallback(&myCallback);
Declare a shared pointer for the stream. Make sure it is declared in an appropriate scope (e.g.the member of a managing class). Avoid declaring it as a global.
std::shared_ptr<oboe::AudioStream> mStream;
Open the stream:
oboe::Result result = builder.openStream(mStream);
Check the result to make sure the stream was opened successfully. Oboe has a convenience method for converting its types into human-readable strings called oboe::convertToText
:
if (result != oboe::Result::OK) {
LOGE("Failed to create stream. Error: %s", oboe::convertToText(result));
}
Note that this sample code uses the logging macros from here.
Check the properties of the created stream. If you did not specify a channelCount, sampleRate, or format then you need to
query the stream to see what you got. The format property will dictate the audioData
type in the AudioStreamDataCallback::onAudioReady
callback. If you did specify any of those three properties then you will get what you requested.
oboe::AudioFormat format = mStream->getFormat();
LOGI("AudioStream format is %s", oboe::convertToText(format));
Now start the stream.
mStream->requestStart();
At this point you should start receiving callbacks.
To stop receiving callbacks call
mStream->requestStop();
It is important to close your stream when you're not using it to avoid hogging audio resources which other apps could use. This is particularly true when using SharingMode::Exclusive
because you might prevent other apps from obtaining a low latency audio stream.
Streams should be explicitly closed when the app is no longer playing audio.
mStream->close();
close()
is a blocking call which also stops the stream.
For apps which only play or record audio when they are in the foreground this is usually done when Activity.onPause()
is called.
After closing, in order to change the configuration of the stream, simply call openStream
again. The existing stream is deleted and a new stream is built and
populates the mStream
variable.
// Modify the builder with some additional properties at runtime.
builder.setDeviceId(MY_DEVICE_ID);
// Re-open the stream with some additional config
// The old AudioStream is automatically deleted
builder.openStream(mStream);
The following class is a complete implementation of an audio player that renders a sine wave.
#include <oboe/Oboe.h>
#include <math.h>
class OboeSinePlayer: public oboe::AudioStreamDataCallback {
public:
virtual ~OboeSinePlayer() = default;
// Call this from Activity onResume()
int32_t startAudio() {
std::lock_guard<std::mutex> lock(mLock);
oboe::AudioStreamBuilder builder;
// The builder set methods can be chained for convenience.
Result result = builder.setSharingMode(oboe::SharingMode::Exclusive)
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setChannelCount(kChannelCount)
->setSampleRate(kSampleRate)
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium);
->setFormat(oboe::AudioFormat::Float)
->setDataCallback(this)
->openStream(mStream);
if (result != Result::OK) return (int32_t) result;
// Typically, start the stream after querying some stream information, as well as some input from the user
result = outStream->requestStart();
return (int32_t) result;
}
// Call this from Activity onPause()
void stopAudio() {
// Stop, close and delete in case not already closed.
std::lock_guard<std::mutex> lock(mLock);
if (mStream) {
mStream->stop();
mStream->close();
mStream.reset();
}
}
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override {
float *floatData = (float *) audioData;
for (int i = 0; i < numFrames; ++i) {
float sampleValue = kAmplitude * sinf(mPhase);
for (int j = 0; j < kChannelCount; j++) {
floatData[i * kChannelCount + j] = sampleValue;
}
mPhase += mPhaseIncrement;
if (mPhase >= kTwoPi) mPhase -= kTwoPi;
}
return oboe::DataCallbackResult::Continue;
}
private:
std::mutex mLock;
std::shared_ptr<oboe::AudioStream> mStream;
// Stream params
static int constexpr kChannelCount = 2;
static int constexpr kSampleRate = 48000;
// Wave params, these could be instance variables in order to modify at runtime
static float constexpr kAmplitude = 0.5f;
static float constexpr kFrequency = 440;
static float constexpr kPI = M_PI;
static float constexpr kTwoPi = kPI * 2;
static double constexpr mPhaseIncrement = kFrequency * kTwoPi / (double) kSampleRate;
// Keeps track of where the wave is
float mPhase = 0.0;
};
Note that this implementation computes sine values at run-time for simplicity, rather than pre-computing them. Additionally, best practice is to implement a separate data callback class, rather than managing the stream and defining its data callback in the same class.
For more examples on how to use Oboe look in the samples folder.
One of the goals of the Oboe library is to provide low latency audio streams on the widest range of hardware configurations. When a stream is opened using AAudio, the optimal rate will be chosen unless the app requests a specific rate. The framesPerBurst is also provided by AAudio.
But OpenSL ES cannot determine those values. So applications should query them using Java and then pass them to Oboe. They will be used for OpenSL ES streams on older devices.
Here's a code sample showing how to set these default values.
MainActivity.java
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
AudioManager myAudioMgr = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
int defaultSampleRate = Integer.parseInt(sampleRateStr);
String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);
native_setDefaultStreamValues(defaultSampleRate, defaultFramesPerBurst);
}
jni-bridge.cpp
JNIEXPORT void JNICALL
Java_com_google_sample_oboe_hellooboe_MainActivity_native_1setDefaultStreamValues(JNIEnv *env,
jclass type,
jint sampleRate,
jint framesPerBurst) {
oboe::DefaultStreamValues::SampleRate = (int32_t) sampleRate;
oboe::DefaultStreamValues::FramesPerBurst = (int32_t) framesPerBurst;
}
Note that the values from Java are for built-in audio devices. Peripheral devices, such as Bluetooth may need larger framesPerBurst.