1/* This file is part of the KDE project
2 Copyright (C) 2009 Colin Guthrie <cguthrie@mandriva.org>
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Lesser General Public
6 License as published by the Free Software Foundation; either
7 version 2.1 of the License, or (at your option) version 3, or any
8 later version accepted by the membership of KDE e.V. (or its
9 successor approved by the membership of KDE e.V.), Nokia Corporation
10 (or its successors, if any) and the KDE Free Qt Foundation, which shall
11 act as a proxy defined in Section 6 of version 3 of the license.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public
19 License along with this library. If not, see <http://www.gnu.org/licenses/>.
20
21*/
22
23#include <QtCore/QAbstractEventDispatcher>
24#include <QtCore/QEventLoop>
25#include <QtCore/QDebug>
26#include <QtCore/QStringList>
27
28#ifdef HAVE_PULSEAUDIO
29#include <glib.h>
30#include <pulse/pulseaudio.h>
31#include <pulse/xmalloc.h>
32#include <pulse/glib-mainloop.h>
33#ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER
34# include <pulse/ext-device-manager.h>
35#endif
36#endif // HAVE_PULSEAUDIO
37
38#include "pulsesupport.h"
39
40QT_BEGIN_NAMESPACE
41
42namespace Phonon
43{
44
45static PulseSupport* s_instance = NULL;
46
47#ifdef HAVE_PULSEAUDIO
48/***
49* Prints a conditional debug message based on the current debug level
50* If obj is provided, classname and objectname will be printed as well
51*
52* see debugLevel()
53*/
54
55static int debugLevel() {
56 static int level = -1;
57 if (level < 1) {
58 level = 0;
59 QByteArray pulseenv = qgetenv("PHONON_PULSEAUDIO_DEBUG");
60 int l = pulseenv.toInt();
61 if (l > 0)
62 level = (l > 2 ? 2 : l);
63 }
64 return level;
65}
66
67static void logMessage(const QString &message, int priority = 2, QObject *obj=0);
68static void logMessage(const QString &message, int priority, QObject *obj)
69{
70 if (debugLevel() > 0) {
71 QString output;
72 if (obj) {
73 // Strip away namespace from className
74 QByteArray className(obj->metaObject()->className());
75 int nameLength = className.length() - className.lastIndexOf(':') - 1;
76 className = className.right(nameLength);
77 output.sprintf("%s %s (%s %p)", message.toLatin1().constData(),
78 obj->objectName().toLatin1().constData(),
79 className.constData(), obj);
80 }
81 else {
82 output = message;
83 }
84 if (priority <= debugLevel()) {
85 qDebug() << QString::fromLatin1("PulseSupport(%1): %2").arg(priority).arg(output);
86 }
87 }
88}
89
90
91class AudioDevice
92{
93 public:
94 inline
95 AudioDevice(QString name, QString desc, QString icon, uint32_t index)
96 : pulseName(name), pulseIndex(index)
97 {
98 properties["name"] = desc;
99 properties["description"] = QLatin1String(""); // We don't have descriptions (well we do, but we use them as the name!)
100 properties["icon"] = icon;
101 properties["available"] = (index != PA_INVALID_INDEX);
102 properties["isAdvanced"] = false; // Nothing is advanced!
103 }
104
105 // Needed for QMap
106 inline AudioDevice() {}
107
108 QString pulseName;
109 uint32_t pulseIndex;
110 QHash<QByteArray, QVariant> properties;
111};
112bool operator!=(const AudioDevice &a, const AudioDevice &b)
113{
114 return !(a.pulseName == b.pulseName && a.properties == b.properties);
115}
116
117class PulseUserData
118{
119 public:
120 inline
121 PulseUserData()
122 {
123 }
124
125 QMap<QString, AudioDevice> newOutputDevices;
126 QMap<Phonon::Category, QMap<int, int> > newOutputDevicePriorities; // prio, device
127
128 QMap<QString, AudioDevice> newCaptureDevices;
129 QMap<Phonon::Category, QMap<int, int> > newCaptureDevicePriorities; // prio, device
130};
131
132static QMap<QString, Phonon::Category> s_roleCategoryMap;
133
134static bool s_pulseActive = false;
135
136static pa_glib_mainloop *s_mainloop = NULL;
137static pa_context *s_context = NULL;
138
139
140
141static int s_deviceIndexCounter = 0;
142
143static QMap<QString, int> s_outputDeviceIndexes;
144static QMap<int, AudioDevice> s_outputDevices;
145static QMap<Phonon::Category, QMap<int, int> > s_outputDevicePriorities; // prio, device
146static QMap<QString, uint32_t> s_outputStreamIndexMap;
147
148static QMap<QString, int> s_captureDeviceIndexes;
149static QMap<int, AudioDevice> s_captureDevices;
150static QMap<Phonon::Category, QMap<int, int> > s_captureDevicePriorities; // prio, device
151static QMap<QString, uint32_t> s_captureStreamIndexMap;
152
153static void createGenericDevices()
154{
155 // OK so we don't have the device manager extension, but we can show a single device and fake it.
156 int index;
157 s_outputDeviceIndexes.clear();
158 s_outputDevices.clear();
159 s_outputDevicePriorities.clear();
160 index = s_deviceIndexCounter++;
161 s_outputDeviceIndexes.insert(QLatin1String("sink:default"), index);
162 s_outputDevices.insert(index, AudioDevice(QLatin1String("sink:default"), QObject::tr("PulseAudio Sound Server"), QLatin1String("audio-backend-pulseaudio"), 0));
163 for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) {
164 Phonon::Category cat = static_cast<Phonon::Category>(i);
165 s_outputDevicePriorities[cat].insert(0, index);
166 }
167
168 s_captureDeviceIndexes.clear();
169 s_captureDevices.clear();
170 s_captureDevicePriorities.clear();
171 index = s_deviceIndexCounter++;
172 s_captureDeviceIndexes.insert(QLatin1String("source:default"), index);
173 s_captureDevices.insert(index, AudioDevice(QLatin1String("source:default"), QObject::tr("PulseAudio Sound Server"), QLatin1String("audio-backend-pulseaudio"), 0));
174 for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) {
175 Phonon::Category cat = static_cast<Phonon::Category>(i);
176 s_captureDevicePriorities[cat].insert(0, index);
177 }
178}
179
180#ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER
181static void ext_device_manager_read_cb(pa_context *c, const pa_ext_device_manager_info *info, int eol, void *userdata) {
182 Q_ASSERT(c);
183 Q_ASSERT(userdata);
184
185 PulseUserData *u = reinterpret_cast<PulseUserData*>(userdata);
186
187 if (eol < 0) {
188 logMessage(QString("Failed to initialize device manager extension: %1").arg(pa_strerror(pa_context_errno(c))));
189 logMessage("Falling back to single device mode");
190 createGenericDevices();
191 delete u;
192
193 // If this is our probe phase, exit now
194 if (s_context != c)
195 pa_context_disconnect(c);
196
197 return;
198 }
199
200 if (eol) {
201 // We're done reading the data, so order it by priority and copy it into the
202 // static variables where it can then be accessed by those classes that need it.
203
204 QMap<QString, AudioDevice>::iterator newdev_it;
205
206 // Check for new output devices or things changing about known output devices.
207 bool output_changed = false;
208 for (newdev_it = u->newOutputDevices.begin(); newdev_it != u->newOutputDevices.end(); ++newdev_it) {
209 QString name = newdev_it.key();
210
211 // The name + index map is always written when a new device is added.
212 Q_ASSERT(s_outputDeviceIndexes.contains(name));
213
214 int index = s_outputDeviceIndexes[name];
215 if (!s_outputDevices.contains(index)) {
216 // This is a totally new device
217 output_changed = true;
218 logMessage(QString("Brand New Output Device Found."));
219 s_outputDevices.insert(index, *newdev_it);
220 } else if (s_outputDevices[index] != *newdev_it) {
221 // We have this device already, but is it different?
222 output_changed = true;
223 logMessage(QString("Change to Existing Output Device (may be Added/Removed or something else)"));
224 s_outputDevices.remove(index);
225 s_outputDevices.insert(index, *newdev_it);
226 }
227 }
228 // Go through the output devices we know about and see if any are no longer mentioned in the list.
229 QMutableMapIterator<QString, int> output_existing_it(s_outputDeviceIndexes);
230 while (output_existing_it.hasNext()) {
231 output_existing_it.next();
232 if (!u->newOutputDevices.contains(output_existing_it.key())) {
233 output_changed = true;
234 logMessage(QString("Output Device Completely Removed"));
235 s_outputDevices.remove(output_existing_it.value());
236 output_existing_it.remove();
237 }
238 }
239
240 // Check for new capture devices or things changing about known capture devices.
241 bool capture_changed = false;
242 for (newdev_it = u->newCaptureDevices.begin(); newdev_it != u->newCaptureDevices.end(); ++newdev_it) {
243 QString name = newdev_it.key();
244
245 // The name + index map is always written when a new device is added.
246 Q_ASSERT(s_captureDeviceIndexes.contains(name));
247
248 int index = s_captureDeviceIndexes[name];
249 if (!s_captureDevices.contains(index)) {
250 // This is a totally new device
251 capture_changed = true;
252 logMessage(QString("Brand New Capture Device Found."));
253 s_captureDevices.insert(index, *newdev_it);
254 } else if (s_captureDevices[index] != *newdev_it) {
255 // We have this device already, but is it different?
256 capture_changed = true;
257 logMessage(QString("Change to Existing Capture Device (may be Added/Removed or something else)"));
258 s_captureDevices.remove(index);
259 s_captureDevices.insert(index, *newdev_it);
260 }
261 }
262 // Go through the capture devices we know about and see if any are no longer mentioned in the list.
263 QMutableMapIterator<QString, int> capture_existing_it(s_captureDeviceIndexes);
264 while (capture_existing_it.hasNext()) {
265 capture_existing_it.next();
266 if (!u->newCaptureDevices.contains(capture_existing_it.key())) {
267 capture_changed = true;
268 logMessage(QString("Capture Device Completely Removed"));
269 s_captureDevices.remove(capture_existing_it.value());
270 capture_existing_it.remove();
271 }
272 }
273
274 // Just copy accross the new priority lists as we know they are valid
275 if (s_outputDevicePriorities != u->newOutputDevicePriorities) {
276 output_changed = true;
277 s_outputDevicePriorities = u->newOutputDevicePriorities;
278 }
279 if (s_captureDevicePriorities != u->newCaptureDevicePriorities) {
280 capture_changed = true;
281 s_captureDevicePriorities = u->newCaptureDevicePriorities;
282 }
283
284 if (s_instance) {
285 // This wont be emitted durring the connection probe phase
286 // which is intensional
287 if (output_changed)
288 s_instance->emitObjectDescriptionChanged(AudioOutputDeviceType);
289 if (capture_changed)
290 s_instance->emitObjectDescriptionChanged(AudioCaptureDeviceType);
291 }
292
293 // We can free the user data as we will not be called again.
294 delete u;
295
296 // Some debug
297 logMessage(QString("Output Device Priority List:"));
298 for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) {
299 Phonon::Category cat = static_cast<Phonon::Category>(i);
300 if (s_outputDevicePriorities.contains(cat)) {
301 logMessage(QString(" Phonon Category %1").arg(cat));
302 int count = 0;
303 foreach (int j, s_outputDevicePriorities[cat]) {
304 QHash<QByteArray, QVariant> &props = s_outputDevices[j].properties;
305 logMessage(QString(" %1. %2 (Available: %3)").arg(++count).arg(props["name"].toString()).arg(props["available"].toBool()));
306 }
307 }
308 }
309 logMessage(QString("Capture Device Priority List:"));
310 for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) {
311 Phonon::Category cat = static_cast<Phonon::Category>(i);
312 if (s_captureDevicePriorities.contains(cat)) {
313 logMessage(QString(" Phonon Category %1").arg(cat));
314 int count = 0;
315 foreach (int j, s_captureDevicePriorities[cat]) {
316 QHash<QByteArray, QVariant> &props = s_captureDevices[j].properties;
317 logMessage(QString(" %1. %2 (Available: %3)").arg(++count).arg(props["name"].toString()).arg(props["available"].toBool()));
318 }
319 }
320 }
321
322 // If this is our probe phase, exit now as we're finished reading
323 // our device info and can exit and reconnect
324 if (s_context != c)
325 pa_context_disconnect(c);
326 }
327
328 if (!info)
329 return;
330
331 Q_ASSERT(info->name);
332 Q_ASSERT(info->description);
333 Q_ASSERT(info->icon);
334
335 // QString wrapper
336 QString name(info->name);
337 int index;
338 QMap<Phonon::Category, QMap<int, int> > *new_prio_map_cats; // prio, device
339 QMap<QString, AudioDevice> *new_devices;
340
341 if (name.startsWith("sink:")) {
342 new_devices = &u->newOutputDevices;
343 new_prio_map_cats = &u->newOutputDevicePriorities;
344
345 if (s_outputDeviceIndexes.contains(name))
346 index = s_outputDeviceIndexes[name];
347 else
348 index = s_outputDeviceIndexes[name] = s_deviceIndexCounter++;
349 } else if (name.startsWith("source:")) {
350 new_devices = &u->newCaptureDevices;
351 new_prio_map_cats = &u->newCaptureDevicePriorities;
352
353 if (s_captureDeviceIndexes.contains(name))
354 index = s_captureDeviceIndexes[name];
355 else
356 index = s_captureDeviceIndexes[name] = s_deviceIndexCounter++;
357 } else {
358 // This indicates a bug in pulseaudio.
359 return;
360 }
361
362 // Add the new device itself.
363 new_devices->insert(name, AudioDevice(name, info->description, info->icon, info->index));
364
365 // For each role in the priority, map it to a phonon category and store the order.
366 for (uint32_t i = 0; i < info->n_role_priorities; ++i) {
367 pa_ext_device_manager_role_priority_info* role_prio = &info->role_priorities[i];
368 Q_ASSERT(role_prio->role);
369
370 if (s_roleCategoryMap.contains(role_prio->role)) {
371 Phonon::Category cat = s_roleCategoryMap[role_prio->role];
372
373 (*new_prio_map_cats)[cat].insert(role_prio->priority, index);
374 }
375 }
376}
377
378static void ext_device_manager_subscribe_cb(pa_context *c, void *) {
379 Q_ASSERT(c);
380
381 pa_operation *o;
382 PulseUserData *u = new PulseUserData;
383 if (!(o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, u))) {
384 logMessage(QString("pa_ext_device_manager_read() failed."));
385 delete u;
386 return;
387 }
388 pa_operation_unref(o);
389}
390#endif
391
392void sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata) {
393 Q_UNUSED(userdata);
394 Q_ASSERT(c);
395
396 if (eol < 0) {
397 if (pa_context_errno(c) == PA_ERR_NOENTITY)
398 return;
399
400 logMessage(QLatin1String("Sink input callback failure"));
401 return;
402 }
403
404 if (eol > 0)
405 return;
406
407 Q_ASSERT(i);
408
409 // loop through (*i) and extract phonon->streamindex...
410 const char *t;
411 if ((t = pa_proplist_gets(i->proplist, "phonon.streamid"))) {
412 logMessage(QString::fromLatin1("Found PulseAudio stream index %1 for Phonon Output Stream %2").arg(i->index).arg(QLatin1String(t)));
413 s_outputStreamIndexMap[QLatin1String(t)] = i->index;
414
415 // Find the sink's phonon index and notify whoever cares...
416 if (PA_INVALID_INDEX != i->sink) {
417 bool found = false;
418 int device;
419 QMap<int, AudioDevice>::iterator it;
420 for (it = s_outputDevices.begin(); it != s_outputDevices.end(); ++it) {
421 if ((*it).pulseIndex == i->sink) {
422 found = true;
423 device = it.key();
424 break;
425 }
426 }
427 if (found) {
428 // OK so we just emit our signal
429 logMessage(QLatin1String("Letting the rest of phonon know about this"));
430 s_instance->emitUsingDevice(QLatin1String(t), device);
431 }
432 }
433 }
434}
435
436void source_output_cb(pa_context *c, const pa_source_output_info *i, int eol, void *userdata) {
437 Q_UNUSED(userdata);
438 Q_ASSERT(c);
439
440 if (eol < 0) {
441 if (pa_context_errno(c) == PA_ERR_NOENTITY)
442 return;
443
444 logMessage(QLatin1String("Source output callback failure"));
445 return;
446 }
447
448 if (eol > 0)
449 return;
450
451 Q_ASSERT(i);
452
453 // loop through (*i) and extract phonon->streamindex...
454 const char *t;
455 if ((t = pa_proplist_gets(i->proplist, "phonon.streamid"))) {
456 logMessage(QString::fromLatin1("Found PulseAudio stream index %1 for Phonon Capture Stream %2").arg(i->index).arg(QLatin1String(t)));
457 s_captureStreamIndexMap[QLatin1String(t)] = i->index;
458
459 // Find the source's phonon index and notify whoever cares...
460 if (PA_INVALID_INDEX != i->source) {
461 bool found = false;
462 int device;
463 QMap<int, AudioDevice>::iterator it;
464 for (it = s_captureDevices.begin(); it != s_captureDevices.end(); ++it) {
465 if ((*it).pulseIndex == i->source) {
466 found = true;
467 device = it.key();
468 break;
469 }
470 }
471 if (found) {
472 // OK so we just emit our signal
473 logMessage(QLatin1String("Letting the rest of phonon know about this"));
474 s_instance->emitUsingDevice(QLatin1String(t), device);
475 }
476 }
477 }
478}
479
480static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) {
481 Q_UNUSED(userdata);
482
483 switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
484 case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
485 if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
486 QString phononid = s_outputStreamIndexMap.key(index);
487 if (!phononid.isEmpty()) {
488 if (s_outputStreamIndexMap.contains(phononid)) {
489 logMessage(QString::fromLatin1("Phonon Output Stream %1 is gone at the PA end. Marking it as invalid in our cache as we may reuse it.").arg(phononid));
490 s_outputStreamIndexMap[phononid] = PA_INVALID_INDEX;
491 } else {
492 logMessage(QString::fromLatin1("Removing Phonon Output Stream %1 (it's gone!)").arg(phononid));
493 s_outputStreamIndexMap.remove(phononid);
494 }
495 }
496 } else {
497 pa_operation *o;
498 if (!(o = pa_context_get_sink_input_info(c, index, sink_input_cb, NULL))) {
499 logMessage(QLatin1String("pa_context_get_sink_input_info() failed"));
500 return;
501 }
502 pa_operation_unref(o);
503 }
504 break;
505
506 case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
507 if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
508 QString phononid = s_captureStreamIndexMap.key(index);
509 if (!phononid.isEmpty()) {
510 if (s_captureStreamIndexMap.contains(phononid)) {
511 logMessage(QString::fromLatin1("Phonon Capture Stream %1 is gone at the PA end. Marking it as invalid in our cache as we may reuse it.").arg(phononid));
512 s_captureStreamIndexMap[phononid] = PA_INVALID_INDEX;
513 } else {
514 logMessage(QString::fromLatin1("Removing Phonon Capture Stream %1 (it's gone!)").arg(phononid));
515 s_captureStreamIndexMap.remove(phononid);
516 }
517 }
518 } else {
519 pa_operation *o;
520 if (!(o = pa_context_get_source_output_info(c, index, source_output_cb, NULL))) {
521 logMessage(QLatin1String("pa_context_get_sink_input_info() failed"));
522 return;
523 }
524 pa_operation_unref(o);
525 }
526 break;
527 }
528}
529
530
531static QString statename(pa_context_state_t state)
532{
533 switch (state)
534 {
535 case PA_CONTEXT_UNCONNECTED: return QLatin1String("Unconnected");
536 case PA_CONTEXT_CONNECTING: return QLatin1String("Connecting");
537 case PA_CONTEXT_AUTHORIZING: return QLatin1String("Authorizing");
538 case PA_CONTEXT_SETTING_NAME: return QLatin1String("Setting Name");
539 case PA_CONTEXT_READY: return QLatin1String("Ready");
540 case PA_CONTEXT_FAILED: return QLatin1String("Failed");
541 case PA_CONTEXT_TERMINATED: return QLatin1String("Terminated");
542 }
543
544 return QString::fromLatin1("Unknown state: %0").arg(state);
545}
546
547static void context_state_callback(pa_context *c, void *)
548{
549 Q_ASSERT(c);
550
551 logMessage(QString::fromLatin1("context_state_callback %1").arg(statename(pa_context_get_state(c))));
552 pa_context_state_t state = pa_context_get_state(c);
553 if (state == PA_CONTEXT_READY) {
554 // We've connected to PA, so it is active
555 s_pulseActive = true;
556
557 // Attempt to load things up
558 pa_operation *o;
559
560 // 1. Register for the stream changes (except during probe)
561 if (s_context == c) {
562 pa_context_set_subscribe_callback(c, subscribe_cb, NULL);
563
564 if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)
565 (PA_SUBSCRIPTION_MASK_SINK_INPUT|
566 PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT), NULL, NULL))) {
567 logMessage(QLatin1String("pa_context_subscribe() failed"));
568 return;
569 }
570 pa_operation_unref(o);
571 }
572
573#ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER
574 // 2a. Attempt to initialise Device Manager info (except during probe)
575 if (s_context == c) {
576 pa_ext_device_manager_set_subscribe_cb(c, ext_device_manager_subscribe_cb, NULL);
577 if (!(o = pa_ext_device_manager_subscribe(c, 1, NULL, NULL))) {
578 logMessage(QString("pa_ext_device_manager_subscribe() failed"));
579 return;
580 }
581 pa_operation_unref(o);
582 }
583
584 // 3. Attempt to read info from Device Manager
585 PulseUserData *u = new PulseUserData;
586 if (!(o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, u))) {
587 logMessage(QString("pa_ext_device_manager_read() failed. Attempting to continue without device manager support"));
588 createGenericDevices();
589 delete u;
590
591 // If this is our probe phase, exit immediately
592 if (s_context != c)
593 pa_context_disconnect(c);
594
595 return;
596 }
597 pa_operation_unref(o);
598
599#else
600 // If we know do not have Device Manager support, we just create our dummy devices now
601 createGenericDevices();
602
603 // If this is our probe phase, exit immediately
604 if (s_context != c)
605 pa_context_disconnect(c);
606#endif
607 } else if (!PA_CONTEXT_IS_GOOD(state)) {
608 /// @todo Deal with reconnection...
609 //logMessage("Connection to PulseAudio lost");
610
611 // If this is our probe phase, exit our context immediately
612 if (s_context != c)
613 pa_context_disconnect(c);
614 }
615}
616#endif // HAVE_PULSEAUDIO
617
618
619PulseSupport* PulseSupport::getInstance()
620{
621 if (NULL == s_instance) {
622 s_instance = new PulseSupport();
623 }
624 return s_instance;
625}
626
627void PulseSupport::shutdown()
628{
629 if (NULL != s_instance) {
630 delete s_instance;
631 s_instance = NULL;
632 }
633}
634
635PulseSupport::PulseSupport()
636 : QObject(), mEnabled(false)
637{
638#ifdef HAVE_PULSEAUDIO
639 // Initialise our map (is there a better way to do this?)
640 s_roleCategoryMap[QLatin1String("none")] = Phonon::NoCategory;
641 s_roleCategoryMap[QLatin1String("video")] = Phonon::VideoCategory;
642 s_roleCategoryMap[QLatin1String("music")] = Phonon::MusicCategory;
643 s_roleCategoryMap[QLatin1String("game")] = Phonon::GameCategory;
644 s_roleCategoryMap[QLatin1String("event")] = Phonon::NotificationCategory;
645 s_roleCategoryMap[QLatin1String("phone")] = Phonon::CommunicationCategory;
646 //s_roleCategoryMap[QLatin1String("animation")]; // No Mapping
647 //s_roleCategoryMap[QLatin1String("production")]; // No Mapping
648 s_roleCategoryMap[QLatin1String("a11y")] = Phonon::AccessibilityCategory;
649
650 // To allow for easy debugging, give an easy way to disable this pulseaudio check
651 QByteArray pulseenv = qgetenv("PHONON_PULSEAUDIO_DISABLE");
652 if (pulseenv.toInt()) {
653 logMessage(QLatin1String("PulseAudio support disabled: PHONON_PULSEAUDIO_DISABLE is set"));
654 return;
655 }
656
657 // We require a glib event loop
658 if (strcmp(QAbstractEventDispatcher::instance()->metaObject()->className(),
659 "QGuiEventDispatcherGlib") != 0) {
660 logMessage(QLatin1String("Disabling PulseAudio integration for lack of GLib event loop."));
661 return;
662 }
663
664 // First of all conenct to PA via simple/blocking means and if that succeeds,
665 // use a fully async integrated mainloop method to connect and get proper support.
666 pa_mainloop *p_test_mainloop;
667 if (!(p_test_mainloop = pa_mainloop_new())) {
668 logMessage(QLatin1String("PulseAudio support disabled: Unable to create mainloop"));
669 return;
670 }
671
672 pa_context *p_test_context;
673 if (!(p_test_context = pa_context_new(pa_mainloop_get_api(p_test_mainloop), "libphonon-probe"))) {
674 logMessage(QLatin1String("PulseAudio support disabled: Unable to create context"));
675 pa_mainloop_free(p_test_mainloop);
676 return;
677 }
678
679 logMessage(QLatin1String("Probing for PulseAudio..."));
680 // (cg) Convert to PA_CONTEXT_NOFLAGS when PulseAudio 0.9.19 is required
681 if (pa_context_connect(p_test_context, NULL, static_cast<pa_context_flags_t>(0), NULL) < 0) {
682 logMessage(QString::fromLatin1("PulseAudio support disabled: %1").arg(QString::fromLocal8Bit(pa_strerror(pa_context_errno(p_test_context)))));
683 pa_context_disconnect(p_test_context);
684 pa_context_unref(p_test_context);
685 pa_mainloop_free(p_test_mainloop);
686 return;
687 }
688
689 pa_context_set_state_callback(p_test_context, &context_state_callback, NULL);
690 for (;;) {
691 pa_mainloop_iterate(p_test_mainloop, 1, NULL);
692
693 if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(p_test_context))) {
694 logMessage(QLatin1String("PulseAudio probe complete."));
695 break;
696 }
697 }
698 pa_context_disconnect(p_test_context);
699 pa_context_unref(p_test_context);
700 pa_mainloop_free(p_test_mainloop);
701
702 if (!s_pulseActive) {
703 logMessage(QLatin1String("PulseAudio support is not available."));
704 return;
705 }
706
707 // If we're still here, PA is available.
708 logMessage(QLatin1String("PulseAudio support enabled"));
709
710 // Now we connect for real using a proper main loop that we can forget
711 // all about processing.
712 s_mainloop = pa_glib_mainloop_new(NULL);
713 Q_ASSERT(s_mainloop);
714 pa_mainloop_api *api = pa_glib_mainloop_get_api(s_mainloop);
715
716 s_context = pa_context_new(api, "libphonon");
717 // (cg) Convert to PA_CONTEXT_NOFLAGS when PulseAudio 0.9.19 is required
718 if (pa_context_connect(s_context, NULL, static_cast<pa_context_flags_t>(0), 0) >= 0)
719 pa_context_set_state_callback(s_context, &context_state_callback, NULL);
720#endif
721}
722
723PulseSupport::~PulseSupport()
724{
725#ifdef HAVE_PULSEAUDIO
726 if (s_context) {
727 pa_context_disconnect(s_context);
728 s_context = NULL;
729 }
730
731 if (s_mainloop) {
732 pa_glib_mainloop_free(s_mainloop);
733 s_mainloop = NULL;
734 }
735#endif
736}
737
738bool PulseSupport::isActive()
739{
740#ifdef HAVE_PULSEAUDIO
741 return mEnabled && s_pulseActive;
742#else
743 return false;
744#endif
745}
746
747void PulseSupport::enable(bool enabled)
748{
749 mEnabled = enabled;
750}
751
752QList<int> PulseSupport::objectDescriptionIndexes(ObjectDescriptionType type) const
753{
754 QList<int> list;
755
756 if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType)
757 return list;
758
759#ifdef HAVE_PULSEAUDIO
760 if (s_pulseActive) {
761 switch (type) {
762
763 case AudioOutputDeviceType: {
764 QMap<QString, int>::iterator it;
765 for (it = s_outputDeviceIndexes.begin(); it != s_outputDeviceIndexes.end(); ++it) {
766 list.append(*it);
767 }
768 break;
769 }
770 case AudioCaptureDeviceType: {
771 QMap<QString, int>::iterator it;
772 for (it = s_captureDeviceIndexes.begin(); it != s_captureDeviceIndexes.end(); ++it) {
773 list.append(*it);
774 }
775 break;
776 }
777 default:
778 break;
779 }
780 }
781#endif
782
783 return list;
784}
785
786QHash<QByteArray, QVariant> PulseSupport::objectDescriptionProperties(ObjectDescriptionType type, int index) const
787{
788 QHash<QByteArray, QVariant> ret;
789
790 if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType)
791 return ret;
792
793#ifndef HAVE_PULSEAUDIO
794 Q_UNUSED(index);
795#else
796 if (s_pulseActive) {
797 switch (type) {
798
799 case AudioOutputDeviceType:
800 Q_ASSERT(s_outputDevices.contains(index));
801 ret = s_outputDevices[index].properties;
802 break;
803
804 case AudioCaptureDeviceType:
805 Q_ASSERT(s_captureDevices.contains(index));
806 ret = s_captureDevices[index].properties;
807 break;
808
809 default:
810 break;
811 }
812 }
813#endif
814
815 return ret;
816}
817
818QList<int> PulseSupport::objectIndexesByCategory(ObjectDescriptionType type, Category category) const
819{
820 QList<int> ret;
821
822 if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType)
823 return ret;
824
825#ifndef HAVE_PULSEAUDIO
826 Q_UNUSED(category);
827#else
828 if (s_pulseActive) {
829 switch (type) {
830
831 case AudioOutputDeviceType:
832 if (s_outputDevicePriorities.contains(category))
833 ret = s_outputDevicePriorities[category].values();
834 break;
835
836 case AudioCaptureDeviceType:
837 if (s_captureDevicePriorities.contains(category))
838 ret = s_captureDevicePriorities[category].values();
839 break;
840
841 default:
842 break;
843 }
844 }
845#endif
846
847 return ret;
848}
849
850#ifdef HAVE_PULSEAUDIO
851static void setDevicePriority(Category category, QStringList list)
852{
853 QString role = s_roleCategoryMap.key(category);
854 if (role.isEmpty())
855 return;
856
857 logMessage(QString::fromLatin1("Reindexing %1: %2").arg(role).arg(list.join(QLatin1String(", "))));
858
859 char **devices;
860 devices = pa_xnew(char *, list.size()+1);
861 int i = 0;
862 foreach (QString str, list) {
863 devices[i++] = pa_xstrdup(str.toUtf8().constData());
864 }
865 devices[list.size()] = NULL;
866
867#ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER
868 pa_operation *o;
869 if (!(o = pa_ext_device_manager_reorder_devices_for_role(s_context, role.toUtf8().constData(), (const char**)devices, NULL, NULL)))
870 logMessage(QString("pa_ext_device_manager_reorder_devices_for_role() failed"));
871 else
872 pa_operation_unref(o);
873#endif
874
875 for (i = 0; i < list.size(); ++i)
876 pa_xfree(devices[i]);
877 pa_xfree(devices);
878}
879#endif
880
881void PulseSupport::setOutputDevicePriorityForCategory(Category category, QList<int> order)
882{
883#ifndef HAVE_PULSEAUDIO
884 Q_UNUSED(category);
885 Q_UNUSED(order);
886#else
887 QStringList list;
888 QList<int>::iterator it;
889
890 for (it = order.begin(); it != order.end(); ++it) {
891 if (s_outputDevices.contains(*it)) {
892 list << s_outputDeviceIndexes.key(*it);
893 }
894 }
895 setDevicePriority(category, list);
896#endif
897}
898
899void PulseSupport::setCaptureDevicePriorityForCategory(Category category, QList<int> order)
900{
901#ifndef HAVE_PULSEAUDIO
902 Q_UNUSED(category);
903 Q_UNUSED(order);
904#else
905 QStringList list;
906 QList<int>::iterator it;
907
908 for (it = order.begin(); it != order.end(); ++it) {
909 if (s_captureDevices.contains(*it)) {
910 list << s_captureDeviceIndexes.key(*it);
911 }
912 }
913 setDevicePriority(category, list);
914#endif
915}
916
917void PulseSupport::setStreamPropList(Category category, QString streamUuid)
918{
919#ifndef HAVE_PULSEAUDIO
920 Q_UNUSED(category);
921 Q_UNUSED(streamUuid);
922#else
923 QString role = s_roleCategoryMap.key(category);
924 if (role.isEmpty())
925 return;
926
927 logMessage(QString::fromLatin1("Setting role to %1 for streamindex %2").arg(role).arg(streamUuid));
928 setenv("PULSE_PROP_media.role", role.toLatin1().constData(), 1);
929 setenv("PULSE_PROP_phonon.streamid", streamUuid.toLatin1().constData(), 1);
930#endif
931}
932
933void PulseSupport::emitObjectDescriptionChanged(ObjectDescriptionType type)
934{
935 emit objectDescriptionChanged(type);
936}
937
938void PulseSupport::emitUsingDevice(QString streamUuid, int device)
939{
940 emit usingDevice(streamUuid, device);
941}
942
943bool PulseSupport::setOutputDevice(QString streamUuid, int device) {
944#ifndef HAVE_PULSEAUDIO
945 Q_UNUSED(streamUuid);
946 Q_UNUSED(device);
947 return false;
948#else
949 if (s_outputDevices.size() < 2)
950 return true;
951
952 if (!s_outputDevices.contains(device)) {
953 logMessage(QString::fromLatin1("Attempting to set Output Device for invalid device id %1.").arg(device));
954 return false;
955 }
956 const QVariant var = s_outputDevices[device].properties["name"];
957 logMessage(QString::fromLatin1("Attempting to set Output Device to '%1' for Output Stream %2").arg(var.toString()).arg(streamUuid));
958
959 // Attempt to look up the pulse stream index.
960 if (s_outputStreamIndexMap.contains(streamUuid) && s_outputStreamIndexMap[streamUuid] != PA_INVALID_INDEX) {
961 logMessage(QLatin1String("... Found in map. Moving now"));
962
963 uint32_t pulse_device_index = s_outputDevices[device].pulseIndex;
964 uint32_t pulse_stream_index = s_outputStreamIndexMap[streamUuid];
965
966 logMessage(QString::fromLatin1("Moving Pulse Sink Input %1 to '%2' (Pulse Sink %3)").arg(pulse_stream_index).arg(var.toString()).arg(pulse_device_index));
967
968 /// @todo Find a way to move the stream without saving it... We don't want to pollute the stream restore db.
969 pa_operation* o;
970 if (!(o = pa_context_move_sink_input_by_index(s_context, pulse_stream_index, pulse_device_index, NULL, NULL))) {
971 logMessage(QLatin1String("pa_context_move_sink_input_by_index() failed"));
972 return false;
973 }
974 pa_operation_unref(o);
975 } else {
976 logMessage(QLatin1String("... Not found in map. We will be notified of the device when the stream appears and we can process any moves needed then"));
977 }
978 return true;
979#endif
980}
981
982bool PulseSupport::setCaptureDevice(QString streamUuid, int device) {
983#ifndef HAVE_PULSEAUDIO
984 Q_UNUSED(streamUuid);
985 Q_UNUSED(device);
986 return false;
987#else
988 if (s_captureDevices.size() < 2)
989 return true;
990
991 if (!s_captureDevices.contains(device)) {
992 logMessage(QString::fromLatin1("Attempting to set Capture Device for invalid device id %1.").arg(device));
993 return false;
994 }
995 const QVariant var = s_captureDevices[device].properties["name"];
996 logMessage(QString::fromLatin1("Attempting to set Capture Device to '%1' for Capture Stream %2").arg(var.toString()).arg(streamUuid));
997
998 // Attempt to look up the pulse stream index.
999 if (s_captureStreamIndexMap.contains(streamUuid) && s_captureStreamIndexMap[streamUuid] == PA_INVALID_INDEX) {
1000 logMessage(QString::fromLatin1("... Found in map. Moving now"));
1001
1002 uint32_t pulse_device_index = s_captureDevices[device].pulseIndex;
1003 uint32_t pulse_stream_index = s_captureStreamIndexMap[streamUuid];
1004
1005 logMessage(QString::fromLatin1("Moving Pulse Source Output %1 to '%2' (Pulse Sink %3)").arg(pulse_stream_index).arg(var.toString()).arg(pulse_device_index));
1006
1007 /// @todo Find a way to move the stream without saving it... We don't want to pollute the stream restore db.
1008 pa_operation* o;
1009 if (!(o = pa_context_move_source_output_by_index(s_context, pulse_stream_index, pulse_device_index, NULL, NULL))) {
1010 logMessage(QString::fromLatin1("pa_context_move_source_output_by_index() failed"));
1011 return false;
1012 }
1013 pa_operation_unref(o);
1014 } else {
1015 logMessage(QString::fromLatin1("... Not found in map. We will be notified of the device when the stream appears and we can process any moves needed then"));
1016 }
1017 return true;
1018#endif
1019}
1020
1021void PulseSupport::clearStreamCache(QString streamUuid) {
1022#ifndef HAVE_PULSEAUDIO
1023 Q_UNUSED(streamUuid);
1024 return;
1025#else
1026 logMessage(QString::fromLatin1("Clearing stream cache for stream %1").arg(streamUuid));
1027 s_outputStreamIndexMap.remove(streamUuid);
1028 s_captureStreamIndexMap.remove(streamUuid);
1029#endif
1030}
1031
1032} // namespace Phonon
1033
1034QT_END_NAMESPACE
1035
1036#include "moc_pulsesupport.cpp"
1037
1038// vim: sw=4 ts=4
1039