0
votes

Getting a message "Got a buffer underflow!" after each write in this simple program.

Beep.hpp:

#pragma once

#include <QTimer>
#include <QAudioOutput>

class Beep: public QObject
{
    Q_OBJECT

public:
    explicit Beep();
    virtual ~Beep();

    void onTimer();

private:
    QAudioOutput m_out;
    QIODevice *m_outDev;
    QTimer m_timer;
};

Beep.cpp:

#include "Beep.hpp"

int ms = 100;

const QAudioFormat defaultAudioFormat = []()
{
    QAudioFormat format;
    format.setSampleRate(8000);
    format.setChannelCount(1);
    format.setSampleSize(16);
    format.setCodec("audio/pcm");
    format.setByteOrder(QAudioFormat::LittleEndian);
    format.setSampleType(QAudioFormat::SignedInt);
    return format;
}();

Beep::Beep() :
        m_out(defaultAudioFormat),
        m_outDev()
{
    m_out.setBufferSize(16 * ms);
    m_outDev = m_out.start();

    QObject::connect(&m_timer, &QTimer::timeout, this, &Beep::onTimer);

    m_timer.setSingleShot(false);
    m_timer.start(ms);
}

Beep::~Beep()
{
}

void Beep::onTimer()
{
    std::vector<uint8_t> samples(16 * ms);
    m_outDev->write((char*) &samples.front(), samples.size());
}

main.cpp:

#include <QCoreApplication>

#include "Beep.hpp"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    Beep beep;
    return app.exec();
}

This test program is just writing buffers with zeros. With real data there are cracking sounds.

Writing more data or changing timings makes it worse. What's wrong with this code?

2

2 Answers

0
votes

Using a Timer is the wrong way to do it.

Use the notify() signal

void AudioManager::init_audio(AudioManager *mgr) {
    if (mgr->stream_id == -1) return;

    mgr->audio_format.setSampleRate(mgr->context->time_base.den);
    mgr->audio_format.setSampleSize(16);
    mgr->audio_format.setChannelCount(2);
    mgr->audio_format.setCodec("audio/pcm");
    mgr->audio_format.setSampleType(QAudioFormat::SignedInt);
    QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
    if (!info.isFormatSupported(mgr->audio_format)) {
        mgr->audio_format = info.nearestFormat(mgr->audio_format);
    }

    mgr->audio_out = new QAudioOutput(mgr->audio_format, nullptr);
    mgr->audio_out->setNotifyInterval(15);
    mgr->audio_out->setBufferSize(mgr->context->time_base.den * 4); // 1 second worth of stereo data

    connect(mgr->audio_out, SIGNAL(notify()), mgr, SLOT(audio_out_notify()));
    connect(mgr->audio_out, SIGNAL(stateChanged(QAudio::State)), mgr, SLOT(audio_out_state_changed(QAudio::State)));
    qreal volume_out = (qreal)parent->volume / 100.0f;
    mgr->audio_out->setVolume(volume_out);
    mgr->audio_out_device = mgr->audio_out->start();
}

This will be called when the audio playback requires more data

void AudioManager::audio_out_notify() {
    qDebug() << "Audio notify";
    check_audio_playback();
}

Most of the below code will be irrelevant but it is also called is audio has stopped playing.

void AudioManager::check_audio_playback() {
    if (stream_id == -1) return;

    pthread_mutex_lock(&audio_mutex);

    if (!audio_out->state() == QAudio::State::IdleState) {
        pthread_mutex_unlock(&audio_mutex);
        return;
    }

    if (parent->pts_start_time < 0.0) {
        if (parent->Video.stream_id == -1 && decode_pos > 65) { // start playback
            parent->pts_start_time = buffers[0].frame_time;
            parent->sys_start_time = (double)parent->timer.elapsed() / 1000.0;
            qDebug() << "Audio playback started";
        } else {
            pthread_mutex_unlock(&audio_mutex);
            return;
        }
    }

    if (playback_pos == decode_pos) {
        pthread_mutex_unlock(&audio_mutex);
        return;
    }



    AudioBuffer *buffer = nullptr;
    double current_sys_time = ((double)parent->timer.elapsed() / 1000.0) - parent->sys_start_time;

    bool bounds = false;
    int skipped = 0;

    while (!bounds) {
        if (playback_pos == decode_pos) bounds = true;
        else {
            AudioBuffer *temp_buffer = &buffers[playback_pos];
            double temp_time = temp_buffer->frame_time - parent->pts_start_time;

            if (temp_time < current_sys_time ) {
                if (buffer) {
                    buffer->used = false;
                    skipped++;
                }
                buffer = temp_buffer;
                playback_pos++; playback_pos %= MAX_AUD_BUFFERS;
            } else {
                bounds = true;
            }
        }
    }

    if (skipped > 0) qDebug("Skipped %d audio buffers on playback", skipped);

    if (buffer) {
        audio_out_device->write((const char *)buffer->data, buffer->buffer_size);
        buffer->used = false;
    }

    pthread_mutex_unlock(&audio_mutex);
}

The example on the Qt website wasn't that obvious http://qt.apidoc.info/5.1.1/qtmultimedia/audiooutput.html at first but when I put it in to test it wasn't too bad.

0
votes

The reason was that the source of audio data wasn't a "production-quality module" (it's a dummy testing class): the timer was drifting because its real interval was 10ms plus the processing time.

Other observations:

  • make QAudioOutput::setBufferSize() bigger
  • do QAudioInput::read() and QAudioOutput::write() in chunks with size that matches QAudioInput::periodSize() and QAudioOutput::periodSize()