qtqaudioinput

QAudioOutput underrun issue on Realtime Play from Microphone with QAudioInput


Sometimes I am getting "underrun occured" from ALSA lib and that means the audioouput is not getting the values on time to play. Alsa then repeats the old buffer values on the speaker.

How can I avoid underruns on QAudioOuput? I am using Qt5.9.1 and ARM Based CPU running on Debian 8.

I tried to change the buffersize:

audioOutput->setBufferSize(144000);
qDebug()<<"buffersize "<<audioOutput->bufferSize()<<" period size" . 
<<audioOutput->periodSize();

I get: buffersize 144000 period size 0

and after audiOutput->start() I get: buffersize 19200 period size 3840

Here is what I am doing:

audioOutput->setBufferSize(144000);
qDebug()<<"buffersize "<<audioOutput->bufferSize()<<" period size" . 
<<audioOutput->periodSize();
m_audioInput = audioInput->start();
m_audioOutput = audioOutput->start();

qDebug()<<"buffersize "<<audioOutput->bufferSize()<<" period size"< 
<<audioOutput->periodSize();
connect(m_audioInput, SIGNAL(readyRead()), SLOT(readBufferSlot()));

Once audio data gets recorded I write to the QIODevice m_audioOutput the values from QIODevice m_audioInput.

So I think I have a timing issue sometimes and the audio interval for both is 1000ms before and after start(). Why cant I increase the buffer size? And how can I avoid underrun?


