add more spacing
[personal-kdebase.git] / runtime / phonon / kded-module / phononserver.cpp
blob99ff255ab45976419c273874aafd156e2cb616c0
1 /* This file is part of the KDE project
2 Copyright (C) 2008 Matthias Kretz <kretz@kde.org>
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License as
6 published by the Free Software Foundation; either version 2 of
7 the License, or (at your option) version 3.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
21 #include "phononserver.h"
22 #include "audiodevice.h"
24 #include <kconfiggroup.h>
25 #include <kprocess.h>
26 #include <kstandarddirs.h>
27 #include <kmessagebox.h>
28 #include <klocale.h>
29 #include <kdebug.h>
30 #include <kdialog.h>
31 #include <KPluginFactory>
32 #include <KPluginLoader>
33 #include <QtCore/QDir>
34 #include <QtCore/QFile>
35 #include <QtCore/QFileSystemWatcher>
36 #include <QtCore/QRegExp>
37 #include <QtCore/QSettings>
38 #include <QtCore/QTimerEvent>
39 #include <QtDBus/QDBusConnection>
40 #include <QtDBus/QDBusMessage>
41 #include <QtDBus/QDBusMetaType>
42 #include <QtCore/QVariant>
43 #include <Solid/AudioInterface>
44 #include <Solid/GenericInterface>
45 #include <Solid/Device>
46 #include <Solid/DeviceNotifier>
48 #ifdef HAVE_PULSEAUDIO
49 #include <pulse/pulseaudio.h>
50 #endif // HAVE_PULSEAUDIO
52 #include <../config-alsa.h>
53 #ifdef HAVE_LIBASOUND2
54 #include <alsa/asoundlib.h>
55 #endif // HAVE_LIBASOUND2
57 K_PLUGIN_FACTORY(PhononServerFactory,
58 registerPlugin<PhononServer>();
60 K_EXPORT_PLUGIN(PhononServerFactory("phononserver"))
62 typedef QList<QPair<QByteArray, QString> > PhononDeviceAccessList;
63 Q_DECLARE_METATYPE(PhononDeviceAccessList)
65 PhononServer::PhononServer(QObject *parent, const QList<QVariant> &)
66 : KDEDModule(parent),
67 m_config(KSharedConfig::openConfig("phonondevicesrc", KConfig::SimpleConfig))
69 findDevices();
70 connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(const QString &)), SLOT(deviceAdded(const QString &)));
71 connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(const QString &)), SLOT(deviceRemoved(const QString &)));
72 qRegisterMetaType<PhononDeviceAccessList>();
73 qRegisterMetaTypeStreamOperators<PhononDeviceAccessList>("PhononDeviceAccessList");
76 PhononServer::~PhononServer()
80 static QString uniqueId(const Solid::Device &device, int deviceNumber)
82 const Solid::GenericInterface *genericIface = device.as<Solid::GenericInterface>();
83 Q_ASSERT(genericIface);
84 const QString &subsystem = genericIface->propertyExists(QLatin1String("info.subsystem")) ?
85 genericIface->property(QLatin1String("info.subsystem")).toString() :
86 genericIface->property(QLatin1String("linux.subsystem")).toString();
87 if (subsystem == "pci") {
88 const QVariant vendor_id = genericIface->property("pci.vendor_id");
89 if (vendor_id.isValid()) {
90 const QVariant product_id = genericIface->property("pci.product_id");
91 if (product_id.isValid()) {
92 const QVariant subsys_vendor_id = genericIface->property("pci.subsys_vendor_id");
93 if (subsys_vendor_id.isValid()) {
94 const QVariant subsys_product_id = genericIface->property("pci.subsys_product_id");
95 if (subsys_product_id.isValid()) {
96 return QString("pci:%1:%2:%3:%4:%5")
97 .arg(vendor_id.toInt(), 4, 16, QLatin1Char('0'))
98 .arg(product_id.toInt(), 4, 16, QLatin1Char('0'))
99 .arg(subsys_vendor_id.toInt(), 4, 16, QLatin1Char('0'))
100 .arg(subsys_product_id.toInt(), 4, 16, QLatin1Char('0'))
101 .arg(deviceNumber);
106 } else if (subsystem == "usb" || subsystem == "usb_device") {
107 const QVariant vendor_id = genericIface->property("usb.vendor_id");
108 if (vendor_id.isValid()) {
109 const QVariant product_id = genericIface->property("usb.product_id");
110 if (product_id.isValid()) {
111 return QString("usb:%1:%2:%3")
112 .arg(vendor_id.toInt(), 4, 16, QLatin1Char('0'))
113 .arg(product_id.toInt(), 4, 16, QLatin1Char('0'))
114 .arg(deviceNumber);
117 } else {
118 // not the right device, need to look at the parent (but not at the top-level device in the
119 // device tree - that would be too far up the hierarchy)
120 if (device.parent().isValid() && device.parent().parent().isValid()) {
121 return uniqueId(device.parent(), deviceNumber);
124 return QString();
127 static void renameDevices(QList<PS::AudioDevice> *devicelist)
129 QHash<QString, int> cardNames;
130 foreach (const PS::AudioDevice &dev, *devicelist) {
131 ++cardNames[dev.name()];
134 // Now look for duplicate names and rename those by appending the device number
135 QMutableListIterator<PS::AudioDevice> it(*devicelist);
136 while (it.hasNext()) {
137 PS::AudioDevice &dev = it.next();
138 if (dev.deviceNumber() > 0 && cardNames.value(dev.name()) > 1) {
139 dev.setPreferredName(dev.name() + QLatin1String(" #") + QString::number(dev.deviceNumber()));
144 struct DeviceHint
146 QString name;
147 QString description;
150 static inline QDebug operator<<(QDebug &d, const DeviceHint &h)
152 d.nospace() << h.name << " (" << h.description << ")";
153 return d;
156 void PhononServer::findVirtualDevices()
158 #ifdef HAVE_LIBASOUND2
159 QList<DeviceHint> deviceHints;
161 // update config to the changes on disc
162 snd_config_update_free_global();
163 snd_config_update();
165 void **hints;
166 //snd_config_update();
167 if (snd_device_name_hint(-1, "pcm", &hints) < 0) {
168 kDebug(601) << "snd_device_name_hint failed for 'pcm'";
169 return;
172 for (void **cStrings = hints; *cStrings; ++cStrings) {
173 DeviceHint nextHint;
174 char *x = snd_device_name_get_hint(*cStrings, "NAME");
175 nextHint.name = QString::fromUtf8(x);
176 free(x);
178 if (nextHint.name.startsWith("front:") ||
179 /*nextHint.name.startsWith("rear:") ||
180 nextHint.name.startsWith("center_lfe:") ||*/
181 nextHint.name.startsWith("surround40:") ||
182 nextHint.name.startsWith("surround41:") ||
183 nextHint.name.startsWith("surround50:") ||
184 nextHint.name.startsWith("surround51:") ||
185 nextHint.name.startsWith("surround71:") ||
186 nextHint.name.startsWith("default:") ||
187 nextHint.name == "null"
189 continue;
192 x = snd_device_name_get_hint(*cStrings, "DESC");
193 nextHint.description = QString::fromUtf8(x);
194 free(x);
196 deviceHints << nextHint;
198 snd_device_name_free_hint(hints);
199 kDebug(601) << deviceHints;
201 snd_config_update_free_global();
202 snd_config_update();
203 Q_ASSERT(snd_config);
204 foreach (const DeviceHint &deviceHint, deviceHints) {
205 const QString &alsaDeviceName = deviceHint.name;
206 const QString &description = deviceHint.description;
207 const QString &uniqueId = description;
208 //const QString &udi = alsaDeviceName;
209 const QStringList &lines = description.split("\n");
210 bool isAdvanced = false;
211 QString cardName = lines.first();
212 if (lines.size() > 1) {
213 cardName = i18nc("%1 is the sound card name, %2 is the description in case it exists", "%1 (%2)", cardName, lines[1]);
215 if (alsaDeviceName.startsWith("front:") ||
216 alsaDeviceName.startsWith("rear:") ||
217 alsaDeviceName.startsWith("center_lfe:") ||
218 alsaDeviceName.startsWith("surround40:") ||
219 alsaDeviceName.startsWith("surround41:") ||
220 alsaDeviceName.startsWith("surround50:") ||
221 alsaDeviceName.startsWith("surround51:") ||
222 alsaDeviceName.startsWith("surround71:") ||
223 alsaDeviceName.startsWith("iec958:")) {
224 isAdvanced = true;
227 bool available = false;
228 bool playbackDevice = false;
229 bool captureDevice = false;
231 snd_pcm_t *pcm;
232 const QByteArray &deviceNameEnc = alsaDeviceName.toUtf8();
233 if (0 == snd_pcm_open(&pcm, deviceNameEnc.constData(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK /*open mode: non-blocking, sync */)) {
234 available = true;
235 playbackDevice = true;
236 snd_pcm_close(pcm);
238 if (0 == snd_pcm_open(&pcm, deviceNameEnc.constData(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK /*open mode: non-blocking, sync */)) {
239 available = true;
240 captureDevice = true;
241 snd_pcm_close(pcm);
245 QString iconName(QLatin1String("audio-card"));
246 int initialPreference = 30;
247 if (description.contains("headset", Qt::CaseInsensitive) ||
248 description.contains("headphone", Qt::CaseInsensitive)) {
249 // it's a headset
250 if (description.contains("usb", Qt::CaseInsensitive)) {
251 iconName = QLatin1String("audio-headset-usb");
252 initialPreference -= 10;
253 } else {
254 iconName = QLatin1String("audio-headset");
255 initialPreference -= 10;
257 } else {
258 if (description.contains("usb", Qt::CaseInsensitive)) {
259 // it's an external USB device
260 iconName = QLatin1String("audio-card-usb");
261 initialPreference -= 10;
265 const PS::AudioDeviceAccess access(QStringList(alsaDeviceName), 0, PS::AudioDeviceAccess::AlsaDriver,
266 captureDevice, playbackDevice);
267 //dev.setUseCache(false);
268 if (playbackDevice) {
269 const PS::AudioDeviceKey key = { uniqueId + QLatin1String("_playback"), -1, -1 };
270 PS::AudioDevice dev(cardName, iconName, key, initialPreference, isAdvanced);
271 dev.addAccess(access);
272 m_audioOutputDevices << dev;
274 if (captureDevice) {
275 const PS::AudioDeviceKey key = { uniqueId + QLatin1String("_capture"), -1, -1 };
276 PS::AudioDevice dev(cardName, iconName, key, initialPreference, isAdvanced);
277 dev.addAccess(access);
278 m_audioCaptureDevices << dev;
279 } else {
280 if (!playbackDevice) {
281 kDebug(601) << deviceHint.name << " doesn't work.";
286 const QString etcFile(QLatin1String("/etc/asound.conf"));
287 const QString homeFile(QDir::homePath() + QLatin1String("/.asoundrc"));
288 const bool etcExists = QFile::exists(etcFile);
289 const bool homeExists = QFile::exists(homeFile);
290 if (etcExists || homeExists) {
291 static QFileSystemWatcher *watcher = 0;
292 if (!watcher) {
293 watcher = new QFileSystemWatcher(this);
294 connect(watcher, SIGNAL(fileChanged(QString)), SLOT(alsaConfigChanged()));
296 // QFileSystemWatcher stops monitoring after a file got removed. Many editors save files by
297 // writing to a temp file and moving it over the other one. QFileSystemWatcher seems to
298 // interpret that as removing and stops watching a file after it got modified by an editor.
299 if (etcExists && !watcher->files().contains(etcFile)) {
300 kDebug(601) << "setup QFileSystemWatcher for" << etcFile;
301 watcher->addPath(etcFile);
303 if (homeExists && !watcher->files().contains(homeFile)) {
304 kDebug(601) << "setup QFileSystemWatcher for" << homeFile;
305 watcher->addPath(homeFile);
308 #endif // HAVE_LIBASOUND2
311 void PhononServer::alsaConfigChanged()
313 kDebug(601);
314 m_updateDeviceListing.start(50, this);
317 static void removeOssOnlyDevices(QList<PS::AudioDevice> *list)
319 QMutableListIterator<PS::AudioDevice> it(*list);
320 while (it.hasNext()) {
321 const PS::AudioDevice &dev = it.next();
322 if (dev.isAvailable()) {
323 bool onlyOss = true;
324 foreach (const PS::AudioDeviceAccess &a, dev.accessList()) {
325 if (a.driver() != PS::AudioDeviceAccess::OssDriver) {
326 onlyOss = false;
327 break;
330 if (onlyOss) {
331 it.remove();
337 #ifdef HAVE_PULSEAUDIO
338 class PulseDetectionUserData
340 public:
341 inline PulseDetectionUserData(PhononServer *p, pa_mainloop_api *api)
342 : phononServer(p), mainloopApi(api), ready(2),
343 alsaHandleMatches(QLatin1String(".*\\s(plughw|hw|front|surround\\d\\d):(\\d+)\\s.*")),
344 captureNameMatches(QLatin1String(".*_sound_card_(\\d+)_.*_(?:playback|capture)_(\\d+)(\\.monitor)?")),
345 playbackNameMatches(QLatin1String(".*_sound_card_(\\d+)_.*_playback_(\\d+)"))
348 PhononServer *const phononServer;
349 QList<QPair<PS::AudioDeviceKey, PS::AudioDeviceAccess> > sinks;
350 QList<QPair<PS::AudioDeviceKey, PS::AudioDeviceAccess> > sources;
352 inline void eol() { if (--ready == 0) { quit(); } }
353 inline void quit() { mainloopApi->quit(mainloopApi, 0); }
354 private:
355 pa_mainloop_api *const mainloopApi;
356 int ready;
357 public:
358 QRegExp alsaHandleMatches;
359 QRegExp captureNameMatches;
360 QRegExp playbackNameMatches;
363 static void pulseSinkInfoListCallback(pa_context *context, const pa_sink_info *i, int eol, void *userdata)
365 PulseDetectionUserData *d = reinterpret_cast<PulseDetectionUserData *>(userdata);
366 if (eol) {
367 d->eol();
368 return;
370 Q_ASSERT(i);
371 kDebug(601).nospace()
372 << "name: " << i->name
373 << ", index: " << i->index
374 << ", description: " << i->description
375 << ", sample_spec: " << i->sample_spec.format << i->sample_spec.rate << i->sample_spec.channels
376 << ", channel_map: " << i->channel_map.channels << i->channel_map.map
377 << ", owner_module: " << i->owner_module
378 //<< ", volume: " << i->volume
379 << ", mute: " << i->mute
380 << ", monitor_source: " << i->monitor_source
381 << ", latency: " << i->latency
382 << ", driver: " << i->driver
383 << ", flags: " << i->flags;
384 const QString &handle = QString::fromUtf8(i->name);
385 if (d->playbackNameMatches.exactMatch(handle)) {
386 const QString &description = QString::fromUtf8(i->description);
387 const bool m = d->alsaHandleMatches.exactMatch(description);
388 const int cardNumber = m ? d->alsaHandleMatches.cap(2).toInt() : -1; // card_name_X in the name always has X == 0 ;( so we can't use d->playbackNameMatches.cap(1).toInt();
389 const int deviceNumber = d->playbackNameMatches.cap(2).toInt();
390 const PS::AudioDeviceKey key = { QString(), cardNumber, deviceNumber };
391 const PS::AudioDeviceAccess access(QStringList(QString::fromUtf8(pa_context_get_server(context)) + QLatin1Char('\n') + handle), 30, PS::AudioDeviceAccess::PulseAudioDriver, false, true);
392 d->sinks << QPair<PS::AudioDeviceKey, PS::AudioDeviceAccess>(key, access);
396 static void pulseSourceInfoListCallback(pa_context *context, const pa_source_info *i, int eol, void *userdata)
398 PulseDetectionUserData *d = reinterpret_cast<PulseDetectionUserData *>(userdata);
399 if (eol) {
400 d->eol();
401 return;
403 Q_ASSERT(i);
404 kDebug(601).nospace()
405 << "name: " << i->name
406 << ", index: " << i->index
407 << ", description: " << i->description
408 << ", sample_spec: " << i->sample_spec.format << i->sample_spec.rate << i->sample_spec.channels
409 << ", channel_map: " << i->channel_map.channels << i->channel_map.map
410 << ", owner_module: " << i->owner_module
411 //<< ", volume: " << i->volume
412 << ", mute: " << i->mute
413 << ", monitor_of_sink: " << i->monitor_of_sink
414 << ", monitor_of_sink_name: " << i->monitor_of_sink_name
415 << ", latency: " << i->latency
416 << ", driver: " << i->driver
417 << ", flags: " << i->flags;
418 const QString &handle = QString::fromUtf8(i->name);
419 if (d->captureNameMatches.exactMatch(handle)) {
420 if (d->captureNameMatches.cap(3).isEmpty()) {
421 const QString &description = QString::fromUtf8(i->description);
422 const bool m = d->alsaHandleMatches.exactMatch(description);
423 const int cardNumber = m ? d->alsaHandleMatches.cap(2).toInt() : d->captureNameMatches.cap(1).toInt();
424 const int deviceNumber = d->captureNameMatches.cap(2).toInt();
425 const PS::AudioDeviceKey key = {
426 d->captureNameMatches.cap(3).isEmpty() ? QString() : handle, cardNumber, deviceNumber
428 const PS::AudioDeviceAccess access(QStringList(QString::fromUtf8(pa_context_get_server(context)) + QLatin1Char(':') + handle), 30, PS::AudioDeviceAccess::PulseAudioDriver, true, false);
429 d->sources << QPair<PS::AudioDeviceKey, PS::AudioDeviceAccess>(key, access);
430 } else {
431 const PS::AudioDeviceKey key = {
432 QString::fromUtf8(i->description), -2, -2
434 const PS::AudioDeviceAccess access(QStringList(QString::fromUtf8(pa_context_get_server(context)) + QLatin1Char(':') + handle), 30, PS::AudioDeviceAccess::PulseAudioDriver, true, false);
435 d->sources << QPair<PS::AudioDeviceKey, PS::AudioDeviceAccess>(key, access);
440 static void pulseContextStateCallback(pa_context *context, void *userdata)
442 switch (pa_context_get_state(context)) {
443 case PA_CONTEXT_READY:
444 /*pa_operation *op1 =*/ pa_context_get_sink_info_list(context, &pulseSinkInfoListCallback, userdata);
445 /*pa_operation *op2 =*/ pa_context_get_source_info_list(context, &pulseSourceInfoListCallback, userdata);
446 break;
447 case PA_CONTEXT_FAILED:
449 PulseDetectionUserData *d = reinterpret_cast<PulseDetectionUserData *>(userdata);
450 d->quit();
452 break;
453 default:
454 break;
457 #endif // HAVE_PULSEAUDIO
459 void PhononServer::findDevices()
461 QHash<PS::AudioDeviceKey, PS::AudioDevice> playbackDevices;
462 QHash<PS::AudioDeviceKey, PS::AudioDevice> captureDevices;
463 bool haveAlsaDevices = false;
464 QHash<QString, QList<int> > listOfCardNumsPerUniqueId;
466 KConfigGroup globalConfigGroup(m_config, "Globals");
467 //const int cacheVersion = globalConfigGroup.readEntry("CacheVersion", 0);
468 // cacheVersion 1 is KDE 4.1, 0 is KDE 4.0
470 const QList<Solid::Device> &allHwDevices =
471 Solid::Device::listFromQuery("AudioInterface.deviceType & 'AudioInput|AudioOutput'");
472 foreach (const Solid::Device &hwDevice, allHwDevices) {
473 const Solid::AudioInterface *audioIface = hwDevice.as<Solid::AudioInterface>();
475 QStringList deviceIds;
476 int accessPreference = 0;
477 PS::AudioDeviceAccess::AudioDriver driver = PS::AudioDeviceAccess::InvalidDriver;
478 bool capture = audioIface->deviceType() & Solid::AudioInterface::AudioInput;
479 bool playback = audioIface->deviceType() & Solid::AudioInterface::AudioOutput;
480 bool valid = true;
481 bool isAdvanced = false;
482 bool preferCardName = false;
483 int cardNum = -1;
484 int deviceNum = -1;
486 switch (audioIface->driver()) {
487 case Solid::AudioInterface::UnknownAudioDriver:
488 valid = false;
489 break;
490 case Solid::AudioInterface::Alsa:
491 if (audioIface->driverHandle().type() != QVariant::List) {
492 valid = false;
493 } else {
494 haveAlsaDevices = true;
495 // ALSA has better naming of the device than the corresponding OSS entry in HAL
496 preferCardName = true;
497 const QList<QVariant> handles = audioIface->driverHandle().toList();
498 if (handles.size() < 1) {
499 valid = false;
500 } else {
501 driver = PS::AudioDeviceAccess::AlsaDriver;
502 accessPreference += 10;
504 bool ok;
505 cardNum = handles.first().toInt(&ok);
506 if (!ok) {
507 cardNum = -1;
509 const QString &cardStr = handles.first().toString();
510 // the first is either an int (card number) or a QString (card id)
511 QString x_phononId = QLatin1String("x-phonon:CARD=") + cardStr;
512 QString fallbackId = QLatin1String("plughw:CARD=") + cardStr;
513 if (handles.size() > 1 && handles.at(1).isValid()) {
514 deviceNum = handles.at(1).toInt();
515 const QString deviceStr = handles.at(1).toString();
516 if (deviceNum == 0) {
517 // prefer DEV=0 devices over DEV>0
518 accessPreference += 1;
519 } else {
520 isAdvanced = true;
522 x_phononId += ",DEV=" + deviceStr;
523 fallbackId += ",DEV=" + deviceStr;
524 if (handles.size() > 2 && handles.at(2).isValid()) {
525 x_phononId += ",SUBDEV=" + handles.at(2).toString();
526 fallbackId += ",SUBDEV=" + handles.at(2).toString();
529 deviceIds << x_phononId << fallbackId;
532 break;
533 case Solid::AudioInterface::OpenSoundSystem:
534 if (audioIface->driverHandle().type() != QVariant::String) {
535 valid = false;
536 } else {
537 const Solid::GenericInterface *genericIface =
538 hwDevice.as<Solid::GenericInterface>();
539 Q_ASSERT(genericIface);
540 cardNum = genericIface->property("oss.card").toInt();
541 deviceNum = genericIface->property("oss.device").toInt();
542 driver = PS::AudioDeviceAccess::OssDriver;
543 deviceIds << audioIface->driverHandle().toString();
545 break;
548 if (!valid || audioIface->soundcardType() == Solid::AudioInterface::Modem) {
549 continue;
552 m_udisOfAudioDevices.append(hwDevice.udi());
554 const PS::AudioDeviceAccess devAccess(deviceIds, accessPreference, driver, capture,
555 playback);
556 int initialPreference = 36 - deviceNum;
558 QString uniqueIdPrefix = uniqueId(hwDevice, deviceNum);
559 // "fix" cards that have the same identifiers, i.e. there's no way for the computer to tell
560 // them apart.
561 // We see that there's a problematic case if the same uniqueIdPrefix has been used for a
562 // different cardNum before. In that case we need to append another number to the
563 // uniqueIdPrefix. The first different cardNum gets a :i1, the second :i2, and so on.
564 QList<int> &cardsForUniqueId = listOfCardNumsPerUniqueId[uniqueIdPrefix];
565 if (cardsForUniqueId.isEmpty()) {
566 cardsForUniqueId << cardNum;
567 } else if (!cardsForUniqueId.contains(cardNum)) {
568 cardsForUniqueId << cardNum;
569 uniqueIdPrefix += QString(":i%1").arg(cardsForUniqueId.size() - 1);
570 } else if (cardsForUniqueId.size() > 1) {
571 const int listIndex = cardsForUniqueId.indexOf(cardNum);
572 if (listIndex > 0) {
573 uniqueIdPrefix += QString(":i%1").arg(listIndex);
576 const PS::AudioDeviceKey pkey = {
577 uniqueIdPrefix + QLatin1String(":playback"), cardNum, deviceNum
579 const bool needNewPlaybackDevice = playback && !playbackDevices.contains(pkey);
580 const PS::AudioDeviceKey ckey = {
581 uniqueIdPrefix + QLatin1String(":capture"), cardNum, deviceNum
583 const bool needNewCaptureDevice = capture && !captureDevices.contains(ckey);
584 if (needNewPlaybackDevice || needNewCaptureDevice) {
585 const QString &icon = hwDevice.icon();
586 switch (audioIface->soundcardType()) {
587 case Solid::AudioInterface::InternalSoundcard:
588 break;
589 case Solid::AudioInterface::UsbSoundcard:
590 initialPreference -= 10;
591 break;
592 case Solid::AudioInterface::FirewireSoundcard:
593 initialPreference -= 15;
594 break;
595 case Solid::AudioInterface::Headset:
596 initialPreference -= 10;
597 break;
598 case Solid::AudioInterface::Modem:
599 initialPreference -= 1000;
600 kWarning(601) << "Modem devices should never show up!";
601 break;
603 if (needNewPlaybackDevice) {
604 PS::AudioDevice dev(audioIface->name(), icon, pkey, initialPreference, isAdvanced);
605 dev.addAccess(devAccess);
606 playbackDevices.insert(pkey, dev);
608 if (needNewCaptureDevice) {
609 PS::AudioDevice dev(audioIface->name(), icon, ckey, initialPreference, isAdvanced);
610 dev.addAccess(devAccess);
611 captureDevices.insert(ckey, dev);
614 if (!needNewPlaybackDevice && playback) {
615 PS::AudioDevice &dev = playbackDevices[pkey];
616 if (preferCardName) {
617 dev.setPreferredName(audioIface->name());
619 dev.addAccess(devAccess);
621 if (!needNewCaptureDevice && capture) {
622 PS::AudioDevice &dev = captureDevices[ckey];
623 if (preferCardName) {
624 dev.setPreferredName(audioIface->name());
626 dev.addAccess(devAccess);
630 m_audioOutputDevices = playbackDevices.values();
631 m_audioCaptureDevices = captureDevices.values();
633 #ifdef HAVE_PULSEAUDIO
635 pa_mainloop *mainloop = pa_mainloop_new();
636 Q_ASSERT(mainloop);
637 pa_mainloop_api *mainloopApi = pa_mainloop_get_api(mainloop);
638 PulseDetectionUserData userData(this, mainloopApi);
639 // XXX I don't want to show up in the client list. All I want to know is the list of sources
640 // and sinks...
641 pa_context *context = pa_context_new(mainloopApi, "KDE");
642 // XXX stupid cast. report a bug about a missing enum value
643 pa_context_connect(context, NULL, static_cast<pa_context_flags_t>(0), 0);
644 pa_context_set_state_callback(context, &pulseContextStateCallback, &userData);
645 pa_mainloop_run(mainloop, NULL);
646 pa_context_disconnect(context);
647 pa_mainloop_free(mainloop);
648 kDebug(601) << "pulse sources:" << userData.sources;
649 kDebug(601) << "pulse sinks: " << userData.sinks;
650 QMutableListIterator<PS::AudioDevice> it(m_audioOutputDevices);
651 typedef QPair<PS::AudioDeviceKey, PS::AudioDeviceAccess> MyPair;
652 static int uniqueDeviceNumber = -2;
653 foreach (const MyPair &pair, userData.sinks) {
654 it.toFront();
655 bool needNewDeviceObject = true;
656 while (it.hasNext()) {
657 PS::AudioDevice &dev = it.next();
658 if (dev.key() == pair.first) {
659 dev.addAccess(pair.second);
660 needNewDeviceObject = false;
661 continue;
664 if (needNewDeviceObject) {
665 const PS::AudioDeviceKey key = {
666 pair.second.deviceIds().first() + QLatin1String("playback"),
667 -1, --uniqueDeviceNumber
669 PS::AudioDevice dev(pair.first.uniqueId, QLatin1String("audio-backend-pulseaudio"), key, 0, true);
670 dev.addAccess(pair.second);
671 m_audioOutputDevices.append(dev);
674 it = m_audioCaptureDevices;
675 foreach (const MyPair &pair, userData.sources) {
676 it.toFront();
677 bool needNewDeviceObject = true;
678 while (it.hasNext()) {
679 PS::AudioDevice &dev = it.next();
680 if (dev.key() == pair.first) {
681 dev.addAccess(pair.second);
682 needNewDeviceObject = false;
683 continue;
686 if (needNewDeviceObject) {
687 const PS::AudioDeviceKey key = {
688 pair.second.deviceIds().first() + QLatin1String("capture"),
689 -1, --uniqueDeviceNumber
691 PS::AudioDevice dev(pair.first.uniqueId, QLatin1String("audio-backend-pulseaudio"), key, 0, true);
692 dev.addAccess(pair.second);
693 m_audioCaptureDevices.append(dev);
697 #endif // HAVE_PULSEAUDIO
699 if (haveAlsaDevices) {
700 // go through the lists and check for devices that have only OSS and remove them since
701 // they're very likely bogus (Solid tells us a device can do capture and playback, even
702 // though it doesn't actually know that).
703 removeOssOnlyDevices(&m_audioOutputDevices);
704 removeOssOnlyDevices(&m_audioCaptureDevices);
707 // now that we know about the hardware let's see what virtual devices we can find in
708 // ~/.asoundrc and /etc/asound.conf
709 findVirtualDevices();
711 QSet<QString> alreadyFoundCards;
712 foreach (const PS::AudioDevice &dev, m_audioOutputDevices) {
713 alreadyFoundCards.insert(QLatin1String("AudioDevice_") + dev.key().uniqueId);
715 foreach (const PS::AudioDevice &dev, m_audioCaptureDevices) {
716 alreadyFoundCards.insert(QLatin1String("AudioDevice_") + dev.key().uniqueId);
718 // now look in the config file for disconnected devices
719 const QStringList &groupList = m_config->groupList();
720 QStringList askToRemove;
721 QList<int> askToRemoveIndexes;
722 foreach (const QString &groupName, groupList) {
723 if (alreadyFoundCards.contains(groupName) || !groupName.startsWith(QLatin1String("AudioDevice_"))) {
724 continue;
727 const KConfigGroup cGroup(m_config, groupName);
728 if (cGroup.readEntry("deleted", false)) {
729 continue;
731 const QString &cardName = cGroup.readEntry("cardName", QString());
732 const QString &iconName = cGroup.readEntry("iconName", QString());
733 const bool hotpluggable = cGroup.readEntry("hotpluggable", true);
734 const int initialPreference = cGroup.readEntry("initialPreference", 0);
735 const int isAdvanced = cGroup.readEntry("isAdvanced", true);
736 const int deviceNumber = cGroup.readEntry("deviceNumber", -1);
737 const PS::AudioDeviceKey key = { groupName.mid(12), -1, deviceNumber };
738 const PS::AudioDevice dev(cardName, iconName, key, initialPreference, isAdvanced);
739 const bool isPlayback = groupName.endsWith(QLatin1String("playback"));
740 if (!hotpluggable) {
741 const QSettings phononSettings(QLatin1String("kde.org"), QLatin1String("libphonon"));
742 if (isAdvanced &&
743 phononSettings.value(QLatin1String("General/HideAdvancedDevices"), true).toBool()) {
744 dev.removeFromCache(m_config);
745 continue;
746 } else {
747 askToRemove << (isPlayback ? i18n("Output: %1", cardName) :
748 i18n("Capture: %1", cardName));
749 askToRemoveIndexes << cGroup.readEntry("index", 0);
752 if (isPlayback) {
753 m_audioOutputDevices << dev;
754 } else if (!groupName.endsWith(QLatin1String("capture"))) {
755 // this entry shouldn't be here
756 m_config->deleteGroup(groupName);
757 } else {
758 m_audioCaptureDevices << dev;
760 alreadyFoundCards.insert(groupName);
762 if (!askToRemove.isEmpty()) {
763 qSort(askToRemove);
764 QMetaObject::invokeMethod(this, "askToRemoveDevices", Qt::QueuedConnection,
765 Q_ARG(QStringList, askToRemove), Q_ARG(QList<int>, askToRemoveIndexes));
768 renameDevices(&m_audioOutputDevices);
769 renameDevices(&m_audioCaptureDevices);
771 qSort(m_audioOutputDevices);
772 qSort(m_audioCaptureDevices);
774 QMutableListIterator<PS::AudioDevice> it(m_audioOutputDevices);
775 while (it.hasNext()) {
776 it.next().syncWithCache(m_config);
778 it = m_audioCaptureDevices;
779 while (it.hasNext()) {
780 it.next().syncWithCache(m_config);
783 m_config->sync();
785 kDebug(601) << "Playback Devices:" << m_audioOutputDevices;
786 kDebug(601) << "Capture Devices:" << m_audioCaptureDevices;
789 QByteArray PhononServer::audioDevicesIndexes(int type)
791 QByteArray *v;
792 switch (type) {
793 case Phonon::AudioOutputDeviceType:
794 v = &m_audioOutputDevicesIndexesCache;
795 break;
796 case Phonon::AudioCaptureDeviceType:
797 v = &m_audioCaptureDevicesIndexesCache;
798 break;
799 default:
800 return QByteArray();
802 if (v->isEmpty()) {
803 updateAudioDevicesCache();
805 return *v;
808 QByteArray PhononServer::audioDevicesProperties(int index)
810 if (m_audioOutputDevicesIndexesCache.isEmpty() || m_audioCaptureDevicesIndexesCache.isEmpty()) {
811 updateAudioDevicesCache();
813 if (m_audioDevicesPropertiesCache.contains(index)) {
814 return m_audioDevicesPropertiesCache.value(index);
816 return QByteArray();
819 bool PhononServer::isAudioDeviceRemovable(int index) const
821 if (!m_audioDevicesPropertiesCache.contains(index)) {
822 return false;
824 const QList<PS::AudioDevice> &deviceList = m_audioOutputDevices + m_audioCaptureDevices;
825 foreach (const PS::AudioDevice &dev, deviceList) {
826 if (dev.index() == index) {
827 return !dev.isAvailable();
830 return false;
833 void PhononServer::removeAudioDevices(const QList<int> &indexes)
835 const QList<PS::AudioDevice> &deviceList = m_audioOutputDevices + m_audioCaptureDevices;
836 foreach (int index, indexes) {
837 foreach (const PS::AudioDevice &dev, deviceList) {
838 if (dev.index() == index) {
839 if (!dev.isAvailable()) {
840 dev.removeFromCache(m_config);
842 break;
846 m_config->sync();
847 m_updateDeviceListing.start(50, this);
850 static inline QByteArray nameForDriver(PS::AudioDeviceAccess::AudioDriver d)
852 switch (d) {
853 case PS::AudioDeviceAccess::AlsaDriver:
854 return "alsa";
855 case PS::AudioDeviceAccess::OssDriver:
856 return "oss";
857 case PS::AudioDeviceAccess::PulseAudioDriver:
858 return "pulseaudio";
859 case PS::AudioDeviceAccess::JackdDriver:
860 return "jackd";
861 case PS::AudioDeviceAccess::EsdDriver:
862 return "esd";
863 case PS::AudioDeviceAccess::ArtsDriver:
864 return "arts";
865 case PS::AudioDeviceAccess::InvalidDriver:
866 break;
868 Q_ASSERT_X(false, "nameForDriver", "unknown driver");
869 return "";
872 template<class T>
873 inline static QByteArray streamToByteArray(const T &data)
875 QByteArray r;
876 QDataStream stream(&r, QIODevice::WriteOnly);
877 stream << data;
878 return r;
881 void PhononServer::updateAudioDevicesCache()
883 QList<int> indexList;
884 foreach (const PS::AudioDevice &dev, m_audioOutputDevices) {
885 QHash<QByteArray, QVariant> properties;
886 properties.insert("name", dev.name());
887 properties.insert("description", dev.description());
888 properties.insert("available", dev.isAvailable());
889 properties.insert("initialPreference", dev.initialPreference());
890 properties.insert("isAdvanced", dev.isAdvanced());
891 properties.insert("icon", dev.icon());
892 PhononDeviceAccessList deviceAccessList;
893 bool first = true;
894 QStringList oldDeviceIds;
895 PS::AudioDeviceAccess::AudioDriver driverId = PS::AudioDeviceAccess::InvalidDriver;
896 foreach (const PS::AudioDeviceAccess &access, dev.accessList()) {
897 const QByteArray &driver = nameForDriver(access.driver());
898 if (first) {
899 driverId = access.driver();
900 // Phonon 4.2 compatibility
901 properties.insert("driver", driver);
902 first = false;
904 foreach (const QString &deviceId, access.deviceIds()) {
905 if (access.driver() == driverId) {
906 oldDeviceIds << deviceId;
908 deviceAccessList << QPair<QByteArray, QString>(driver, deviceId);
911 properties.insert("deviceAccessList", QVariant::fromValue(deviceAccessList));
913 // Phonon 4.2 compatibility
914 properties.insert("deviceIds", oldDeviceIds);
916 indexList << dev.index();
917 m_audioDevicesPropertiesCache.insert(dev.index(), streamToByteArray(properties));
919 m_audioOutputDevicesIndexesCache = streamToByteArray(indexList);
921 indexList.clear();
922 foreach (const PS::AudioDevice &dev, m_audioCaptureDevices) {
923 QHash<QByteArray, QVariant> properties;
924 properties.insert("name", dev.name());
925 properties.insert("description", dev.description());
926 properties.insert("available", dev.isAvailable());
927 properties.insert("initialPreference", dev.initialPreference());
928 properties.insert("isAdvanced", dev.isAdvanced());
929 properties.insert("icon", dev.icon());
930 PhononDeviceAccessList deviceAccessList;
931 foreach (const PS::AudioDeviceAccess &access, dev.accessList()) {
932 const QByteArray &driver = nameForDriver(access.driver());
933 foreach (const QString &deviceId, access.deviceIds()) {
934 deviceAccessList << QPair<QByteArray, QString>(driver, deviceId);
937 properties.insert("deviceAccessList", QVariant::fromValue(deviceAccessList));
938 // no Phonon 4.2 compatibility for capture devices necessary as 4.2 never really supported
939 // capture
941 indexList << dev.index();
942 m_audioDevicesPropertiesCache.insert(dev.index(), streamToByteArray(properties));
944 m_audioCaptureDevicesIndexesCache = streamToByteArray(indexList);
947 void PhononServer::deviceAdded(const QString &udi)
949 Solid::Device device(udi);
950 Solid::AudioInterface *audiohw = device.as<Solid::AudioInterface>();
951 if (!audiohw || 0 == (audiohw->deviceType() & (Solid::AudioInterface::AudioInput | Solid::AudioInterface::AudioOutput))) {
952 return;
954 m_updateDeviceListing.start(50, this);
957 void PhononServer::timerEvent(QTimerEvent *e)
959 if (e->timerId() == m_updateDeviceListing.timerId()) {
960 m_updateDeviceListing.stop();
961 m_audioOutputDevices.clear();
962 m_audioCaptureDevices.clear();
963 m_udisOfAudioDevices.clear();
964 findDevices();
965 m_audioOutputDevicesIndexesCache.clear();
966 m_audioCaptureDevicesIndexesCache.clear();
968 QDBusMessage signal = QDBusMessage::createSignal("/modules/phononserver", "org.kde.PhononServer", "audioDevicesChanged");
969 QDBusConnection::sessionBus().send(signal);
973 void PhononServer::deviceRemoved(const QString &udi)
975 if (m_udisOfAudioDevices.contains(udi)) {
976 m_updateDeviceListing.start(50, this);
980 void PhononServer::askToRemoveDevices(const QStringList &devList, const QList<int> &indexes)
982 const QString &dontAskAgainName = QLatin1String("phonon_forget_devices_") +
983 devList.join(QLatin1String("_"));
984 KMessageBox::ButtonCode result;
985 if (!KMessageBox::shouldBeShownYesNo(dontAskAgainName, result)) {
986 if (result == KMessageBox::Yes) {
987 kDebug(601) << "removeAudioDevices" << indexes;
988 removeAudioDevices(indexes);
990 return;
993 class MyDialog: public KDialog
995 public:
996 MyDialog() : KDialog(0, Qt::Dialog) {}
998 protected:
999 virtual void slotButtonClicked(int button)
1001 if (button == KDialog::User1) {
1002 kDebug(601) << "start kcm_phonon";
1003 KProcess::startDetached(QLatin1String("kcmshell4"), QStringList(QLatin1String("kcm_phonon")));
1004 reject();
1005 } else {
1006 KDialog::slotButtonClicked(button);
1009 } *dialog = new MyDialog;
1010 dialog->setPlainCaption(i18n("Removed Sound Devices"));
1011 dialog->setButtons(KDialog::Yes | KDialog::No | KDialog::User1);
1012 KIcon icon("audio-card");
1013 dialog->setWindowIcon(icon);
1014 dialog->setModal(false);
1015 KGuiItem yes(KStandardGuiItem::yes());
1016 yes.setToolTip(i18n("Forget about the sound devices."));
1017 dialog->setButtonGuiItem(KDialog::Yes, yes);
1018 dialog->setButtonGuiItem(KDialog::No, KStandardGuiItem::no());
1019 dialog->setButtonGuiItem(KDialog::User1, KGuiItem(i18nc("short string for a button, it opens "
1020 "the Phonon page of System Settings", "Manage Devices"),
1021 KIcon("preferences-system"),
1022 i18n("Open the System Settings page for sound device configuration where you can "
1023 "manually remove disconnected devices from the cache.")));
1024 dialog->setEscapeButton(KDialog::No);
1025 dialog->setDefaultButton(KDialog::User1);
1027 bool checkboxResult = false;
1028 int res = KMessageBox::createKMessageBox(dialog, icon,
1029 i18n("<html><p>KDE detected that one or more internal sound devices were removed.</p>"
1030 "<p><b>Do you want KDE to permanently forget about these devices?</b></p>"
1031 "<p>This is the list of devices KDE thinks can be removed:<ul><li>%1</li></ul></p></html>",
1032 devList.join(QLatin1String("</li><li>"))),
1033 QStringList(),
1034 i18n("Do not ask again for these devices"),
1035 &checkboxResult, KMessageBox::Notify);
1036 result = (res == KDialog::Yes ? KMessageBox::Yes : KMessageBox::No);
1037 if (result == KMessageBox::Yes) {
1038 kDebug(601) << "removeAudioDevices" << indexes;
1039 removeAudioDevices(indexes);
1041 if (checkboxResult) {
1042 KMessageBox::saveDontShowAgainYesNo(dontAskAgainName, result);