Phonon

audiooutput.cpp
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(d->getStreamUuid(), 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(category);
73}
74
76 : AbstractAudioOutput(*new AudioOutputPrivate, parent)
77{
78 P_D(AudioOutput);
79 d->init(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(stream, SIGNAL(usingDevice(int)), SLOT(_k_deviceChanged(int)));
99 q->connect(stream, SIGNAL(volumeChanged(qreal)), SLOT(_k_volumeChanged(qreal)));
100 q->connect(stream, SIGNAL(muteChanged(bool)), SLOT(_k_mutedChanged(bool)));
101
102 AudioOutputInterface47 *iface = Iface<AudioOutputInterface47>::cast(this);
103 if (iface)
104 iface->setStreamUuid(streamUuid);
105 else
106 pulse->setupStreamEnvironment(streamUuid);
107 }
108 }
109#endif
110
111 q->connect(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(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(GlobalConfig().audioOutputDeviceFor(category, GlobalConfig::AdvancedDevicesFromSettings | GlobalConfig::HideUnavailableDevices));
129 if (m_backendObject) {
130 setupBackendObject();
131 }
132}
133
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(d->getStreamUuid(), newName);
150 else
151 setVolume(Platform::loadVolume(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(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(volume);
173 }
174 } else {
175 emit volumeChanged(volume);
176 }
177 if (!pulse->isActive())
178 Platform::saveVolume(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()), 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(d->volume) / log10over20;
199 return 0.67 * log(INTERFACE_CALL(volume())) / log10over20;
200}
201
202void AudioOutput::setVolumeDecibel(qreal newVolumeDecibel)
203{
204 setVolume(exp(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(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
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(d->category);
265 if (newIndex == d->device.index()) {
266 return true;
267 }
268 d->device = AudioOutputDevice::fromIndex(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, 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(m_backendObject, SIGNAL(volumeChanged(qreal)), q, SLOT(_k_volumeChanged(qreal)));
297 QObject::connect(m_backendObject, SIGNAL(audioDeviceFailed()), q, SLOT(_k_audioDeviceFailed()));
298 if (Iface<IFACES9>(this)) {
299 QObject::connect(m_backendObject, SIGNAL(mutedChanged(bool)),
300 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(this, device) && !outputDeviceOverridden) {
318 // fall back in the preference list of output devices
319 QList<int> deviceList = GlobalConfig().audioOutputDeviceListFor(category, 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(deviceList.at(i));
325 if (callSetOutputDevice(this, dev)) {
326 handleAutomaticDeviceChange(dev, 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(this, none);
333 handleAutomaticDeviceChange(none, FallbackChange);
334 }
335#endif //QT_NO_PHONON_SETTINGSGROUP
336 }
337}
338
339void AudioOutputPrivate::_k_volumeChanged(qreal newVolume)
340{
341 volume = pow(newVolume, LOUDNESS_TO_VOLTAGE_EXPONENT);
342 if (!muted) {
343 P_Q(AudioOutput);
344 emit q->volumeChanged(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(deviceBeforeFallback);
361 callSetOutputDevice(this, device);
362 P_Q(AudioOutput);
363 emit q->outputDeviceChanged(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, 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(devIndex);
382 if (callSetOutputDevice(this, info)) {
383 handleAutomaticDeviceChange(info, 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(this, none);
392 handleAutomaticDeviceChange(none, 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("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, 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(devIndex);
412 if (!info.property("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(this, info)) {
427 handleAutomaticDeviceChange(info, 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(deviceIndex);
444 if (currentDevice != device) {
445 if (!callSetOutputDevice(this, 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(deviceIndex);
456 if (currentDevice != device) {
457 // The device is not what we think it is, so lets say what is happening.
458 handleAutomaticDeviceChange(currentDevice, SoundSystemChange);
459 }
460 }
461}
462
463static struct
464{
465 int first;
466 int second;
467} g_lastFallback = { 0, 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(device2);
475 const AudioOutputDevice &device1 = AudioOutputDevice::fromIndex(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("<html>The audio playback device <b>%1</b> does not work.<br/>"
482 "Falling back to <b>%2</b>.</html>").arg(device1.name()).arg(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("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("<html>Switching to the audio playback device <b>%1</b><br/>"
495 "which just became available and has higher preference.</html>").arg(device2.name());
496 Platform::notification("AudioDeviceFallback", text,
497 QStringList(AudioOutput::tr("Revert back to device '%1'").arg(device1.name())),
498 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("available").toBool()) {
512 const QString text = AudioOutput::tr("<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(device2.name());
514 Platform::notification("AudioDeviceFallback", text,
515 QStringList(AudioOutput::tr("Revert back to device '%1'").arg(device1.name())),
516 q, SLOT(_k_revertFallback()));
517 } else {
518 const QString &text =
519 AudioOutput::tr("<html>The audio playback device <b>%1</b> does not work.<br/>"
520 "Falling back to <b>%2</b>.</html>").arg(device1.name()).arg(device2.name());
521 Platform::notification("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(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
Common base class for all audio outputs.
Class for audio output to the soundcard.
Definition audiooutput.h:49
void mutedChanged(bool)
This signal is emitted when the muted property has changed.
QString name
This is the name that appears in Mixer applications that control the volume of this output.
Definition audiooutput.h:60
AudioOutput(Phonon::Category category, QObject *parent=nullptr)
Creates a new AudioOutput that defines output to a physical device.
void volumeChanged(qreal newVolume)
This signal is emitted whenever the volume has changed.
qreal volume
This is the current loudness of the output (it is using Stevens' law to calculate the change in volta...
Definition audiooutput.h:67
Phonon::Category category() const
Returns the category of this output.
AudioOutputDevice outputDevice
This property holds the (hardware) destination for the output.
Definition audiooutput.h:89
qreal volumeDecibel
This is the current volume of the output in decibel.
Definition audiooutput.h:77
Provides a tuple of enduser visible name and description.
static ObjectDescription< T > fromIndex(int index)
Returns a new description object that describes the device/effect/codec/... with the given index.
void setVolume(qreal volume)
const_reference at(qsizetype i) const const
qsizetype count() const const
bool isEmpty() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString tr(const char *sourceText, const char *disambiguation, int n)
QString arg(Args &&... args) const const
QUuid createUuid()
QString toString(StringFormat mode) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:10:22 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.