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