From 6bf2ccbd72176a8cbdfb3cdb2c15ee1c2db594b8 Mon Sep 17 00:00:00 2001 From: Loïc Collignon Date: Tue, 13 Mar 2018 08:56:37 +0100 Subject: make use of alsacore and hal bindings to control audio volume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ib7e90a7d2a148a067566bc04929fda445b46ab45 Signed-off-by: Loïc Collignon --- app/Mixer.qml | 161 ++++++++++++++---------- app/app.pro | 12 +- app/main.cpp | 26 +--- app/mixer.cpp | 209 +++++++++++++++++++++++++++++++ app/mixer.h | 74 +++++++++++ app/paclient.cpp | 329 ------------------------------------------------- app/paclient.h | 97 --------------- app/pacontrolmodel.cpp | 208 ------------------------------- app/pacontrolmodel.h | 92 -------------- app/qafbwsclient.cpp | 136 ++++++++++++++++++++ app/qafbwsclient.h | 50 ++++++++ app/qafbwsmsg.cpp | 44 +++++++ app/qafbwsmsg.h | 33 +++++ 13 files changed, 653 insertions(+), 818 deletions(-) create mode 100644 app/mixer.cpp create mode 100644 app/mixer.h delete mode 100644 app/paclient.cpp delete mode 100644 app/paclient.h delete mode 100644 app/pacontrolmodel.cpp delete mode 100644 app/pacontrolmodel.h create mode 100644 app/qafbwsclient.cpp create mode 100644 app/qafbwsclient.h create mode 100644 app/qafbwsmsg.cpp create mode 100644 app/qafbwsmsg.h (limited to 'app') diff --git a/app/Mixer.qml b/app/Mixer.qml index 96875e0..2b6d5cc 100644 --- a/app/Mixer.qml +++ b/app/Mixer.qml @@ -18,11 +18,21 @@ import QtQuick 2.6 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.0 import AGL.Demo.Controls 1.0 -import PaControlModel 1.0 +import Mixer 1.0 ApplicationWindow { id: root + Mixer { + id: mixer + Component.objectName: { + mixer.open(bindingAddress) + } + onMasterVolumeChanged: sliderMasterVolume.value = masterVolume + onPcmVolumeChanged: sliderPcmVolume.value = pcmVolume + onMicrophoneVolumeChanged: sliderMicrophoneVolume.value = microphoneVolume + } + Label { id: title font.pixelSize: 48 @@ -30,69 +40,92 @@ ApplicationWindow { anchors.horizontalCenter: parent.horizontalCenter } - Component { - id: ctldesc - Label { - font.pixelSize: 32 - width: listView.width - wrapMode: Text.WordWrap - property var typeString: {modelType ? "Output" : "Input"} - text: "[" + typeString + " #" + modelCIndex + "]: " + modelDesc - } - } + ColumnLayout { + anchors.margins: 80 + anchors.top: title.bottom + anchors.left: parent.left + anchors.right: parent.right - Component { - id: empty - Item { - } - } + ComboBox { + id: soundCardSelector + anchors.left: parent.left + anchors.right: parent.right + model: mixer.hals + onCurrentIndexChanged: mixer.ActiveHal = currentIndex + } - ListView { - id: listView - anchors.left: parent.left - anchors.top: title.bottom - anchors.margins: 80 - anchors.fill: parent - model: PaControlModel { objectName: "pacm" } - delegate: ColumnLayout { - width: parent.width - spacing: 40 - Connections { - target: listView.model - onDataChanged: slider.value = volume - } - Loader { - property int modelType: type - property int modelCIndex: cindex - property string modelDesc: name - sourceComponent: (channel == 0) ? ctldesc : empty - } - RowLayout { - Layout.minimumHeight: 75 - Label { - font.pixelSize: 24 - text: cdesc - Layout.minimumWidth: 150 - } - Label { - font.pixelSize: 24 - text: "0 %" - } - Slider { - id: slider - Layout.fillWidth: true - from: 0 - to: 65536 - stepSize: 256 - snapMode: Slider.SnapOnRelease - onValueChanged: volume = value - Component.onCompleted: value = volume - } - Label { - font.pixelSize: 24 - text: "100 %" - } - } - } - } + RowLayout { + Layout.minimumHeight: 75 + Label { + font.pixelSize: 24 + text: "Master" + Layout.minimumWidth: 150 + } + Label { + id: textMasterVolume + font.pixelSize: 24 + text: "0 %" + } + Slider { + id: sliderMasterVolume + Layout.fillWidth: true + from: 0 + to: 100 + stepSize: 1 + snapMode: Slider.SnapOnRelease + onValueChanged: { + textMasterVolume.text = value + " %" + mixer.masterVolume = value + } + } + } +/* + RowLayout { + Layout.minimumHeight: 75 + Label { + font.pixelSize: 24 + text: "PCM" + Layout.minimumWidth: 150 + } + Label { + font.pixelSize: 24 + text: "0 %" + } + Slider { + id: sliderPcmVolume + Layout.fillWidth: true + from: 0 + to: 100 + stepSize: 1 + snapMode: Slider.SnapOnRelease + onValueChanged: mixer.pcmVolume = value + } + } +*/ + RowLayout { + Layout.minimumHeight: 75 + Label { + font.pixelSize: 24 + text: "Microphone" + Layout.minimumWidth: 150 + } + Label { + id: textMicrophoneVolume + font.pixelSize: 24 + text: "0 %" + } + Slider { + id: sliderMicrophoneVolume + Layout.fillWidth: true + from: 0 + to: 100 + stepSize: 1 + snapMode: Slider.SnapOnRelease + onValueChanged: { + textMicrophoneVolume.text = value + " %" + mixer.microphoneVolume = value + } + } + } + } } diff --git a/app/app.pro b/app/app.pro index a33fc0d..239dc8f 100644 --- a/app/app.pro +++ b/app/app.pro @@ -1,13 +1,15 @@ TARGET = mixer -QT = quickcontrols2 +QT = quickcontrols2 websockets core HEADERS += \ - pacontrolmodel.h \ - paclient.h + mixer.h \ + qafbwsmsg.h \ + qafbwsclient.h SOURCES = main.cpp \ - pacontrolmodel.cpp \ - paclient.cpp + mixer.cpp \ + qafbwsclient.cpp \ + qafbwsmsg.cpp CONFIG += link_pkgconfig PKGCONFIG += libhomescreen qlibwindowmanager libpulse diff --git a/app/main.cpp b/app/main.cpp index bfce498..a0a5667 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -15,9 +15,6 @@ * limitations under the License. */ -#include "paclient.h" -#include "pacontrolmodel.h" - #include #include #include @@ -34,6 +31,7 @@ #include #include #include +#include "mixer.h" int main(int argc, char *argv[]) { @@ -51,14 +49,7 @@ int main(int argc, char *argv[]) parser.process(app); QStringList positionalArguments = parser.positionalArguments(); - // Fire up PA client QThread - QThread* pat = new QThread; - PaClient* client = new PaClient(); - client->moveToThread(pat); - pat->start(); - - // Register the PA Control Model - qmlRegisterType("PaControlModel", 1, 0, "PaControlModel"); + qmlRegisterType("Mixer", 1, 0, "Mixer"); QQmlApplicationEngine engine; if (positionalArguments.length() == 2) { @@ -88,7 +79,7 @@ int main(int argc, char *argv[]) exit(EXIT_FAILURE); } // Create an event callbnewack against an event type. Here a lambda is called when SyncDraw event occurs - qwm->set_event_handler(QLibWindowmanager::Event_SyncDraw, [qwm, myname](json_object *object) { + qwm->set_event_handler(QLibWindowmanager::Event_SyncDraw, [qwm, myname](json_object*) { fprintf(stderr, "Surface got syncDraw!\n"); qwm->endDraw(myname); }); @@ -116,17 +107,6 @@ int main(int argc, char *argv[]) QQuickWindow *window = qobject_cast(mobjs.first()); QObject::connect(window, SIGNAL(frameSwapped()), qwm, SLOT(slotActivateSurface())); - - PaControlModel *pacm = mobjs.first()->findChild("pacm"); - QObject::connect(client, SIGNAL(controlAdded(int, QString, QString, int, int, const char *, int)), - pacm, SLOT(addOneControl(int, QString, QString, int, int, const char *, int))); - QObject::connect(client, SIGNAL(volumeExternallyChanged(uint32_t, uint32_t, uint32_t, uint32_t)), - pacm, SLOT(changeExternalVolume(uint32_t, uint32_t, uint32_t, uint32_t))); - QObject::connect(pacm, SIGNAL(volumeChanged(uint32_t, uint32_t, uint32_t, uint32_t)), - client, SLOT(setVolume(uint32_t, uint32_t, uint32_t, uint32_t))); - - // Initalize PA client - client->init(); } return app.exec(); } diff --git a/app/mixer.cpp b/app/mixer.cpp new file mode 100644 index 0000000..8c2d2af --- /dev/null +++ b/app/mixer.cpp @@ -0,0 +1,209 @@ +#include +#include +#include +#include "mixer.h" + +Mixer::Mixer(QObject* parent) + : QObject(parent) + , m_masterVolume{0} + , m_pcmVolume{0} + , m_microphoneVolume{0} +{ + connect(&m_client, SIGNAL(connected()), this, SLOT(onClientConnected())); +} + +QVariantList Mixer::hals() const +{ + return m_hallist; +} + +int Mixer::masterVolume() const +{ + return m_masterVolume; +} + +int Mixer::pcmVolume() const +{ + return m_pcmVolume; +} + +int Mixer::microphoneVolume() const +{ + return m_microphoneVolume; +} + +void Mixer::setMasterVolume(int v) +{ + qDebug() << "Mixer::setMasterVolume(" << v << ")"; + if (v != m_masterVolume) + { + m_masterVolumePending = v; + QJsonObject arg; + arg["label"] = "Master_Playback_Volume"; + arg["val"] = static_cast(v); + m_setMasterVolume = m_client.call(m_activeHal, "ctlset", arg); + connect(m_setMasterVolume.data(), SIGNAL(closed()), this, SLOT(onSetMasterVolume())); + } +} + +void Mixer::setPcmVolume(int v) +{ + qDebug() << "Mixer::setPcmVolume(" << v << ")"; + if (v != m_pcmVolume) + { + m_pcmVolumePending = v; + QJsonObject arg; + arg["label"] = "PCM_Playback_Volume"; + arg["val"] = static_cast(v); + m_setPcmVolume = m_client.call(m_activeHal, "ctlset", arg); + connect(m_setPcmVolume.data(), SIGNAL(closed()), this, SLOT(onSetPcmVolume())); + } +} + +void Mixer::setMicrophoneVolume(int v) +{ + qDebug() << "Mixer::setMicrophoneVolume(" << v << ")"; + if (v != m_microphoneVolume) + { + m_microphoneVolumePending = v; + QJsonObject arg; + arg["label"] = "Capture_Volume"; + arg["val"] = static_cast(v); + m_setMicrophoneVolume = m_client.call(m_activeHal, "ctlset", arg); + connect(m_setMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onSetMicrophoneVolume())); + } +} + +QString Mixer::activeHal() const +{ + return m_activeHal; +} + +void Mixer::setActiveHal(QString h) +{ + if (h != m_activeHal) + { + m_activeHal = h; + qDebug() << "Mixer::setActiveHal: " << h; + // Get volumes for this card + + QJsonObject arg; + + arg["label"] = "Master_Playback_Volume"; + m_getMasterVolume = m_client.call(m_activeHal, "ctlget", arg); + connect(m_getMasterVolume.data(), SIGNAL(closed()), this, SLOT(onGetMasterVolume())); + + arg["label"] = "PCM_Playback_Volume"; + m_getPcmVolume = m_client.call(m_activeHal, "ctlget", arg); + connect(m_getPcmVolume.data(), SIGNAL(closed()), this, SLOT(onGetPcmVolume())); + + arg["label"] = "Capture_Volume"; + m_getMicrophoneVolume = m_client.call(m_activeHal, "ctlget", arg); + connect(m_getMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onGetMicrophoneVolume())); + + emit activeHalChanged(); + } +} + +void Mixer::open(const QUrl &url) +{ + m_client.open(url); +} + +void Mixer::onClientConnected() +{ + // Call HAL to populate list + m_alsacoreHallist = m_client.call("alsacore", "hallist"); + connect(m_alsacoreHallist.data(), SIGNAL(closed()), this, SLOT(onHalListClosed())); +} + +void Mixer::onHalListClosed() +{ + qDebug() << "Mixer::onHalListClosed"; + if(m_alsacoreHallist->messageType() == AfMsgType::RetOk) + { + m_hallist.clear(); + + QJsonArray cards = m_alsacoreHallist->value().toObject()["response"].toArray(); + qDebug() << "Mixer::onHalListClosed - founds: " << cards; + foreach (const QJsonValue& card, cards) + { + QJsonObject c = card.toObject(); + QVariant v(c["api"].toString()); + m_hallist.append(v); + qDebug() << "Mixer::onHalListClosed - added this HAL: " << v; + } + + m_alsacoreHallist.clear(); + + setActiveHal(m_hallist[0].toString()); + + emit halsChanged(); + } +} + +void Mixer::onGetMasterVolume() +{ + qDebug() << "Mixer::onGetMasterVolume()"; + disconnect(m_getMasterVolume.data(), SIGNAL(closed()), this, SLOT(onGetMasterVolume())); + if (m_getMasterVolume->messageType() == AfMsgType::RetOk && m_getMasterVolume->value().isObject()) + { + setMasterVolume(m_getMasterVolume->value().toObject()["response"].toObject()["val"].toArray()[0].toInt()); + } +} + +void Mixer::onGetPcmVolume() +{ + qDebug() << "Mixer::onGetPcmVolume()"; + disconnect(m_getPcmVolume.data(), SIGNAL(closed()), this, SLOT(onGetPcmVolume())); + if (m_getPcmVolume->messageType() == AfMsgType::RetOk && m_getPcmVolume->value().isObject()) + { + setPcmVolume(m_getPcmVolume->value().toObject()["response"].toObject()["val"].toArray()[0].toInt()); + } +} + +void Mixer::onGetMicrophoneVolume() +{ + qDebug() << "Mixer::onGetMicrophoneVolume()"; + disconnect(m_getMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onGetMicrophoneVolume())); + if (m_getMicrophoneVolume->messageType() == AfMsgType::RetOk && m_getMicrophoneVolume->value().isObject()) + { + setMicrophoneVolume(m_getMicrophoneVolume->value().toObject()["response"].toObject()["val"].toArray()[0].toInt()); + } +} + +void Mixer::onSetMasterVolume() +{ + qDebug() << "Mixer::onSetMasterVolume()"; + disconnect(m_setMasterVolume.data(), SIGNAL(closed()), this, SLOT(onSetMasterVolume())); + if (m_setMasterVolume->messageType() == AfMsgType::RetOk && m_setMasterVolume->value().isObject()) + { + m_masterVolume = m_masterVolumePending; + emit masterVolumeChanged(); + } +} + +void Mixer::onSetPcmVolume() +{ + qDebug() << "Mixer::onSetPcmVolume()"; + disconnect(m_setPcmVolume.data(), SIGNAL(closed()), this, SLOT(onSetPcmVolume())); + if (m_setPcmVolume->messageType() == AfMsgType::RetOk && m_setPcmVolume->value().isObject()) + { + m_pcmVolume = m_pcmVolumePending; + emit pcmVolumeChanged(); + } +} + +void Mixer::onSetMicrophoneVolume() +{ + qDebug() << "Mixer::onSetMicrophoneVolume()"; + disconnect(m_setMicrophoneVolume.data(), SIGNAL(closed()), this, SLOT(onSetMicrophoneVolume())); + if (m_setMicrophoneVolume->messageType() == AfMsgType::RetOk && m_setMicrophoneVolume->value().isObject()) + { + m_microphoneVolume = m_microphoneVolumePending; + qDebug() << "Mixer::onSetMicrophoneVolume() - "; + emit microphoneVolumeChanged(); + } +} + + diff --git a/app/mixer.h b/app/mixer.h new file mode 100644 index 0000000..2c25562 --- /dev/null +++ b/app/mixer.h @@ -0,0 +1,74 @@ +#ifndef MIXER_H +#define MIXER_H + +#include +#include +#include +#include "qafbwsclient.h" +#include "qafbwsmsg.h" + +class Mixer + : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariantList hals READ hals NOTIFY halsChanged) + Q_PROPERTY(QString activeHal READ activeHal WRITE setActiveHal NOTIFY activeHalChanged) + + Q_PROPERTY(int masterVolume READ masterVolume WRITE setMasterVolume NOTIFY masterVolumeChanged) + Q_PROPERTY(int pcmVolume READ pcmVolume WRITE setPcmVolume NOTIFY pcmVolumeChanged) + Q_PROPERTY(int microphoneVolume READ microphoneVolume WRITE setMicrophoneVolume NOTIFY microphoneVolumeChanged) + +public: + explicit Mixer(QObject* parent = nullptr); + Mixer(const Mixer&) = delete; + + Q_INVOKABLE void open(const QUrl& url); + Q_INVOKABLE QVariantList hals() const; + Q_INVOKABLE QString activeHal() const; + Q_INVOKABLE void setActiveHal(QString h); + + Q_INVOKABLE int masterVolume() const; + Q_INVOKABLE int pcmVolume() const; + Q_INVOKABLE int microphoneVolume() const; + + Q_INVOKABLE void setMasterVolume(int v); + Q_INVOKABLE void setPcmVolume(int v); + Q_INVOKABLE void setMicrophoneVolume(int v); + +signals: + void halsChanged(); + void activeHalChanged(); + void masterVolumeChanged(); + void pcmVolumeChanged(); + void microphoneVolumeChanged(); + +private slots: + void onClientConnected(); + void onHalListClosed(); + void onGetMasterVolume(); + void onGetPcmVolume(); + void onGetMicrophoneVolume(); + void onSetMasterVolume(); + void onSetPcmVolume(); + void onSetMicrophoneVolume(); + +private: + int m_masterVolume; + int m_pcmVolume; + int m_microphoneVolume; + int m_masterVolumePending; + int m_pcmVolumePending; + int m_microphoneVolumePending; + QAfbWsClient m_client; + QVariantList m_hallist; + QString m_activeHal; + QSharedPointer m_alsacoreHallist; + QSharedPointer m_getMasterVolume; + QSharedPointer m_getPcmVolume; + QSharedPointer m_getMicrophoneVolume; + QSharedPointer m_setMasterVolume; + QSharedPointer m_setPcmVolume; + QSharedPointer m_setMicrophoneVolume; +}; + +#endif // MIXER_H diff --git a/app/paclient.cpp b/app/paclient.cpp deleted file mode 100644 index bd53cde..0000000 --- a/app/paclient.cpp +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright (C) 2016,2017 Konsulko Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "paclient.h" - -#include - -PaClient::PaClient() - : m_init(false), m_ml(nullptr), m_mlapi(nullptr), m_ctx(nullptr) -{ -} - -PaClient::~PaClient() -{ - if (m_init) - close(); -} - -void PaClient::close() -{ - if (!m_init) return; - pa_threaded_mainloop_stop(m_ml); - pa_threaded_mainloop_free(m_ml); - m_init = false; -} - -static void set_sink_volume_cb(pa_context *c, int success, void *data) -{ - Q_UNUSED(data); - - if (!success) - qWarning() << "PaClient: set sink volume: " << - pa_strerror(pa_context_errno(c)); -} - -static void set_source_volume_cb(pa_context *c, int success, void *data) -{ - Q_UNUSED(data); - - if (!success) - qWarning() << "PaClient: set source volume: " << - pa_strerror(pa_context_errno(c)); -} - -void PaClient::setVolume(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume) -{ - pa_operation *o; - pa_context *c = context(); - pa_cvolume *cvolume = NULL; - - if (type == C_SINK) { - cvolume = m_sink_states.value(index); - cvolume->values[channel] = volume; - if (!(o = pa_context_set_sink_volume_by_index(c, index, cvolume, set_sink_volume_cb, NULL))) { - qWarning() << "PaClient: set sink #" << index << - " channel #" << channel << - " volume: " << pa_strerror(pa_context_errno(c)); - return; - } - pa_operation_unref(o); - } else if (type == C_SOURCE) { - cvolume = m_source_states.value(index); - cvolume->values[channel] = volume; - if (!(o = pa_context_set_source_volume_by_index(c, index, cvolume, set_source_volume_cb, NULL))) { - qWarning() << "PaClient: set source #" << index << - " channel #" << channel << - " volume: " << pa_strerror(pa_context_errno(c)); - return; - } - pa_operation_unref(o); - } -} - -void get_source_list_cb(pa_context *c, - const pa_source_info *i, - int eol, - void *data) -{ - int chan; - - PaClient *self = reinterpret_cast(data); - - if (eol < 0) { - qWarning() << "PaClient: get source list: " << - pa_strerror(pa_context_errno(c)); - - self->close(); - return; - } - - if (!eol) { - self->addOneControlState(C_SOURCE, i->index, &i->volume); - for (chan = 0; chan < i->channel_map.channels; chan++) { - // NOTE: hide input control - if (QString(i->name).endsWith("monitor")) - continue; - - emit self->controlAdded(i->index, QString(i->name), QString(i->description), - C_SOURCE, chan, channel_position_string[i->channel_map.map[chan]], - i->volume.values[chan]); - } - } -} - -void get_sink_list_cb(pa_context *c, - const pa_sink_info *i, - int eol, - void *data) -{ - PaClient *self = reinterpret_cast(data); - int chan; - - if(eol < 0) { - qWarning() << "PaClient: get sink list: " << - pa_strerror(pa_context_errno(c)); - self->close(); - return; - } - - if(!eol) { - self->addOneControlState(C_SINK, i->index, &i->volume); - for (chan = 0; chan < i->channel_map.channels; chan++) { - emit self->controlAdded(i->index, QString(i->name), QString(i->description), - C_SINK, chan, channel_position_string[i->channel_map.map[chan]], - i->volume.values[chan]); - } - } -} - -void get_sink_info_change_cb(pa_context *c, - const pa_sink_info *i, - int eol, - void *data) -{ - Q_UNUSED(c); - Q_ASSERT(i); - Q_ASSERT(data); - - if (eol) return; - - for (int chan = 0; chan < i->channel_map.channels; chan++) { - PaClient *self = reinterpret_cast(data); - QHash states = self->sink_states(); - pa_cvolume *cvolume = states.value(i->index); - // Check each channel for volume change - if (cvolume->values[chan] != i->volume.values[chan]) { - // On change, update cache and signal - cvolume->values[chan] = i->volume.values[chan]; - emit self->volumeExternallyChanged(C_SINK, i->index, chan, i->volume.values[chan]); - } - } -} - -void get_source_info_change_cb(pa_context *c, - const pa_source_info *i, - int eol, - void *data) -{ - Q_UNUSED(c); - Q_ASSERT(i); - Q_ASSERT(data); - - if (eol) return; - - for (int chan = 0; chan < i->channel_map.channels; chan++) { - PaClient *self = reinterpret_cast(data); - QHash states = self->source_states(); - pa_cvolume *cvolume = states.value(i->index); - // Check each channel for volume change - if (cvolume->values[chan] != i->volume.values[chan]) { - // On change, update cache and signal - cvolume->values[chan] = i->volume.values[chan]; - emit self->volumeExternallyChanged(C_SOURCE, i->index, chan, i->volume.values[chan]); - } - } -} - - -void subscribe_cb(pa_context *c, - pa_subscription_event_type_t type, - uint32_t index, - void *data) -{ - pa_operation *o; - - if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE) { - qWarning("PaClient: unhandled subscribe event operation"); - return; - } - - switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { - case PA_SUBSCRIPTION_EVENT_SINK: - if (!(o = pa_context_get_sink_info_by_index(c, index, get_sink_info_change_cb, data))) { - qWarning() << "PaClient: get sink info by index: " << - pa_strerror(pa_context_errno(c)); - return; - } - break; - case PA_SUBSCRIPTION_EVENT_SOURCE: - if (!(o = pa_context_get_source_info_by_index(c, index, get_source_info_change_cb, data))) { - qWarning() << "PaClient: get source info by index: " << - pa_strerror(pa_context_errno(c)); - return; - } - break; - default: - qWarning("PaClient: unhandled subscribe event facility"); - } -} - -void context_state_cb(pa_context *c, void *data) -{ - pa_operation *o; - PaClient *self = reinterpret_cast(data); - - switch (pa_context_get_state(c)) { - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - break; - case PA_CONTEXT_READY: - // Fetch the controls of interest - if (!(o = pa_context_get_source_info_list(c, get_source_list_cb, data))) { - qWarning() << "PaClient: get source info list: " << - pa_strerror(pa_context_errno(c)); - return; - } - pa_operation_unref(o); - if (!(o = pa_context_get_sink_info_list(c, &get_sink_list_cb, data))) { - qWarning() << "PaClient: get sink info list: " << - pa_strerror(pa_context_errno(c)); - return; - } - pa_operation_unref(o); - pa_context_set_subscribe_callback(c, subscribe_cb, data); - if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE), NULL, NULL))) { - qWarning() << "PaClient: subscribe: " << - pa_strerror(pa_context_errno(c)); - return; - } - break; - case PA_CONTEXT_TERMINATED: - self->close(); - break; - - case PA_CONTEXT_FAILED: - default: - qCritical() << "PaClient: connection failed: " << - pa_strerror(pa_context_errno(c)); - self->close(); - break; - } -} - -void PaClient::init() -{ - m_ml = pa_threaded_mainloop_new(); - if (!m_ml) { - qCritical("PaClient: failed to create mainloop"); - return; - } - - pa_threaded_mainloop_set_name(m_ml, "PaClient mainloop"); - - m_mlapi = pa_threaded_mainloop_get_api(m_ml); - - lock(); - - m_ctx = pa_context_new(m_mlapi, "Mixer"); - if (!m_ctx) { - qCritical("PaClient: failed to create context"); - pa_threaded_mainloop_free(m_ml); - return; - } - pa_context_set_state_callback(m_ctx, context_state_cb, this); - - if (pa_context_connect(m_ctx, 0, (pa_context_flags_t)0, 0) < 0) { - qCritical("PaClient: failed to connect"); - pa_context_unref(m_ctx); - pa_threaded_mainloop_free(m_ml); - return; - } - - if (pa_threaded_mainloop_start(m_ml) != 0) { - qCritical("PaClient: failed to start mainloop"); - pa_context_unref(m_ctx); - pa_threaded_mainloop_free(m_ml); - return; - } - - unlock(); - - m_init = true; -} - -void PaClient::addOneControlState(int type, int index, const pa_cvolume *cvolume) -{ - pa_cvolume *cvolume_new = new pa_cvolume; - cvolume_new->channels = cvolume->channels; - for (int i = 0; i < cvolume->channels; i++) - cvolume_new->values[i] = cvolume->values[i]; - if (type == C_SINK) - m_sink_states.insert(index, cvolume_new); - else if (type == C_SOURCE) - m_source_states.insert(index, cvolume_new); -} - -QHash PaClient::sink_states(void) -{ - return m_sink_states; -} - -QHash PaClient::source_states(void) -{ - return m_source_states; -} diff --git a/app/paclient.h b/app/paclient.h deleted file mode 100644 index 73137f2..0000000 --- a/app/paclient.h +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2016,2017 Konsulko Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include -#include - -const char * const channel_position_string[] = -{ - "Mono", - "Front Left", - "Front Right", - "Center", - "Rear Center", - "Rear Left", - "Rear Right", - "LFE", - "Left Center", - "Right Center", - "Side Left", - "Side Right", -}; - -enum control_type -{ - C_SOURCE, - C_SINK -}; - -typedef struct -{ - uint32_t type; - uint32_t index; - pa_cvolume cvolume; -} CState; - -class PaClient : public QObject -{ - Q_OBJECT - public: - PaClient(); - ~PaClient(); - - void init(); - void close(); - - inline pa_context *context() const - { - return m_ctx; - } - - inline void lock() - { - pa_threaded_mainloop_lock(m_ml); - } - - inline void unlock() - { - pa_threaded_mainloop_unlock(m_ml); - } - - void addOneControlState(int type, int index, const pa_cvolume *cvolume); - - QHash sink_states(); - QHash source_states(); - - public slots: - void setVolume(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume); - - signals: - void controlAdded(int cindex, QString name, QString desc, int type, int channel, const char *cdesc, int volume); - void volumeExternallyChanged(uint32_t type, uint32_t cindex, uint32_t channel, uint32_t volume); - - private: - bool m_init; - pa_threaded_mainloop *m_ml; - pa_mainloop_api *m_mlapi; - pa_context *m_ctx; - QHash m_sink_states; - QHash m_source_states; - - public slots: -}; diff --git a/app/pacontrolmodel.cpp b/app/pacontrolmodel.cpp deleted file mode 100644 index 9489052..0000000 --- a/app/pacontrolmodel.cpp +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) 2016,2017 Konsulko Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "pacontrolmodel.h" - -PaControl::PaControl(const quint32 &cindex, const QString &name, const QString &desc, const quint32 &type, const quint32 &channel, const QString &cdesc, const quint32 &volume) - : m_cindex(cindex), m_name(name), m_desc(desc), m_type(type), m_channel(channel), m_cdesc(cdesc), m_volume(volume) -{ -} - -quint32 PaControl::cindex() const -{ - return m_cindex; -} - -QString PaControl::name() const -{ - QStringList list = m_name.split("."); - - return list.at(1); -} - -QString PaControl::desc() const -{ - return m_desc; -} - -quint32 PaControl::type() const -{ - return m_type; -} - -quint32 PaControl::channel() const -{ - return m_channel; -} - -QString PaControl::cdesc() const -{ - return m_cdesc; -} - - -quint32 PaControl::volume() const -{ - return m_volume; -} - -// FIXME: Not all of these should be editable roles -void PaControl::setCIndex(const QVariant &cindex) -{ - m_cindex = cindex.toUInt(); -} - -void PaControl::setName(const QVariant &name) -{ - m_name = name.toString(); -} - -void PaControl::setDesc(const QVariant &desc) -{ - m_desc = desc.toString(); -} - -void PaControl::setType(const QVariant &type) -{ - m_type = type.toUInt(); -} - -void PaControl::setChannel(const QVariant &channel) -{ - m_channel = channel.toUInt(); -} - -void PaControl::setCDesc(const QVariant &cdesc) -{ - m_cdesc = cdesc.toString(); -} - -void PaControl::setVolume(PaControlModel *pacm, const QVariant &volume) -{ - if (volume != m_volume) { - m_volume = volume.toUInt(); - if (pacm) - emit pacm->volumeChanged(type(), cindex(), channel(), m_volume); - } -} - -PaControlModel::PaControlModel(QObject *parent) - : QAbstractListModel(parent) -{ -} - -void PaControlModel::addControl(const PaControl &control) -{ - beginInsertRows(QModelIndex(), rowCount(), rowCount()); - m_controls << control; - endInsertRows(); -} - -void PaControlModel::addOneControl(int cindex, QString name, QString desc, int type, int channel, const char *cdesc, int volume) -{ - addControl(PaControl(cindex, name, desc, type, channel, cdesc, volume)); -} - -void PaControlModel::changeExternalVolume(uint32_t type, uint32_t cindex, uint32_t channel, uint32_t volume) -{ - QList::iterator i; - int row; - - for (i = m_controls.begin(), row = 0; i < m_controls.end(); ++i, ++row) { - if ((i->type() == type) && - (i->cindex() == cindex) && - (i->channel() == channel)) { - break; - } - } - - i->setVolume(NULL, QVariant(volume)); - QModelIndex qmindex = index(row); - QVector roles; - roles.push_back(VolumeRole); - emit dataChanged(qmindex, qmindex, roles); -} - -int PaControlModel::rowCount(const QModelIndex & parent) const { - Q_UNUSED(parent); - return m_controls.count(); -} - -bool PaControlModel::setData(const QModelIndex &index, const QVariant &value, int role) { - if (index.row() < 0 || index.row() >= m_controls.count()) - return false; - PaControl &control = m_controls[index.row()]; - if (role == CIndexRole) - control.setCIndex(value); - else if (role == NameRole) - control.setName(value); - else if (role == DescRole) - control.setDesc(value); - else if (role == TypeRole) - control.setType(value); - else if (role == ChannelRole) - control.setChannel(value); - else if (role == CDescRole) - control.setCDesc(value); - else if (role == VolumeRole) - control.setVolume(this, value); - QVector roles; - roles.push_back(role); - emit dataChanged(index, index, roles); - return true; -} - -QVariant PaControlModel::data(const QModelIndex & index, int role) const { - if (index.row() < 0 || index.row() >= m_controls.count()) - return QVariant(); - - const PaControl &control = m_controls[index.row()]; - if (role == CIndexRole) - return control.cindex(); - else if (role == NameRole) - return control.name(); - else if (role == DescRole) - return control.desc(); - else if (role == TypeRole) - return control.type(); - else if (role == ChannelRole) - return control.channel(); - else if (role == CDescRole) - return control.cdesc(); - else if (role == VolumeRole) - return control.volume(); - return QVariant(); -} - -Qt::ItemFlags PaControlModel::flags(const QModelIndex &index) const -{ - if (!index.isValid()) - return Qt::ItemIsEnabled; - - return QAbstractListModel::flags(index) | Qt::ItemIsEditable; -} - -QHash PaControlModel::roleNames() const { - QHash roles; - roles[CIndexRole] = "cindex"; - roles[NameRole] = "name"; - roles[DescRole] = "desc"; - roles[TypeRole] = "type"; - roles[ChannelRole] = "channel"; - roles[CDescRole] = "cdesc"; - roles[VolumeRole] = "volume"; - return roles; -} diff --git a/app/pacontrolmodel.h b/app/pacontrolmodel.h deleted file mode 100644 index 81eb70b..0000000 --- a/app/pacontrolmodel.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2016,2017 Konsulko Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include -#include - -class PaControlModel; - -class PaControl -{ - public: - PaControl(const quint32 &index, const QString &name, const QString &desc, const quint32 &type, const quint32 &channel, const QString &cdesc, const quint32 &volume); - - quint32 cindex() const; - QString name() const; - QString desc() const; - quint32 type() const; - quint32 channel() const; - QString cdesc() const; - quint32 volume() const; - void setCIndex(const QVariant&); - void setName(const QVariant&); - void setDesc(const QVariant&); - void setType(const QVariant&); - void setChannel(const QVariant&); - void setCDesc(const QVariant&); - void setVolume(PaControlModel *, const QVariant&); - - private: - quint32 m_cindex; - QString m_name; - QString m_desc; - quint32 m_type; - quint32 m_channel; - QString m_cdesc; - quint32 m_volume; -}; - -class PaControlModel : public QAbstractListModel -{ - Q_OBJECT - public: - enum PaControlRoles { - CIndexRole = Qt::UserRole + 1, - NameRole, - DescRole, - TypeRole, - ChannelRole, - CDescRole, - VolumeRole - }; - - PaControlModel(QObject *parent = 0); - - void addControl(const PaControl &control); - - int rowCount(const QModelIndex &parent = QModelIndex()) const; - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; - - bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); - - Qt::ItemFlags flags(const QModelIndex &index) const; - - public slots: - void addOneControl(int cindex, QString name, QString desc, int type, int channel, const char *cdesc, int volume); - void changeExternalVolume(uint32_t type, uint32_t cindex, uint32_t chan, uint32_t volume); - - signals: - void volumeChanged(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume); - - protected: - QHash roleNames() const; - private: - QList m_controls; - pa_context *pa_ctx; -}; diff --git a/app/qafbwsclient.cpp b/app/qafbwsclient.cpp new file mode 100644 index 0000000..2816907 --- /dev/null +++ b/app/qafbwsclient.cpp @@ -0,0 +1,136 @@ +#include "qafbwsclient.h" +#include "qafbwsmsg.h" +#include +#include +#include + +QAfbWsClient::QAfbWsClient(QObject* parent) + : QObject{parent} + , m_nextCallId{0} +{ + connect(&m_socket, SIGNAL(connected()), this, SLOT(onSocketConnected())); + connect(&m_socket, SIGNAL(disconnected()), this, SLOT(onSocketDisconnected())); + connect(&m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError))); +} + +QUrl QAfbWsClient::url() const +{ + return m_socket.requestUrl(); +} + +QSharedPointer QAfbWsClient::call(const QString& api, const QString& verb, const QJsonValue& args) +{ + while(m_calls.constFind(m_nextCallId) != m_calls.cend()) m_nextCallId++; // Make sure that callId is not currently used + + qDebug() << "QAfbWsClient::call(" << api << ", " << verb << ", " << args << ")"; + QJsonArray arr; + arr.append(static_cast(AfMsgType::Call)); + arr.append(m_nextCallId); + arr.append(api + "/" + verb); + arr.append(args); + + QJsonDocument doc; + doc.setArray(arr); + + QSharedPointer msg(new QAfbWsMsg(m_nextCallId, api, verb)); + m_calls[m_nextCallId] = msg; + + // TODO: handle failure of sendTextMessage + m_socket.sendTextMessage(doc.toJson(QJsonDocument::Compact)); + qDebug() << "m_socket.sendTextMessage(" << doc.toJson(QJsonDocument::Compact) << ")"; + + m_nextCallId++; + + return msg; +} + +void QAfbWsClient::onSocketConnected() +{ + qDebug() << "QAfbWsClient::onSocketConnected()"; + connect(&m_socket, SIGNAL(textMessageReceived(QString)), this, SLOT(onSocketTextMessageReceived(QString))); + emit connected(); +} + +void QAfbWsClient::onSocketDisconnected() +{ + qDebug() << "QAfbWsClient::onSocketDisconnected()"; + m_nextCallId = 0; + disconnect(&m_socket, SIGNAL(textMessageReceived(QString)), this, SLOT(onSocketTextMessageReceived(QString))); + emit disconnected(); +} + +void QAfbWsClient::onSocketError(QAbstractSocket::SocketError err) +{ + qDebug() << "QAfbWsClient::onSocketError(" << err << ")"; + emit error(err, m_socket.errorString()); +} + +void QAfbWsClient::onSocketTextMessageReceived(QString msg) +{ + qDebug() << "QAfbWsClient::onSocketTextMessageReceived(" << msg << ")"; + + if (msg.size() == 0) + { + qCritical() << "QAfbWsClient::onSocketTextMessageReceived: received an empty message."; + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(msg.toUtf8()); + if (doc.isEmpty() || doc.isNull() || !doc.isArray()) + { + qCritical() << "QAfbWsClient::onSocketTextMessageReceived: received an invalid message."; + return; + } + + QJsonArray arr = doc.array(); + AfMsgType msgType = static_cast(arr[0].toInt()); + int callId = arr[1].isString() ? arr[1].toString().toInt() : arr[1].toInt(); + QString api; + QJsonValue value; + + switch(msgType) + { + case AfMsgType::Call: + qCritical() << "QAfbWsClient::onSocketTextMessageReceived: Client received a call, which should not happen."; + break; + + case AfMsgType::Event: + qDebug() << "QAfbWsClient::onSocketTextMessageReceived: Client received an event."; + // TODO: handle events + value = arr[3]; + break; + + case AfMsgType::RetOk: + case AfMsgType::RetErr: + { + value = arr[2]; + QMap>::const_iterator it = m_calls.find(callId); + if (it == m_calls.end()) + { + qCritical() << "QAfbWsClient::onSocketTextMessageReceived: received a response to a not found query."; + return; + } + + QSharedPointer m = *it; + m_calls.remove(callId); + m->close(msgType, value); + } + break; + + default: + qCritical() << "QAfbWsClient::onSocketTextMessageReceived: Client received an unsupported message."; + break; + } +} + +void QAfbWsClient::open(const QUrl& url) +{ + qDebug() << "QAfbWsClient::open(" << url << ")"; + m_socket.open(url); +} + +void QAfbWsClient::close() +{ + qDebug() << "QAfbWsClient::close()"; + m_socket.close(); +} diff --git a/app/qafbwsclient.h b/app/qafbwsclient.h new file mode 100644 index 0000000..2576ecb --- /dev/null +++ b/app/qafbwsclient.h @@ -0,0 +1,50 @@ +#ifndef QAFBWSCLIENT_H +#define QAFBWSCLIENT_H + +#include +#include +#include +#include +#include + +enum class AfMsgType + : int +{ + Call = 2, + RetOk = 3, + RetErr = 4, + Event = 5 +}; + +class QAfbWsMsg; + +class QAfbWsClient + : public QObject +{ + Q_OBJECT +public: + explicit QAfbWsClient(QObject* parent = nullptr); + + void open(const QUrl& url); + void close(); + QUrl url() const; + QSharedPointer call(const QString &api, const QString &verb, const QJsonValue &args = QJsonValue()); + +signals: + void connected(); + void disconnected(); + void error(QAbstractSocket::SocketError err, QString errStr); + +private slots: + void onSocketConnected(); + void onSocketDisconnected(); + void onSocketError(QAbstractSocket::SocketError err); + void onSocketTextMessageReceived(QString msg); + +private: + int m_nextCallId; + QWebSocket m_socket; + QMap> m_calls; +}; + +#endif // QAFBWSCLIENT_H diff --git a/app/qafbwsmsg.cpp b/app/qafbwsmsg.cpp new file mode 100644 index 0000000..3f07cb0 --- /dev/null +++ b/app/qafbwsmsg.cpp @@ -0,0 +1,44 @@ +#include "qafbwsmsg.h" + +QAfbWsMsg::QAfbWsMsg(int callId, const QString& api, const QString& verb, QObject* parent) + : QObject{parent} + , m_callId{callId} +{ + m_api = api; + m_verb = verb; +} + +void QAfbWsMsg::close(AfMsgType type, const QJsonValue& result) +{ + m_type = type; + m_value = result; + + qDebug() << "QAfbWsMsg::close: type=" << static_cast(m_type) << ", api=" << m_api << ", verb=" << m_verb << ", value=" << m_value; + + emit closed(); +} + +int QAfbWsMsg::callId() const +{ + return m_callId; +} + +AfMsgType QAfbWsMsg::messageType() const +{ + return m_type; +} + +QString QAfbWsMsg::api() const +{ + return m_api; +} + +QString QAfbWsMsg::verb() const +{ + return m_verb; +} + +QJsonValue QAfbWsMsg::value() const +{ + return m_value; +} diff --git a/app/qafbwsmsg.h b/app/qafbwsmsg.h new file mode 100644 index 0000000..6acce59 --- /dev/null +++ b/app/qafbwsmsg.h @@ -0,0 +1,33 @@ +#ifndef QAFBWSMSG_H +#define QAFBWSMSG_H + +#include +#include +#include "qafbwsclient.h" + +class QAfbWsMsg + : public QObject +{ + Q_OBJECT +public: + explicit QAfbWsMsg(int callId = 0, const QString& api = "", const QString& verb = "", QObject* parent = nullptr); + void close(AfMsgType type, const QJsonValue& result); + + int callId() const; + AfMsgType messageType() const; + QString api() const; + QString verb() const; + QJsonValue value() const; + +signals: + void closed(); + +private: + int m_callId; + AfMsgType m_type; + QString m_api; + QString m_verb; + QJsonValue m_value; +}; + +#endif // QAFBWSMSG_H -- cgit 1.2.3-korg