1 | /* This file is part of the KDE project |
2 | Copyright (C) 2005-2006 Matthias Kretz <kretz@kde.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 | #include "audiooutput.h" |
23 | #include "audiooutput_p.h" |
24 | #include "factory_p.h" |
25 | #include "objectdescription.h" |
26 | #include "audiooutputadaptor_p.h" |
27 | #include "globalconfig.h" |
28 | #include "audiooutputinterface.h" |
29 | #include "phononnamespace_p.h" |
30 | #include "platform_p.h" |
31 | #include "pulsesupport.h" |
32 | |
33 | #include <QtCore/qmath.h> |
34 | #include <QtCore/quuid.h> |
35 | |
36 | #define PHONON_CLASSNAME AudioOutput |
37 | #define IFACES2 AudioOutputInterface42 |
38 | #define IFACES1 IFACES2 |
39 | #define IFACES0 AudioOutputInterface40, IFACES1 |
40 | #define PHONON_INTERFACENAME IFACES0 |
41 | |
42 | QT_BEGIN_NAMESPACE |
43 | |
44 | namespace Phonon |
45 | { |
46 | |
47 | static inline bool callSetOutputDevice(AudioOutputPrivate *const d, int index) |
48 | { |
49 | PulseSupport *pulse = PulseSupport::getInstance(); |
50 | if (pulse->isActive()) |
51 | return pulse->setOutputDevice(d->getStreamUuid(), index); |
52 | |
53 | Iface<IFACES2> iface(d); |
54 | if (iface) { |
55 | return iface->setOutputDevice(AudioOutputDevice::fromIndex(index)); |
56 | } |
57 | return Iface<IFACES0>::cast(d)->setOutputDevice(index); |
58 | } |
59 | |
60 | static inline bool callSetOutputDevice(AudioOutputPrivate *const d, const AudioOutputDevice &dev) |
61 | { |
62 | PulseSupport *pulse = PulseSupport::getInstance(); |
63 | if (pulse->isActive()) |
64 | return pulse->setOutputDevice(d->getStreamUuid(), dev.index()); |
65 | |
66 | Iface<IFACES2> iface(d); |
67 | if (iface) { |
68 | return iface->setOutputDevice(dev); |
69 | } |
70 | return Iface<IFACES0>::cast(d)->setOutputDevice(dev.index()); |
71 | } |
72 | |
73 | AudioOutput::AudioOutput(Phonon::Category category, QObject *parent) |
74 | : AbstractAudioOutput(*new AudioOutputPrivate, parent) |
75 | { |
76 | K_D(AudioOutput); |
77 | d->init(category); |
78 | } |
79 | |
80 | AudioOutput::AudioOutput(QObject *parent) |
81 | : AbstractAudioOutput(*new AudioOutputPrivate, parent) |
82 | { |
83 | K_D(AudioOutput); |
84 | d->init(NoCategory); |
85 | } |
86 | |
87 | void AudioOutputPrivate::init(Phonon::Category c) |
88 | { |
89 | Q_Q(AudioOutput); |
90 | #ifndef QT_NO_DBUS |
91 | adaptor = new AudioOutputAdaptor(q); |
92 | static unsigned int number = 0; |
93 | const QString &path = QLatin1String("/AudioOutputs/" ) + QString::number(number++); |
94 | QDBusConnection con = QDBusConnection::sessionBus(); |
95 | con.registerObject(path, q); |
96 | emit adaptor->newOutputAvailable(con.baseService(), path); |
97 | q->connect(q, SIGNAL(volumeChanged(qreal)), adaptor, SIGNAL(volumeChanged(qreal))); |
98 | q->connect(q, SIGNAL(mutedChanged(bool)), adaptor, SIGNAL(mutedChanged(bool))); |
99 | #endif |
100 | |
101 | category = c; |
102 | streamUuid = QUuid::createUuid().toString(); |
103 | PulseSupport *pulse = PulseSupport::getInstance(); |
104 | pulse->setStreamPropList(category, streamUuid); |
105 | q->connect(pulse, SIGNAL(usingDevice(QString,int)), SLOT(_k_deviceChanged(QString,int))); |
106 | |
107 | createBackendObject(); |
108 | |
109 | q->connect(Factory::sender(), SIGNAL(availableAudioOutputDevicesChanged()), SLOT(_k_deviceListChanged())); |
110 | } |
111 | |
112 | QString AudioOutputPrivate::getStreamUuid() |
113 | { |
114 | return streamUuid; |
115 | } |
116 | |
117 | void AudioOutputPrivate::createBackendObject() |
118 | { |
119 | if (m_backendObject) |
120 | return; |
121 | Q_Q(AudioOutput); |
122 | m_backendObject = Factory::createAudioOutput(q); |
123 | device = AudioOutputDevice::fromIndex(GlobalConfig().audioOutputDeviceFor(category, GlobalConfig::AdvancedDevicesFromSettings | GlobalConfig::HideUnavailableDevices)); |
124 | if (m_backendObject) { |
125 | setupBackendObject(); |
126 | } |
127 | } |
128 | |
129 | QString AudioOutput::name() const |
130 | { |
131 | K_D(const AudioOutput); |
132 | return d->name; |
133 | } |
134 | |
135 | void AudioOutput::setName(const QString &newName) |
136 | { |
137 | K_D(AudioOutput); |
138 | if (d->name == newName) { |
139 | return; |
140 | } |
141 | d->name = newName; |
142 | setVolume(Platform::loadVolume(newName)); |
143 | #ifndef QT_NO_DBUS |
144 | if (d->adaptor) { |
145 | emit d->adaptor->nameChanged(newName); |
146 | } |
147 | #endif |
148 | } |
149 | |
150 | static const qreal LOUDNESS_TO_VOLTAGE_EXPONENT = qreal(0.67); |
151 | static const qreal VOLTAGE_TO_LOUDNESS_EXPONENT = qreal(1.0/LOUDNESS_TO_VOLTAGE_EXPONENT); |
152 | |
153 | void AudioOutput::setVolume(qreal volume) |
154 | { |
155 | K_D(AudioOutput); |
156 | d->volume = volume; |
157 | if (k_ptr->backendObject() && !d->muted) { |
158 | // using Stevens' power law loudness is proportional to (sound pressure)^0.67 |
159 | // sound pressure is proportional to voltage: |
160 | // p² \prop P \prop V² |
161 | // => if a factor for loudness of x is requested |
162 | INTERFACE_CALL(setVolume(pow(volume, VOLTAGE_TO_LOUDNESS_EXPONENT))); |
163 | } else { |
164 | emit volumeChanged(volume); |
165 | } |
166 | Platform::saveVolume(d->name, volume); |
167 | } |
168 | |
169 | qreal AudioOutput::volume() const |
170 | { |
171 | K_D(const AudioOutput); |
172 | if (d->muted || !d->m_backendObject) { |
173 | return d->volume; |
174 | } |
175 | return pow(INTERFACE_CALL(volume()), LOUDNESS_TO_VOLTAGE_EXPONENT); |
176 | } |
177 | |
178 | #ifndef PHONON_LOG10OVER20 |
179 | #define PHONON_LOG10OVER20 |
180 | static const qreal log10over20 = qreal(0.1151292546497022842); // ln(10) / 20 |
181 | #endif // PHONON_LOG10OVER20 |
182 | |
183 | qreal AudioOutput::volumeDecibel() const |
184 | { |
185 | K_D(const AudioOutput); |
186 | if (d->muted || !d->m_backendObject) { |
187 | return log(d->volume) / log10over20; |
188 | } |
189 | return 0.67 * log(INTERFACE_CALL(volume())) / log10over20; |
190 | } |
191 | |
192 | void AudioOutput::setVolumeDecibel(qreal newVolumeDecibel) |
193 | { |
194 | setVolume(exp(newVolumeDecibel * log10over20)); |
195 | } |
196 | |
197 | bool AudioOutput::isMuted() const |
198 | { |
199 | K_D(const AudioOutput); |
200 | return d->muted; |
201 | } |
202 | |
203 | void AudioOutput::setMuted(bool mute) |
204 | { |
205 | K_D(AudioOutput); |
206 | if (d->muted != mute) { |
207 | if (mute) { |
208 | d->muted = mute; |
209 | if (k_ptr->backendObject()) { |
210 | INTERFACE_CALL(setVolume(0.0)); |
211 | } |
212 | } else { |
213 | if (k_ptr->backendObject()) { |
214 | INTERFACE_CALL(setVolume(pow(d->volume, VOLTAGE_TO_LOUDNESS_EXPONENT))); |
215 | } |
216 | d->muted = mute; |
217 | } |
218 | emit mutedChanged(mute); |
219 | } |
220 | } |
221 | |
222 | Category AudioOutput::category() const |
223 | { |
224 | K_D(const AudioOutput); |
225 | return d->category; |
226 | } |
227 | |
228 | AudioOutputDevice AudioOutput::outputDevice() const |
229 | { |
230 | K_D(const AudioOutput); |
231 | return d->device; |
232 | } |
233 | |
234 | bool AudioOutput::setOutputDevice(const AudioOutputDevice &newAudioOutputDevice) |
235 | { |
236 | K_D(AudioOutput); |
237 | if (!newAudioOutputDevice.isValid()) { |
238 | d->outputDeviceOverridden = d->forceMove = false; |
239 | const int newIndex = GlobalConfig().audioOutputDeviceFor(d->category); |
240 | if (newIndex == d->device.index()) { |
241 | return true; |
242 | } |
243 | d->device = AudioOutputDevice::fromIndex(newIndex); |
244 | } else { |
245 | d->outputDeviceOverridden = d->forceMove = true; |
246 | if (d->device == newAudioOutputDevice) { |
247 | return true; |
248 | } |
249 | d->device = newAudioOutputDevice; |
250 | } |
251 | if (k_ptr->backendObject()) { |
252 | return callSetOutputDevice(d, d->device.index()); |
253 | } |
254 | return true; |
255 | } |
256 | |
257 | bool AudioOutputPrivate::aboutToDeleteBackendObject() |
258 | { |
259 | if (m_backendObject) { |
260 | volume = pINTERFACE_CALL(volume()); |
261 | } |
262 | return AbstractAudioOutputPrivate::aboutToDeleteBackendObject(); |
263 | } |
264 | |
265 | void AudioOutputPrivate::setupBackendObject() |
266 | { |
267 | Q_Q(AudioOutput); |
268 | Q_ASSERT(m_backendObject); |
269 | AbstractAudioOutputPrivate::setupBackendObject(); |
270 | |
271 | QObject::connect(m_backendObject, SIGNAL(volumeChanged(qreal)), q, SLOT(_k_volumeChanged(qreal))); |
272 | QObject::connect(m_backendObject, SIGNAL(audioDeviceFailed()), q, SLOT(_k_audioDeviceFailed())); |
273 | |
274 | // set up attributes |
275 | pINTERFACE_CALL(setVolume(pow(volume, VOLTAGE_TO_LOUDNESS_EXPONENT))); |
276 | |
277 | #ifndef QT_NO_PHONON_SETTINGSGROUP |
278 | // if the output device is not available and the device was not explicitly set |
279 | // There is no need to set the output device initially if PA is used as |
280 | // we know it will not work (stream doesn't exist yet) and that this will be |
281 | // handled by _k_deviceChanged() |
282 | if (!PulseSupport::getInstance()->isActive() && !callSetOutputDevice(this, device) && !outputDeviceOverridden) { |
283 | // fall back in the preference list of output devices |
284 | QList<int> deviceList = GlobalConfig().audioOutputDeviceListFor(category, GlobalConfig::AdvancedDevicesFromSettings | GlobalConfig::HideUnavailableDevices); |
285 | if (deviceList.isEmpty()) { |
286 | return; |
287 | } |
288 | for (int i = 0; i < deviceList.count(); ++i) { |
289 | const AudioOutputDevice &dev = AudioOutputDevice::fromIndex(deviceList.at(i)); |
290 | if (callSetOutputDevice(this, dev)) { |
291 | handleAutomaticDeviceChange(dev, AudioOutputPrivate::FallbackChange); |
292 | return; // found one that works |
293 | } |
294 | } |
295 | // if we get here there is no working output device. Tell the backend. |
296 | const AudioOutputDevice none; |
297 | callSetOutputDevice(this, none); |
298 | handleAutomaticDeviceChange(none, FallbackChange); |
299 | } |
300 | #endif //QT_NO_PHONON_SETTINGSGROUP |
301 | } |
302 | |
303 | void AudioOutputPrivate::_k_volumeChanged(qreal newVolume) |
304 | { |
305 | if (!muted) { |
306 | Q_Q(AudioOutput); |
307 | emit q->volumeChanged(pow(newVolume, qreal(0.67))); |
308 | } |
309 | } |
310 | |
311 | void AudioOutputPrivate::_k_revertFallback() |
312 | { |
313 | if (deviceBeforeFallback == -1) { |
314 | return; |
315 | } |
316 | device = AudioOutputDevice::fromIndex(deviceBeforeFallback); |
317 | callSetOutputDevice(this, device); |
318 | Q_Q(AudioOutput); |
319 | emit q->outputDeviceChanged(device); |
320 | #ifndef QT_NO_DBUS |
321 | emit adaptor->outputDeviceIndexChanged(device.index()); |
322 | #endif |
323 | } |
324 | |
325 | void AudioOutputPrivate::_k_audioDeviceFailed() |
326 | { |
327 | if (PulseSupport::getInstance()->isActive()) |
328 | return; |
329 | |
330 | #ifndef QT_NO_PHONON_SETTINGSGROUP |
331 | |
332 | pDebug() << Q_FUNC_INFO; |
333 | // outputDeviceIndex identifies a failing device |
334 | // fall back in the preference list of output devices |
335 | const QList<int> deviceList = GlobalConfig().audioOutputDeviceListFor(category, GlobalConfig::AdvancedDevicesFromSettings | GlobalConfig::HideUnavailableDevices); |
336 | for (int i = 0; i < deviceList.count(); ++i) { |
337 | const int devIndex = deviceList.at(i); |
338 | // if it's the same device as the one that failed, ignore it |
339 | if (device.index() != devIndex) { |
340 | const AudioOutputDevice &info = AudioOutputDevice::fromIndex(devIndex); |
341 | if (callSetOutputDevice(this, info)) { |
342 | handleAutomaticDeviceChange(info, FallbackChange); |
343 | return; // found one that works |
344 | } |
345 | } |
346 | } |
347 | #endif //QT_NO_PHONON_SETTINGSGROUP |
348 | // if we get here there is no working output device. Tell the backend. |
349 | const AudioOutputDevice none; |
350 | callSetOutputDevice(this, none); |
351 | handleAutomaticDeviceChange(none, FallbackChange); |
352 | } |
353 | |
354 | void AudioOutputPrivate::_k_deviceListChanged() |
355 | { |
356 | if (PulseSupport::getInstance()->isActive()) |
357 | return; |
358 | |
359 | #ifndef QT_NO_PHONON_SETTINGSGROUP |
360 | pDebug() << Q_FUNC_INFO; |
361 | // Check to see if we have an override and do not change to a higher priority device if the overridden device is still present. |
362 | if (outputDeviceOverridden && device.property("available" ).toBool()) { |
363 | return; |
364 | } |
365 | // let's see if there's a usable device higher in the preference list |
366 | const QList<int> deviceList = GlobalConfig().audioOutputDeviceListFor(category, GlobalConfig::AdvancedDevicesFromSettings); |
367 | DeviceChangeType changeType = HigherPreferenceChange; |
368 | for (int i = 0; i < deviceList.count(); ++i) { |
369 | const int devIndex = deviceList.at(i); |
370 | const AudioOutputDevice &info = AudioOutputDevice::fromIndex(devIndex); |
371 | if (!info.property("available" ).toBool()) { |
372 | if (device.index() == devIndex) { |
373 | // we've reached the currently used device and it's not available anymore, so we |
374 | // fallback to the next available device |
375 | changeType = FallbackChange; |
376 | } |
377 | pDebug() << devIndex << "is not available" ; |
378 | continue; |
379 | } |
380 | pDebug() << devIndex << "is available" ; |
381 | if (device.index() == devIndex) { |
382 | // we've reached the currently used device, nothing to change |
383 | break; |
384 | } |
385 | if (callSetOutputDevice(this, info)) { |
386 | handleAutomaticDeviceChange(info, changeType); |
387 | break; // found one with higher preference that works |
388 | } |
389 | } |
390 | #endif //QT_NO_PHONON_SETTINGSGROUP |
391 | } |
392 | |
393 | void AudioOutputPrivate::_k_deviceChanged(QString inStreamUuid, int deviceIndex) |
394 | { |
395 | // Note that this method is only used by PulseAudio at present. |
396 | if (inStreamUuid == streamUuid) { |
397 | // 1. Check to see if we are overridden. If we are, and devices do not match, |
398 | // then try and apply our own device as the output device. |
399 | // We only do this the first time |
400 | if (outputDeviceOverridden && forceMove) { |
401 | forceMove = false; |
402 | const AudioOutputDevice ¤tDevice = AudioOutputDevice::fromIndex(deviceIndex); |
403 | if (currentDevice != device) { |
404 | if (!callSetOutputDevice(this, device)) { |
405 | // What to do if we are overridden and cannot change to our preferred device? |
406 | } |
407 | } |
408 | } |
409 | // 2. If we are not overridden, then we need to update our perception of what |
410 | // device we are using. If the devices do not match, something lower in the |
411 | // stack is overriding our preferences (e.g. a per-application stream preference, |
412 | // specific application move, priority list changed etc. etc.) |
413 | else if (!outputDeviceOverridden) { |
414 | const AudioOutputDevice ¤tDevice = AudioOutputDevice::fromIndex(deviceIndex); |
415 | if (currentDevice != device) { |
416 | // The device is not what we think it is, so lets say what is happening. |
417 | handleAutomaticDeviceChange(currentDevice, SoundSystemChange); |
418 | } |
419 | } |
420 | } |
421 | } |
422 | |
423 | static struct |
424 | { |
425 | int first; |
426 | int second; |
427 | } g_lastFallback = { 0, 0 }; |
428 | |
429 | void AudioOutputPrivate::handleAutomaticDeviceChange(const AudioOutputDevice &device2, DeviceChangeType type) |
430 | { |
431 | Q_Q(AudioOutput); |
432 | deviceBeforeFallback = device.index(); |
433 | device = device2; |
434 | emit q->outputDeviceChanged(device2); |
435 | #ifndef QT_NO_DBUS |
436 | emit adaptor->outputDeviceIndexChanged(device.index()); |
437 | #endif |
438 | const AudioOutputDevice &device1 = AudioOutputDevice::fromIndex(deviceBeforeFallback); |
439 | switch (type) { |
440 | case FallbackChange: |
441 | if (g_lastFallback.first != device1.index() || g_lastFallback.second != device2.index()) { |
442 | #ifndef QT_NO_PHONON_PLATFORMPLUGIN |
443 | const QString &text = //device2.isValid() ? |
444 | AudioOutput::tr("<html>The audio playback device <b>%1</b> does not work.<br/>" |
445 | "Falling back to <b>%2</b>.</html>" ).arg(device1.name()).arg(device2.name()) /*: |
446 | AudioOutput::tr("<html>The audio playback device <b>%1</b> does not work.<br/>" |
447 | "No other device available.</html>").arg(device1.name())*/; |
448 | Platform::notification("AudioDeviceFallback" , text); |
449 | #endif //QT_NO_PHONON_PLATFORMPLUGIN |
450 | g_lastFallback.first = device1.index(); |
451 | g_lastFallback.second = device2.index(); |
452 | } |
453 | break; |
454 | case HigherPreferenceChange: |
455 | { |
456 | #ifndef QT_NO_PHONON_PLATFORMPLUGIN |
457 | const QString text = AudioOutput::tr("<html>Switching to the audio playback device <b>%1</b><br/>" |
458 | "which just became available and has higher preference.</html>" ).arg(device2.name()); |
459 | Platform::notification("AudioDeviceFallback" , text, |
460 | QStringList(AudioOutput::tr("Revert back to device '%1'" ).arg(device1.name())), |
461 | q, SLOT(_k_revertFallback())); |
462 | #endif //QT_NO_PHONON_PLATFORMPLUGIN |
463 | g_lastFallback.first = 0; |
464 | g_lastFallback.second = 0; |
465 | } |
466 | break; |
467 | case SoundSystemChange: |
468 | { |
469 | #ifndef QT_NO_PHONON_PLATFORMPLUGIN |
470 | if (device1.property("available" ).toBool()) { |
471 | const QString text = AudioOutput::tr("<html>Switching to the audio playback device <b>%1</b><br/>" |
472 | "which has higher preference or is specifically configured for this stream.</html>" ).arg(device2.name()); |
473 | Platform::notification("AudioDeviceFallback" , text, |
474 | QStringList(AudioOutput::tr("Revert back to device '%1'" ).arg(device1.name())), |
475 | q, SLOT(_k_revertFallback())); |
476 | } else { |
477 | const QString &text = |
478 | AudioOutput::tr("<html>The audio playback device <b>%1</b> does not work.<br/>" |
479 | "Falling back to <b>%2</b>.</html>" ).arg(device1.name()).arg(device2.name()); |
480 | Platform::notification("AudioDeviceFallback" , text); |
481 | } |
482 | #endif //QT_NO_PHONON_PLATFORMPLUGIN |
483 | //outputDeviceOverridden = true; |
484 | g_lastFallback.first = 0; |
485 | g_lastFallback.second = 0; |
486 | } |
487 | break; |
488 | } |
489 | } |
490 | |
491 | AudioOutputPrivate::~AudioOutputPrivate() |
492 | { |
493 | PulseSupport::getInstance()->clearStreamCache(streamUuid); |
494 | #ifndef QT_NO_DBUS |
495 | if (adaptor) { |
496 | emit adaptor->outputDestroyed(); |
497 | } |
498 | #endif |
499 | } |
500 | |
501 | } //namespace Phonon |
502 | |
503 | QT_END_NAMESPACE |
504 | |
505 | #include "moc_audiooutput.cpp" |
506 | |
507 | #undef PHONON_CLASSNAME |
508 | #undef PHONON_INTERFACENAME |
509 | #undef IFACES2 |
510 | #undef IFACES1 |
511 | #undef IFACES0 |
512 | |