Commit 1fd019b0 authored by Jonathan Marten's avatar Jonathan Marten
Browse files

Merge branch 'feedback-common-all-backends'

parents 1da1a84b 5749abf2
......@@ -245,7 +245,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)
......@@ -307,6 +308,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