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