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;
32 QAudioDevicePrivate::AudioDeviceFormat format;
34 format.minimumChannelCount = 1;
35 format.maximumChannelCount = PA_CHANNELS_MAX;
36 format.minimumSampleRate = 1;
37 format.maximumSampleRate = PA_RATE_MAX;
39 constexpr bool isBigEndian = QSysInfo::ByteOrder == QSysInfo::BigEndian;
43 pa_sample_format pa_fmt;
52 for (
const auto &f : formatMap) {
53 if (pa_sample_format_valid(f.pa_fmt) != 0)
54 format.supportedSampleFormats.append(f.qt_fmt);
57 QAudioFormat preferredFormat = sampleSpecToAudioFormat(spec);
62 Q_ASSERT(spec.format != PA_SAMPLE_INVALID);
63 if (!format.supportedSampleFormats.contains(preferredFormat
.sampleFormat()))
67 format.preferredFormat = preferredFormat;
68 format.preferredFormat.setChannelConfig(channelConfig);
69 format.channelConfiguration = channelConfig;
70 Q_ASSERT(format.preferredFormat.isValid());
72 return std::make_unique<QPulseAudioDevicePrivate>(QByteArray(device), mode, QString::fromUtf8(desc), isDef, format);
75template<
typename Info>
77 QMap<
int, QAudioDevice> &devices, QAudioDevice::Mode mode,
80 QWriteLocker locker(&lock);
82 bool isDefault = defaultDeviceId == info.name;
83 auto newDeviceInfo = makeQAudioDevicePrivate(info.name, info.description, isDefault, mode,
84 info.channel_map, info.sample_spec);
86 auto &device = devices[info.index];
87 QAudioDevicePrivateAllMembersEqual compare;
88 const QAudioDevicePrivate *handle = QAudioDevicePrivate::handle(device);
89 if (handle && compare(*newDeviceInfo, *handle))
92 device = QAudioDevicePrivate::createQAudioDevice(std::move(newDeviceInfo));
97 QMap<
int, QAudioDevice> &devices)
99 QWriteLocker locker(&lock);
103 for (QAudioDevice &device : devices) {
104 auto deviceInfo = QAudioDevicePrivate::handle<QPulseAudioDevicePrivate>(device);
105 const auto isDefault = deviceInfo->id == defaultDeviceId;
106 if (deviceInfo->isDefault != isDefault) {
107 auto newDeviceInfo = std::make_unique<QPulseAudioDevicePrivate>(*deviceInfo);
108 newDeviceInfo->isDefault = isDefault;
109 device = QAudioDevicePrivate::createQAudioDevice(std::move(newDeviceInfo));
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);
158 pulseEngine->m_serverName = QString::fromUtf8(info->server_name);
160 if (pulseEngine->m_defaultSink != info->default_sink_name) {
161 pulseEngine->m_defaultSink = info->default_sink_name;
162 defaultSinkChanged =
true;
165 if (pulseEngine->m_defaultSource != info->default_source_name) {
166 pulseEngine->m_defaultSource = info->default_source_name;
167 defaultSourceChanged =
true;
171 if (defaultSinkChanged
172 && updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink,
173 pulseEngine->m_sinks))
174 emit pulseEngine->audioOutputsChanged();
176 if (defaultSourceChanged
177 && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
178 pulseEngine->m_sources))
179 emit pulseEngine->audioInputsChanged();
181 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
185 int isLast,
void *userdata)
187 using namespace Qt::Literals;
188 using namespace QPulseAudioInternal;
193 qWarning() <<
"Failed to get sink information:" << currentError(context);
198 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
204 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
205 static const QFlatMap<pa_sink_state, QStringView> stateMap{
206 { PA_SINK_INVALID_STATE, u"n/a" }, { PA_SINK_RUNNING, u"RUNNING" },
207 { PA_SINK_IDLE, u"IDLE" }, { PA_SINK_SUSPENDED, u"SUSPENDED" },
208 { PA_SINK_UNLINKED, u"UNLINKED" },
211 qCDebug(qLcPulseAudioEngine)
212 << QStringLiteral(
"Sink #%1\n"
215 "\tDescription: %4\n")
216 .arg(QString::number(info->index), stateMap.value(info->state),
217 QString::fromUtf8(info->name),
218 QString::fromUtf8(info->description));
221 if (updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks,
222 QAudioDevice::Output, *info))
223 emit pulseEngine->audioOutputsChanged();
227 int isLast,
void *userdata)
229 using namespace Qt::Literals;
235 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
241 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) {
242 static const QFlatMap<pa_source_state, QStringView> stateMap{
243 { PA_SOURCE_INVALID_STATE, u"n/a" }, { PA_SOURCE_RUNNING, u"RUNNING" },
244 { PA_SOURCE_IDLE, u"IDLE" }, { PA_SOURCE_SUSPENDED, u"SUSPENDED" },
245 { PA_SOURCE_UNLINKED, u"UNLINKED" },
248 qCDebug(qLcPulseAudioEngine)
249 << QStringLiteral(
"Source #%1\n"
252 "\tDescription: %4\n")
253 .arg(QString::number(info->index), stateMap.value(info->state),
254 QString::fromUtf8(info->name),
255 QString::fromUtf8(info->description));
259 if (info->monitor_of_sink != PA_INVALID_INDEX)
262 if (updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource,
263 pulseEngine->m_sources, QAudioDevice::Input, *info))
264 emit pulseEngine->audioInputsChanged();
268 uint32_t index,
void *userdata)
272 int type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK;
273 int facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
276 case PA_SUBSCRIPTION_EVENT_NEW:
277 case PA_SUBSCRIPTION_EVENT_CHANGE:
279 case PA_SUBSCRIPTION_EVENT_SERVER: {
280 PAOperationHandle op{
281 pa_context_get_server_info(context, serverInfoCallback, userdata),
282 PAOperationHandle::HasRef,
285 qWarning() <<
"PulseAudioService: failed to get server info";
288 case PA_SUBSCRIPTION_EVENT_SINK: {
289 PAOperationHandle op{
290 pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata),
291 PAOperationHandle::HasRef,
295 qWarning() <<
"PulseAudioService: failed to get sink info";
298 case PA_SUBSCRIPTION_EVENT_SOURCE: {
299 PAOperationHandle op{
300 pa_context_get_source_info_by_index(context, index, sourceInfoCallback, userdata),
301 PAOperationHandle::HasRef,
305 qWarning() <<
"PulseAudioService: failed to get source info";
312 case PA_SUBSCRIPTION_EVENT_REMOVE:
314 case PA_SUBSCRIPTION_EVENT_SINK: {
315 QWriteLocker locker(&pulseEngine->m_sinkLock);
316 pulseEngine->m_sinks.remove(index);
319 case PA_SUBSCRIPTION_EVENT_SOURCE: {
320 QWriteLocker locker(&pulseEngine->m_sourceLock);
321 pulseEngine->m_sources.remove(index);
337 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
338 qCDebug(qLcPulseAudioEngine) << pa_context_get_state(context);
342 pa_threaded_mainloop_signal(pulseEngine
->mainloop(), 0);
348 pa_context_state_t state = pa_context_get_state(c);
350 if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg)))
351 qCDebug(qLcPulseAudioEngine) << state;
353 if (state == PA_CONTEXT_FAILED)
354 QMetaObject::invokeMethod(self, &QPulseAudioContextManager::onContextFailed,
355 Qt::QueuedConnection);
372 using namespace QPulseAudioInternal;
373 bool keepGoing =
true;
376 m_mainLoop.reset(pa_threaded_mainloop_new());
377 if (m_mainLoop ==
nullptr) {
378 qCritical() <<
"PulseAudioService: unable to create pulseaudio mainloop";
382 pa_threaded_mainloop_set_name(
383 m_mainLoop.get(),
"QPulseAudioEngi");
385 if (pa_threaded_mainloop_start(m_mainLoop.get()) != 0) {
386 qCritical() <<
"PulseAudioService: unable to start pulseaudio mainloop";
391 m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop.get());
393 std::unique_lock guard{ *
this };
395 PAProplistHandle proplist{
398 if (!QGuiApplication::applicationDisplayName().isEmpty())
399 pa_proplist_sets(proplist.get(), PA_PROP_APPLICATION_NAME,
400 qUtf8Printable(QGuiApplication::applicationDisplayName()));
401 if (!QGuiApplication::desktopFileName().isEmpty())
402 pa_proplist_sets(proplist.get(), PA_PROP_APPLICATION_ID,
403 qUtf8Printable(QGuiApplication::desktopFileName()));
404 if (
const QString windowIconName = QGuiApplication::windowIcon().name();
405 !windowIconName.isEmpty())
406 pa_proplist_sets(proplist.get(), PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName));
408 m_context = PAContextHandle{
409 pa_context_new_with_proplist(m_mainLoopApi,
nullptr, proplist.get()),
410 PAContextHandle::HasRef,
414 qCritical() <<
"PulseAudioService: Unable to create new pulseaudio context";
421 pa_context_set_state_callback(m_context.get(), contextStateCallbackInit,
this);
423 if (pa_context_connect(m_context.get(),
nullptr,
static_cast<pa_context_flags_t>(0),
nullptr)
425 qWarning() <<
"PulseAudioService: pa_context_connect() failed";
432 pa_threaded_mainloop_wait(m_mainLoop.get());
435 switch (pa_context_get_state(m_context.get())) {
436 case PA_CONTEXT_CONNECTING:
437 case PA_CONTEXT_AUTHORIZING:
438 case PA_CONTEXT_SETTING_NAME:
441 case PA_CONTEXT_READY:
442 qCDebug(qLcPulseAudioEngine) <<
"Connection established.";
446 case PA_CONTEXT_TERMINATED:
447 qCritical(
"PulseAudioService: Context terminated.");
452 case PA_CONTEXT_FAILED:
454 qCritical() <<
"PulseAudioService: Connection failure:"
455 << currentError(m_context.get());
461 pa_threaded_mainloop_wait(m_mainLoop.get());
465 pa_context_set_state_callback(m_context.get(), contextStateCallback,
this);
467 pa_context_set_subscribe_callback(m_context.get(), eventCallback,
this);
468 PAOperationHandle op{
469 pa_context_subscribe(m_context.get(),
470 pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK
471 | PA_SUBSCRIPTION_MASK_SOURCE
472 | PA_SUBSCRIPTION_MASK_SERVER),
474 PAOperationHandle::HasRef,
478 qWarning() <<
"PulseAudioService: failed to subscribe to context notifications";
496 std::lock_guard lock{ *
this };
497 pa_context_disconnect(m_context.get());
502 pa_threaded_mainloop_stop(m_mainLoop.get());
509 std::lock_guard lock(*
this);
513 pa_context_get_server_info(m_context.get(), serverInfoCallback,
this)
);
516 qWarning() <<
"PulseAudioService: failed to get server info";
520 pa_context_get_sink_info_list(m_context.get(), sinkInfoCallback,
this)
);
523 qWarning() <<
"PulseAudioService: failed to get sink info";
527 pa_context_get_source_info_list(m_context.get(), sourceInfoCallback,
this)
);
530 qWarning() <<
"PulseAudioService: failed to get source info";
538 QTimer::singleShot(3000,
this, &QPulseAudioContextManager::prepare);
543 return pulseEngine();
548 PAOperationHandle operation{
550 PAOperationHandle::HasRef,
567 if (mode == QAudioDevice::Output) {
568 QReadLocker locker(&m_sinkLock);
569 return m_sinks.values();
572 if (mode == QAudioDevice::Input) {
573 QReadLocker locker(&m_sourceLock);
574 return m_sources.values();
582 return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource;
587 auto lock = std::lock_guard{ *
this };
588 return pa_context_get_state(m_context.get());
598 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)