Over the past several Qt releases, culminating in Qt 6.12, we have reworked the audio input and output layer of Qt Multimedia to make QAudioSink and QAudioSource behave consistently across platforms.
Historically, each backend implementation managed the underlying audio stream in its own way, including how data was transferred between the audio thread and the public audio API. This affected the sink’s and source’s control methods and their buffer properties, such as bufferSize() and bytesFree(). Some backends, such as PulseAudio on Linux, relied on native stream buffers, with control methods operating directly on the stream. Others, such as CoreAudio on Apple platforms, maintained their own ring buffers, with control methods affecting both the ring buffer and the underlying stream.
These differences led to inconsistent behavior across platforms, occasional audio stuttering, and an increasing maintenance burden. Furthermore, advanced use cases requiring direct access to the audio thread without intermediate buffering were not supported by the Qt Multimedia API.
Let’s break down the changes we’ve made to address these problems.
One data transfer model for all backends
QAudioSink and QAudioSource now sit on a platform-independent layer: a wait-free ring buffer on top of a platform audio stream that drives the native audio thread the same way on every backend. This rolled out gradually: PulseAudio, Windows/WASAPI and CoreAudio first, then PipeWire and Android in 6.10, and the new QNX io-snd and OpenHarmony backends in 6.12.
The properties bufferSize()/bufferFrameCount() and bytesFree()/framesFree() now always describe this ring buffer. You can set the buffer size without guessing what the backend will do; the default is 250 ms. The QIODevice-based API, in both push and pull mode, is meant to be used from the sink or source instance’s thread and only touches the ring buffer. Audio running states now reflect the ring buffer workload: whenever the ring buffer of a sink runs dry, or the ring buffer of a source fills up, the state changes to IdleState, then goes back to ActiveState once the buffer has data or room again, respectively.
Control methods behaviour unification
start(), stop(), reset(), suspend() and resume() now each have a clear role in managing the backend stream and the ring buffer:
start() creates the ring buffer and the backend audio stream, then starts the stream.
stop() detaches the stream from the instance. A sink returns immediately, while the detached stream plays out what is left in the ring buffer. A source keeps its QIODevice alive, so you can still read the remaining data.
reset() immediately stops the stream, discards the ring buffer contents.
suspend()/resume() pause and resume the audio stream. While suspended, the stream doesn’t access the ring buffer.
This unified behaviour makes audio stream management more predictable and reduces the need for platform-specific workarounds.
Qt 6.11: the audio callback API
Some use cases need low latency and don’t need a buffer to absorb delays on the application side. Typical examples include real-time audio synthesis, processing, and monitoring.
For these use cases, Qt 6.11 introduced a callback-based overload of start(). Your callable is invoked directly on the audio thread with a span of samples. The sample type is float, int16_t, int32_t or uint8_t and must match the QAudioFormat. In this mode there is no ring buffer or any other intermediate transfer: the backend decides the size of each span, setBufferSize() has no effect, and the sink or source is never in IdleState.
The important catch: the callback runs on a soft-real-time audio thread. It must never block, which means no mutexes, no blocking I/O and no regular heap allocation. Once you install a callback, making your audio handling lock-free and allocation-free is your job. Clang’s RealtimeSanitizer is a great tool for checking that.
A minimal example of an audio sink callback generating a sine wave looks like this:
QAudioSink sink(format); // format: Float, 48 kHz, mono
sink.start([phase = 0.f](QSpan<float> out) mutable {
for (float &s : out) {
s = 0.2f * std::sin(phase);
phase += 0.05f;
}
});
More advanced examples, such as a low-latency audio mixer, deserve a separate blog post. With the right prompt, AI can also generate such examples.
Providing an API for convenient interaction with the audio thread could be a valuable future improvement. We already have such functionality internally, but it is not yet exposed as a public API. In Qt 6.11, QSoundEffect was reimplemented on top of the callback API and an internal real-time audio engine with a real-time-safe allocator, improving its reliability and reducing latency. What kind of helpers would you find useful when working with audio callbacks? We'd love to hear your feedback!
Qt 6.12: controlling the native period size
In some cases, especially when optimizing latency with the callback API, you may want to tune the buffering of the native audio backend itself. Qt 6.12 introduces setNativePeriodFrameCount() for QAudioSink and QAudioSource. The method allows you to configure the native audio period size, which also influences how many frames each callback receives. A larger period increases latency but reduces the risk of buffer underruns.
Audio backend updates
Since Qt 6.10, Qt Multimedia on Linux has provided a PipeWire backend, which is selected by default when a PipeWire server is running. PulseAudio remains available when the system runs only a PulseAudio server or when explicitly selected using the QT_AUDIO_BACKEND environment variable. We recommend using the PipeWire backend wherever possible.
Also in Qt 6.10, Android switched from OpenSL ES to the modern AAudio API. This change eliminated several corner-case bugs and simplified the implementation, bringing it in line with the common cross-platform audio architecture.
6.12 brings two more backends:
On QNX, io-snd backend became available and default, allowing to run audio on QNX8 devices.
Audio implementation on OHOS became avialable, so users, still utilizing audio in Qt5, can transition to Qt 6.12.
Data flow diagram
The diagram below summarizes the technical aspects discussed in this post. It illustrates the data flow between the public audio IO API, the underlying audio backend, and the hardware, highlighting the roles of the key components. Understanding this architecture will help you choose the most suitable audio IO mode for your application's requirements.
[image: fa80e14c-52fb-4129-b78a-d3167a0e2aaa.png]
Manual audio testing
The audio output and audio source examples cover all three modes (pull, push and callback) and let you choose sample format, channel count and sample rate. 6.12 adds a native period size selector, and the audio source example can now record to a WAV file. The examples have two goals: show a clear flow for using the audio IO API, and let anyone run manual tests that check the important use cases on their hardware. The manual test plan lives in the Qt sources: qtmultimedia/tests/manual/ManualTestingPlan.md.
Summary
Together, all these changes give Qt Multimedia a more consistent and maintainable foundation for audio input and output, while providing applications with greater control over latency, buffering, and real-time audio processing.
We're interested in your feedback! If you have an interesting use case to share, insights into the Audio IO API or the manual testing infrastructure, or suggestions for further improvements, please let us know.