PulseAudio Qt Bindings

context.cpp
1 /*
2  SPDX-FileCopyrightText: 2014-2015 Harald Sitter <[email protected]>
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 
33 namespace PulseAudioQt
34 {
35 qint64 normalVolume()
36 {
37  return PA_VOLUME_NORM;
38 }
39 
40 qint64 minimumVolume()
41 {
42  return PA_VOLUME_MUTED;
43 }
44 
45 qint64 maximumVolume()
46 {
47  return PA_VOLUME_MAX;
48 }
49 
51 {
52  return PA_VOLUME_UI_MAX;
53 }
54 
55 QString ContextPrivate::s_applicationId;
56 
57 #ifndef K_DOXYGEN
58 
59 static 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 
76 static 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 
85 static 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 
104 static 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 
116 static 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 
133 static 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 
142 static 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 
151 static 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 
160 static 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 
173 static void context_state_callback(pa_context *context, void *data)
174 {
175  Q_ASSERT(data);
176  static_cast<ContextPrivate *>(data)->contextStateCallback(context);
177 }
178 
179 static 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 
185 static 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 
195 static 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 
204 static 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 
224 static 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 
248 Context::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  d->m_references = 0;
256 
257  d->connectToDaemon();
258 
259  QDBusServiceWatcher *watcher =
260  new QDBusServiceWatcher(QStringLiteral("org.pulseaudio.Server"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this);
261  connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
262  d->connectToDaemon();
263  });
264 
265  connect(&d->m_sinks, &MapBaseQObject::added, this, [this](int, QObject *object) {
266  Q_EMIT sinkAdded(static_cast<Sink *>(object));
267  });
268  connect(&d->m_sinks, &MapBaseQObject::removed, this, [this](int, QObject *object) {
269  Q_EMIT sinkRemoved(static_cast<Sink *>(object));
270  });
271 
272  connect(&d->m_sinkInputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
273  Q_EMIT sinkInputAdded(static_cast<SinkInput *>(object));
274  });
275  connect(&d->m_sinkInputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
276  Q_EMIT sinkInputRemoved(static_cast<SinkInput *>(object));
277  });
278 
279  connect(&d->m_sources, &MapBaseQObject::added, this, [this](int, QObject *object) {
280  Q_EMIT sourceAdded(static_cast<Source *>(object));
281  });
282  connect(&d->m_sources, &MapBaseQObject::removed, this, [this](int, QObject *object) {
283  Q_EMIT sourceRemoved(static_cast<Source *>(object));
284  });
285 
286  connect(&d->m_sourceOutputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
287  Q_EMIT sourceOutputAdded(static_cast<SourceOutput *>(object));
288  });
289  connect(&d->m_sourceOutputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
290  Q_EMIT sourceOutputRemoved(static_cast<SourceOutput *>(object));
291  });
292 
293  connect(&d->m_clients, &MapBaseQObject::added, this, [this](int, QObject *object) {
294  Q_EMIT clientAdded(static_cast<Client *>(object));
295  });
296  connect(&d->m_clients, &MapBaseQObject::removed, this, [this](int, QObject *object) {
297  Q_EMIT clientRemoved(static_cast<Client *>(object));
298  });
299 
300  connect(&d->m_cards, &MapBaseQObject::added, this, [this](int, QObject *object) {
301  Q_EMIT cardAdded(static_cast<Card *>(object));
302  });
303  connect(&d->m_cards, &MapBaseQObject::removed, this, [this](int, QObject *object) {
304  Q_EMIT cardRemoved(static_cast<Card *>(object));
305  });
306 
307  connect(&d->m_modules, &MapBaseQObject::added, this, [this](int, QObject *object) {
308  Q_EMIT moduleAdded(static_cast<Module *>(object));
309  });
310  connect(&d->m_modules, &MapBaseQObject::removed, this, [this](int, QObject *object) {
311  Q_EMIT moduleRemoved(static_cast<Module *>(object));
312  });
313 
314  connect(&d->m_streamRestores, &MapBaseQObject::added, this, [this](int, QObject *object) {
315  Q_EMIT streamRestoreAdded(static_cast<StreamRestore *>(object));
316  });
317  connect(&d->m_streamRestores, &MapBaseQObject::removed, this, [this](int, QObject *object) {
318  Q_EMIT streamRestoreRemoved(static_cast<StreamRestore *>(object));
319  });
320 }
321 
322 ContextPrivate::ContextPrivate(Context *q)
323  : q(q)
324 {
325 }
326 
327 Context::~Context()
328 {
329  delete d;
330 }
331 
332 ContextPrivate::~ContextPrivate()
333 {
334  if (m_context) {
335  pa_context_unref(m_context);
336  m_context = nullptr;
337  }
338 
339  if (m_mainloop) {
340  pa_glib_mainloop_free(m_mainloop);
341  m_mainloop = nullptr;
342  }
343 
344  reset();
345 }
346 
347 Context *Context::instance()
348 {
349  static std::unique_ptr<Context> context(new Context);
350  return context.get();
351 }
352 
353 void ContextPrivate::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index)
354 {
355  Q_ASSERT(context == m_context);
356 
357  switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
358  case PA_SUBSCRIPTION_EVENT_SINK:
359  if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
360  m_sinks.removeEntry(index);
361  } else {
362  if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb, this))) {
363  qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_info_by_index() failed";
364  return;
365  }
366  }
367  break;
368 
369  case PA_SUBSCRIPTION_EVENT_SOURCE:
370  if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
371  m_sources.removeEntry(index);
372  } else {
373  if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb, this))) {
374  qCWarning(PULSEAUDIOQT) << "pa_context_get_source_info_by_index() failed";
375  return;
376  }
377  }
378  break;
379 
380  case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
381  if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
382  m_sinkInputs.removeEntry(index);
383  } else {
384  if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback, this))) {
385  qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info() failed";
386  return;
387  }
388  }
389  break;
390 
391  case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
392  if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
393  m_sourceOutputs.removeEntry(index);
394  } else {
395  if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb, this))) {
396  qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info() failed";
397  return;
398  }
399  }
400  break;
401 
402  case PA_SUBSCRIPTION_EVENT_CLIENT:
403  if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
404  m_clients.removeEntry(index);
405  } else {
406  if (!PAOperation(pa_context_get_client_info(context, index, client_cb, this))) {
407  qCWarning(PULSEAUDIOQT) << "pa_context_get_client_info() failed";
408  return;
409  }
410  }
411  break;
412 
413  case PA_SUBSCRIPTION_EVENT_CARD:
414  if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
415  m_cards.removeEntry(index);
416  } else {
417  if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb, this))) {
418  qCWarning(PULSEAUDIOQT) << "pa_context_get_card_info_by_index() failed";
419  return;
420  }
421  }
422  break;
423 
424  case PA_SUBSCRIPTION_EVENT_MODULE:
425  if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
426  m_modules.removeEntry(index);
427  } else {
428  if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb, this))) {
429  qCWarning(PULSEAUDIOQT) << "pa_context_get_module_info_list() failed";
430  return;
431  }
432  }
433  break;
434 
435  case PA_SUBSCRIPTION_EVENT_SERVER:
436  if (!PAOperation(pa_context_get_server_info(context, server_cb, this))) {
437  qCWarning(PULSEAUDIOQT) << "pa_context_get_server_info() failed";
438  return;
439  }
440  break;
441  }
442 }
443 
444 void ContextPrivate::contextStateCallback(pa_context *c)
445 {
446  qCDebug(PULSEAUDIOQT) << "state callback";
447  pa_context_state_t state = pa_context_get_state(c);
448  if (state == PA_CONTEXT_READY) {
449  qCDebug(PULSEAUDIOQT) << "ready";
450 
451  // 1. Register for the stream changes (except during probe)
452  if (m_context == c) {
453  pa_context_set_subscribe_callback(c, subscribe_cb, this);
454 
455  if (!PAOperation(
456  pa_context_subscribe(c,
457  (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_CLIENT
458  | PA_SUBSCRIPTION_MASK_SINK_INPUT | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT
459  | PA_SUBSCRIPTION_MASK_CARD | PA_SUBSCRIPTION_MASK_MODULE | PA_SUBSCRIPTION_MASK_SERVER),
460  nullptr,
461  nullptr))) {
462  qCWarning(PULSEAUDIOQT) << "pa_context_subscribe() failed";
463  return;
464  }
465  }
466 
467  if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) {
468  qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_info_list() failed";
469  return;
470  }
471 
472  if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) {
473  qCWarning(PULSEAUDIOQT) << "pa_context_get_source_info_list() failed";
474  return;
475  }
476 
477  if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) {
478  qCWarning(PULSEAUDIOQT) << "pa_context_client_info_list() failed";
479  return;
480  }
481 
482  if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) {
483  qCWarning(PULSEAUDIOQT) << "pa_context_get_card_info_list() failed";
484  return;
485  }
486 
487  if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) {
488  qCWarning(PULSEAUDIOQT) << "pa_context_get_sink_input_info_list() failed";
489  return;
490  }
491 
492  if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) {
493  qCWarning(PULSEAUDIOQT) << "pa_context_get_source_output_info_list() failed";
494  return;
495  }
496 
497  if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) {
498  qCWarning(PULSEAUDIOQT) << "pa_context_get_module_info_list() failed";
499  return;
500  }
501 
502  if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) {
503  qCWarning(PULSEAUDIOQT) << "pa_context_get_server_info() failed";
504  return;
505  }
506 
507  if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, this))) {
508  pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, this);
509  PAOperation(pa_ext_stream_restore_subscribe(c, 1, nullptr, this));
510  } else {
511  qCWarning(PULSEAUDIOQT) << "Failed to initialize stream_restore extension";
512  }
513  } else if (!PA_CONTEXT_IS_GOOD(state)) {
514  qCWarning(PULSEAUDIOQT) << "context kaput";
515  if (m_context) {
516  pa_context_unref(m_context);
517  m_context = nullptr;
518  }
519  reset();
520  QTimer::singleShot(1000, q, [this] {
521  connectToDaemon();
522  });
523  }
524 }
525 
526 void ContextPrivate::sinkCallback(const pa_sink_info *info)
527 {
528  // This parenting here is a bit weird
529  m_sinks.updateEntry(info, q);
530 }
531 
532 void ContextPrivate::sinkInputCallback(const pa_sink_input_info *info)
533 {
534  m_sinkInputs.updateEntry(info, q);
535 }
536 
537 void ContextPrivate::sourceCallback(const pa_source_info *info)
538 {
539  m_sources.updateEntry(info, q);
540 }
541 
542 void ContextPrivate::sourceOutputCallback(const pa_source_output_info *info)
543 {
544  m_sourceOutputs.updateEntry(info, q);
545 }
546 
547 void ContextPrivate::clientCallback(const pa_client_info *info)
548 {
549  m_clients.updateEntry(info, q);
550 }
551 
552 void ContextPrivate::cardCallback(const pa_card_info *info)
553 {
554  m_cards.updateEntry(info, q);
555 }
556 
557 void ContextPrivate::moduleCallback(const pa_module_info *info)
558 {
559  m_modules.updateEntry(info, q);
560 }
561 
562 void ContextPrivate::streamRestoreCallback(const pa_ext_stream_restore_info *info)
563 {
564  if (qstrcmp(info->name, "sink-input-by-media-role:event") != 0) {
565  return;
566  }
567 
568  const int eventRoleIndex = 1;
569  StreamRestore *obj = qobject_cast<StreamRestore *>(m_streamRestores.data().value(eventRoleIndex));
570 
571  if (!obj) {
572  QVariantMap props;
573  props.insert(QStringLiteral("application.icon_name"), QStringLiteral("preferences-desktop-notification"));
574  obj = new StreamRestore(eventRoleIndex, props, q);
575  obj->d->update(info);
576  m_streamRestores.insert(obj);
577  } else {
578  obj->d->update(info);
579  }
580 }
581 
582 void ContextPrivate::serverCallback(const pa_server_info *info)
583 {
584  m_server->d->update(info);
585 }
586 
587 void Context::setCardProfile(quint32 index, const QString &profile)
588 {
589  if (!d->m_context) {
590  return;
591  }
592  qCDebug(PULSEAUDIOQT) << index << profile;
593  if (!PAOperation(pa_context_set_card_profile_by_index(d->m_context, index, profile.toUtf8().constData(), nullptr, nullptr))) {
594  qCWarning(PULSEAUDIOQT) << "pa_context_set_card_profile_by_index failed";
595  return;
596  }
597 }
598 
599 void Context::setDefaultSink(const QString &name)
600 {
601  if (!d->m_context) {
602  return;
603  }
604  const QByteArray nameData = name.toUtf8();
605  if (!PAOperation(pa_context_set_default_sink(d->m_context, nameData.constData(), nullptr, nullptr))) {
606  qCWarning(PULSEAUDIOQT) << "pa_context_set_default_sink failed";
607  }
608 
609  // Change device for all entries in stream-restore database
610  d->m_newDefaultSink = name;
611  if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_sink_cb, d))) {
612  qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read failed";
613  }
614 }
615 
616 void Context::setDefaultSource(const QString &name)
617 {
618  if (!d->m_context) {
619  return;
620  }
621  const QByteArray nameData = name.toUtf8();
622  if (!PAOperation(pa_context_set_default_source(d->m_context, nameData.constData(), nullptr, nullptr))) {
623  qCWarning(PULSEAUDIOQT) << "pa_context_set_default_source failed";
624  }
625 
626  // Change device for all entries in stream-restore database
627  d->m_newDefaultSource = name;
628  if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_source_cb, d))) {
629  qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_read failed";
630  }
631 }
632 
633 void ContextPrivate::streamRestoreWrite(const pa_ext_stream_restore_info *info)
634 {
635  if (!m_context) {
636  return;
637  }
638  if (!PAOperation(pa_ext_stream_restore_write(m_context, PA_UPDATE_REPLACE, info, 1, true, nullptr, nullptr))) {
639  qCWarning(PULSEAUDIOQT) << "pa_ext_stream_restore_write failed";
640  }
641 }
642 
643 void ContextPrivate::connectToDaemon()
644 {
645  if (m_context) {
646  return;
647  }
648 
649  // We require a glib event loop
650  if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("Glib")) {
651  qCWarning(PULSEAUDIOQT) << "Disabling PulseAudio integration for lack of GLib event loop";
652  return;
653  }
654 
655  qCDebug(PULSEAUDIOQT) << "Attempting connection to PulseAudio sound daemon";
656  if (!m_mainloop) {
657  m_mainloop = pa_glib_mainloop_new(nullptr);
658  Q_ASSERT(m_mainloop);
659  }
660 
661  pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop);
662  Q_ASSERT(api);
663 
664  pa_proplist *proplist = pa_proplist_new();
665  pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, QGuiApplication::applicationDisplayName().toUtf8().constData());
666  if (!s_applicationId.isEmpty()) {
667  pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, s_applicationId.toUtf8().constData());
668  } else {
669  pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, QGuiApplication::desktopFileName().toUtf8().constData());
670  }
671  pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, QGuiApplication::windowIcon().name().toUtf8().constData());
672  m_context = pa_context_new_with_proplist(api, nullptr, proplist);
673  pa_proplist_free(proplist);
674  Q_ASSERT(m_context);
675 
676  if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL, nullptr) < 0) {
677  pa_context_unref(m_context);
678  pa_glib_mainloop_free(m_mainloop);
679  m_context = nullptr;
680  m_mainloop = nullptr;
681  return;
682  }
683  pa_context_set_state_callback(m_context, &context_state_callback, this);
684 }
685 
686 void ContextPrivate::reset()
687 {
688  m_sinks.reset();
689  m_sinkInputs.reset();
690  m_sources.reset();
691  m_sourceOutputs.reset();
692  m_clients.reset();
693  m_cards.reset();
694  m_modules.reset();
695  m_streamRestores.reset();
696  m_server->reset();
697 }
698 
699 bool Context::isValid()
700 {
701  return d->m_context && d->m_mainloop;
702 }
703 
704 QVector<Sink *> Context::sinks() const
705 {
706  return d->m_sinks.data();
707 }
708 
709 QVector<SinkInput *> Context::sinkInputs() const
710 {
711  return d->m_sinkInputs.data();
712 }
713 
714 QVector<Source *> Context::sources() const
715 {
716  return d->m_sources.data();
717 }
718 
719 QVector<SourceOutput *> Context::sourceOutputs() const
720 {
721  return d->m_sourceOutputs.data();
722 }
723 
724 QVector<Client *> Context::clients() const
725 {
726  return d->m_clients.data();
727 }
728 
729 QVector<Card *> Context::cards() const
730 {
731  return d->m_cards.data();
732 }
733 
734 QVector<Module *> Context::modules() const
735 {
736  return d->m_modules.data();
737 }
738 
739 QVector<StreamRestore *> Context::streamRestores() const
740 {
741  return d->m_streamRestores.data();
742 }
743 
744 Server *Context::server() const
745 {
746  return d->m_server;
747 }
748 
749 void ContextPrivate::setGenericVolume(
750  quint32 index,
751  int channel,
752  qint64 newVolume,
753  pa_cvolume cVolume,
754  const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
755 {
756  if (!m_context) {
757  return;
758  }
759  newVolume = qBound<qint64>(0, newVolume, PA_VOLUME_MAX);
760  pa_cvolume newCVolume = cVolume;
761  if (channel == -1) { // -1 all channels
762  const qint64 diff = newVolume - pa_cvolume_max(&cVolume);
763  for (int i = 0; i < newCVolume.channels; ++i) {
764  newCVolume.values[i] = qBound<qint64>(0, newCVolume.values[i] + diff, PA_VOLUME_MAX);
765  }
766  } else {
767  Q_ASSERT(newCVolume.channels > channel);
768  newCVolume.values[channel] = newVolume;
769  }
770  if (!pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr)) {
771  qCWarning(PULSEAUDIOQT) << "pa_set_volume failed";
772  return;
773  }
774 }
775 
776 void ContextPrivate::setGenericMute(quint32 index,
777  bool mute,
778  const std::function<pa_operation *(pa_context *, uint32_t, int, pa_context_success_cb_t, void *)> &pa_set_mute)
779 {
780  if (!m_context) {
781  return;
782  }
783  if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) {
784  qCWarning(PULSEAUDIOQT) << "pa_set_mute failed";
785  return;
786  }
787 }
788 
789 void ContextPrivate::setGenericPort(quint32 index,
790  const QString &portName,
791  const std::function<pa_operation *(pa_context *, uint32_t, const char *, pa_context_success_cb_t, void *)> &pa_set_port)
792 {
793  if (!m_context) {
794  return;
795  }
796  if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) {
797  qCWarning(PULSEAUDIOQT) << "pa_set_port failed";
798  return;
799  }
800 }
801 
802 void ContextPrivate::setGenericDeviceForStream(
803  quint32 streamIndex,
804  quint32 deviceIndex,
805  const std::function<pa_operation *(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t, void *)> &pa_move_stream_to_device)
806 {
807  if (!m_context) {
808  return;
809  }
810  if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) {
811  qCWarning(PULSEAUDIOQT) << "pa_move_stream_to_device failed";
812  return;
813  }
814 }
815 
816 void ContextPrivate::setGenericVolumes(
817  quint32 index,
818  QVector<qint64> channelVolumes,
819  pa_cvolume cVolume,
820  const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
821 {
822  if (!m_context) {
823  return;
824  }
825  Q_ASSERT(channelVolumes.count() == cVolume.channels);
826 
827  pa_cvolume newCVolume = cVolume;
828  for (int i = 0; i < channelVolumes.count(); ++i) {
829  newCVolume.values[i] = qBound<qint64>(0, channelVolumes.at(i), PA_VOLUME_MAX);
830  }
831 
832  if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) {
833  qCWarning(PULSEAUDIOQT) << "pa_set_volume failed";
834  return;
835  }
836 }
837 
838 void Context::setApplicationId(const QString &applicationId)
839 {
840  ContextPrivate::s_applicationId = applicationId;
841 }
842 
843 pa_context *Context::context() const
844 {
845  return d->m_context;
846 }
847 
848 } // PulseAudioQt
T * data()
The primary namespace of PulseAudioQt.
Definition: card.cpp:16
QDBusConnection sessionBus()
qint64 maximumVolume()
The maximum volume PulseAudio can store.
Definition: context.cpp:45
qint64 minimumVolume()
The minimum volume (0%).
Definition: context.cpp:40
const T & at(int i) const const
QByteArray toUtf8() const const
void serviceRegistered(const QString &serviceName)
qint64 normalVolume()
The normal volume (100%, 0 dB).
Definition: context.cpp:35
const char * constData() const const
QString name(StandardShortcut id)
KGuiItem reset()
int count(const T &value) const const
qint64 maximumUIVolume()
The maximum volume suitable to display in a UI.
Definition: context.cpp:50
QAbstractEventDispatcher * instance(QThread *thread)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Feb 7 2023 04:08:28 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.