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 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | using PAOperationHandle = QPulseAudioInternal::PAOperationHandle; |
23 | |
24 | static std::unique_ptr<QAudioDevicePrivate> |
25 | makeQAudioDevicePrivate(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 | |
76 | template<typename Info> |
77 | static 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 | |
96 | static 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 | |
117 | void 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 | |
183 | void 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 | |
225 | void 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 | |
266 | void 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 | |
332 | void 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 | |
344 | void 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 | |
357 | Q_GLOBAL_STATIC(QPulseAudioContextManager, pulseEngine); |
358 | |
359 | QPulseAudioContextManager::QPulseAudioContextManager(QObject *parent) : QObject(parent) |
360 | { |
361 | prepare(); |
362 | } |
363 | |
364 | QPulseAudioContextManager::~QPulseAudioContextManager() |
365 | { |
366 | release(); |
367 | } |
368 | |
369 | void 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 | |
491 | void 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 | |
505 | void 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 | |
541 | void 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 | |
552 | QPulseAudioContextManager *QPulseAudioContextManager::instance() |
553 | { |
554 | return pulseEngine(); |
555 | } |
556 | |
557 | QList<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 | |
572 | QByteArray QPulseAudioContextManager::defaultDevice(QAudioDevice::Mode mode) const |
573 | { |
574 | return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource; |
575 | } |
576 | |
577 | QT_END_NAMESPACE |
578 | |