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
15QT_BEGIN_NAMESPACE
16
17template<typename Info>
18static 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
38static 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
61static 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
124static 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
162static 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
200static 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
256static 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
266static 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
279Q_GLOBAL_STATIC(QPulseAudioEngine, pulseEngine);
280
281QPulseAudioEngine::QPulseAudioEngine(QObject *parent)
282 : QObject(parent)
283 , m_mainLoopApi(nullptr)
284 , m_context(nullptr)
285 , m_prepared(false)
286{
287 prepare();
288}
289
290QPulseAudioEngine::~QPulseAudioEngine()
291{
292 if (m_prepared)
293 release();
294}
295
296void 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
403void 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
423void 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
455void 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
466QPulseAudioEngine *QPulseAudioEngine::instance()
467{
468 return pulseEngine();
469}
470
471QList<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
486QByteArray QPulseAudioEngine::defaultDevice(QAudioDevice::Mode mode) const
487{
488 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
489}
490
491QT_END_NAMESPACE
492

source code of qtmultimedia/src/multimedia/pulseaudio/qaudioengine_pulse.cpp