Skip to content

Using ADPF for High‐Quality Audio Performance

Robert Wu edited this page Oct 29, 2025 · 4 revisions

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.


Basic Usage

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.

1. Add AdpfWrapper to Your Callback Class

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;
};

2. Open a Session When the Stream Starts

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);
}

3. Report Workload in onAudioReady

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;
        }
    }
}

4. Close the Session When the Stream Stops

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();
}

Advanced Usage and Best Practices

While the basic usage covers most cases, the AdpfWrapper provides more tools to handle complex scenarios and achieve even better performance.

Proactively Reporting Workload Changes

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);
}

Hinting at Future Workload Changes (API level 36+)

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.

Using a durationScaler

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 like 1.1 or 1.2 can 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.

Clone this wiki locally