PulseAudio Qt Bindings

context.cpp
1/*
2 SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "context.h"
8#include "server.h"
9
10#include "debug.h"
11#include <QAbstractEventDispatcher>
12#include <QDBusConnection>
13#include <QDBusServiceWatcher>
14#include <QGuiApplication>
15#include <QIcon>
16#include <QTimer>
17
18#include <memory>
19
20#include "card.h"
21#include "client.h"
22#include "module.h"
23#include "sink.h"
24#include "sinkinput.h"
25#include "source.h"
26#include "sourceoutput.h"
27#include "streamrestore.h"
28
29#include "context_p.h"
30#include "server_p.h"
31#include "streamrestore_p.h"
32
33namespace PulseAudioQt
34{
36{
37 return PA_VOLUME_NORM;
38}
39
41{
42 return PA_VOLUME_MUTED;
43}
44
46{
47 return PA_VOLUME_MAX;
48}
49
51{
52 return PA_VOLUME_UI_MAX;
53}
54
55QString ContextPrivate::s_applicationId;
56
57#ifndef K_DOXYGEN
58
59static bool isGoodState(int eol)
60{
61 if (eol < 0) {
62 // Error
63 return false;
64 }
65
66 if (eol > 0) {
67 // End of callback chain
68 return false;
69 }
70
71 return true;
72}
73
74// --------------------------
75
76static void sink_cb(pa_context *context, const pa_sink_info *info, int eol, void *data)
77{
78 if (!isGoodState(eol))
79 return;
80 Q_ASSERT(context);
81 Q_ASSERT(data);
82 static_cast<ContextPrivate *>(data)->sinkCallback(info);
83}
84
85static void sink_input_callback(pa_context *context, const pa_sink_input_info *info, int eol, void *data)
86{
87 if (!isGoodState(eol))
88 return;
89 // pulsesink probe is used by gst-pulse only to query sink formats (not for playback)
90 if (qstrcmp(info->name, "pulsesink probe") == 0) {
91 return;
92 }
93 if (const char *id = pa_proplist_gets(info->proplist, "module-stream-restore.id")) {
94 if (qstrcmp(id, "sink-input-by-media-role:event") == 0) {
95 qCDebug(PULSEAUDIOQT) << "Ignoring event role sink input.";
96 return;
97 }
98 }
99 Q_ASSERT(context);
100 Q_ASSERT(data);
101 static_cast<ContextPrivate *>(data)->sinkInputCallback(info);
102}
103
104static void source_cb(pa_context *context, const pa_source_info *info, int eol, void *data)
105{
106 if (!isGoodState(eol))
107 return;
108 // FIXME: This forces excluding monitors
109 if (info->monitor_of_sink != PA_INVALID_INDEX)
110 return;
111 Q_ASSERT(context);
112 Q_ASSERT(data);
113 static_cast<ContextPrivate *>(data)->sourceCallback(info);
114}
115
116static void source_output_cb(pa_context *context, const pa_source_output_info *info, int eol, void *data)
117{
118 if (!isGoodState(eol))
119 return;
120 // FIXME: This forces excluding these apps
121 if (const char *app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID)) {
122 if (strcmp(app, "org.PulseAudio.pavucontrol") == 0 //
123 || strcmp(app, "org.gnome.VolumeControl") == 0 //
124 || strcmp(app, "org.kde.kmixd") == 0 //
125 || strcmp(app, "org.kde.plasma-pa") == 0) //
126 return;
127 }
128 Q_ASSERT(context);
129 Q_ASSERT(data);
130 static_cast<ContextPrivate *>(data)->sourceOutputCallback(info);
131}
132
133static void client_cb(pa_context *context, const pa_client_info *info, int eol, void *data)
134{
135 if (!isGoodState(eol))
136 return;
137 Q_ASSERT(context);
138 Q_ASSERT(data);
139 static_cast<ContextPrivate *>(data)->clientCallback(info);
140}
141
142static void card_cb(pa_context *context, const pa_card_info *info, int eol, void *data)
143{
144 if (!isGoodState(eol))
145 return;
146 Q_ASSERT(context);
147 Q_ASSERT(data);
148 static_cast<ContextPrivate *>(data)->cardCallback(info);
149}
150
151static void module_info_list_cb(pa_context *context, const pa_module_info *info, int eol, void *data)
152{
153 if (!isGoodState(eol))
154 return;
155 Q_ASSERT(context);
156 Q_ASSERT(data);
157 static_cast<ContextPrivate *>(data)->moduleCallback(info);
158}
159
160static void server_cb(pa_context *context, const pa_server_info *info, void *data)
161{
162 Q_ASSERT(context);
163 Q_ASSERT(data);
164 if (!info) {
165 // info may be nullptr when e.g. the server doesn't reply in time (e.g. it is stuck)
166 // https://bugs.kde.org/show_bug.cgi?id=454647
167 qCWarning(PULSEAUDIOQT) << "server_cb() called without info!";
168 return;
169 }
170 static_cast<ContextPrivate *>(data)->serverCallback(info);
171}
172
173static void context_state_callback(pa_context *context, void *data)
174{
175 Q_ASSERT(data);
176 static_cast<ContextPrivate *>(data)->contextStateCallback(context);
177}
178
179static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index, void *data)
180{
181 Q_ASSERT(data);
182 static_cast<ContextPrivate *>(data)->subscribeCallback(context, type, index);
183}
184
185static void ext_stream_restore_read_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
186{
187 if (!isGoodState(eol)) {
188 return;
189 }
190 Q_ASSERT(context);
191 Q_ASSERT(data);
192 static_cast<ContextPrivate *>(data)->streamRestoreCallback(info);
193}
194
195static void ext_stream_restore_subscribe_cb(pa_context *context, void *data)
196{
197 Q_ASSERT(context);
198 Q_ASSERT(data);
199 if (!PAOperation(pa_ext_stream_restore_read(context, ext_stream_restore_read_cb, data))) {
200 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read() failed";
201 }
202}
203
204static void ext_stream_restore_change_sink_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
205{
206 if (!isGoodState(eol)) {
207 return;
208 }
209 Q_ASSERT(context);
210 Q_ASSERT(data);
211 if (qstrncmp(info->name, "sink-input-by", 13) == 0) {
212 ContextPrivate *contextp = static_cast<ContextPrivate *>(data);
213 const QByteArray deviceData = contextp->m_newDefaultSink.toUtf8();
214 pa_ext_stream_restore_info newinfo;
215 newinfo.name = info->name;
216 newinfo.channel_map = info->channel_map;
217 newinfo.volume = info->volume;
218 newinfo.mute = info->mute;
219 newinfo.device = deviceData.constData();
220 contextp->streamRestoreWrite(&newinfo);
221 }
222}
223
224static void ext_stream_restore_change_source_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
225{
226 if (!isGoodState(eol)) {
227 return;
228 }
229 Q_ASSERT(context);
230 Q_ASSERT(data);
231 if (qstrncmp(info->name, "source-output-by", 16) == 0) {
232 ContextPrivate *contextp = static_cast<ContextPrivate *>(data);
233 const QByteArray deviceData = contextp->m_newDefaultSource.toUtf8();
234 pa_ext_stream_restore_info newinfo;
235 newinfo.name = info->name;
236 newinfo.channel_map = info->channel_map;
237 newinfo.volume = info->volume;
238 newinfo.mute = info->mute;
239 newinfo.device = deviceData.constData();
240 contextp->streamRestoreWrite(&newinfo);
241 }
242}
243
244#endif
245
246// --------------------------
247
248Context::Context(QObject *parent)
249 : QObject(parent)
250 , d(new ContextPrivate(this))
251{
252 d->m_server = new Server(this);
253 d->m_context = nullptr;
254 d->m_mainloop = nullptr;
255
256 d->connectToDaemon();
257
258 QDBusServiceWatcher *watcher =
259 new QDBusServiceWatcher(QStringLiteral("org.pulseaudio.Server"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this);
260 connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
261 d->connectToDaemon();
262 });
263
264 connect(&d->m_connectTimer, &QTimer::timeout, this, [this] {
265 d->connectToDaemon();
266 d->checkConnectTries();
267 });
268
269 connect(&d->m_sinks, &MapBaseQObject::added, this, [this](int, QObject *object) {
270 Q_EMIT sinkAdded(static_cast<Sink *>(object));
271 });
272 connect(&d->m_sinks, &MapBaseQObject::removed, this, [this](int, QObject *object) {
273 Q_EMIT sinkRemoved(static_cast<Sink *>(object));
274 });
275
276 connect(&d->m_sinkInputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
277 Q_EMIT sinkInputAdded(static_cast<SinkInput *>(object));
278 });
279 connect(&d->m_sinkInputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
280 Q_EMIT sinkInputRemoved(static_cast<SinkInput *>(object));
281 });
282
283 connect(&d->m_sources, &MapBaseQObject::added, this, [this](int, QObject *object) {
284 Q_EMIT sourceAdded(static_cast<Source *>(object));
285 });
286 connect(&d->m_sources, &MapBaseQObject::removed, this, [this](int, QObject *object) {
287 Q_EMIT sourceRemoved(static_cast<Source *>(object));
288 });
289
290 connect(&d->m_sourceOutputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
291 Q_EMIT sourceOutputAdded(static_cast<SourceOutput *>(object));
292 });
293 connect(&d->m_sourceOutputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
294 Q_EMIT sourceOutputRemoved(static_cast<SourceOutput *>(object));
295 });
296
297 connect(&d->m_clients, &MapBaseQObject::added, this, [this](int, QObject *object) {
298 Q_EMIT clientAdded(static_cast<Client *>(object));
299 });
300 connect(&d->m_clients, &MapBaseQObject::removed, this, [this](int, QObject *object) {
301 Q_EMIT clientRemoved(static_cast<Client *>(object));
302 });
303
304 connect(&d->m_cards, &MapBaseQObject::added, this, [this](int, QObject *object) {
305 Q_EMIT cardAdded(static_cast<Card *>(object));
306 });
307 connect(&d->m_cards, &MapBaseQObject::removed, this, [this](int, QObject *object) {
308 Q_EMIT cardRemoved(static_cast<Card *>(object));
309 });
310
311 connect(&d->m_modules, &MapBaseQObject::added, this, [this](int, QObject *object) {
312 Q_EMIT moduleAdded(static_cast<Module *>(object));
313 });
314 connect(&d->m_modules, &MapBaseQObject::removed, this, [this](int, QObject *object) {
315 Q_EMIT moduleRemoved(static_cast<Module *>(object));
316 });
317
318 connect(&d->m_streamRestores, &MapBaseQObject::added, this, [this](int, QObject *object) {
319 Q_EMIT streamRestoreAdded(static_cast<StreamRestore *>(object));
320 });
321 connect(&d->m_streamRestores, &MapBaseQObject::removed, this, [this](int, QObject *object) {
322 Q_EMIT streamRestoreRemoved(static_cast<StreamRestore *>(object));
323 });
324}
325
326ContextPrivate::ContextPrivate(Context *q)
327 : q(q)
328{
329}
330
331Context::~Context()
332{
333 delete d;
334}
335
336ContextPrivate::~ContextPrivate()
337{
338 if (m_context) {
339 pa_context_unref(m_context);
340 m_context = nullptr;
341 }
342
343 if (m_mainloop) {
344 pa_glib_mainloop_free(m_mainloop);
345 m_mainloop = nullptr;
346 }
347
348 reset();
349}
350
351Context *Context::instance()
352{
353 static std::unique_ptr<Context> context(new Context);
354 return context.get();
355}
356
357void ContextPrivate::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index)
358{
359 Q_ASSERT(context == m_context);
360
361 switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
362 case PA_SUBSCRIPTION_EVENT_SINK:
363 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
364 m_sinks.removeEntry(index);
365 } else {
366 if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb, this))) {
367 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_info_by_index() failed";
368 return;
369 }
370 }
371 break;
372
373 case PA_SUBSCRIPTION_EVENT_SOURCE:
374 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
375 m_sources.removeEntry(index);
376 } else {
377 if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb, this))) {
378 qCWarning(PULSEAUDIOQT) << "pa_context_get_source_info_by_index() failed";
379 return;
380 }
381 }
382 break;
383
384 case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
385 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
386 m_sinkInputs.removeEntry(index);
387 } else {
388 if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback, this))) {
389 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info() failed";
390 return;
391 }
392 }
393 break;
394
395 case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
396 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
397 m_sourceOutputs.removeEntry(index);
398 } else {
399 if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb, this))) {
400 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info() failed";
401 return;
402 }
403 }
404 break;
405
406 case PA_SUBSCRIPTION_EVENT_CLIENT:
407 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
408 m_clients.removeEntry(index);
409 } else {
410 if (!PAOperation(pa_context_get_client_info(context, index, client_cb, this))) {
411 qCWarning(PULSEAUDIOQT) << "pa_context_get_client_info() failed";
412 return;
413 }
414 }
415 break;
416
417 case PA_SUBSCRIPTION_EVENT_CARD:
418 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
419 m_cards.removeEntry(index);
420 } else {
421 if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb, this))) {
422 qCWarning(PULSEAUDIOQT) << "pa_context_get_card_info_by_index() failed";
423 return;
424 }
425 }
426 break;
427
428 case PA_SUBSCRIPTION_EVENT_MODULE:
429 if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
430 m_modules.removeEntry(index);
431 } else {
432 if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb, this))) {
433 qCWarning(PULSEAUDIOQT) << "pa_context_get_module_info_list() failed";
434 return;
435 }
436 }
437 break;
438
439 case PA_SUBSCRIPTION_EVENT_SERVER:
440 if (!PAOperation(pa_context_get_server_info(context, server_cb, this))) {
441 qCWarning(PULSEAUDIOQT) << "pa_context_get_server_info() failed";
442 return;
443 }
444 break;
445 }
446}
447
448void ContextPrivate::contextStateCallback(pa_context *c)
449{
450 qCDebug(PULSEAUDIOQT) << "state callback";
451 pa_context_state_t state = pa_context_get_state(c);
452 if (state == PA_CONTEXT_READY) {
453 qCDebug(PULSEAUDIOQT) << "ready, stopping connect timer";
454 m_connectTimer.stop();
455
456 // 1. Register for the stream changes (except during probe)
457 if (m_context == c) {
458 pa_context_set_subscribe_callback(c, subscribe_cb, this);
459
460 if (!PAOperation(
461 pa_context_subscribe(c,
462 (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_CLIENT
463 | PA_SUBSCRIPTION_MASK_SINK_INPUT | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT
464 | PA_SUBSCRIPTION_MASK_CARD | PA_SUBSCRIPTION_MASK_MODULE | PA_SUBSCRIPTION_MASK_SERVER),
465 nullptr,
466 nullptr))) {
467 qCWarning(PULSEAUDIOQT) << "pa_context_subscribe() failed";
468 return;
469 }
470 }
471
472 if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) {
473 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_info_list() failed";
474 return;
475 }
476
477 if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) {
478 qCWarning(PULSEAUDIOQT) << "pa_context_get_source_info_list() failed";
479 return;
480 }
481
482 if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) {
483 qCWarning(PULSEAUDIOQT) << "pa_context_client_info_list() failed";
484 return;
485 }
486
487 if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) {
488 qCWarning(PULSEAUDIOQT) << "pa_context_get_card_info_list() failed";
489 return;
490 }
491
492 if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) {
493 qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info_list() failed";
494 return;
495 }
496
497 if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) {
498 qCWarning(PULSEAUDIOQT) << "pa_context_get_source_output_info_list() failed";
499 return;
500 }
501
502 if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) {
503 qCWarning(PULSEAUDIOQT) << "pa_context_get_module_info_list() failed";
504 return;
505 }
506
507 if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) {
508 qCWarning(PULSEAUDIOQT) << "pa_context_get_server_info() failed";
509 return;
510 }
511
512 if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, this))) {
513 pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, this);
514 PAOperation(pa_ext_stream_restore_subscribe(c, 1, nullptr, this));
515 } else {
516 qCWarning(PULSEAUDIOQT) << "Failed to initialize stream_restore extension";
517 }
518 } else if (!PA_CONTEXT_IS_GOOD(state)) {
519 qCWarning(PULSEAUDIOQT) << "context kaput";
520 if (m_context) {
521 pa_context_unref(m_context);
522 m_context = nullptr;
523 }
524 reset();
525 qCDebug(PULSEAUDIOQT) << "Starting connect timer";
526 m_connectTimer.start(std::chrono::seconds(5));
527 }
528}
529
530void ContextPrivate::sinkCallback(const pa_sink_info *info)
531{
532 // This parenting here is a bit weird
533 m_sinks.updateEntry(info, q);
534}
535
536void ContextPrivate::sinkInputCallback(const pa_sink_input_info *info)
537{
538 m_sinkInputs.updateEntry(info, q);
539}
540
541void ContextPrivate::sourceCallback(const pa_source_info *info)
542{
543 m_sources.updateEntry(info, q);
544}
545
546void ContextPrivate::sourceOutputCallback(const pa_source_output_info *info)
547{
548 m_sourceOutputs.updateEntry(info, q);
549}
550
551void ContextPrivate::clientCallback(const pa_client_info *info)
552{
553 m_clients.updateEntry(info, q);
554}
555
556void ContextPrivate::cardCallback(const pa_card_info *info)
557{
558 m_cards.updateEntry(info, q);
559}
560
561void ContextPrivate::moduleCallback(const pa_module_info *info)
562{
563 m_modules.updateEntry(info, q);
564}
565
566void ContextPrivate::streamRestoreCallback(const pa_ext_stream_restore_info *info)
567{
568 if (qstrcmp(info->name, "sink-input-by-media-role:event") != 0) {
569 return;
570 }
571
572 const int eventRoleIndex = 1;
573 StreamRestore *obj = qobject_cast<StreamRestore *>(m_streamRestores.data().value(eventRoleIndex));
574
575 if (!obj) {
576 QVariantMap props;
577 props.insert(QStringLiteral("application.icon_name"), QStringLiteral("preferences-desktop-notification"));
578 obj = new StreamRestore(eventRoleIndex, props, q);
579 obj->d->update(info);
580 m_streamRestores.insert(obj);
581 } else {
582 obj->d->update(info);
583 }
584}
585
586void ContextPrivate::serverCallback(const pa_server_info *info)
587{
588 m_server->d->update(info);
589}
590
591void Context::setCardProfile(quint32 index, const QString &profile)
592{
593 if (!d->m_context) {
594 return;
595 }
596 qCDebug(PULSEAUDIOQT) << index << profile;
597 if (!PAOperation(pa_context_set_card_profile_by_index(d->m_context, index, profile.toUtf8().constData(), nullptr, nullptr))) {
598 qCWarning(PULSEAUDIOQT) << "pa_context_set_card_profile_by_index failed";
599 return;
600 }
601}
602
603void Context::setDefaultSink(const QString &name)
604{
605 if (!d->m_context) {
606 return;
607 }
608 const QByteArray nameData = name.toUtf8();
609 if (!PAOperation(pa_context_set_default_sink(d->m_context, nameData.constData(), nullptr, nullptr))) {
610 qCWarning(PULSEAUDIOQT) << "pa_context_set_default_sink failed";
611 }
612
613 // Change device for all entries in stream-restore database
614 d->m_newDefaultSink = name;
615 if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_sink_cb, d))) {
616 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read failed";
617 }
618}
619
620void Context::setDefaultSource(const QString &name)
621{
622 if (!d->m_context) {
623 return;
624 }
625 const QByteArray nameData = name.toUtf8();
626 if (!PAOperation(pa_context_set_default_source(d->m_context, nameData.constData(), nullptr, nullptr))) {
627 qCWarning(PULSEAUDIOQT) << "pa_context_set_default_source failed";
628 }
629
630 // Change device for all entries in stream-restore database
631 d->m_newDefaultSource = name;
632 if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_source_cb, d))) {
633 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read failed";
634 }
635}
636
637void ContextPrivate::streamRestoreWrite(const pa_ext_stream_restore_info *info)
638{
639 if (!m_context) {
640 return;
641 }
642 if (!PAOperation(pa_ext_stream_restore_write(m_context, PA_UPDATE_REPLACE, info, 1, true, nullptr, nullptr))) {
643 qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_write failed";
644 }
645}
646
647void ContextPrivate::connectToDaemon()
648{
649 if (m_context) {
650 return;
651 }
652
653 // We require a glib event loop
654 if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("Glib")) {
655 qCWarning(PULSEAUDIOQT) << "Disabling PulseAudio integration for lack of GLib event loop";
656 return;
657 }
658
659 qCDebug(PULSEAUDIOQT) << "Attempting connection to PulseAudio sound daemon";
660 if (!m_mainloop) {
661 m_mainloop = pa_glib_mainloop_new(nullptr);
662 Q_ASSERT(m_mainloop);
663 }
664
665 pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop);
666 Q_ASSERT(api);
667
668 pa_proplist *proplist = pa_proplist_new();
669 pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, QGuiApplication::applicationDisplayName().toUtf8().constData());
670 if (!s_applicationId.isEmpty()) {
671 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, s_applicationId.toUtf8().constData());
672 } else {
673 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, QGuiApplication::desktopFileName().toUtf8().constData());
674 }
675 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, QGuiApplication::windowIcon().name().toUtf8().constData());
676 m_context = pa_context_new_with_proplist(api, nullptr, proplist);
677 pa_proplist_free(proplist);
678 Q_ASSERT(m_context);
679
680 if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL, nullptr) < 0) {
681 pa_context_unref(m_context);
682 pa_glib_mainloop_free(m_mainloop);
683 m_context = nullptr;
684 m_mainloop = nullptr;
685 return;
686 }
687 pa_context_set_state_callback(m_context, &context_state_callback, this);
688}
689
690void ContextPrivate::checkConnectTries()
691{
692 if (++m_connectTries == 5) {
693 qCWarning(PULSEAUDIOQT) << "Giving up after" << m_connectTries << "tries to connect";
694 m_connectTimer.stop();
695 }
696}
697
698void ContextPrivate::reset()
699{
700 m_sinks.reset();
701 m_sinkInputs.reset();
702 m_sources.reset();
703 m_sourceOutputs.reset();
704 m_clients.reset();
705 m_cards.reset();
706 m_modules.reset();
707 m_streamRestores.reset();
708 m_server->reset();
709 m_connectTries = 0;
710}
711
712bool Context::isValid()
713{
714 return d->m_context && d->m_mainloop;
715}
716
717QVector<Sink *> Context::sinks() const
718{
719 return d->m_sinks.data();
720}
721
722QVector<SinkInput *> Context::sinkInputs() const
723{
724 return d->m_sinkInputs.data();
725}
726
727QVector<Source *> Context::sources() const
728{
729 return d->m_sources.data();
730}
731
732QVector<SourceOutput *> Context::sourceOutputs() const
733{
734 return d->m_sourceOutputs.data();
735}
736
737QVector<Client *> Context::clients() const
738{
739 return d->m_clients.data();
740}
741
742QVector<Card *> Context::cards() const
743{
744 return d->m_cards.data();
745}
746
747QVector<Module *> Context::modules() const
748{
749 return d->m_modules.data();
750}
751
752QVector<StreamRestore *> Context::streamRestores() const
753{
754 return d->m_streamRestores.data();
755}
756
757Server *Context::server() const
758{
759 return d->m_server;
760}
761
762void ContextPrivate::setGenericVolume(
763 quint32 index,
764 int channel,
765 qint64 newVolume,
766 pa_cvolume cVolume,
767 const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
768{
769 if (!m_context) {
770 return;
771 }
772 newVolume = qBound<qint64>(0, newVolume, PA_VOLUME_MAX);
773 pa_cvolume newCVolume = cVolume;
774 if (channel == -1) { // -1 all channels
775 const qint64 orig = pa_cvolume_max(&cVolume);
776 const qint64 diff = newVolume - orig;
777 for (int i = 0; i < newCVolume.channels; ++i) {
778 const qint64 channel = newCVolume.values[i];
779 const qint64 channelDiff = orig == 0 ? diff : diff * channel / orig;
780 newCVolume.values[i] = qBound<qint64>(0, newCVolume.values[i] + channelDiff, PA_VOLUME_MAX);
781 }
782 } else {
783 Q_ASSERT(newCVolume.channels > channel);
784 newCVolume.values[channel] = newVolume;
785 }
786 if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) {
787 qCWarning(PULSEAUDIOQT) << "pa_set_volume failed";
788 return;
789 }
790}
791
792void ContextPrivate::setGenericMute(quint32 index,
793 bool mute,
794 const std::function<pa_operation *(pa_context *, uint32_t, int, pa_context_success_cb_t, void *)> &pa_set_mute)
795{
796 if (!m_context) {
797 return;
798 }
799 if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) {
800 qCWarning(PULSEAUDIOQT) << "pa_set_mute failed";
801 return;
802 }
803}
804
805void ContextPrivate::setGenericPort(quint32 index,
806 const QString &portName,
807 const std::function<pa_operation *(pa_context *, uint32_t, const char *, pa_context_success_cb_t, void *)> &pa_set_port)
808{
809 if (!m_context) {
810 return;
811 }
812 if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) {
813 qCWarning(PULSEAUDIOQT) << "pa_set_port failed";
814 return;
815 }
816}
817
818void ContextPrivate::setGenericDeviceForStream(
819 quint32 streamIndex,
820 quint32 deviceIndex,
821 const std::function<pa_operation *(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t, void *)> &pa_move_stream_to_device)
822{
823 if (!m_context) {
824 return;
825 }
826 if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) {
827 qCWarning(PULSEAUDIOQT) << "pa_move_stream_to_device failed";
828 return;
829 }
830}
831
832void ContextPrivate::setGenericVolumes(
833 quint32 index,
834 QVector<qint64> channelVolumes,
835 pa_cvolume cVolume,
836 const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
837{
838 if (!m_context) {
839 return;
840 }
841 Q_ASSERT(channelVolumes.count() == cVolume.channels);
842
843 pa_cvolume newCVolume = cVolume;
844 for (int i = 0; i < channelVolumes.count(); ++i) {
845 newCVolume.values[i] = qBound<qint64>(0, channelVolumes.at(i), PA_VOLUME_MAX);
846 }
847
848 if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) {
849 qCWarning(PULSEAUDIOQT) << "pa_set_volume failed";
850 return;
851 }
852}
853
854void Context::setApplicationId(const QString &applicationId)
855{
856 ContextPrivate::s_applicationId = applicationId;
857}
858
859pa_context *Context::context() const
860{
861 return d->m_context;
862}
863
864} // PulseAudioQt
KGuiItem reset()
QString name(StandardShortcut id)
The primary namespace of PulseAudioQt.
Definition card.cpp:17
qint64 normalVolume()
The normal volume (100%, 0 dB).
Definition context.cpp:35
qint64 maximumUIVolume()
The maximum volume suitable to display in a UI.
Definition context.cpp:50
qint64 minimumVolume()
The minimum volume (0%).
Definition context.cpp:40
qint64 maximumVolume()
The maximum volume PulseAudio can store.
Definition context.cpp:45
QAbstractEventDispatcher * instance(QThread *thread)
const char * constData() const const
QDBusConnection sessionBus()
void serviceRegistered(const QString &serviceName)
const_reference at(qsizetype i) const const
qsizetype count() const const
pointer data()
QByteArray toUtf8() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:07 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.