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>
22using PAOperationHandle = QPulseAudioInternal::PAOperationHandle;
26 const pa_channel_map &map,
const pa_sample_spec &spec)
28 using namespace QPulseAudioInternal;
30 auto deviceInfo = std::make_unique<QAudioDevicePrivate>(device, mode, QString::fromUtf8(desc));
33 deviceInfo->isDefault = isDef;
34 deviceInfo->channelConfiguration = channelConfig;
36 deviceInfo->minimumChannelCount = 1;
37 deviceInfo->maximumChannelCount = PA_CHANNELS_MAX;
38 deviceInfo->minimumSampleRate = 1;
39 deviceInfo->maximumSampleRate = PA_RATE_MAX;
41 constexpr bool isBigEndian = QSysInfo::ByteOrder == QSysInfo::BigEndian;
45 pa_sample_format pa_fmt;
54 for (
const auto &f : formatMap) {
55 if (pa_sample_format_valid(f.pa_fmt) != 0)
56 deviceInfo->supportedSampleFormats.append(f.qt_fmt);
59 QAudioFormat preferredFormat = sampleSpecToAudioFormat(spec);
64 Q_ASSERT(spec.format != PA_SAMPLE_INVALID);
65 if (!deviceInfo->supportedSampleFormats.contains(preferredFormat
.sampleFormat()))
69 deviceInfo->preferredFormat = preferredFormat;
70 deviceInfo->preferredFormat.setChannelConfig(channelConfig);
71 Q_ASSERT(deviceInfo->preferredFormat.isValid());
76template<
typename Info>
78 QMap<
int, QAudioDevice> &devices, QAudioDevice::Mode mode,
81 QWriteLocker locker(&lock);
83 bool isDefault = defaultDeviceId == info.name;
84 auto newDeviceInfo = makeQAudioDevicePrivate(info.name, info.description, isDefault, mode,
85 info.channel_map, info.sample_spec);
87 auto &device = devices[info.index];
88 QAudioDevicePrivateAllMembersEqual compare;
89 if (device.handle() && compare(*newDeviceInfo, *device.handle()))
92 device = newDeviceInfo.release()->create();
97 QMap<
int, QAudioDevice> &devices)
99 QWriteLocker locker(&lock);
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>(*deviceInfo);
108 newDeviceInfo->isDefault = isDefault;
109 device = newDeviceInfo.release()->create();
120 using namespace Qt::Literals;
121 using namespace QPulseAudioInternal;
124 qWarning() <<
"Failed to get server information:" << currentError(context);
128 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
129 char ss[PA_SAMPLE_SPEC_SNPRINT_MAX], cm[PA_CHANNEL_MAP_SNPRINT_MAX];
131 pa_sample_spec_snprint(ss,
sizeof(ss), &info->sample_spec);
132 pa_channel_map_snprint(cm,
sizeof(cm), &info->channel_map);
134 qCDebug(qLcPulseAudioEngine)
135 << QStringLiteral(
"User name: %1\n"
138 "Server Version: %4\n"
139 "Default Sample Specification: %5\n"
140 "Default Channel Map: %6\n"
142 "Default Source: %8\n")
143 .arg(QString::fromUtf8(info->user_name),
144 QString::fromUtf8(info->host_name),
145 QString::fromUtf8(info->server_name),
146 QLatin1StringView(info->server_version), QLatin1StringView(ss),
147 QLatin1StringView(cm), QString::fromUtf8(info->default_sink_name),
148 QString::fromUtf8(info->default_source_name));
153 bool defaultSinkChanged =
false;
154 bool defaultSourceChanged =
false;
157 QWriteLocker locker(&pulseEngine->m_serverLock);
159 if (pulseEngine->m_defaultSink != info->default_sink_name) {
160 pulseEngine->m_defaultSink = info->default_sink_name;
161 defaultSinkChanged =
true;
164 if (pulseEngine->m_defaultSource != info->default_source_name) {
165 pulseEngine->m_defaultSource = info->default_source_name;
166 defaultSourceChanged =
true;
170 if (defaultSinkChanged
171 && updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink,
172 pulseEngine->m_sinks))
173 emit pulseEngine->audioOutputsChanged();
175 if (defaultSourceChanged
176 && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
177 pulseEngine->m_sources))
178 emit pulseEngine->audioInputsChanged();
180 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
184 int isLast,
void *userdata)
186 using namespace Qt::Literals;
187 using namespace QPulseAudioInternal;
192 qWarning() <<
"Failed to get sink information:" << currentError(context);
197 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
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" },
210 qCDebug(qLcPulseAudioEngine)
211 << QStringLiteral(
"Sink #%1\n"
214 "\tDescription: %4\n")
215 .arg(QString::number(info->index), stateMap.value(info->state),
216 QString::fromUtf8(info->name),
217 QString::fromUtf8(info->description));
220 if (updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks,
221 QAudioDevice::Output, *info))
222 emit pulseEngine->audioOutputsChanged();
226 int isLast,
void *userdata)
228 using namespace Qt::Literals;
234 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
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" },
247 qCDebug(qLcPulseAudioEngine)
248 << QStringLiteral(
"Source #%1\n"
251 "\tDescription: %4\n")
252 .arg(QString::number(info->index), stateMap.value(info->state),
253 QString::fromUtf8(info->name),
254 QString::fromUtf8(info->description));
258 if (info->monitor_of_sink != PA_INVALID_INDEX)
261 if (updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
262 pulseEngine->m_sources, QAudioDevice::Input, *info))
263 emit pulseEngine->audioInputsChanged();
267 uint32_t index,
void *userdata)
271 int type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
272 int facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
275 case PA_SUBSCRIPTION_EVENT_NEW:
276 case PA_SUBSCRIPTION_EVENT_CHANGE:
278 case PA_SUBSCRIPTION_EVENT_SERVER: {
279 PAOperationHandle op{
280 pa_context_get_server_info(context, serverInfoCallback, userdata),
281 PAOperationHandle::HasRef,
284 qWarning() <<
"PulseAudioService: failed to get server info";
287 case PA_SUBSCRIPTION_EVENT_SINK: {
288 PAOperationHandle op{
289 pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata),
290 PAOperationHandle::HasRef,
294 qWarning() <<
"PulseAudioService: failed to get sink info";
297 case PA_SUBSCRIPTION_EVENT_SOURCE: {
298 PAOperationHandle op{
299 pa_context_get_source_info_by_index(context, index, sourceInfoCallback, userdata),
300 PAOperationHandle::HasRef,
304 qWarning() <<
"PulseAudioService: failed to get source info";
311 case PA_SUBSCRIPTION_EVENT_REMOVE:
313 case PA_SUBSCRIPTION_EVENT_SINK: {
314 QWriteLocker locker(&pulseEngine->m_sinkLock);
315 pulseEngine->m_sinks.remove(index);
318 case PA_SUBSCRIPTION_EVENT_SOURCE: {
319 QWriteLocker locker(&pulseEngine->m_sourceLock);
320 pulseEngine->m_sources.remove(index);
336 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
337 qCDebug(qLcPulseAudioEngine) << pa_context_get_state(context);
341 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
347 pa_context_state_t state = pa_context_get_state(c);
349 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
350 qCDebug(qLcPulseAudioEngine) << state;
352 if (state == PA_CONTEXT_FAILED)
353 QMetaObject::invokeMethod(self, &QPulseAudioContextManager::onContextFailed,
354 Qt::QueuedConnection);
371 using namespace QPulseAudioInternal;
372 bool keepGoing =
true;
375 m_mainLoop.reset(pa_threaded_mainloop_new());
376 if (m_mainLoop ==
nullptr) {
377 qWarning() <<
"PulseAudioService: unable to create pulseaudio mainloop";
381 pa_threaded_mainloop_set_name(
382 m_mainLoop.get(),
"QPulseAudioEngi");
384 if (pa_threaded_mainloop_start(m_mainLoop.get()) != 0) {
385 qWarning() <<
"PulseAudioService: unable to start pulseaudio mainloop";
390 m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop.get());
392 std::unique_lock guard{ *
this };
394 pa_proplist *proplist = pa_proplist_new();
395 if (!QGuiApplication::applicationDisplayName().isEmpty())
396 pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME,
397 qUtf8Printable(QGuiApplication::applicationDisplayName()));
398 if (!QGuiApplication::desktopFileName().isEmpty())
399 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID,
400 qUtf8Printable(QGuiApplication::desktopFileName()));
401 if (
const QString windowIconName = QGuiApplication::windowIcon().name();
402 !windowIconName.isEmpty())
403 pa_proplist_sets(proplist, PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
405 m_context = PAContextHandle{
406 pa_context_new_with_proplist(m_mainLoopApi,
nullptr, proplist),
407 PAContextHandle::HasRef,
409 pa_proplist_free(proplist);
412 qWarning() <<
"PulseAudioService: Unable to create new pulseaudio context";
413 pa_threaded_mainloop_unlock(m_mainLoop.get());
419 pa_context_set_state_callback(m_context.get(), contextStateCallbackInit,
this);
421 if (pa_context_connect(m_context.get(),
nullptr,
static_cast<pa_context_flags_t>(0),
nullptr)
423 qWarning() <<
"PulseAudioService: pa_context_connect() failed";
430 pa_threaded_mainloop_wait(m_mainLoop.get());
433 switch (pa_context_get_state(m_context.get())) {
434 case PA_CONTEXT_CONNECTING:
435 case PA_CONTEXT_AUTHORIZING:
436 case PA_CONTEXT_SETTING_NAME:
439 case PA_CONTEXT_READY:
440 qCDebug(qLcPulseAudioEngine) <<
"Connection established.";
444 case PA_CONTEXT_TERMINATED:
445 qCritical(
"PulseAudioService: Context terminated.");
450 case PA_CONTEXT_FAILED:
452 qCritical() <<
"PulseAudioService: Connection failure:"
453 << currentError(m_context.get());
459 pa_threaded_mainloop_wait(m_mainLoop.get());
463 pa_context_set_state_callback(m_context.get(), contextStateCallback,
this);
465 pa_context_set_subscribe_callback(m_context.get(), eventCallback,
this);
466 PAOperationHandle op{
467 pa_context_subscribe(m_context.get(),
468 pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK
469 | PA_SUBSCRIPTION_MASK_SOURCE
470 | PA_SUBSCRIPTION_MASK_SERVER),
472 PAOperationHandle::HasRef,
476 qWarning() <<
"PulseAudioService: failed to subscribe to context notifications";
494 std::unique_lock lock{ *
this };
495 pa_context_disconnect(m_context.get());
500 pa_threaded_mainloop_stop(m_mainLoop.get());
507 std::lock_guard lock(*
this);
510 PAOperationHandle operation{
511 pa_context_get_server_info(m_context.get(), serverInfoCallback,
this),
512 PAOperationHandle::HasRef,
518 qWarning() <<
"PulseAudioService: failed to get server info";
521 operation = PAOperationHandle{
522 pa_context_get_sink_info_list(m_context.get(), sinkInfoCallback,
this),
523 PAOperationHandle::HasRef,
528 qWarning() <<
"PulseAudioService: failed to get sink info";
531 operation = PAOperationHandle{
532 pa_context_get_source_info_list(m_context.get(), sourceInfoCallback,
this),
533 PAOperationHandle::HasRef,
538 qWarning() <<
"PulseAudioService: failed to get source info";
544 emit contextFailed();
549 QTimer::singleShot(3000,
this, &QPulseAudioContextManager::prepare);
554 return pulseEngine();
559 if (mode == QAudioDevice::Output) {
560 QReadLocker locker(&m_sinkLock);
561 return m_sinks.values();
564 if (mode == QAudioDevice::Input) {
565 QReadLocker locker(&m_sourceLock);
566 return m_sources.values();
574 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
~QPulseAudioContextManager() override
QByteArray defaultDevice(QAudioDevice::Mode mode) const
static QPulseAudioContextManager * instance()
pa_threaded_mainloop * mainloop()
QList< QAudioDevice > availableDevices(QAudioDevice::Mode mode) const
QPulseAudioContextManager(QObject *parent=nullptr)
static bool updateDevicesMap(QReadWriteLock &lock, const QByteArray &defaultDeviceId, QMap< int, QAudioDevice > &devices, QAudioDevice::Mode mode, const Info &info)
Q_GLOBAL_STATIC(QPulseAudioContextManager, pulseEngine)
static std::unique_ptr< QAudioDevicePrivate > makeQAudioDevicePrivate(const char *device, const char *desc, bool isDef, QAudioDevice::Mode mode, const pa_channel_map &map, const pa_sample_spec &spec)
static bool updateDevicesMap(QReadWriteLock &lock, const QByteArray &defaultDeviceId, QMap< int, QAudioDevice > &devices)