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
qqnxsndaudiosource.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(lcQnxSndInput, "qt.multimedia.qnxsnd.input")
16
17using QtMultimediaPrivate::QPlatformAudioSourceStream;
18using QtMultimediaPrivate::QPlatformAudioIOStream;
19using QtMultimediaPrivate::runAudioCallback;
20using QtMultimediaPrivate::withTemporaryBuffer;
21
22QQnxSndAudioSourceStream::QQnxSndAudioSourceStream(QAudioDevice device, const QAudioFormat &format,
23 std::optional<qsizetype> ringbufferSize,
24 QQnxSndAudioSource *parent, float volume,
25 std::optional<NativePeriodFrames> nativePeriodFrames)
27 std::move(device),
28 format,
31 volume,
32 },
34{
35}
36
37QQnxSndAudioSourceStream::~QQnxSndAudioSourceStream()
38{
39 // Defensive: the front class's stop() should already have torn the
40 // worker down, but if a partial-construction unwind or a future
41 // refactor drops the shared_ptr without calling stop(), make sure
42 // we do not leak the worker thread or the SALSA handle.
43 joinWorkerThread();
44 closePcmDevice();
45}
46
48{
49 return openPcmDevice();
50}
51
52int QQnxSndAudioSourceStream::recoverFromXrun(int err)
53{
54 return QnxSndHelpers::recoverFromXrun(m_handle, err);
55}
56
57bool QQnxSndAudioSourceStream::openPcmDevice()
58{
60 .direction = SND_PCM_STREAM_CAPTURE,
61 .deviceId = m_audioDevice.id(),
62 .format = m_format,
63 .category = lcQnxSndInput(),
64 .streamLabel = "Capture",
65 .periodCountEnvVar = "QT_QNXSND_INPUT_PERIODS",
66 .periodFrames = m_nativePeriodFrames
67 ? std::optional<uint32_t>{ qToUnderlying(*m_nativePeriodFrames) }
68 : std::nullopt,
69 };
71 if (!r)
72 return false;
73
74 m_handle = r.handle;
75 m_periodFrames = r.periodFrames;
76 m_nativeFormat = r.nativeFormat;
77 return true;
78}
79
80void QQnxSndAudioSourceStream::closePcmDevice()
81{
82 // See QQnxSndAudioSinkStream::closePcmDevice — same m_handle-lifetime
83 // invariant on the capture side.
84 Q_ASSERT(!m_workerThread || !m_workerThread->isRunning());
85 if (m_handle) {
86 // See QQnxSndAudioSinkStream::closePcmDevice — log negative returns
87 // (typically -ENODEV after hot-unplug or -EBADFD after a prior drop)
88 // but proceed with cleanup.
89 if (const int err = snd_pcm_close(m_handle); err < 0) {
90 qCWarning(lcQnxSndInput) << "snd_pcm_close failed:"
91 << snd_strerror(err) << "(" << err << ")";
92 }
93 m_handle = nullptr;
94 }
95}
96
97bool QQnxSndAudioSourceStream::start(QIODevice *ioDevice)
98{
99 setQIODevice(ioDevice);
100 createQIODeviceConnections(ioDevice);
101 startWorker();
102 return true;
103}
104
106{
107 QIODevice *ioDevice = createRingbufferReaderDevice();
108 setQIODevice(ioDevice);
109 createQIODeviceConnections(ioDevice);
110
111 startWorker();
112 return ioDevice;
113}
114
115bool QQnxSndAudioSourceStream::start(AudioCallback audioCallback)
116{
117 m_audioCallback = std::move(audioCallback);
118 startWorker();
119 return true;
120}
121
123{
124 // See QQnxSndAudioSinkStream::suspend — touching the device here races
125 // with the worker thread blocked in snd_pcm_readi.
126 m_suspended.store(true, std::memory_order_release);
127 m_wakePipe.wake(); // break the worker out of poll() so it stops reading
128}
129
131{
132 // The first readi after resume may return -EPIPE if hardware overran
133 // while suspended; processOnePeriod recovers via snd_pcm_prepare.
134 m_suspended.store(false, std::memory_order_release);
135 m_wakePipe.wake(); // wake the worker out of its suspended poll-wait
136}
137
138void QQnxSndAudioSourceStream::stop(ShutdownPolicy shutdownPolicy)
139{
140 requestStop();
141 disconnectQIODeviceConnections();
142
143 // joinWorkerThread() issues snd_pcm_drop itself to break the worker
144 // out of any blocking snd_pcm_readi.
145 joinWorkerThread();
146 closePcmDevice();
147 m_parent.store(nullptr, std::memory_order_release);
148
149 finalizeQIODevice(shutdownPolicy);
150 if (shutdownPolicy == ShutdownPolicy::DiscardRingbuffer)
151 emptyRingbuffer();
152}
153
155{
156 // See QQnxSndAudioSinkStream::updateStreamIdle — read once into a local
157 // to avoid a TOCTOU with stop() nulling m_parent.
158 if (auto *parent = m_parent.load(std::memory_order_acquire))
159 parent->updateStreamIdle(streamIsIdle);
160}
161
162void QQnxSndAudioSourceStream::startWorker()
163{
164 // See QQnxSndAudioSinkStream::startWorker — the wake pipe lets
165 // suspend()/resume()/stop() break the worker out of poll().
166 if (!m_wakePipe.open())
167 qCWarning(lcQnxSndInput) << "wake pipe creation failed; worker wakeups degraded";
168
169 m_workerThread.reset(QThread::create([this] {
170 runProcessLoop();
171 }));
172 m_workerThread->setObjectName(u"QQnxSndAudioSourceStream");
173 // The worker raises itself into the SCHED_FIFO 11-23 band at the top of
174 // runProcessLoop (setWorkerRealtimePriority) — see QQnxSndAudioSinkStream::startWorker.
175 m_workerThread->start();
176}
177
178void QQnxSndAudioSourceStream::joinWorkerThread()
179{
180 requestStop();
181 // Break the worker out of poll(); it then sees the stop request and exits.
182 m_wakePipe.wake();
183 if (m_handle) {
184 // See QQnxSndAudioSinkStream::joinWorkerThread — drop discards queued
185 // capture and lets any in-flight snd_pcm_readi return; surface failures.
186 if (const int err = snd_pcm_drop(m_handle); err < 0) {
187 qCWarning(lcQnxSndInput) << "snd_pcm_drop failed:"
188 << snd_strerror(err) << "(" << err << ")";
189 }
190 }
191 if (m_workerThread) {
192 m_workerThread->wait();
193 m_workerThread = {};
194 }
195 m_wakePipe.close();
196}
197
198void QQnxSndAudioSourceStream::runProcessLoop()
199{
201
202 // openConfiguredPcm only prepares the stream; start capture explicitly here
203 // (the capture analogue of the sink's prefill-then-start).
204 if (const int err = QnxSndHelpers::startPcm(m_handle)) {
205 handleSndPcmError(err);
206 return;
207 }
208
209 for (;;) {
210 if (isStopRequested(std::memory_order_acquire))
211 return;
212
213 if (m_suspended.load(std::memory_order_acquire)) {
214 // Block on the wake pipe only — never touch the device while
215 // suspended. resume()/stop() call wake() to release us.
216 m_wakePipe.waitForWake();
217 continue;
218 }
219
220 // Wait until io-snd has a period of capture data, or a stop/suspend wakes us.
221 switch (QnxSndHelpers::pollPcm(m_handle, m_wakePipe)) {
223 continue; // re-evaluate stop/suspend at the loop top
225 handleSndPcmError();
226 return;
228 break;
229 }
230
231 if (!processOnePeriod()) {
232 if (!isStopRequested(std::memory_order_acquire))
233 return;
234 }
235 }
236}
237
238bool QQnxSndAudioSourceStream::processOnePeriod()
239{
240 // Read exactly what io-snd has captured (capped at one period so the stack
241 // scratch stays bounded). poll() wakes us with avail_min == one period ready.
242 // NB: snd_pcm_avail() (the hwsync variant) returns -ENOTSUP on QNX SALSA;
243 // snd_pcm_avail_update() reflects the just-polled state and is supported.
244 const snd_pcm_sframes_t avail = snd_pcm_avail_update(m_handle);
245 if (avail < 0) {
246 if (isStopRequested(std::memory_order_acquire))
247 return false;
248 // Overrun/suspend surfaced via avail; recover and let the loop re-poll.
249 if (const int err = recoverFromXrun(static_cast<int>(avail)); err < 0) {
250 handleSndPcmError(err);
251 return false;
252 }
253 return true;
254 }
255 if (avail == 0)
256 return true; // poll() raced ahead of the data; re-poll rather than spin.
257
258 const snd_pcm_uframes_t framesToRead =
259 std::min<snd_pcm_uframes_t>(static_cast<snd_pcm_uframes_t>(avail), m_periodFrames);
260 // io-snd delivers samples in the device-native format, so size the host buffer
261 // in native bytes; process()/runAudioCallback convert to the application format.
262 const int nativeBytesPerFrame = static_cast<int>(m_format.channelCount()
263 * QAudioHelperInternal::bytesPerSample(m_nativeFormat));
264 const size_t hostBytes = static_cast<size_t>(framesToRead) * nativeBytesPerFrame;
265
266 return withTemporaryBuffer(hostBytes, [&](QSpan<std::byte> hostBufferSpan) -> bool {
267 const snd_pcm_sframes_t framesRead =
268 snd_pcm_readi(m_handle, hostBufferSpan.data(), framesToRead);
269 if (framesRead == -EAGAIN)
270 return true; // raced poll(); the outer loop will poll again.
271 if (framesRead < 1) {
272 if (isStopRequested(std::memory_order_acquire))
273 return false;
274 if (const int err = recoverFromXrun(static_cast<int>(framesRead)); err < 0) {
275 handleSndPcmError(err);
276 return false;
277 }
278 return true;
279 }
280
281 // framesRead >= 1 with a positive nativeBytesPerFrame, so the byte count is
282 // always valid (no snd_pcm_frames_to_bytes stale-handle guard needed).
283 const qsizetype bytesRead = static_cast<qsizetype>(framesRead) * nativeBytesPerFrame;
284 QSpan<const std::byte> filled = hostBufferSpan.first(bytesRead);
285
286 if (m_audioCallback) {
287 runAudioCallback(*m_audioCallback, filled, m_format, volume(), m_nativeFormat);
288 } else {
289 const uint64_t framesWritten =
290 QPlatformAudioSourceStream::process(filled, framesRead, m_nativeFormat);
291 if (framesWritten != static_cast<uint64_t>(framesRead)) {
292 invokeOnAppThread([self = shared_from_this()] {
293 // updateStreamIdle reads m_parent atomically and no-ops on null.
294 self->updateStreamIdle(true);
295 });
296 }
297 }
298 return true;
299 });
300}
301
302void QQnxSndAudioSourceStream::handleSndPcmError(int err)
303{
304 requestStop();
305 // Read m_parent at fire-time inside the lambda; see QQnxSndAudioSinkStream
306 // counterpart for the lifetime argument.
307 invokeOnAppThread([self = shared_from_this(), err] {
308 qCWarning(lcQnxSndInput) << "audio input error, stopping stream:"
309 << (err ? snd_strerror(err) : "I/O error");
310 if (auto *parent = self->m_parent.load(std::memory_order_acquire))
311 self->handleIOError(parent);
312 });
313}
314
315///////////////////////////////////////////////////////////////////////////////////////////////////
316
317QQnxSndAudioSource::QQnxSndAudioSource(QAudioDevice device, const QAudioFormat &format,
318 QObject *parent)
320{
321}
322
323QQnxSndAudioSource::~QQnxSndAudioSource()
324{
325 // See QQnxSndAudioSink counterpart. Discard joins the worker
326 // synchronously and clears m_parent before the front class is freed.
327 if (m_stream) {
328 m_stream->stop(ShutdownPolicy::DiscardRingbuffer);
329 m_stream = {};
330 }
331}
332
333QT_END_NAMESPACE
QQnxSndAudioSource(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)
void updateStreamIdle(bool) override
QtMultimediaPrivate::QPlatformAudioSourceStream::AudioCallback AudioCallback
QQnxSndAudioSourceStream(QAudioDevice, const QAudioFormat &, std::optional< qsizetype > ringbufferSize, QQnxSndAudioSource *parent, float volume, std::optional< NativePeriodFrames > nativePeriodFrames)