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
qqnxsndhelpers.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
5
6#include <QtCore/qsysinfo.h>
7#include <QtCore/qthread.h>
8#include <QtCore/qvarlengtharray.h>
9
10#include <array>
11#include <cerrno>
12
13#include <fcntl.h>
14#include <poll.h>
15#include <pthread.h>
16#include <sched.h>
17#include <unistd.h>
18
19QT_BEGIN_NAMESPACE
20
21namespace QnxSndHelpers {
22
24{
25 close();
26}
27
28bool WakePipe::open()
29{
30 if (isOpen())
31 return true;
32 if (::pipe(m_fds) != 0)
33 return false;
34 // Non-blocking both ends so wake()/drain() never block.
35 for (int fd : m_fds)
36 ::fcntl(fd, F_SETFL, ::fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
37 return true;
38}
39
40void WakePipe::close() noexcept
41{
42 for (int &fd : m_fds) {
43 if (fd >= 0)
44 ::close(fd);
45 fd = -1;
46 }
47}
48
49void WakePipe::wake() const noexcept
50{
51 if (m_fds[1] < 0)
52 return;
53 const char byte = 1;
54 // A full pipe already means "wake pending", so EAGAIN is fine to ignore.
55 ssize_t n = ::write(m_fds[1], &byte, 1);
56 Q_UNUSED(n);
57}
58
59void WakePipe::drain() const noexcept
60{
61 if (m_fds[0] < 0)
62 return;
63 char buf[64];
64 while (::read(m_fds[0], buf, sizeof(buf)) > 0) { }
65}
66
67void WakePipe::waitForWake() const noexcept
68{
69 if (m_fds[0] < 0)
70 return;
71 pollfd pfd = { m_fds[0], POLLIN, 0 };
72 // The wake byte persists until drained, so a wake() racing just ahead of
73 // poll() is not missed.
74 if (::poll(&pfd, 1, -1) > 0)
75 drain();
76}
77
78PollOutcome pollPcm(snd_pcm_t *handle, const WakePipe &wake)
79{
80 int count = snd_pcm_poll_descriptors_count(handle);
81 if (count <= 0)
82 return PollOutcome::Error;
83
84 // io-snd reports one descriptor; the fixed buffer holds it plus the
85 // wake-pipe fd without a per-period heap allocation. The capacity is sized
86 // well above the single descriptor SALSA reports; assert in debug so a
87 // future PCM type that needs more is caught rather than silently truncated.
88 constexpr int kMaxPollDescriptors = 8; // PCM descriptors + 1 wake-pipe fd
89 std::array<pollfd, kMaxPollDescriptors> fds{};
90 Q_ASSERT(count <= int(fds.size()) - 1);
91 if (count > int(fds.size()) - 1)
92 count = int(fds.size()) - 1;
93 const int n = snd_pcm_poll_descriptors(handle, fds.data(), count);
94 if (n < 0)
95 return PollOutcome::Error;
96
97 fds[n].fd = wake.readFd();
98 fds[n].events = POLLIN;
99
100 const int rc = ::poll(fds.data(), n + 1, -1);
101 if (rc < 0)
102 return errno == EINTR ? PollOutcome::Woken : PollOutcome::Error;
103
104 if (fds[n].revents & POLLIN) {
105 wake.drain();
106 return PollOutcome::Woken;
107 }
108
109 unsigned short revents = 0;
110 if (snd_pcm_poll_descriptors_revents(handle, fds.data(), n, &revents) < 0)
111 return PollOutcome::Error;
112 if (revents & (POLLERR | POLLNVAL | POLLHUP))
113 return PollOutcome::Error;
114 if (revents & (POLLIN | POLLOUT))
115 return PollOutcome::Ready;
116
117 // Spurious wakeup: re-evaluate state.
118 return PollOutcome::Woken;
119}
120
121snd_pcm_format_t mapSampleFormat(QAudioFormat::SampleFormat fmt) noexcept
122{
123 constexpr bool isBigEndian = QSysInfo::ByteOrder == QSysInfo::BigEndian;
124
125 switch (fmt) {
126 case QAudioFormat::UInt8:
127 return SND_PCM_FORMAT_U8;
128 case QAudioFormat::Int16:
129 return isBigEndian ? SND_PCM_FORMAT_S16_BE : SND_PCM_FORMAT_S16_LE;
130 case QAudioFormat::Int32:
131 return isBigEndian ? SND_PCM_FORMAT_S32_BE : SND_PCM_FORMAT_S32_LE;
132 case QAudioFormat::Float:
133 return isBigEndian ? SND_PCM_FORMAT_FLOAT_BE : SND_PCM_FORMAT_FLOAT_LE;
134 case QAudioFormat::Unknown:
135 case QAudioFormat::NSampleFormats:
136 break;
137 }
138 return SND_PCM_FORMAT_UNKNOWN;
139}
140
141namespace {
142
143// Map a NativeSampleFormat (incl. the 24-bit formats QAudioFormat cannot
144// express) to the corresponding ALSA PCM format for this platform's byte order.
145snd_pcm_format_t nativeToPcmFormat(QAudioHelperInternal::NativeSampleFormat fmt) noexcept
146{
147 using QAudioHelperInternal::NativeSampleFormat;
148 constexpr bool isBigEndian = QSysInfo::ByteOrder == QSysInfo::BigEndian;
149
150 switch (fmt) {
151 case NativeSampleFormat::uint8_t:
152 return SND_PCM_FORMAT_U8;
153 case NativeSampleFormat::int16_t:
154 return isBigEndian ? SND_PCM_FORMAT_S16_BE : SND_PCM_FORMAT_S16_LE;
155 case NativeSampleFormat::int24_t_3b:
156 return isBigEndian ? SND_PCM_FORMAT_S24_3BE : SND_PCM_FORMAT_S24_3LE;
157 case NativeSampleFormat::int24_t_4b_low:
158 return isBigEndian ? SND_PCM_FORMAT_S24_BE : SND_PCM_FORMAT_S24_LE;
159 case NativeSampleFormat::int32_t:
160 return isBigEndian ? SND_PCM_FORMAT_S32_BE : SND_PCM_FORMAT_S32_LE;
161 case NativeSampleFormat::float32_t:
162 return isBigEndian ? SND_PCM_FORMAT_FLOAT_BE : SND_PCM_FORMAT_FLOAT_LE;
163 }
164 return SND_PCM_FORMAT_UNKNOWN;
165}
166
167// "Always best-native + convert": probe the device-native formats the opened PCM
168// accepts and pick the best one for the requested application format, then set it.
169// Returns the chosen NativeSampleFormat (the application/native conversion is done
170// by the base-class process()/runAudioCallback helpers). On failure returns the
171// snd_pcm error code via err and leaves the result format unset.
172int negotiateNativeFormat(snd_pcm_t *handle, snd_pcm_hw_params_t *hwparams,
173 const QAudioFormat &format, const QLoggingCategory &category,
174 QAudioHelperInternal::NativeSampleFormat &chosen)
175{
176 using QAudioHelperInternal::NativeSampleFormat;
177 // Candidates in ascending byte width; bestNativeSampleFormat picks among the
178 // subset the device actually supports according to its precision heuristics.
179 static constexpr NativeSampleFormat candidates[] = {
180 NativeSampleFormat::uint8_t, NativeSampleFormat::int16_t,
181 NativeSampleFormat::int24_t_3b, NativeSampleFormat::int24_t_4b_low,
182 NativeSampleFormat::int32_t, NativeSampleFormat::float32_t,
183 };
184
185 QVarLengthArray<NativeSampleFormat, std::size(candidates)> supported;
186 for (NativeSampleFormat nf : candidates) {
187 if (snd_pcm_hw_params_test_format(handle, hwparams, nativeToPcmFormat(nf)) == 0)
188 supported.append(nf);
189 }
190 if (supported.isEmpty()) {
191 qCWarning(category) << "device supports no usable native sample format";
192 return -EINVAL;
193 }
194
195 chosen = QAudioHelperInternal::bestNativeSampleFormat(format, supported);
196 qCDebug(category) << "native format: requested" << format.sampleFormat()
197 << "device-supported" << supported << "chosen" << chosen;
198 return snd_pcm_hw_params_set_format(handle, hwparams, nativeToPcmFormat(chosen));
199}
200
201} // namespace
202
203int recoverFromXrun(snd_pcm_t *handle, int err)
204{
205 int estrpipe = EIO;
206#ifdef ESTRPIPE
207 estrpipe = ESTRPIPE;
208#endif
209
210 if (err == -EPIPE) {
211 err = snd_pcm_prepare(handle);
212 } else if ((err == -estrpipe) || (err == -EIO)) {
213 int count = 0;
214 // ALSA cookbook idiom: retry snd_pcm_resume in ~100 ms increments.
215 while ((err = snd_pcm_resume(handle)) == -EAGAIN) {
216 QThread::msleep(kSuspendResumeRetryDelay.count());
217 if (++count > kSuspendResumeRetryLimit)
218 break;
219 }
220 if (err < 0)
221 err = snd_pcm_prepare(handle);
222 }
223 return err;
224}
225
226namespace {
227// Period (chunk) count from the environment, clamped to [kMinPeriodCount,
228// kMaxPeriodCount]. Returns kDefaultPeriodCount when unset or out of range.
229unsigned periodCountFromEnv(const char *name, const QLoggingCategory &category)
230{
231 bool ok = false;
232 const int value = qEnvironmentVariableIntValue(name, &ok);
233 if (!ok)
234 return kDefaultPeriodCount;
235 if (value < int(kMinPeriodCount) || value > int(kMaxPeriodCount)) {
236 qCWarning(category) << name << "value" << value << "is outside ["
238 << "]; using default" << kDefaultPeriodCount;
239 return kDefaultPeriodCount;
240 }
241 return static_cast<unsigned>(value);
242}
243} // namespace
244
245int startPcm(snd_pcm_t *handle)
246{
247 // A prefill write may have already auto-started the stream via the device's
248 // default start threshold, so only start it if it is still PREPARED.
249 if (snd_pcm_state(handle) != SND_PCM_STATE_PREPARED)
250 return 0;
251 // -EAGAIN means "will start on first I/O" — not fatal for a non-blocking PCM.
252 const int err = snd_pcm_start(handle);
253 return (err < 0 && err != -EAGAIN) ? err : 0;
254}
255
256void setWorkerRealtimePriority(const QLoggingCategory &category)
257{
258 int prio = kDefaultWorkerPriority;
259 bool ok = false;
260 if (const int value = qEnvironmentVariableIntValue("QT_QNXSND_WORKER_PRIO", &ok); ok) {
261 if (value < kMinWorkerPriority || value > kMaxWorkerPriority) {
262 qCWarning(category) << "QT_QNXSND_WORKER_PRIO value" << value << "is outside ["
264 << "]; using default" << kDefaultWorkerPriority;
265 } else {
266 prio = value;
267 }
268 }
269
270 sched_param param = {};
271 param.sched_priority = prio;
272 // Raise the worker into the 11-23 band so a busy application thread (default
273 // priority 10) cannot preempt the audio feed, while staying strictly below
274 // io-snd's data/helper threads (24/25). On failure keep inherited scheduling
275 // rather than aborting the stream.
276 if (const int rc = pthread_setschedparam(pthread_self(), SCHED_FIFO, &param); rc != 0) {
277 qCWarning(category) << "could not set worker SCHED_FIFO priority" << prio << ":"
278 << qt_error_string(rc) << "- keeping inherited scheduling";
279 return;
280 }
281 qCDebug(category) << "worker thread running SCHED_FIFO at priority" << prio;
282}
283
285{
286 const QLoggingCategory &category = config.category;
287
288 PcmOpenResult result;
289
290 qCDebug(category) << "Opening" << config.streamLabel
291 << "device:" << config.deviceId;
292
293 int dir = 0;
294 snd_pcm_t *handle = nullptr;
295
296 // Open non-blocking: a blocking open (flag 0) stalls this (app) thread
297 // indefinitely if the device is held exclusively by another client, and the
298 // later snd_pcm_nonblock() cannot unblock an open already in progress. The
299 // worker gates all subsequent I/O on poll(), so writei/readi return -EAGAIN
300 // instead of blocking. Mirrors probeDeviceFormat's open in qqnxsndaudiodevice.
301 int err = snd_pcm_open(&handle, config.deviceId.constData(), config.direction,
302 SND_PCM_NONBLOCK);
303 if (err < 0 || handle == nullptr) {
304 qCWarning(category) << "Failed to open" << config.streamLabel
305 << "device:" << config.deviceId
306 << "error:" << snd_strerror(err);
307 return {};
308 }
309
310 auto fail = [&](const char *what) -> PcmOpenResult {
311 qCWarning(category) << config.streamLabel << ":" << what
312 << "err =" << snd_strerror(err);
313 snd_pcm_close(handle);
314 return {};
315 };
316
317 snd_pcm_hw_params_t *hwparams;
318 snd_pcm_hw_params_alloca(&hwparams);
319
320 unsigned int sampleRate = config.format.sampleRate();
321
322 if ((err = snd_pcm_hw_params_any(handle, hwparams)) < 0)
323 return fail("snd_pcm_hw_params_any");
324
325 if (snd_pcm_hw_params_set_rate_resample(handle, hwparams, 1) < 0) {
326 // QNX SALSA has no plugin/resampling layer (unlike Linux ALSA), so this
327 // returns -EINVAL; rate handling is left to the device. Non-fatal.
328 qCDebug(category) << "snd_pcm_hw_params_set_rate_resample not supported, continuing";
329 }
330
331 if ((err = snd_pcm_hw_params_set_access(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
332 return fail("snd_pcm_hw_params_set_access");
333
334 if ((err = negotiateNativeFormat(handle, hwparams, config.format, category,
335 result.nativeFormat)) < 0)
336 return fail("snd_pcm_hw_params_set_format");
337
338 if ((err = snd_pcm_hw_params_set_channels(
339 handle, hwparams,
340 static_cast<unsigned int>(config.format.channelCount()))) < 0)
341 return fail("snd_pcm_hw_params_set_channels");
342
343 if ((err = snd_pcm_hw_params_set_rate_near(handle, hwparams, &sampleRate, 0)) < 0)
344 return fail("snd_pcm_hw_params_set_rate_near");
345
346 // Buffer = period * chunks: period frames (NativePeriodFrames override or
347 // default) and chunk count, both clamped to the device range by _near.
348 snd_pcm_uframes_t periodFrames = config.periodFrames.value_or(kDefaultPeriodFrames);
349 if ((err = snd_pcm_hw_params_set_period_size_near(handle, hwparams, &periodFrames, &dir)) < 0)
350 return fail("snd_pcm_hw_params_set_period_size_near");
351
352 unsigned int chunks = periodCountFromEnv(config.periodCountEnvVar, category);
353 if ((err = snd_pcm_hw_params_set_periods_near(handle, hwparams, &chunks, &dir)) < 0)
354 return fail("snd_pcm_hw_params_set_periods_near");
355
356 if ((err = snd_pcm_hw_params(handle, hwparams)) < 0)
357 return fail("snd_pcm_hw_params");
358
359 if ((err = snd_pcm_hw_params_get_buffer_size(hwparams, &result.bufferFrames)) < 0)
360 return fail("snd_pcm_hw_params_get_buffer_size");
361 if ((err = snd_pcm_hw_params_get_period_size(hwparams, &result.periodFrames, &dir)) < 0)
362 return fail("snd_pcm_hw_params_get_period_size");
363 result.periodBytes = snd_pcm_frames_to_bytes(handle, result.periodFrames);
364 if (result.periodFrames == 0 || result.periodBytes <= 0) {
365 err = -EINVAL;
366 return fail("invalid period geometry");
367 }
368
369 snd_pcm_sw_params_t *swparams;
370 snd_pcm_sw_params_alloca(&swparams);
371
372 if ((err = snd_pcm_sw_params_current(handle, swparams)) < 0)
373 return fail("snd_pcm_sw_params_current");
374 // avail_min = one period, so poll() reports ready once a full period of
375 // space/data is available. Start/stop thresholds stay at their defaults; the
376 // worker starts the stream explicitly (startPcm).
377 if ((err = snd_pcm_sw_params_set_avail_min(handle, swparams, result.periodFrames)) < 0)
378 return fail("snd_pcm_sw_params_set_avail_min");
379 if ((err = snd_pcm_sw_params(handle, swparams)) < 0)
380 return fail("snd_pcm_sw_params");
381
382 if ((err = snd_pcm_prepare(handle)) < 0)
383 return fail("snd_pcm_prepare");
384
385 result.handle = handle;
386
387 qCDebug(category) << config.streamLabel << "opened:" << config.deviceId
388 << "buffer_frames:" << result.bufferFrames
389 << "period_frames:" << result.periodFrames
390 << "period_bytes:" << result.periodBytes
391 << "chunks:" << chunks;
392 return result;
393}
394
395} // namespace QnxSndHelpers
396
397QT_END_NAMESPACE
bool isOpen() const noexcept
void drain() const noexcept
int readFd() const noexcept
void waitForWake() const noexcept
void wake() const noexcept
constexpr unsigned kDefaultPeriodCount
void setWorkerRealtimePriority(const QLoggingCategory &category)
constexpr unsigned kMaxPeriodCount
constexpr int kDefaultWorkerPriority
int startPcm(snd_pcm_t *handle)
snd_pcm_format_t mapSampleFormat(QAudioFormat::SampleFormat) noexcept
constexpr int kSuspendResumeRetryLimit
constexpr snd_pcm_uframes_t kDefaultPeriodFrames
int recoverFromXrun(snd_pcm_t *handle, int err)
constexpr int kMaxWorkerPriority
PollOutcome pollPcm(snd_pcm_t *handle, const WakePipe &wake)
constexpr unsigned kMinPeriodCount
constexpr int kMinWorkerPriority
PcmOpenResult openConfiguredPcm(const PcmOpenConfig &config)