Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qaudioengine_pulse.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
4#include <QtCore/qdebug.h>
5
6#include <qaudiodevice.h>
7#include <QGuiApplication>
8#include <QIcon>
9#include <QTimer>
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
18
19template<typename Info>
20static 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(info.channel_map);
29 newDeviceInfo->preferredFormat = QPulseAudioInternal::sampleSpecToAudioFormat(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
40static 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 *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
63static 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(ss, sizeof(ss), &info->sample_spec);
77 pa_channel_map_snprint(cm, sizeof(cm), &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(QString::fromUtf8(info->user_name),
89 QString::fromUtf8(info->host_name),
90 QString::fromUtf8(info->server_name),
91 QLatin1StringView(info->server_version), QLatin1StringView(ss),
92 QLatin1StringView(cm), QString::fromUtf8(info->default_sink_name),
93 QString::fromUtf8(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(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink,
117 pulseEngine->m_sinks))
118 emit pulseEngine->audioOutputsChanged();
119
120 if (defaultSourceChanged
121 && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
122 pulseEngine->m_sources))
123 emit pulseEngine->audioInputsChanged();
124
125 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
126}
127
128static 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(pulseEngine->mainloop(), 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(QString::number(info->index), stateMap.value(info->state),
160 QString::fromUtf8(info->name),
161 QString::fromUtf8(info->description));
162 }
163
164 if (updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks,
166 emit pulseEngine->audioOutputsChanged();
167}
168
169static void sourceInfoCallback(pa_context *context, const pa_source_info *info, int isLast, void *userdata)
170{
171 using namespace Qt::Literals;
172
174 QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata);
175
176 if (isLast) {
177 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 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(QString::number(info->index), stateMap.value(info->state),
196 QString::fromUtf8(info->name),
197 QString::fromUtf8(info->description));
198 }
199
200 // skip monitor channels
201 if (info->monitor_of_sink != PA_INVALID_INDEX)
202 return;
203
204 if (updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
205 pulseEngine->m_sources, QAudioDevice::Input, *info))
206 emit pulseEngine->audioInputsChanged();
207}
208
209static 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(context, serverInfoCallback, userdata));
222 if (!op)
223 qWarning() << "PulseAudioService: failed to get server info";
224 break;
225 }
226 case PA_SUBSCRIPTION_EVENT_SINK: {
228 pa_context_get_sink_info_by_index(context, index, 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(context, index,
235 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(index);
249 break;
250 }
251 case PA_SUBSCRIPTION_EVENT_SOURCE: {
252 QWriteLocker locker(&pulseEngine->m_sourceLock);
253 pulseEngine->m_sources.remove(index);
254 break;
255 }
256 default:
257 break;
258 }
259 break;
260 default:
261 break;
262 }
263}
264
265static void contextStateCallbackInit(pa_context *context, void *userdata)
266{
268
269 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
270 qCDebug(qLcPulseAudioEngine) << pa_context_get_state(context);
271
272 QPulseAudioEngine *pulseEngine = reinterpret_cast<QPulseAudioEngine*>(userdata);
273 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
274}
275
276static 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(self, "onContextFailed", Qt::QueuedConnection);
286}
287
289
291 : QObject(parent)
292 , m_mainLoopApi(nullptr)
293 , m_context(nullptr)
294 , m_prepared(false)
295{
296 prepare();
297}
298
300{
301 if (m_prepared)
302 release();
303}
304
305void 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 if (pa_threaded_mainloop_start(m_mainLoop) != 0) {
318 qWarning() << "PulseAudioService: unable to start pulseaudio mainloop";
319 pa_threaded_mainloop_free(m_mainLoop);
320 m_mainLoop = nullptr;
321 return;
322 }
323
324 m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop);
325
326 lock();
327
328 pa_proplist *proplist = pa_proplist_new();
330 pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, qUtf8Printable(QGuiApplication::applicationDisplayName()));
331 if (!QGuiApplication::desktopFileName().isEmpty())
332 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, qUtf8Printable(QGuiApplication::desktopFileName()));
333 if (const QString windowIconName = QGuiApplication::windowIcon().name(); !windowIconName.isEmpty())
334 pa_proplist_sets(proplist, PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
335
336 m_context = pa_context_new_with_proplist(m_mainLoopApi, nullptr, proplist);
337 pa_proplist_free(proplist);
338
339 if (m_context == nullptr) {
340 qWarning() << "PulseAudioService: Unable to create new pulseaudio context";
341 pa_threaded_mainloop_unlock(m_mainLoop);
342 pa_threaded_mainloop_free(m_mainLoop);
343 m_mainLoop = nullptr;
344 onContextFailed();
345 return;
346 }
347
348 pa_context_set_state_callback(m_context, contextStateCallbackInit, this);
349
350 if (pa_context_connect(m_context, nullptr, static_cast<pa_context_flags_t>(0), nullptr) < 0) {
351 qWarning() << "PulseAudioService: pa_context_connect() failed";
352 pa_context_unref(m_context);
353 pa_threaded_mainloop_unlock(m_mainLoop);
354 pa_threaded_mainloop_free(m_mainLoop);
355 m_mainLoop = nullptr;
356 m_context = nullptr;
357 return;
358 }
359
360 pa_threaded_mainloop_wait(m_mainLoop);
361
362 while (keepGoing) {
363 switch (pa_context_get_state(m_context)) {
364 case PA_CONTEXT_CONNECTING:
365 case PA_CONTEXT_AUTHORIZING:
366 case PA_CONTEXT_SETTING_NAME:
367 break;
368
369 case PA_CONTEXT_READY:
370 qCDebug(qLcPulseAudioEngine) << "Connection established.";
371 keepGoing = false;
372 break;
373
374 case PA_CONTEXT_TERMINATED:
375 qCritical("PulseAudioService: Context terminated.");
376 keepGoing = false;
377 ok = false;
378 break;
379
380 case PA_CONTEXT_FAILED:
381 default:
382 qCritical() << "PulseAudioService: Connection failure:"
383 << currentError(m_context);
384 keepGoing = false;
385 ok = false;
386 }
387
388 if (keepGoing)
389 pa_threaded_mainloop_wait(m_mainLoop);
390 }
391
392 if (ok) {
393 pa_context_set_state_callback(m_context, contextStateCallback, this);
394
395 pa_context_set_subscribe_callback(m_context, event_cb, this);
396 PAOperationUPtr op(pa_context_subscribe(
397 m_context,
398 pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE
399 | PA_SUBSCRIPTION_MASK_SERVER),
400 nullptr, nullptr));
401 if (!op)
402 qWarning() << "PulseAudioService: failed to subscribe to context notifications";
403 } else {
404 pa_context_unref(m_context);
405 m_context = nullptr;
406 }
407
408 unlock();
409
410 if (ok) {
411 updateDevices();
412 m_prepared = true;
413 } else {
414 pa_threaded_mainloop_free(m_mainLoop);
415 m_mainLoop = nullptr;
416 onContextFailed();
417 }
418}
419
420void QPulseAudioEngine::release()
421{
422 if (!m_prepared)
423 return;
424
425 if (m_context) {
426 pa_context_disconnect(m_context);
427 pa_context_unref(m_context);
428 m_context = nullptr;
429 }
430
431 if (m_mainLoop) {
432 pa_threaded_mainloop_stop(m_mainLoop);
433 pa_threaded_mainloop_free(m_mainLoop);
434 m_mainLoop = nullptr;
435 }
436
437 m_prepared = false;
438}
439
440void QPulseAudioEngine::updateDevices()
441{
442 std::lock_guard lock(*this);
443
444 // Get default input and output devices
445 PAOperationUPtr operation(pa_context_get_server_info(m_context, serverInfoCallback, this));
446 if (operation) {
447 while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING)
448 pa_threaded_mainloop_wait(m_mainLoop);
449 } else {
450 qWarning() << "PulseAudioService: failed to get server info";
451 }
452
453 // Get output devices
454 operation.reset(pa_context_get_sink_info_list(m_context, sinkInfoCallback, this));
455 if (operation) {
456 while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING)
457 pa_threaded_mainloop_wait(m_mainLoop);
458 } else {
459 qWarning() << "PulseAudioService: failed to get sink info";
460 }
461
462 // Get input devices
463 operation.reset(pa_context_get_source_info_list(m_context, sourceInfoCallback, this));
464 if (operation) {
465 while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING)
466 pa_threaded_mainloop_wait(m_mainLoop);
467 } else {
468 qWarning() << "PulseAudioService: failed to get source info";
469 }
470}
471
472void QPulseAudioEngine::onContextFailed()
473{
474 // Give a chance to the connected slots to still use the Pulse main loop before releasing it.
476
477 release();
478
479 // Try to reconnect later
480 QTimer::singleShot(3000, this, &QPulseAudioEngine::prepare);
481}
482
484{
485 return pulseEngine();
486}
487
489{
490 if (mode == QAudioDevice::Output) {
491 QReadLocker locker(&m_sinkLock);
492 return m_sinks.values();
493 }
494
495 if (mode == QAudioDevice::Input) {
496 QReadLocker locker(&m_sourceLock);
497 return m_sources.values();
498 }
499
500 return {};
501}
502
507
IOBluetoothDevice * device
The QAudioDevice class provides an information about audio devices and their functionality.
Mode
Describes the mode of this device.
\inmodule QtCore
Definition qbytearray.h:57
QString applicationDisplayName
the user-visible name of this application
QString desktopFileName
the base name of the desktop entry for this application
QIcon windowIcon
the default window icon
QList< T > values() const
Definition qmap.h:397
size_type remove(const Key &key)
Definition qmap.h:300
\inmodule QtCore
Definition qobject.h:103
QReadWriteLock m_sinkLock
QMap< int, QAudioDevice > m_sinks
static QPulseAudioEngine * instance()
QPulseAudioEngine(QObject *parent=0)
pa_threaded_mainloop * mainloop()
QByteArray defaultDevice(QAudioDevice::Mode mode) const
void audioOutputsChanged()
QMap< int, QAudioDevice > m_sources
QList< QAudioDevice > availableDevices(QAudioDevice::Mode mode) const
QReadWriteLock m_sourceLock
void audioInputsChanged()
QReadWriteLock m_serverLock
\inmodule QtCore
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:6018
static QString number(int, int base=10)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:8084
bool singleShot
whether the timer is a single-shot timer
Definition qtimer.h:22
\inmodule QtCore
else opt state
[0]
QUtf8StringView currentError(const pa_context *context)
QAudioFormat::ChannelConfig channelConfigFromMap(const pa_channel_map &map)
QAudioFormat sampleSpecToAudioFormat(const pa_sample_spec &spec)
Combined button and popup list for selecting options.
@ QueuedConnection
static void * context
static void contextStateCallback(pa_context *c, void *userdata)
static QT_BEGIN_NAMESPACE bool updateDevicesMap(QReadWriteLock &lock, QByteArray defaultDeviceId, QMap< int, QAudioDevice > &devices, QAudioDevice::Mode mode, const Info &info)
static void serverInfoCallback(pa_context *context, const pa_server_info *info, void *userdata)
static void event_cb(pa_context *context, pa_subscription_event_type_t t, uint32_t index, void *userdata)
static void sourceInfoCallback(pa_context *context, const pa_source_info *info, int isLast, void *userdata)
static void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int isLast, void *userdata)
static void contextStateCallbackInit(pa_context *context, void *userdata)
#define Q_UNLIKELY(x)
EGLDeviceEXT * devices
#define Q_GLOBAL_STATIC(TYPE, NAME,...)
#define qCritical
Definition qlogging.h:167
@ QtDebugMsg
Definition qlogging.h:30
#define qWarning
Definition qlogging.h:166
#define qCDebug(category,...)
GLenum mode
GLuint index
[2]
GLenum type
GLuint name
const GLubyte * c
GLdouble GLdouble t
Definition qopenglext.h:243
GLuint64EXT * result
[6]
std::unique_ptr< pa_operation, PAOperationDeleter > PAOperationUPtr
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define qUtf8Printable(string)
Definition qstring.h:1535
#define QStringLiteral(str)
#define emit
#define Q_UNUSED(x)
QObject::connect nullptr
QReadWriteLock lock
[0]
QHostInfo info
[0]
static bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType, QGenericReturnArgument ret, QGenericArgument val0=QGenericArgument(nullptr), QGenericArgument val1=QGenericArgument(), QGenericArgument val2=QGenericArgument(), QGenericArgument val3=QGenericArgument(), QGenericArgument val4=QGenericArgument(), QGenericArgument val5=QGenericArgument(), QGenericArgument val6=QGenericArgument(), QGenericArgument val7=QGenericArgument(), QGenericArgument val8=QGenericArgument(), QGenericArgument val9=QGenericArgument())
\threadsafe This is an overloaded member function, provided for convenience. It differs from the abov...