diff options
-rw-r--r-- | app/Mixer.qml | 2 | ||||
-rw-r--r-- | app/app.pro | 4 | ||||
-rw-r--r-- | app/main.cpp | 25 | ||||
-rw-r--r-- | app/pac.c | 225 | ||||
-rw-r--r-- | app/pac.h | 54 | ||||
-rw-r--r-- | app/paclient.cpp | 228 | ||||
-rw-r--r-- | app/paclient.h | 89 | ||||
-rw-r--r-- | app/pacontrolmodel.cpp | 16 | ||||
-rw-r--r-- | app/pacontrolmodel.h | 20 |
9 files changed, 359 insertions, 304 deletions
diff --git a/app/Mixer.qml b/app/Mixer.qml index 7a099ce..75673fe 100644 --- a/app/Mixer.qml +++ b/app/Mixer.qml @@ -53,7 +53,7 @@ ApplicationWindow { anchors.top: title.bottom anchors.margins: 80 anchors.fill: parent - model: PaControlModel {} + model: PaControlModel { objectName: "pacm" } delegate: ColumnLayout { width: parent.width spacing: 40 diff --git a/app/app.pro b/app/app.pro index e8fe05b..34c37f4 100644 --- a/app/app.pro +++ b/app/app.pro @@ -3,11 +3,11 @@ QT = quickcontrols2 HEADERS += \ pacontrolmodel.h \ - pac.h + paclient.h SOURCES = main.cpp \ pacontrolmodel.cpp \ - pac.c + paclient.cpp CONFIG += link_pkgconfig PKGCONFIG += libpulse diff --git a/app/main.cpp b/app/main.cpp index d89287c..76e755b 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,6 +1,6 @@ /* * Copyright (C) 2016 The Qt Company Ltd. - * Copyright (C) 2016 Konsulko Group + * 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. @@ -15,9 +15,13 @@ * limitations under the License. */ +#include "paclient.h" +#include "pacontrolmodel.h" + #include <QtCore/QDebug> #include <QtCore/QDir> #include <QtCore/QStandardPaths> +#include <QtCore/QThread> #include <QtGui/QGuiApplication> #include <QtQml/QQmlApplicationEngine> #include <QtQml/QQmlContext> @@ -26,7 +30,6 @@ #include <QtQuick/qquickitem.h> #include <QtQuick/qquickview.h> -#include "pacontrolmodel.h" #ifdef HAVE_LIBHOMESCREEN #include <libhomescreen.hpp> @@ -47,10 +50,28 @@ int main(int argc, char *argv[]) QQuickStyle::setStyle("AGL"); + // 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>("PaControlModel", 1, 0, "PaControlModel"); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/Mixer.qml"))); + // Find the instantiated model QObject and connect the signals/slots + QList<QObject *> mobjs = engine.rootObjects(); + PaControlModel *pacm = mobjs.first()->findChild<PaControlModel *>("pacm"); + QObject::connect(client, SIGNAL(controlAdded(int, QString, int, int, const char *, int)), + pacm, SLOT(addOneControl(int, QString, int, int, const char *, int))); + 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/pac.c b/app/pac.c deleted file mode 100644 index 83b4604..0000000 --- a/app/pac.c +++ /dev/null @@ -1,225 +0,0 @@ -/* - * 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 deleted file mode 100644 index ef0209d..0000000 --- a/app/pac.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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/paclient.cpp b/app/paclient.cpp new file mode 100644 index 0000000..78567ce --- /dev/null +++ b/app/paclient.cpp @@ -0,0 +1,228 @@ +/* + * 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 <QtCore/QDebug> + +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 __attribute__((unused))) +{ + 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 __attribute__((unused))) +{ + 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(); + CState *cstate = NULL; + + foreach (cstate, m_cstatelist) + if ((cstate->index == index) && (cstate->type == type)) + break; + + cstate->cvolume.values[channel] = volume; + + if (type == C_SINK) { + if (!(o = pa_context_set_sink_volume_by_index(c, index, &cstate->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) { + if (!(o = pa_context_set_source_volume_by_index(c, index, &cstate->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<PaClient*>(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++) { + emit self->controlAdded(i->index, 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) +{ + int chan; + PaClient *self = reinterpret_cast<PaClient*>(data); + + 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->description), C_SINK, chan, + channel_position_string[i->channel_map.map[chan]], + i->volume.values[chan]); + } + } +} + +void context_state_cb(pa_context *c, void *data) +{ + pa_operation *o; + PaClient *self = reinterpret_cast<PaClient*>(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; + } + 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) +{ + int i; + CState *cstate; + + cstate = new CState; + 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]; + + m_cstatelist.append(cstate); +} diff --git a/app/paclient.h b/app/paclient.h new file mode 100644 index 0000000..1367e81 --- /dev/null +++ b/app/paclient.h @@ -0,0 +1,89 @@ +/* + * 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 <pulse/pulseaudio.h> + +#include <QtCore/QObject> + +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); + + public slots: + void setVolume(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume); + + signals: + void controlAdded(int cindex, QString desc, int type, int channel, const char *cdesc, int volume); + + private: + bool m_init; + pa_threaded_mainloop *m_ml; + pa_mainloop_api *m_mlapi; + pa_context *m_ctx; + QList<CState *> m_cstatelist; +}; diff --git a/app/pacontrolmodel.cpp b/app/pacontrolmodel.cpp index 520233b..bca72c5 100644 --- a/app/pacontrolmodel.cpp +++ b/app/pacontrolmodel.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Konsulko Group + * 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. @@ -15,7 +15,6 @@ */ #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) @@ -79,18 +78,17 @@ void PaControl::setCDesc(const QVariant &cdesc) m_cdesc = cdesc.toString(); } -void PaControl::setVolume(pa_context *pa_ctx, const QVariant &volume) +void PaControl::setVolume(PaControlModel *pacm, const QVariant &volume) { if (volume != m_volume) { m_volume = volume.toUInt(); - pac_set_volume(pa_ctx, type(), cindex(), channel(), m_volume); + emit pacm->volumeChanged(type(), cindex(), channel(), m_volume); } } PaControlModel::PaControlModel(QObject *parent) : QAbstractListModel(parent) { - pa_ctx = pac_init(this, "Mixer"); } void PaControlModel::addControl(const PaControl &control) @@ -100,11 +98,9 @@ void PaControlModel::addControl(const PaControl &control) endInsertRows(); } -void add_one_control(void *ctx, int cindex, const char *desc, int type, int channel, const char *cdesc, int volume) +void PaControlModel::addOneControl(int cindex, QString 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)); + addControl(PaControl(cindex, desc, type, channel, cdesc, volume)); } int PaControlModel::rowCount(const QModelIndex & parent) const { @@ -127,7 +123,7 @@ bool PaControlModel::setData(const QModelIndex &index, const QVariant &value, in else if (role == CDescRole) control.setCDesc(value); else if (role == VolumeRole) - control.setVolume(pa_ctx, value); + control.setVolume(this, value); emit dataChanged(index, index); return true; } diff --git a/app/pacontrolmodel.h b/app/pacontrolmodel.h index aa34a79..475f7ce 100644 --- a/app/pacontrolmodel.h +++ b/app/pacontrolmodel.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Konsulko Group + * 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. @@ -16,13 +16,8 @@ #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> +#include <QtCore/QAbstractListModel> +#include <QtCore/QList> class PaControlModel; @@ -42,7 +37,7 @@ class PaControl void setType(const QVariant&); void setChannel(const QVariant&); void setCDesc(const QVariant&); - void setVolume(pa_context *, const QVariant&); + void setVolume(PaControlModel *, const QVariant&); private: quint32 m_cindex; @@ -78,10 +73,15 @@ class PaControlModel : public QAbstractListModel Qt::ItemFlags flags(const QModelIndex &index) const; + public slots: + void addOneControl(int cindex, QString desc, int type, int channel, const char *cdesc, int volume); + + signals: + void volumeChanged(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume); + protected: QHash<int, QByteArray> roleNames() const; private: QList<PaControl> m_controls; pa_context *pa_ctx; }; -#endif // __cplusplus |