Skip to content

Using FullDuplexStream for Synchronized IO

Robert Wu edited this page Oct 10, 2025 · 1 revision

The FullDuplexStream class is a powerful helper in the Oboe library designed to simplify the creation of synchronized, full-duplex (simultaneous input and output) audio applications. It handles the complex logic of buffering and timing, allowing you to focus on your audio processing. 🎤🎧

This class is ideal for applications like live effects processors, synthesizers with audio input, or low-latency monitoring.


How It Works

Instead of managing two separate streams and callbacks, FullDuplexStream acts as a single, specialized callback that you attach to your output stream. Internally, it performs blocking reads on the input stream to gather audio.

When audio data is available from the input and the output is ready for more data, it calls a single, unified function—onBothStreamsReady()—where you perform your audio processing. This abstracts away the tricky synchronization logic.


Step-by-Step Implementation

Here’s how to set up and use FullDuplexStream in your project.

1. Inherit from FullDuplexStream

First, create your own audio engine class that inherits from oboe::FullDuplexStream. You must implement the pure virtual function onBothStreamsReady().

#include <oboe/Oboe.h>

class LiveEffectEngine : public oboe::FullDuplexStream {
public:
    LiveEffectEngine() = default;

    // This is where all your audio processing will happen!
    oboe::DataCallbackResult onBothStreamsReady(
            const void *inputData,
            int numInputFrames,
            void *outputData,
            int numOutputFrames) override;
    
private:
    // Your stream objects
    std::shared_ptr<oboe::AudioStream> mInputStream;
    std::shared_ptr<oboe::AudioStream> mOutputStream;
};

2. Implement onBothStreamsReady()

This function is the heart of your audio processing. It provides you with buffers for both input and output. numInputFrames may be different from numOutputFrames, so your logic should handle that.

A simple "pass-through" effect would look like this:

oboe::DataCallbackResult LiveEffectEngine::onBothStreamsReady(
        const void *inputData,
        int numInputFrames,
        void *outputData,
        int numOutputFrames) {

    // For this simple example, we'll just copy the input to the output.
    // We will pretend the channel counts are the same.
    int framesToProcess = std::min(numInputFrames, numOutputFrames);
    int numInputChannels = getInputStream()->getChannelCount();
    int bytesPerSample = getInputStream()->getBytesPerSample();

    memcpy(outputData, inputData, framesToProcess * numInputChannels * bytesPerSample);

    return oboe::DataCallbackResult::Continue;
}

3. Build and Configure Your Streams

Setting up the streams requires following specific recommendations for the best performance.

  • Build the output stream first to query its native properties.
  • Match the input stream's properties (sample rate) to the output stream.
  • Set the input stream's buffer capacity to be double the output stream's capacity. This is crucial for stability.
  • Set your LiveEffectEngine instance as the callback for the output stream only.
void LiveEffectEngine::setupStreams() {
    oboe::AudioStreamBuilder builder;
    builder.setPerformanceMode(oboe::PerformanceMode::LowLatency)
           ->setSharingMode(oboe::SharingMode::Exclusive)
           ->setDirection(oboe::Direction::Output)
           ->setCallback(this); // IMPORTANT: Set this class as the callback

    // Open the output stream
    oboe::Result result = builder.openStream(mOutputStream);
    // Handle result...

    // Now, configure and open the input stream
    builder.setDirection(oboe::Direction::Input)
           ->setSampleRate(mOutputStream->getSampleRate()) // Match sample rate
           ->setChannelCount(1); // Or your desired input channel count

    // IMPORTANT: Set buffer capacity
    builder.setBufferCapacityInFrames(mOutputStream->getBufferCapacityInFrames() * 2);

    result = builder.openStream(mInputStream);
    // Handle result...

    // Connect the streams to the FullDuplexStream helper
    setSharedInputStream(mInputStream);
    setSharedOutputStream(mOutputStream);
}

4. Manage the Stream Lifecycle

Use the start() and stop() methods of your FullDuplexStream subclass to manage both streams at once. You are still responsible for closing the streams you opened.

void LiveEffectEngine::start() {
    FullDuplexStream::start(); // This starts both input and output
}

void LiveEffectEngine::stop() {
    FullDuplexStream::stop(); // This stops both input and output
}

void LiveEffectEngine::closeStreams() {
    if (mOutputStream) {
        mOutputStream->close();
        mOutputStream.reset();
    }
    if (mInputStream) {
        mInputStream->close();
        mInputStream.reset();
    }
}

Error Handling

Error handling with FullDuplexStream has a specific requirement. You should register an onError* callback on the output stream.

When an error occurs (like a device being disconnected), Oboe will automatically stop and close the output stream. However, you are responsible for stopping and closing the input stream yourself from within the error callback.

class MyErrorCallback : public oboe::AudioStreamErrorCallback {
public:
    MyErrorCallback(LiveEffectEngine &parent) : mParent(parent) {}
    void onError(oboe::AudioStream*, oboe::Result) override {
        // The output stream is already stopped.
        // We must stop and close the input stream.
        mParent.getInputStream()->stop();
        mParent.getInputStream()->close();
    }
private:
    LiveEffectEngine &mParent;
};

    
// When building the output stream:
// MyErrorCallback myErrorCallback(*this);
// builder.setErrorCallback(&myErrorCallback);

Advanced Tuning

The FullDuplexStream class provides methods to fine-tune its internal buffering, allowing you to trade between lower latency and higher resilience against glitches.

  • setNumInputBurstsCushion(int32_t numBursts): Sets a "cushion" of audio bursts to keep in the input buffer. 0 is for the lowest latency but is more likely to glitch. 1 provides more stability.
  • setMinimumFramesBeforeRead(int32_t numFrames): Sets the threshold for how many frames must be available in the input buffer before a read is attempted.

For an excellent, complete example of FullDuplexStream in action, see the LiveEffect sample in the Oboe repository.

Clone this wiki locally