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
21 #include "phononserver.h"
22 #include "audiodevice.h"
24 #include <kconfiggroup.h>
26 #include <kstandarddirs.h>
27 #include <kmessagebox.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
> &)
67 m_config(KSharedConfig::openConfig("phonondevicesrc", KConfig::SimpleConfig
))
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'))
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'))
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
);
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()));
150 static inline QDebug
operator<<(QDebug
&d
, const DeviceHint
&h
)
152 d
.nospace() << h
.name
<< " (" << h
.description
<< ")";
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();
166 //snd_config_update();
167 if (snd_device_name_hint(-1, "pcm", &hints
) < 0) {
168 kDebug(601) << "snd_device_name_hint failed for 'pcm'";
172 for (void **cStrings
= hints
; *cStrings
; ++cStrings
) {
174 char *x
= snd_device_name_get_hint(*cStrings
, "NAME");
175 nextHint
.name
= QString::fromUtf8(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"
192 x
= snd_device_name_get_hint(*cStrings
, "DESC");
193 nextHint
.description
= QString::fromUtf8(x
);
196 deviceHints
<< nextHint
;
198 snd_device_name_free_hint(hints
);
199 kDebug(601) << deviceHints
;
201 snd_config_update_free_global();
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:")) {
227 bool available
= false;
228 bool playbackDevice
= false;
229 bool captureDevice
= false;
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 */)) {
235 playbackDevice
= true;
238 if (0 == snd_pcm_open(&pcm
, deviceNameEnc
.constData(), SND_PCM_STREAM_CAPTURE
, SND_PCM_NONBLOCK
/*open mode: non-blocking, sync */)) {
240 captureDevice
= true;
245 QString
iconName(QLatin1String("audio-card"));
246 int initialPreference
= 30;
247 if (description
.contains("headset", Qt::CaseInsensitive
) ||
248 description
.contains("headphone", Qt::CaseInsensitive
)) {
250 if (description
.contains("usb", Qt::CaseInsensitive
)) {
251 iconName
= QLatin1String("audio-headset-usb");
252 initialPreference
-= 10;
254 iconName
= QLatin1String("audio-headset");
255 initialPreference
-= 10;
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
;
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
;
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;
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()
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()) {
324 foreach (const PS::AudioDeviceAccess
&a
, dev
.accessList()) {
325 if (a
.driver() != PS::AudioDeviceAccess::OssDriver
) {
337 #ifdef HAVE_PULSEAUDIO
338 class PulseDetectionUserData
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); }
355 pa_mainloop_api
*const mainloopApi
;
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
);
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
);
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
);
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
);
447 case PA_CONTEXT_FAILED
:
449 PulseDetectionUserData
*d
= reinterpret_cast<PulseDetectionUserData
*>(userdata
);
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
;
481 bool isAdvanced
= false;
482 bool preferCardName
= false;
486 switch (audioIface
->driver()) {
487 case Solid::AudioInterface::UnknownAudioDriver
:
490 case Solid::AudioInterface::Alsa
:
491 if (audioIface
->driverHandle().type() != QVariant::List
) {
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) {
501 driver
= PS::AudioDeviceAccess::AlsaDriver
;
502 accessPreference
+= 10;
505 cardNum
= handles
.first().toInt(&ok
);
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;
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
;
533 case Solid::AudioInterface::OpenSoundSystem
:
534 if (audioIface
->driverHandle().type() != QVariant::String
) {
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();
548 if (!valid
|| audioIface
->soundcardType() == Solid::AudioInterface::Modem
) {
552 m_udisOfAudioDevices
.append(hwDevice
.udi());
554 const PS::AudioDeviceAccess
devAccess(deviceIds
, accessPreference
, driver
, capture
,
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
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
);
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
:
589 case Solid::AudioInterface::UsbSoundcard
:
590 initialPreference
-= 10;
592 case Solid::AudioInterface::FirewireSoundcard
:
593 initialPreference
-= 15;
595 case Solid::AudioInterface::Headset
:
596 initialPreference
-= 10;
598 case Solid::AudioInterface::Modem
:
599 initialPreference
-= 1000;
600 kWarning(601) << "Modem devices should never show up!";
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();
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
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
) {
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;
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
) {
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;
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_"))) {
727 const KConfigGroup
cGroup(m_config
, groupName
);
728 if (cGroup
.readEntry("deleted", false)) {
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"));
741 const QSettings
phononSettings(QLatin1String("kde.org"), QLatin1String("libphonon"));
743 phononSettings
.value(QLatin1String("General/HideAdvancedDevices"), true).toBool()) {
744 dev
.removeFromCache(m_config
);
747 askToRemove
<< (isPlayback
? i18n("Output: %1", cardName
) :
748 i18n("Capture: %1", cardName
));
749 askToRemoveIndexes
<< cGroup
.readEntry("index", 0);
753 m_audioOutputDevices
<< dev
;
754 } else if (!groupName
.endsWith(QLatin1String("capture"))) {
755 // this entry shouldn't be here
756 m_config
->deleteGroup(groupName
);
758 m_audioCaptureDevices
<< dev
;
760 alreadyFoundCards
.insert(groupName
);
762 if (!askToRemove
.isEmpty()) {
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
);
785 kDebug(601) << "Playback Devices:" << m_audioOutputDevices
;
786 kDebug(601) << "Capture Devices:" << m_audioCaptureDevices
;
789 QByteArray
PhononServer::audioDevicesIndexes(int type
)
793 case Phonon::AudioOutputDeviceType
:
794 v
= &m_audioOutputDevicesIndexesCache
;
796 case Phonon::AudioCaptureDeviceType
:
797 v
= &m_audioCaptureDevicesIndexesCache
;
803 updateAudioDevicesCache();
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
);
819 bool PhononServer::isAudioDeviceRemovable(int index
) const
821 if (!m_audioDevicesPropertiesCache
.contains(index
)) {
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();
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
);
847 m_updateDeviceListing
.start(50, this);
850 static inline QByteArray
nameForDriver(PS::AudioDeviceAccess::AudioDriver d
)
853 case PS::AudioDeviceAccess::AlsaDriver
:
855 case PS::AudioDeviceAccess::OssDriver
:
857 case PS::AudioDeviceAccess::PulseAudioDriver
:
859 case PS::AudioDeviceAccess::JackdDriver
:
861 case PS::AudioDeviceAccess::EsdDriver
:
863 case PS::AudioDeviceAccess::ArtsDriver
:
865 case PS::AudioDeviceAccess::InvalidDriver
:
868 Q_ASSERT_X(false, "nameForDriver", "unknown driver");
873 inline static QByteArray
streamToByteArray(const T
&data
)
876 QDataStream
stream(&r
, QIODevice::WriteOnly
);
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
;
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());
899 driverId
= access
.driver();
900 // Phonon 4.2 compatibility
901 properties
.insert("driver", driver
);
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
);
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
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
))) {
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();
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
);
993 class MyDialog
: public KDialog
996 MyDialog() : KDialog(0, Qt::Dialog
) {}
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")));
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>"))),
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
);