-
Notifications
You must be signed in to change notification settings - Fork 618
Using ADPF for High‐Quality Audio Performance
The Android Dynamic Performance Framework (ADPF) allows your app to give performance hints to the Android system. For real-time audio, this is a powerful tool for preventing audio glitches (underruns or "xruns"). When the system knows your audio callback needs more CPU power, it can boost CPU frequencies and schedule your thread more favorably to meet its deadline. 🚀
The AdpfWrapper.h class in Oboe provides a simple, high-level interface to the ADPF performance_hint.h NDK API, making it easy to integrate into your Oboe app.
The core idea is to measure the duration of each audio callback and report it to the system. This allows ADPF to learn your app's workload and provide the right amount of CPU resources. Here’s a step-by-step guide.
In your oboe::AudioStreamCallback implementation, add an AdpfWrapper member variable.
#include "common/AdpfWrapper.h" // Path to AdpfWrapper.h in your project
class MyAudioEngine : public oboe::AudioStreamCallback {
public:
// ... other methods
private:
// ... other members
oboe::AdpfWrapper mAdpfWrapper;
};The best place to initialize the ADPF session is when the audio stream becomes active. A great place to do this is in onAudioStreamStarted.
You need to provide two key pieces of information to the open() method:
-
Thread ID: The ID of the real-time audio callback thread. You can get this with a call to
gettid(). - Target Duration: The deadline for your callback, in nanoseconds. This is the period of your callback, which can be calculated from the stream's sample rate and frames per callback.
void MyAudioEngine::onAudioStreamStarted(oboe::AudioStream *oboeStream) {
// It's recommended to get the tid in the callback thread itself,
// but getting it here in onAudioStreamStarted also works.
pid_t threadId = gettid();
// Calculate the target duration (deadline) for each callback.
int32_t framesPerCallback = oboeStream->getFramesPerCallback();
int32_t sampleRate = oboeStream->getSampleRate();
int64_t targetDurationNanos = (int64_t)(1e9 * framesPerCallback / sampleRate);
// Open the ADPF session for this thread.
mAdpfWrapper.open(threadId, targetDurationNanos);
}This is the most important step. In your main audio processing callback (onAudioReady), you must tell the AdpfWrapper when your work begins and ends.
- Call
onBeginCallback()at the very beginning of the function. - Call
onEndCallback()at the very end of the function.
The wrapper will automatically measure the time between these two calls and report it to ADPF.
oboe::DataCallbackResult MyAudioEngine::onAudioReady(
oboe::AudioStream *oboeStream,
void *audioData,
int32_t numFrames) {
mAdpfWrapper.onBeginCallback();
// === YOUR AUDIO PROCESSING LOGIC GOES HERE ===
// renderMySynth(audioData, numFrames);
// =============================================
// The durationScaler is normally 1.0. See advanced usage for other values.
mAdpfWrapper.onEndCallback(1.0);
return oboe::DataCallbackResult::Continue;
}Below is the code for AdpfWrapper.onEndCallback(). Notice how reportActualDuration() is called for the callback. This is what lets ADPF and the system know about how long your workload actually took.
void AdpfWrapper::onEndCallback(double durationScaler) {
if (isOpen()) {
int64_t endCallbackNanos = oboe::AudioClock::getNanoseconds();
int64_t actualDurationNanos = endCallbackNanos - mBeginCallbackNanos;
int64_t scaledDurationNanos = static_cast<int64_t>(actualDurationNanos * durationScaler);
reportActualDuration(scaledDurationNanos);
// When the workload is non-zero, update the conversion factor from workload
// units to nanoseconds duration.
if (mPreviousWorkload > 0) {
mNanosPerWorkloadUnit = ((double) scaledDurationNanos) / mPreviousWorkload;
}
}
}To release system resources, close the ADPF session when your stream is no longer active. A good place for this is in onAudioStreamStopped.
void MyAudioEngine::onAudioStreamStopped(oboe::AudioStream *oboeStream) {
mAdpfWrapper.close();
}While the basic usage covers most cases, the AdpfWrapper provides more tools to handle complex scenarios and achieve even better performance.
Sometimes you know the workload is about to increase before it actually happens (e.g., the user just added a CPU-intensive effect). Waiting for the next onEndCallback to report this might be too late and could cause a glitch.
You can proactively report an expected workload using reportActualDuration(). This gives the system a heads-up to ramp up CPU performance now.
void MyAudioEngine::addReverbEffect() {
mReverbEnabled = true;
// This effect will add ~5ms to our callback duration.
// Let's tell ADPF immediately so it can ramp up the CPU.
int64_t expectedExtraNanos = 5 * 1000 * 1000;
// We can base the new workload on the previous one.
// Note: You'd need to add a method to get the last duration.
int64_t currentWorkloadNanos = mAdpfWrapper.getLastActualDuration();
mAdpfWrapper.reportActualDuration(currentWorkloadNanos + expectedExtraNanos);
}Proactively reporting workload changes by calling reportActualDuration() with an longer duration above can lead to mistakes if you are not careful.
On Android 16 (API level 36) and higher, you can use more specific hints to describe upcoming changes to the workload. This is more expressive than just reporting a longer duration. For audio, you will almost always be hinting for the CPU.
-
notifyWorkloadIncrease(): Call this right before a sustained increase in workload (e.g., adding more voices to a synthesizer). -
notifyWorkloadReset(): Call this when the workload is about to change unpredictably (e.g., loading a completely new song or patch). This tells ADPF to discard its historical data and learn the new workload from scratch. -
notifyWorkloadSpike(): Call this before a one-time, unusually expensive operation inside your callback that should not be considered part of the normal workload (e.g., initializing a large effect the first time it's used).
// Example: Adding a new synth voice
mAdpfWrapper.notifyWorkloadIncrease(true /* cpu */, false /* gpu */, "add_voice");
mSynth.addVoice();
// Example: Loading a totally new sound preset
mAdpfWrapper.notifyWorkloadReset(true /* cpu */, false /* gpu */, "load_preset");
mSynth.loadPreset("monster_bass");Thus, for Android 36+, we prefer that apps do not call mAdpfWrapper.reportActualDuration() directly. They should just use mAdpfWrapper.onEndCallback() and the new notifyWorkloadIncrease()/notifyWorkloadReset()/notifyWorkloadSpike() APIs.
The durationScaler parameter in onEndCallback(double durationScaler) allows you to report a duration that is a multiple of the actual measured time. This can be used to add a "safety margin."
-
durationScaler = 1.0: Standard usage. Reports the actual measured duration. -
durationScaler > 1.0: Reports a longer duration than measured. This makes the system respond more aggressively to workload increases. A value like1.1or1.2can help smooth out performance if your workload fluctuates slightly. -
durationScaler < 1.0: Reports a shorter duration. This is less common but could be used if you have a temporary, non-repeating workload spike that you want to downplay.
- Apps Using Oboe or AAudio
- Tech Notes
- Buffer Size, Capacity and Bursts
- Glitches and Latency
- How to Avoid Crashes
- Bluetooth Audio
- Using Audio Effects with Oboe
- Disconnected Streams
- Assert in releaseBuffer()
- Crash during callback after routing
- Using ADPF for High‐Quality Audio Performance
- Using FullDuplexStream for Synchronized IO
- Using Offload Playback for Power‐Saving
- OboeTester Instructions
- Quirks and Bugs
- Developer Notes