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