Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqnxsndaudiosink.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6
7#include <QtCore/qspan.h>
8#include <QtCore/qthread.h>
9#include <QLoggingCategory>
10
11#include <algorithm>
12
14
15Q_STATIC_LOGGING_CATEGORY(lcQnxSndOutput, "qt.multimedia.qnxsnd.output")
16
17using QtMultimediaPrivate::QPlatformAudioSinkStream;
18using QtMultimediaPrivate::QPlatformAudioIOStream;
19using QtMultimediaPrivate::runAudioCallback;
20using QtMultimediaPrivate::withTemporaryBuffer;
21
22QQnxSndAudioSinkStream::QQnxSndAudioSinkStream(QAudioDevice device, const QAudioFormat &format,
23 std::optional<qsizetype> ringbufferSize,
24 QQnxSndAudioSink *parent, float volume,
25 std::optional<NativePeriodFrames> nativePeriodFrames,
26 AudioEndpointRole /*role*/)
28 std::move(device),
29 format,
32 volume,
33 },
35{
36}
37
38QQnxSndAudioSinkStream::~QQnxSndAudioSinkStream()
39{
40 // Defensive: the front class's stop() should already have torn the
41 // worker down, but if a partial-construction unwind or a future
42 // refactor drops the shared_ptr without calling stop(), make sure
43 // we do not leak the worker thread or the SALSA handle.
44 joinWorkerThread();
45 closePcmDevice();
46}
47
49{
50 return openPcmDevice();
51}
52
53int QQnxSndAudioSinkStream::recoverFromXrun(int err)
54{
55 return QnxSndHelpers::recoverFromXrun(m_handle, err);
56}
57
58bool QQnxSndAudioSinkStream::openPcmDevice()
59{
61 .direction = SND_PCM_STREAM_PLAYBACK,
62 .deviceId = m_audioDevice.id(),
63 .format = m_format,
64 .category = lcQnxSndOutput(),
65 .streamLabel = "Playback",
66 .periodCountEnvVar = "QT_QNXSND_OUTPUT_PERIODS",
67 .periodFrames = m_nativePeriodFrames
68 ? std::optional<uint32_t>{ qToUnderlying(*m_nativePeriodFrames) }
69 : std::nullopt,
70 };
72 if (!r)
73 return false;
74
75 m_handle = r.handle;
76 m_periodFrames = r.periodFrames;
77 m_nativeFormat = r.nativeFormat;
78 return true;
79}
80
81void QQnxSndAudioSinkStream::closePcmDevice()
82{
83 // The worker thread uses m_handle directly; closing it while the worker
84 // is still running would create a use-after-free window. Every caller
85 // path joins the worker first; assert the invariant.
86 Q_ASSERT(!m_workerThread || !m_workerThread->isRunning());
87 if (m_handle) {
88 // Negative returns here typically mean the device was already lost
89 // (-ENODEV) or invalidated by a prior drop (-EBADFD); log so the
90 // case isn't silent in CI but proceed with cleanup either way.
91 if (const int err = snd_pcm_close(m_handle); err < 0) {
92 qCWarning(lcQnxSndOutput) << "snd_pcm_close failed:"
93 << snd_strerror(err) << "(" << err << ")";
94 }
95 m_handle = nullptr;
96 }
97}
98
99bool QQnxSndAudioSinkStream::start(QIODevice *ioDevice)
100{
101 setQIODevice(ioDevice);
102 createQIODeviceConnections(ioDevice);
103 pullFromQIODevice();
104
105 startWorker(StreamType::Ringbuffer);
106 return true;
107}
108
110{
111 QIODevice *ioDevice = createRingbufferWriterDevice();
112 setQIODevice(ioDevice);
113 createQIODeviceConnections(ioDevice);
114
115 startWorker(StreamType::Ringbuffer);
116 return ioDevice;
117}
118
119bool QQnxSndAudioSinkStream::start(AudioCallback audioCallback)
120{
121 m_audioCallback = std::move(audioCallback);
122 startWorker(StreamType::Callback);
123 return true;
124}
125
127{
128 // Gating the worker on m_suspended lets the hardware buffer drain
129 // naturally. Calling snd_pcm_drain or snd_pcm_drop here would race with
130 // the worker thread blocked in snd_pcm_writei and force it into xrun
131 // recovery, occasionally tripping handleSndPcmError -> StoppedState.
132 m_suspended.store(true, std::memory_order_release);
133 m_wakePipe.wake(); // break the worker out of poll() so it stops writing
134}
135
137{
138 // After resume the worker's next snd_pcm_writei may return -EPIPE if the
139 // hardware underran while we were suspended; processOnePeriod handles
140 // that via recoverFromXrun, so no explicit prepare/start is needed here.
141 m_suspended.store(false, std::memory_order_release);
142 m_wakePipe.wake(); // wake the worker out of its suspended poll-wait
143}
144
145void QQnxSndAudioSinkStream::stop(ShutdownPolicy shutdownPolicy)
146{
147 // Draining is meaningless on a suspended stream — downgrade so we
148 // can shut down synchronously instead of waiting on a drain that
149 // can never complete.
150 if (m_suspended.load(std::memory_order_acquire))
151 shutdownPolicy = ShutdownPolicy::DiscardRingbuffer;
152
153 m_shutdownPolicy.store(shutdownPolicy, std::memory_order_release);
154 requestStop();
155
156 // Sever the source-QIODevice signals on every shutdown path. Otherwise
157 // pullFromQIODevice() can fire mid-teardown and refill the ringbuffer
158 // we are tearing down. Matches the pipewire backend's pattern.
159 disconnectQIODeviceConnections();
160
161 switch (shutdownPolicy) {
162 case ShutdownPolicy::DiscardRingbuffer:
163 // joinWorkerThread() issues snd_pcm_drop itself to break the
164 // worker out of any blocking snd_pcm_writei.
165 joinWorkerThread();
166 closePcmDevice();
167 m_parent.store(nullptr, std::memory_order_release);
168 return;
169 case ShutdownPolicy::DrainRingbuffer:
170 // Clear m_parent now, synchronously on the app thread, before this call
171 // returns. The drain completion runs asynchronously and keeps the stream
172 // alive via the captured shared_ptr, but the front class (m_parent) may
173 // be destroyed as soon as stop() returns. Nulling it here guarantees any
174 // worker-originated callback fired during the drain reads nullptr rather
175 // than a dangling pointer (the completion lambda re-stores it for
176 // defence-in-depth).
177 m_parent.store(nullptr, std::memory_order_release);
178 m_ringbufferDrained.callOnActivated([self = shared_from_this()]() mutable {
179 self->joinWorkerThread();
180 self->closePcmDevice();
181 self->m_parent.store(nullptr, std::memory_order_release);
182 self = {};
183 });
184 return;
185 }
186}
187
189{
190 // Read once into a local: the app thread may null m_parent between the
191 // check and the call. The front class joins the worker before clearing
192 // m_parent, so a non-null read here is guaranteed to point at a live
193 // object for the duration of this call.
194 if (auto *parent = m_parent.load(std::memory_order_acquire))
195 parent->updateStreamIdle(streamIsIdle);
196}
197
198void QQnxSndAudioSinkStream::startWorker(StreamType streamType)
199{
200 // Create the wake pipe before the worker starts so suspend()/resume()/stop()
201 // can signal it; on failure, stop still breaks the worker via snd_pcm_drop.
202 if (!m_wakePipe.open())
203 qCWarning(lcQnxSndOutput) << "wake pipe creation failed; worker wakeups degraded";
204
205 m_workerThread.reset(QThread::create([this, streamType] {
206 runProcessLoop(streamType);
207 }));
208 m_workerThread->setObjectName(u"QQnxSndAudioSinkStream");
209 // The worker raises itself to a SCHED_FIFO priority in the 11-23 band at the
210 // top of runProcessLoop (setWorkerRealtimePriority): above normal app threads
211 // (10) so a busy application cannot preempt the audio feed, but strictly below
212 // io-snd's own data/helper threads (24/25) — raising into that band wedges the
213 // driver (the worker ends up REPLY-blocked on io-snd after preempting it).
214 m_workerThread->start();
215}
216
217void QQnxSndAudioSinkStream::joinWorkerThread()
218{
219 requestStop();
220 // Break the worker out of poll(); it then sees the stop request and exits.
221 m_wakePipe.wake();
222 if (m_handle) {
223 // Discard whatever is still queued so playback stops promptly and any
224 // in-flight snd_pcm_writei returns. Failures (e.g. -ENODEV on hot-unplug)
225 // are worth surfacing but do not block teardown.
226 if (const int err = snd_pcm_drop(m_handle); err < 0) {
227 qCWarning(lcQnxSndOutput) << "snd_pcm_drop failed:"
228 << snd_strerror(err) << "(" << err << ")";
229 }
230 }
231 if (m_workerThread) {
232 m_workerThread->wait();
233 m_workerThread = {};
234 }
235 m_wakePipe.close();
236}
237
238void QQnxSndAudioSinkStream::runProcessLoop(StreamType streamType)
239{
241
242 // Prime the device with the first period, then start playback explicitly
243 // (the qwindowsaudiosink pattern): openConfiguredPcm only prepares the
244 // stream, so we control exactly when it starts and avoid a startup underrun.
245 if (!processOnePeriod(streamType) && isStopRequested(std::memory_order_acquire))
246 return;
247 if (const int err = QnxSndHelpers::startPcm(m_handle)) {
248 handleSndPcmError(err);
249 return;
250 }
251
252 for (;;) {
253 if (isStopRequested(std::memory_order_acquire)) {
254 switch (m_shutdownPolicy.load(std::memory_order_acquire)) {
255 case ShutdownPolicy::DiscardRingbuffer:
256 return;
257 case ShutdownPolicy::DrainRingbuffer: {
258 const bool bufferDrained = visitRingbuffer([](const auto &ringbuffer) {
259 return ringbuffer.used() == 0;
260 });
261 // While suspended we cannot drain (processOnePeriod would
262 // block on snd_pcm_writei against a stopped device), so
263 // signal the front class regardless and let the callback
264 // tear the stream down.
265 if (bufferDrained || m_suspended.load(std::memory_order_acquire)) {
266 // We do not call snd_pcm_drain here: in some post-suspend
267 // / post-xrun states it wedges io-snd internally. The
268 // hardware will keep playing trailing samples already in
269 // the ALSA buffer until snd_pcm_close is called.
270 m_ringbufferDrained.set();
271 return;
272 }
273 break;
274 }
275 }
276 }
277
278 if (m_suspended.load(std::memory_order_acquire)) {
279 // Block on the wake pipe only — never touch the device while
280 // suspended (snd_pcm_writei against a stopped stream wedges io-snd).
281 // resume()/stop() call wake() to release us.
282 m_wakePipe.waitForWake();
283 continue;
284 }
285
286 // Wait until io-snd can accept a period, or a stop/suspend wakes us.
287 switch (QnxSndHelpers::pollPcm(m_handle, m_wakePipe)) {
289 continue; // re-evaluate stop/suspend at the loop top
291 handleSndPcmError();
292 return;
294 break;
295 }
296
297 if (!processOnePeriod(streamType)) {
298 if (!isStopRequested(std::memory_order_acquire))
299 return;
300 }
301 }
302}
303
304bool QQnxSndAudioSinkStream::processOnePeriod(StreamType streamType)
305{
306 // Ask io-snd how many frames it can accept right now and fill exactly that
307 // (capped at one period so the stack scratch stays bounded). In steady state
308 // poll() wakes us with avail_min == one period free; on the initial prefill
309 // (called before snd_pcm_start, while the stream is PREPARED) avail_update
310 // reports the whole buffer free, so we still prime exactly one period.
311 // NB: snd_pcm_avail() (the hwsync variant) returns -ENOTSUP on QNX SALSA;
312 // snd_pcm_avail_update() reflects the just-polled state and is supported.
313 const snd_pcm_sframes_t avail = snd_pcm_avail_update(m_handle);
314 if (avail < 0) {
315 if (isStopRequested(std::memory_order_acquire))
316 return false;
317 // Underrun/suspend surfaced via avail; recover and let the loop re-poll.
318 if (const int err = recoverFromXrun(static_cast<int>(avail)); err < 0) {
319 handleSndPcmError(err);
320 return false;
321 }
322 return true;
323 }
324 if (avail == 0)
325 return true; // poll() raced ahead of free space; re-poll rather than spin.
326
327 const snd_pcm_uframes_t framesToWrite =
328 std::min<snd_pcm_uframes_t>(static_cast<snd_pcm_uframes_t>(avail), m_periodFrames);
329 // The host buffer handed to io-snd is in the device-native format, so size it
330 // in native bytes; process()/runAudioCallback convert from the application
331 // format (which may differ, e.g. Float -> 24-bit) while filling it.
332 const size_t nativeBytesPerFrame =
333 m_format.channelCount() * QAudioHelperInternal::bytesPerSample(m_nativeFormat);
334 const size_t hostBytes = static_cast<size_t>(framesToWrite) * nativeBytesPerFrame;
335
336 return withTemporaryBuffer(hostBytes, [&](QSpan<std::byte> hostBufferSpan) -> bool {
337 switch (streamType) {
338 case StreamType::Ringbuffer:
339 QPlatformAudioSinkStream::process(hostBufferSpan, framesToWrite, m_nativeFormat);
340 break;
342 // StreamType::Callback is only ever set by start(AudioCallback), which
343 // assigns m_audioCallback before starting the worker, so it is non-null.
344 Q_ASSERT(m_audioCallback);
345 runAudioCallback(*m_audioCallback, hostBufferSpan, m_format, volume(), m_nativeFormat);
346 break;
347 }
348
349 snd_pcm_uframes_t framesPending = framesToWrite;
350 auto *cursor = hostBufferSpan.data();
351 while (framesPending > 0) {
352 if (isStopRequested(std::memory_order_acquire))
353 return false;
354
355 snd_pcm_sframes_t written = snd_pcm_writei(m_handle, cursor, framesPending);
356 if (written == -EAGAIN || written == 0) {
357 // Bounded by avail above, so a short/again write here is rare;
358 // wait for space rather than spinning. The loop-top stop check
359 // handles a wake caused by teardown.
360 if (QnxSndHelpers::pollPcm(m_handle, m_wakePipe)
362 handleSndPcmError();
363 return false;
364 }
365 continue;
366 }
367 if (written < 0) {
368 if (const int err = recoverFromXrun(static_cast<int>(written)); err < 0) {
369 handleSndPcmError(err);
370 return false;
371 }
372 continue;
373 }
374 framesPending -= static_cast<snd_pcm_uframes_t>(written);
375 cursor += written * nativeBytesPerFrame;
376 }
377 return true;
378 });
379}
380
381void QQnxSndAudioSinkStream::handleSndPcmError(int err)
382{
383 requestStop();
384 // Re-read m_parent inside the lambda at fire-time. Both teardown paths clear
385 // m_parent synchronously on the app thread before the front class can be
386 // freed: Discard via joinWorkerThread() (~QQnxSndAudioSink / stop()), Drain
387 // at the top of stop()'s Drain branch. So the load sees a live pointer or
388 // nullptr — never a dangling one.
389 invokeOnAppThread([self = shared_from_this(), err] {
390 qCWarning(lcQnxSndOutput) << "audio output error, stopping stream:"
391 << (err ? snd_strerror(err) : "I/O error");
392 if (auto *parent = self->m_parent.load(std::memory_order_acquire))
393 self->handleIOError(parent);
394 });
395}
396
397///////////////////////////////////////////////////////////////////////////////////////////////////
398
399QQnxSndAudioSink::QQnxSndAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
401{
402}
403
404QQnxSndAudioSink::~QQnxSndAudioSink()
405{
406 // The base destructor calls stop() with Drain, which schedules
407 // teardown on the app event loop and returns before the worker has
408 // joined. That leaves a window where the worker can post lambdas
409 // capturing the front-class address, which then fire after we're
410 // freed. Force Discard here so joinWorkerThread() runs synchronously
411 // and m_parent is nulled before *this is destroyed; queued lambdas
412 // then see nullptr at fire-time and no-op.
413 if (m_stream) {
414 m_stream->stop(ShutdownPolicy::DiscardRingbuffer);
415 m_stream = {};
416 }
417}
418
419QT_END_NAMESPACE
QQnxSndAudioSink(QAudioDevice, const QAudioFormat &, QObject *parent)
void setWorkerRealtimePriority(const QLoggingCategory &category)
int startPcm(snd_pcm_t *handle)
int recoverFromXrun(snd_pcm_t *handle, int err)
PollOutcome pollPcm(snd_pcm_t *handle, const WakePipe &wake)
PcmOpenResult openConfiguredPcm(const PcmOpenConfig &config)
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)
bool start(AudioCallback)
QtMultimediaPrivate::QPlatformAudioSinkStream::AudioCallback AudioCallback
void updateStreamIdle(bool) override
void stop(ShutdownPolicy)
QQnxSndAudioSinkStream(QAudioDevice, const QAudioFormat &, std::optional< qsizetype > ringbufferSize, QQnxSndAudioSink *parent, float volume, std::optional< NativePeriodFrames > nativePeriodFrames, AudioEndpointRole)