Commit 5749abf2 authored by Jonathan Marten's avatar Jonathan Marten
Browse files

Make the volume feedback implementation common to all backends

Using a new singleton class VolumeFeedback, which receives volume
change events via a ControlManager announcement and plays the sound
for a volume change on the current global master device.  This also
handles rate limiting and stopping at maximum volume as per the
referenced bugs.  Remove the corresponding code from the PulseAudio
and ALSA backends.

In theory this means that volume feedback will also work with OSS,
assuming that Canberra supports OSS output.

CCBUG:302361
CCBUG:302474
CCBUG:323608
parent 0198cd80
......@@ -243,7 +243,8 @@ if (PulseAudio_FOUND)
endif (PulseAudio_FOUND)
if (CANBERRA_FOUND)
target_link_libraries(kmixcore PRIVATE ${CANBERRA_LIBRARIES})
# VolumeFeedback calls Canberra directly, so public linking is required
target_link_libraries(kmixcore PUBLIC ${CANBERRA_LIBRARIES})
endif (CANBERRA_FOUND)
install(TARGETS kmixcore DESTINATION ${KDE_INSTALL_LIBDIR} LIBRARY NAMELINK_SKIP)
......@@ -305,6 +306,10 @@ set(kmix_SRCS
${kmix_debug_SRCS}
)
if (CANBERRA_FOUND)
set(kmix_SRCS ${kmix_SRCS} apps/volumefeedback.cpp)
endif (CANBERRA_FOUND)
add_executable(kmix ${kmix_SRCS})
target_link_libraries(kmix
kmixcore
......
......@@ -22,19 +22,13 @@
// include files for Qt
#include <QApplication>
#include <QCheckBox>
#include <QLabel>
#include <QDesktopWidget>
#include <QMenuBar>
#include <QPushButton>
#include <qradiobutton.h>
#include <QCursor>
#include <QTabWidget>
#include <QPointer>
#include <QHash>
#include <QTimer>
#include <QDBusInterface>
#include <QDBusPendingCall>
#include <QKeySequence>
// include files for KDE
#include <kglobalaccel.h>
......@@ -42,26 +36,26 @@
#include <klocalizedstring.h>
#include <kstandardaction.h>
#include <kxmlguifactory.h>
#include <kactioncollection.h>
// KMix
#include "kmix_debug.h"
#include "core/ControlManager.h"
#include "core/MasterControl.h"
#include "core/MediaController.h"
#include "core/mixertoolbox.h"
#include "core/kmixdevicemanager.h"
#include "gui/guiprofile.h"
#include "gui/kmixerwidget.h"
#include "gui/kmixprefdlg.h"
#include "gui/kmixdockwidget.h"
#include "gui/kmixtoolbox.h"
#include "gui/viewdockareapopup.h"
#include "gui/dialogaddview.h"
#include "gui/dialogselectmaster.h"
#include "dbus/dbusmixsetwrapper.h"
#include "settings.h"
#ifdef HAVE_CANBERRA
#include "volumefeedback.h"
#endif
/* KMixWindow
* Constructs a mixer window (KMix main window)
*/
......@@ -103,8 +97,7 @@ KMixWindow::KMixWindow(bool invisible, bool reset) :
connect(theKMixDeviceManager, &KMixDeviceManager::unplugged, this, &KMixWindow::unplugged);
theKMixDeviceManager->initHotplug();
if (m_startVisible && !invisible)
show(); // Started visible
if (m_startVisible && !invisible) show(); // Started visible
connect(qApp, SIGNAL(aboutToQuit()), SLOT(saveConfig()) );
......@@ -112,7 +105,9 @@ KMixWindow::KMixWindow(bool invisible, bool reset) :
QString(), // All mixers (as the Global master Mixer might change)
ControlManager::ControlList|ControlManager::MasterChanged, this,
"KMixWindow");
#ifdef HAVE_CANBERRA
VolumeFeedback::instance()->init(); // set up for volume feedback
#endif
// Send an initial volume refresh (otherwise all volumes are 0 until the next change)
ControlManager::instance().announce(QString(), ControlManager::Volume, "Startup");
}
......@@ -750,6 +745,8 @@ void KMixWindow::newView()
QPointer<DialogAddView> dav = new DialogAddView(this, mixer);
int ret = dav->exec();
// TODO: it is pointless using a smart pointer for the dialogue
// (which is good practice) here and then not checking it!
if (QDialog::Accepted == ret)
{
QString profileName = dav->getresultViewName();
......
......@@ -21,18 +21,14 @@
#ifndef KMIXWINDOW_H
#define KMIXWINDOW_H
// Qt
#include <qboxlayout.h>
#include <qtimer.h>
// KDE
// class KAccel;
#include <kxmlguiwindow.h>
// KMix
#include "core/ControlManager.h"
#include "gui/kmixprefdlg.h"
class QTabWidget;
class KToggleAction;
......
/*
* KMix -- KDE's full featured mini mixer
*
* Copyright (C) 2021 Jonathan Marten <jjm@keelhaul.me.uk>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, see
* <https://www.gnu.org/licenses>.
*/
#include "volumefeedback.h"
// Qt
#include <QTimer>
// KDE
#include <klocalizedstring.h>
// KMix
#include "kmix_debug.h"
#include "core/mixer.h"
#include "settings.h"
// Others
extern "C"
{
#include <canberra.h>
}
// The Canberra API is described at
// https://developer.gnome.org/libcanberra/unstable/libcanberra-canberra.html
VolumeFeedback *VolumeFeedback::instance()
{
static VolumeFeedback *sInstance = new VolumeFeedback;
return (sInstance);
}
VolumeFeedback::VolumeFeedback()
{
qCDebug(KMIX_LOG);
m_currentMaster = nullptr;
m_veryFirstTime = true;
int ret = ca_context_create(&m_ccontext);
if (ret<0)
{
qCDebug(KMIX_LOG) << "Canberra context create failed, volume feedback unavailable - " << ca_strerror(ret);
m_ccontext = nullptr;
return;
}
m_feedbackTimer = new QTimer(this);
m_feedbackTimer->setSingleShot(true);
// This timer interval should be longer than the expected duration of the
// feedback sound, so that multiple sounds do not overlap or blend into
// a continuous sound. However, it should be short so as to be responsive
// to the user's actions. The freedesktop theme sound "audio-volume-change"
// is about 70 milliseconds long.
m_feedbackTimer->setInterval(150);
connect(m_feedbackTimer, &QTimer::timeout, this, &VolumeFeedback::slotPlayFeedback);
ControlManager::instance().addListener(QString(), // any mixer
ControlManager::MasterChanged, // type of change
this, // receiver
"VolumeFeedback (master)"); // source ID
}
VolumeFeedback::~VolumeFeedback()
{
if (m_ccontext!=nullptr) ca_context_destroy(m_ccontext);
}
void VolumeFeedback::init()
{
masterChanged();
}
void VolumeFeedback::controlsChange(ControlManager::ChangeType changeType)
{
switch (changeType)
{
case ControlManager::MasterChanged:
masterChanged();
break;
case ControlManager::Volume:
if (m_currentMaster==nullptr) return; // no current master device
if (!Settings::beepOnVolumeChange()) return; // feedback sound not wanted
volumeChanged(); // check volume and play sound
break;
default: ControlManager::warnUnexpectedChangeType(changeType, this);
break;
}
}
void VolumeFeedback::volumeChanged()
{
const Mixer *m = Mixer::getGlobalMasterMixer(); // current global master
const shared_ptr<MixDevice> md = m->getLocalMasterMD(); // its master device
int newvol = md->userVolumeLevel(); // current volume level
//qCDebug(KMIX_LOG) << m_currentVolume << "->" << newvol;
if (newvol==m_currentVolume) return; // volume has not changes
m_feedbackTimer->start(); // restart the timer
m_currentVolume = newvol; // note new current volume
}
void VolumeFeedback::masterChanged()
{
const Mixer *globalMaster = Mixer::getGlobalMasterMixer();
if (globalMaster==nullptr)
{
qCDebug(KMIX_LOG) << "no current global master";
m_currentMaster.clear();
return;
}
const shared_ptr<MixDevice> md = globalMaster->getLocalMasterMD();
const Volume &vol = md->playbackVolume();
if (!vol.hasVolume())
{
qCDebug(KMIX_LOG) << "device" << md->id() << "has no playback volume";
m_currentMaster.clear();
return;
}
// Make a unique name for the mixer and master device.
const QString masterId = globalMaster->id()+"|"+md->id();
// Then check whether it is the same as already recorded.
if (masterId==m_currentMaster)
{
qCDebug(KMIX_LOG) << "current master is already" << m_currentMaster;
return;
}
qCDebug(KMIX_LOG) << "from" << (m_currentMaster.isEmpty() ? "(none)" : m_currentMaster)
<< "to" << masterId;
m_currentMaster = masterId;
// Remove only the listener for ControlManager::Volume,
// retaining the one for ControlManager::MasterChanged.
ControlManager::instance().removeListener(this, ControlManager::Volume, "VolumeFeedback");
// Then monitor for a volume change on the new master
ControlManager::instance().addListener(globalMaster->id(), // mixer ID
ControlManager::Volume, // type of change
this, // receiver
"VolumeFeedback (volume)"); // source ID
// Set the Canberra driver to match the master device.
// I can't seem to find any documentation on the driver
// names that are supported, so this is just a guess based
// on the name set by original PulseAudio implementation.
QString driver = globalMaster->getDriverName().toLower();
if (driver=="pulseaudio") driver = "pulse";
qCDebug(KMIX_LOG) << "Setting Canberra driver to" << driver;
ca_context_set_driver(m_ccontext, driver.toLocal8Bit());
// Similarly, this is just a guess based on the existing
// PulseAudio and ALSA support. All existing backends
// set the UDI to the equivalent of the hardware device
// name.
QString device = globalMaster->udi();
if (!device.isEmpty())
{
qCDebug(KMIX_LOG) << "Setting Canberra device to" << device;
ca_context_change_device(m_ccontext, device.toLocal8Bit());
}
m_currentVolume = -1; // always make a sound after change
controlsChange(ControlManager::Volume); // simulate a volume change
}
// Originally taken from Mixer_PULSE::writeVolumeToHW()
void VolumeFeedback::slotPlayFeedback()
{
if (m_ccontext==nullptr) return; // Canberra is not initialised
// Inhibit the very first feedback sound after KMix has started.
// Otherwise it will be played during desktop startup, possibly
// interfering with the login sound and definitely confusing users.
if (m_veryFirstTime)
{
m_veryFirstTime = false;
return;
}
int playing = 0;
// Note that '2' is simply an index we've picked.
// It's mostly irrelevant.
int cindex = 2;
ca_context_playing(m_ccontext, cindex, &playing);
// Note: Depending on how this is desired to work,
// we may want to simply skip playing, or cancel the
// currently playing sound and play our
// new one... for now, let's do the latter.
if (playing)
{
ca_context_cancel(m_ccontext, cindex);
playing = 0;
}
if (playing==0)
{
// ca_context_set_driver() and ca_context_change_device()
// have already been done in masterChanged() above.
// Ideally we'd use something like ca_gtk_play_for_widget()...
ca_context_play(
m_ccontext,
cindex,
CA_PROP_EVENT_DESCRIPTION, i18n("Volume Control Feedback Sound").toUtf8(),
CA_PROP_EVENT_ID, "audio-volume-change",
CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
CA_PROP_CANBERRA_ENABLE, "1",
nullptr);
ca_context_change_device(m_ccontext, nullptr);
}
}
/*
* KMix -- KDE's full featured mini mixer
*
* Copyright (C) 2021 Jonathan Marten <jjm@keelhaul.me.uk>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, see
* <https://www.gnu.org/licenses>.
*/
#ifndef VOLUMEFEEDBACK_H
#define VOLUMEFEEDBACK_H
#include <qobject.h>
#include "core/ControlManager.h"
class QTimer;
class ca_context;
class VolumeFeedback : public QObject
{
Q_OBJECT
public:
static VolumeFeedback *instance();
void init();
public slots:
void controlsChange(ControlManager::ChangeType changeType);
private:
VolumeFeedback();
virtual ~VolumeFeedback();
void volumeChanged();
void masterChanged();
private slots:
void slotPlayFeedback();
private:
QString m_currentMaster;
int m_currentVolume;
QTimer *m_feedbackTimer;
bool m_veryFirstTime;
ca_context *m_ccontext;
};
#endif // VOLUMEFEEDBACK_H
......@@ -30,31 +30,13 @@
#include "core/kmixdevicemanager.h"
#include "core/mixer.h"
#include "core/volume.h"
#include "settings.h"
// KDE
#include <klocalizedstring.h>
// Qt
#include <qtimer.h>
// STD Headers
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <assert.h>
#include <qsocketnotifier.h>
#ifdef HAVE_CANBERRA
#include <canberra.h>
#endif
#ifdef HAVE_CANBERRA
static ca_context *s_ccontext = nullptr;
static unsigned int s_refcount = 0;
#endif
// #define if you want MUCH debugging output
//#define ALSA_SWITCH_DEBUG
//#define KMIX_ALSA_VOLUME_DEBUG
......@@ -76,49 +58,12 @@ Mixer_ALSA::Mixer_ALSA( Mixer* mixer, int device ) : Mixer_Backend(mixer, devic
_handle = 0;
ctl_handle = 0;
_initialUpdate = true;
#ifdef HAVE_CANBERRA
++s_refcount; // increment Canberra reference count
if (s_refcount==1) // initialise Canberra the first time
{
int ret = ca_context_create(&s_ccontext);
if (ret<0)
{
qCWarning(KMIX_LOG) << "Failed to create Canberra context for volume feedback," << ca_strerror(ret);
s_ccontext = nullptr;
return;
}
ca_context_set_driver(s_ccontext, "alsa");
qCDebug(KMIX_LOG) << "Initialised Canberra context for volume feedback";
}
m_playFeedbackTimer = new QTimer(this);
m_playFeedbackTimer->setSingleShot(true);
m_playFeedbackTimer->setInterval(100);
m_playFeedbackTimer->callOnTimeout(this, &Mixer_ALSA::playFeedbackSound);
#endif
}
Mixer_ALSA::~Mixer_ALSA()
{
close();
#ifdef HAVE_CANBERRA
if (s_refcount>0) // have some Canberra references
{
--s_refcount; // decrement Canberra reference count
if (s_refcount==0) // no more references remaining
{
if (s_ccontext!=nullptr) // have a Canberra context
{
ca_context_destroy(s_ccontext); // don't need it any more
s_ccontext = nullptr;
qCDebug(KMIX_LOG) << "Finished with Canberra context";
}
}
}
#endif
}
......@@ -968,11 +913,6 @@ Mixer_ALSA::writeVolumeToHW( const QString& id, shared_ptr<MixDevice> md )
//if (id== "Master:0" || id== "PCM:0" ) { qCDebug(KMIX_LOG) << "volumePlayback control=" << id << ", chid=" << vc.chid << ", vol=" << vc.volume; }
}
}
#ifdef HAVE_CANBERRA
m_playFeedbackTimer->start();
#endif
} // has playback volume
// --- capture volume
......@@ -1044,54 +984,3 @@ QString Mixer_ALSA::getDriverName()
{
return QStringLiteral("ALSA");
}
#ifdef HAVE_CANBERRA
void Mixer_ALSA::playFeedbackSound()
{
if (!Settings::beepOnVolumeChange()) return; // no feedback sound required
if (s_ccontext==nullptr) return; // Canberra not set up
int playing = 0;
// Note that '2' is simply an index we've picked.
// It's mostly irrelevant.
const int cindex = 2;
ca_context_playing(s_ccontext, cindex, &playing);
// Note: Depending on how this is desired to work,
// we may want to simply skip playing, or cancel the
// currently playing sound and play our
// new one... for now, let's do the latter.
if (playing!=0)
{
ca_context_cancel(s_ccontext, cindex);
playing = 0;
}
if (playing==0)
{
if (!m_deviceName.isEmpty()) ca_context_change_device(s_ccontext, m_deviceName.constData());
// Ideally we'd use something like ca_gtk_play_for_widget()...
int ret = ca_context_play(s_ccontext,
cindex,
CA_PROP_EVENT_DESCRIPTION, i18n("Volume Control Feedback Sound").toUtf8().constData(),
CA_PROP_EVENT_ID, "audio-volume-change",
CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
CA_PROP_CANBERRA_ENABLE, "1",
nullptr);
// Sometimes trying to play sounds in quick succession returns the
// error CA_ERROR_NOTAVAILABLE = "Not available", even though playing
// the previous sound has been cancelled above. Ignore that error.
if (ret<0 && ret!=CA_ERROR_NOTAVAILABLE)
{
qCWarning(KMIX_LOG) << "Failed to play Canberra sound for volume feedback," << ca_strerror(ret);
}
ca_context_change_device(s_ccontext, nullptr);
}
}
#endif // HAVE_CANBERRA
......@@ -46,7 +46,7 @@ public:
unsigned int enumIdHW(const QString& id) override;
bool hasChangedControls() override;
bool needsPolling() override { return false; }
bool needsPolling() override { return (false); }
QString getDriverName() override;
protected:
......@@ -54,13 +54,7 @@ protected:
int close() override;
int id2num(const QString& id);
private slots:
#ifdef HAVE_CANBERRA
void playFeedbackSound();
#endif
private:
int openAlsaDevice(const QString& devName);
void addEnumerated(snd_mixer_elem_t *elem, QList<QString*>&);
Volume* addVolume(snd_mixer_elem_t *elem, bool capture);
......@@ -72,6 +66,8 @@ private:
snd_mixer_elem_t* getMixerElem(int devnum);
QString errorText(int mixer_error) override;
private:
typedef QList<snd_mixer_selem_id_t *>AlsaMixerSidList;
AlsaMixerSidList mixer_sid_list;
typedef QList<snd_mixer_elem_t *> AlsaMixerElemList;
......@@ -88,11 +84,6 @@ private:
QList<QSocketNotifier*> m_sns;
QByteArray m_deviceName;
#ifdef HAVE_CANBERRA
QTimer *m_playFeedbackTimer;
#endif
};
#endif
......@@ -23,9 +23,6 @@
#include "qtpamainloop.h"
#include <cstdlib>
#include <QAbstractEventDispatcher>
#include <QTimer>
#include <QStringBuilder>
#include <klocalizedstring.h>
......@@ -35,9 +32,6 @@
#include "settings.h"
#include <pulse/ext-stream-restore.h>
#ifdef HAVE_CANBERRA