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
qpulseaudio_contextmanager.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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/qdebug.h>
7#include <QtCore/qtimer.h>
8#include <QtCore/private/qflatmap_p.h>
9#include <QtGui/qguiapplication.h>
10#include <QtGui/qicon.h>
11#include <QtMultimedia/qaudiodevice.h>
12#include <QtMultimedia/private/qaudiodevice_p.h>
13#include <QtMultimedia/private/qpulsehelpers_p.h>
14#include <QtMultimedia/private/qpulseaudiodevice_p.h>
15
16#include <sys/types.h>
17#include <unistd.h>
18#include <mutex> // for lock_guard
19
20QT_BEGIN_NAMESPACE
21
22using PAOperationHandle = QPulseAudioInternal::PAOperationHandle;
23
25makeQAudioDevicePrivate(const char *device, const char *desc, bool isDef, QAudioDevice::Mode mode,
26 const pa_channel_map &map, const pa_sample_spec &spec)
27{
28 using namespace QPulseAudioInternal;
29
30 auto deviceInfo = std::make_unique<QPulseAudioDevicePrivate>(device, mode, QString::fromUtf8(desc));
31 QAudioFormat::ChannelConfig channelConfig = channelConfigFromMap(map);
32
33 deviceInfo->isDefault = isDef;
34 deviceInfo->channelConfiguration = channelConfig;
35
36 deviceInfo->minimumChannelCount = 1;
37 deviceInfo->maximumChannelCount = PA_CHANNELS_MAX;
38 deviceInfo->minimumSampleRate = 1;
39 deviceInfo->maximumSampleRate = PA_RATE_MAX;
40
41 constexpr bool isBigEndian = QSysInfo::ByteOrder == QSysInfo::BigEndian;
42
43 constexpr struct
44 {
45 pa_sample_format pa_fmt;
47 } formatMap[] = {
48 { PA_SAMPLE_U8, QAudioFormat::UInt8 },
49 { isBigEndian ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE, QAudioFormat::Int16 },
50 { isBigEndian ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE, QAudioFormat::Int32 },
51 { isBigEndian ? PA_SAMPLE_FLOAT32BE : PA_SAMPLE_FLOAT32LE, QAudioFormat::Float },
52 };
53
54 for (const auto &f : formatMap) {
55 if (pa_sample_format_valid(f.pa_fmt) != 0)
56 deviceInfo->supportedSampleFormats.append(f.qt_fmt);
57 }
58
59 QAudioFormat preferredFormat = sampleSpecToAudioFormat(spec);
60 if (!preferredFormat.isValid()) {
61 preferredFormat.setChannelCount(spec.channels ? spec.channels : 2);
62 preferredFormat.setSampleRate(spec.rate ? spec.rate : 48000);
63
64 Q_ASSERT(spec.format != PA_SAMPLE_INVALID);
65 if (!deviceInfo->supportedSampleFormats.contains(preferredFormat.sampleFormat()))
67 }
68
69 deviceInfo->preferredFormat = preferredFormat;
70 deviceInfo->preferredFormat.setChannelConfig(channelConfig);
71 Q_ASSERT(deviceInfo->preferredFormat.isValid());
72
73 return deviceInfo;
74}
75
76template<typename Info>
77static bool updateDevicesMap(QReadWriteLock &lock, const QByteArray &defaultDeviceId,
78 QMap<int, QAudioDevice> &devices, QAudioDevice::Mode mode,
79 const Info &info)
80{
81 QWriteLocker locker(&lock);
82
83 bool isDefault = defaultDeviceId == info.name;
84 auto newDeviceInfo = makeQAudioDevicePrivate(info.name, info.description, isDefault, mode,
85 info.channel_map, info.sample_spec);
86
87 auto &device = devices[info.index];
88 QAudioDevicePrivateAllMembersEqual compare;
89 const QAudioDevicePrivate *handle = QAudioDevicePrivate::handle(device);
90 if (handle && compare(*newDeviceInfo, *handle))
91 return false;
92
93 device = QAudioDevicePrivate::createQAudioDevice(std::move(newDeviceInfo));
94 return true;
95}
96
97static bool updateDevicesMap(QReadWriteLock &lock, const QByteArray &defaultDeviceId,
98 QMap<int, QAudioDevice> &devices)
99{
100 QWriteLocker locker(&lock);
101
102 bool result = false;
103
104 for (QAudioDevice &device : devices) {
105 auto deviceInfo = QAudioDevicePrivate::handle<QPulseAudioDevicePrivate>(device);
106 const auto isDefault = deviceInfo->id == defaultDeviceId;
107 if (deviceInfo->isDefault != isDefault) {
108 auto newDeviceInfo = std::make_unique<QPulseAudioDevicePrivate>(*deviceInfo);
109 newDeviceInfo->isDefault = isDefault;
110 device = QAudioDevicePrivate::createQAudioDevice(std::move(newDeviceInfo));
111 result = true;
112 }
113 }
114
115 return result;
116};
117
118void QPulseAudioContextManager::serverInfoCallback(pa_context *context, const pa_server_info *info,
119 void *userdata)
120{
121 using namespace Qt::Literals;
122 using namespace QPulseAudioInternal;
123
124 if (!info) {
125 qWarning() << "Failed to get server information:" << currentError(context);
126 return;
127 }
128
129 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
130 char ss[PA_SAMPLE_SPEC_SNPRINT_MAX], cm[PA_CHANNEL_MAP_SNPRINT_MAX];
131
132 pa_sample_spec_snprint(ss, sizeof(ss), &info->sample_spec);
133 pa_channel_map_snprint(cm, sizeof(cm), &info->channel_map);
134
135 qCDebug(qLcPulseAudioEngine)
136 << QStringLiteral("User name: %1\n"
137 "Host Name: %2\n"
138 "Server Name: %3\n"
139 "Server Version: %4\n"
140 "Default Sample Specification: %5\n"
141 "Default Channel Map: %6\n"
142 "Default Sink: %7\n"
143 "Default Source: %8\n")
144 .arg(QString::fromUtf8(info->user_name),
145 QString::fromUtf8(info->host_name),
146 QString::fromUtf8(info->server_name),
147 QLatin1StringView(info->server_version), QLatin1StringView(ss),
148 QLatin1StringView(cm), QString::fromUtf8(info->default_sink_name),
149 QString::fromUtf8(info->default_source_name));
150 }
151
152 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
153
154 bool defaultSinkChanged = false;
155 bool defaultSourceChanged = false;
156
157 {
158 QWriteLocker locker(&pulseEngine->m_serverLock);
159 pulseEngine->m_serverName = QString::fromUtf8(info->server_name);
160
161 if (pulseEngine->m_defaultSink != info->default_sink_name) {
162 pulseEngine->m_defaultSink = info->default_sink_name;
163 defaultSinkChanged = true;
164 }
165
166 if (pulseEngine->m_defaultSource != info->default_source_name) {
167 pulseEngine->m_defaultSource = info->default_source_name;
168 defaultSourceChanged = true;
169 }
170 }
171
172 if (defaultSinkChanged
173 && updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink,
174 pulseEngine->m_sinks))
175 emit pulseEngine->audioOutputsChanged();
176
177 if (defaultSourceChanged
178 && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
179 pulseEngine->m_sources))
180 emit pulseEngine->audioInputsChanged();
181
182 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
183}
184
185void QPulseAudioContextManager::sinkInfoCallback(pa_context *context, const pa_sink_info *info,
186 int isLast, void *userdata)
187{
188 using namespace Qt::Literals;
189 using namespace QPulseAudioInternal;
190
191 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
192
193 if (isLast < 0) {
194 qWarning() << "Failed to get sink information:" << currentError(context);
195 return;
196 }
197
198 if (isLast) {
199 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
200 return;
201 }
202
203 Q_ASSERT(info);
204
205 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
206 static const QFlatMap<pa_sink_state, QStringView> stateMap{
207 { PA_SINK_INVALID_STATE, u"n/a" }, { PA_SINK_RUNNING, u"RUNNING" },
208 { PA_SINK_IDLE, u"IDLE" }, { PA_SINK_SUSPENDED, u"SUSPENDED" },
209 { PA_SINK_UNLINKED, u"UNLINKED" },
210 };
211
212 qCDebug(qLcPulseAudioEngine)
213 << QStringLiteral("Sink #%1\n"
214 "\tState: %2\n"
215 "\tName: %3\n"
216 "\tDescription: %4\n")
217 .arg(QString::number(info->index), stateMap.value(info->state),
218 QString::fromUtf8(info->name),
219 QString::fromUtf8(info->description));
220 }
221
222 if (updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks,
223 QAudioDevice::Output, *info))
224 emit pulseEngine->audioOutputsChanged();
225}
226
227void QPulseAudioContextManager::sourceInfoCallback(pa_context *context, const pa_source_info *info,
228 int isLast, void *userdata)
229{
230 using namespace Qt::Literals;
231
232 Q_UNUSED(context);
233 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
234
235 if (isLast) {
236 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
237 return;
238 }
239
240 Q_ASSERT(info);
241
242 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
243 static const QFlatMap<pa_source_state, QStringView> stateMap{
244 { PA_SOURCE_INVALID_STATE, u"n/a" }, { PA_SOURCE_RUNNING, u"RUNNING" },
245 { PA_SOURCE_IDLE, u"IDLE" }, { PA_SOURCE_SUSPENDED, u"SUSPENDED" },
246 { PA_SOURCE_UNLINKED, u"UNLINKED" },
247 };
248
249 qCDebug(qLcPulseAudioEngine)
250 << QStringLiteral("Source #%1\n"
251 "\tState: %2\n"
252 "\tName: %3\n"
253 "\tDescription: %4\n")
254 .arg(QString::number(info->index), stateMap.value(info->state),
255 QString::fromUtf8(info->name),
256 QString::fromUtf8(info->description));
257 }
258
259 // skip monitor channels
260 if (info->monitor_of_sink != PA_INVALID_INDEX)
261 return;
262
263 if (updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
264 pulseEngine->m_sources, QAudioDevice::Input, *info))
265 emit pulseEngine->audioInputsChanged();
266}
267
268void QPulseAudioContextManager::eventCallback(pa_context *context, pa_subscription_event_type_t t,
269 uint32_t index, void *userdata)
270{
271 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
272
273 int type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
274 int facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
275
276 switch (type) {
277 case PA_SUBSCRIPTION_EVENT_NEW:
278 case PA_SUBSCRIPTION_EVENT_CHANGE:
279 switch (facility) {
280 case PA_SUBSCRIPTION_EVENT_SERVER: {
281 PAOperationHandle op{
282 pa_context_get_server_info(context, serverInfoCallback, userdata),
283 PAOperationHandle::HasRef,
284 };
285 if (!op)
286 qWarning() << "PulseAudioService: failed to get server info";
287 break;
288 }
289 case PA_SUBSCRIPTION_EVENT_SINK: {
290 PAOperationHandle op{
291 pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata),
292 PAOperationHandle::HasRef,
293 };
294
295 if (!op)
296 qWarning() << "PulseAudioService: failed to get sink info";
297 break;
298 }
299 case PA_SUBSCRIPTION_EVENT_SOURCE: {
300 PAOperationHandle op{
301 pa_context_get_source_info_by_index(context, index, sourceInfoCallback, userdata),
302 PAOperationHandle::HasRef,
303 };
304
305 if (!op)
306 qWarning() << "PulseAudioService: failed to get source info";
307 break;
308 }
309 default:
310 break;
311 }
312 break;
313 case PA_SUBSCRIPTION_EVENT_REMOVE:
314 switch (facility) {
315 case PA_SUBSCRIPTION_EVENT_SINK: {
316 QWriteLocker locker(&pulseEngine->m_sinkLock);
317 pulseEngine->m_sinks.remove(index);
318 break;
319 }
320 case PA_SUBSCRIPTION_EVENT_SOURCE: {
321 QWriteLocker locker(&pulseEngine->m_sourceLock);
322 pulseEngine->m_sources.remove(index);
323 break;
324 }
325 default:
326 break;
327 }
328 break;
329 default:
330 break;
331 }
332}
333
334void QPulseAudioContextManager::contextStateCallbackInit(pa_context *context, void *userdata)
335{
336 Q_UNUSED(context);
337
338 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
339 qCDebug(qLcPulseAudioEngine) << pa_context_get_state(context);
340
341 QPulseAudioContextManager *pulseEngine =
342 reinterpret_cast<QPulseAudioContextManager *>(userdata);
343 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
344}
345
346void QPulseAudioContextManager::contextStateCallback(pa_context *c, void *userdata)
347{
348 QPulseAudioContextManager *self = reinterpret_cast<QPulseAudioContextManager *>(userdata);
349 pa_context_state_t state = pa_context_get_state(c);
350
351 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
352 qCDebug(qLcPulseAudioEngine) << state;
353
354 if (state == PA_CONTEXT_FAILED)
355 QMetaObject::invokeMethod(self, &QPulseAudioContextManager::onContextFailed,
356 Qt::QueuedConnection);
357}
358
360
362{
363 prepare();
364}
365
370
371void QPulseAudioContextManager::prepare()
372{
373 using namespace QPulseAudioInternal;
374 bool keepGoing = true;
375 bool ok = true;
376
377 m_mainLoop.reset(pa_threaded_mainloop_new());
378 if (m_mainLoop == nullptr) {
379 qCritical() << "PulseAudioService: unable to create pulseaudio mainloop";
380 return;
381 }
382
383 pa_threaded_mainloop_set_name(
384 m_mainLoop.get(), "QPulseAudioEngi"); // thread names are limited to 15 chars on linux
385
386 if (pa_threaded_mainloop_start(m_mainLoop.get()) != 0) {
387 qCritical() << "PulseAudioService: unable to start pulseaudio mainloop";
388 m_mainLoop = {};
389 return;
390 }
391
392 m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop.get());
393
394 std::unique_lock guard{ *this };
395
396 PAProplistHandle proplist{
397 pa_proplist_new(),
398 };
399 if (!QGuiApplication::applicationDisplayName().isEmpty())
400 pa_proplist_sets(proplist.get(), PA_PROP_APPLICATION_NAME,
401 qUtf8Printable(QGuiApplication::applicationDisplayName()));
402 if (!QGuiApplication::desktopFileName().isEmpty())
403 pa_proplist_sets(proplist.get(), PA_PROP_APPLICATION_ID,
404 qUtf8Printable(QGuiApplication::desktopFileName()));
405 if (const QString windowIconName = QGuiApplication::windowIcon().name();
406 !windowIconName.isEmpty())
407 pa_proplist_sets(proplist.get(), PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
408
409 m_context = PAContextHandle{
410 pa_context_new_with_proplist(m_mainLoopApi, nullptr, proplist.get()),
411 PAContextHandle::HasRef,
412 };
413
414 if (!m_context) {
415 qCritical() << "PulseAudioService: Unable to create new pulseaudio context";
416 guard.unlock();
417 m_mainLoop = {};
418 onContextFailed();
419 return;
420 }
421
422 pa_context_set_state_callback(m_context.get(), contextStateCallbackInit, this);
423
424 if (pa_context_connect(m_context.get(), nullptr, static_cast<pa_context_flags_t>(0), nullptr)
425 < 0) {
426 qWarning() << "PulseAudioService: pa_context_connect() failed";
427 m_context = {};
428 guard.unlock();
429 m_mainLoop = {};
430 return;
431 }
432
433 pa_threaded_mainloop_wait(m_mainLoop.get());
434
435 while (keepGoing) {
436 switch (pa_context_get_state(m_context.get())) {
437 case PA_CONTEXT_CONNECTING:
438 case PA_CONTEXT_AUTHORIZING:
439 case PA_CONTEXT_SETTING_NAME:
440 break;
441
442 case PA_CONTEXT_READY:
443 qCDebug(qLcPulseAudioEngine) << "Connection established.";
444 keepGoing = false;
445 break;
446
447 case PA_CONTEXT_TERMINATED:
448 qCritical("PulseAudioService: Context terminated.");
449 keepGoing = false;
450 ok = false;
451 break;
452
453 case PA_CONTEXT_FAILED:
454 default:
455 qCritical() << "PulseAudioService: Connection failure:"
456 << currentError(m_context.get());
457 keepGoing = false;
458 ok = false;
459 }
460
461 if (keepGoing)
462 pa_threaded_mainloop_wait(m_mainLoop.get());
463 }
464
465 if (ok) {
466 pa_context_set_state_callback(m_context.get(), contextStateCallback, this);
467
468 pa_context_set_subscribe_callback(m_context.get(), eventCallback, this);
469 PAOperationHandle op{
470 pa_context_subscribe(m_context.get(),
471 pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK
472 | PA_SUBSCRIPTION_MASK_SOURCE
473 | PA_SUBSCRIPTION_MASK_SERVER),
474 nullptr, nullptr),
475 PAOperationHandle::HasRef,
476 };
477
478 if (!op)
479 qWarning() << "PulseAudioService: failed to subscribe to context notifications";
480 } else {
481 m_context = {};
482 }
483
484 guard.unlock();
485
486 if (ok) {
487 updateDevices();
488 } else {
489 m_mainLoop = {};
490 onContextFailed();
491 }
492}
493
494void QPulseAudioContextManager::release()
495{
496 if (m_context) {
497 std::lock_guard lock{ *this };
498 pa_context_disconnect(m_context.get());
499 m_context = {};
500 }
501
502 if (m_mainLoop) {
503 pa_threaded_mainloop_stop(m_mainLoop.get());
504 m_mainLoop = {};
505 }
506}
507
508void QPulseAudioContextManager::updateDevices()
509{
510 std::lock_guard lock(*this);
511
512 // Get default input and output devices
513 bool success = waitForAsyncOperation(
514 pa_context_get_server_info(m_context.get(), serverInfoCallback, this));
515
516 if (!success)
517 qWarning() << "PulseAudioService: failed to get server info";
518
519 // Get output devices
520 success = waitForAsyncOperation(
521 pa_context_get_sink_info_list(m_context.get(), sinkInfoCallback, this));
522
523 if (!success)
524 qWarning() << "PulseAudioService: failed to get sink info";
525
526 // Get input devices
527 success = waitForAsyncOperation(
528 pa_context_get_source_info_list(m_context.get(), sourceInfoCallback, this));
529
530 if (!success)
531 qWarning() << "PulseAudioService: failed to get source info";
532}
533
534void QPulseAudioContextManager::onContextFailed()
535{
536 release();
537
538 // Try to reconnect later
539 QTimer::singleShot(3000, this, &QPulseAudioContextManager::prepare);
540}
541
543{
544 return pulseEngine();
545}
546
548{
549 PAOperationHandle operation{
550 op,
551 PAOperationHandle::HasRef,
552 };
553
554 return waitForAsyncOperation(operation);
555}
556
557bool QPulseAudioContextManager::waitForAsyncOperation(const PAOperationHandle &op)
558{
559 if (!op)
560 return false;
561
562 wait(op);
563 return true;
564}
565
567{
568 if (mode == QAudioDevice::Output) {
569 QReadLocker locker(&m_sinkLock);
570 return m_sinks.values();
571 }
572
573 if (mode == QAudioDevice::Input) {
574 QReadLocker locker(&m_sourceLock);
575 return m_sources.values();
576 }
577
578 return {};
579}
580
582{
583 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
584}
585
587{
588 auto lock = std::lock_guard{ *this };
589 return pa_context_get_state(m_context.get());
590}
591
593{
594 return PA_CONTEXT_IS_GOOD(getContextState());
595}
596
598{
599 QReadLocker locker(&pulseEngine->m_serverLock);
600 return m_serverName;
601}
602
603QT_END_NAMESPACE
The QAudioFormat class stores audio stream parameter information.
constexpr SampleFormat sampleFormat() const noexcept
Returns the current sample format.
constexpr void setChannelCount(int channelCount) noexcept
Sets the channel count to channels.
constexpr void setSampleFormat(SampleFormat f) noexcept
Sets the sample format to format.
constexpr bool isValid() const noexcept
Returns true if all of the parameters are valid.
SampleFormat
Qt will always expect and use samples in the endianness of the host platform.
constexpr void setSampleRate(int sampleRate) noexcept
Sets the sample rate to samplerate in Hertz.
ChannelConfig
\variable QAudioFormat::NChannelPositions
QByteArray defaultDevice(QAudioDevice::Mode mode) const
static QPulseAudioContextManager * instance()
bool waitForAsyncOperation(const PAOperationHandle &)
bool waitForAsyncOperation(pa_operation *op)
QList< QAudioDevice > availableDevices(QAudioDevice::Mode mode) const
QPulseAudioContextManager(QObject *parent=nullptr)
static bool updateDevicesMap(QReadWriteLock &lock, const QByteArray &defaultDeviceId, QMap< int, QAudioDevice > &devices, QAudioDevice::Mode mode, const Info &info)
Q_GLOBAL_STATIC(QPulseAudioContextManager, pulseEngine)
static std::unique_ptr< QAudioDevicePrivate > makeQAudioDevicePrivate(const char *device, const char *desc, bool isDef, QAudioDevice::Mode mode, const pa_channel_map &map, const pa_sample_spec &spec)
static bool updateDevicesMap(QReadWriteLock &lock, const QByteArray &defaultDeviceId, QMap< int, QAudioDevice > &devices)