-
Notifications
You must be signed in to change notification settings - Fork 618
Using Offload Playback for Power‐Saving
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.
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 (
AudioStreamPartialDataCallbackis recommended) or by using blockingwrite(). - 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.
Here’s how to set up and use an offloaded audio stream.
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).
You have two primary ways to send data to the stream: using a data callback or using blocking writes.
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.
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();
}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);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...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.
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 dataNote: Compressed audio offload was added in API level 36. The stream creation will fail on older Android versions.
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.
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.
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.
- 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