Commit ed0c5863 authored by Nicolas Fella's avatar Nicolas Fella

[KCM] Add speaker placement test

Summary:
An equivalent to the Phonon KCM speaker test.
One step closer to T9091

Stereo:
{F6041265}

5.1:
{F6041267}

7.1:
{F6041270}

Test Plan:
When siwtching profiles the appropriate setup is shown.
Sound is working for Stereo, 5.1/7.1 is not tested because I don't have the hardware for it.

Known issue:
Input-only profile still shows Front-left and front-right

Reviewers: drosca

Reviewed By: drosca

Subscribers: ngraham, #vdg, plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D13931
parent 8d7543eb
......@@ -22,6 +22,7 @@ set(cpp_SRCS
server.cpp
streamrestore.cpp
module.cpp
canberracontext.cpp
qml/globalactioncollection.cpp
qml/plugin.cpp
qml/volumeosd.cpp
......
/*
Copyright 2018 Nicolas Fella <nicolas.fella@gmx.de>
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 <http://www.gnu.org/licenses/>.
*/
#include "canberracontext.h"
#include <QObject>
namespace QPulseAudio
{
CanberraContext *CanberraContext::s_context = nullptr;
CanberraContext *CanberraContext::instance()
{
if (!s_context) {
s_context = new CanberraContext;
}
return s_context;
}
CanberraContext::CanberraContext(QObject *parent)
: QObject(parent)
{
ca_context_create(&m_canberra);
}
CanberraContext::~CanberraContext()
{
if (m_canberra) {
ca_context_destroy(m_canberra);
}
}
ca_context *CanberraContext::canberra()
{
return m_canberra;
}
void CanberraContext::ref()
{
++m_references;
}
void CanberraContext::unref()
{
if (--m_references == 0) {
delete this;
s_context = nullptr;
}
}
}
/*
Copyright 2018 Nicolas Fella <nicolas.fella@gmx.de>
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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <canberra.h>
#include <QObject>
namespace QPulseAudio
{
class CanberraContext : public QObject
{
Q_OBJECT
public:
explicit CanberraContext(QObject *parent = nullptr);
virtual ~CanberraContext();
static CanberraContext *instance();
ca_context *canberra();
void ref();
void unref();
private:
ca_context *m_canberra = nullptr;
int m_references = 0;
static CanberraContext *s_context;
};
}
......@@ -23,6 +23,8 @@ import QtQuick.Layouts 1.1
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 @@ ScrollView {
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")
}
}
}
}
}
......@@ -20,57 +20,53 @@
*/
#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",
......@@ -79,5 +75,5 @@ void VolumeFeedback::play(quint32 sinkIndex)
nullptr
);
ca_context_change_device(m_context, nullptr);
ca_context_change_device(context, nullptr);
}
......@@ -38,9 +38,6 @@ public:
public slots:
void play(quint32 sinkIndex);
private:
ca_context *m_context = nullptr;
};
#endif // VOLUMEFEEDBACK_H
......@@ -22,6 +22,8 @@
#include "context.h"
#include "server.h"
#include "canberracontext.h"
#include <pulse/channelmap.h>
namespace QPulseAudio
{
......@@ -30,6 +32,12 @@ 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 @@ void Sink::setDefault(bool enable)
}
}
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
......@@ -22,6 +22,8 @@
#define SINK_H
#include "device.h"
#include <pulse/channelmap.h>
#include <canberra.h>
namespace QPulseAudio
{
......@@ -31,6 +33,7 @@ class Sink : public Device
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 @@ public:
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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment