c++qtaudio

QAudioSink started and reads data, but doesn't play sound


I am attempting to use the QAudioSink type from Qt 6.7.2 to play procedurally-generated sound clips. I've written a short piece of code that attempts to play white noise. It seems like the QAudioSink object is indeed reading data and in the active state, but I don't hear anything played through my speakers.

Here's the code:

#include <QCoreApplication>
#include <QAudioSink>
#include <QIODevice>
#include <QtTypes>
#include <QFile>
#include <iostream>
#include <cstdlib>

/* Sample rate; chosen Arbitrarily and Capriciously. */
const int kSampleRate = 11025;

/* Class that holds a data buffer of random values, serving as a white
 * noise source.
 */
class NoiseWave: public QIODevice {
public:
    /* Fills a buffer with pseudorandom values. */
    NoiseWave() {
        for (int i = 0; i < kBufferSize; i++) {
            buffer[i] = std::rand() % 256;
        }
    }

protected:
    /* Hand back the data in the buffer. */
    qint64 readData(char* data, qint64 maxSize) override {
        std::cout << "Requesting " << maxSize << " bytes." << std::endl;
        if (maxSize > kBufferSize) maxSize = kBufferSize;

        std::cout << "Giving back " << maxSize << std::endl;
        std::memcpy(data, buffer, maxSize);

        return maxSize;
    }

    /* Only readable, not writable */
    qint64 writeData(const char*, qint64) override {
        return -1;
    }

private:
    /* Buffer holding raw data. */
    static const int kBufferSize = 10000;
    quint8 buffer[kBufferSize];
};

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    /* Set the audio format. */
    QAudioFormat format;
    format.setSampleRate(kSampleRate);
    format.setChannelCount(1);
    format.setSampleFormat(QAudioFormat::UInt8);

    /* Get an audio sink we can write to. */
    auto* audio = new QAudioSink(format, nullptr);

    /* Make the noise pattern to repeat. */
    auto* wave  = new NoiseWave();
    wave->open(QIODevice::ReadOnly);

    /* Start the sound. */
    audio->start(wave);

    /* Print basic stats. */
    std::cout << "Audio state is now  " << audio->state() << std::endl;
    std::cout << "Audio error code is " << audio->error() << std::endl;
    return a.exec();
}

Here is the output from the program. (I've added the bits in square brackets to indicate the passage of time; that's not in the actual program output.)

Audio state is now  0
Audio error code is 0
Requesting 16384 bytes.
Giving back 10000

[ ... time passes ... ]

Requesting 16384 bytes.
Giving back 10000

[ ... time passes ... ]

Requesting 16384 bytes.
Giving back 10000

[ ... time passes ... ]

Requesting 16384 bytes.
Giving back 10000

From what I'm seeing here it looks like the audio device is in active mode (QtAudio::ActiveState has enum value 0) and there is no error. It's also clear that the NoiseWave::readData function is being invoked repeatedly and at about the rate I would expect given the sampling rate. However, there's no audio playing.

This code runs cleanly under valgrind, so I don't think the issue is a memory error.

Can anyone provide guidance as to why I'm not hearing anything?

Update, Aug 2: It seems like changing the constructor call magically fixes things. Replacing this line:

auto* audio = new QAudioSink(format, nullptr);

with this one:

auto* audio = new QAudioSink(QMediaDevices::defaultAudioOutput(), 
                             format, nullptr);

suddenly fixes everything. However, I'm not sure why this is the case. The documentation for the two-argument QAudioSink constructor says the following:

QAudioSink::QAudioSink(const QAudioFormat &format = QAudioFormat(), QObject *parent = nullptr)

Construct a new audio output and attach it to parent. The default audio output device is used with the output format parameters.

Given this, it's unclear to me why explicitly specifying that the audio device is QMediaDevices::defaultAudioOutput() would have any effect. The documentation for QMediaDevices::defaultAudioOutput() says that it "[r]eturns the default audio output device." Why would specifying this have any effect?


Solution

  • To answer your edit. It seems to be a bug either in the documentation or the code; choose one.

    Code snippet from QAudioSink (qaudiosink.cpp Qt6.7.2):

    /*!
        Construct a new audio output and attach it to \a parent.
        The default audio output device is used with the output
        \a format parameters.
    */
    QAudioSink::QAudioSink(const QAudioFormat &format, QObject *parent)
        : QAudioSink({}, format, parent) // <<<<<< Bug here
    {
    }
    
    /*!
        Construct a new audio output and attach it to \a parent.
        The device referenced by \a audioDevice is used with the output
        \a format parameters.
    */
    QAudioSink::QAudioSink(const QAudioDevice &audioDevice, const QAudioFormat &format, QObject *parent):
        QObject(parent)
    {
        d = QPlatformMediaIntegration::instance()->mediaDevices()->audioOutputDevice(format, audioDevice, parent);
        if (d)
            connect(d, &QPlatformAudioSink::stateChanged, this, [this](QAudio::State state) {
                // if the signal has been emitted from another thread,
                // the state may be already changed by main one
                if (state == d->state())
                    emit stateChanged(state);
            });
        else
            qWarning() << ("No audio device detected");
    }
    

    Despite describing in the documentation "The default audio output device is used with the output format parameters" the code calls the default constructor of the QAudioDevice class which is a "null QAudioDevice object".

    P.S.: You could argue the default audio output device is the null QAudioDevice, since the "default" constructor constructs a null object. But I'm being a devils advocate here. ;)

    Edit1: It seems my analysis was not deep enough as shown in the comments in the original question. QAudioSinks constructor calls QPlatformMediaIntegration::instance()->mediaDevices()->audioOutputDevice(format, audioDevice, parent). There if a null QAudioDevice is passed, a valid one is selected like that qplatformmediadevices.cpp:

    QAudioDevice info = deviceInfo;
    if (info.isNull())
        info = audioOutputs().value(0);
    

    While the implementation of QMediaDevices::defaultAudioOutput() looks like that:

    QAudioDevice QMediaDevices::defaultAudioOutput()
    {
        const auto outputs = audioOutputs();
        if (outputs.isEmpty())
            return {};
        for (const auto &info : outputs)
            if (info.isDefault())
                return info;
        return outputs.value(0);
    }
    

    So it seems the code is buggy and it should be info = QMediaDevices::defaultAudioOutput();.