diff options
author | Matt Porter <mporter@konsulko.com> | 2016-12-19 13:55:11 -0500 |
---|---|---|
committer | Matt Porter <mporter@konsulko.com> | 2016-12-21 08:24:39 -0500 |
commit | 392effc544e3d94b82f806378d4ac1d11a185422 (patch) | |
tree | 6467743066dd6153941529087fef244dc8639c24 /app |
AGL-style PulseAudio mixer app
Change-Id: I566050a1a8f241f140523df236de81ab951c1394
Signed-off-by: Matt Porter <mporter@konsulko.com>
Diffstat (limited to 'app')
-rw-r--r-- | app/Mixer.qml | 93 | ||||
-rw-r--r-- | app/Mixer.qrc | 5 | ||||
-rw-r--r-- | app/app.pri | 12 | ||||
-rw-r--r-- | app/app.pro | 18 | ||||
-rw-r--r-- | app/main.cpp | 56 | ||||
-rw-r--r-- | app/pac.c | 225 | ||||
-rw-r--r-- | app/pac.h | 54 | ||||
-rw-r--r-- | app/pacontrolmodel.cpp | 172 | ||||
-rw-r--r-- | app/pacontrolmodel.h | 87 |
9 files changed, 722 insertions, 0 deletions
diff --git a/app/Mixer.qml b/app/Mixer.qml new file mode 100644 index 0000000..7a099ce --- /dev/null +++ b/app/Mixer.qml @@ -0,0 +1,93 @@ +/* + * Copyright 2016 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. + */ + +import QtQuick 2.6 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.0 +import AGL.Demo.Controls 1.0 +import PaControlModel 1.0 + +ApplicationWindow { + id: root + + Label { + id: title + font.pixelSize: 48 + text: "Mixer" + 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 + } + } + + Component { + id: empty + Item { + } + } + + ListView { + id: listView + anchors.left: parent.left + anchors.top: title.bottom + anchors.margins: 80 + anchors.fill: parent + model: PaControlModel {} + delegate: ColumnLayout { + width: parent.width + spacing: 40 + Loader { + property int modelType: type + property int modelCIndex: cindex + property string modelDesc: desc + 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 { + 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 %" + } + } + } + } +} diff --git a/app/Mixer.qrc b/app/Mixer.qrc new file mode 100644 index 0000000..27f216c --- /dev/null +++ b/app/Mixer.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>Mixer.qml</file> + </qresource> +</RCC> diff --git a/app/app.pri b/app/app.pri new file mode 100644 index 0000000..014646f --- /dev/null +++ b/app/app.pri @@ -0,0 +1,12 @@ +TEMPLATE = app + +load(configure) +qtCompileTest(libhomescreen) + +config_libhomescreen { + CONFIG += link_pkgconfig + PKGCONFIG += homescreen + DEFINES += HAVE_LIBHOMESCREEN +} + +DESTDIR = $${OUT_PWD}/../package/root/bin diff --git a/app/app.pro b/app/app.pro new file mode 100644 index 0000000..e8fe05b --- /dev/null +++ b/app/app.pro @@ -0,0 +1,18 @@ +TARGET = mixer +QT = quickcontrols2 + +HEADERS += \ + pacontrolmodel.h \ + pac.h + +SOURCES = main.cpp \ + pacontrolmodel.cpp \ + pac.c + +CONFIG += link_pkgconfig +PKGCONFIG += libpulse + +RESOURCES += \ + Mixer.qrc + +include(app.pri) diff --git a/app/main.cpp b/app/main.cpp new file mode 100644 index 0000000..d89287c --- /dev/null +++ b/app/main.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 The Qt Company Ltd. + * Copyright (C) 2016 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 <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QStandardPaths> +#include <QtGui/QGuiApplication> +#include <QtQml/QQmlApplicationEngine> +#include <QtQml/QQmlContext> +#include <QtQml/qqml.h> +#include <QtQuickControls2/QQuickStyle> +#include <QtQuick/qquickitem.h> +#include <QtQuick/qquickview.h> + +#include "pacontrolmodel.h" + +#ifdef HAVE_LIBHOMESCREEN +#include <libhomescreen.hpp> +#endif + +int main(int argc, char *argv[]) +{ +#ifdef HAVE_LIBHOMESCREEN + LibHomeScreen libHomeScreen; + + if (!libHomeScreen.renderAppToAreaAllowed(0, 1)) { + qWarning() << "renderAppToAreaAllowed is denied"; + return -1; + } +#endif + + QGuiApplication app(argc, argv); + + QQuickStyle::setStyle("AGL"); + + qmlRegisterType<PaControlModel>("PaControlModel", 1, 0, "PaControlModel"); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/Mixer.qml"))); + + return app.exec(); +} diff --git a/app/pac.c b/app/pac.c new file mode 100644 index 0000000..83b4604 --- /dev/null +++ b/app/pac.c @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2016 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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <signal.h> +#include <errno.h> +#include <unistd.h> +#include <limits.h> +#include <assert.h> + +#include <pulse/pulseaudio.h> +#include <sys/queue.h> + +#include "pacontrolmodel.h" +#include "pac.h" + +/* FIXME: move these into a pac context */ +static pa_threaded_mainloop* m; +TAILQ_HEAD(pac_cstateq, pac_cstate); +static struct pac_cstateq cstateq; + +static void add_one_cstate(int type, int index, const pa_cvolume *cvolume) +{ + struct pac_cstate *cstate; + int i; + + cstate = pa_xnew(struct pac_cstate, 1); + cstate->type = type; + cstate->index = index; + cstate->cvolume.channels = cvolume->channels; + for (i = 0; i < cvolume->channels; i++) + cstate->cvolume.values[i] = cvolume->values[i]; + + TAILQ_INSERT_TAIL(&cstateq, cstate, tailq); +} + +static void get_source_list_cb(pa_context *c, + const pa_source_info *i, + int eol, + void *data) +{ + int chan; + + if (eol < 0) { + fprintf(stderr, "get source list: %s\n", + pa_strerror(pa_context_errno(c))); + + pa_threaded_mainloop_stop(m); + return; + } + + if (!eol) { + assert(i); + add_one_cstate(C_SOURCE, i->index, &i->volume); + for (chan = 0; chan < i->channel_map.channels; chan++) { + add_one_control(data, i->index, i->description, + C_SOURCE, chan, + channel_position_string[i->channel_map.map[chan]], + i->volume.values[chan]); + } + } +} + +static void get_sink_list_cb(pa_context *c, + const pa_sink_info *i, + int eol, + void *data) +{ + int chan; + + if(eol < 0) { + fprintf(stderr, "get sink list: %s\n", + pa_strerror(pa_context_errno(c))); + + pa_threaded_mainloop_stop(m); + return; + } + + if(!eol) { + assert(i); + add_one_cstate(C_SINK, i->index, &i->volume); + for (chan = 0; chan < i->channel_map.channels; chan++) { + add_one_control(data, i->index, i->description, + C_SINK, chan, + channel_position_string[i->channel_map.map[chan]], + i->volume.values[chan]); + } + } +} + +static void context_state_cb(pa_context *c, void *data) { + pa_operation *o; + + assert(c); + 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))) { + fprintf(stderr, "get source info list: %s\n", + 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))) { + fprintf(stderr, "get sink info list: %s\n", + pa_strerror(pa_context_errno(c))); + return; + } + break; + case PA_CONTEXT_TERMINATED: + pa_threaded_mainloop_stop(m); + break; + case PA_CONTEXT_FAILED: + default: + fprintf(stderr, "PA connection failed: %s\n", + pa_strerror(pa_context_errno(c))); + pa_threaded_mainloop_stop(m); + } +} + +static void pac_set_source_volume_cb(pa_context *c, int success, void *userdata __attribute__((unused))) { + assert(c); + if (!success) + fprintf(stderr, "Set source volume: %s\n", + pa_strerror(pa_context_errno(c))); +} + +static void pac_set_sink_volume_cb(pa_context *c, int success, void *userdata __attribute__((unused))) { + assert(c); + if (!success) + fprintf(stderr, "Set source volume: %s\n", + pa_strerror(pa_context_errno(c))); +} + +void pac_set_volume(pa_context *c, uint32_t type, uint32_t idx, uint32_t channel, uint32_t volume) +{ + pa_operation *o; + struct pac_cstate *cstate; + + TAILQ_FOREACH(cstate, &cstateq, tailq) + if (cstate->index == idx) + break; + cstate->cvolume.values[channel] = volume; + + if (type == C_SOURCE) { + if (!(o = pa_context_set_source_volume_by_index(c, idx, &cstate->cvolume, pac_set_source_volume_cb, NULL))) { + fprintf(stderr, "set source #%d channel #%d volume: %s\n", + idx, channel, pa_strerror(pa_context_errno(c))); + return; + } + pa_operation_unref(o); + } else if (type == C_SINK) { + if (!(o = pa_context_set_sink_volume_by_index(c, idx, &cstate->cvolume, pac_set_sink_volume_cb, NULL))) { + fprintf(stderr, "set sink #%d channel #%d volume: %s\n", + idx, channel, pa_strerror(pa_context_errno(c))); + return; + } + pa_operation_unref(o); + } +} + +pa_context *pac_init(void *this, const char *name) { + pa_context *c; + pa_mainloop_api *mapi; + char *server = NULL; + char *client = pa_xstrdup(name); + + TAILQ_INIT(&cstateq); + + if (!(m = pa_threaded_mainloop_new())) { + fprintf(stderr, "pa_mainloop_new() failed.\n"); + return NULL; + } + + pa_threaded_mainloop_set_name(m, "pa_mainloop"); + mapi = pa_threaded_mainloop_get_api(m); + + if (!(c = pa_context_new(mapi, client))) { + fprintf(stderr, "pa_context_new() failed.\n"); + goto exit; + } + + pa_context_set_state_callback(c, context_state_cb, this); + if (pa_context_connect(c, server, 0, NULL) < 0) { + fprintf(stderr, "pa_context_connect(): %s", pa_strerror(pa_context_errno(c))); + goto exit; + } + + if (pa_threaded_mainloop_start(m) < 0) { + fprintf(stderr, "pa_mainloop_run() failed.\n"); + goto exit; + } + + return c; + +exit: + if (c) + pa_context_unref(c); + + if (m) + pa_threaded_mainloop_free(m); + + pa_xfree(client); + + return NULL; +} diff --git a/app/pac.h b/app/pac.h new file mode 100644 index 0000000..ef0209d --- /dev/null +++ b/app/pac.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 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 <pulse/pulseaudio.h> +#include <sys/queue.h> + +#ifdef __cplusplus +extern "C" void pac_set_volume(pa_context *, uint32_t, uint32_t, uint32_t, uint32_t); +extern "C" pa_context *pac_init(void *, const char *); +#else +static char * 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 +}; + +struct pac_cstate +{ + TAILQ_ENTRY(pac_cstate) tailq; + int type; + uint32_t index; + pa_cvolume cvolume; +}; + +#endif diff --git a/app/pacontrolmodel.cpp b/app/pacontrolmodel.cpp new file mode 100644 index 0000000..520233b --- /dev/null +++ b/app/pacontrolmodel.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2016 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" +#include "pac.h" + +PaControl::PaControl(const quint32 &cindex, const QString &desc, const quint32 &type, const quint32 &channel, const QString &cdesc, const quint32 &volume) + : m_cindex(cindex), m_desc(desc), m_type(type), m_channel(channel), m_cdesc(cdesc), m_volume(volume) +{ +} + +quint32 PaControl::cindex() const +{ + return m_cindex; +} + +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::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(pa_context *pa_ctx, const QVariant &volume) +{ + if (volume != m_volume) { + m_volume = volume.toUInt(); + pac_set_volume(pa_ctx, type(), cindex(), channel(), m_volume); + } +} + +PaControlModel::PaControlModel(QObject *parent) + : QAbstractListModel(parent) +{ + pa_ctx = pac_init(this, "Mixer"); +} + +void PaControlModel::addControl(const PaControl &control) +{ + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_controls << control; + endInsertRows(); +} + +void add_one_control(void *ctx, int cindex, const char *desc, int type, int channel, const char *cdesc, int volume) +{ + // Get the PaControlModel object from the opaque pointer context + PaControlModel *pacm = static_cast<PaControlModel*>(ctx); + pacm->addControl(PaControl(cindex, desc, type, channel, cdesc, volume)); +} + +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 == 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(pa_ctx, value); + emit dataChanged(index, index); + 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 == 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<int, QByteArray> PaControlModel::roleNames() const { + QHash<int, QByteArray> roles; + roles[CIndexRole] = "cindex"; + 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 new file mode 100644 index 0000000..aa34a79 --- /dev/null +++ b/app/pacontrolmodel.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016 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 <pulse/pulseaudio.h> + +#ifndef __cplusplus +extern void add_one_control(void *ctx, int, const char *, int, int, const char *, int); +#else +extern "C" void add_one_control(void *ctx, int, const char *, int, int, const char *, int); + +#include <QAbstractListModel> +#include <QStringList> + +class PaControlModel; + +class PaControl +{ + public: + PaControl(const quint32 &index, const QString &desc, const quint32 &type, const quint32 &channel, const QString &cdesc, const quint32 &volume); + + quint32 cindex() const; + QString desc() const; + quint32 type() const; + quint32 channel() const; + QString cdesc() const; + quint32 volume() const; + void setCIndex(const QVariant&); + void setDesc(const QVariant&); + void setType(const QVariant&); + void setChannel(const QVariant&); + void setCDesc(const QVariant&); + void setVolume(pa_context *, const QVariant&); + + private: + quint32 m_cindex; + 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, + 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; + + protected: + QHash<int, QByteArray> roleNames() const; + private: + QList<PaControl> m_controls; + pa_context *pa_ctx; +}; +#endif // __cplusplus |