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

source code of phonon/phonon/audiooutput.cpp