File indexing completed on 2022-09-28 10:22:33

0001 /*
0002  * KMix -- KDE's full featured mini mixer
0003  *
0004  * Copyright (C) 2021 Jonathan Marten <jjm@keelhaul.me.uk>
0005  *
0006  * This program is free software; you can redistribute it and/or
0007  * modify it under the terms of the GNU Library General Public
0008  * License as published by the Free Software Foundation; either
0009  * version 2 of the License, or (at your option) any later version.
0010  *
0011  * This program is distributed in the hope that it will be useful,
0012  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0014  * Library General Public License for more details.
0015  *
0016  * You should have received a copy of the GNU Library General Public
0017  * License along with this program; if not, see
0018  * <https://www.gnu.org/licenses>.
0019  */
0020 
0021 #include "volumefeedback.h"
0022 
0023 // Qt
0024 #include <QTimer>
0025 
0026 // KDE
0027 #include <klocalizedstring.h>
0028 
0029 // KMix
0030 #include "kmix_debug.h"
0031 #include "core/mixer.h"
0032 #include "settings.h"
0033 
0034 // Others
0035 extern "C"
0036 {
0037 #include <canberra.h>
0038 }
0039 
0040 // The Canberra API is described at
0041 // https://developer.gnome.org/libcanberra/unstable/libcanberra-canberra.html
0042 
0043 
0044 VolumeFeedback *VolumeFeedback::instance()
0045 {
0046     static VolumeFeedback *sInstance = new VolumeFeedback;
0047     return (sInstance);
0048 }
0049 
0050 
0051 VolumeFeedback::VolumeFeedback()
0052 {
0053     qCDebug(KMIX_LOG);
0054     m_currentMaster = nullptr;
0055     m_veryFirstTime = true;
0056 
0057     int ret = ca_context_create(&m_ccontext);
0058     if (ret<0)
0059     {
0060                 qCDebug(KMIX_LOG) << "Canberra context create failed, volume feedback unavailable - " << ca_strerror(ret);
0061                 m_ccontext = nullptr;
0062         return;
0063     }
0064 
0065     m_feedbackTimer = new QTimer(this);
0066     m_feedbackTimer->setSingleShot(true);
0067     // This timer interval should be longer than the expected duration of the
0068     // feedback sound, so that multiple sounds do not overlap or blend into
0069     // a continuous sound.  However, it should be short so as to be responsive
0070     // to the user's actions.  The freedesktop theme sound "audio-volume-change"
0071     // is about 70 milliseconds long.
0072     m_feedbackTimer->setInterval(150);
0073     connect(m_feedbackTimer, &QTimer::timeout, this, &VolumeFeedback::slotPlayFeedback);
0074 
0075     ControlManager::instance().addListener(QString(),           // any mixer
0076                            ControlManager::MasterChanged,   // type of change
0077                            this,                // receiver
0078                            "VolumeFeedback (master)");  // source ID
0079 }
0080 
0081 
0082 VolumeFeedback::~VolumeFeedback()
0083 {
0084     if (m_ccontext!=nullptr) ca_context_destroy(m_ccontext);
0085 }
0086 
0087 
0088 void VolumeFeedback::init()
0089 {
0090     masterChanged();
0091 }
0092 
0093 
0094 void VolumeFeedback::controlsChange(ControlManager::ChangeType changeType)
0095 {
0096     switch (changeType)
0097     {
0098 case ControlManager::MasterChanged:
0099         masterChanged();
0100         break;
0101 
0102 case ControlManager::Volume:
0103         if (m_currentMaster==nullptr) return;       // no current master device
0104         if (!Settings::beepOnVolumeChange()) return;    // feedback sound not wanted
0105         volumeChanged();                // check volume and play sound
0106         break;
0107 
0108 default:    ControlManager::warnUnexpectedChangeType(changeType, this);
0109         break;
0110     }
0111 }
0112 
0113 
0114 void VolumeFeedback::volumeChanged()
0115 {
0116     const Mixer *m = Mixer::getGlobalMasterMixer();     // current global master
0117     const shared_ptr<MixDevice> md = m->getLocalMasterMD(); // its master device
0118     if (md==nullptr)
0119     {
0120         qCDebug(KMIX_LOG) << "global master doest have a local master MD ( MixDevice )";
0121         m_currentMaster.clear();
0122         return;
0123     }
0124 
0125     int newvol = md->userVolumeLevel();         // current volume level
0126     //qCDebug(KMIX_LOG) << m_currentVolume << "->" << newvol;
0127 
0128     if (newvol==m_currentVolume) return;            // volume has not changes
0129     m_feedbackTimer->start();               // restart the timer
0130     m_currentVolume = newvol;               // note new current volume
0131 }
0132 
0133 
0134 void VolumeFeedback::masterChanged()
0135 {
0136     const Mixer *globalMaster = Mixer::getGlobalMasterMixer();
0137     if (globalMaster==nullptr)
0138     {
0139         qCDebug(KMIX_LOG) << "no current global master";
0140         m_currentMaster.clear();
0141         return;
0142     }
0143 
0144     const shared_ptr<MixDevice> md = globalMaster->getLocalMasterMD();
0145     if (md==nullptr)
0146     {
0147         qCDebug(KMIX_LOG) << "global master doest have a local master MD ( MixDevice )";
0148         m_currentMaster.clear();
0149         return;
0150     }
0151     const Volume &vol = md->playbackVolume();
0152     if (!vol.hasVolume())
0153     {
0154         qCDebug(KMIX_LOG) << "device" << md->id() << "has no playback volume";
0155         m_currentMaster.clear();
0156         return;
0157     }
0158 
0159     // Make a unique name for the mixer and master device.
0160     const QString masterId = globalMaster->id()+"|"+md->id();
0161     // Then check whether it is the same as already recorded.
0162     if (masterId==m_currentMaster)
0163     {
0164         qCDebug(KMIX_LOG) << "current master is already" << m_currentMaster;
0165         return;
0166     }
0167 
0168     qCDebug(KMIX_LOG) << "from" << (m_currentMaster.isEmpty() ? "(none)" : m_currentMaster)
0169               << "to" << masterId;
0170     m_currentMaster = masterId;
0171 
0172     // Remove only the listener for ControlManager::Volume,
0173     // retaining the one for ControlManager::MasterChanged.
0174     ControlManager::instance().removeListener(this, ControlManager::Volume, "VolumeFeedback");
0175 
0176     // Then monitor for a volume change on the new master
0177     ControlManager::instance().addListener(globalMaster->id(),      // mixer ID
0178                            ControlManager::Volume,      // type of change
0179                            this,                // receiver
0180                            "VolumeFeedback (volume)");  // source ID
0181 
0182     // Set the Canberra driver to match the master device.
0183     // I can't seem to find any documentation on the driver
0184     // names that are supported, so this is just a guess based
0185     // on the name set by original PulseAudio implementation.
0186     QString driver = globalMaster->getDriverName().toLower();
0187     if (driver=="pulseaudio") driver = "pulse";
0188     qCDebug(KMIX_LOG) << "Setting Canberra driver to" << driver;
0189     ca_context_set_driver(m_ccontext, driver.toLocal8Bit());
0190 
0191     // Similarly, this is just a guess based on the existing
0192     // PulseAudio and ALSA support.  All existing backends
0193     // set the UDI to the equivalent of the hardware device
0194     // name.
0195     QString device = globalMaster->udi();
0196     if (!device.isEmpty())
0197     {
0198         qCDebug(KMIX_LOG) << "Setting Canberra device to" << device;
0199         ca_context_change_device(m_ccontext, device.toLocal8Bit());
0200     }
0201 
0202     m_currentVolume = -1;               // always make a sound after change
0203     controlsChange(ControlManager::Volume);     // simulate a volume change
0204 }
0205 
0206 
0207 // Originally taken from Mixer_PULSE::writeVolumeToHW()
0208 
0209 void VolumeFeedback::slotPlayFeedback()
0210 {
0211     if (m_ccontext==nullptr) return;        // Canberra is not initialised
0212 
0213     // Inhibit the very first feedback sound after KMix has started.
0214     // Otherwise it will be played during desktop startup, possibly
0215     // interfering with the login sound and definitely confusing users.
0216     if (m_veryFirstTime)
0217     {
0218         m_veryFirstTime = false;
0219         return;
0220     }
0221 
0222     int playing = 0;
0223     // Note that '2' is simply an index we've picked.
0224     // It's mostly irrelevant.
0225     int cindex = 2;
0226 
0227     ca_context_playing(m_ccontext, cindex, &playing);
0228 
0229     // Note: Depending on how this is desired to work,
0230     // we may want to simply skip playing, or cancel the
0231     // currently playing sound and play our
0232     // new one... for now, let's do the latter.
0233     if (playing)
0234     {
0235         ca_context_cancel(m_ccontext, cindex);
0236         playing = 0;
0237     }
0238 
0239     if (playing==0)
0240     {
0241         // ca_context_set_driver() and ca_context_change_device()
0242         // have already been done in masterChanged() above.
0243 
0244         // Ideally we'd use something like ca_gtk_play_for_widget()...
0245         ca_context_play(
0246             m_ccontext,
0247             cindex,
0248             CA_PROP_EVENT_DESCRIPTION, i18n("Volume Control Feedback Sound").toUtf8().constData(),
0249             CA_PROP_EVENT_ID, "audio-volume-change",
0250             CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
0251             CA_PROP_CANBERRA_ENABLE, "1",
0252             nullptr);
0253 
0254         ca_context_change_device(m_ccontext, nullptr);
0255     }
0256 }