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,13 @@ 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) 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,5 +1,5 @@ -if (Phonon4Qt5_FOUND) - include_directories(${PHONON_INCLUDE_DIR}) +if (CANBERRA_FOUND) + include_directories(${CANBERRA_INCLUDE_DIRS}) endif() ecm_create_qm_loader(knotifications_QM_LOADER knotifications5_qt) @@ -27,7 +27,7 @@ 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.cpp) endif() @@ -77,9 +77,9 @@ KF5::Codecs ) -if (Phonon4Qt5_FOUND) +if (CANBERRA_FOUND) target_link_libraries(KF5Notifications PRIVATE - ${PHONON_LIBRARIES}) + ${CANBERRA_LIBRARIES}) endif() if (Qt5TextToSpeech_FOUND) diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp --- a/src/knotificationmanager.cpp +++ b/src/knotificationmanager.cpp @@ -41,7 +41,7 @@ #include "notifybyflatpak.h" #include "debug_p.h" -#ifdef HAVE_PHONON4QT5 +#ifdef HAVE_CANBERRA #include "notifybyaudio.h" #endif @@ -137,7 +137,7 @@ plugin = new NotifyByTaskbar(this); addPlugin(plugin); } else if (action == QLatin1String("Sound")) { -#ifdef HAVE_PHONON4QT5 +#ifdef HAVE_CANBERRA plugin = new NotifyByAudio(this); addPlugin(plugin); #endif diff --git a/src/notifybyaudio.h b/src/notifybyaudio.h --- a/src/notifybyaudio.h +++ b/src/notifybyaudio.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); + + void 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.cpp b/src/notifybyaudio.cpp --- a/src/notifybyaudio.cpp +++ b/src/notifybyaudio.cpp @@ -1,5 +1,6 @@ /* 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 @@ -22,36 +23,39 @@ #include "notifybyaudio.h" #include "debug_p.h" -#include +#include #include -#include -#include +#include +#include #include #include "knotifyconfig.h" #include "knotification.h" -#include -#include -#include +#include NotifyByAudio::NotifyByAudio(QObject *parent) - : KNotificationPlugin(parent), - m_audioOutput(nullptr) + : KNotificationPlugin(parent) { + qRegisterMetaType("uint32_t"); + + ca_context_create(&m_context); + + 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); } NotifyByAudio::~NotifyByAudio() { - qDeleteAll(m_reusablePhonons); - delete m_audioOutput; + ca_context_destroy(m_context); + m_context = nullptr; } void NotifyByAudio::notify(KNotification *notification, KNotifyConfig *config) { - if (!m_audioOutput) { - m_audioOutput = new Phonon::AudioOutput(Phonon::NotificationCategory, this); - } 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"; @@ -66,7 +70,7 @@ soundURL = QUrl::fromUserInput(soundFilename, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile); - if (soundURL.isLocalFile() && QFile::exists(soundURL.toLocalFile())) { + if (soundURL.isLocalFile() && QFileInfo::exists(soundURL.toLocalFile())) { break; } else if (!soundURL.isLocalFile() && soundURL.isValid()) { break; @@ -79,110 +83,76 @@ return; } - Phonon::MediaObject *m; - - if (m_reusablePhonons.isEmpty()) { - m = new Phonon::MediaObject(this); - connect(m, &Phonon::MediaObject::finished, this, &NotifyByAudio::onAudioFinished); - connect(m, &Phonon::MediaObject::stateChanged, this, &NotifyByAudio::stateChanged); - Phonon::createPath(m, m_audioOutput); - } else { - m = m_reusablePhonons.takeFirst(); - } - - m->setCurrentSource(soundURL); - m->play(); + // Looping happens in the finishCallback + playSound(m_currentId, soundURL); if (notification->flags() & KNotification::LoopSound) { - // Enqueing essentially prevents the subsystem pipeline from partial teardown - // which is the most desired thing in terms of load and delay between loop cycles. - // All of this is timing dependent, which is why we want at least one source queued; - // in reality the shorter the source the more sources we want to be queued to prevent - // the MO from running out of sources. - // Point being that all phonon signals are forcefully queued (becuase qthread has problems detecting !pthread threads), - // so when you get for example the aboutToFinish signal the MO might already have stopped playing. - // - // And so we queue it three times at least; doesn't cost anything and keeps us safe. - - m->enqueue(soundURL); - m->enqueue(soundURL); - m->enqueue(soundURL); - - connect(m, SIGNAL(currentSourceChanged(Phonon::MediaSource)), SLOT(onAudioSourceChanged(Phonon::MediaSource))); + m_loopSoundUrls.insert(m_currentId, soundURL); } - Q_ASSERT(!m_notifications.value(m)); - m_notifications.insert(m, notification); -} + Q_ASSERT(!m_notifications.value(m_currentId)); + m_notifications.insert(m_currentId, notification); -void NotifyByAudio::stateChanged(Phonon::State newState, Phonon::State oldState) -{ - qCDebug(LOG_KNOTIFICATIONS) << "Changing audio state from" << oldState << "to" << newState; + ++m_currentId; } -void NotifyByAudio::close(KNotification *notification) +void NotifyByAudio::playSound(quint32 id, const QUrl &url) { - Phonon::MediaObject *m = m_notifications.key(notification); + ca_proplist *props = nullptr; + ca_proplist_create(&props); - if (!m) { - return; - } + // 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"); + + ca_context_play_full(m_context, id, props, &ca_finish_callback, this); - m->stop(); - finishNotification(notification, m); + ca_proplist_destroy(props); + props = nullptr; } -void NotifyByAudio::onAudioFinished() +void NotifyByAudio::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata) { - Phonon::MediaObject *m = qobject_cast(sender()); - - if (!m) { - return; - } - - KNotification *notification = m_notifications.value(m, nullptr); - - if (!notification) { - // This means that close was called already so there's nothing else to do. - // Ideally we should not be getting here if close has already been called - // since stoping a mediaobject means it won't emit finished() *BUT* - // since the finished signal is a queued connection in phonon it can happen - // that the playing had already finished and we just had not got the signal yet - return; - } - - //if the sound is short enough, we can't guarantee new sounds are - //enqueued before finished is emitted. - //so to make sure we are looping restart it when the sound finished - if (notification && (notification->flags() & KNotification::LoopSound)) { - m->play(); - return; - } - - finishNotification(notification, m); + Q_UNUSED(c); + QMetaObject::invokeMethod(static_cast(userdata), + "finishCallback", + Q_ARG(uint32_t, id), + Q_ARG(int, error_code)); } -void NotifyByAudio::finishNotification(KNotification *notification, Phonon::MediaObject *m) +void NotifyByAudio::finishCallback(uint32_t id, int error_code) { - m_notifications.remove(m); - - if (notification) { - finish(notification); + 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()) { + playSound(id, soundUrl); + return; + } + } else if (error_code != CA_ERROR_CANCELED) { + qCWarning(LOG_KNOTIFICATIONS) << "Playing audio notification failed:" << ca_strerror(error_code); } - disconnect(m, SIGNAL(currentSourceChanged(Phonon::MediaSource)), this, SLOT(onAudioSourceChanged(Phonon::MediaSource))); - - m_reusablePhonons.append(m); + finishNotification(notification, id); } -void NotifyByAudio::onAudioSourceChanged(const Phonon::MediaSource &source) +void NotifyByAudio::close(KNotification *notification) { - Phonon::MediaObject *m = qobject_cast(sender()); - - if (!m) { + if (!m_notifications.values().contains(notification)) { return; } - m->enqueue(source); + const auto id = m_notifications.key(notification); + ca_context_cancel(m_context, id); } + +void NotifyByAudio::finishNotification(KNotification *notification, quint32 id) +{ + m_notifications.remove(id); + m_loopSoundUrls.remove(id); + finish(notification); +}