diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ server.cpp streamrestore.cpp module.cpp + canberracontext.cpp qml/globalactioncollection.cpp qml/plugin.cpp qml/volumeosd.cpp diff --git a/src/qml/volumefeedback.h b/src/canberracontext.h copy from src/qml/volumefeedback.h copy to src/canberracontext.h --- a/src/qml/volumefeedback.h +++ b/src/canberracontext.h @@ -1,5 +1,5 @@ /* - Copyright 2016 David Rosca + Copyright 2018 Nicolas Fella This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -17,30 +17,35 @@ You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ +#pragma once -#ifndef VOLUMEFEEDBACK_H -#define VOLUMEFEEDBACK_H - +#include #include -#include +namespace QPulseAudio +{ -class VolumeFeedback : public QObject +class CanberraContext : public QObject { Q_OBJECT - Q_PROPERTY(bool valid READ isValid CONSTANT) public: - explicit VolumeFeedback(QObject *parent = nullptr); - ~VolumeFeedback() override; + explicit CanberraContext(QObject *parent = nullptr); + virtual ~CanberraContext(); - bool isValid() const; + static CanberraContext *instance(); -public slots: - void play(quint32 sinkIndex); + ca_context *canberra(); + + void ref(); + void unref(); private: - ca_context *m_context = nullptr; + ca_context *m_canberra = nullptr; + int m_references = 0; + + static CanberraContext *s_context; + }; -#endif // VOLUMEFEEDBACK_H +} diff --git a/src/sink.h b/src/canberracontext.cpp copy from src/sink.h copy to src/canberracontext.cpp --- a/src/sink.h +++ b/src/canberracontext.cpp @@ -1,5 +1,5 @@ /* - Copyright 2014-2015 Harald Sitter + Copyright 2018 Nicolas Fella This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -18,30 +18,51 @@ License along with this library. If not, see . */ -#ifndef SINK_H -#define SINK_H - -#include "device.h" +#include "canberracontext.h" +#include namespace QPulseAudio { -class Sink : public Device +CanberraContext *CanberraContext::s_context = nullptr; + +CanberraContext *CanberraContext::instance() { - Q_OBJECT -public: - explicit Sink(QObject *parent); + if (!s_context) { + s_context = new CanberraContext; + } + return s_context; +} - void update(const pa_sink_info *info); - void setVolume(qint64 volume) override; - void setMuted(bool muted) override; - void setActivePortIndex(quint32 port_index) override; - void setChannelVolume(int channel, qint64 volume) override; +CanberraContext::CanberraContext(QObject *parent) + : QObject(parent) +{ + ca_context_create(&m_canberra); +} - bool isDefault() const override; - void setDefault(bool enable) override; -}; -} // QPulseAudio +CanberraContext::~CanberraContext() +{ + if (m_canberra) { + ca_context_destroy(m_canberra); + } +} -#endif // SINK_H +ca_context *CanberraContext::canberra() +{ + return m_canberra; +} + +void CanberraContext::ref() +{ + ++m_references; +} + +void CanberraContext::unref() +{ + if (--m_references == 0) { + delete this; + s_context = nullptr; + } +} +} diff --git a/src/kcm/package/contents/ui/Advanced.qml b/src/kcm/package/contents/ui/Advanced.qml --- a/src/kcm/package/contents/ui/Advanced.qml +++ b/src/kcm/package/contents/ui/Advanced.qml @@ -23,6 +23,8 @@ import QtQuick.Controls 1.3 import org.kde.plasma.private.volume 0.1 +import org.kde.kcoreaddons 1.0 as KCoreAddons + ScrollView { id: scrollView @@ -93,5 +95,151 @@ text: i18n("Requires 'module-gconf' PulseAudio module") visible: moduleManager.settingsSupported && moduleManager.loadedModules.indexOf("module-gconf") == -1 } + + Header { + Layout.fillWidth: true + text: i18n("Speaker Placement and Testing") + } + + RowLayout { + Layout.margins: units.gridUnit / 2 + visible: sinks.count > 1 + + Label { + text: i18nc("@label", "Output:") + font.bold: true + } + + ComboBox { + id: sinks + + property var pulseObject: null + + Layout.fillWidth: true + textRole: "Description" + model: SinkModel { + onRowsInserted: sinks.updatePulseObject() + onRowsRemoved: sinks.updatePulseObject() + onDataChanged: sinks.updatePulseObject() + } + onCurrentIndexChanged: updatePulseObject() + onCurrentTextChanged: updatePulseObject() + Component.onCompleted: updatePulseObject() + + function updatePulseObject() { + Qt.callLater(function() { + pulseObject = model.data(model.index(sinks.currentIndex, 0), model.role("PulseObject")); + }); + } + } + } + + Grid { + id: grid + columns: 3 + spacing: 5 + Layout.fillWidth: true + + Item { + width: grid.width/3 + height: 50 + + Button{ + text: i18n("Front Left") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Front Left") > -1 : false + onClicked: sinks.pulseObject.testChannel("Front Left") + } + } + Item { + width: grid.width/3 + height: 50 + + Button{ + text: i18n("Front Center") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Front Center") > -1 : false + onClicked: sinks.pulseObject.testChannel("Front Center") + } + } + Item { + width: grid.width/3 + height: 50 + + Button{ + text: i18n("Front Right") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Front Right") > -1 : false + onClicked: sinks.pulseObject.testChannel("Front Right") + } + } + Item { + width: grid.width/3 + height: 50 + + Button{ + text: i18n("Side Left") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Side Left") > -1 : false + onClicked: sinks.pulseObject.testChannel("Side Left") + + } + } + Item { + width: grid.width/3 + height: 50 + + KCoreAddons.KUser { + id: kuser + } + + Image { + source: kuser.faceIconUrl + anchors.centerIn: parent + width: 50 + height: 50 + } + } + Item { + width: grid.width/3 + height: 50 + Button{ + text: i18n("Side Right") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Side Right") > -1 : false + onClicked: sinks.pulseObject.testChannel("Side Right") + } + } + Item { + width: grid.width/3 + height: 50 + Button{ + text: i18n("Rear Left") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Rear Left") > -1 : false + onClicked: sinks.pulseObject.testChannel("Rear Left") + } + } + Item { + width: grid.width/3 + height: 50 + Button{ + text: i18n("Subwoofer") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Subwoofer") > -1 : false + onClicked: sinks.pulseObject.testChannel("Subwoofer") + } + } + Item { + width: grid.width/3 + height: 50 + Button{ + text: i18n("Rear Right") + anchors.centerIn: parent + visible: sinks.pulseObject ? sinks.pulseObject.channels.indexOf("Rear Right") > -1 : false + onClicked: sinks.pulseObject.testChannel("Rear Right") + } + } + } } } diff --git a/src/qml/volumefeedback.h b/src/qml/volumefeedback.h --- a/src/qml/volumefeedback.h +++ b/src/qml/volumefeedback.h @@ -38,9 +38,6 @@ public slots: void play(quint32 sinkIndex); - -private: - ca_context *m_context = nullptr; }; #endif // VOLUMEFEEDBACK_H diff --git a/src/qml/volumefeedback.cpp b/src/qml/volumefeedback.cpp --- a/src/qml/volumefeedback.cpp +++ b/src/qml/volumefeedback.cpp @@ -20,64 +20,60 @@ */ #include "volumefeedback.h" +#include "canberracontext.h" VolumeFeedback::VolumeFeedback(QObject *parent) : QObject(parent) { - if (ca_context_create(&m_context) < 0) { - m_context = nullptr; - return; - } - if (ca_context_set_driver(m_context, "pulse") < 0) { - ca_context_destroy(m_context); - m_context = nullptr; + QPulseAudio::CanberraContext::instance()->ref(); + if (ca_context_set_driver(QPulseAudio::CanberraContext::instance()->canberra(), "pulse") < 0) { return; } } VolumeFeedback::~VolumeFeedback() { - if (m_context) { - ca_context_destroy(m_context); - } + QPulseAudio::CanberraContext::instance()->unref(); } bool VolumeFeedback::isValid() const { - return m_context; + return QPulseAudio::CanberraContext::instance()->canberra(); } void VolumeFeedback::play(quint32 sinkIndex) { - if (!m_context) { + auto context = QPulseAudio::CanberraContext::instance()->canberra(); + + if (!context) { return; } int playing = 0; const int cindex = 2; // Note "2" is simply the index we've picked. It's somewhat irrelevant. - ca_context_playing(m_context, cindex, &playing); + ca_context_playing(context, cindex, &playing); // NB Depending on how this is desired to work, we may want to simply // skip playing, or cancel the currently playing sound and play our // new one... for now, let's do the latter. if (playing) { - ca_context_cancel(m_context, cindex); + ca_context_cancel(context, cindex); } char dev[64]; snprintf(dev, sizeof(dev), "%lu", (unsigned long) sinkIndex); - ca_context_change_device(m_context, dev); + ca_context_change_device(context, dev); // Ideally we'd use something like ca_gtk_play_for_widget()... ca_context_play( - m_context, + context, cindex, CA_PROP_EVENT_DESCRIPTION, "Volume Control Feedback Sound", CA_PROP_EVENT_ID, "audio-volume-change", CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", CA_PROP_CANBERRA_ENABLE, "1", nullptr ); - ca_context_change_device(m_context, nullptr); + ca_context_change_device(context, nullptr); } diff --git a/src/sink.h b/src/sink.h --- a/src/sink.h +++ b/src/sink.h @@ -22,6 +22,8 @@ #define SINK_H #include "device.h" +#include +#include namespace QPulseAudio { @@ -31,6 +33,7 @@ Q_OBJECT public: explicit Sink(QObject *parent); + virtual ~Sink(); void update(const pa_sink_info *info); void setVolume(qint64 volume) override; @@ -40,6 +43,15 @@ bool isDefault() const override; void setDefault(bool enable) override; + +public slots: + void testChannel(const QString &name); + +private: + pa_channel_position_t channelNameToPosition(const QString &name); + QString positionToChannelName(pa_channel_position_t position); + QString positionAsString(pa_channel_position_t pos); + }; } // QPulseAudio diff --git a/src/sink.cpp b/src/sink.cpp --- a/src/sink.cpp +++ b/src/sink.cpp @@ -22,14 +22,22 @@ #include "context.h" #include "server.h" +#include "canberracontext.h" +#include namespace QPulseAudio { Sink::Sink(QObject *parent) : Device(parent) { connect(context()->server(), &Server::defaultSinkChanged, this, &Sink::defaultChanged); + CanberraContext::instance()->ref(); +} + +Sink::~Sink() +{ + CanberraContext::instance()->unref(); } void Sink::update(const pa_sink_info *info) @@ -74,4 +82,117 @@ } } +pa_channel_position_t Sink::channelNameToPosition(const QString &name) +{ + if (name == QLatin1String("Front Left")) { + return PA_CHANNEL_POSITION_FRONT_LEFT; + } else if (name == QLatin1String("Front Center")) { + return PA_CHANNEL_POSITION_FRONT_CENTER; + } else if (name == QLatin1String("Front Right")) { + return PA_CHANNEL_POSITION_FRONT_RIGHT; + } else if (name == QLatin1String("Side Left")) { + return PA_CHANNEL_POSITION_SIDE_LEFT; + } else if (name == QLatin1String("Side Right")) { + return PA_CHANNEL_POSITION_SIDE_RIGHT; + } else if (name == QLatin1String("Rear Left")) { + return PA_CHANNEL_POSITION_REAR_LEFT; + } else if (name == QLatin1String("Rear Right")) { + return PA_CHANNEL_POSITION_REAR_RIGHT; + } else if (name == QLatin1String("Subwoofer")) { + return PA_CHANNEL_POSITION_SUBWOOFER; + } + + return PA_CHANNEL_POSITION_MONO; +} + +QString Sink::positionToChannelName(pa_channel_position_t position) +{ + switch (position) { + case PA_CHANNEL_POSITION_FRONT_LEFT: + return QStringLiteral("Front Left"); + case PA_CHANNEL_POSITION_FRONT_RIGHT: + return QStringLiteral("Front Right"); + case PA_CHANNEL_POSITION_FRONT_CENTER: + return QStringLiteral("Front Center"); + case PA_CHANNEL_POSITION_SIDE_LEFT: + return QStringLiteral("Side Left"); + case PA_CHANNEL_POSITION_SIDE_RIGHT: + return QStringLiteral("Side Right"); + case PA_CHANNEL_POSITION_REAR_LEFT: + return QStringLiteral("Rear Left"); + case PA_CHANNEL_POSITION_REAR_RIGHT: + return QStringLiteral("Rear Right"); + case PA_CHANNEL_POSITION_SUBWOOFER: + return QStringLiteral("Subwoofer"); + default: + return QStringLiteral("Mono"); + } +} + +void Sink::testChannel(const QString &name) +{ + auto context = CanberraContext::instance()->canberra(); + if (!context) + return; + + char dev[64]; + snprintf(dev, sizeof(dev), "%lu", (unsigned long) m_index); + ca_context_change_device(context, dev); + + QString sound_name = QStringLiteral("audio-channel-") + positionAsString(channelNameToPosition(name)); + ca_proplist *proplist; + ca_proplist_create(&proplist); + + ca_proplist_sets(proplist, CA_PROP_MEDIA_ROLE, "test"); + ca_proplist_sets(proplist, CA_PROP_MEDIA_NAME, name.toLatin1().constData()); + ca_proplist_sets(proplist, CA_PROP_CANBERRA_FORCE_CHANNEL, positionAsString(channelNameToPosition(name)).toLatin1().data()); + ca_proplist_sets(proplist, CA_PROP_CANBERRA_ENABLE, "1"); + + ca_proplist_sets(proplist, CA_PROP_EVENT_ID, sound_name.toLatin1().data()); + if (ca_context_play_full(context, 0, proplist, nullptr, NULL) < 0) { + // Try a different sound name. + ca_proplist_sets(proplist, CA_PROP_EVENT_ID, "audio-test-signal"); + if (ca_context_play_full(context, 0, proplist, nullptr, NULL) < 0) { + // Finaly try this... if this doesn't work, then stuff it. + ca_proplist_sets(proplist, CA_PROP_EVENT_ID, "bell-window-system"); + ca_context_play_full(context, 0, proplist, nullptr, NULL); + } + } + + ca_context_change_device(context, nullptr); + ca_proplist_destroy(proplist); +} + +QString Sink::positionAsString(pa_channel_position_t pos) +{ + switch (pos) { + case PA_CHANNEL_POSITION_FRONT_LEFT: + return QStringLiteral("front-left"); + case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER: + return QStringLiteral("front-left-of-center"); + case PA_CHANNEL_POSITION_FRONT_CENTER: + return QStringLiteral("front-center"); + case PA_CHANNEL_POSITION_MONO: + return QStringLiteral("mono"); + case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER: + return QStringLiteral("front-right-of-center"); + case PA_CHANNEL_POSITION_FRONT_RIGHT: + return QStringLiteral("front-right"); + case PA_CHANNEL_POSITION_SIDE_LEFT: + return QStringLiteral("side-left"); + case PA_CHANNEL_POSITION_SIDE_RIGHT: + return QStringLiteral("side-right"); + case PA_CHANNEL_POSITION_REAR_LEFT: + return QStringLiteral("rear-left"); + case PA_CHANNEL_POSITION_REAR_CENTER: + return QStringLiteral("rear-center"); + case PA_CHANNEL_POSITION_REAR_RIGHT: + return QStringLiteral("rear-right"); + case PA_CHANNEL_POSITION_SUBWOOFER: + return QStringLiteral("subwoofer"); + default: + break; + } + return QStringLiteral("invalid"); +} } // QPulseAudio