diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e275d3c..4c81139e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,353 +1,352 @@ cmake_minimum_required(VERSION 2.8.12) project(kmix) set(PROJECT_VERSION "5.13.80") set(PROJECT_VERSION_MAJOR 5) add_definitions( -DTRANSLATION_DOMAIN=\"kmix\" ) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") set (QT_MIN_VERSION "5.7.0") set (KF5_MIN_VERSION "5.41.0") set (PA_MIN_VERSION "0.9.16") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMPackageConfigHelpers) include(ECMInstallIcons) include(ECMQtDeclareLoggingCategory) include(GenerateExportHeader) find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core DBus Gui Widgets Xml ) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Completion Config ConfigWidgets Crash DBusAddons DocTools GlobalAccel I18n IconThemes Notifications Plasma Solid WidgetsAddons WindowSystem XmlGui ) set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH}) # PulseAudio is an optional dependency # find_package(PulseAudio "${PA_MIN_VERSION}") # PulseAudio requires GLib2 if (PulseAudio_FOUND) find_package(GLIB2 REQUIRED) endif(PulseAudio_FOUND) # Canberra is an optional dependency find_package(Canberra) find_package(ALSA) include(CheckCXXSourceCompiles) check_cxx_source_compiles(" #include int main() { std::shared_ptr p; return 0; } " HAVE_STD_SHARED_PTR) check_cxx_source_compiles(" #include int main() { std::tr1::shared_ptr p; return 0; } " HAVE_STD_TR1_SHARED_PTR) configure_file (config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h ) #################################################################################################### ########### compile definitions #################################################################### #################################################################################################### include_directories(${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) # TODO: is the next line needed now? include_directories("/usr/lib/oss/include") # TODO: is the next line really needed? Nothing seems to use anything to do with tagging. if (MSVC) include_directories(${TAGLIB_INCLUDES}) endif (MSVC) if (ALSA_FOUND) set(HAVE_LIBASOUND2 TRUE) add_definitions(-DHAVE_LIBASOUND2) endif (ALSA_FOUND) if (PulseAudio_FOUND) add_definitions(-DHAVE_PULSE) include_directories(${PulseAudio_INCLUDE_DIRS}) include_directories(${GLIB2_INCLUDE_DIR}) endif (PulseAudio_FOUND) if (CANBERRA_FOUND) add_definitions(-DHAVE_CANBERRA) include_directories(${CANBERRA_INCLUDE_DIRS}) endif (CANBERRA_FOUND) #################################################################################################### ########### subdirectories ######################################################################### #################################################################################################### add_subdirectory(doc) add_subdirectory(pics) add_subdirectory(profiles) #add_subdirectory(tests) add_subdirectory(plasma) #################################################################################################### ########### definitions: logging ################################################################### #################################################################################################### ecm_qt_declare_logging_category(kmix_debug_SRCS HEADER kmix_debug.h IDENTIFIER KMIX_LOG CATEGORY_NAME org.kde.kmix) #################################################################################################### ########### definitions: DBus adaptor ############################################################## #################################################################################################### set(kmix_adaptor_SRCS dbus/dbusmixerwrapper.cpp dbus/dbusmixsetwrapper.cpp dbus/dbuscontrolwrapper.cpp ) qt5_add_dbus_adaptor( kmix_adaptor_SRCS dbus/org.kde.kmix.control.xml dbus/dbuscontrolwrapper.h DBusControlWrapper ) qt5_add_dbus_adaptor( kmix_adaptor_SRCS dbus/org.kde.kmix.mixer.xml dbus/dbusmixerwrapper.h DBusMixerWrapper ) qt5_add_dbus_adaptor( kmix_adaptor_SRCS dbus/org.kde.kmix.mixset.xml dbus/dbusmixsetwrapper.h DBusMixSetWrapper ) install(FILES dbus/org.kde.kmix.control.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) install(FILES dbus/org.kde.kmix.mixer.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) install(FILES dbus/org.kde.kmix.mixset.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) #################################################################################################### ########### definitions: backends ################################################################## #################################################################################################### set(kmix_backend_SRCS backends/mixer_backend.cpp backends/mixer_mpris2.cpp ) if (HAVE_LIBASOUND2) set(kmix_backend_SRCS ${kmix_backend_SRCS} backends/mixer_alsa9.cpp ) endif (HAVE_LIBASOUND2) if (PulseAudio_FOUND) set(kmix_backend_SRCS ${kmix_backend_SRCS} backends/mixer_pulse.cpp ) endif (PulseAudio_FOUND) #################################################################################################### ########### target: kmixcore library ############################################################### #################################################################################################### set(kmixcore_SRCS core/MediaController.cpp core/mixertoolbox.cpp core/kmixdevicemanager.cpp core/ControlManager.cpp core/GlobalConfig.cpp core/MasterControl.cpp core/mixer.cpp core/mixset.cpp core/mixdevice.cpp core/mixdevicecomposite.cpp core/volume.cpp ) add_library(kmixcore SHARED ${kmixcore_SRCS} ${kmix_adaptor_SRCS} ${kmix_backend_SRCS} ${kmix_debug_SRCS} ) target_link_libraries(kmixcore PUBLIC Qt5::Core Qt5::Widgets PRIVATE Qt5::DBus KF5::I18n KF5::Solid PUBLIC KF5::ConfigCore KF5::ConfigGui ) set_target_properties(kmixcore PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) generate_export_header(kmixcore BASE_NAME kmixcore EXPORT_FILE_NAME kmixcore_export.h) if (HAVE_LIBASOUND2) target_link_libraries(kmixcore PRIVATE ${ALSA_LIBRARIES}) endif (HAVE_LIBASOUND2) if (PulseAudio_FOUND) target_link_libraries(kmixcore PRIVATE ${PulseAudio_LIBRARIES} ${PulseAudio_MAINLOOP_LIBRARY} ${GLIB2_LIBRARIES}) endif (PulseAudio_FOUND) if (CANBERRA_FOUND) target_link_libraries(kmixcore PRIVATE ${CANBERRA_LIBRARIES}) endif (CANBERRA_FOUND) install(TARGETS kmixcore DESTINATION ${KDE_INSTALL_LIBDIR} LIBRARY NAMELINK_SKIP) #################################################################################################### ########### target: kmixgui library ################################################################ #################################################################################################### set(kmixgui_SRCS gui/dialogbase.cpp gui/dialogstatesaver.cpp gui/kmixdockwidget.cpp gui/kmixprefdlg.cpp gui/viewbase.cpp gui/viewdockareapopup.cpp gui/viewsliders.cpp gui/mixdevicewidget.cpp - gui/mdwmoveaction.cpp gui/mdwslider.cpp gui/mdwenum.cpp gui/kmixerwidget.cpp gui/ksmallslider.cpp gui/verticaltext.cpp gui/volumeslider.cpp gui/kmixtoolbox.cpp gui/dialogaddview.cpp gui/dialogviewconfiguration.cpp gui/dialogselectmaster.cpp gui/dialogchoosebackends.cpp gui/guiprofile.cpp gui/toggletoolbutton.cpp ) add_library(kmixgui STATIC ${kmixgui_SRCS} ${kmix_debug_SRCS} ) target_link_libraries(kmixgui kmixcore Qt5::Core Qt5::Widgets KF5::I18n KF5::ConfigCore KF5::ConfigGui KF5::IconThemes KF5::GlobalAccel KF5::Notifications KF5::XmlGui KF5::WindowSystem ) #################################################################################################### ########### target: kmix ########################################################################### #################################################################################################### set(kmix_SRCS apps/main.cpp apps/kmix.cpp apps/KMixApp.cpp ${kmix_debug_SRCS} ) add_executable(kmix ${kmix_SRCS}) target_link_libraries(kmix kmixcore kmixgui KF5::I18n KF5::IconThemes KF5::DBusAddons KF5::GlobalAccel KF5::XmlGui KF5::Notifications KF5::WindowSystem ) install(TARGETS kmix ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES desktop/kmixui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kmix) install(PROGRAMS desktop/org.kde.kmix.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES desktop/org.kde.kmix.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES desktop/kmix_autostart.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) install(FILES desktop/kmix.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR} ) #################################################################################################### ########### target: kded_kmixd ##################################################################### #################################################################################################### set(kmixd_SRCS apps/kmixd.cpp ${kmix_debug_SRCS} ) add_library(kded_kmixd MODULE ${kmixd_SRCS}) set_target_properties(kded_kmixd PROPERTIES OUTPUT_NAME kmixd) kcoreaddons_desktop_to_json(kded_kmixd desktop/kmixd.desktop) target_link_libraries(kded_kmixd kmixcore KF5::I18n KF5::CoreAddons KF5::DBusAddons ) install(TARGETS kded_kmixd DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/kded) #################################################################################################### ########### target: kmixctrl ####################################################################### #################################################################################################### set(kmixctrl_SRCS apps/kmixctrl.cpp ${kmix_debug_SRCS} ) add_executable(kmixctrl ${kmixctrl_SRCS}) target_link_libraries(kmixctrl kmixcore KF5::I18n KF5::CoreAddons ) install(TARGETS kmixctrl ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES desktop/kmixctrl_restore.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) #################################################################################################### ########### other installs ######################################################################### #################################################################################################### install(PROGRAMS apps/kmixremote DESTINATION ${KDE_INSTALL_BINDIR}) install(FILES desktop/restore_kmix_volumes.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) #################################################################################################### ########### end #################################################################################### #################################################################################################### feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/backends/mixer_backend.cpp b/backends/mixer_backend.cpp index cf7ca0d3..25a5a717 100644 --- a/backends/mixer_backend.cpp +++ b/backends/mixer_backend.cpp @@ -1,337 +1,348 @@ /* * KMix -- KDE's full featured mini mixer * * Copyright 2006-2007 Christian Esken * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mixer_backend.h" #include // for the "ERR_" declarations, #include mixer.h #include "core/mixer.h" #include "core/ControlManager.h" #include #define POLL_RATE_SLOW 1500 #define POLL_RATE_FAST 50 #include "mixer_backend_i18n.cpp" Mixer_Backend::Mixer_Backend(Mixer *mixer, int device) : m_devnum (device) , m_isOpen(false), m_recommendedMaster(), _mixer(mixer), _pollingTimer(0), _cardInstance(1), _cardRegistered(false) { // In all cases create a QTimer. We will use it once as a singleShot(), even if something smart // like ::select() is possible (as in ALSA). And force to do an update. _readSetFromHWforceUpdate = true; _pollingTimer = new QTimer(); // will be started on open() and stopped on close() connect( _pollingTimer, SIGNAL(timeout()), this, SLOT(readSetFromHW()), Qt::QueuedConnection); } void Mixer_Backend::closeCommon() { freeMixDevices(); } int Mixer_Backend::close() { qCDebug(KMIX_LOG) << "Implicit close on " << this << ". Please instead call closeCommon() and close() explicitly (in concrete Backend destructor)"; // ^^^ Background. before the destructor runs, the C++ runtime changes the virtual pointers to point back // to the common base class. So what actually runs is not run Mixer_ALSA::close(), but this method. // // See http://stackoverflow.com/questions/99552/where-do-pure-virtual-function-call-crashes-come-from?lq=1 // // Comment: IMO this is totally stupid and insane behavior of C++, because you cannot simply cannot call // the overwritten (cleanup) methods in the destructor. return 0; } Mixer_Backend::~Mixer_Backend() { unregisterCard(this->getName()); if (!m_mixDevices.isEmpty()) { qCDebug(KMIX_LOG) << "Implicit close on " << this << ". Please instead call closeCommon() and close() explicitly (in concrete Backend destructor)"; } delete _pollingTimer; } void Mixer_Backend::freeMixDevices() { foreach (shared_ptr md, m_mixDevices) md->close(); m_mixDevices.clear(); } bool Mixer_Backend::openIfValid() { const int ret = open(); if (ret!=0) { //qCWarning(KMIX_LOG) << "open" << getName() << "failed" << ret; return false; // could not open } qCDebug(KMIX_LOG) << "opened" << getName() << "count" << m_mixDevices.count() << "dynamic?" << _mixer->isDynamic() << "needsPolling?" << needsPolling(); if (m_mixDevices.count() > 0 || _mixer->isDynamic()) { if (needsPolling()) { _pollingTimer->start(POLL_RATE_FAST); } else { // The initial state must be read manually QTimer::singleShot( POLL_RATE_FAST, this, SLOT(readSetFromHW())); } return true; // could be opened } else { qCWarning(KMIX_LOG) << "no mix devices and not dynamic"; return false; // could not open } } bool Mixer_Backend::isOpen() { return m_isOpen; } /** * Queries the backend driver whether there are new changes in any of the controls. * If you cannot find out for a backend, return "true" - this is also the default implementation. * @return true, if there are changes. Otherwise false is returned. */ bool Mixer_Backend::hasChangedControls() { return true; } /** * The name of the Mixer this backend represents. * Often it is just a name/id for the kernel. so name and id are usually identical. Virtual/abstracting backends are * different, as they represent some distinct function like "Application streams" or "Capture Devices". Also backends * that do not have names might can to set ID and name different like i18n("SUN Audio") and "SUNAudio". */ QString Mixer_Backend::getName() const { return m_mixerName; } /** * The id of the Mixer this backend represents. The default implementation simply returns the name. * Often it is just a name/id for the kernel. so name and id are usually identical. See also #Mixer_Backend::getName(). * You must override this method if you want to set ID different from name. */ QString Mixer_Backend::getId() const { return m_mixerName; // Backwards compatibility. PulseAudio overrides it. } /** * After calling this, readSetFromHW() will do a complete update. This will * trigger emitting the appropriate signals like controlChanged(). * * This method is useful, if you need to get a "refresh signal" - used at: * 1) Start of KMix - so that we can be sure an initial signal is emitted * 2) When reconstructing any MixerWidget (e.g. DockIcon after applying preferences) */ void Mixer_Backend::readSetFromHWforceUpdate() const { _readSetFromHWforceUpdate = true; } /** * You can call this to retrieve the freshest information from the mixer HW. * This method is also called regularly by the mixer timer. */ void Mixer_Backend::readSetFromHW() { bool updated = hasChangedControls(); if ( (! updated) && (! _readSetFromHWforceUpdate) ) { // Some drivers (ALSA) are smart. We don't need to run the following // time-consuming update loop if there was no change qCDebug(KMIX_LOG) << "Mixer::readSetFromHW(): smart-update-tick"; return; } _readSetFromHWforceUpdate = false; int ret = Mixer::OK_UNCHANGED; foreach (shared_ptr md, m_mixDevices ) { //bool debugMe = (md->id() == "PCM:0" ); bool debugMe = false; if (debugMe) qCDebug(KMIX_LOG) << "Old PCM:0 playback state" << md->isMuted() << ", vol=" << md->playbackVolume().getAvgVolumePercent(Volume::MALL); int retLoop = readVolumeFromHW( md->id(), md ); if (debugMe) qCDebug(KMIX_LOG) << "New PCM:0 playback state" << md->isMuted() << ", vol=" << md->playbackVolume().getAvgVolumePercent(Volume::MALL); if (md->isEnum() ) { /* * This could be reworked: * Plan: Read everything (including enum's) in readVolumeFromHW(). * readVolumeFromHW() should then be renamed to readHW(). */ md->setEnumId( enumIdHW(md->id()) ); } // Transition the outer return value with the value from this loop iteration if ( retLoop == Mixer::OK && ret == Mixer::OK_UNCHANGED ) { // Unchanged => OK (Changed) ret = Mixer::OK; } else if ( retLoop != Mixer::OK && retLoop != Mixer::OK_UNCHANGED ) { // If current ret from loop in not OK, then transition to that: ret (Something) => retLoop (Error) ret = retLoop; } } if ( ret == Mixer::OK ) { // We explicitly exclude Mixer::OK_UNCHANGED and Mixer::ERROR_READ if ( needsPolling() ) { // Upgrade polling frequency temporarily to be more smoooooth _pollingTimer->setInterval(POLL_RATE_FAST); QTime fastPollingEndsAt = QTime::currentTime (); fastPollingEndsAt = fastPollingEndsAt.addSecs(5); _fastPollingEndsAt = fastPollingEndsAt; //_fastPollingEndsAt = fastPollingEndsAt; qCDebug(KMIX_LOG) << "Start fast polling from " << QTime::currentTime() <<"until " << _fastPollingEndsAt; } ControlManager::instance().announce(_mixer->id(), ControlManager::Volume, QString("Mixer.fromHW")); } else { // This code path is entered on Mixer::OK_UNCHANGED and ERROR bool fastPollingEndsNow = (!_fastPollingEndsAt.isNull()) && _fastPollingEndsAt < QTime::currentTime (); if ( fastPollingEndsNow ) { qCDebug(KMIX_LOG) << "End fast polling"; _fastPollingEndsAt = QTime(); // NULL time _pollingTimer->setInterval(POLL_RATE_SLOW); } } } /** * Return the MixDevice, that would qualify best as MasterDevice. The default is to return the * first device in the device list. Backends can override this (i.e. the ALSA Backend does so). * The users preference is NOT returned by this method - see the Mixer class for that. */ shared_ptr Mixer_Backend::recommendedMaster() { if ( m_recommendedMaster ) { // Backend has set a recommended master. Thats fine. Using it. return m_recommendedMaster; } else if ( ! m_mixDevices.isEmpty() ) { // Backend has NOT set a recommended master. Evil backend // => lets help out, using the first device (if exists) return m_mixDevices.at(0); } else { if ( !_mixer->isDynamic()) // This should never ever happen, as KMix does NOT accept soundcards without controls qCCritical(KMIX_LOG) << "Mixer_Backend::recommendedMaster(): returning invalid master. This is a bug in KMix. Please file a bug report stating how you produced this."; } // If we reach this code path, then obviously m_recommendedMaster == 0 (see above) return m_recommendedMaster; } /** * Sets the ID of the currently selected Enum entry. * This is a dummy implementation - if the Mixer backend * wants to support it, it must implement the driver specific * code in its subclass (see Mixer_ALSA.cpp for an example). */ void Mixer_Backend::setEnumIdHW(const QString& , unsigned int) { return; } /** * Return the ID of the currently selected Enum entry. * This is a dummy implementation - if the Mixer backend * wants to support it, it must implement the driver specific * code in its subclass (see Mixer_ALSA.cpp for an example). */ unsigned int Mixer_Backend::enumIdHW(const QString& ) { return 0; } + /** * Move the stream to a new destination */ -bool Mixer_Backend::moveStream( const QString& id, const QString& destId ) { - Q_UNUSED(id); +bool Mixer_Backend::moveStream(const QString &id, const QString &destId) +{ + qCDebug(KMIX_LOG) << "called for unsupported" << id; Q_UNUSED(destId); - return false; + return (false); +} + +/** + * Get the current destination device of a stream + */ +QString Mixer_Backend::currentStreamDevice(const QString &id) const +{ + qCDebug(KMIX_LOG) << "called for unsupported" << id; + return (QString()); } QString Mixer_Backend::errorText(int mixer_error) { QString l_s_errmsg; switch (mixer_error) { case Mixer::ERR_PERM: l_s_errmsg = i18n("kmix:You do not have permission to access the mixer device.\n" \ "Please check your operating systems manual to allow the access."); break; case Mixer::ERR_WRITE: l_s_errmsg = i18n("kmix: Could not write to mixer."); break; case Mixer::ERR_READ: l_s_errmsg = i18n("kmix: Could not read from mixer."); break; case Mixer::ERR_OPEN: l_s_errmsg = i18n("kmix: Mixer cannot be found.\n" \ "Please check that the soundcard is installed and that\n" \ "the soundcard driver is loaded.\n"); break; default: l_s_errmsg = i18n("kmix: Unknown error. Please report how you produced this error."); break; } return l_s_errmsg; } diff --git a/backends/mixer_backend.h b/backends/mixer_backend.h index 5a8715ce..894afd43 100644 --- a/backends/mixer_backend.h +++ b/backends/mixer_backend.h @@ -1,244 +1,245 @@ //-*-C++-*- /* * KMix -- KDE's full featured mini mixer * * Copyright 2006-2007 Christian Esken * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MIXER_BACKEND_H #define MIXER_BACKEND_H #include #include #include #include "core/mixdevice.h" #include "core/mixset.h" #include "kmix_debug.h" class Mixer; class Mixer_Backend : public QObject { Q_OBJECT friend class Mixer; // The Mixer Backend's may only be accessed from the Mixer class. protected: Mixer_Backend(Mixer *mixer, int devnum); virtual ~Mixer_Backend(); /** * Derived classes MUST implement this to open the mixer. * * @return a KMix error code (O=OK). */ virtual int open() = 0; /** * Derived classes MUST implement this to close the mixer. Do not call this directly, but use shutdown() instead. * The method cannot be made pure virtual, as we use close() in the destructor, and C++ does not allow this. * http://stackoverflow.com/questions/99552/where-do-pure-virtual-function-call-crashes-come-from?lq=1 * * @return a KMix error code (O=OK). */ virtual int close(); // Not pure virtual. See comment! /** * Shutdown deinitializes this MixerBackend, freeing resources */ void closeCommon(); /** * Returns the driver name, e.g. "ALSA" or "OSS". This virtual method is for looking up the * driver name on instantiated objects. * * Please note, that there is also a static implementation of the driverName * (Because there is no "virtual static" in C++, I need the method twice). * The static implementation is for the Mixer Factory (who needs it *before* instantiating an object). * While it is not a member function, its implementation can still be found in the corresponding * Backend implementation. For example in mixer_oss.cpp there is a global function called OSS_getDriverName(). */ virtual QString getDriverName() = 0; /** * Opens the mixer, if it constitutes a valid Device. You should return "false", when * the Mixer with the devnum given in the constructor is not supported by the Backend. The two * typical cases are: * (1) No such hardware installed * (2) The hardware exists, but has no mixer support (e.g. external soundcard with only mechanical volume knobs) * The implementation calls open(), checks the return code and whether the number of * supported channels is > 0. The device remains opened if it is valid, otherwise a close() is done. */ bool openIfValid(); /** @return true, if the Mixer is open (and thus can be operated) */ bool isOpen(); virtual bool hasChangedControls(); void readSetFromHWforceUpdate() const; /// Volume Read virtual int readVolumeFromHW( const QString& id, shared_ptr ) = 0; /// Volume Write virtual int writeVolumeToHW( const QString& id, shared_ptr ) = 0; /// Enums virtual void setEnumIdHW(const QString& id, unsigned int); virtual unsigned int enumIdHW(const QString& id); - virtual bool moveStream( const QString& id, const QString& destId ); + virtual bool moveStream(const QString &id, const QString &destId); + virtual QString currentStreamDevice(const QString &id) const; // Future directions: Move media*() methods to MediaController class virtual int mediaPlay(QString ) { return 0; }; // implement in the backend if it supports it virtual int mediaPrev(QString ) { return 0; }; // implement in the backend if it supports it virtual int mediaNext(QString ) { return 0;}; // implement in the backend if it supports it /// Overwrite in the backend if the backend can see changes without polling virtual bool needsPolling() { return true; } shared_ptr recommendedMaster(); /** * Return a translated error text for the given error number. * Subclasses can override this method to produce platform * specific error descriptions. */ virtual QString errorText(int mixer_error); /// Returns translated WhatsThis messages for a control.Translates from virtual QString translateKernelToWhatsthis(const QString &kernelName); // Return an Universal Device Identification (suitable for the OS, especially for Hotplug and Unplug events) virtual QString& udi() { return _udi; }; int m_devnum; /** * User friendly name of the Mixer (e.g. "USB 7.1 Surround System"). If your mixer API gives you a usable name, use that name. */ virtual QString getName() const; virtual QString getId() const; virtual int getCardInstance() const { return _cardInstance; } // All controls of this card MixSet m_mixDevices; /****************************************************************************************** * Please don't access the next vars from the Mixer class (even though Mixer is a friend). * There are proper access methods for them. ******************************************************************************************/ bool m_isOpen; // The MixDevice that would qualify best as MasterDevice (according to the taste of the Backend developer) shared_ptr m_recommendedMaster; // The Mixer is stored her only for one reason: The backend creates the MixDevice's, and it has shown // that it is helpful if the MixDevice's know their corresponding Mixer. KMix lived 10 years without that, // but just believe me. It's *really* better, for example, you can put controls of different soundcards in // one View. That is very cool! Also the MDW doesn't need to store the Mixer any longer (MDW is a GUI element, // so that was 'wrong' anyhow Mixer* _mixer; QTimer* _pollingTimer; QString _udi; // Universal Device Identification mutable bool _readSetFromHWforceUpdate; signals: void controlChanged( void ); // TODO remove? public slots: /** * Re-initialize. Currently only implemented by PulseAudio backend, and this slot might get moved there */ virtual void reinit() {}; protected: void freeMixDevices(); QMap s_mixerNums; /** * Registers the card for this Backend and sets the card discriminator for the given card name. * You MUST call this before creating the first MixDevice. Reason is, that each MixDevice instance register a * DBUS name that includes the mixer ID (and this means also the _cardInstance). * * The discriminator should always be 1, unless a second card with * the same name of a registered card was already registered. Default implementation will return 2, 3 and so on * for more cards. Subclasses can override this and return arbitrary ID's, but any ID that is not 1 will be * displayed to the user everywhere where a mixer name is shown, like in the tab name. * * For the background please see BKO-327471 and read the following info: * "Count mixer nums for every mixer name to identify mixers with equal names. * This is for creating persistent (reusable) primary keys, which can safely * be referenced (especially for config file access, so it is meant to be persistent!)." * * * * @param cardBaseName */ void registerCard(QString cardBaseName) { m_mixerName = cardBaseName; int cardDiscriminator = 1 + s_mixerNums[cardBaseName]; qCDebug(KMIX_LOG) << "cardBaseName=" << cardBaseName << ", cardDiscriminator=" << cardDiscriminator; _cardInstance = cardDiscriminator; _cardRegistered = true; } /** * Unregisters the card of this Backend. The cardDiscriminator counter for this card name is reduced by 1. * See #registerCard() for more info. * * TODO This is not entirely correct. Example: If the first card (cardDiscrimiator == 1) is unpluggged, then * s_mixerNums["cardName"] is changed from 2 to 1. The next plug of registerCard("cardName") will use * cardDiscriminator == 2, but the card with that discriminator was not unplugged => BANG!!! * * @param cardBaseName */ void unregisterCard(QString cardBaseName) { QMap::const_iterator it = s_mixerNums.constFind(cardBaseName); if (it != s_mixerNums.constEnd()) { int beforeValue = it.value(); int afterValue = beforeValue-1; if (beforeValue > 0) s_mixerNums[cardBaseName] = afterValue; qCDebug(KMIX_LOG) << "beforeValue=" << beforeValue << ", afterValue" << afterValue; } } int _cardInstance; bool _cardRegistered; protected slots: virtual void readSetFromHW(); private: QTime _fastPollingEndsAt; QString m_mixerName; }; typedef Mixer_Backend *getMixerFunc( Mixer* mixer, int device ); typedef QString getDriverNameFunc( ); struct MixerFactory { getMixerFunc *getMixer; getDriverNameFunc *getDriverName; }; #endif diff --git a/backends/mixer_mpris2.cpp b/backends/mixer_mpris2.cpp index e08109f9..275c2de3 100644 --- a/backends/mixer_mpris2.cpp +++ b/backends/mixer_mpris2.cpp @@ -1,701 +1,695 @@ /** * KMix -- MPRIS2 backend * * Copyright (C) 2011 Christian Esken * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "mixer_mpris2.h" #include "core/mixer.h" #include "core/ControlManager.h" #include "core/GlobalConfig.h" #include "kmix_debug.h" #include #include #include #include #include // Set the QDBUS_DEBUG env variable for debugging Qt DBUS calls. Mixer_Backend* MPRIS2_getMixer(Mixer *mixer, int device ) { return new Mixer_MPRIS2(mixer, device ); } Mixer_MPRIS2::Mixer_MPRIS2(Mixer *mixer, int device) : Mixer_Backend(mixer, device ) { } int Mixer_MPRIS2::open() { if ( m_devnum != 0 ) return Mixer::ERR_OPEN; registerCard(i18n("Playback Streams")); _id = "Playback Streams"; _mixer->setDynamic(); return addAllRunningPlayersAndInitHotplug(); } int Mixer_MPRIS2::close() { m_isOpen = false; closeCommon(); qDeleteAll(controls); controls.clear(); return 0; } int Mixer_MPRIS2::mediaPlay(QString id) { return mediaControl(id, "PlayPause"); } int Mixer_MPRIS2::mediaPrev(QString id) { return mediaControl(id, "Previous"); } int Mixer_MPRIS2::mediaNext(QString id) { return mediaControl(id, "Next"); } /** * Sends a media control command to the given application. * @param applicationId The MPRIS applicationId * @returns Always 0. Hint: Currently nobody uses the return code */ int Mixer_MPRIS2::mediaControl(QString applicationId, QString commandName) { MPrisControl* mad = controls.value(applicationId); if ( mad == 0 ) return 0; // Might have disconnected recently => simply ignore command qCDebug(KMIX_LOG) << "Send " << commandName << " to id=" << applicationId; QDBusPendingReply<> repl2 = mad->playerIfc->asyncCall(commandName); QDBusPendingCallWatcher* watchMediaControlReply = new QDBusPendingCallWatcher(repl2, mad); connect(watchMediaControlReply, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(watcherMediaControl(QDBusPendingCallWatcher*))); return 0; // Presume everything went well. Can't do more for ASYNC calls } void Mixer_MPRIS2::watcherMediaControl(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) { return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) } // Actually the code below in this method is more or less just debugging const QDBusMessage& msg = watcher->reply(); QString id = mprisCtl->getId(); QString busDestination = mprisCtl->getBusDestination(); qCDebug(KMIX_LOG) << "Media control for id=" << id << ", path=" << msg.path() << ", interface=" << msg.interface() << ", busDestination" << busDestination; } /** * readVolumeFromHW() should be used only for hotplug (and even that should go away). Everything should operate via * the slot volumeChanged in the future. */ int Mixer_MPRIS2::readVolumeFromHW( const QString& /*id*/, shared_ptr /*md*/) { // Everything is done by notifications => no code necessary return Mixer::OK_UNCHANGED; } /** * A slot that processes data from the MPrisControl that emit the signal. * * @param The emitting MPrisControl * @param newVolume The new volume */ void Mixer_MPRIS2::playbackStateChanged(MPrisControl* mad, MediaController::PlayState playState) { shared_ptr md = m_mixDevices.get(mad->getId()); md->mediaController()->setPlayState(playState); QMetaObject::invokeMethod(this, "announceGUI", Qt::QueuedConnection); // ControlManager::instance().announce(_mixer->id(), ControlManager::GUI, QString("MixerMPRIS2.playbackStateChanged")); } /** * A slot that processes data from the MPrisControl that emit the signal. * * @param The emitting MPrisControl * @param newVolume The new volume */ void Mixer_MPRIS2::volumeChanged(MPrisControl* mad, double newVolume) { shared_ptr md = m_mixDevices.get(mad->getId()); int volInt = newVolume *100; if (GlobalConfig::instance().data.debugVolume) qCDebug(KMIX_LOG) << "changed" << volInt; volumeChangedInternal(md, volInt); } void Mixer_MPRIS2::volumeChangedInternal(shared_ptr md, int volumePercentage) { if ( md->isVirtuallyMuted() && volumePercentage == 0) { // Special code path for virtual mute switches. Don't write back the volume if it is muted in the KMix GUI return; } Volume& vol = md->playbackVolume(); vol.setVolume( Volume::LEFT, volumePercentage); md->setMuted(volumePercentage == 0); QMetaObject::invokeMethod(this, "announceVolume", Qt::QueuedConnection); // ControlManager::instance().announce(_mixer->id(), ControlManager::Volume, QString("MixerMPRIS2.volumeChanged")); } // The following is an example message for an incoming volume change: /* signal sender=:1.125 -> dest=(null destination) serial=503 path=/org/mpris/MediaPlayer2; interface=org.freedesktop.DBus.Properties; member=PropertiesChanged string "org.mpris.MediaPlayer2.Player" array [ dict entry( string "Volume" variant double 0.81 ) ] array [ ] */ /** * @overload * * @param id * @param md * @return */ int Mixer_MPRIS2::writeVolumeToHW( const QString& id, shared_ptr md ) { Volume& vol = md->playbackVolume(); double volFloat = 0; if ( ! md->isMuted() ) { int volInt = vol.getVolume(Volume::LEFT); volFloat = volInt/100.0; } QList arg; arg.append(QString("org.mpris.MediaPlayer2.Player")); arg.append(QString("Volume")); arg << QVariant::fromValue(QDBusVariant(volFloat)); MPrisControl* mad = controls.value(id); if ( !mad ) { qCDebug(KMIX_LOG) << "id does not exist:" << id; return 0; } QVariant v1 = QVariant(QString("org.mpris.MediaPlayer2.Player")); QVariant v2 = QVariant(QString("Volume")); QVariant v3 = QVariant::fromValue(QDBusVariant(volFloat)); // QVariant v3 = QVariant(volFloat); // I don't care too much for the reply, as I won't receive a result. Thus fire-and-forget here. mad->propertyIfc->asyncCall("Set", v1, v2, v3); return 0; } void Mixer_MPRIS2::setEnumIdHW(const QString&, unsigned int) { // no enums in MPRIS } unsigned int Mixer_MPRIS2::enumIdHW(const QString&) { // no enums in MPRIS return 0; } -bool Mixer_MPRIS2::moveStream( const QString&, const QString& ) -{ - // not supported in MPRIS - return false; -} - /** * Adds all currently running players and then starts listening * for changes (new players, and disappearing players).
* * @return int **/ int Mixer_MPRIS2::addAllRunningPlayersAndInitHotplug() { QDBusConnection dbusConn = QDBusConnection::sessionBus(); if (! dbusConn.isConnected() ) { qCCritical(KMIX_LOG) << "Cannot connect to the D-Bus session bus.\n" << "To start it, run:\n" <<"\teval `dbus-launch --auto-syntax`\n"; return Mixer::ERR_OPEN; } // Start listening for new Mediaplayers bool connected = dbusConn.connect("", QString("/org/freedesktop/DBus"), "org.freedesktop.DBus", "NameOwnerChanged", this, SLOT(newMediaPlayer(QString,QString,QString)) ); if (!connected) { qCWarning(KMIX_LOG) << "MPRIS2 hotplug init failure. New Media Players will not be detected."; } /* Here is a small concurrency issue. * If new players appear between registeredServiceNames() below and the connect() above these players *might* show up doubled in KMix. * There is no simple solution (reversing could have the problem of not-adding), so we live for now with it. */ /* * Bug 311189: Introspecting via "dbusConn.interface()->registeredServiceNames()" does not work too well. * Comment: I am not so sure that registeredServiceNames() is really an issue. It is more likely * in a later step, when talking to the probed apps. Still, I now do a hand crafted 3-line version of * registeredServiceNames() via "ListNames", so I can later more easily change to async. */ QDBusInterface dbusIfc("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", dbusConn); QDBusPendingReply repl = dbusIfc.asyncCall("ListNames"); repl.waitForFinished(); if (! repl.isValid() ) { qCCritical(KMIX_LOG) << "Invalid reply while listing Media Players. MPRIS2 players will not be available." << repl.error(); return 1; } QString busDestination; foreach ( busDestination , repl.value() ) { if ( busDestination.startsWith(QLatin1String("org.mpris.MediaPlayer2")) ) { addMprisControlAsync(busDestination); qCDebug(KMIX_LOG) << "MPRIS2: Attached media player on busDestination=" << busDestination; } } return 0; } QString Mixer_MPRIS2::busDestinationToControlId(const QString& busDestination) { const QString prefix = "org.mpris.MediaPlayer2."; if (! busDestination.startsWith(prefix)) { qCWarning(KMIX_LOG) << "Ignoring unsupported control, busDestination=" << busDestination; return QString(); } return busDestination.mid(prefix.length()); } /** * Asynchronously add the MPRIS control designated by the DBUS busDestination. * to the internal apps list. * * @param conn An open connection to the DBUS Session Bus * @param busDestination The DBUS busDestination, e.g. "org.mpris.MediaPlayer2.amarok" */ void Mixer_MPRIS2::addMprisControlAsync(QString busDestination) { // -1- Create a MPrisControl. Its fields will be filled partially here, partially via ASYNC DUBUS replies QString id = busDestinationToControlId(busDestination); qCDebug(KMIX_LOG) << "Get control of busDestination=" << busDestination << "id=" << id; QDBusConnection conn = QDBusConnection::sessionBus(); QDBusInterface *qdbiProps = new QDBusInterface(QString(busDestination), QString("/org/mpris/MediaPlayer2"), "org.freedesktop.DBus.Properties", conn, this); QDBusInterface *qdbiPlayer = new QDBusInterface(QString(busDestination), QString("/org/mpris/MediaPlayer2"), "org.mpris.MediaPlayer2.Player", conn, this); // -2- Add the control to our official control list MPrisControl* mad = new MPrisControl(id, busDestination); mad->propertyIfc = qdbiProps; mad->playerIfc = qdbiPlayer; controls.insert(id, mad); /* * WTF: - asyncCall("Get", arg) : returns an error message (see below) * - asyncCallWithArgumentList("Get", arg) : returns an error message (see below) * - callWithArgumentList(QDBus::Block, "Get", arg) : works * - syncCall("Get", v1, v2) : works * * kmix(13543) Mixer_MPRIS2::addMPrisControl: (marok), msg2= QDBusMessage(type=Error, service=":1.44", error name="org.freedesktop.DBus.Error.UnknownMethod", error message="No such method 'Get' in interface 'org.freedesktop.DBus.Properties' at object path '/org/mpris/MediaPlayer2' (signature 'av')", signature="s", contents=("No such method 'Get' in interface 'org.freedesktop.DBus.Properties' at object path '/org/mpris/MediaPlayer2' (signature 'av')") ) , isValid= false , isFinished= true , isError= true * * This behavior is total counter-intuitive :-((( */ // Create ASYNC DBUS queries for the new control. This effectively starts a chain of async DBUS commands. QVariant v1 = QVariant(QString("org.mpris.MediaPlayer2")); QVariant v2 = QVariant(QString("Identity")); QDBusPendingReply repl2 = mad->propertyIfc->asyncCall("Get", v1, v2); QDBusPendingCallWatcher* watchIdentity = new QDBusPendingCallWatcher(repl2, mad); connect(watchIdentity, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(watcherPlugControlId(QDBusPendingCallWatcher*))); } MixDevice::ChannelType Mixer_MPRIS2::getChannelTypeFromPlayerId(const QString& id) { // TODO This hardcoded application list is a quick hack. It should be generalized. MixDevice::ChannelType ct = MixDevice::APPLICATION_STREAM; if (id.startsWith(QLatin1String("amarok"))) { ct = MixDevice::APPLICATION_AMAROK; } else if (id.startsWith(QLatin1String("banshee"))) { ct = MixDevice::APPLICATION_BANSHEE; } else if (id.startsWith(QLatin1String("vlc"))) { ct = MixDevice::APPLICATION_VLC; } else if (id.startsWith(QLatin1String("xmms"))) { ct = MixDevice::APPLICATION_XMM2; } else if (id.startsWith(QLatin1String("tomahawk"))) { ct = MixDevice::APPLICATION_TOMAHAWK; } else if (id.startsWith(QLatin1String("clementine"))) { ct = MixDevice::APPLICATION_CLEMENTINE; } return ct; } void Mixer_MPRIS2::watcherInitialVolume(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) const QDBusMessage& msg = watcher->reply(); QList repl = msg.arguments(); if ( ! repl.isEmpty() ) { QDBusVariant dbusVariant = qvariant_cast(repl.at(0)); QVariant result2 = dbusVariant.variant(); double volume = result2.toDouble(); volumeChanged(mprisCtl, volume); } watcher->deleteLater(); } void Mixer_MPRIS2::watcherInitialPlayState(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) const QDBusMessage& msg = watcher->reply(); QList repl = msg.arguments(); if ( ! repl.isEmpty() ) { QDBusVariant dbusVariant = qvariant_cast(repl.at(0)); QVariant result2 = dbusVariant.variant(); QString playbackStateString = result2.toString(); MediaController::PlayState playState = Mixer_MPRIS2::mprisPlayStateString2PlayState(playbackStateString); playbackStateChanged(mprisCtl, playState); } watcher->deleteLater(); } /** * Convenience method for the watcher*() methods. * Returns the MPrisControl that is parent of the given watcher, if the reply is valid. In this case you can * use the result and call watcher->deleteLater() after processing the result. * * Otherwise 0 is returned, and watcher->deleteLater() is called. Important You must call watcher->deleteLater() * yourself for the other (normal/good) case. * * @param watcher * @return */ MPrisControl* Mixer_MPRIS2::watcherHelperGetMPrisControl(QDBusPendingCallWatcher* watcher) { const QDBusMessage& msg = watcher->reply(); if ( msg.type() == QDBusMessage::ReplyMessage ) { QObject* obj = watcher->parent(); MPrisControl* mad = qobject_cast(obj); if (mad != 0) { return mad; } qCWarning(KMIX_LOG) << "Ignoring unexpected Control Id. object=" << obj; } else if ( msg.type() == QDBusMessage::ErrorMessage ) { qCCritical(KMIX_LOG) << "ERROR in Media control operation, path=" << msg.path() << ", msg=" << msg; } watcher->deleteLater(); return 0; } void Mixer_MPRIS2::watcherPlugControlId(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) { return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) } const QDBusMessage& msg = watcher->reply(); QString id = mprisCtl->getId(); QString busDestination = mprisCtl->getBusDestination(); QString readableName = id; // Start with ID, but replace with reply (if exists) qCDebug(KMIX_LOG) << "Plugging id=" << id << ", busDestination" << busDestination << ", name= " << readableName; QList repl = msg.arguments(); if ( ! repl.isEmpty() ) { // We have to do some very ugly casting from QVariant to QDBusVariant to QVariant. This API totally sucks. QDBusVariant dbusVariant = qvariant_cast(repl.at(0)); QVariant result2 = dbusVariant.variant(); readableName = result2.toString(); // qCDebug(KMIX_LOG) << "REPLY " << result2.type() << ": " << readableName; MixDevice::ChannelType ct = getChannelTypeFromPlayerId(id); MixDevice* mdNew = new MixDevice(_mixer, id, readableName, ct); // MPRIS2 doesn't support an actual mute switch. Mute is defined as volume = 0.0 // Thus we won't add the playback switch Volume* vol = new Volume( 100, 0, false, false); vol->addVolumeChannel(VolumeChannel(Volume::LEFT)); // MPRIS is only one control ("Mono") MediaController* mediaContoller = mdNew->mediaController(); mediaContoller->addMediaPlayControl(); mediaContoller->addMediaNextControl(); mediaContoller->addMediaPrevControl(); mdNew->setApplicationStream(true); mdNew->addPlaybackVolume(*vol); m_mixDevices.append( mdNew->addToPool() ); delete vol; // vol is only temporary. mdNew has its own volume object. => delete QDBusConnection sessionBus = QDBusConnection::sessionBus(); sessionBus.connect(busDestination, QString("/org/mpris/MediaPlayer2"), "org.freedesktop.DBus.Properties", "PropertiesChanged", mprisCtl, SLOT(onPropertyChange(QString,QVariantMap,QStringList)) ); connect(mprisCtl, SIGNAL(volumeChanged(MPrisControl*,double)), this, SLOT(volumeChanged(MPrisControl*,double)) ); connect(mprisCtl, SIGNAL(playbackStateChanged(MPrisControl*,MediaController::PlayState)), SLOT (playbackStateChanged(MPrisControl*,MediaController::PlayState)) ); sessionBus.connect(busDestination, QString("/Player"), "org.freedesktop.MediaPlayer", "TrackChange", mprisCtl, SLOT(trackChangedIncoming(QVariantMap)) ); // The following line is evil: mad->playerIfc->property("Volume") is in fact a synchronous call, and // sync calls are strictly forbidden, see bug 317926 //volumeChanged(mad, mad->playerIfc->property("Volume").toDouble()); // --- Query initial state -------------------------------------------------------------------------------- QVariant v1 = QVariant(QString("org.mpris.MediaPlayer2.Player")); QVariant v2 = QVariant(QString("Volume")); QDBusPendingReply repl2 = mprisCtl->propertyIfc->asyncCall("Get", v1, v2); QDBusPendingCallWatcher* watcherOutgoing = new QDBusPendingCallWatcher(repl2, mprisCtl); connect(watcherOutgoing, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(watcherInitialVolume(QDBusPendingCallWatcher*))); v2 = QVariant(QString("PlaybackStatus")); repl2 = mprisCtl->propertyIfc->asyncCall("Get", v1, v2); watcherOutgoing = new QDBusPendingCallWatcher(repl2, mprisCtl); connect(watcherOutgoing, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(watcherInitialPlayState(QDBusPendingCallWatcher*))); // Push notifyToReconfigureControls to stack, so it will not be executed synchronously announceControlListAsync(id); } watcher->deleteLater(); } // ----------------------------------------------------------------------------------------------------------- // ASYNC announce slots, including convenience wrappers // ----------------------------------------------------------------------------------------------------------- /** * Convenience wrapper to do the ASYNC call to #announceControlList() * @param */ void Mixer_MPRIS2::announceControlListAsync(QString /*streamId*/) { // currently we do not use the streamId QMetaObject::invokeMethod(this, "announceControlList", Qt::QueuedConnection); } void Mixer_MPRIS2::announceControlList() { ControlManager::instance().announce(_mixer->id(), ControlManager::ControlList, getDriverName()); } void Mixer_MPRIS2::announceGUI() { ControlManager::instance().announce(_mixer->id(), ControlManager::GUI, getDriverName()); } void Mixer_MPRIS2::announceVolume() { ControlManager::instance().announce(_mixer->id(), ControlManager::Volume, getDriverName()); } // ----------------------------------------------------------------------------------------------------------- /** * Handles the hotplug of new MPRIS2 enabled Media Players */ void Mixer_MPRIS2::newMediaPlayer(QString name, QString oldOwner, QString newOwner) { if ( name.startsWith(QLatin1String("org.mpris.MediaPlayer2")) ) { if ( oldOwner.isEmpty() && !newOwner.isEmpty()) { qCDebug(KMIX_LOG) << "Mediaplayer registers: " << name; addMprisControlAsync(name); } else if ( !oldOwner.isEmpty() && newOwner.isEmpty()) { QString id = busDestinationToControlId(name); qCDebug(KMIX_LOG) << "Mediaplayer unregisters: " << name << " , id=" << id; // -1- Remove Mediaplayer connection if (controls.contains(id)) { const MPrisControl *control = controls.value(id); QObject::disconnect(control,0,0,0); controls.remove(id); } // -2- Remove MixDevice from internal list shared_ptr md = m_mixDevices.get(id); if (md) { // We know about the player that is unregistering => remove internally md->close(); m_mixDevices.removeById(id); announceControlListAsync(id); qCDebug(KMIX_LOG) << "MixDevice 4 useCount=" << md.use_count(); } } else { qCWarning(KMIX_LOG) << "Mediaplayer has registered under a new name. This is currently not supported by KMix"; } } } /** * This slot is a simple proxy that enriches the DBUS signal with our data, which especially contains the id of the MixDevice. */ void MPrisControl::trackChangedIncoming(QVariantMap /*msg*/) { qCDebug(KMIX_LOG) << "Track changed"; } MediaController::PlayState Mixer_MPRIS2::mprisPlayStateString2PlayState(const QString& playbackStatus) { MediaController::PlayState playState = MediaController::PlayStopped; // presume Stopped for unknown state if (playbackStatus == "Playing") { playState = MediaController::PlayPlaying; } else if (playbackStatus == "Stopped") { playState = MediaController::PlayStopped; } else if (playbackStatus == "Paused") { playState = MediaController::PlayPaused; } return playState; } /** * This slot is a simple proxy that enriches the DBUS signal with our data, which especially contains the id of the MixDevice. */ void MPrisControl::onPropertyChange(QString /*ifc*/,QVariantMap msg ,QStringList /*sl*/) { QMap::iterator v = msg.find("Volume"); if (v != msg.end() ) { double volDouble = v.value().toDouble(); qCDebug(KMIX_LOG) << "volumeChanged incoming: vol=" << volDouble; emit volumeChanged( this, volDouble); } v = msg.find("PlaybackStatus"); if (v != msg.end() ) { QString playbackStatus = v.value().toString(); MediaController::PlayState playState = Mixer_MPRIS2::mprisPlayStateString2PlayState(playbackStatus); qCDebug(KMIX_LOG) << "PlaybackStatus is now " << playbackStatus; emit playbackStateChanged(this, playState); } } Mixer_MPRIS2::~Mixer_MPRIS2() { close(); } MPrisControl::MPrisControl(QString id, QString busDestination) : propertyIfc(0) , playerIfc(0) { volume = 0; this->id = id; this->busDestination = busDestination; retrievedElems = MPrisControl::NONE; } MPrisControl::~MPrisControl() { delete propertyIfc; delete playerIfc; } QString Mixer_MPRIS2::getDriverName() { return "MPRIS2"; } QString MPRIS2_getDriverName() { return "MPRIS2"; } diff --git a/backends/mixer_mpris2.h b/backends/mixer_mpris2.h index 93c96dda..80eaa3ca 100644 --- a/backends/mixer_mpris2.h +++ b/backends/mixer_mpris2.h @@ -1,175 +1,174 @@ /** * KMix -- MPRIS2 backend * * Copyright (C) 2011 Christian Esken * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef Mixer_MPRIS2_H #define Mixer_MPRIS2_H #include #include #include #include #include #include #include #include "mixer_backend.h" class MPrisControl : public QObject { Q_OBJECT public: MPrisControl(QString id, QString busDestination); ~MPrisControl(); enum DATA_ELEM { NONE = 0, NAME = 1, VOLUME = 2, ALL = 3 }; QDBusInterface* propertyIfc; QDBusInterface* playerIfc; private: QString id; QString busDestination; QString name; int volume; int retrievedElems; public: const QString& getId() const { return id; } const QString& getBusDestination() const { return busDestination; } const QString& getName() const { return name; } void setName(const QString& name) { retrievedElems |= MPrisControl::NAME; this->name = name; } int getVolume() const { return volume; } void setVolume(int volume) { retrievedElems |= MPrisControl::VOLUME; this->volume = volume; } bool isComplete() { return retrievedElems == MPrisControl::ALL; } public slots: void trackChangedIncoming(QVariantMap msg); void onPropertyChange(QString,QVariantMap,QStringList); signals: void volumeChanged(MPrisControl* mad, double); void playbackStateChanged(MPrisControl* mad, MediaController::PlayState); }; class Mixer_MPRIS2 : public Mixer_Backend { Q_OBJECT // friend class Mixer_MPRIS2_Thread; public: Mixer_MPRIS2(Mixer *mixer, int device); virtual ~Mixer_MPRIS2(); QString getDriverName() Q_DECL_OVERRIDE; QString getId() const Q_DECL_OVERRIDE { return _id; }; int open() Q_DECL_OVERRIDE; int close() Q_DECL_OVERRIDE; int readVolumeFromHW( const QString& id, shared_ptr ) Q_DECL_OVERRIDE; int writeVolumeToHW( const QString& id, shared_ptr ) Q_DECL_OVERRIDE; void setEnumIdHW(const QString& id, unsigned int) Q_DECL_OVERRIDE; unsigned int enumIdHW(const QString& id) Q_DECL_OVERRIDE; - bool moveStream( const QString& id, const QString& destId ) Q_DECL_OVERRIDE; bool needsPolling() Q_DECL_OVERRIDE { return false; } int mediaPlay(QString id) Q_DECL_OVERRIDE; int mediaPrev(QString id) Q_DECL_OVERRIDE; int mediaNext(QString id) Q_DECL_OVERRIDE; virtual int mediaControl(QString id, QString command); static MediaController::PlayState mprisPlayStateString2PlayState(const QString& playbackStatus); public slots: void volumeChanged(MPrisControl *mad, double); void playbackStateChanged(MPrisControl* mad, MediaController::PlayState); void newMediaPlayer(QString name, QString oldOwner, QString newOwner); void addMprisControlAsync(QString arg1); void announceControlListAsync(QString streamId); private slots: // asynchronous announce call slots void announceControlList(); void announceGUI(); void announceVolume(); // Async QDBusPendingCallWatcher's void watcherMediaControl(QDBusPendingCallWatcher* watcher); void watcherPlugControlId(QDBusPendingCallWatcher* watcher); void watcherInitialVolume(QDBusPendingCallWatcher* watcher); void watcherInitialPlayState(QDBusPendingCallWatcher* watcher); private: // Helpers for the watchers MPrisControl* watcherHelperGetMPrisControl(QDBusPendingCallWatcher* watcher); private: // void asyncAddMprisControl(QString busDestination); // void messageQueueThreadLoop(); int addAllRunningPlayersAndInitHotplug(); void volumeChangedInternal(shared_ptr md, int volumePercentage); QString busDestinationToControlId(const QString& busDestination); MixDevice::ChannelType getChannelTypeFromPlayerId(const QString& id); QMap controls; QString _id; }; #endif diff --git a/backends/mixer_pulse.cpp b/backends/mixer_pulse.cpp index 47f8eaef..dfe303ff 100644 --- a/backends/mixer_pulse.cpp +++ b/backends/mixer_pulse.cpp @@ -1,1418 +1,1444 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 2008 Helio Chissini de Castro * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mixer_pulse.h" #include #include #include #include #include #include "core/mixer.h" #include "core/ControlManager.h" #include "core/GlobalConfig.h" #include #include #if defined(HAVE_CANBERRA) # include #endif // PA_VOLUME_UI_MAX landed in pulseaudio-0.9.23, so this can be removed when/if // minimum requirement is ever bumped up (from 0.9.12 currently) #ifndef PA_VOLUME_UI_MAX #define PA_VOLUME_UI_MAX (pa_sw_volume_from_dB(+11.0)) #endif #define HAVE_SOURCE_OUTPUT_VOLUMES PA_CHECK_VERSION(1,0,0) #define KMIXPA_PLAYBACK 0 #define KMIXPA_CAPTURE 1 #define KMIXPA_APP_PLAYBACK 2 #define KMIXPA_APP_CAPTURE 3 #define KMIXPA_WIDGET_MAX KMIXPA_APP_CAPTURE #define KMIXPA_EVENT_KEY "sink-input-by-media-role:event" static unsigned int refcount = 0; static pa_glib_mainloop *s_mainloop = NULL; static pa_context *s_context = NULL; static enum { UNKNOWN, ACTIVE, INACTIVE } s_pulseActive = UNKNOWN; static int s_outstandingRequests = 0; #if defined(HAVE_CANBERRA) static ca_context *s_ccontext = NULL; #endif QMap s_mixers; static devmap outputDevices; static devmap captureDevices; static QMap clients; static devmap outputStreams; static devmap captureStreams; static devmap outputRoles; typedef struct { pa_channel_map channel_map; pa_cvolume volume; bool mute; QString device; } restoreRule; static QMap s_RestoreRules; static void dec_outstanding(pa_context *c) { if (s_outstandingRequests <= 0) return; if (--s_outstandingRequests == 0) { s_pulseActive = ACTIVE; // If this is our probe phase, exit our context immediately if (s_context != c) { pa_context_disconnect(c); } else qCDebug(KMIX_LOG) << "Reconnected to PulseAudio"; } } static void translateMasksAndMaps(devinfo& dev) { dev.chanMask = Volume::MNONE; dev.chanIDs.clear(); if (dev.channel_map.channels != dev.volume.channels) { qCCritical(KMIX_LOG) << "Hideous Channel mixup map says " << dev.channel_map.channels << ", volume says: " << dev.volume.channels; return; } if (1 == dev.channel_map.channels && PA_CHANNEL_POSITION_MONO == dev.channel_map.map[0]) { // We just use the left channel to represent this. dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MLEFT); dev.chanIDs[0] = Volume::LEFT; } else { for (uint8_t i = 0; i < dev.channel_map.channels; ++i) { switch (dev.channel_map.map[i]) { case PA_CHANNEL_POSITION_MONO: qCWarning(KMIX_LOG) << "Channel Map contains a MONO element but has >1 channel - we can't handle this."; return; case PA_CHANNEL_POSITION_FRONT_LEFT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MLEFT); dev.chanIDs[i] = Volume::LEFT; break; case PA_CHANNEL_POSITION_FRONT_RIGHT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MRIGHT); dev.chanIDs[i] = Volume::RIGHT; break; case PA_CHANNEL_POSITION_FRONT_CENTER: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MCENTER); dev.chanIDs[i] = Volume::CENTER; break; case PA_CHANNEL_POSITION_REAR_CENTER: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARCENTER); dev.chanIDs[i] = Volume::REARCENTER; break; case PA_CHANNEL_POSITION_REAR_LEFT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MSURROUNDLEFT); dev.chanIDs[i] = Volume::SURROUNDLEFT; break; case PA_CHANNEL_POSITION_REAR_RIGHT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MSURROUNDRIGHT); dev.chanIDs[i] = Volume::SURROUNDRIGHT; break; case PA_CHANNEL_POSITION_LFE: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MWOOFER); dev.chanIDs[i] = Volume::WOOFER; break; case PA_CHANNEL_POSITION_SIDE_LEFT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARSIDELEFT); dev.chanIDs[i] = Volume::REARSIDELEFT; break; case PA_CHANNEL_POSITION_SIDE_RIGHT: dev.chanMask = (Volume::ChannelMask)( dev.chanMask | Volume::MREARSIDERIGHT); dev.chanIDs[i] = Volume::REARSIDERIGHT; break; default: qCWarning(KMIX_LOG) << "Channel Map contains a pa_channel_position we cannot handle " << dev.channel_map.map[i]; break; } } } } static QString getIconNameFromProplist(pa_proplist *l) { const char *t; if ((t = pa_proplist_gets(l, PA_PROP_MEDIA_ICON_NAME))) return QString::fromUtf8(t); if ((t = pa_proplist_gets(l, PA_PROP_WINDOW_ICON_NAME))) return QString::fromUtf8(t); if ((t = pa_proplist_gets(l, PA_PROP_APPLICATION_ICON_NAME))) return QString::fromUtf8(t); if ((t = pa_proplist_gets(l, PA_PROP_MEDIA_ROLE))) { if (strcmp(t, "video") == 0 || strcmp(t, "phone") == 0) return QString::fromUtf8(t); if (strcmp(t, "music") == 0) return "audio"; if (strcmp(t, "game") == 0) return "applications-games"; if (strcmp(t, "event") == 0) return "dialog-information"; } return ""; } static void sink_cb(pa_context *c, const pa_sink_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Sink callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_PLAYBACK)) s_mixers[KMIXPA_PLAYBACK]->triggerUpdate(); return; } devinfo s; s.index = s.device_index = i->index; s.name = QString::fromUtf8(i->name).replace(' ', '_'); s.description = QString::fromUtf8(i->description); s.icon_name = QString::fromUtf8(pa_proplist_gets(i->proplist, PA_PROP_DEVICE_ICON_NAME)); s.volume = i->volume; s.channel_map = i->channel_map; s.mute = !!i->mute; s.stream_restore_rule = ""; s.priority = 0; if (i->active_port != NULL) s.priority = i->active_port->priority; translateMasksAndMaps(s); bool is_new = !outputDevices.contains(s.index); outputDevices[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about sink: " << s.description; if (s_mixers.contains(KMIXPA_PLAYBACK)) { if (is_new) s_mixers[KMIXPA_PLAYBACK]->addWidget(s.index); else { int mid = s_mixers[KMIXPA_PLAYBACK]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_PLAYBACK]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static void source_cb(pa_context *c, const pa_source_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Source callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_CAPTURE)) s_mixers[KMIXPA_CAPTURE]->triggerUpdate(); return; } // Do something.... if (PA_INVALID_INDEX != i->monitor_of_sink) { // qCDebug(KMIX_LOG) << "Ignoring Monitor Source: " << i->description; return; } devinfo s; s.index = s.device_index = i->index; s.name = QString::fromUtf8(i->name).replace(' ', '_'); s.description = QString::fromUtf8(i->description); s.icon_name = QString::fromUtf8(pa_proplist_gets(i->proplist, PA_PROP_DEVICE_ICON_NAME)); s.volume = i->volume; s.channel_map = i->channel_map; s.mute = !!i->mute; s.stream_restore_rule = ""; translateMasksAndMaps(s); bool is_new = !captureDevices.contains(s.index); captureDevices[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about source: " << s.description; if (s_mixers.contains(KMIXPA_CAPTURE)) { if (is_new) s_mixers[KMIXPA_CAPTURE]->addWidget(s.index); else { int mid = s_mixers[KMIXPA_CAPTURE]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_CAPTURE]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static void client_cb(pa_context *c, const pa_client_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Client callback failure"; return; } if (eol > 0) { dec_outstanding(c); return; } clients[i->index] = QString::fromUtf8(i->name); //qCDebug(KMIX_LOG) << "Got some info about client: " << clients[i->index]; } static void sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Sink Input callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) s_mixers[KMIXPA_APP_PLAYBACK]->triggerUpdate(); return; } const char *t; if ((t = pa_proplist_gets(i->proplist, "module-stream-restore.id"))) { if (strcmp(t, KMIXPA_EVENT_KEY) == 0) { //qCDebug(KMIX_LOG) << "Ignoring sink-input due to it being designated as an event and thus handled by the Event slider"; return; } } QString appname = i18n("Unknown Application"); if (clients.contains(i->client)) appname = clients.value(i->client); devinfo s; s.index = i->index; s.device_index = i->sink; s.description = appname % QLatin1String(": ") % QString::fromUtf8(i->name); s.name = QString("stream:") + QString::number(i->index); //appname.replace(' ', '_').toLower(); s.icon_name = getIconNameFromProplist(i->proplist); s.channel_map = i->channel_map; s.volume = i->volume; s.mute = !!i->mute; s.stream_restore_rule = QString::fromUtf8(t); translateMasksAndMaps(s); bool is_new = !outputStreams.contains(s.index); outputStreams[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about sink input (playback stream): " << s.description; if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) { if (is_new) s_mixers[KMIXPA_APP_PLAYBACK]->addWidget(s.index, true); else { int mid = s_mixers[KMIXPA_APP_PLAYBACK]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_APP_PLAYBACK]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static void source_output_cb(pa_context *c, const pa_source_output_info *i, int eol, void *) { if (eol < 0) { if (pa_context_errno(c) == PA_ERR_NOENTITY) return; qCWarning(KMIX_LOG) << "Source Output callback failure"; return; } if (eol > 0) { dec_outstanding(c); if (s_mixers.contains(KMIXPA_APP_CAPTURE)) s_mixers[KMIXPA_APP_CAPTURE]->triggerUpdate(); return; } /* NB Until Source Outputs support volumes, we just use the volume of the source itself */ if (!captureDevices.contains(i->source)) { qCDebug(KMIX_LOG) << "Source Output refers to a Source we don't have any info for (probably just a peak meter or similar)"; return; } QString appname = i18n("Unknown Application"); if (clients.contains(i->client)) appname = clients.value(i->client); devinfo s; s.index = i->index; s.device_index = i->source; s.description = appname % QLatin1String(": ") % QString::fromUtf8(i->name); s.name = QString("stream:") + QString::number(i->index); //appname.replace(' ', '_').toLower(); s.icon_name = getIconNameFromProplist(i->proplist); s.channel_map = i->channel_map; #if HAVE_SOURCE_OUTPUT_VOLUMES s.volume = i->volume; s.mute = !!i->mute; #else s.volume = captureDevices[i->source].volume; s.mute = captureDevices[i->source].mute; #endif s.stream_restore_rule = QString::fromUtf8(pa_proplist_gets(i->proplist, "module-stream-restore.id")); translateMasksAndMaps(s); bool is_new = !captureStreams.contains(s.index); captureStreams[s.index] = s; // qCDebug(KMIX_LOG) << "Got some info about source output (capture stream): " << s.description; if (s_mixers.contains(KMIXPA_APP_CAPTURE)) { if (is_new) s_mixers[KMIXPA_APP_CAPTURE]->addWidget(s.index, true); else { int mid = s_mixers[KMIXPA_APP_CAPTURE]->id2num(s.name); if (mid >= 0) { MixSet *ms = s_mixers[KMIXPA_APP_CAPTURE]->getMixSet(); (*ms)[mid]->setReadableName(s.description); } } } } static devinfo create_role_devinfo(QString name) { Q_ASSERT(s_RestoreRules.contains(name)); devinfo s; s.index = s.device_index = PA_INVALID_INDEX; s.description = i18n("Event Sounds"); s.name = QString("restore:") + name; s.icon_name = "dialog-information"; s.channel_map = s_RestoreRules[name].channel_map; s.volume = s_RestoreRules[name].volume; s.mute = s_RestoreRules[name].mute; s.stream_restore_rule = name; translateMasksAndMaps(s); return s; } /** * Helper for performing an operation on the device specified by @p id. * Search for the device by name in the specified @p devices map, perform * the function @c func on it, and return the result of that. If the * device is not found then do nothing. */ static int doForDevice(const QString &id, const devmap *devices, shared_ptr md, int (*func)(const devinfo &, shared_ptr)) { for (devmap::const_iterator iter = devices->constBegin(); iter!=devices->constEnd(); ++iter) { if (iter->name==id) return ((*func)(*iter, md)); } qCDebug(KMIX_LOG) << "Device" << id << "not in map"; return (Mixer::OK); } /** * Helper for checking the result of a PulseAudio operation. * If the operation @p op failed, log a message identifying the * call @p explain, then return @c false. Return @c true if the * operation succeeeded, after dereferencing the operation. */ static bool checkOpResult(pa_operation *op, const char *explain) { if (op==nullptr) { qCWarning(KMIX_LOG) << "PulseAudio operation" << explain << "failed," << pa_strerror(pa_context_errno(s_context)); return (false); } pa_operation_unref(op); return (true); } void ext_stream_restore_read_cb(pa_context *c, const pa_ext_stream_restore_info *i, int eol, void *) { if (eol < 0) { dec_outstanding(c); qCWarning(KMIX_LOG) << "Failed to initialize stream_restore extension," << pa_strerror(pa_context_errno(s_context)); return; } if (eol > 0) { dec_outstanding(c); // Special case: ensure that our media events exists. // On first login by a new user this won't be in our // database, so we should create it. if (!s_RestoreRules.contains(KMIXPA_EVENT_KEY)) { // Create a fake rule restoreRule rule; rule.channel_map.channels = 1; rule.channel_map.map[0] = PA_CHANNEL_POSITION_MONO; rule.volume.channels = 1; rule.volume.values[0] = PA_VOLUME_NORM; rule.mute = false; rule.device = ""; s_RestoreRules[KMIXPA_EVENT_KEY] = rule; qCDebug(KMIX_LOG) << "Initializing restore rule for 'Event Sounds'"; } if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) { // If we have rules, it will be created below... but if no rules // then we add it here. if (!outputRoles.contains(PA_INVALID_INDEX)) { devinfo s = create_role_devinfo(KMIXPA_EVENT_KEY); outputRoles[s.index] = s; s_mixers[KMIXPA_APP_PLAYBACK]->addWidget(s.index); } s_mixers[KMIXPA_APP_PLAYBACK]->triggerUpdate(); } return; } - QString name = QString::fromUtf8(i->name); // qCDebug(KMIX_LOG) << QString("Got some info about restore rule: '%1' (Device: %2)").arg(name).arg(i->device ? i->device : "None"); restoreRule rule; rule.channel_map = i->channel_map; rule.volume = i->volume; rule.mute = !!i->mute; rule.device = i->device; if (rule.channel_map.channels < 1 && name == KMIXPA_EVENT_KEY) { // Stream restore rules may not have valid volumes/channel maps (as these are optional) // but we need a valid volume+channelmap for our events sounds so fix it up. rule.channel_map.channels = 1; rule.channel_map.map[0] = PA_CHANNEL_POSITION_MONO; rule.volume.channels = 1; rule.volume.values[0] = PA_VOLUME_NORM; } s_RestoreRules[name] = rule; if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) { // We only want to know about Sound Events for now... if (name == KMIXPA_EVENT_KEY) { devinfo s = create_role_devinfo(name); bool is_new = !outputRoles.contains(s.index); outputRoles[s.index] = s; if (is_new) s_mixers[KMIXPA_APP_PLAYBACK]->addWidget(s.index, true); } } } static void ext_stream_restore_subscribe_cb(pa_context *c, void *) { Q_ASSERT(c == s_context); pa_operation *op = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, NULL); checkOpResult(op, "pa_ext_stream_restore_read"); } static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *) { Q_ASSERT(c == s_context); switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { case PA_SUBSCRIPTION_EVENT_SINK: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_PLAYBACK)) s_mixers[KMIXPA_PLAYBACK]->removeWidget(index); } else { pa_operation *op = pa_context_get_sink_info_by_index(c, index, sink_cb, NULL); checkOpResult(op, "pa_context_get_sink_info_by_index"); } break; case PA_SUBSCRIPTION_EVENT_SOURCE: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_CAPTURE)) s_mixers[KMIXPA_CAPTURE]->removeWidget(index); } else { pa_operation *op = pa_context_get_source_info_by_index(c, index, source_cb, NULL); checkOpResult(op, "pa_context_get_source_info_by_index"); } break; case PA_SUBSCRIPTION_EVENT_SINK_INPUT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_APP_PLAYBACK)) s_mixers[KMIXPA_APP_PLAYBACK]->removeWidget(index); } else { pa_operation *op = pa_context_get_sink_input_info(c, index, sink_input_cb, NULL); checkOpResult(op, "pa_context_get_sink_input_info"); } break; case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { if (s_mixers.contains(KMIXPA_APP_CAPTURE)) s_mixers[KMIXPA_APP_CAPTURE]->removeWidget(index); } else { pa_operation *op = pa_context_get_source_output_info(c, index, source_output_cb, NULL); checkOpResult(op, "pa_context_get_sink_input_info"); } break; case PA_SUBSCRIPTION_EVENT_CLIENT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { clients.remove(index); } else { pa_operation *op = pa_context_get_client_info(c, index, client_cb, NULL); checkOpResult(op, "pa_context_get_client_info"); } break; } } static void context_state_callback(pa_context *c, void *) { pa_context_state_t state = pa_context_get_state(c); if (state == PA_CONTEXT_READY) { // Attempt to load things up pa_operation *op; // 1. Register for the stream changes (except during probe) if (s_context == c) { pa_context_set_subscribe_callback(c, subscribe_cb, NULL); op = pa_context_subscribe(c, (pa_subscription_mask_t) (PA_SUBSCRIPTION_MASK_SINK| PA_SUBSCRIPTION_MASK_SOURCE| PA_SUBSCRIPTION_MASK_CLIENT| PA_SUBSCRIPTION_MASK_SINK_INPUT| PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT), NULL, NULL); if (!checkOpResult(op, "pa_context_subscribe")) return; } op = pa_context_get_sink_info_list(c, sink_cb, NULL); if (!checkOpResult(op, "pa_context_get_sink_info_list")) return; s_outstandingRequests++; op = pa_context_get_source_info_list(c, source_cb, NULL); if (!checkOpResult(op, "pa_context_get_source_info_list")) return; s_outstandingRequests++; op = pa_context_get_client_info_list(c, client_cb, NULL); if (!checkOpResult(op, "pa_context_client_info_list")) return; s_outstandingRequests++; op = pa_context_get_sink_input_info_list(c, sink_input_cb, NULL); if (!checkOpResult(op, "pa_context_get_sink_input_info_list")) return; s_outstandingRequests++; op = pa_context_get_source_output_info_list(c, source_output_cb, NULL); if (!checkOpResult(op, "pa_context_get_source_output_info_list")) return; s_outstandingRequests++; /* These calls are not always supported */ op = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, NULL); if (checkOpResult(op, "pa_ext_stream_restore_read")) { s_outstandingRequests++; pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, NULL); pa_ext_stream_restore_subscribe(c, 1, NULL, NULL); } } else if (!PA_CONTEXT_IS_GOOD(state)) { // If this is our probe phase, exit our context immediately if (s_context != c) { pa_context_disconnect(c); } else { // If we're not probing, it means we've been disconnected from our // GLib context pa_context_unref(s_context); s_context = NULL; // Remove all GUI elements QMap::iterator it; for (it = s_mixers.begin(); it != s_mixers.end(); ++it) { (*it)->removeAllWidgets(); } // This one is not handled above. clients.clear(); if (s_mixers.contains(KMIXPA_PLAYBACK)) { qCWarning(KMIX_LOG) << "Connection to PulseAudio daemon closed. Attempting reconnection."; s_pulseActive = UNKNOWN; QTimer::singleShot(50, s_mixers[KMIXPA_PLAYBACK], SLOT(reinit())); } } } } static void setVolumeFromPulse(Volume& volume, const devinfo& dev) { chanIDMap::const_iterator iter; for (iter = dev.chanIDs.begin(); iter != dev.chanIDs.end(); ++iter) { //qCDebug(KMIX_LOG) << "Setting volume for channel " << iter.value() << " to " << (long)dev.volume.values[iter.key()] << " (" << ((100*(long)dev.volume.values[iter.key()]) / PA_VOLUME_NORM) << "%)"; volume.setVolume(iter.value(), (long)dev.volume.values[iter.key()]); } } static pa_cvolume genVolumeForPulse(const devinfo& dev, Volume& volume) { pa_cvolume cvol = dev.volume; chanIDMap::const_iterator iter; for (iter = dev.chanIDs.begin(); iter != dev.chanIDs.end(); ++iter) { cvol.values[iter.key()] = (uint32_t)volume.getVolume(iter.value()); //qCDebug(KMIX_LOG) << "Setting volume for channel " << iter.value() << " to " << cvol.values[iter.key()] << " (" << ((100*cvol.values[iter.key()]) / PA_VOLUME_NORM) << "%)"; } return cvol; } static devmap* get_widget_map(int type, QString id = QString()) { Q_ASSERT(type >= 0 && type <= KMIXPA_WIDGET_MAX); if (KMIXPA_PLAYBACK == type) return &outputDevices; else if (KMIXPA_CAPTURE == type) return &captureDevices; else if (KMIXPA_APP_PLAYBACK == type) { if (id.startsWith(QLatin1String("restore:"))) return &outputRoles; return &outputStreams; } else if (KMIXPA_APP_CAPTURE == type) return &captureStreams; Q_ASSERT(0); return NULL; } + static devmap* get_widget_map(int type, int index) { if (PA_INVALID_INDEX == (uint32_t)index) return get_widget_map(type, "restore:"); return get_widget_map(type); } void Mixer_PULSE::emitControlsReconfigured() { // emit controlsReconfigured(_mixer->id()); // Do not emit directly to ensure all connected slots are executed // in their own event loop. /* * Bug 309464: * * Comment by cesken: I am not really sure what the comment above means. * 1) IIRC coling told me "otherwise KMix crashes". * 2) There are also bug reports that heavily indicate the crash when operation the "move stream" from a popup * menu. * 3) I don't know what the "executed in their own event loop" means. Are we in a "wrong" thread here (PA), * which is not suitable for GUI code?!? * * Work note: Ouch. it means PA thread makes direct calls via announce(), and do even GUI code. OUCH. Redo this comments! * * Conclusions: * a) It seems there seems to be some object deletion hazard with a QMenu (the one for "move stream") * b) I do not see why executing it Queued is better, because you can never know when it is actually being * executed: it could be "right now". It looks like Qt currently executes it after the QMenu hazard has * resolved itself miraculously. * c) I am definitely strongly opposed on this "execute later" approach. It is pure gambling IMO and might be * broken any time (from DEBUG to RELEASE build, or by a new Qt or KDE version). * * TODO Somebody with more Qt and PA internal insight might help to clear up things here. * * Temporary solution: Do the QueuedConnection until we really know hat is going on. But the called code * pulseControlsReconfigured() will then do the standard announce() so that every part of * KMix automatically gets updated. * */ QMetaObject::invokeMethod(this, "pulseControlsReconfigured", Qt::QueuedConnection); // QMetaObject::invokeMethod(this, // "pulseControlsReconfigured", // Qt::QueuedConnection, // Q_ARG(QString, _mixer->id())); } void Mixer_PULSE::pulseControlsReconfigured() { qCDebug(KMIX_LOG) << "Reconfigure " << _mixer->id(); ControlManager::instance().announce(_mixer->id(), ControlManager::ControlList, getDriverName()); } void Mixer_PULSE::pulseControlsReconfigured(QString mixerId) { qCDebug(KMIX_LOG) << "Reconfigure " << mixerId; ControlManager::instance().announce(mixerId, ControlManager::ControlList, getDriverName()); } void Mixer_PULSE::updateRecommendedMaster(devmap* map) { unsigned int prio = 0; shared_ptr res; MixSet::iterator iter; for (iter = m_mixDevices.begin(); iter != m_mixDevices.end(); ++iter) { unsigned int devprio = map->value( id2num((*iter)->id()) ).priority; if (( devprio > prio ) || !res ) { prio = devprio; res = *iter; } } if (res) qCDebug(KMIX_LOG) << "Selecting master " << res->id() << " for type " << m_devnum; m_recommendedMaster = res; } void Mixer_PULSE::addWidget(int index, bool isAppStream) { devmap* map = get_widget_map(m_devnum, index); if (!map->contains(index)) { qCWarning(KMIX_LOG) << "New " << m_devnum << " widget notified for index " << index << " but I cannot find it in my list :s"; return; } if (addDevice((*map)[index], isAppStream)) updateRecommendedMaster(map); emitControlsReconfigured(); } void Mixer_PULSE::removeWidget(int index) { devmap* map = get_widget_map(m_devnum); if (!map->contains(index)) { qCDebug(KMIX_LOG) << "Removing " << m_devnum << " widget notified for index " << index << " but I cannot find it in my list :s"; // Sometimes we ignore things (e.g. event sounds) so don't be too noisy here. return; } QString id = (*map)[index].name; map->remove(index); // We need to find the MixDevice that goes with this widget and remove it. MixSet::iterator iter; shared_ptr md; for (iter = m_mixDevices.begin(); iter != m_mixDevices.end(); ++iter) { if ((*iter)->id() == id) { md = m_mixDevices.get(id); qCDebug(KMIX_LOG) << "MixDevice 1 useCount=" << md.use_count(); md->close(); qCDebug(KMIX_LOG) << "MixDevice 2 useCount=" << md.use_count(); m_mixDevices.erase(iter); qCDebug(KMIX_LOG) << "MixDevice 3 useCount=" << md.use_count(); break; } } if (md) updateRecommendedMaster(map); emitControlsReconfigured(); qCDebug(KMIX_LOG) << "MixDevice 4 useCount=" << md.use_count(); } void Mixer_PULSE::removeAllWidgets() { devmap* map = get_widget_map(m_devnum); map->clear(); // Special case if (KMIXPA_APP_PLAYBACK == m_devnum) outputRoles.clear(); freeMixDevices(); emitControlsReconfigured(); } bool Mixer_PULSE::addDevice(devinfo& dev, bool isAppStream) { if (dev.chanMask==Volume::MNONE) return (false); MixSet *ms = nullptr; if (m_devnum==KMIXPA_APP_PLAYBACK && s_mixers.contains(KMIXPA_PLAYBACK)) { ms = s_mixers[KMIXPA_PLAYBACK]->getMixSet(); } else if (m_devnum==KMIXPA_APP_CAPTURE && s_mixers.contains(KMIXPA_CAPTURE)) { ms = s_mixers[KMIXPA_CAPTURE]->getMixSet(); } const bool isCapture = (m_devnum==KMIXPA_APP_CAPTURE || m_devnum==KMIXPA_CAPTURE); const int maxVol = (!isCapture && GlobalConfig::instance().data.volumeOverdrive) ? PA_VOLUME_UI_MAX : PA_VOLUME_NORM; Volume v(maxVol, PA_VOLUME_MUTED, true, false); v.addVolumeChannels(dev.chanMask); setVolumeFromPulse(v, dev); MixDevice* md = new MixDevice( _mixer, dev.name, dev.description, dev.icon_name, ms); if (isAppStream) md->setApplicationStream(true); //qCDebug(KMIX_LOG) << "Adding Pulse volume" << dev.name // << "isCapture" << isCapture // << "isAppStream" << isAppStream << "=" << md->isApplicationStream() // << "devnum" << m_devnum; if (isCapture) { md->addCaptureVolume(v); md->setRecSource(!dev.mute); } else { md->addPlaybackVolume(v); md->setMuted(dev.mute); } m_mixDevices.append(md->addToPool()); return (true); } Mixer_Backend* PULSE_getMixer( Mixer *mixer, int devnum ) { Mixer_Backend *l_mixer; l_mixer = new Mixer_PULSE( mixer, devnum ); return l_mixer; } bool Mixer_PULSE::connectToDaemon() { Q_ASSERT(NULL == s_context); qCDebug(KMIX_LOG) << "Attempting connection to PulseAudio sound daemon"; pa_mainloop_api *api = pa_glib_mainloop_get_api(s_mainloop); Q_ASSERT(api); s_context = pa_context_new(api, "KMix"); Q_ASSERT(s_context); if (pa_context_connect(s_context, NULL, PA_CONTEXT_NOFAIL, 0) < 0) { pa_context_unref(s_context); s_context = NULL; return false; } pa_context_set_state_callback(s_context, &context_state_callback, NULL); return true; } Mixer_PULSE::Mixer_PULSE(Mixer *mixer, int devnum) : Mixer_Backend(mixer, devnum) { if ( devnum == -1 ) m_devnum = 0; QString pulseenv = qgetenv("KMIX_PULSEAUDIO_DISABLE"); if (pulseenv.toInt()) s_pulseActive = INACTIVE; // We require a glib event loop if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("EventDispatcherGlib") && !QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("GlibEventDispatcher")) { qCDebug(KMIX_LOG) << "Disabling PulseAudio integration for lack of GLib event loop"; s_pulseActive = INACTIVE; } ++refcount; if (INACTIVE != s_pulseActive && 1 == refcount) { // First of all connect to PA via simple/blocking means and if that succeeds, // use a fully async integrated mainloop method to connect and get proper support. pa_mainloop *p_test_mainloop = pa_mainloop_new(); if (p_test_mainloop==nullptr) { qCDebug(KMIX_LOG) << "PulseAudio support disabled, unable to create mainloop"; s_pulseActive = INACTIVE; goto endconstruct; } pa_context *p_test_context = pa_context_new(pa_mainloop_get_api(p_test_mainloop), "kmix-probe"); if (p_test_context==nullptr) { qCDebug(KMIX_LOG) << "PulseAudio support disabled, unable to create context"; pa_mainloop_free(p_test_mainloop); s_pulseActive = INACTIVE; goto endconstruct; } qCDebug(KMIX_LOG) << "Probing for PulseAudio..."; // (cg) Convert to PA_CONTEXT_NOFLAGS when PulseAudio 0.9.19 is required if (pa_context_connect(p_test_context, NULL, static_cast(0), NULL) < 0) { qCDebug(KMIX_LOG) << QString("PulseAudio support disabled, %1").arg(pa_strerror(pa_context_errno(p_test_context))); pa_context_disconnect(p_test_context); pa_context_unref(p_test_context); pa_mainloop_free(p_test_mainloop); s_pulseActive = INACTIVE; goto endconstruct; } // Assume we are inactive, it will be set to active if appropriate s_pulseActive = INACTIVE; pa_context_set_state_callback(p_test_context, &context_state_callback, NULL); for (;;) { pa_mainloop_iterate(p_test_mainloop, 1, NULL); if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(p_test_context))) { qCDebug(KMIX_LOG) << "PulseAudio probe complete."; break; } } pa_context_disconnect(p_test_context); pa_context_unref(p_test_context); pa_mainloop_free(p_test_mainloop); if (INACTIVE != s_pulseActive) { // Reconnect via integrated mainloop s_mainloop = pa_glib_mainloop_new(NULL); Q_ASSERT(s_mainloop); connectToDaemon(); #if defined(HAVE_CANBERRA) int ret = ca_context_create(&s_ccontext); if (ret < 0) { qCDebug(KMIX_LOG) << "Disabling sound feedback, Canberra context create failed"; s_ccontext = NULL; } else ca_context_set_driver(s_ccontext, "pulse"); #endif } qCDebug(KMIX_LOG) << "PulseAudio status: " << (s_pulseActive==UNKNOWN ? "Unknown (bug)" : (s_pulseActive==ACTIVE ? "Active" : "Inactive")); } endconstruct: s_mixers[m_devnum] = this; } Mixer_PULSE::~Mixer_PULSE() { s_mixers.remove(m_devnum); if (refcount > 0) { --refcount; if (0 == refcount) { #if defined(HAVE_CANBERRA) if (s_ccontext) { ca_context_destroy(s_ccontext); s_ccontext = NULL; } #endif if (s_context) { pa_context_unref(s_context); s_context = NULL; } if (s_mainloop) { pa_glib_mainloop_free(s_mainloop); s_mainloop = NULL; } } } closeCommon(); } int Mixer_PULSE::open() { //qCDebug(KMIX_LOG) << "Trying Pulse sink"; if (ACTIVE == s_pulseActive && m_devnum <= KMIXPA_APP_CAPTURE) { // Make sure the GUI layers know we are dynamic so as to always paint us _mixer->setDynamic(); devmap::iterator iter; if (KMIXPA_PLAYBACK == m_devnum) { _id = "Playback Devices"; registerCard(i18n("Playback Devices")); for (iter = outputDevices.begin(); iter != outputDevices.end(); ++iter) addDevice(*iter); updateRecommendedMaster(&outputDevices); } else if (KMIXPA_CAPTURE == m_devnum) { _id = "Capture Devices"; registerCard(i18n("Capture Devices")); for (iter = captureDevices.begin(); iter != captureDevices.end(); ++iter) addDevice(*iter); updateRecommendedMaster(&outputDevices); } else if (KMIXPA_APP_PLAYBACK == m_devnum) { _id = "Playback Streams"; registerCard(i18n("Playback Streams")); for (iter = outputRoles.begin(); iter != outputRoles.end(); ++iter) addDevice(*iter, true); updateRecommendedMaster(&outputRoles); for (iter = outputStreams.begin(); iter != outputStreams.end(); ++iter) addDevice(*iter, true); updateRecommendedMaster(&outputStreams); } else if (KMIXPA_APP_CAPTURE == m_devnum) { _id = "Capture Streams"; registerCard(i18n("Capture Streams")); for (iter = captureStreams.begin(); iter != captureStreams.end(); ++iter) addDevice(*iter); updateRecommendedMaster(&captureStreams); } qCDebug(KMIX_LOG) << "Using PulseAudio for mixer: " << getName(); m_isOpen = true; } return 0; } int Mixer_PULSE::close() { closeCommon(); return 1; } int Mixer_PULSE::id2num(const QString& id) { int num = -1; // todo: Store this in a hash or similar int i; for (i = 0; i < m_mixDevices.size(); ++i) { if (m_mixDevices[i]->id() == id) { num = i; break; } } //qCDebug(KMIX_LOG) << "id2num() num=" << num; return num; } int Mixer_PULSE::readVolumeFromHW(const QString &id, shared_ptr md) { const devmap *map = get_widget_map(m_devnum, id); return (doForDevice(id, map, md, [](const devinfo &dev, shared_ptr md) -> int { setVolumeFromPulse(md->playbackVolume(), dev); md->setMuted(dev.mute); // to cover both playback md->setRecSource(!dev.mute); // and capture channels return (0); })); } int Mixer_PULSE::writeVolumeToHW(const QString &id, shared_ptr md) { switch (m_devnum) { case KMIXPA_PLAYBACK: return (doForDevice(id, &outputDevices, md, [](const devinfo &dev, shared_ptr md) -> int { pa_cvolume volume = genVolumeForPulse(dev, md->playbackVolume()); pa_operation *op = pa_context_set_sink_volume_by_index(s_context, dev.index, &volume, NULL, NULL); if (!checkOpResult(op,"pa_context_set_sink_volume_by_index")) return (Mixer::ERR_WRITE); op = pa_context_set_sink_mute_by_index(s_context, dev.index, (md->isMuted() ? 1 : 0), NULL, NULL); if (!checkOpResult(op, "pa_context_set_sink_mute_by_index")) return (Mixer::ERR_WRITE); #ifdef HAVE_CANBERRA if (s_ccontext!=nullptr && Mixer::getBeepOnVolumeChange()) { int playing = 0; // Note that '2' is simply an index we've picked. // It's mostly irrelevant. 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) { ca_context_cancel(s_ccontext, cindex); playing = 0; } if (playing==0) { char devnum[64]; snprintf(devnum, sizeof(devnum), "%lu", (unsigned long) dev.index); ca_context_change_device(s_ccontext, devnum); // Ideally we'd use something like ca_gtk_play_for_widget()... 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", NULL); ca_context_change_device(s_ccontext, NULL); } } #endif // HAVE_CANBERRA return (Mixer::OK); })); case KMIXPA_CAPTURE: return (doForDevice(id, &captureDevices, md, [](const devinfo &dev, shared_ptr md) -> int { pa_cvolume volume = genVolumeForPulse(dev, md->captureVolume()); pa_operation *op = pa_context_set_source_volume_by_index(s_context, dev.index, &volume, NULL, NULL); if (!checkOpResult(op, "pa_context_set_source_volume_by_index")) return (Mixer::ERR_WRITE); op = pa_context_set_source_mute_by_index(s_context, dev.index, (md->isRecSource() ? 0 : 1), NULL, NULL); if (!checkOpResult(op, "pa_context_set_source_mute_by_index")) return (Mixer::ERR_WRITE); return (Mixer::OK); })); case KMIXPA_APP_PLAYBACK: if (id.startsWith(QLatin1String("stream:"))) { return (doForDevice(id, &outputStreams, md, [](const devinfo &dev, shared_ptr md) -> int { pa_cvolume volume = genVolumeForPulse(dev, md->playbackVolume()); pa_operation *op = pa_context_set_sink_input_volume(s_context, dev.index, &volume, NULL, NULL); if (!checkOpResult(op, "pa_context_set_sink_input_volume")) return (Mixer::ERR_WRITE); op = pa_context_set_sink_input_mute(s_context, dev.index, (md->isMuted() ? 1 : 0), NULL, NULL); if (!checkOpResult(op, "pa_context_set_sink_input_mute")) return (Mixer::ERR_WRITE); return (Mixer::OK); })); } else if (id.startsWith(QLatin1String("restore:"))) { return (doForDevice(id, &outputRoles, md, [](const devinfo &dev, shared_ptr md) -> int { restoreRule &rule = s_RestoreRules[dev.stream_restore_rule]; pa_ext_stream_restore_info info; info.name = dev.stream_restore_rule.toUtf8().constData(); info.channel_map = rule.channel_map; info.volume = genVolumeForPulse(dev, md->playbackVolume()); info.device = rule.device.isEmpty() ? NULL : rule.device.toUtf8().constData(); info.mute = (md->isMuted() ? 1 : 0); pa_operation *op = pa_ext_stream_restore_write(s_context, PA_UPDATE_REPLACE, &info, 1, true, NULL, NULL); if (!checkOpResult(op, "pa_ext_stream_restore_write")) return (Mixer::ERR_WRITE); return (Mixer::OK); })); } else return (Mixer::OK); case KMIXPA_APP_CAPTURE: return (doForDevice(id, &captureStreams, md, [](const devinfo &dev, shared_ptr md) -> int { #if HAVE_SOURCE_OUTPUT_VOLUMES pa_cvolume volume = genVolumeForPulse(dev, md->captureVolume()); pa_operation *op = pa_context_set_source_output_volume(s_context, dev.index, &volume, NULL, NULL); if (!checkOpResult(op, "pa_context_set_source_output_volume")) return (Mixer::ERR_WRITE); op = pa_context_set_source_output_mute(s_context, dev.index, (md->isRecSource() ? 0 : 1), NULL, NULL); if (!checkOpResult(op, "pa_context_set_source_output_mute")) return (Mixer::ERR_WRITE); #else // Note that this is different from APP_PLAYBACK in that // we set the volume on the source itself. pa_cvolume volume = genVolumeForPulse(dev, md->captureVolume()); pa_operation *op = pa_context_set_source_volume_by_index(s_context, dev.device_index, &volume, NULL, NULL); if (!checkOpResult(op, "pa_context_set_source_volume_by_index")) return (Mixer::ERR_WRITE); op = pa_context_set_source_mute_by_index(s_context, dev.device_index, (md->isRecSource() ? 0 : 1), NULL, NULL); if (!checkOpResult(op, "pa_context_set_source_mute_by_index")) return (Mixer::ERR_WRITE); #endif return (Mixer::OK); })); default: qCWarning(KMIX_LOG) << "Unknown device index" << m_devnum; } return (Mixer::OK); } +static const devinfo *getStreamInfo(int devnum, const QString &id) +{ + //qCDebug(KMIX_LOG) << "dev" << devnum << "id" << id; + const devmap *map = get_widget_map(devnum); + for (devmap::const_iterator iter = map->constBegin(); iter!=map->constEnd(); ++iter) + { + // I think this will work! '*iter' dereferences the iterator and obtains + // a reference to the actual map item. '&' then takes the address of that + // and returns a pointer. + if (iter->name==id) return (&*iter); + } + + qCWarning(KMIX_LOG) << "Cannot find stream index for" << id; + return (nullptr); +} + + /** * Move the stream to a new destination. */ bool Mixer_PULSE::moveStream(const QString &id, const QString &destId) { Q_ASSERT(m_devnum==KMIXPA_APP_PLAYBACK || m_devnum==KMIXPA_APP_CAPTURE); - qCDebug(KMIX_LOG) << "move" << id << "->" << destId; - - // Look up the stream index - uint32_t stream_index = PA_INVALID_INDEX; - QString stream_restore_rule; + qCDebug(KMIX_LOG) << "dev" << m_devnum << "move" << id << "->" << destId; - const devmap *map = get_widget_map(m_devnum); - for (devmap::const_iterator iter = map->constBegin(); iter!=map->constEnd(); ++iter) - { - if (iter->name==id) - { - stream_index = iter->index; - stream_restore_rule = iter->stream_restore_rule; - break; - } - } - - if (stream_index==PA_INVALID_INDEX) - { - qCCritical(KMIX_LOG) << "Cannot find stream index for" << id; - return false; - } + const devinfo *info = getStreamInfo(m_devnum, id); + if (info==nullptr) return (false); if (destId.isEmpty()) { - // Reset the stream to automatic destination, using the previously saved + // Reset the stream to its automatic destination, using the previously saved // restore rule. + + const QString stream_restore_rule = info->stream_restore_rule; if (stream_restore_rule.isEmpty() || !s_RestoreRules.contains(stream_restore_rule)) { qCWarning(KMIX_LOG) << "Stream has no restore rule"; } else { // We want to remove any specific device in the stream restore rule. const restoreRule &rule = s_RestoreRules[stream_restore_rule]; pa_ext_stream_restore_info info; info.name = stream_restore_rule.toUtf8().constData(); info.channel_map = rule.channel_map; info.volume = rule.volume; info.device = NULL; info.mute = rule.mute ? 1 : 0; pa_operation *op = pa_ext_stream_restore_write(s_context, PA_UPDATE_REPLACE, &info, 1, true, NULL, NULL); if (!checkOpResult(op, "pa_ext_stream_restore_write")) return (false); } } else { // Move the stream to the specified destination. + + const uint32_t stream_index = info->index; if (m_devnum==KMIXPA_APP_PLAYBACK) { pa_operation *op = pa_context_move_sink_input_by_name(s_context, stream_index, destId.toUtf8().constData(), NULL, NULL); if (!checkOpResult(op, "pa_context_move_sink_input_by_name")) return (false); } else { pa_operation *op = pa_context_move_source_output_by_name(s_context, stream_index, destId.toUtf8().constData(), NULL, NULL); if (!checkOpResult(op, "pa_context_move_source_output_by_name")) return (false); } } return (true); } +QString Mixer_PULSE::currentStreamDevice(const QString &id) const +{ + Q_ASSERT(m_devnum==KMIXPA_APP_PLAYBACK || m_devnum==KMIXPA_APP_CAPTURE); + //qCDebug(KMIX_LOG) << "dev" << m_devnum << "id" << id; + + const devinfo *info = getStreamInfo(m_devnum, id); + if (info==nullptr) return (QString()); + + const int dev = info->device_index; + // Look in 'outputDevices' for KMIXPA_APP_PLAYBACK, or 'captureDevices' for KMIXPA_APP_CAPTURE + const devmap *deviceMap = get_widget_map(m_devnum==KMIXPA_APP_PLAYBACK ? KMIXPA_PLAYBACK : KMIXPA_CAPTURE); + if (deviceMap->contains(dev)) + { + const devinfo &s = deviceMap->value(dev); + //qDebug() << " dev" << s.device_index << "desc" << s.description << "->" << s.name; + return (s.name); + } + //else qDebug() << " no device info"; + + return (QString()); +} + + void Mixer_PULSE::reinit() { // We only support reinit on our primary mixer. Q_ASSERT(KMIXPA_PLAYBACK == m_devnum); connectToDaemon(); } void Mixer_PULSE::triggerUpdate() { readSetFromHWforceUpdate(); readSetFromHW(); } // Please see KMixWindow::initActionsAfterInitMixer(), it uses the driverName QString PULSE_getDriverName() { return "PulseAudio"; } QString Mixer_PULSE::getDriverName() { return "PulseAudio"; } diff --git a/backends/mixer_pulse.h b/backends/mixer_pulse.h index 7eb433d6..a89992c2 100644 --- a/backends/mixer_pulse.h +++ b/backends/mixer_pulse.h @@ -1,95 +1,96 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 2008 Helio Chissini de Castro * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MIXER_PULSE_H #define MIXER_PULSE_H #include #include "mixer_backend.h" #include typedef QMap chanIDMap; typedef struct { int index; int device_index; QString name; QString description; QString icon_name; pa_cvolume volume; pa_channel_map channel_map; bool mute; QString stream_restore_rule; Volume::ChannelMask chanMask; chanIDMap chanIDs; unsigned int priority; } devinfo; typedef QMap devmap; class Mixer_PULSE : public Mixer_Backend { Q_OBJECT public: Mixer_PULSE(Mixer *mixer, int devnum); virtual ~Mixer_PULSE(); int readVolumeFromHW( const QString& id, shared_ptr ) Q_DECL_OVERRIDE; int writeVolumeToHW ( const QString& id, shared_ptr ) Q_DECL_OVERRIDE; + QString currentStreamDevice(const QString &id) const Q_DECL_OVERRIDE; bool moveStream( const QString& id, const QString& destId ) Q_DECL_OVERRIDE; QString getDriverName() Q_DECL_OVERRIDE; QString getId() const Q_DECL_OVERRIDE { return _id; }; bool needsPolling() Q_DECL_OVERRIDE { return false; } void triggerUpdate(); void addWidget(int index, bool = false); void removeWidget(int index); void removeAllWidgets(); MixSet *getMixSet() { return &m_mixDevices; } int id2num(const QString& id); protected: int open() Q_DECL_OVERRIDE; int close() Q_DECL_OVERRIDE; int fd; QString _id; private: bool addDevice(devinfo& dev, bool isAppStream = false); bool connectToDaemon(); void emitControlsReconfigured(); void updateRecommendedMaster(devmap* map); protected slots: void pulseControlsReconfigured(QString mixerId); void pulseControlsReconfigured(); public: void reinit() Q_DECL_OVERRIDE; }; #endif diff --git a/core/mixer.cpp b/core/mixer.cpp index 38698c08..bcdd3c8d 100644 --- a/core/mixer.cpp +++ b/core/mixer.cpp @@ -1,720 +1,726 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 1996-2004 Christian Esken - esken@kde.org * 2002 Helio Chissini de Castro - helio@conectiva.com.br * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "core/mixer.h" #include #include #include "backends/mixer_backend.h" #include "backends/kmix-backends.cpp" #include "core/ControlManager.h" #include "core/GlobalConfig.h" #include "core/volume.h" /** * Some general design hints. Hierarchy is Mixer->MixDevice->Volume */ QList Mixer::s_mixers; MasterControl Mixer::_globalMasterCurrent; MasterControl Mixer::_globalMasterPreferred; bool Mixer::m_beepOnVolumeChange = false; int Mixer::numDrivers() { MixerFactory *factory = g_mixerFactories; int num = 0; while( factory->getMixer!=0 ) { num++; factory++; } return num; } /* * Returns a reference of the current mixer list. */ QList& Mixer::mixers() { return s_mixers; } /** * Returns whether there is at least one dynamic mixer active. * @returns true, if at least one dynamic mixer is active */ bool Mixer::dynamicBackendsPresent() { foreach ( Mixer* mixer, Mixer::mixers() ) { if ( mixer->isDynamic() ) return true; } return false; } bool Mixer::pulseaudioPresent() { foreach ( Mixer* mixer, Mixer::mixers() ) { if ( mixer->getDriverName() == "PulseAudio" ) return true; } return false; } Mixer::Mixer(const QString &ref_driverName, int device) : m_balance(0), _mixerBackend(nullptr), m_dynamic(false) { _mixerBackend = 0; int driverCount = numDrivers(); for (int driver=0; driver retrieve Mixer factory for that driver getMixerFunc *f = g_mixerFactories[driver].getMixer; if( f!=0 ) { _mixerBackend = f( this, device ); readSetFromHWforceUpdate(); // enforce an initial update on first readSetFromHW() } break; } } } Mixer::~Mixer() { // Close the mixer. This might also free memory, depending on the called backend method close(); delete _mixerBackend; } /* * Find a Mixer. If there is no mixer with the given id, 0 is returned */ Mixer* Mixer::findMixer( const QString& mixer_id) { Mixer *mixer = 0; int mixerCount = Mixer::mixers().count(); for ( int i=0; iid() == mixer_id ) { mixer = (Mixer::mixers())[i]; break; } } return mixer; } /** * Set the final ID of this Mixer. *
Warning: This method is VERY fragile, because it is requires information that we have very late, * especially the _cardInstance. We only know the _cardInstance, when we know the ID of the _mixerBackend->getId(). * OTOH, the Mixer backend needs the _cardInstance during construction of its MixDevice instances. * * This means, we need the _cardInstance during construction of the Mixer, but we only know it after its constructed. * Actually its a design error. The _cardInstance MUST be set and managed by the backend. * * The current solution works but is very hacky - cardInstance is a parameter of openIfValid(). * */ void Mixer::recreateId() { /* As we use "::" and ":" as separators, the parts %1,%2 and %3 may not * contain it. * %1, the driver name is from the KMix backends, it does not contain colons. * %2, the mixer name, is typically coming from an OS driver. It could contain colons. * %3, the mixer number, is a number: it does not contain colons. */ QString mixerName = _mixerBackend->getId(); mixerName.replace(':','_'); QString primaryKeyOfMixer = QString("%1::%2:%3") .arg(getDriverName(), mixerName) .arg(getCardInstance()); // The following 3 replaces are for not messing up the config file primaryKeyOfMixer.replace(']','_'); primaryKeyOfMixer.replace('[','_'); // not strictly necessary, but lets play safe primaryKeyOfMixer.replace(' ','_'); primaryKeyOfMixer.replace('=','_'); _id = primaryKeyOfMixer; // qCDebug(KMIX_LOG) << "Early _id=" << _id; } const QString Mixer::dbusPath() { // _id needs to be fixed from the very beginning, as the MixDevice construction uses MixDevice::dbusPath(). // So once the first MixDevice is created, this must return the correct value if (_id.isEmpty()) { if (! _mixerBackend->_cardRegistered) { // Bug 308014: By checking _cardRegistered, we can be sure that everything is fine, including the fact that // the cardId (aka "card instance") is set. If _cardRegistered would be false, we will create potentially // wrong/duplicated DBUS Paths here. qCWarning(KMIX_LOG) << "Mixer id was empty when creating DBUS path. Emergency code created the id=" <<_id; } // Bug 308014: Actually this a shortcut (you could also call it a hack). It would likely better if registerCard() // would create the Id, but it requires cooperation from ALL backends. Also Mixer->getId() would need to // proxy that to the backend. // So for now we lazily create the MixerId here, while creating the first MixDevice for that card. recreateId(); } // mixerName may contain arbitrary characters, so replace all that are not allowed to be be part of a DBUS path QString cardPath = _id; cardPath.replace(QRegExp("[^a-zA-Z0-9_]"), "_"); cardPath.replace(QLatin1String("//"), QLatin1String("/")); return QString("/Mixers/" + cardPath); } void Mixer::volumeSave( KConfig *config ) { // qCDebug(KMIX_LOG) << "Mixer::volumeSave()"; _mixerBackend->readSetFromHW(); QString grp("Mixer"); grp.append(id()); _mixerBackend->m_mixDevices.write( config, grp ); // This might not be the standard application config object // => Better be safe and call sync(). config->sync(); } void Mixer::volumeLoad( KConfig *config ) { QString grp("Mixer"); grp.append(id()); if ( ! config->hasGroup(grp) ) { // no such group. Volumes (of this mixer) were never saved beforehand. // Thus don't restore anything (also see Bug #69320 for understanding the real reason) return; // make sure to bail out immediately } // else restore the volumes if ( ! _mixerBackend->m_mixDevices.read( config, grp ) ) { // Some mixer backends don't support reading the volume into config // files, so bail out early if that's the case. return; } // set new settings for(int i=0; i<_mixerBackend->m_mixDevices.count() ; i++ ) { shared_ptr md = _mixerBackend->m_mixDevices[i]; if ( md.get() == 0 ) continue; _mixerBackend->writeVolumeToHW( md->id(), md ); if ( md->isEnum() ) _mixerBackend->setEnumIdHW( md->id(), md->enumId() ); } } /** * Opens the mixer. * Also, starts the polling timer, for polling the Volumes from the Mixer. * * @return true, if Mixer could be opened. */ bool Mixer::openIfValid() { if (_mixerBackend==nullptr) { // If we did not instantiate a suitable backend, then the mixer is invalid. qCWarning(KMIX_LOG) << "no mixer backend"; return false; } bool ok = _mixerBackend->openIfValid(); if (!ok) return (false); recreateId(); shared_ptr recommendedMaster = _mixerBackend->recommendedMaster(); if (recommendedMaster.get()!=nullptr) { QString recommendedMasterStr = recommendedMaster->id(); setLocalMasterMD( recommendedMasterStr ); qCDebug(KMIX_LOG) << "Detected master" << recommendedMaster->id(); } else { if (!m_dynamic) qCCritical(KMIX_LOG) << "No master detected and not dynamic"; else qCDebug(KMIX_LOG) << "No master detected but dynamic"; QString noMaster = "---no-master-detected---"; setLocalMasterMD(noMaster); // no master } new DBusMixerWrapper(this, dbusPath()); return (true); } /** * Closes the mixer. */ void Mixer::close() { if ( _mixerBackend != 0) _mixerBackend->closeCommon(); } /* ------- WRAPPER METHODS. START ------------------------------ */ unsigned int Mixer::size() const { return _mixerBackend->m_mixDevices.count(); } shared_ptr Mixer::operator[](int num) { shared_ptr md = _mixerBackend->m_mixDevices.at( num ); return md; } MixSet& Mixer::getMixSet() { return _mixerBackend->m_mixDevices; } /** * Returns the driver name, that handles this Mixer. */ QString Mixer::getDriverName() const { QString driverName = _mixerBackend->getDriverName(); // qCDebug(KMIX_LOG) << "Mixer::getDriverName() = " << driverName << "\n"; return driverName; } bool Mixer::isOpen() const { if ( _mixerBackend == 0 ) return false; else return _mixerBackend->isOpen(); } void Mixer::readSetFromHWforceUpdate() const { _mixerBackend->readSetFromHWforceUpdate(); } /// Returns translated WhatsThis messages for a control.Translates from QString Mixer::translateKernelToWhatsthis(const QString &kernelName) { return _mixerBackend->translateKernelToWhatsthis(kernelName); } /* ------- WRAPPER METHODS. END -------------------------------- */ int Mixer::balance() const { return m_balance; } void Mixer::setBalance(int balance) { if( balance == m_balance ) { // balance unchanged => return return; } m_balance = balance; shared_ptr master = getLocalMasterMD(); if ( master.get() == 0 ) { // no master device available => return return; } Volume& volP = master->playbackVolume(); setBalanceInternal(volP); Volume& volC = master->captureVolume(); setBalanceInternal(volC); _mixerBackend->writeVolumeToHW( master->id(), master ); emit newBalance( volP ); } void Mixer::setBalanceInternal(Volume& vol) { //_mixerBackend->readVolumeFromHW( master->id(), master ); int left = vol.getVolume(Volume::LEFT); int right = vol.getVolume( Volume::RIGHT ); int refvol = left > right ? left : right; if( m_balance < 0 ) // balance left { vol.setVolume( Volume::LEFT, refvol); vol.setVolume( Volume::RIGHT, (m_balance * refvol) / 100 + refvol ); } else { vol.setVolume( Volume::LEFT, -(m_balance * refvol) / 100 + refvol ); vol.setVolume( Volume::RIGHT, refvol); } } /** * Returns a name suitable for a human user to read (on a label, ...) */ QString Mixer::readableName(bool ampersandQuoted) const { QString finalName = _mixerBackend->getName(); if (ampersandQuoted) finalName.replace('&', "&&"); if ( getCardInstance() > 1) finalName = finalName.append(" %1").arg(getCardInstance()); // qCDebug(KMIX_LOG) << "name=" << _mixerBackend->getName() << "instance=" << getCardInstance() << ", finalName" << finalName; return finalName; } QString Mixer::getBaseName() const { return _mixerBackend->getName(); } /** * Queries the Driver Factory for a driver. * @par driver Index number. 0 <= driver < numDrivers() */ QString Mixer::driverName( int driver ) { getDriverNameFunc *f = g_mixerFactories[driver].getDriverName; if( f!=0 ) return f(); else return "unknown"; } /* obsoleted by setInstance() void Mixer::setID(QString& ref_id) { _id = ref_id; } */ const QString &Mixer::id() const { return _id; } const QString &Mixer::udi() const { return _mixerBackend->udi(); } /** * Set the global master, which is shown in the dock area and which is accessible via the * DBUS masterVolume() method. * * The parameters are taken over as-is, this means without checking for validity. * This allows the User to define a master card that is not always available * (e.g. it is an USB hotplugging device). Also you can set the master at any time you * like, e.g. after reading the KMix configuration file and before actually constructing * the Mixer instances (hint: this method is static!). * * @param ref_card The card id * @param ref_control The control id. The corresponding control must be present in the card. * @param preferred Whether this is the preferred master (auto-selected on coldplug and hotplug). */ void Mixer::setGlobalMaster(QString ref_card, QString ref_control, bool preferred) { qCDebug(KMIX_LOG) << "ref_card=" << ref_card << ", ref_control=" << ref_control << ", preferred=" << preferred; _globalMasterCurrent.set(ref_card, ref_control); if ( preferred ) _globalMasterPreferred.set(ref_card, ref_control); qCDebug(KMIX_LOG) << "Mixer::setGlobalMaster() card=" <id(); return mixer; } /** * Return the preferred global master. * If there is no preferred global master, returns the current master instead. */ MasterControl& Mixer::getGlobalMasterPreferred(bool fallbackAllowed) { static MasterControl result; if ( !fallbackAllowed || _globalMasterPreferred.isValid() ) { // qCDebug(KMIX_LOG) << "Returning preferred master"; return _globalMasterPreferred; } Mixer* mm = Mixer::getGlobalMasterMixerNoFalback(); if (mm) { result.set(_globalMasterPreferred.getCard(), mm->getRecommendedDeviceId()); if (!result.getControl().isEmpty()) // qCDebug(KMIX_LOG) << "Returning extended preferred master"; return result; } qCDebug(KMIX_LOG) << "Returning current master"; return _globalMasterCurrent; } shared_ptr Mixer::getGlobalMasterMD() { return getGlobalMasterMD(true); } shared_ptr Mixer::getGlobalMasterMD(bool fallbackAllowed) { shared_ptr mdRet; shared_ptr firstDevice; Mixer *mixer = fallbackAllowed ? Mixer::getGlobalMasterMixer() : Mixer::getGlobalMasterMixerNoFalback(); if ( mixer == 0 ) return mdRet; if (_globalMasterCurrent.getControl().isEmpty()) { // Default (recommended) control return mixer->_mixerBackend->recommendedMaster(); } foreach (shared_ptr md, mixer->_mixerBackend->m_mixDevices ) { if ( md.get() == 0 ) continue; // invalid firstDevice=md; if ( md->id() == _globalMasterCurrent.getControl() ) { mdRet = md; break; // found } } if ( mdRet.get() == 0 ) { //For some sound cards when using pulseaudio the mixer id is not proper hence returning the first device as master channel device //This solves the bug id:290177 and problems stated in review #105422 qCDebug(KMIX_LOG) << "Mixer::masterCardDevice() returns 0 (no globalMaster), returning the first device"; mdRet=firstDevice; } return mdRet; } QString Mixer::getRecommendedDeviceId() { if ( _mixerBackend != 0 ) { shared_ptr recommendedMaster = _mixerBackend->recommendedMaster(); if ( recommendedMaster.get() != 0 ) return recommendedMaster->id(); } return QString(); } shared_ptr Mixer::getLocalMasterMD() const { if (_mixerBackend && _masterDevicePK.isEmpty()) return _mixerBackend->recommendedMaster(); return find( _masterDevicePK ); } void Mixer::setLocalMasterMD(QString &devPK) { _masterDevicePK = devPK; } shared_ptr Mixer::find(const QString& mixdeviceID) const { shared_ptr mdRet; foreach (shared_ptr md, _mixerBackend->m_mixDevices ) { if ( md.get() == 0 ) continue; // invalid if ( md->id() == mixdeviceID ) { mdRet = md; break; // found } } return mdRet; } shared_ptr Mixer::getMixdeviceById( const QString& mixdeviceID ) { qCDebug(KMIX_LOG) << "id=" << mixdeviceID << "md=" << _mixerBackend->m_mixDevices.get(mixdeviceID).get()->id(); return _mixerBackend->m_mixDevices.get(mixdeviceID); // shared_ptr md; // int num = _mixerBackend->id2num(mixdeviceID); // if ( num!=-1 && num < (int)size() ) // { // md = (*this)[num]; // } // return md; } /** Call this if you have a *reference* to a Volume object and have modified that locally. Pass the MixDevice associated to that Volume to this method for writing back the changed value to the mixer. Hint: Why do we do it this way? - It is fast (no copying of Volume objects required) - It is easy to understand ( read - modify - commit ) */ void Mixer::commitVolumeChange(shared_ptr md) { _mixerBackend->writeVolumeToHW(md->id(), md); if (md->isEnum()) { _mixerBackend->setEnumIdHW(md->id(), md->enumId()); } if (md->captureVolume().hasSwitch()) { // Make sure to re-read the hardware, because setting capture might have failed. // This is due to exclusive capture groups. // If we wouldn't do this, KMix might show a Capture Switch disabled, but // in reality the capture switch is still on. // // We also cannot rely on a notification from the driver (SocketNotifier), because // nothing has changed, and so there s nothing to notify. _mixerBackend->readSetFromHWforceUpdate(); if (GlobalConfig::instance().data.debugControlManager) qCDebug(KMIX_LOG) << "committing a control with capture volume, that might announce: " << md->id(); _mixerBackend->readSetFromHW(); } if (GlobalConfig::instance().data.debugControlManager) qCDebug(KMIX_LOG) << "committing announces the change of: " << md->id(); // We announce the change we did, so all other parts of KMix can pick up the change ControlManager::instance().announce(md->mixer()->id(), ControlManager::Volume, QString("Mixer.commitVolumeChange()")); } // @dbus, used also in kmix app void Mixer::increaseVolume( const QString& mixdeviceID ) { increaseOrDecreaseVolume(mixdeviceID, false); } // @dbus void Mixer::decreaseVolume( const QString& mixdeviceID ) { increaseOrDecreaseVolume(mixdeviceID, true); } /** * Increase or decrease all playback and capture channels of the given control. * This method is very similar to MDWSlider::increaseOrDecreaseVolume(), but it will * NOT auto-unmute. * * @param mixdeviceID The control name * @param decrease true for decrease. false for increase */ void Mixer::increaseOrDecreaseVolume( const QString& mixdeviceID, bool decrease ) { shared_ptr md= getMixdeviceById( mixdeviceID ); if (md.get() != 0) { Volume& volP=md->playbackVolume(); if ( volP.hasVolume() ) { volP.changeAllVolumes(volP.volumeStep(decrease)); } Volume& volC=md->captureVolume(); if ( volC.hasVolume() ) { volC.changeAllVolumes(volC.volumeStep(decrease)); } _mixerBackend->writeVolumeToHW(mixdeviceID, md); } ControlManager::instance().announce(md->mixer()->id(), ControlManager::Volume, QString("Mixer.increaseOrDecreaseVolume()")); /************************************************************ It is important, not to implement this method like this: int vol=volume(mixdeviceID); setVolume(mixdeviceID, vol-5); It creates too big rounding errors. If you don't believe me, then do a decreaseVolume() and increaseVolume() with "vol.maxVolume() == 31". ***********************************************************/ } void Mixer::setDynamic ( bool dynamic ) { m_dynamic = dynamic; } bool Mixer::isDynamic() { return m_dynamic; } -bool Mixer::moveStream( const QString id, const QString& destId ) + +bool Mixer::moveStream(const QString &id, const QString &destId) { // We should really check that id is within our md's.... - bool ret = _mixerBackend->moveStream( id, destId ); + bool ret = _mixerBackend->moveStream(id, destId); ControlManager::instance().announce(QString(), ControlManager::ControlList, QString("Mixer.moveStream()")); - return ret; + return (ret); } + +QString Mixer::currentStreamDevice(const QString &id) const +{ + return (_mixerBackend->currentStreamDevice(id)); +} diff --git a/core/mixer.h b/core/mixer.h index ef5cbe0a..e3ae794e 100644 --- a/core/mixer.h +++ b/core/mixer.h @@ -1,225 +1,225 @@ //-*-C++-*- /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 2000 Stefan Schimanski <1Stein@gmx.de> * 1996-2000 Christian Esken * Sven Fischer * 2002 - Helio Chissini de Castro * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef RANDOMPREFIX_MIXER_H #define RANDOMPREFIX_MIXER_H #include #include #include #include "core/volume.h" #include "backends/mixer_backend.h" #include "core/MasterControl.h" #include "mixset.h" #include "core/GlobalConfig.h" #include "core/mixdevice.h" #include "dbus/dbusmixerwrapper.h" #include "kmixcore_export.h" class Volume; class KConfig; class KMIXCORE_EXPORT Mixer : public QObject { Q_OBJECT public: /** * Status for Mixer operations. * * OK_UNCHANGED is a special variant of OK. It must be implemented by * backends that use needsPolling() == true. See Mixer_OSS.cpp for an * example. Rationale is that we need a proper change check: Otherwise * the DBUS Session Bus is massively spammed. Also quite likely the Mixer * GUI might get updated all the time. * */ enum MixerError { OK=0, ERR_PERM=1, ERR_WRITE, ERR_READ, ERR_OPEN, OK_UNCHANGED }; Mixer(const QString &ref_driverName, int device); virtual ~Mixer(); static int numDrivers(); QString getDriverName() const; shared_ptr find(const QString& devPK) const; static Mixer* findMixer( const QString& mixer_id); void volumeSave( KConfig *config ); void volumeLoad( KConfig *config ); /// Tells the number of the mixing devices unsigned int size() const; /// Returns a pointer to the mix device with the given number // TODO remove this method. Only used by ViewDockAreaPopup: dockMD = (*mixer)[0]; shared_ptr operator[](int val_i_num); /// Returns a pointer to the mix device whose type matches the value /// given by the parameter and the array MixerDevNames given in /// mixer_oss.cpp (0 is Volume, 4 is PCM, etc.) shared_ptr getMixdeviceById( const QString& deviceID ); /// Open/grab the mixer for further interaction bool openIfValid(); /// Returns whether the card is open/operational bool isOpen() const; /// Close/release the mixer virtual void close(); /// Reads balance int balance() const; /// Returns a detailed state message after errors. Only for diagnostic purposes, no i18n. QString& stateMessage() const; /** * Returns the name of the card/chip/hardware, as given by the driver. The name is NOT instance specific, * so if you install two identical soundcards, two of them will deliver the same mixerName(). * Use this method if you need an instance-UNspecific name, e.g. for finding an appropriate * mixer layout for this card, or as a prefix for constructing instance specific ID's like in id(). */ virtual QString getBaseName() const; /// Wrapper to Mixer_Backend QString translateKernelToWhatsthis(const QString &kernelName); /** * Get a name suitable for a human user to read, possibly with quoted ampersand. * The latter is required by some GUI elements like QRadioButton or when used as a * tab label, as '&' introduces an accelerator there. * * @param ampersandQuoted @c true if '&' characters are to be quoted * @return the readable device name */ QString readableName(bool ampersandQuoted = false) const; // Returns the name of the driver, e.g. "OSS" or "ALSA0.9" static QString driverName(int num); static bool getBeepOnVolumeChange() { GlobalConfigData& gcd = GlobalConfig::instance().data; return gcd.beepOnVolumeChange; } /** * Returns an unique ID of the Mixer. It currently looks like ":::" */ const QString &id() const; int getCardInstance() const { return _mixerBackend->getCardInstance(); } /// Returns an Universal Device Identification of the Mixer. This is an ID that relates to the underlying operating system. // For OSS and ALSA this is taken from Solid (actually HAL). For Solaris this is just the device name. // Examples: // ALSA: /org/freedesktop/Hal/devices/usb_device_d8c_1_noserial_if0_sound_card_0_2_alsa_control__1 // OSS: /org/freedesktop/Hal/devices/usb_device_d8c_1_noserial_if0_sound_card_0_2_oss_mixer__1 // Solaris: /dev/audio const QString &udi() const; // Returns a DBus path for this mixer // Used also by MixDevice to bind to this path const QString dbusPath(); static QList & mixers(); /****************************************** The KMix GLOBAL master card. Please note that KMix and KMixPanelApplet can have a different MasterCard's at the moment (but actually KMixPanelApplet does not read/save this yet). At the moment it is only used for selecting the Mixer to use in KMix's DockIcon. ******************************************/ static void setGlobalMaster(QString ref_card, QString ref_control, bool preferred); static shared_ptr getGlobalMasterMD(); static shared_ptr getGlobalMasterMD(bool fallbackAllowed); static Mixer* getGlobalMasterMixer(); static Mixer* getGlobalMasterMixerNoFalback(); static MasterControl& getGlobalMasterPreferred(bool fallbackAllowed = true); QString getRecommendedDeviceId(); /****************************************** The recommended master of this Mixer. ******************************************/ shared_ptr getLocalMasterMD() const; void setLocalMasterMD(QString&); /// get the actual MixSet MixSet& getMixSet(); /// DBUS oriented methods virtual void increaseVolume( const QString& mixdeviceID ); virtual void decreaseVolume( const QString& mixdeviceID ); /// Says if we are dynamic (e.g. widgets can come and go) virtual void setDynamic( bool dynamic = true ); virtual bool isDynamic(); static bool dynamicBackendsPresent(); static bool pulseaudioPresent(); - virtual bool moveStream( const QString id, const QString& destId ); + virtual bool moveStream(const QString &id, const QString &destId); + virtual QString currentStreamDevice(const QString &id) const; virtual int mediaPlay(QString id) { return _mixerBackend->mediaPlay(id); }; virtual int mediaPrev(QString id) { return _mixerBackend->mediaPrev(id); }; virtual int mediaNext(QString id) { return _mixerBackend->mediaNext(id); }; - void commitVolumeChange( shared_ptr md ); public slots: void readSetFromHWforceUpdate() const; virtual void setBalance(int balance); // sets the m_balance (see there) signals: void newBalance(Volume& ); void controlChanged(void); // TODO remove? protected: int m_balance; // from -100 (just left) to 100 (just right) static QList s_mixers; private: void setBalanceInternal(Volume& vol); void recreateId(); void increaseOrDecreaseVolume( const QString& mixdeviceID, bool decrease ); Mixer_Backend *_mixerBackend; QString _id; QString _masterDevicePK; static MasterControl _globalMasterCurrent; static MasterControl _globalMasterPreferred; bool m_dynamic; static bool m_beepOnVolumeChange; }; #endif diff --git a/gui/mdwmoveaction.cpp b/gui/mdwmoveaction.cpp deleted file mode 100644 index 2f81769c..00000000 --- a/gui/mdwmoveaction.cpp +++ /dev/null @@ -1,50 +0,0 @@ -/* - * KMix -- KDE's full featured mini mixer - * - * - * Copyright (C) 1996-2004 Christian Esken - * - * 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, write to the Free - * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - - -#include "mdwmoveaction.h" - -//KMix -#include "core/mixdevice.h" - -// Qt -#include -#include - -MDWMoveAction::MDWMoveAction(shared_ptr md, QObject *parent) - : QAction(parent), m_mixDevice(md) -{ - Q_ASSERT(md); - - setText(m_mixDevice->readableName()); - setIcon(QIcon::fromTheme(m_mixDevice->iconName())); - connect(this, SIGNAL(triggered(bool)), SLOT(triggered(bool))); -} - -MDWMoveAction::~MDWMoveAction() -{ -} - -void MDWMoveAction::triggered(bool checked) -{ - Q_UNUSED(checked); - emit moveRequest(m_mixDevice->id()); -} diff --git a/gui/mdwmoveaction.h b/gui/mdwmoveaction.h deleted file mode 100644 index b41ce1a1..00000000 --- a/gui/mdwmoveaction.h +++ /dev/null @@ -1,46 +0,0 @@ -//-*-C++-*- -/* - * KMix -- KDE's full featured mini mixer - * - * Copyright Christian Esken - * - * 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, write to the Free - * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -#ifndef MDWMoveAction_h -#define MDWMoveAction_h - -#include - -#include "core/mixdevice.h" - -class MDWMoveAction : public QAction -{ - Q_OBJECT - - public: - MDWMoveAction(shared_ptr md, QObject *parent); - ~MDWMoveAction(); - - signals: - void moveRequest(QString id); - - protected slots: - void triggered(bool checked); - - private: - shared_ptr m_mixDevice; -}; - -#endif diff --git a/gui/mdwslider.cpp b/gui/mdwslider.cpp index 07dcef68..6ed852ca 100644 --- a/gui/mdwslider.cpp +++ b/gui/mdwslider.cpp @@ -1,1241 +1,1252 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 1996-2007 Christian Esken * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "gui/mdwslider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "core/ControlManager.h" #include "core/mixer.h" #include "gui/guiprofile.h" #include "gui/volumeslider.h" #include "gui/viewbase.h" #include "gui/ksmallslider.h" #include "gui/verticaltext.h" -#include "gui/mdwmoveaction.h" #include "gui/toggletoolbutton.h" bool MDWSlider::debugMe = false; /** * MixDeviceWidget that represents a single mix device, including PopUp, muteLED, ... * * Used in KMix main window and DockWidget and PanelApplet. * It can be configured to include or exclude the captureLED and the muteLED. * The direction (horizontal, vertical) can be configured and whether it should * be "small" (uses KSmallSlider instead of a normal slider widget). * * Due to the many options, this is the most complicated MixDeviceWidget subclass. */ MDWSlider::MDWSlider(shared_ptr md, MixDeviceWidget::MDWFlags flags, ViewBase *view, ProfControl *pctl) : MixDeviceWidget(md, flags, view, pctl), m_linked(true), m_controlGrid(nullptr), m_controlIcon(nullptr), m_controlLabel(nullptr), m_muteButton(nullptr), m_captureButton(nullptr), m_mediaPlayButton(nullptr), m_controlButtonSize(QSize()), - _mdwMoveActions(new KActionCollection(this)), m_moveMenu(nullptr), m_sliderInWork(false), m_waitForSoundSetComplete(0) { //qCDebug(KMIX_LOG) << "for" << mixDevice()->readableName() << "flags" << MixDeviceWidget::flags(); createActions(); createWidgets(); createShortcutActions(); // Yes, this looks odd - monitor all events sent to myself by myself? // But it's so that wheel events over the MDWSlider background can be // handled by eventFilter() in the same way as wheel events over child // widgets. Each child widget apart from the sliders themselves also // also needs to have the event filter installed on it, because QWidget // by default ignores the wheel event and does not propagate it. installEventFilter(this); update(); } MDWSlider::~MDWSlider() { qDeleteAll(m_slidersPlayback); qDeleteAll(m_slidersCapture); } void MDWSlider::createActions() { // create actions (on _mdwActions, see MixDeviceWidget) KToggleAction *taction = _mdwActions->add( "stereo" ); - taction->setText( i18n("&Split Channels") ); + taction->setText( i18n("Split Channels") ); connect( taction, SIGNAL(triggered(bool)), SLOT(toggleStereoLinked()) ); // QAction *action; // if ( ! mixDevice()->mixer()->isDynamic() ) { // action = _mdwActions->add( "hide" ); // action->setText( i18n("&Hide") ); // connect( action, SIGNAL(triggered(bool)), SLOT(setDisabled(bool)) ); // } if( mixDevice()->hasMuteSwitch() ) { taction = _mdwActions->add( "mute" ); - taction->setText( i18n("&Muted") ); + taction->setText( i18n("Mute") ); connect( taction, SIGNAL(toggled(bool)), SLOT(toggleMuted()) ); } if( mixDevice()->captureVolume().hasSwitch() ) { taction = _mdwActions->add( "recsrc" ); - taction->setText( i18n("Captu&re") ); + taction->setText( i18n("Capture") ); connect( taction, SIGNAL(toggled(bool)), SLOT(toggleRecsrc()) ); } if( mixDevice()->isMovable() ) { - m_moveMenu = new QMenu( i18n("Mo&ve"), this); + m_moveMenu = new QMenu( i18n("Use Device"), this); connect( m_moveMenu, SIGNAL(aboutToShow()), SLOT(showMoveMenu()) ); } QAction* qaction = _mdwActions->addAction( "keys" ); qaction->setText( i18n("Channel Shortcuts...") ); connect( qaction, SIGNAL(triggered(bool)), SLOT(defineKeys()) ); } void MDWSlider::addGlobalShortcut(QAction* qaction, const QString& label, bool dynamicControl) { QString finalLabel(label); finalLabel += " - " + mixDevice()->readableName() + ", " + mixDevice()->mixer()->readableName(); qaction->setText(label); if (!dynamicControl) { // virtual / dynamic controls won't get shortcuts // #ifdef __GNUC__ // #warning GLOBAL SHORTCUTS ARE NOW ASSIGNED TO ALL CONTROLS, as enableGlobalShortcut(), has not been committed // #endif // b->enableGlobalShortcut(); // enableGlobalShortcut() is not there => use workaround KGlobalAccel::setGlobalShortcut(qaction, QKeySequence()); } } void MDWSlider::createShortcutActions() { bool dynamicControl = mixDevice()->mixer()->isDynamic(); // The following actions are for the "Configure Shortcuts" dialog /* PLEASE NOTE THAT global shortcuts are saved with the name as set with setName(), instead of their action name. This is a bug according to the thread "Global shortcuts are saved with their text-name and not their action-name - Bug?" on kcd. I work around this by using a text with setText() that is unique, but still readable to the user. */ QString actionSuffix = QString(" - %1, %2").arg( mixDevice()->readableName(), mixDevice()->mixer()->readableName() ); QAction *bi, *bd, *bm; // -1- INCREASE VOLUME SHORTCUT ----------------------------------------- bi = _mdwPopupActions->addAction( QString("Increase volume %1").arg( actionSuffix ) ); QString increaseVolumeName = i18n( "Increase Volume" ); addGlobalShortcut(bi, increaseVolumeName, dynamicControl); if ( ! dynamicControl ) connect( bi, SIGNAL(triggered(bool)), SLOT(increaseVolume()) ); // -2- DECREASE VOLUME SHORTCUT ----------------------------------------- bd = _mdwPopupActions->addAction( QString("Decrease volume %1").arg( actionSuffix ) ); QString decreaseVolumeName = i18n( "Decrease Volume" ); addGlobalShortcut(bd, decreaseVolumeName, dynamicControl); if ( ! dynamicControl ) connect(bd, SIGNAL(triggered(bool)), SLOT(decreaseVolume())); // -3- MUTE VOLUME SHORTCUT ----------------------------------------- bm = _mdwPopupActions->addAction( QString("Toggle mute %1").arg( actionSuffix ) ); QString muteVolumeName = i18n( "Toggle Mute" ); addGlobalShortcut(bm, muteVolumeName, dynamicControl); if ( ! dynamicControl ) connect( bm, SIGNAL(triggered(bool)), SLOT(toggleMuted()) ); } QSizePolicy MDWSlider::sizePolicy() const { if (orientation()==Qt::Vertical) { return QSizePolicy( QSizePolicy::Preferred, QSizePolicy::MinimumExpanding ); } else { return QSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred ); } } QSize MDWSlider::sizeHint() const { return QSize( 90, QWidget::sizeHint().height()); } /** * This method is a helper for users of this class who would like * to show multiple MDWSlider, and align the sliders. * It returns the "height" (if vertical) of this slider's label. * Warning: Line wraps are computed for a fixed size (100), this may be inaccurate in case, * the widgets have different sizes. */ int MDWSlider::labelExtentHint() const { if (m_controlLabel==nullptr) return (0); if (orientation()==Qt::Vertical) return (m_controlLabel->heightForWidth(m_controlLabel->minimumWidth())); else return (m_controlLabel->sizeHint().width()); } /** * If a label from another widget has more lines than this widget, then a spacer is added under the label */ void MDWSlider::setLabelExtent(int extent) { if (m_controlGrid==nullptr) return; if (orientation()==Qt::Vertical) m_controlGrid->setRowMinimumHeight(1, extent); else m_controlGrid->setColumnMinimumWidth(1, extent); } /** * Alignment helper */ bool MDWSlider::hasMuteButton() const { return (m_muteButton!=nullptr); } /** * See "hasMuteButton" */ bool MDWSlider::hasCaptureLED() const { return (m_captureButton!=nullptr); } void MDWSlider::guiAddCaptureButton(const QString &captureTooltipText) { m_captureButton = new ToggleToolButton("media-record", this); m_captureButton->setSmallSize(flags() & MixDeviceWidget::SmallSize); m_captureButton->installEventFilter(this); connect(m_captureButton, SIGNAL(clicked(bool)), this, SLOT(toggleRecsrc())); m_captureButton->setToolTip(captureTooltipText); } void MDWSlider::guiAddMuteButton(const QString &muteTooltipText) { m_muteButton = new ToggleToolButton("audio-volume-high", this); m_muteButton->setInactiveIcon("audio-volume-muted"); m_muteButton->setSmallSize(flags() & MixDeviceWidget::SmallSize); m_muteButton->installEventFilter(this); connect(m_muteButton, SIGNAL(clicked(bool)), this, SLOT(toggleMuted())); m_muteButton->setToolTip(muteTooltipText); } void MDWSlider::guiAddControlLabel(Qt::Alignment alignment, const QString &channelName) { m_controlLabel = new QLabel(channelName, this); m_controlLabel->setWordWrap(true); m_controlLabel->setAlignment(alignment); m_controlLabel->installEventFilter(this); } void MDWSlider::guiAddControlIcon(const QString &tooltipText) { m_controlIcon = new QLabel(this); ToggleToolButton::setIndicatorIcon(mixDevice()->iconName(), m_controlIcon, (flags() & MixDeviceWidget::SmallSize)); m_controlIcon->setToolTip(tooltipText); m_controlIcon->installEventFilter(this); } QWidget *MDWSlider::guiAddButtonSpacer() { if (hasMuteButton() || hasCaptureLED()) return (nullptr); // spacer not needed QWidget *buttonSpacer = new QWidget(this); if (orientation()==Qt::Vertical) // vertical sliders { buttonSpacer->setMinimumHeight(controlButtonSize().height()); buttonSpacer->setMaximumWidth(1); } else // horizontal sliders { buttonSpacer->setMinimumWidth(controlButtonSize().width()); buttonSpacer->setMaximumHeight(1); } buttonSpacer->installEventFilter(this); return (buttonSpacer); } QSize MDWSlider::controlButtonSize() { if (!m_controlButtonSize.isValid()) // not calculated yet { auto *buttonSpacer = new QToolButton(); ToggleToolButton::setIndicatorIcon("unknown", buttonSpacer, (flags() & MixDeviceWidget::SmallSize)); m_controlButtonSize = buttonSpacer->sizeHint(); qCDebug(KMIX_LOG) << m_controlButtonSize; delete buttonSpacer; } return (m_controlButtonSize); } /** * Creates all widgets : Icon, Label, Mute-Button, Slider(s) and Capture-Button. */ void MDWSlider::createWidgets() { const bool includePlayback = profileControl()->useSubcontrolPlayback(); const bool includeCapture = profileControl()->useSubcontrolCapture(); const bool wantsPlaybackSliders = includePlayback && (mixDevice()->playbackVolume().count()>0); const bool wantsCaptureSliders = includeCapture && (mixDevice()->captureVolume().count()>0); const bool wantsCaptureLED = includeCapture && (flags() & MixDeviceWidget::ShowCapture); const bool wantsMuteButton = includePlayback && (flags() & MixDeviceWidget::ShowMute); const MediaController *mediaController = mixDevice()->mediaController(); const bool wantsMediaControls = mediaController->hasControls(); const QString channelName = mixDevice()->readableName(); QString tooltipText = channelName; QString captureTooltipText = i18nc("%1=channel", "Capture/Uncapture %1", channelName); QString muteTooltipText = i18nc("%1=channel", "Mute/Unmute %1", channelName); if (flags() & MixDeviceWidget::ShowMixerName) { const QString mixerName = mixDevice()->mixer()->readableName(); tooltipText = i18nc("%1=device %2=channel", "%1\n%2", mixerName, tooltipText); captureTooltipText = i18nc("%1=device %2=channel", "%1\n%2", mixerName, captureTooltipText); muteTooltipText = i18nc("%1=device %2=channel", "%1\n%2", mixerName, muteTooltipText); } m_controlGrid = new QGridLayout(this); setLayout(m_controlGrid); QBoxLayout *volLayout; if (orientation()==Qt::Vertical) // vertical sliders { m_controlGrid->setContentsMargins(2, 0, 2, 0); const Qt::Alignment sliderAlign = Qt::AlignHCenter|Qt::AlignBottom; // Row 0: Control type icon guiAddControlIcon(tooltipText); m_controlGrid->addWidget(m_controlIcon, 0, 0, 1, -1, Qt::AlignHCenter|Qt::AlignTop); // Row 1: Device name label guiAddControlLabel(Qt::AlignHCenter, channelName); m_controlGrid->addWidget(m_controlLabel, 1, 0, 1, -1, Qt::AlignHCenter|Qt::AlignTop); // Row 2: Sliders int col = 0; // current column being filled int playbackCol = 0; // where these sliders ended up int captureCol = 1; // or default button column if none if (wantsPlaybackSliders) { volLayout = new QHBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'p', mixDevice()->playbackVolume(), m_slidersPlayback, tooltipText); m_controlGrid->addLayout(volLayout, 2, col); playbackCol = col; ++col; } if (wantsCaptureSliders) { volLayout = new QHBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'c', mixDevice()->captureVolume(), m_slidersCapture, tooltipText); m_controlGrid->addLayout(volLayout, 2, col); captureCol = col; ++col; } if (wantsMediaControls) { volLayout = new QHBoxLayout(); volLayout->setAlignment(sliderAlign); addMediaControls(volLayout); m_controlGrid->addLayout(volLayout, 2, col); } m_controlGrid->setRowStretch(2, 1); // sliders need the most space // Row 3: Control buttons if (wantsMuteButton && mixDevice()->hasMuteSwitch()) { guiAddMuteButton(muteTooltipText); m_controlGrid->addWidget(m_muteButton, 3, playbackCol, Qt::AlignHCenter|Qt::AlignTop); } if (wantsCaptureLED && mixDevice()->captureVolume().hasSwitch()) { guiAddCaptureButton(captureTooltipText); m_controlGrid->addWidget(m_captureButton, 3, captureCol, Qt::AlignHCenter|Qt::AlignTop); } // If nether a mute nor a capture button is present, then put a // dummy spacer button (in column 0, where the mute button would // normally go). This is to maintain the size of the slider // relative to others that do have one or both buttons. // // We have to do this, rather than setting a minimum height for row 3, // as in the case where it is needed row 3 will be empty and QGridLayout // ignores the minimum height set on it. QWidget *buttonSpacer = guiAddButtonSpacer(); if (buttonSpacer!=nullptr) m_controlGrid->addWidget(buttonSpacer, 3, 0); } else // horizontal sliders { const Qt::Alignment sliderAlign = Qt::AlignHCenter|Qt::AlignVCenter; // Column 0: Control type icon guiAddControlIcon(tooltipText); m_controlGrid->addWidget(m_controlIcon, 0, 0, -1, 1, Qt::AlignLeft|Qt::AlignVCenter); // Column 1: Device name label guiAddControlLabel(Qt::AlignLeft, channelName); m_controlGrid->addWidget(m_controlLabel, 0, 1, -1, 1, Qt::AlignLeft|Qt::AlignVCenter); // Column 2: Sliders int row = 0; // current row being filled int playbackRow = 0; // where these sliders ended up int captureRow = 1; // or default button row if none if (wantsPlaybackSliders) { volLayout = new QVBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'p', mixDevice()->playbackVolume(), m_slidersPlayback, tooltipText); m_controlGrid->addLayout(volLayout, row, 2); playbackRow = row; ++row; } if (wantsCaptureSliders) { volLayout = new QVBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'c', mixDevice()->captureVolume(), m_slidersCapture, tooltipText); m_controlGrid->addLayout(volLayout, row, 2); captureRow = row; ++row; } if (wantsMediaControls) { volLayout = new QVBoxLayout(); volLayout->setAlignment(sliderAlign); addMediaControls(volLayout); m_controlGrid->addLayout(volLayout, row, 2); } m_controlGrid->setColumnStretch(2, 1); // sliders need the most space // Column 3: Control buttons if (wantsMuteButton && mixDevice()->hasMuteSwitch()) { guiAddMuteButton(muteTooltipText); m_controlGrid->addWidget(m_muteButton, playbackRow, 3, Qt::AlignRight|Qt::AlignVCenter); } if (wantsCaptureLED && mixDevice()->captureVolume().hasSwitch()) { guiAddCaptureButton(captureTooltipText); m_controlGrid->addWidget(m_captureButton, captureRow, 3, Qt::AlignRight|Qt::AlignVCenter); } // Dummy spacer button QWidget *buttonSpacer = guiAddButtonSpacer(); if (buttonSpacer!=nullptr) m_controlGrid->addWidget(buttonSpacer, 0, 3); } const bool stereoLinked = !profileControl()->isSplit(); setStereoLinked( stereoLinked ); // Activate it explicitly in KDE3 because of PanelApplet/Kicker issues. // Not sure whether this is necessary 2 generations later. layout()->activate(); } QString MDWSlider::calculatePlaybackIcon(MediaController::PlayState playState) { QString mediaIconName; switch (playState) { case MediaController::PlayPlaying: // playing => show pause icon mediaIconName = "media-playback-pause"; break; case MediaController::PlayPaused: // stopped/paused => show play icon mediaIconName = "media-playback-start"; break; case MediaController::PlayStopped: // stopped/paused => show play icon mediaIconName = "media-playback-start"; break; default: // unknown => not good, probably result from player has not yet arrived => show a play button mediaIconName = "media-playback-start"; break; } return mediaIconName; } void MDWSlider::addMediaControls(QBoxLayout* volLayout) { MediaController *mediaController = mixDevice()->mediaController(); QBoxLayout *mediaLayout; if (orientation()==Qt::Vertical) mediaLayout = new QVBoxLayout(); else mediaLayout = new QHBoxLayout(); // QFrame* frame1 = new QFrame(this); // frame1->setFrameShape(QFrame::StyledPanel); QWidget* frame = this; // or frame1 mediaLayout->addStretch(); if (mediaController->hasMediaPrevControl()) { QToolButton *lbl = addMediaButton("media-skip-backward", mediaLayout, frame); connect(lbl, SIGNAL(clicked(bool)), this, SLOT(mediaPrev(bool))); } if (mediaController->hasMediaPlayControl()) { MediaController::PlayState playState = mediaController->getPlayState(); QString mediaIcon = calculatePlaybackIcon(playState); m_mediaPlayButton = addMediaButton(mediaIcon, mediaLayout, frame); connect(m_mediaPlayButton, SIGNAL(clicked(bool)), this, SLOT(mediaPlay(bool))); } if (mediaController->hasMediaNextControl()) { QToolButton *lbl = addMediaButton("media-skip-forward", mediaLayout, frame); connect(lbl, SIGNAL(clicked(bool)), this, SLOT(mediaNext(bool))); } mediaLayout->addStretch(); volLayout->addLayout(mediaLayout); } QToolButton* MDWSlider::addMediaButton(QString iconName, QLayout* layout, QWidget *parent) { QToolButton *lbl = new QToolButton(parent); lbl->setIconSize(QSize(IconSize(KIconLoader::Toolbar), IconSize(KIconLoader::Toolbar))); lbl->setAutoRaise(true); lbl->setCheckable(false); ToggleToolButton::setIndicatorIcon(iconName, lbl); layout->addWidget(lbl); return lbl; } /** * Updates the icon according to the data model. */ void MDWSlider::updateMediaButton() { if (m_mediaPlayButton == 0) return; // has no media button MediaController *mediaController = mixDevice()->mediaController(); QString mediaIconName = calculatePlaybackIcon(mediaController->getPlayState()); ToggleToolButton::setIndicatorIcon(mediaIconName, m_mediaPlayButton); } void MDWSlider::mediaPrev(bool) { mixDevice()->mediaPrev(); } void MDWSlider::mediaNext(bool) { mixDevice()->mediaNext(); } void MDWSlider::mediaPlay(bool) { mixDevice()->mediaPlay(); } static QWidget *createLabel(QWidget *parent, const QString &label, Qt::Orientation orient, bool small) { QFont qf; qf.setPointSize(8); QWidget *labelWidget; if (orient == Qt::Horizontal) { auto *ql = new QLabel(label, parent); if (small) ql->setFont(qf); labelWidget = ql; } else { auto *vt = new VerticalText(parent, label); if (small) vt->setFont(qf); labelWidget = vt; } return (labelWidget); } void MDWSlider::addSliders( QBoxLayout *volLayout, char type, Volume& vol, QList& ref_sliders, QString tooltipText) { const int minSliderSize = fontMetrics().height() * 10; long minvol = vol.minVolume(); long maxvol = vol.maxVolume(); QMap vols = vol.getVolumes(); foreach (VolumeChannel vc, vols ) { //qCDebug(KMIX_LOG) << "Add label to " << vc.chid << ": " << Volume::channelNameReadable(vc.chid); QWidget *subcontrolLabel; QString subcontrolTranslation; if ( type == 'c' ) subcontrolTranslation += i18n("Capture") + ' '; subcontrolTranslation += Volume::channelNameReadable(vc.chid); subcontrolLabel = createLabel(this, subcontrolTranslation, orientation(), true); volLayout->addWidget(subcontrolLabel); QAbstractSlider* slider; if (flags() & MixDeviceWidget::SmallSize) { slider = new KSmallSlider( minvol, maxvol, (maxvol-minvol+1) / Volume::VOLUME_PAGESTEP_DIVISOR, vol.getVolume( vc.chid ), orientation(), this ); } // small else { slider = new VolumeSlider(orientation(), this); slider->setMinimum(minvol); slider->setMaximum(maxvol); slider->setPageStep(maxvol / Volume::VOLUME_PAGESTEP_DIVISOR); slider->setValue( vol.getVolume( vc.chid ) ); volumeValues.push_back( vol.getVolume( vc.chid ) ); extraData(slider).setSubcontrolLabel(subcontrolLabel); if (orientation()==Qt::Vertical) slider->setMinimumHeight(minSliderSize); else slider->setMinimumWidth(minSliderSize); if ( !profileControl()->getBackgroundColor().isEmpty() ) { slider->setStyleSheet("QSlider { background-color: " + profileControl()->getBackgroundColor() + " }"); } } // not small extraData(slider).setChid(vc.chid); // slider->installEventFilter( this ); if ( type == 'p' ) { slider->setToolTip( tooltipText ); } else { QString captureTip( i18n( "%1 (capture)", tooltipText ) ); slider->setToolTip( captureTip ); } volLayout->addWidget( slider ); // add to layout ref_sliders.append ( slider ); // add to list //ref_slidersChids.append(vc.chid); connect( slider, SIGNAL(valueChanged(int)), SLOT(volumeChange(int)) ); connect( slider, SIGNAL(sliderPressed()), SLOT(sliderPressed()) ); connect( slider, SIGNAL(sliderReleased()), SLOT(sliderReleased()) ); } // for all channels of this device } /** * Return the VolumeSliderExtraData from either VolumeSlider or KSmallSlider. * You MUST extend this method, should you decide to add more Slider Widget classes. * * @param slider * @return */ VolumeSliderExtraData& MDWSlider::extraData(QAbstractSlider *slider) { VolumeSlider* sl = qobject_cast(slider); if ( sl ) return sl->extraData; KSmallSlider* sl2 = qobject_cast(slider); return sl2->extraData; } void MDWSlider::sliderPressed() { m_sliderInWork = true; } void MDWSlider::sliderReleased() { m_sliderInWork = false; } QString MDWSlider::iconName() { return mixDevice()->iconName(); } void MDWSlider::toggleStereoLinked() { setStereoLinked( !isStereoLinked() ); } void MDWSlider::setStereoLinked(bool value) { m_linked = value; int overallSlidersToShow = 0; if ( ! m_slidersPlayback.isEmpty() ) overallSlidersToShow += ( m_linked ? 1 : m_slidersPlayback.count() ); if ( ! m_slidersCapture.isEmpty() ) overallSlidersToShow += ( m_linked ? 1 : m_slidersCapture.count() ); bool showSubcontrolLabels = (overallSlidersToShow >= 2); setStereoLinkedInternal(m_slidersPlayback, showSubcontrolLabels); setStereoLinkedInternal(m_slidersCapture , showSubcontrolLabels); update(); // Call update(), so that the sliders can adjust EITHER to the individual values OR the average value. } void MDWSlider::setStereoLinkedInternal(QList& ref_sliders, bool showSubcontrolLabels) { if ( ref_sliders.isEmpty()) return; bool first = true; foreach ( QAbstractSlider* slider1, ref_sliders ) { slider1->setVisible(!m_linked || first); // One slider (the 1st) is always shown extraData(slider1).getSubcontrolLabel()->setVisible(!m_linked && showSubcontrolLabels); // (*) first = false; /* (*) cesken: I have excluded the "|| first" check because the text would not be nice: * It would be "Left" or "Capture Left", while it should be "Playback" and "Capture" in the "linked" case. * * But the only affected situation is when we have playback AND capture on the same control, where we show no label. * It would be nice to put at least a "Capture" label on the capture subcontrol instead. * To achieve this we would need to exchange the Text on the first capture subcontrol dynamically. This can * be done, but I'll leave this open for now. */ } // Redo the tickmarks to last slider in the slider list. // The implementation is not obvious, so lets explain: // We ALWAYS have tickmarks on the LAST slider. Sometimes the slider is not shown, and then we just don't bother. // a) So, if the last slider has tickmarks, we can always call setTicks( true ). // b) if the last slider has NO tickmarks, there ae no tickmarks at all, and we don't need to redo the tickmarks. QSlider* slider = qobject_cast( ref_sliders.last() ); if( slider && slider->tickPosition() != QSlider::NoTicks) setTicks( true ); } void MDWSlider::setLabeled(bool value) { if ( m_controlLabel != 0) m_controlLabel->setVisible(value); layout()->activate(); } void MDWSlider::setTicks( bool value ) { if (m_slidersPlayback.count() != 0) setTicksInternal(m_slidersPlayback, value); if (m_slidersCapture.count() != 0) setTicksInternal(m_slidersCapture, value); } /** * Enables or disables tickmarks * Please note that always only the first and last slider have tickmarks. */ void MDWSlider::setTicksInternal(QList& ref_sliders, bool ticks) { VolumeSlider* slider = qobject_cast( ref_sliders[0]); if (slider == 0 ) return; // Ticks are only in VolumeSlider, but not in KSmallslider if( ticks ) { if( isStereoLinked() ) slider->setTickPosition( QSlider::TicksRight ); else { slider->setTickPosition( QSlider::NoTicks ); slider = qobject_cast(ref_sliders.last()); slider->setTickPosition( QSlider::TicksLeft ); } } else { slider->setTickPosition( QSlider::NoTicks ); slider = qobject_cast(ref_sliders.last()); slider->setTickPosition( QSlider::NoTicks ); } } void MDWSlider::setIcons(bool value) { if ( m_controlIcon != 0 ) { if ( ( !m_controlIcon->isHidden() ) !=value ) { if (value) m_controlIcon->show(); else m_controlIcon->hide(); layout()->activate(); } } // if it has an icon } void MDWSlider::setColors( QColor high, QColor low, QColor back ) { for( int i=0; i(slider); if ( smallSlider ) smallSlider->setColors( high, low, back ); } for( int i=0; i(slider); if ( smallSlider ) smallSlider->setColors( high, low, back ); } } void MDWSlider::setMutedColors( QColor high, QColor low, QColor back ) { for( int i=0; i(slider); if ( smallSlider ) smallSlider->setGrayColors( high, low, back ); } for( int i=0; i(slider); if ( smallSlider ) smallSlider->setGrayColors( high, low, back ); } } /** This slot is called, when a user has changed the volume via the KMix Slider. */ void MDWSlider::volumeChange( int ) { // if ( mixDevice()->id() == "Headphone:0" ) // { // qCDebug(KMIX_LOG) << "headphone bug"; // } if (!m_slidersPlayback.isEmpty()) { ++m_waitForSoundSetComplete; volumeValues.push_back(m_slidersPlayback.first()->value()); volumeChangeInternal(mixDevice()->playbackVolume(), m_slidersPlayback); } if (!m_slidersCapture.isEmpty()) { volumeChangeInternal(mixDevice()->captureVolume(), m_slidersCapture); } QSignalBlocker blocker(view()); mixDevice()->mixer()->commitVolumeChange(mixDevice()); } void MDWSlider::volumeChangeInternal(Volume& vol, QList& ref_sliders) { if (isStereoLinked()) { QAbstractSlider* firstSlider = ref_sliders.first(); mixDevice()->setMuted(false); vol.setAllVolumes(firstSlider->value()); } else { for (int i = 0; i < ref_sliders.count(); i++) { if (mixDevice()->isMuted()) { // changing from muted state: unmute (the "if" above is actually superfluous) mixDevice()->setMuted(false); } QAbstractSlider *sliderWidget = ref_sliders[i]; vol.setVolume(extraData(sliderWidget).getChid(), sliderWidget->value()); } // iterate over all sliders } } /** This slot is called, when a user has clicked the recsrc button. Also it is called by any other associated QAction like the context menu. */ void MDWSlider::toggleRecsrc() { setRecsrc( !mixDevice()->isRecSource() ); } void MDWSlider::setRecsrc(bool value) { if ( mixDevice()->captureVolume().hasSwitch() ) { mixDevice()->setRecSource( value ); mixDevice()->mixer()->commitVolumeChange( mixDevice() ); } } /** This slot is called, when a user has clicked the mute button. Also it is called by any other associated QAction like the context menu. */ void MDWSlider::toggleMuted() { setMuted( !mixDevice()->isMuted() ); } void MDWSlider::setMuted(bool value) { if ( mixDevice()->hasMuteSwitch() ) { mixDevice()->setMuted( value ); mixDevice()->mixer()->commitVolumeChange(mixDevice()); } } void MDWSlider::setDisabled( bool hide ) { emit guiVisibilityChange(this, !hide); } /** * This slot is called on a Keyboard Shortcut event, except for the XF86Audio* shortcuts which are handled by the * KMixWindow class. So for 99.9% of all users, this method is never called. */ void MDWSlider::increaseVolume() { increaseOrDecreaseVolume(false, Volume::Both); } /** * This slot is called on a Keyboard Shortcut event, except for the XF86Audio* shortcuts which hare handled by the * KMixWindow class. So for 99.9% of all users, this method is never called. */ void MDWSlider::decreaseVolume() { increaseOrDecreaseVolume(true, Volume::Both); } /** * Increase or decrease all playback and capture channels of the given control. * This method is very similar to Mixer::increaseOrDecreaseVolume(), but it will * auto-unmute on increase. * * @param mixdeviceID The control name * @param decrease true for decrease. false for increase */ void MDWSlider::increaseOrDecreaseVolume(bool decrease, Volume::VolumeTypeFlag volumeType) { mixDevice()->increaseOrDecreaseVolume(decrease, volumeType); // I should possibly not block, as the changes that come back from the Soundcard // will be ignored (e.g. because of capture groups) // qCDebug(KMIX_LOG) << "MDWSlider is blocking signals for " << view()->id(); // bool oldViewBlockSignalState = view()->blockSignals(true); mixDevice()->mixer()->commitVolumeChange(mixDevice()); // qCDebug(KMIX_LOG) << "MDWSlider is unblocking signals for " << view()->id(); // view()->blockSignals(oldViewBlockSignalState); } -void MDWSlider::moveStreamAutomatic() -{ - mixDevice()->mixer()->moveStream(mixDevice()->id(), ""); -} -void MDWSlider::moveStream(QString destId) +/** + * Must be called by the triggered(bool) signal from a QAction. + */ +void MDWSlider::moveStream(bool checked) { - mixDevice()->mixer()->moveStream(mixDevice()->id(), destId); + Q_UNUSED(checked); + QAction *act = qobject_cast(sender()); + Q_ASSERT(act!=nullptr); + const QString destId = act->data().toString(); + mixDevice()->mixer()->moveStream(mixDevice()->id(), destId); } + /** * This is called whenever there are volume updates pending from the hardware for this MDW. */ void MDWSlider::update() { // bool debugMe = (mixDevice()->id() == "PCM:0" ); // if (debugMe) qCDebug(KMIX_LOG) << "The update() PCM:0 playback state" << mixDevice()->isMuted() // << ", vol=" << mixDevice()->playbackVolume().getAvgVolumePercent(Volume::MALL); if ( m_slidersPlayback.count() != 0 || mixDevice()->hasMuteSwitch() ) updateInternal(mixDevice()->playbackVolume(), m_slidersPlayback, mixDevice()->isMuted() ); if ( m_slidersCapture.count() != 0 || mixDevice()->captureVolume().hasSwitch() ) updateInternal(mixDevice()->captureVolume(), m_slidersCapture, mixDevice()->isNotRecSource() ); if (m_controlLabel!=nullptr) { QLabel *l; VerticalText *v; if ((l = dynamic_cast(m_controlLabel))) l->setText(mixDevice()->readableName()); else if ((v = dynamic_cast(m_controlLabel))) v->setText(mixDevice()->readableName()); } updateAccesability(); } /** * * @param vol * @param ref_sliders * @param muted Future directions: passing "muted" should not be necessary any longer - due to getVolumeForGUI() */ void MDWSlider::updateInternal(Volume& vol, QList& ref_sliders, bool muted) { // bool debugMe = (mixDevice()->id() == "PCM:0" ); // if (debugMe) // { // qCDebug(KMIX_LOG) << "The updateInternal() PCM:0 playback state" << mixDevice()->isMuted() // << ", vol=" << mixDevice()->playbackVolume().getAvgVolumePercent(Volume::MALL); // } for (int i = 0; i-1 && --m_waitForSoundSetComplete<1) { m_waitForSoundSetComplete = 0; volumeValues.removeAt(volume_index); if (!m_sliderInWork) slider->setValue(useVolume); } else if (!m_sliderInWork && m_waitForSoundSetComplete<1) { slider->setValue(useVolume); } // --- Avoid feedback loops END ----------------- KSmallSlider *smallSlider = qobject_cast(slider); if (smallSlider!=nullptr) // faster than QObject::inherits() { smallSlider->setGray(mixDevice()->isMuted()); } } // for all sliders // update mute state if (m_muteButton!=nullptr) { QSignalBlocker blocker(m_muteButton); m_muteButton->setActive(!mixDevice()->isMuted()); } // update capture state if (m_captureButton!=nullptr) { QSignalBlocker blocker(m_captureButton); m_captureButton->setActive(mixDevice()->isRecSource()); } } #ifndef QT_NO_ACCESSIBILITY void MDWSlider::updateAccesability() { if (m_linked) { if (!m_slidersPlayback.isEmpty()) m_slidersPlayback[0]->setAccessibleName(m_slidersPlayback[0]->toolTip()); if (!m_slidersCapture.isEmpty()) m_slidersCapture[0]->setAccessibleName(m_slidersCapture[0]->toolTip()); } else { QList vols = mixDevice()->playbackVolume().getVolumes().values(); foreach (QAbstractSlider *slider, m_slidersPlayback) { slider->setAccessibleName(slider->toolTip()+ " (" +Volume::channelNameReadable(vols.first().chid)+')'); vols.pop_front(); } vols = mixDevice()->captureVolume().getVolumes().values(); foreach (QAbstractSlider *slider, m_slidersCapture) { slider->setAccessibleName(slider->toolTip()+ " (" +Volume::channelNameReadable(vols.first().chid)+')'); vols.pop_front(); } } } #endif void MDWSlider::showContextMenu(const QPoint &pos) { if (view()==nullptr) return; QMenu *menu = view()->getPopup(); menu->addSection( SmallIcon( "kmix" ), mixDevice()->readableName() ); if (m_moveMenu) { MixSet *ms = mixDevice()->moveDestinationMixSet(); Q_ASSERT(ms!=nullptr); - m_moveMenu->setEnabled((ms->count() > 1)); + m_moveMenu->setEnabled(ms->count()>1); + // The "Event Sounds" stream cannot be moved at present. This is because + // Mixer_PULSE::moveStream() does not record the stream ID in the + // output stream list and hence cannot get its PulseAudio stream index. + // I don't know whether this is a design decision or a PA limitation. + if (mixDevice()->id().endsWith(":event")) m_moveMenu->setEnabled(false); + menu->addMenu( m_moveMenu ); } if ( m_slidersPlayback.count()>1 || m_slidersCapture.count()>1) { KToggleAction *stereo = qobject_cast(_mdwActions->action("stereo")); if (stereo!=nullptr) { QSignalBlocker blocker(stereo); stereo->setChecked(!isStereoLinked()); menu->addAction( stereo ); } } if ( mixDevice()->captureVolume().hasSwitch() ) { KToggleAction *ta = qobject_cast(_mdwActions->action("recsrc")); if (ta!=nullptr) { QSignalBlocker blocker(ta); ta->setChecked( mixDevice()->isRecSource() ); menu->addAction( ta ); } } if ( mixDevice()->hasMuteSwitch() ) { KToggleAction *ta = qobject_cast(_mdwActions->action("mute")); if (ta!=nullptr) { QSignalBlocker blocker(ta); ta->setChecked( mixDevice()->isMuted() ); menu->addAction( ta ); } } // QAction *a = _mdwActions->action( "hide" ); // if ( a ) // menu->addAction( a ); QAction *b = _mdwActions->action( "keys" ); if (b!=nullptr) { menu->addSeparator(); menu->addAction(b); } menu->popup(pos); } void MDWSlider::showMoveMenu() { - MixSet *ms = mixDevice()->moveDestinationMixSet(); + const MixSet *ms = mixDevice()->moveDestinationMixSet(); Q_ASSERT(ms!=nullptr); - _mdwMoveActions->clear(); + const QString cur = mixDevice()->mixer()->currentStreamDevice(mixDevice()->id()); + + // There is no need to keep a record of the actions (in a KActionCollection + // or otherwise); QMenu::clear() will delete them as long as they are owned + // by the menu. m_moveMenu->clear(); - // Default - QAction *a = new QAction(_mdwMoveActions); - a->setText( i18n("Automatic According to Category") ); - _mdwMoveActions->addAction( QString("moveautomatic"), a); - connect(a, SIGNAL(triggered(bool)), SLOT(moveStreamAutomatic()), Qt::QueuedConnection); - m_moveMenu->addAction( a ); + // Default action + QAction *act = new QAction(i18n("Automatic (according to category)"), m_moveMenu); + act->setData(QString()); + connect(act, &QAction::triggered, this, &MDWSlider::moveStream, Qt::QueuedConnection); + m_moveMenu->addAction(act); - a = new QAction(_mdwMoveActions); - a->setSeparator(true); - _mdwMoveActions->addAction( QString("-"), a); + m_moveMenu->addSeparator(); - m_moveMenu->addAction( a ); - foreach (shared_ptr md, *ms) + // Device actions + foreach (const shared_ptr md, *ms) { - a = new MDWMoveAction(md, _mdwMoveActions); - _mdwMoveActions->addAction( QString("moveto") + md->id(), a); - connect(a, SIGNAL(moveRequest(QString)), SLOT(moveStream(QString)), Qt::QueuedConnection); - m_moveMenu->addAction( a ); + act = new QAction(QIcon::fromTheme(md->iconName()), md->readableName(), m_moveMenu); + act->setData(md->id()); + act->setCheckable(true); + if (md->id()==cur) act->setChecked(true); + connect(act, &QAction::triggered, this, &MDWSlider::moveStream, Qt::QueuedConnection); + m_moveMenu->addAction(act); } } /** * An event filter for the various widgets making up this control. * * Redirect all wheel events to the main slider, so that they will be * handled consistently regardless of where the pointer actually is. */ bool MDWSlider::eventFilter(QObject *obj, QEvent *ev) { if (ev->type()!=QEvent::Wheel) return (QWidget::eventFilter(obj, ev)); // only want wheel events if (!ev->spontaneous()) return (false); // avoid recursion on slider QAbstractSlider *slider = qobject_cast(obj); if (slider!=nullptr) // event is over a slider { // Do nothing in this case. No event filter is installed // on a slider, and it will handle the wheel event itself. qCWarning(KMIX_LOG) << "unexpected wheel event on slider" << slider; return (false); } // Mouse is not over a slider. Find the principal slider (the first // playback control if there are any, otherwise the first capture // control if any) and redirect the event to that. if (!m_slidersPlayback.isEmpty()) slider = m_slidersPlayback.first(); else if (!m_slidersCapture.isEmpty()) slider = m_slidersCapture.first(); else slider = nullptr; if (slider!=nullptr) { //qCDebug(KMIX_LOG) << "identified for slider" << slider; QCoreApplication::sendEvent(slider, ev); } return (true); // wheel event handled } diff --git a/gui/mdwslider.h b/gui/mdwslider.h index 6f7f3850..05886731 100644 --- a/gui/mdwslider.h +++ b/gui/mdwslider.h @@ -1,160 +1,158 @@ //-*-C++-*- /* * KMix -- KDE's full featured mini mixer * * * Copyright Chrisitan Esken * * 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, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MDWSLIDER_H #define MDWSLIDER_H #include #include #include "gui/volumeslider.h" #include "gui/mixdevicewidget.h" #include "core/volume.h" class QBoxLayout; class QGridLayout; class QToolButton; class QLabel; class QMenu; class MixDevice; class VerticalText; class ViewBase; class ToggleToolButton; class MDWSlider : public MixDeviceWidget { Q_OBJECT public: MDWSlider(shared_ptr md, MixDeviceWidget::MDWFlags flags, ViewBase *view, ProfControl *pctl = nullptr); virtual ~MDWSlider(); enum LabelType { LT_ALL, LT_FIRST_CAPTURE, LT_NONE }; void addActionToPopup( QAction *action ); void createActions(); void createShortcutActions(); // GUI bool isStereoLinked() const Q_DECL_OVERRIDE { return m_linked; } void setStereoLinked( bool value ) Q_DECL_OVERRIDE; void setLabeled( bool value ) Q_DECL_OVERRIDE; void setTicks( bool ticks ) Q_DECL_OVERRIDE; void setIcons( bool value ) Q_DECL_OVERRIDE; QToolButton* addMediaButton(QString iconName, QLayout* layout, QWidget *parent); void updateMediaButton(); void setColors( QColor high, QColor low, QColor back ) Q_DECL_OVERRIDE; void setMutedColors( QColor high, QColor low, QColor back ) Q_DECL_OVERRIDE; bool eventFilter(QObject *obj, QEvent *ev) Q_DECL_OVERRIDE; QString iconName(); // Layout QSizePolicy sizePolicy() const; QSize sizeHint() const Q_DECL_OVERRIDE; int labelExtentHint() const Q_DECL_OVERRIDE; void setLabelExtent(int extent) Q_DECL_OVERRIDE; bool hasMuteButton() const; bool hasCaptureLED() const; static bool debugMe; public slots: void toggleRecsrc(); void toggleMuted(); void toggleStereoLinked(); void setDisabled( bool value ) Q_DECL_OVERRIDE; void update() Q_DECL_OVERRIDE; void showMoveMenu(); void showContextMenu( const QPoint &pos = QCursor::pos() ) Q_DECL_OVERRIDE; void increaseOrDecreaseVolume(bool arg1, Volume::VolumeTypeFlag volumeType); VolumeSliderExtraData& extraData(QAbstractSlider *slider); void addMediaControls(QBoxLayout* arg1); private slots: void setRecsrc(bool value); void setMuted(bool value); void volumeChange( int ); void sliderPressed(); void sliderReleased(); void increaseVolume(); void decreaseVolume(); - void moveStreamAutomatic(); - void moveStream( QString destId ); - void mediaPlay(bool); void mediaNext(bool); void mediaPrev(bool); + void moveStream(bool checked); + private: void createWidgets(); void addSliders( QBoxLayout *volLayout, char type, Volume& vol, QList& ref_sliders, QString tooltipText ); // Methods that are called two times from a wrapper. Once for playabck, once for capture void setStereoLinkedInternal( QList< QAbstractSlider* >& ref_sliders, bool showSubcontrolLabels); void setTicksInternal( QList< QAbstractSlider* >& ref_sliders, bool ticks ); void volumeChangeInternal(Volume& vol, QList< QAbstractSlider* >& ref_sliders ); void updateInternal(Volume& vol, QList< QAbstractSlider* >& ref_sliders, bool muted); #ifndef QT_NO_ACCESSIBILITY void updateAccesability(); #endif QString calculatePlaybackIcon(MediaController::PlayState playState); QWidget *guiAddButtonSpacer(); void guiAddCaptureButton(const QString &captureTooltipText); void guiAddMuteButton(const QString &muteTooltipText); void guiAddControlIcon(const QString &tooltipText); void guiAddControlLabel(Qt::Alignment alignment, const QString &channelName); void addGlobalShortcut(QAction* action, const QString& label, bool dynamicControl); QSize controlButtonSize(); bool m_linked; QGridLayout *m_controlGrid; QLabel *m_controlIcon; QLabel *m_controlLabel; // is either QLabel or VerticalText ToggleToolButton *m_muteButton; ToggleToolButton *m_captureButton; QToolButton *m_mediaPlayButton; QSize m_controlButtonSize; - KActionCollection* _mdwMoveActions; QMenu *m_moveMenu; QList m_slidersPlayback; QList m_slidersCapture; bool m_sliderInWork; int m_waitForSoundSetComplete; QList volumeValues; }; #endif