Skip to content

Using Offload Playback for Power‐Saving

Robert Wu edited this page Nov 7, 2025 · 2 revisions

Oboe provides access to Android's PCM offload capabilities, a powerful feature for saving battery life during long-running audio playback. When a stream is "offloaded," the audio decoding and playback are delegated to dedicated hardware, allowing the main application processor to sleep. 😴🔋

This is ideal for music players, podcast apps, or any application that plays back long-form audio content. Offload playback can also support hardware audio effects if an offload version is provided by the device.


How It Works

Offload playback works by sending large chunks of audio data to a dedicated hardware path for playback. Oboe abstracts the underlying AAudio implementation to make this easy to use.

The key components are:

  • PerformanceMode::PowerSavingOffloaded: This tells Oboe you want to use an offloaded stream.
  • Data Transfer: You can provide audio data via a callback (AudioStreamPartialDataCallback is recommended) or by using blocking write().
  • End of Stream Notification: You can optionally signal to the framework when you have finished writing data for a track. This is required to receive a presentation callback when all written data has been played.
  • Presentation Callback: A callback notifies you when the hardware has finished playing all the buffered data, allowing you to chain tracks or release resources.

Step-by-Step Implementation

Here’s how to set up and use an offloaded audio stream.

1. Configure the Stream for Offload

When building your stream, the most important step is to set the performance mode to PowerSavingOffloaded. You must also provide the exact format that the hardware supports (e.g., sample rate, channel mask, and format). No data conversion is performed in offload mode.

#include <oboe/Oboe.h>

// Create your stream builder
oboe::AudioStreamBuilder builder;
builder.setDirection(oboe::Direction::Output)
       ->setPerformanceMode(oboe::PerformanceMode::PowerSavingOffloaded)
       ->setSharingMode(oboe::SharingMode::Exclusive) // Offload is always exclusive
       ->setSampleRate(44100)
       ->setChannelMask(oboe::ChannelMask::Stereo)
       ->setFormat(oboe::AudioFormat::I16); // Or I24, I32, Float, etc.

If a stream cannot be opened with these settings, the device may not support offload playback for the specified audio format. You can query for offload support using the AudioManager APIs in the Android SDK. For example, by checking AudioDeviceInfo.isOffloadPlaybackSupported(AudioFormat).

2. Provide Audio Data

You have two primary ways to send data to the stream: using a data callback or using blocking writes.

Option A: Using a Data Callback (Recommended)

Using a data callback is the most efficient way to handle data transfer. AudioStreamPartialDataCallback is highly recommended for offload streams because it allows your app to provide a variable amount of data with each callback. For example, if the stream requests 100ms of data but you only have 50ms remaining in your source, you can provide just 50ms. If no callback is set, the callback size defaults to the burst size, which is typically small.

1. Implement AudioStreamPartialDataCallback:

class OffloadStreamCallback : public oboe::AudioStreamPartialDataCallback {
public:
    int32_t onPartialAudioReady(
            oboe::AudioStream *audioStream,
            void *audioData,
            int32_t numFrames) override {
        
        // Your logic to render or copy audio data into `audioData`.
        int32_t framesWritten = renderAudio(audioData, numFrames);

        if (isEndOfTrack()) {
            // Let the framework know we are done writing data for this track.
            audioStream->setOffloadEndOfStream();
        }
        
        return framesWritten; // Return the number of frames you actually wrote.
    }
private:
    // Your rendering/data source logic here...
    int32_t renderAudio(void *audioData, int32_t numFrames);
    bool isEndOfTrack();
};

2. Set the Callback on the Builder:

auto myCallback = std::make_shared<OffloadStreamCallback>();
builder.setPartialDataCallback(myCallback);

Note: If you must use AudioStreamDataCallback, it is recommended to request a very small callback size (setFramesPerCallback()) to avoid having to provide a huge buffer of data on every callback.

Option B: Using Blocking Write

Alternatively, you can push data to the stream using a blocking write(). This is simpler but requires you to manage the data flow on a dedicated thread.

void playTrack(oboe::AudioStream *stream, MyDataSource *dataSource) {
    // The timeout should be long enough to handle the amount of data being written.
    // If you are writing 1 second of audio, the timeout should be at least 1 second.
    const int32_t sampleRate = stream->getSampleRate();
    
    while (auto audioBuffer = dataSource->getNextBlock()) {
        // Calculate a timeout based on the number of frames being written.
        int64_t timeoutNanos = (audioBuffer->numFrames() * 1e9) / sampleRate;
        auto result = stream->write(audioBuffer->data(), audioBuffer->numFrames(), timeoutNanos);
        if (!result) {
            // Handle error
            break;
        }
    }
    
    // This is only needed if you need to know when the track has finished playing.
    stream->setOffloadEndOfStream();
}

3. Handle End of Presentation

When setOffloadEndOfStream() has been called and the hardware has finished playing all the data you've written, a presentation callback will be fired. This is your signal to start playing the next track, update the UI, or clean up resources.

1. Implement AudioStreamPresentationCallback:

class MyPresentationCallback : public oboe::AudioStreamPresentationCallback {
public:
    void onPresentationEnded(oboe::AudioStream* audioStream) override {
        // All data has been played.
        // Ready to play the next song, close the stream, etc.
        LOGI("Track finished playing!");
    }
};

2. Set the Callback on the Builder:

auto presentationCallback = std::make_shared<MyPresentationCallback>();
builder.setPresentationCallback(presentationCallback);

4. Open and Start the Stream

Finally, open and start your stream.

std.shared_ptr<oboe::AudioStream> oboeStream;
oboe::Result result = builder.openStream(oboeStream);

if (result != oboe::Result::OK) {
    LOGE("Failed to open offload stream. Error: %s", oboe::convertToText(result));
    return;
}

result = oboeStream->start();
// Handle result...

Compressed Audio Offload

In addition to PCM data, Oboe supports offloading compressed audio formats such as AAC. With compressed offload, the encoded data is sent directly to the hardware for decoding, offering significant power savings.

Using compressed offload is very similar to PCM offload, with one key difference: the audio format must be set to a compressed format.

1. Set a Compressed Format

When building the stream, set the format to one of the compressed formats defined in oboe::AudioFormat, such as oboe::AudioFormat::AAC_LC.

oboe::AudioStreamBuilder builder;
builder.setDirection(oboe::Direction::Output)
       ->setPerformanceMode(oboe::PerformanceMode::PowerSavingOffloaded)
       ->setFormat(oboe::AudioFormat::AAC_LC) // Set the compressed format
       ->setSampleRate(48000) // Should match the encoded data
       ->setChannelMask(oboe::ChannelMask::Stereo);  // Should match the encoded data

Note: Compressed audio offload was added in API level 36. The stream creation will fail on older Android versions.

2. Write Compressed Data

You must use blocking write() to send the compressed audio data. Data callbacks are not used for compressed streams. The data you write must be the raw compressed data frames (e.g., AAC frames) without any media container (e.g., MP4).

void writeCompressedTrack(oboe::AudioStream *stream, DataSource *dataSource) {
    int64_t timeoutNanos = 100 * 1e6; // 100ms
    
    // dataSource should provide raw AAC frames
    while (auto aacFrame = dataSource->getNextFrame()) {
        auto result = stream->write(aacFrame->data(), aacFrame->numFrames(), timeoutNanos);
        if (!result) {
            // Handle error
            break;
        }
    }
    
    // setOffloadEndOfStream is only needed if apps care when their written data has all played.
    stream->setOffloadEndOfStream();
}

The rest of the process, including using setOffloadEndOfStream() and AudioStreamPresentationCallback, is the same as with PCM offload.


Advanced Controls

Oboe provides a few extra functions for fine-tuning offload streams:

  • setOffloadDelayPadding(int32_t delayInFrames, int32_t paddingInFrames): Sets the delay and padding of the decoded audio stream. This is only used for compressed offload to achieve gapless playback.
  • getOffloadDelay(): Returns the server-side buffer delay in frames. Only used for compressed offload.
  • getOffloadPadding(): Returns the number of padding frames at the end of a stream. Only used for compressed offload.
  • flushFromFrame(FlushFromAccuracy accuracy, int64_t positionInFrames): This function is used to discard buffered data in response to a user action, such as changing effects or playlists. It allows you to specify a frame position from which to flush, ensuring that playback can resume smoothly from a new point without playing stale, buffered audio. It is not a substitute for seeking in the data source itself.

Example

For a complete, working example of PCM offload, see the PowerPlay sample in the Oboe repository. You can also test offload functionality with the OboeTester app.

Clone this wiki locally