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#include <QtMultimedia/private/qpulsehelpers_p.h>
14#include <QtMultimedia/private/qpulseaudiodevice_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<QPulseAudioDevicePrivate>(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 const QAudioDevicePrivate *handle = QAudioDevicePrivate::handle(device);
90 if (handle && compare(*newDeviceInfo, *handle))
93 device = QAudioDevicePrivate::createQAudioDevice(std::move(newDeviceInfo));
98 QMap<
int, QAudioDevice> &devices)
100 QWriteLocker locker(&lock);
104 for (QAudioDevice &device : devices) {
105 auto deviceInfo = QAudioDevicePrivate::handle<QPulseAudioDevicePrivate>(device);
106 const auto isDefault = deviceInfo->id == defaultDeviceId;
107 if (deviceInfo->isDefault != isDefault) {
108 auto newDeviceInfo = std::make_unique<QPulseAudioDevicePrivate>(*deviceInfo);
109 newDeviceInfo->isDefault = isDefault;
110 device = QAudioDevicePrivate::createQAudioDevice(std::move(newDeviceInfo));
121 using namespace Qt::Literals;
122 using namespace QPulseAudioInternal;
125 qWarning() <<
"Failed to get server information:" << currentError(context);
129 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
130 char ss[PA_SAMPLE_SPEC_SNPRINT_MAX], cm[PA_CHANNEL_MAP_SNPRINT_MAX];
132 pa_sample_spec_snprint(ss,
sizeof(ss), &info->sample_spec);
133 pa_channel_map_snprint(cm,
sizeof(cm), &info->channel_map);
135 qCDebug(qLcPulseAudioEngine)
136 << QStringLiteral(
"User name: %1\n"
139 "Server Version: %4\n"
140 "Default Sample Specification: %5\n"
141 "Default Channel Map: %6\n"
143 "Default Source: %8\n")
144 .arg(QString::fromUtf8(info->user_name),
145 QString::fromUtf8(info->host_name),
146 QString::fromUtf8(info->server_name),
147 QLatin1StringView(info->server_version), QLatin1StringView(ss),
148 QLatin1StringView(cm), QString::fromUtf8(info->default_sink_name),
149 QString::fromUtf8(info->default_source_name));
154 bool defaultSinkChanged =
false;
155 bool defaultSourceChanged =
false;
158 QWriteLocker locker(&pulseEngine->m_serverLock);
159 pulseEngine->m_serverName = QString::fromUtf8(info->server_name);
161 if (pulseEngine->m_defaultSink != info->default_sink_name) {
162 pulseEngine->m_defaultSink = info->default_sink_name;
163 defaultSinkChanged =
true;
166 if (pulseEngine->m_defaultSource != info->default_source_name) {
167 pulseEngine->m_defaultSource = info->default_source_name;
168 defaultSourceChanged =
true;
172 if (defaultSinkChanged
173 && updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink,
174 pulseEngine->m_sinks))
175 emit pulseEngine->audioOutputsChanged();
177 if (defaultSourceChanged
178 && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
179 pulseEngine->m_sources))
180 emit pulseEngine->audioInputsChanged();
182 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
186 int isLast,
void *userdata)
188 using namespace Qt::Literals;
189 using namespace QPulseAudioInternal;
194 qWarning() <<
"Failed to get sink information:" << currentError(context);
199 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
205 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
206 static const QFlatMap<pa_sink_state, QStringView> stateMap{
207 { PA_SINK_INVALID_STATE, u"n/a" }, { PA_SINK_RUNNING, u"RUNNING" },
208 { PA_SINK_IDLE, u"IDLE" }, { PA_SINK_SUSPENDED, u"SUSPENDED" },
209 { PA_SINK_UNLINKED, u"UNLINKED" },
212 qCDebug(qLcPulseAudioEngine)
213 << QStringLiteral(
"Sink #%1\n"
216 "\tDescription: %4\n")
217 .arg(QString::number(info->index), stateMap.value(info->state),
218 QString::fromUtf8(info->name),
219 QString::fromUtf8(info->description));
222 if (updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks,
223 QAudioDevice::Output, *info))
224 emit pulseEngine->audioOutputsChanged();
228 int isLast,
void *userdata)
230 using namespace Qt::Literals;
236 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
242 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
243 static const QFlatMap<pa_source_state, QStringView> stateMap{
244 { PA_SOURCE_INVALID_STATE, u"n/a" }, { PA_SOURCE_RUNNING, u"RUNNING" },
245 { PA_SOURCE_IDLE, u"IDLE" }, { PA_SOURCE_SUSPENDED, u"SUSPENDED" },
246 { PA_SOURCE_UNLINKED, u"UNLINKED" },
249 qCDebug(qLcPulseAudioEngine)
250 << QStringLiteral(
"Source #%1\n"
253 "\tDescription: %4\n")
254 .arg(QString::number(info->index), stateMap.value(info->state),
255 QString::fromUtf8(info->name),
256 QString::fromUtf8(info->description));
260 if (info->monitor_of_sink != PA_INVALID_INDEX)
263 if (updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
264 pulseEngine->m_sources, QAudioDevice::Input, *info))
265 emit pulseEngine->audioInputsChanged();
269 uint32_t index,
void *userdata)
273 int type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
274 int facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
277 case PA_SUBSCRIPTION_EVENT_NEW:
278 case PA_SUBSCRIPTION_EVENT_CHANGE:
280 case PA_SUBSCRIPTION_EVENT_SERVER: {
281 PAOperationHandle op{
282 pa_context_get_server_info(context, serverInfoCallback, userdata),
283 PAOperationHandle::HasRef,
286 qWarning() <<
"PulseAudioService: failed to get server info";
289 case PA_SUBSCRIPTION_EVENT_SINK: {
290 PAOperationHandle op{
291 pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata),
292 PAOperationHandle::HasRef,
296 qWarning() <<
"PulseAudioService: failed to get sink info";
299 case PA_SUBSCRIPTION_EVENT_SOURCE: {
300 PAOperationHandle op{
301 pa_context_get_source_info_by_index(context, index, sourceInfoCallback, userdata),
302 PAOperationHandle::HasRef,
306 qWarning() <<
"PulseAudioService: failed to get source info";
313 case PA_SUBSCRIPTION_EVENT_REMOVE:
315 case PA_SUBSCRIPTION_EVENT_SINK: {
316 QWriteLocker locker(&pulseEngine->m_sinkLock);
317 pulseEngine->m_sinks.remove(index);
320 case PA_SUBSCRIPTION_EVENT_SOURCE: {
321 QWriteLocker locker(&pulseEngine->m_sourceLock);
322 pulseEngine->m_sources.remove(index);
338 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
339 qCDebug(qLcPulseAudioEngine) << pa_context_get_state(context);
343 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
349 pa_context_state_t state = pa_context_get_state(c);
351 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
352 qCDebug(qLcPulseAudioEngine) << state;
354 if (state == PA_CONTEXT_FAILED)
355 QMetaObject::invokeMethod(self, &QPulseAudioContextManager::onContextFailed,
356 Qt::QueuedConnection);
373 using namespace QPulseAudioInternal;
374 bool keepGoing =
true;
377 m_mainLoop.reset(pa_threaded_mainloop_new());
378 if (m_mainLoop ==
nullptr) {
379 qCritical() <<
"PulseAudioService: unable to create pulseaudio mainloop";
383 pa_threaded_mainloop_set_name(
384 m_mainLoop.get(),
"QPulseAudioEngi");
386 if (pa_threaded_mainloop_start(m_mainLoop.get()) != 0) {
387 qCritical() <<
"PulseAudioService: unable to start pulseaudio mainloop";
392 m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop.get());
394 std::unique_lock guard{ *
this };
396 PAProplistHandle proplist{
399 if (!QGuiApplication::applicationDisplayName().isEmpty())
400 pa_proplist_sets(proplist.get(), PA_PROP_APPLICATION_NAME,
401 qUtf8Printable(QGuiApplication::applicationDisplayName()));
402 if (!QGuiApplication::desktopFileName().isEmpty())
403 pa_proplist_sets(proplist.get(), PA_PROP_APPLICATION_ID,
404 qUtf8Printable(QGuiApplication::desktopFileName()));
405 if (
const QString windowIconName = QGuiApplication::windowIcon().name();
406 !windowIconName.isEmpty())
407 pa_proplist_sets(proplist.get(), PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
409 m_context = PAContextHandle{
410 pa_context_new_with_proplist(m_mainLoopApi,
nullptr, proplist.get()),
411 PAContextHandle::HasRef,
415 qCritical() <<
"PulseAudioService: Unable to create new pulseaudio context";
422 pa_context_set_state_callback(m_context.get(), contextStateCallbackInit,
this);
424 if (pa_context_connect(m_context.get(),
nullptr,
static_cast<pa_context_flags_t>(0),
nullptr)
426 qWarning() <<
"PulseAudioService: pa_context_connect() failed";
433 pa_threaded_mainloop_wait(m_mainLoop.get());
436 switch (pa_context_get_state(m_context.get())) {
437 case PA_CONTEXT_CONNECTING:
438 case PA_CONTEXT_AUTHORIZING:
439 case PA_CONTEXT_SETTING_NAME:
442 case PA_CONTEXT_READY:
443 qCDebug(qLcPulseAudioEngine) <<
"Connection established.";
447 case PA_CONTEXT_TERMINATED:
448 qCritical(
"PulseAudioService: Context terminated.");
453 case PA_CONTEXT_FAILED:
455 qCritical() <<
"PulseAudioService: Connection failure:"
456 << currentError(m_context.get());
462 pa_threaded_mainloop_wait(m_mainLoop.get());
466 pa_context_set_state_callback(m_context.get(), contextStateCallback,
this);
468 pa_context_set_subscribe_callback(m_context.get(), eventCallback,
this);
469 PAOperationHandle op{
470 pa_context_subscribe(m_context.get(),
471 pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK
472 | PA_SUBSCRIPTION_MASK_SOURCE
473 | PA_SUBSCRIPTION_MASK_SERVER),
475 PAOperationHandle::HasRef,
479 qWarning() <<
"PulseAudioService: failed to subscribe to context notifications";
497 std::lock_guard lock{ *
this };
498 pa_context_disconnect(m_context.get());
503 pa_threaded_mainloop_stop(m_mainLoop.get());
510 std::lock_guard lock(*
this);
513 bool success = waitForAsyncOperation(
514 pa_context_get_server_info(m_context.get(), serverInfoCallback,
this));
517 qWarning() <<
"PulseAudioService: failed to get server info";
520 success = waitForAsyncOperation(
521 pa_context_get_sink_info_list(m_context.get(), sinkInfoCallback,
this));
524 qWarning() <<
"PulseAudioService: failed to get sink info";
527 success = waitForAsyncOperation(
528 pa_context_get_source_info_list(m_context.get(), sourceInfoCallback,
this));
531 qWarning() <<
"PulseAudioService: failed to get source info";
539 QTimer::singleShot(3000,
this, &QPulseAudioContextManager::prepare);
544 return pulseEngine();
549 PAOperationHandle operation{
551 PAOperationHandle::HasRef,
568 if (mode == QAudioDevice::Output) {
569 QReadLocker locker(&m_sinkLock);
570 return m_sinks.values();
573 if (mode == QAudioDevice::Input) {
574 QReadLocker locker(&m_sourceLock);
575 return m_sources.values();
583 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
588 auto lock = std::lock_guard{ *
this };
589 return pa_context_get_state(m_context.get());
599 QReadLocker locker(&pulseEngine->m_serverLock);
~QPulseAudioContextManager() override
QByteArray defaultDevice(QAudioDevice::Mode mode) const
static QPulseAudioContextManager * instance()
bool waitForAsyncOperation(const PAOperationHandle &)
pa_context_state_t getContextState()
bool waitForAsyncOperation(pa_operation *op)
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)