Qt 6.x
The Qt SDK
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 <QTimer>
10#include "qpulsehelpers_p.h"
11#include <sys/types.h>
12#include <unistd.h>
13#include <mutex> // for lock_guard
14
16
17template<typename Info>
18static bool updateDevicesMap(QReadWriteLock &lock, QByteArray defaultDeviceId,
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(info.channel_map);
27 newDeviceInfo->preferredFormat = QPulseAudioInternal::sampleSpecToAudioFormat(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,
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 *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("Failed to get server information: %s").arg(QString::fromUtf8(pa_strerror(pa_context_errno(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(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink,
113 pulseEngine->m_sinks))
114 emit pulseEngine->audioOutputsChanged();
115
116 if (defaultSourceChanged
117 && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
118 pulseEngine->m_sources))
119 emit pulseEngine->audioInputsChanged();
120
121 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 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("Failed to get sink information: %s").arg(QString::fromUtf8(pa_strerror(pa_context_errno(context))));
130 return;
131 }
132
133 if (isLast) {
134 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 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(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks,
159 emit pulseEngine->audioOutputsChanged();
160}
161
162static void sourceInfoCallback(pa_context *context, const pa_source_info *info, int isLast, void *userdata)
163{
165 QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata);
166
167 if (isLast) {
168 pa_threaded_mainloop_signal(pulseEngine->mainloop(), 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(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
196 pulseEngine->m_sources, QAudioDevice::Input, *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(context, serverInfoCallback, userdata));
213 if (!op)
214 qWarning("PulseAudioService: failed to get server info");
215 break;
216 }
217 case PA_SUBSCRIPTION_EVENT_SINK: {
219 pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata));
220 if (!op)
221 qWarning("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(context, index,
226 sourceInfoCallback, userdata));
227 if (!op)
228 qWarning("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(index);
240 break;
241 }
242 case PA_SUBSCRIPTION_EVENT_SOURCE: {
243 QWriteLocker locker(&pulseEngine->m_sourceLock);
244 pulseEngine->m_sources.remove(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{
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(pulseEngine->mainloop(), 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
273#endif
274
275 if (state == PA_CONTEXT_FAILED)
276 QMetaObject::invokeMethod(self, "onContextFailed", Qt::QueuedConnection);
277}
278
280
282 : QObject(parent)
283 , m_mainLoopApi(nullptr)
284 , m_context(nullptr)
285 , m_prepared(false)
286{
287 prepare();
288}
289
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("PulseAudioService: unable to create pulseaudio mainloop");
304 return;
305 }
306
307 if (pa_threaded_mainloop_start(m_mainLoop) != 0) {
308 qWarning("PulseAudioService: unable to start pulseaudio mainloop");
309 pa_threaded_mainloop_free(m_mainLoop);
310 m_mainLoop = nullptr;
311 return;
312 }
313
314 m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop);
315
316 lock();
317
318 m_context = pa_context_new(m_mainLoopApi, QString(QLatin1String("QtPulseAudio:%1")).arg(::getpid()).toLatin1().constData());
319
320 if (m_context == nullptr) {
321 qWarning("PulseAudioService: Unable to create new pulseaudio context");
322 pa_threaded_mainloop_unlock(m_mainLoop);
323 pa_threaded_mainloop_free(m_mainLoop);
324 m_mainLoop = nullptr;
325 onContextFailed();
326 return;
327 }
328
329 pa_context_set_state_callback(m_context, contextStateCallbackInit, this);
330
331 if (pa_context_connect(m_context, nullptr, (pa_context_flags_t)0, nullptr) < 0) {
332 qWarning("PulseAudioService: pa_context_connect() failed");
333 pa_context_unref(m_context);
334 pa_threaded_mainloop_unlock(m_mainLoop);
335 pa_threaded_mainloop_free(m_mainLoop);
336 m_mainLoop = nullptr;
337 m_context = nullptr;
338 return;
339 }
340
341 pa_threaded_mainloop_wait(m_mainLoop);
342
343 while (keepGoing) {
344 switch (pa_context_get_state(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("PulseAudioService: Context terminated.");
359 keepGoing = false;
360 ok = false;
361 break;
362
363 case PA_CONTEXT_FAILED:
364 default:
365 qCritical() << QString::fromLatin1("PulseAudioService: Connection failure: %1")
366 .arg(QString::fromUtf8(pa_strerror(pa_context_errno(m_context))));
367 keepGoing = false;
368 ok = false;
369 }
370
371 if (keepGoing)
372 pa_threaded_mainloop_wait(m_mainLoop);
373 }
374
375 if (ok) {
376 pa_context_set_state_callback(m_context, contextStateCallback, this);
377
378 pa_context_set_subscribe_callback(m_context, event_cb, this);
379 PAOperationUPtr op(pa_context_subscribe(
380 m_context,
381 pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE
382 | PA_SUBSCRIPTION_MASK_SERVER),
383 nullptr, nullptr));
384 if (!op)
385 qWarning("PulseAudioService: failed to subscribe to context notifications");
386 } else {
387 pa_context_unref(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_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(m_context);
410 pa_context_unref(m_context);
411 m_context = nullptr;
412 }
413
414 if (m_mainLoop) {
415 pa_threaded_mainloop_stop(m_mainLoop);
416 pa_threaded_mainloop_free(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(m_context, serverInfoCallback, this));
429 if (operation) {
430 while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING)
431 pa_threaded_mainloop_wait(m_mainLoop);
432 } else {
433 qWarning("PulseAudioService: failed to get server info");
434 }
435
436 // Get output devices
437 operation.reset(pa_context_get_sink_info_list(m_context, sinkInfoCallback, this));
438 if (operation) {
439 while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING)
440 pa_threaded_mainloop_wait(m_mainLoop);
441 } else {
442 qWarning("PulseAudioService: failed to get sink info");
443 }
444
445 // Get input devices
446 operation.reset(pa_context_get_source_info_list(m_context, sourceInfoCallback, this));
447 if (operation) {
448 while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING)
449 pa_threaded_mainloop_wait(m_mainLoop);
450 } else {
451 qWarning("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.
459
460 release();
461
462 // Try to reconnect later
463 QTimer::singleShot(3000, this, SLOT(prepare()));
464}
465
467{
468 return pulseEngine();
469}
470
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
487{
489}
490
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
Definition qlist.h:74
Definition qmap.h:186
QList< T > values() const
Definition qmap.h:396
size_type remove(const Key &key)
Definition qmap.h:299
\inmodule QtCore
Definition qobject.h:90
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:127
static QString fromLatin1(QByteArrayView ba)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:5710
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:5857
QString arg(qlonglong a, int fieldwidth=0, int base=10, QChar fillChar=u' ') const
Definition qstring.cpp:8606
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:7822
bool singleShot
whether the timer is a single-shot timer
Definition qtimer.h:22
\inmodule QtCore
else opt state
[0]
QAudioFormat::ChannelConfig channelConfigFromMap(const pa_channel_map &map)
QAudioFormat sampleSpecToAudioFormat(const pa_sample_spec &spec)
static QString stateToQString(pa_stream_state_t state)
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)
EGLDeviceEXT * devices
#define Q_GLOBAL_STATIC(TYPE, NAME,...)
#define qCritical
Definition qlogging.h:163
#define qDebug
[1]
Definition qlogging.h:160
#define qWarning
Definition qlogging.h:162
#define SLOT(a)
Definition qobjectdefs.h:51
GLenum mode
GLuint index
[2]
GLenum type
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
SSL_CTX int(*) void arg)
QLatin1StringView QLatin1String
Definition qstringfwd.h:31
#define emit
#define Q_UNUSED(x)
QFileInfo info(fileName)
[8]
QObject::connect nullptr
QReadWriteLock lock
[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...
IUIAutomationTreeWalker __RPC__deref_out_opt IUIAutomationElement ** parent