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
4#include "qpulseaudio_contextmanager_p.h"
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
24static std::unique_ptr<QAudioDevicePrivate>
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>(args&: device, args&: mode, args: QString::fromUtf8(utf8: 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;
46 QAudioFormat::SampleFormat qt_fmt;
47 } formatMap[] = {
48 { PA_SAMPLE_U8, .qt_fmt: QAudioFormat::UInt8 },
49 { .pa_fmt: isBigEndian ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE, .qt_fmt: QAudioFormat::Int16 },
50 { .pa_fmt: isBigEndian ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE, .qt_fmt: QAudioFormat::Int32 },
51 { .pa_fmt: isBigEndian ? PA_SAMPLE_FLOAT32BE : PA_SAMPLE_FLOAT32LE, .qt_fmt: QAudioFormat::Float },
52 };
53
54 for (const auto &f : formatMap) {
55 if (pa_sample_format_valid(format: f.pa_fmt) != 0)
56 deviceInfo->supportedSampleFormats.append(t: 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(t: preferredFormat.sampleFormat()))
66 preferredFormat.setSampleFormat(QAudioFormat::Float);
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(devicePrivate: 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>(args: *deviceInfo);
109 newDeviceInfo->isDefault = isDefault;
110 device = QAudioDevicePrivate::createQAudioDevice(devicePrivate: 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(s: ss, l: sizeof(ss), spec: &info->sample_spec);
133 pa_channel_map_snprint(s: cm, l: sizeof(cm), map: &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(args: QString::fromUtf8(utf8: info->user_name),
145 args: QString::fromUtf8(utf8: info->host_name),
146 args: QString::fromUtf8(utf8: info->server_name),
147 args: QLatin1StringView(info->server_version), args: QLatin1StringView(ss),
148 args: QLatin1StringView(cm), args: QString::fromUtf8(utf8: info->default_sink_name),
149 args: QString::fromUtf8(utf8: 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(utf8: 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(lock&: pulseEngine->m_sinkLock, defaultDeviceId: pulseEngine->m_defaultSink,
174 devices&: pulseEngine->m_sinks))
175 emit pulseEngine->audioOutputsChanged();
176
177 if (defaultSourceChanged
178 && updateDevicesMap(lock&: pulseEngine->m_sourceLock, defaultDeviceId: pulseEngine->m_defaultSource,
179 devices&: pulseEngine->m_sources))
180 emit pulseEngine->audioInputsChanged();
181
182 pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 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(m: pulseEngine->mainloop(), wait_for_accept: 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(args: QString::number(info->index), args: stateMap.value(key: info->state),
218 args: QString::fromUtf8(utf8: info->name),
219 args: QString::fromUtf8(utf8: info->description));
220 }
221
222 if (updateDevicesMap(lock&: pulseEngine->m_sinkLock, defaultDeviceId: pulseEngine->m_defaultSink, devices&: pulseEngine->m_sinks,
223 mode: QAudioDevice::Output, info: *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(m: pulseEngine->mainloop(), wait_for_accept: 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(args: QString::number(info->index), args: stateMap.value(key: info->state),
255 args: QString::fromUtf8(utf8: info->name),
256 args: QString::fromUtf8(utf8: info->description));
257 }
258
259 // skip monitor channels
260 if (info->monitor_of_sink != PA_INVALID_INDEX)
261 return;
262
263 if (updateDevicesMap(lock&: pulseEngine->m_sourceLock, defaultDeviceId: pulseEngine->m_defaultSource,
264 devices&: pulseEngine->m_sources, mode: QAudioDevice::Input, info: *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(c: context, cb: 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(c: context, idx: index, cb: 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(c: context, idx: index, cb: 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(key: index);
318 break;
319 }
320 case PA_SUBSCRIPTION_EVENT_SOURCE: {
321 QWriteLocker locker(&pulseEngine->m_sourceLock);
322 pulseEngine->m_sources.remove(key: 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(c: context);
340
341 QPulseAudioContextManager *pulseEngine =
342 reinterpret_cast<QPulseAudioContextManager *>(userdata);
343 pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 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(object: self, function: &QPulseAudioContextManager::onContextFailed,
356 type: Qt::QueuedConnection);
357}
358
359Q_GLOBAL_STATIC(QPulseAudioContextManager, pulseEngine);
360
361QPulseAudioContextManager::QPulseAudioContextManager(QObject *parent) : QObject(parent)
362{
363 prepare();
364}
365
366QPulseAudioContextManager::~QPulseAudioContextManager()
367{
368 release();
369}
370
371void QPulseAudioContextManager::prepare()
372{
373 using namespace QPulseAudioInternal;
374 bool keepGoing = true;
375 bool ok = true;
376
377 m_mainLoop.reset(p: 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: m_mainLoop.get(), name: "QPulseAudioEngi"); // thread names are limited to 15 chars on linux
385
386 if (pa_threaded_mainloop_start(m: 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: 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(p: proplist.get(), PA_PROP_APPLICATION_NAME,
401 qUtf8Printable(QGuiApplication::applicationDisplayName()));
402 if (!QGuiApplication::desktopFileName().isEmpty())
403 pa_proplist_sets(p: 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(p: proplist.get(), PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
408
409 m_context = PAContextHandle{
410 pa_context_new_with_proplist(mainloop: m_mainLoopApi, name: nullptr, proplist: 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(c: m_context.get(), cb: contextStateCallbackInit, userdata: this);
423
424 if (pa_context_connect(c: m_context.get(), server: nullptr, flags: static_cast<pa_context_flags_t>(0), api: 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: m_mainLoop.get());
434
435 while (keepGoing) {
436 switch (pa_context_get_state(c: 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(msg: "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: m_mainLoop.get());
463 }
464
465 if (ok) {
466 pa_context_set_state_callback(c: m_context.get(), cb: contextStateCallback, userdata: this);
467
468 pa_context_set_subscribe_callback(c: m_context.get(), cb: eventCallback, userdata: this);
469 PAOperationHandle op{
470 pa_context_subscribe(c: m_context.get(),
471 m: pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK
472 | PA_SUBSCRIPTION_MASK_SOURCE
473 | PA_SUBSCRIPTION_MASK_SERVER),
474 cb: nullptr, userdata: 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(c: m_context.get());
499 m_context = {};
500 }
501
502 if (m_mainLoop) {
503 pa_threaded_mainloop_stop(m: 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 op: pa_context_get_server_info(c: m_context.get(), cb: serverInfoCallback, userdata: this));
515
516 if (!success)
517 qWarning() << "PulseAudioService: failed to get server info";
518
519 // Get output devices
520 success = waitForAsyncOperation(
521 op: pa_context_get_sink_info_list(c: m_context.get(), cb: sinkInfoCallback, userdata: this));
522
523 if (!success)
524 qWarning() << "PulseAudioService: failed to get sink info";
525
526 // Get input devices
527 success = waitForAsyncOperation(
528 op: pa_context_get_source_info_list(c: m_context.get(), cb: sourceInfoCallback, userdata: 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(interval: 3000, receiver: this, slot: &QPulseAudioContextManager::prepare);
540}
541
542QPulseAudioContextManager *QPulseAudioContextManager::instance()
543{
544 return pulseEngine();
545}
546
547bool QPulseAudioContextManager::waitForAsyncOperation(pa_operation *op)
548{
549 PAOperationHandle operation{
550 op,
551 PAOperationHandle::HasRef,
552 };
553
554 if (!operation)
555 return false;
556
557 wait(op: operation);
558 return true;
559}
560
561QList<QAudioDevice> QPulseAudioContextManager::availableDevices(QAudioDevice::Mode mode) const
562{
563 if (mode == QAudioDevice::Output) {
564 QReadLocker locker(&m_sinkLock);
565 return m_sinks.values();
566 }
567
568 if (mode == QAudioDevice::Input) {
569 QReadLocker locker(&m_sourceLock);
570 return m_sources.values();
571 }
572
573 return {};
574}
575
576QByteArray QPulseAudioContextManager::defaultDevice(QAudioDevice::Mode mode) const
577{
578 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
579}
580
581pa_context_state_t QPulseAudioContextManager::getContextState()
582{
583 auto lock = std::lock_guard{ *this };
584 return pa_context_get_state(c: m_context.get());
585}
586
587bool QPulseAudioContextManager::contextIsGood()
588{
589 return PA_CONTEXT_IS_GOOD(x: getContextState());
590}
591
592QString QPulseAudioContextManager::serverName()
593{
594 QReadLocker locker(&pulseEngine->m_serverLock);
595 return m_serverName;
596}
597
598QT_END_NAMESPACE
599

source code of qtmultimedia/src/multimedia/pulseaudio/qpulseaudio_contextmanager.cpp