Solution

  • Based on my experience with QAudioOutput, it's buffer is intended just to keep real-time playing, you can't for example drop 1 minute of sound directly to the QIODevice expecting it gets buffered and played sequentially, but it do not means that you can't buffer sound, just means that you need to do it by yourself.

    I made the following example in "C-Style" to make an all-in-one solution, it buffers 1000 milliseconds (1 second) of the input before play it.

    The event loop needs to be available to process the Qt SIGNALs.

    In my tests, 1 second buffering is fairly enough to avoid under runs.

    #include <QtCore>
    #include <QtMultimedia>
    
    #define MAX_BUFFERED_TIME 1000
    
    static inline int timeToSize(int ms, const QAudioFormat &format)
    {
        return ((format.channelCount() * (format.sampleSize() / 8) * format.sampleRate()) * ms / 1000);
    }
    
    struct AudioContext
    {
        QAudioInput *m_audio_input;
        QIODevice *m_input_device;
    
        QAudioOutput *m_audio_output;
        QIODevice *m_output_device;
    
        QByteArray m_buffer;
    
        QAudioDeviceInfo m_input_device_info;
        QAudioDeviceInfo m_output_device_info;
        QAudioFormat m_format;
    
        int m_time_to_buffer;
        int m_max_size_to_buffer;
    
        int m_size_to_buffer;
    
        bool m_buffer_requested = true; //Needed
        bool m_play_called = false;
    };
    
    void play(AudioContext *ctx)
    {
        //Set that last async call was triggered
        ctx->m_play_called = false;
    
        if (ctx->m_buffer.isEmpty())
        {
            //If data is empty set that nothing should be played
            //until the buffer has at least the minimum buffered size already set
            ctx->m_buffer_requested = true;
            return;
        }
        else if (ctx->m_buffer.size() < ctx->m_size_to_buffer)
        {
            //If buffer doesn't contains enough data,
            //check if exists a already flag telling that the buffer comes
            //from a empty state and should not play anything until have the minimum data size
            if (ctx->m_buffer_requested)
                return;
        }
        else
        {
            //Buffer is ready and data can be played
            ctx->m_buffer_requested = false;
        }
    
        int readlen = ctx->m_audio_output->periodSize();
    
        int chunks = ctx->m_audio_output->bytesFree() / readlen;
    
        //Play data while it's available in the output device
        while (chunks)
        {
            //Get chunk from the buffer
            QByteArray samples = ctx->m_buffer.mid(0, readlen);
            int len = samples.size();
            ctx->m_buffer.remove(0, len);
    
            //Write data to the output device after the volume was applied
            if (len)
            {
                ctx->m_output_device->write(samples);
            }
    
            //If chunk is smaller than the output chunk size, exit loop
            if (len != readlen)
                break;
    
            //Decrease the available number of chunks
            chunks--;
        }
    }
    
    void preplay(AudioContext *ctx)
    {
        //Verify if exists a pending call to play function
        //If not, call the play function async
        if (!ctx->m_play_called)
        {
            ctx->m_play_called = true;
            QTimer::singleShot(0, [=]{play(ctx);});
        }
    }
    
    void init(AudioContext *ctx)
    {
        /***** INITIALIZE INPUT *****/
    
        //Check if format is supported by the choosen input device
        if (!ctx->m_input_device_info.isFormatSupported(ctx->m_format))
        {
            qDebug() << "Format not supported by the input device";
            return;
        }
    
        //Initialize the audio input device
        ctx->m_audio_input = new QAudioInput(ctx->m_input_device_info, ctx->m_format, qApp);
    
        ctx->m_input_device = ctx->m_audio_input->start();
    
        if (!ctx->m_input_device)
        {
            qDebug() << "Failed to open input audio device";
            return;
        }
    
        //Call the readyReadPrivate function when data are available in the input device
        QObject::connect(ctx->m_input_device, &QIODevice::readyRead, [=]{
            //Read sound samples from input device to buffer
            ctx->m_buffer.append(ctx->m_input_device->readAll());
            preplay(ctx);
        });
    
        /***** INITIALIZE INPUT *****/
    
        /***** INITIALIZE OUTPUT *****/
    
        //Check if format is supported by the choosen output device
        if (!ctx->m_output_device_info.isFormatSupported(ctx->m_format))
        {
            qDebug() << "Format not supported by the output device";
            return;
        }
    
        int internal_buffer_size;
    
        //Adjust internal buffer size
        if (ctx->m_format.sampleRate() >= 44100)
            internal_buffer_size = (1024 * 10) * ctx->m_format.channelCount();
        else if (ctx->m_format.sampleRate() >= 24000)
            internal_buffer_size = (1024 * 6) * ctx->m_format.channelCount();
        else
            internal_buffer_size = (1024 * 4) * ctx->m_format.channelCount();
    
        //Initialize the audio output device
        ctx->m_audio_output = new QAudioOutput(ctx->m_output_device_info, ctx->m_format, qApp);
        //Increase the buffer size to enable higher sample rates
        ctx->m_audio_output->setBufferSize(internal_buffer_size);
    
        //Compute the size in bytes to be buffered based on the current format
        ctx->m_size_to_buffer = int(timeToSize(ctx->m_time_to_buffer, ctx->m_format));
        //Define a highest size that the buffer are allowed to have in the given time
        //This value is used to discard too old buffered data
        ctx->m_max_size_to_buffer = ctx->m_size_to_buffer + int(timeToSize(MAX_BUFFERED_TIME, ctx->m_format));
    
        ctx->m_output_device = ctx->m_audio_output->start();
    
        if (!ctx->m_output_device)
        {
            qDebug() << "Failed to open output audio device";
            return;
        }
    
        //Timer that helps to keep playing data while it's available on the internal buffer
        QTimer *timer_play = new QTimer(qApp);
        timer_play->setTimerType(Qt::PreciseTimer);
        QObject::connect(timer_play, &QTimer::timeout, [=]{
            preplay(ctx);
        });
        timer_play->start(10);
    
        //Timer that checks for too old data in the buffer
        QTimer *timer_verifier = new QTimer(qApp);
        QObject::connect(timer_verifier, &QTimer::timeout, [=]{
            if (ctx->m_buffer.size() >= ctx->m_max_size_to_buffer)
                ctx->m_buffer.clear();
        });
        timer_verifier->start(qMax(ctx->m_time_to_buffer, 10));
    
        /***** INITIALIZE OUTPUT *****/
    
        qDebug() << "Playing...";
    }
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
    
        AudioContext ctx;
    
        QAudioFormat format;
        format.setCodec("audio/pcm");
        format.setSampleRate(44100);
        format.setChannelCount(1);
        format.setSampleSize(16);
        format.setByteOrder(QAudioFormat::LittleEndian);
        format.setSampleType(QAudioFormat::SignedInt);
    
        ctx.m_format = format;
    
        ctx.m_input_device_info = QAudioDeviceInfo::defaultInputDevice();
        ctx.m_output_device_info = QAudioDeviceInfo::defaultOutputDevice();
    
        ctx.m_time_to_buffer = 1000;
    
        init(&ctx);
    
        return a.exec();
    }