Search code examples
androidaudioandroid-ndkoboe

Underrun in Oboe/AAudio playback stream


I'm working on an Android app dealing with a device which is basically a USB microphone. I need to read the input data and process it. Sometimes, I need to send data the device (4 shorts * the number of channels which is usually 2) and this data does not depend on the input.

I'm using Oboe, and all the phones I use for testing use AAudio underneath.

The reading part works, but when I try to write data to the output stream, I get the following warning in logcat and nothing is written to the output:

 W/AudioTrack: releaseBuffer() track 0x78e80a0400 disabled due to previous underrun, restarting

Here's my callback:

oboe::DataCallbackResult
OboeEngine::onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) {

    // check if there's data to write, agcData is a buffer previously allocated
    // and h2iaudio::getAgc() returns true if data's available
    if (h2iaudio::getAgc(this->agcData)) {

        // padding the buffer
        short* padPos = this->agcData+ 4 * playStream->getChannelCount();
        memset(padPos, 0,
            static_cast<size_t>((numFrames - 4) * playStream->getBytesPerFrame()));

        // write the data
        oboe::ResultWithValue<int32_t> result = 
            this->playStream->write(this->agcData, numFrames, 1);

        if (result != oboe::Result::OK){
            LOGE("Failed to create stream. Error: %s",
                oboe::convertToText(result.error()));
            return oboe::DataCallbackResult::Stop;
        }
    }else{
        // if there's nothing to write, write silence
        memset(this->agcData, 0,
            static_cast<size_t>(numFrames * playStream->getBytesPerFrame()));
    }

    // data processing here
    h2iaudio::processData(static_cast<short*>(audioData),
                          static_cast<size_t>(numFrames * oboeStream->getChannelCount()),
                          oboeStream->getSampleRate());

    return oboe::DataCallbackResult::Continue;
}

//...

oboe::AudioStreamBuilder *OboeEngine::setupRecordingStreamParameters(
  oboe::AudioStreamBuilder *builder) {

    builder->setCallback(this)
        ->setDeviceId(this->recordingDeviceId)
        ->setDirection(oboe::Direction::Input)
        ->setSampleRate(this->sampleRate)
        ->setChannelCount(this->inputChannelCount)
        ->setFramesPerCallback(1024);
    return setupCommonStreamParameters(builder);
}

As seen in setupRecordingStreamParameters, I'm registering the callback to the input stream. In all the Oboe examples, the callback is registered on the output stream, and the reading is blocking. Does this have an importance? If not, how many frames do I need to write to the stream to avoid underruns?

EDIT In the meantime, I found the source of the underruns. The output stream was not reading the same amount of frames as the input stream (which in hindsight seems logical), so writing the amount of frames given by playStream->getFramesPerBurst() fix my issue. Here's my new callback:

oboe::DataCallbackResult
OboeEngine::onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) {
    int framesToWrite = playStream->getFramesPerBurst();

    memset(agcData, 0, static_cast<size_t>(framesToWrite * 
           this->playStream->getChannelCount()));
    h2iaudio::getAgc(agcData);

    oboe::ResultWithValue<int32_t> result = 
        this->playStream->write(agcData, framesToWrite, 0);

    if (result != oboe::Result::OK) {
        LOGE("Failed to write AGC data. Error: %s", 
             oboe::convertToText(result.error()));
    }

    // data processing here
    h2iaudio::processData(static_cast<short*>(audioData),
                          static_cast<size_t>(numFrames * oboeStream->getChannelCount()),
                          oboeStream->getSampleRate());

    return oboe::DataCallbackResult::Continue;
}

It works this way, I'll change which stream has the callback attached if I notice any performance issue, for now I'll keep it this way.


Solution

  • Sometimes, I need to send data the device

    You always need to write data to the output. Generally you need to write at least numFrames, maybe more. If you don't have any valid data to send then write zeros. Warning: in your else block you are calling memset() but not writing to the stream.

    ->setFramesPerCallback(1024);

    Do you need 1024 specifically? Is that for an FFT? If not then AAudio can optimize the callbacks better if the FramesPerCallback is not specified.

    In all the Oboe examples, the callback is registered on the output stream, and the reading is blocking. Does this have an importance?

    Actually the read is NON-blocking. Whatever stream does not have the callback should be non-blocking. Use a timeoutNanos=0.

    It is important to use the output stream for the callback if you want low latency. That is because the output stream can only provide low latency mode with callbacks and not with direct write()s. But an input stream can provide low latency with both callback and with read()s.

    Once the streams are stabilized then you can read or write the same number of frames in each callback. But before it is stable, you may need to to read or write extra frames.

    With an output callback you should drain the input for a while so that it is running close to empty.

    With an input callback you should fill the output for a while so that it is running close to full.

    write(this->agcData, numFrames, 1);

    Your 1 nanosecond timeout is very small. But Oboe will still block. You should use a timeoutNanos of 0 for non-blocking mode.