diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) -set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) include(GenerateExportHeader) @@ -65,13 +65,23 @@ find_package(KF5Codecs ${KF5_DEP_VERSION} REQUIRED) find_package(KF5CoreAddons ${KF5_DEP_VERSION} REQUIRED) -find_package(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) -set_package_properties(Phonon4Qt5 PROPERTIES - DESCRIPTION "Qt-based audio library" - TYPE REQUIRED - PURPOSE "Required to build audio notification support") -if (Phonon4Qt5_FOUND) - add_definitions(-DHAVE_PHONON4QT5) +find_package(Canberra) +set_package_properties(Canberra PROPERTIES DESCRIPTION "Library for generating event sounds" + PURPOSE "Needed to build audio notification support" + URL "http://0pointer.de/lennart/projects/libcanberra" + TYPE OPTIONAL) +if (CANBERRA_FOUND) + add_definitions(-DHAVE_CANBERRA) +else() + find_package(Phonon4Qt5 4.6.60 NO_MODULE) + set_package_properties(Phonon4Qt5 PROPERTIES + DESCRIPTION "Qt-based audio library" + # This is REQUIRED since you cannot tell CMake "either one of those two optional ones are required" + TYPE REQUIRED + PURPOSE "Needed to build audio notification support when Canberra isn't available") + if (Phonon4Qt5_FOUND) + add_definitions(-DHAVE_PHONON4QT5) + endif() endif() remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) diff --git a/cmake/modules/FindCanberra.cmake b/cmake/modules/FindCanberra.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindCanberra.cmake @@ -0,0 +1,50 @@ +# - Find libcanberra's libraries and headers. +# This module defines the following variables: +# +# CANBERRA_FOUND - true if libcanberra was found +# CANBERRA_LIBRARIES - libcanberra libraries to link against +# CANBERRA_INCLUDE_DIRS - include path for libcanberra +# +# Copyright (c) 2012 Raphael Kubo da Costa +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the University nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +find_package(PkgConfig) +pkg_check_modules(PC_CANBERRA libcanberra) + +find_library(CANBERRA_LIBRARIES + NAMES canberra + HINTS ${PC_CANBERRA_LIBRARY_DIRS} ${PC_CANBERRA_LIBDIR} +) + +find_path(CANBERRA_INCLUDE_DIRS + NAMES canberra.h + HINTS ${PC_CANBERRA_INCLUDE_DIRS} ${PC_CANBERRA_INCLUDEDIR} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Canberra REQUIRED_VARS CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS) + +mark_as_advanced(CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,9 @@ if (Phonon4Qt5_FOUND) include_directories(${PHONON_INCLUDE_DIR}) endif() +if (CANBERRA_FOUND) + include_directories(${CANBERRA_INCLUDE_DIRS}) +endif() ecm_create_qm_loader(knotifications_QM_LOADER knotifications5_qt) @@ -27,9 +30,12 @@ ecm_qt_declare_logging_category(knotifications_SRCS HEADER debug_p.h IDENTIFIER LOG_KNOTIFICATIONS CATEGORY_NAME org.kde.knotifications) -if (Phonon4Qt5_FOUND) +if (CANBERRA_FOUND) + set(knotifications_SRCS ${knotifications_SRCS} + notifybyaudio_canberra.cpp) +elseif (Phonon4Qt5_FOUND) set(knotifications_SRCS ${knotifications_SRCS} - notifybyaudio.cpp) + notifybyaudio_phonon.cpp) endif() if (Qt5TextToSpeech_FOUND) @@ -82,6 +88,11 @@ ${PHONON_LIBRARIES}) endif() +if (CANBERRA_FOUND) + target_link_libraries(KF5Notifications PRIVATE + ${CANBERRA_LIBRARIES}) +endif() + if (Qt5TextToSpeech_FOUND) target_link_libraries(KF5Notifications PRIVATE Qt5::TextToSpeech) endif() diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp --- a/src/knotificationmanager.cpp +++ b/src/knotificationmanager.cpp @@ -41,8 +41,10 @@ #include "notifybyflatpak.h" #include "debug_p.h" -#ifdef HAVE_PHONON4QT5 -#include "notifybyaudio.h" +#if defined(HAVE_CANBERRA) +#include "notifybyaudio_canberra.h" +#elif defined(HAVE_PHONON4QT5) +#include "notifybyaudio_phonon.h" #endif #ifdef HAVE_SPEECH @@ -137,7 +139,7 @@ plugin = new NotifyByTaskbar(this); addPlugin(plugin); } else if (action == QLatin1String("Sound")) { -#ifdef HAVE_PHONON4QT5 +#if defined(HAVE_PHONON4QT5) || defined(HAVE_CANBERRA) plugin = new NotifyByAudio(this); addPlugin(plugin); #endif diff --git a/src/notifybyaudio.h b/src/notifybyaudio_canberra.h rename from src/notifybyaudio.h rename to src/notifybyaudio_canberra.h --- a/src/notifybyaudio.h +++ b/src/notifybyaudio_canberra.h @@ -25,16 +25,13 @@ #include "knotificationplugin.h" -#include - -namespace Phonon { -// class MediaObject; -class MediaSource; -class AudioOutput; -} +#include +#include class KNotification; +struct ca_context; + class NotifyByAudio : public KNotificationPlugin { Q_OBJECT @@ -48,17 +45,24 @@ void close(KNotification *notification) override; private Q_SLOTS: - void onAudioFinished(); - void onAudioSourceChanged(const Phonon::MediaSource &source); - void stateChanged(Phonon::State newState, Phonon::State oldState); - + void finishCallback(uint32_t id, + int error_code); private: - void finishNotification(KNotification *notification, Phonon::MediaObject *m); + static void ca_finish_callback(ca_context *c, + uint32_t id, + int error_code, + void *userdata); + + void finishNotification(KNotification *notification, quint32 id); + + bool playSound(quint32 id, const QUrl &url); - QList m_reusablePhonons; - QHash m_notifications; - Phonon::AudioOutput *m_audioOutput; + ca_context *m_context = nullptr; + quint32 m_currentId = 0; + QHash m_notifications; + // in case we loop we store the URL for the notification to be able to replay it + QHash m_loopSoundUrls; }; #endif diff --git a/src/notifybyaudio_canberra.cpp b/src/notifybyaudio_canberra.cpp new file mode 100644 --- /dev/null +++ b/src/notifybyaudio_canberra.cpp @@ -0,0 +1,190 @@ +/* This file is part of the KDE libraries + Copyright (C) 2014-2015 by Martin Klapetek + Copyright (C) 2018 Kai Uwe Broulik + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . + +*/ + +#include "notifybyaudio_canberra.h" +#include "debug_p.h" + +#include +#include +#include +#include +#include + +#include "knotifyconfig.h" +#include "knotification.h" + +#include + +NotifyByAudio::NotifyByAudio(QObject *parent) + : KNotificationPlugin(parent) +{ + qRegisterMetaType("uint32_t"); + + int ret = ca_context_create(&m_context); + if (ret != CA_SUCCESS) { + qCWarning(LOG_KNOTIFICATIONS) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret); + m_context = nullptr; + return; + } + + ret = ca_context_change_props(m_context, + CA_PROP_APPLICATION_NAME, qUtf8Printable(qApp->applicationDisplayName()), + CA_PROP_APPLICATION_ID, qUtf8Printable(qApp->desktopFileName()), + CA_PROP_APPLICATION_ICON_NAME, qUtf8Printable(qApp->windowIcon().name()), + nullptr); + if (ret != CA_SUCCESS) { + qCWarning(LOG_KNOTIFICATIONS) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret); + } +} + +NotifyByAudio::~NotifyByAudio() +{ + if (m_context) { + ca_context_destroy(m_context); + } + m_context = nullptr; +} + +void NotifyByAudio::notify(KNotification *notification, KNotifyConfig *config) +{ + const QString soundFilename = config->readEntry(QStringLiteral("Sound")); + if (soundFilename.isEmpty()) { + qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but no sound file provided in notifyrc file, aborting audio notification"; + + finish(notification); + return; + } + + QUrl soundURL; + const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + for (const QString &dataLocation : dataLocations) { + soundURL = QUrl::fromUserInput(soundFilename, + dataLocation + QStringLiteral("/sounds"), + QUrl::AssumeLocalFile); + if (soundURL.isLocalFile() && QFileInfo::exists(soundURL.toLocalFile())) { + break; + } else if (!soundURL.isLocalFile() && soundURL.isValid()) { + break; + } + soundURL.clear(); + } + if (soundURL.isEmpty()) { + qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but sound file from notifyrc file was not found, aborting audio notification"; + finish(notification); + return; + } + + // Looping happens in the finishCallback + if (!playSound(m_currentId, soundURL)) { + finish(notification); + return; + } + + if (notification->flags() & KNotification::LoopSound) { + m_loopSoundUrls.insert(m_currentId, soundURL); + } + + Q_ASSERT(!m_notifications.value(m_currentId)); + m_notifications.insert(m_currentId, notification); + + ++m_currentId; +} + +bool NotifyByAudio::playSound(quint32 id, const QUrl &url) +{ + if (!m_context) { + qCWarning(LOG_KNOTIFICATIONS) << "Cannot play notification sound without canberra context"; + return false; + } + + ca_proplist *props = nullptr; + ca_proplist_create(&props); + + // We'll also want this cached for a time. volatile makes sure the cache is + // dropped after some time or when the cache is under pressure. + ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, QFile::encodeName(url.toLocalFile()).constData()); + ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile"); + + int ret = ca_context_play_full(m_context, id, props, &ca_finish_callback, this); + + ca_proplist_destroy(props); + + if (ret != CA_SUCCESS) { + qCWarning(LOG_KNOTIFICATIONS) << "Failed to play sound with canberra:" << ca_strerror(ret); + return false; + } + + return true; +} + +void NotifyByAudio::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata) +{ + Q_UNUSED(c); + QMetaObject::invokeMethod(static_cast(userdata), + "finishCallback", + Q_ARG(uint32_t, id), + Q_ARG(int, error_code)); +} + +void NotifyByAudio::finishCallback(uint32_t id, int error_code) +{ + KNotification *notification = m_notifications.value(id, nullptr); + + if (error_code == CA_SUCCESS) { + // Loop the sound now if we have one + const QUrl soundUrl = m_loopSoundUrls.value(id); + if (soundUrl.isValid()) { + if (!playSound(id, soundUrl)) { + finishNotification(notification, id); + } + return; + } + } else if (error_code != CA_ERROR_CANCELED) { + qCWarning(LOG_KNOTIFICATIONS) << "Playing audio notification failed:" << ca_strerror(error_code); + } + + finishNotification(notification, id); +} + +void NotifyByAudio::close(KNotification *notification) +{ + if (!m_notifications.values().contains(notification)) { + return; + } + + const auto id = m_notifications.key(notification); + if (m_context) { + int ret = ca_context_cancel(m_context, id); + if (ret != CA_SUCCESS) { + qCWarning(LOG_KNOTIFICATIONS) << "Failed to cancel canberra context for audio notification:" << ca_strerror(ret); + return; + } + } +} + + +void NotifyByAudio::finishNotification(KNotification *notification, quint32 id) +{ + m_notifications.remove(id); + m_loopSoundUrls.remove(id); + finish(notification); +} diff --git a/src/notifybyaudio.h b/src/notifybyaudio_phonon.h rename from src/notifybyaudio.h rename to src/notifybyaudio_phonon.h diff --git a/src/notifybyaudio.cpp b/src/notifybyaudio_phonon.cpp rename from src/notifybyaudio.cpp rename to src/notifybyaudio_phonon.cpp --- a/src/notifybyaudio.cpp +++ b/src/notifybyaudio_phonon.cpp @@ -19,7 +19,7 @@ */ -#include "notifybyaudio.h" +#include "notifybyaudio_phonon.h" #include "debug_p.h" #include