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
14#include "qpulsehelpers_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<QAudioDevicePrivate>(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 if (device.handle() && compare(*newDeviceInfo, *device.handle()))
90 return false;
91
92 device = newDeviceInfo.release()->create();
93 return true;
94}
95
96static bool updateDevicesMap(QReadWriteLock &lock, const QByteArray &defaultDeviceId,
97 QMap<int, QAudioDevice> &devices)
98{
99 QWriteLocker locker(&lock);
100
101 bool result = false;
102
103 for (QAudioDevice &device : devices) {
104 auto deviceInfo = device.handle();
105 const auto isDefault = deviceInfo->id == defaultDeviceId;
106 if (deviceInfo->isDefault != isDefault) {
107 auto newDeviceInfo = std::make_unique<QAudioDevicePrivate>(args: *deviceInfo);
108 newDeviceInfo->isDefault = isDefault;
109 device = newDeviceInfo.release()->create();
110 result = true;
111 }
112 }
113
114 return result;
115};
116
117void QPulseAudioContextManager::serverInfoCallback(pa_context *context, const pa_server_info *info,
118 void *userdata)
119{
120 using namespace Qt::Literals;
121 using namespace QPulseAudioInternal;
122
123 if (!info) {
124 qWarning() << "Failed to get server information:" << currentError(context);
125 return;
126 }
127
128 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
129 char ss[PA_SAMPLE_SPEC_SNPRINT_MAX], cm[PA_CHANNEL_MAP_SNPRINT_MAX];
130
131 pa_sample_spec_snprint(s: ss, l: sizeof(ss), spec: &info->sample_spec);
132 pa_channel_map_snprint(s: cm, l: sizeof(cm), map: &info->channel_map);
133
134 qCDebug(qLcPulseAudioEngine)
135 << QStringLiteral("User name: %1\n"
136 "Host Name: %2\n"
137 "Server Name: %3\n"
138 "Server Version: %4\n"
139 "Default Sample Specification: %5\n"
140 "Default Channel Map: %6\n"
141 "Default Sink: %7\n"
142 "Default Source: %8\n")
143 .arg(args: QString::fromUtf8(utf8: info->user_name),
144 args: QString::fromUtf8(utf8: info->host_name),
145 args: QString::fromUtf8(utf8: info->server_name),
146 args: QLatin1StringView(info->server_version), args: QLatin1StringView(ss),
147 args: QLatin1StringView(cm), args: QString::fromUtf8(utf8: info->default_sink_name),
148 args: QString::fromUtf8(utf8: info->default_source_name));
149 }
150
151 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
152
153 bool defaultSinkChanged = false;
154 bool defaultSourceChanged = false;
155
156 {
157 QWriteLocker locker(&pulseEngine->m_serverLock);
158
159 if (pulseEngine->m_defaultSink != info->default_sink_name) {
160 pulseEngine->m_defaultSink = info->default_sink_name;
161 defaultSinkChanged = true;
162 }
163
164 if (pulseEngine->m_defaultSource != info->default_source_name) {
165 pulseEngine->m_defaultSource = info->default_source_name;
166 defaultSourceChanged = true;
167 }
168 }
169
170 if (defaultSinkChanged
171 && updateDevicesMap(lock&: pulseEngine->m_sinkLock, defaultDeviceId: pulseEngine->m_defaultSink,
172 devices&: pulseEngine->m_sinks))
173 emit pulseEngine->audioOutputsChanged();
174
175 if (defaultSourceChanged
176 && updateDevicesMap(lock&: pulseEngine->m_sourceLock, defaultDeviceId: pulseEngine->m_defaultSource,
177 devices&: pulseEngine->m_sources))
178 emit pulseEngine->audioInputsChanged();
179
180 pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0);
181}
182
183void QPulseAudioContextManager::sinkInfoCallback(pa_context *context, const pa_sink_info *info,
184 int isLast, void *userdata)
185{
186 using namespace Qt::Literals;
187 using namespace QPulseAudioInternal;
188
189 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
190
191 if (isLast < 0) {
192 qWarning() << "Failed to get sink information:" << currentError(context);
193 return;
194 }
195
196 if (isLast) {
197 pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0);
198 return;
199 }
200
201 Q_ASSERT(info);
202
203 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
204 static const QFlatMap<pa_sink_state, QStringView> stateMap{
205 { PA_SINK_INVALID_STATE, u"n/a" }, { PA_SINK_RUNNING, u"RUNNING" },
206 { PA_SINK_IDLE, u"IDLE" }, { PA_SINK_SUSPENDED, u"SUSPENDED" },
207 { PA_SINK_UNLINKED, u"UNLINKED" },
208 };
209
210 qCDebug(qLcPulseAudioEngine)
211 << QStringLiteral("Sink #%1\n"
212 "\tState: %2\n"
213 "\tName: %3\n"
214 "\tDescription: %4\n")
215 .arg(args: QString::number(info->index), args: stateMap.value(key: info->state),
216 args: QString::fromUtf8(utf8: info->name),
217 args: QString::fromUtf8(utf8: info->description));
218 }
219
220 if (updateDevicesMap(lock&: pulseEngine->m_sinkLock, defaultDeviceId: pulseEngine->m_defaultSink, devices&: pulseEngine->m_sinks,
221 mode: QAudioDevice::Output, info: *info))
222 emit pulseEngine->audioOutputsChanged();
223}
224
225void QPulseAudioContextManager::sourceInfoCallback(pa_context *context, const pa_source_info *info,
226 int isLast, void *userdata)
227{
228 using namespace Qt::Literals;
229
230 Q_UNUSED(context);
231 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
232
233 if (isLast) {
234 pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0);
235 return;
236 }
237
238 Q_ASSERT(info);
239
240 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
241 static const QFlatMap<pa_source_state, QStringView> stateMap{
242 { PA_SOURCE_INVALID_STATE, u"n/a" }, { PA_SOURCE_RUNNING, u"RUNNING" },
243 { PA_SOURCE_IDLE, u"IDLE" }, { PA_SOURCE_SUSPENDED, u"SUSPENDED" },
244 { PA_SOURCE_UNLINKED, u"UNLINKED" },
245 };
246
247 qCDebug(qLcPulseAudioEngine)
248 << QStringLiteral("Source #%1\n"
249 "\tState: %2\n"
250 "\tName: %3\n"
251 "\tDescription: %4\n")
252 .arg(args: QString::number(info->index), args: stateMap.value(key: info->state),
253 args: QString::fromUtf8(utf8: info->name),
254 args: QString::fromUtf8(utf8: info->description));
255 }
256
257 // skip monitor channels
258 if (info->monitor_of_sink != PA_INVALID_INDEX)
259 return;
260
261 if (updateDevicesMap(lock&: pulseEngine->m_sourceLock, defaultDeviceId: pulseEngine->m_defaultSource,
262 devices&: pulseEngine->m_sources, mode: QAudioDevice::Input, info: *info))
263 emit pulseEngine->audioInputsChanged();
264}
265
266void QPulseAudioContextManager::eventCallback(pa_context *context, pa_subscription_event_type_t t,
267 uint32_t index, void *userdata)
268{
269 QPulseAudioContextManager *pulseEngine = static_cast<QPulseAudioContextManager *>(userdata);
270
271 int type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
272 int facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
273
274 switch (type) {
275 case PA_SUBSCRIPTION_EVENT_NEW:
276 case PA_SUBSCRIPTION_EVENT_CHANGE:
277 switch (facility) {
278 case PA_SUBSCRIPTION_EVENT_SERVER: {
279 PAOperationHandle op{
280 pa_context_get_server_info(c: context, cb: serverInfoCallback, userdata),
281 PAOperationHandle::HasRef,
282 };
283 if (!op)
284 qWarning() << "PulseAudioService: failed to get server info";
285 break;
286 }
287 case PA_SUBSCRIPTION_EVENT_SINK: {
288 PAOperationHandle op{
289 pa_context_get_sink_info_by_index(c: context, idx: index, cb: sinkInfoCallback, userdata),
290 PAOperationHandle::HasRef,
291 };
292
293 if (!op)
294 qWarning() << "PulseAudioService: failed to get sink info";
295 break;
296 }
297 case PA_SUBSCRIPTION_EVENT_SOURCE: {
298 PAOperationHandle op{
299 pa_context_get_source_info_by_index(c: context, idx: index, cb: sourceInfoCallback, userdata),
300 PAOperationHandle::HasRef,
301 };
302
303 if (!op)
304 qWarning() << "PulseAudioService: failed to get source info";
305 break;
306 }
307 default:
308 break;
309 }
310 break;
311 case PA_SUBSCRIPTION_EVENT_REMOVE:
312 switch (facility) {
313 case PA_SUBSCRIPTION_EVENT_SINK: {
314 QWriteLocker locker(&pulseEngine->m_sinkLock);
315 pulseEngine->m_sinks.remove(key: index);
316 break;
317 }
318 case PA_SUBSCRIPTION_EVENT_SOURCE: {
319 QWriteLocker locker(&pulseEngine->m_sourceLock);
320 pulseEngine->m_sources.remove(key: index);
321 break;
322 }
323 default:
324 break;
325 }
326 break;
327 default:
328 break;
329 }
330}
331
332void QPulseAudioContextManager::contextStateCallbackInit(pa_context *context, void *userdata)
333{
334 Q_UNUSED(context);
335
336 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
337 qCDebug(qLcPulseAudioEngine) << pa_context_get_state(c: context);
338
339 QPulseAudioContextManager *pulseEngine =
340 reinterpret_cast<QPulseAudioContextManager *>(userdata);
341 pa_threaded_mainloop_signal(m: pulseEngine->mainloop(), wait_for_accept: 0);
342}
343
344void QPulseAudioContextManager::contextStateCallback(pa_context *c, void *userdata)
345{
346 QPulseAudioContextManager *self = reinterpret_cast<QPulseAudioContextManager *>(userdata);
347 pa_context_state_t state = pa_context_get_state(c);
348
349 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
350 qCDebug(qLcPulseAudioEngine) << state;
351
352 if (state == PA_CONTEXT_FAILED)
353 QMetaObject::invokeMethod(object: self, function: &QPulseAudioContextManager::onContextFailed,
354 type: Qt::QueuedConnection);
355}
356
357Q_GLOBAL_STATIC(QPulseAudioContextManager, pulseEngine);
358
359QPulseAudioContextManager::QPulseAudioContextManager(QObject *parent) : QObject(parent)
360{
361 prepare();
362}
363
364QPulseAudioContextManager::~QPulseAudioContextManager()
365{
366 release();
367}
368
369void QPulseAudioContextManager::prepare()
370{
371 using namespace QPulseAudioInternal;
372 bool keepGoing = true;
373 bool ok = true;
374
375 m_mainLoop.reset(p: pa_threaded_mainloop_new());
376 if (m_mainLoop == nullptr) {
377 qWarning() << "PulseAudioService: unable to create pulseaudio mainloop";
378 return;
379 }
380
381 pa_threaded_mainloop_set_name(
382 m: m_mainLoop.get(), name: "QPulseAudioEngi"); // thread names are limited to 15 chars on linux
383
384 if (pa_threaded_mainloop_start(m: m_mainLoop.get()) != 0) {
385 qWarning() << "PulseAudioService: unable to start pulseaudio mainloop";
386 m_mainLoop = {};
387 return;
388 }
389
390 m_mainLoopApi = pa_threaded_mainloop_get_api(m: m_mainLoop.get());
391
392 std::unique_lock guard{ *this };
393
394 pa_proplist *proplist = pa_proplist_new();
395 if (!QGuiApplication::applicationDisplayName().isEmpty())
396 pa_proplist_sets(p: proplist, PA_PROP_APPLICATION_NAME,
397 qUtf8Printable(QGuiApplication::applicationDisplayName()));
398 if (!QGuiApplication::desktopFileName().isEmpty())
399 pa_proplist_sets(p: proplist, PA_PROP_APPLICATION_ID,
400 qUtf8Printable(QGuiApplication::desktopFileName()));
401 if (const QString windowIconName = QGuiApplication::windowIcon().name();
402 !windowIconName.isEmpty())
403 pa_proplist_sets(p: proplist, PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
404
405 m_context = PAContextHandle{
406 pa_context_new_with_proplist(mainloop: m_mainLoopApi, name: nullptr, proplist),
407 PAContextHandle::HasRef,
408 };
409 pa_proplist_free(p: proplist);
410
411 if (!m_context) {
412 qWarning() << "PulseAudioService: Unable to create new pulseaudio context";
413 pa_threaded_mainloop_unlock(m: m_mainLoop.get());
414 m_mainLoop = {};
415 onContextFailed();
416 return;
417 }
418
419 pa_context_set_state_callback(c: m_context.get(), cb: contextStateCallbackInit, userdata: this);
420
421 if (pa_context_connect(c: m_context.get(), server: nullptr, flags: static_cast<pa_context_flags_t>(0), api: nullptr)
422 < 0) {
423 qWarning() << "PulseAudioService: pa_context_connect() failed";
424 m_context = {};
425 guard.unlock();
426 m_mainLoop = {};
427 return;
428 }
429
430 pa_threaded_mainloop_wait(m: m_mainLoop.get());
431
432 while (keepGoing) {
433 switch (pa_context_get_state(c: m_context.get())) {
434 case PA_CONTEXT_CONNECTING:
435 case PA_CONTEXT_AUTHORIZING:
436 case PA_CONTEXT_SETTING_NAME:
437 break;
438
439 case PA_CONTEXT_READY:
440 qCDebug(qLcPulseAudioEngine) << "Connection established.";
441 keepGoing = false;
442 break;
443
444 case PA_CONTEXT_TERMINATED:
445 qCritical(msg: "PulseAudioService: Context terminated.");
446 keepGoing = false;
447 ok = false;
448 break;
449
450 case PA_CONTEXT_FAILED:
451 default:
452 qCritical() << "PulseAudioService: Connection failure:"
453 << currentError(m_context.get());
454 keepGoing = false;
455 ok = false;
456 }
457
458 if (keepGoing)
459 pa_threaded_mainloop_wait(m: m_mainLoop.get());
460 }
461
462 if (ok) {
463 pa_context_set_state_callback(c: m_context.get(), cb: contextStateCallback, userdata: this);
464
465 pa_context_set_subscribe_callback(c: m_context.get(), cb: eventCallback, userdata: this);
466 PAOperationHandle op{
467 pa_context_subscribe(c: m_context.get(),
468 m: pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK
469 | PA_SUBSCRIPTION_MASK_SOURCE
470 | PA_SUBSCRIPTION_MASK_SERVER),
471 cb: nullptr, userdata: nullptr),
472 PAOperationHandle::HasRef,
473 };
474
475 if (!op)
476 qWarning() << "PulseAudioService: failed to subscribe to context notifications";
477 } else {
478 m_context = {};
479 }
480
481 guard.unlock();
482
483 if (ok) {
484 updateDevices();
485 } else {
486 m_mainLoop = {};
487 onContextFailed();
488 }
489}
490
491void QPulseAudioContextManager::release()
492{
493 if (m_context) {
494 std::unique_lock lock{ *this };
495 pa_context_disconnect(c: m_context.get());
496 m_context = {};
497 }
498
499 if (m_mainLoop) {
500 pa_threaded_mainloop_stop(m: m_mainLoop.get());
501 m_mainLoop = {};
502 }
503}
504
505void QPulseAudioContextManager::updateDevices()
506{
507 std::lock_guard lock(*this);
508
509 // Get default input and output devices
510 PAOperationHandle operation{
511 pa_context_get_server_info(c: m_context.get(), cb: serverInfoCallback, userdata: this),
512 PAOperationHandle::HasRef,
513 };
514
515 if (operation)
516 wait(op: operation);
517 else
518 qWarning() << "PulseAudioService: failed to get server info";
519
520 // Get output devices
521 operation = PAOperationHandle{
522 pa_context_get_sink_info_list(c: m_context.get(), cb: sinkInfoCallback, userdata: this),
523 PAOperationHandle::HasRef,
524 };
525 if (operation)
526 wait(op: operation);
527 else
528 qWarning() << "PulseAudioService: failed to get sink info";
529
530 // Get input devices
531 operation = PAOperationHandle{
532 pa_context_get_source_info_list(c: m_context.get(), cb: sourceInfoCallback, userdata: this),
533 PAOperationHandle::HasRef,
534 };
535 if (operation)
536 wait(op: operation);
537 else
538 qWarning() << "PulseAudioService: failed to get source info";
539}
540
541void QPulseAudioContextManager::onContextFailed()
542{
543 // Give a chance to the connected slots to still use the Pulse main loop before releasing it.
544 emit contextFailed();
545
546 release();
547
548 // Try to reconnect later
549 QTimer::singleShot(interval: 3000, receiver: this, slot: &QPulseAudioContextManager::prepare);
550}
551
552QPulseAudioContextManager *QPulseAudioContextManager::instance()
553{
554 return pulseEngine();
555}
556
557QList<QAudioDevice> QPulseAudioContextManager::availableDevices(QAudioDevice::Mode mode) const
558{
559 if (mode == QAudioDevice::Output) {
560 QReadLocker locker(&m_sinkLock);
561 return m_sinks.values();
562 }
563
564 if (mode == QAudioDevice::Input) {
565 QReadLocker locker(&m_sourceLock);
566 return m_sources.values();
567 }
568
569 return {};
570}
571
572QByteArray QPulseAudioContextManager::defaultDevice(QAudioDevice::Mode mode) const
573{
574 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
575}
576
577QT_END_NAMESPACE
578

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