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
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<QAudioDevicePrivate>(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 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>(*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(ss, sizeof(ss), &info->sample_spec);
132 pa_channel_map_snprint(cm, sizeof(cm), &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(QString::fromUtf8(info->user_name),
144 QString::fromUtf8(info->host_name),
145 QString::fromUtf8(info->server_name),
146 QLatin1StringView(info->server_version), QLatin1StringView(ss),
147 QLatin1StringView(cm), QString::fromUtf8(info->default_sink_name),
148 QString::fromUtf8(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(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink,
172 pulseEngine->m_sinks))
173 emit pulseEngine->audioOutputsChanged();
174
175 if (defaultSourceChanged
176 && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
177 pulseEngine->m_sources))
178 emit pulseEngine->audioInputsChanged();
179
180 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 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(pulseEngine->mainloop(), 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(QString::number(info->index), stateMap.value(info->state),
216 QString::fromUtf8(info->name),
217 QString::fromUtf8(info->description));
218 }
219
220 if (updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks,
221 QAudioDevice::Output, *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(pulseEngine->mainloop(), 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(QString::number(info->index), stateMap.value(info->state),
253 QString::fromUtf8(info->name),
254 QString::fromUtf8(info->description));
255 }
256
257 // skip monitor channels
258 if (info->monitor_of_sink != PA_INVALID_INDEX)
259 return;
260
261 if (updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
262 pulseEngine->m_sources, QAudioDevice::Input, *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(context, 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(context, index, 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(context, index, 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(index);
316 break;
317 }
318 case PA_SUBSCRIPTION_EVENT_SOURCE: {
319 QWriteLocker locker(&pulseEngine->m_sourceLock);
320 pulseEngine->m_sources.remove(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(context);
338
339 QPulseAudioContextManager *pulseEngine =
340 reinterpret_cast<QPulseAudioContextManager *>(userdata);
341 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 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(self, &QPulseAudioContextManager::onContextFailed,
354 Qt::QueuedConnection);
355}
356
358
360{
361 prepare();
362}
363
368
369void QPulseAudioContextManager::prepare()
370{
371 using namespace QPulseAudioInternal;
372 bool keepGoing = true;
373 bool ok = true;
374
375 m_mainLoop.reset(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_mainLoop.get(), "QPulseAudioEngi"); // thread names are limited to 15 chars on linux
383
384 if (pa_threaded_mainloop_start(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_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(proplist, PA_PROP_APPLICATION_NAME,
397 qUtf8Printable(QGuiApplication::applicationDisplayName()));
398 if (!QGuiApplication::desktopFileName().isEmpty())
399 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID,
400 qUtf8Printable(QGuiApplication::desktopFileName()));
401 if (const QString windowIconName = QGuiApplication::windowIcon().name();
402 !windowIconName.isEmpty())
403 pa_proplist_sets(proplist, PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
404
405 m_context = PAContextHandle{
406 pa_context_new_with_proplist(m_mainLoopApi, nullptr, proplist),
407 PAContextHandle::HasRef,
408 };
409 pa_proplist_free(proplist);
410
411 if (!m_context) {
412 qWarning() << "PulseAudioService: Unable to create new pulseaudio context";
413 pa_threaded_mainloop_unlock(m_mainLoop.get());
414 m_mainLoop = {};
415 onContextFailed();
416 return;
417 }
418
419 pa_context_set_state_callback(m_context.get(), contextStateCallbackInit, this);
420
421 if (pa_context_connect(m_context.get(), nullptr, static_cast<pa_context_flags_t>(0), 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_mainLoop.get());
431
432 while (keepGoing) {
433 switch (pa_context_get_state(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("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_mainLoop.get());
460 }
461
462 if (ok) {
463 pa_context_set_state_callback(m_context.get(), contextStateCallback, this);
464
465 pa_context_set_subscribe_callback(m_context.get(), eventCallback, this);
466 PAOperationHandle op{
467 pa_context_subscribe(m_context.get(),
468 pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK
469 | PA_SUBSCRIPTION_MASK_SOURCE
470 | PA_SUBSCRIPTION_MASK_SERVER),
471 nullptr, 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(m_context.get());
496 m_context = {};
497 }
498
499 if (m_mainLoop) {
500 pa_threaded_mainloop_stop(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(m_context.get(), serverInfoCallback, this),
512 PAOperationHandle::HasRef,
513 };
514
515 if (operation)
516 wait(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(m_context.get(), sinkInfoCallback, this),
523 PAOperationHandle::HasRef,
524 };
525 if (operation)
526 wait(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(m_context.get(), sourceInfoCallback, this),
533 PAOperationHandle::HasRef,
534 };
535 if (operation)
536 wait(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(3000, this, &QPulseAudioContextManager::prepare);
550}
551
553{
554 return pulseEngine();
555}
556
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
573{
574 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
575}
576
577QT_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()
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)