Most QT resources seem to want me to use higher-level classes like
QMediaPlayer
or QSoundEffect
to point at extant files or possibly a
QAudioBuffer
which is already filled with a QByteArray
. There’s really
not much out there about synthesizing audio and playing it straight
through a QAudioDevice
.
The latter may operate in push mode or pull mode; the docs will tell you as
much. What may be less obvious is how those modes are chosen. It’s simply
your choice of overloaded method:
QIODevice QAudioOutput::start(); // push mode
void QAudioOutput::start(QIODevice *dev); // pull mode
Looking over the pre-made classes that implement QIODevice
, I notice that
they love to tell you when data is ready to read but not when data may be
written. And in both modes, the QAudioOutput
is doing the reading and
your application needs to know, somehow, when it should write.
So, I implemented my own QIODevice
around my own buffer type. The class
actually holds a pair of buffers, one being written by the generator thread
and one being read by QAudioOutput
. When both are finished, the buffers are
swapped. Whenever the output calls read
, I format data from the app’s
buffer into bytes, directly into the output pointer.
And actually, nobody calls write. The generator can be connected directly to
the app buffer, which provides its own append()
method that consumes
(potentially many) snd_sample_t
s.
There are a pair of signals and slots each side uses to communicate about
their shared buffer:
class SndIO : public QIODevice {
Q_OBJECT
...
public slots:
void writeComplete(SndBuf *buf);
signals:
void readyWrite(SndBuf *buf);
void emptied();
void underrun();
}
class SndThread : public QObject {
Q_OBJECT
...
public slots:
void start(SndBuf *buf);
signals:
void finished(SndBuf *buf);
}
The main thread then connects SndIO::readyWrite
to SndThread::start
and
SndThread::finished
to SndIO::writeComplete
. The SndBuf itself isn’t used
simultaneously by multiple threads, because the IO won’t touch it until the
Thread has signaled that it has finished filling it.
This produces a four-state system, starting with “both buffers empty” before
the main thread wires the objects and initiates the first fill on the IO.
1. empty/empty
2. empty/filling
3. draining/filling
4. draining/full
When state 2 emits finished, state 3 begins; emission of emptied there returns
to state 2, otherwise, finished happens again and state 4 is entered. From
state 4, the only move is back to state 3 when the emptied signal is emitted.
After the initialization period (once state 3 has been entered for the first
time), it’s possible for state 2 to receive another read from the
QAudioOutput
; this emits the underrun signal without changing state.
This is a actually a slightly-more-complex version of the producer/consumer
problem. Instead of a single producer and consumer directly communicating,
there is an intervening QIODevice
carrying data across threads, consuming
the generator’s output, then marshaling and producing it for the output to
consume.
I’m also effectively using signals and slots to let the event loop block the
producer when it gets too far ahead. When that happens, no “produce more”
signal is delivered until the output can catch up.
All that said, this setup compiles but I haven’t finished wrapping a test
program around it yet.