Add support for handling external sink/source volume change events
[apps/mixer.git] / app / paclient.cpp
1 /*
2  * Copyright (C) 2016,2017 Konsulko Group
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #include "paclient.h"
18
19 #include <QtCore/QDebug>
20
21 PaClient::PaClient()
22         : m_init(false), m_ml(nullptr), m_mlapi(nullptr), m_ctx(nullptr)
23 {
24 }
25
26 PaClient::~PaClient()
27 {
28         if (m_init)
29                 close();
30 }
31
32 void PaClient::close()
33 {
34         if (!m_init) return;
35         pa_threaded_mainloop_stop(m_ml);
36         pa_threaded_mainloop_free(m_ml);
37         m_init = false;
38 }
39
40 static void set_sink_volume_cb(pa_context *c, int success, void *data)
41 {
42         Q_UNUSED(data);
43
44         if (!success)
45                 qWarning() << "PaClient: set sink volume: " <<
46                         pa_strerror(pa_context_errno(c));
47 }
48
49 static void set_source_volume_cb(pa_context *c, int success, void *data)
50 {
51         Q_UNUSED(data);
52
53         if (!success)
54                 qWarning() << "PaClient: set source volume: " <<
55                         pa_strerror(pa_context_errno(c));
56 }
57
58 void PaClient::setVolume(uint32_t type, uint32_t index, uint32_t channel, uint32_t volume)
59 {
60         pa_operation *o;
61         pa_context *c = context();
62         pa_cvolume *cvolume = NULL;
63
64         if (type == C_SINK) {
65                 cvolume = m_sink_states.value(index);
66                 cvolume->values[channel] = volume;
67                 if (!(o = pa_context_set_sink_volume_by_index(c, index, cvolume, set_sink_volume_cb, NULL))) {
68                         qWarning() << "PaClient: set sink #" << index <<
69                                 " channel #" << channel <<
70                                 " volume: " << pa_strerror(pa_context_errno(c));
71                         return;
72                 }
73                 pa_operation_unref(o);
74         } else if (type == C_SOURCE) {
75                 cvolume = m_source_states.value(index);
76                 cvolume->values[channel] = volume;
77                 if (!(o = pa_context_set_source_volume_by_index(c, index, cvolume, set_source_volume_cb, NULL))) {
78                         qWarning() << "PaClient: set source #" << index <<
79                                 " channel #" << channel <<
80                                 " volume: " << pa_strerror(pa_context_errno(c));
81                         return;
82                 }
83                 pa_operation_unref(o);
84         }
85 }
86
87 void get_source_list_cb(pa_context *c,
88                 const pa_source_info *i,
89                 int eol,
90                 void *data)
91 {
92         int chan;
93
94         PaClient *self = reinterpret_cast<PaClient*>(data);
95
96         if (eol < 0) {
97                 qWarning() << "PaClient: get source list: " <<
98                         pa_strerror(pa_context_errno(c));
99
100                 self->close();
101                 return;
102         }
103
104         if (!eol) {
105                 self->addOneControlState(C_SOURCE, i->index, &i->volume);
106                 for (chan = 0; chan < i->channel_map.channels; chan++) {
107                         emit self->controlAdded(i->index, QString(i->description), C_SOURCE, chan,
108                                                 channel_position_string[i->channel_map.map[chan]],
109                                                 i->volume.values[chan]);
110                 }
111         }
112 }
113
114 void get_sink_list_cb(pa_context *c,
115                 const pa_sink_info *i,
116                 int eol,
117                 void *data)
118 {
119         PaClient *self = reinterpret_cast<PaClient*>(data);
120         int chan;
121
122         if(eol < 0) {
123                 qWarning() << "PaClient: get sink list: " <<
124                         pa_strerror(pa_context_errno(c));
125                 self->close();
126                 return;
127         }
128
129         if(!eol) {
130                 self->addOneControlState(C_SINK, i->index, &i->volume);
131                 for (chan = 0; chan < i->channel_map.channels; chan++) {
132                         emit self->controlAdded(i->index, QString(i->description), C_SINK, chan,
133                                                  channel_position_string[i->channel_map.map[chan]],
134                                                  i->volume.values[chan]);
135                 }
136         }
137 }
138
139 void get_sink_info_change_cb(pa_context *c,
140                              const pa_sink_info *i,
141                              int eol,
142                              void *data)
143 {
144         Q_UNUSED(c);
145         Q_ASSERT(i);
146         Q_ASSERT(data);
147
148         if (eol) return;
149
150         for (int chan = 0; chan < i->channel_map.channels; chan++) {
151                 PaClient *self = reinterpret_cast<PaClient*>(data);
152                 QHash<int, pa_cvolume *> states = self->sink_states();
153                 pa_cvolume *cvolume = states.value(i->index);
154                 // Check each channel for volume change
155                 if (cvolume->values[chan] != i->volume.values[chan]) {
156                         // On change, update cache and signal
157                         cvolume->values[chan] = i->volume.values[chan];
158                         emit self->volumeExternallyChanged(C_SINK, i->index, chan, i->volume.values[chan]);
159                 }
160         }
161 }
162
163 void get_source_info_change_cb(pa_context *c,
164                                const pa_source_info *i,
165                                int eol,
166                                void *data)
167 {
168         Q_UNUSED(c);
169         Q_ASSERT(i);
170         Q_ASSERT(data);
171
172         if (eol) return;
173
174         for (int chan = 0; chan < i->channel_map.channels; chan++) {
175                 PaClient *self = reinterpret_cast<PaClient*>(data);
176                 QHash<int, pa_cvolume *> states = self->source_states();
177                 pa_cvolume *cvolume = states.value(i->index);
178                 // Check each channel for volume change
179                 if (cvolume->values[chan] != i->volume.values[chan]) {
180                         // On change, update cache and signal
181                         cvolume->values[chan] = i->volume.values[chan];
182                         emit self->volumeExternallyChanged(C_SOURCE, i->index, chan, i->volume.values[chan]);
183                 }
184         }
185 }
186
187
188 void subscribe_cb(pa_context *c,
189                   pa_subscription_event_type_t type,
190                   uint32_t index,
191                   void *data)
192 {
193         pa_operation *o;
194
195         if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE) {
196                 qWarning("PaClient: unhandled subscribe event operation");
197                 return;
198         }
199
200         switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
201                 case PA_SUBSCRIPTION_EVENT_SINK:
202                         if (!(o = pa_context_get_sink_info_by_index(c, index, get_sink_info_change_cb, data))) {
203                                 qWarning() << "PaClient: get sink info by index: " <<
204                                         pa_strerror(pa_context_errno(c));
205                                 return;
206                         }
207                         break;
208                 case PA_SUBSCRIPTION_EVENT_SOURCE:
209                         if (!(o = pa_context_get_source_info_by_index(c, index, get_source_info_change_cb, data))) {
210                                 qWarning() << "PaClient: get source info by index: " <<
211                                         pa_strerror(pa_context_errno(c));
212                                 return;
213                         }
214                         break;
215                 default:
216                         qWarning("PaClient: unhandled subscribe event facility");
217         }
218 }
219
220 void context_state_cb(pa_context *c, void *data)
221 {
222         pa_operation *o;
223         PaClient *self = reinterpret_cast<PaClient*>(data);
224
225         switch (pa_context_get_state(c)) {
226                 case PA_CONTEXT_CONNECTING:
227                 case PA_CONTEXT_AUTHORIZING:
228                 case PA_CONTEXT_SETTING_NAME:
229                         break;
230                 case PA_CONTEXT_READY:
231                         // Fetch the controls of interest
232                         if (!(o = pa_context_get_source_info_list(c, get_source_list_cb, data))) {
233                                 qWarning() << "PaClient: get source info list: " <<
234                                         pa_strerror(pa_context_errno(c));
235                                 return;
236                         }
237                         pa_operation_unref(o);
238                         if (!(o = pa_context_get_sink_info_list(c, &get_sink_list_cb, data))) {
239                                 qWarning() << "PaClient: get sink info list: " <<
240                                         pa_strerror(pa_context_errno(c));
241                                 return;
242                         }
243                         pa_operation_unref(o);
244                         pa_context_set_subscribe_callback(c, subscribe_cb, data);
245                         if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE), NULL, NULL))) {
246                                 qWarning() << "PaClient: subscribe: " <<
247                                         pa_strerror(pa_context_errno(c));
248                                 return;
249                         }
250                         break;
251                 case PA_CONTEXT_TERMINATED:
252                         self->close();
253                         break;
254
255                 case PA_CONTEXT_FAILED:
256                 default:
257                         qCritical() << "PaClient: connection failed: " <<
258                                 pa_strerror(pa_context_errno(c));
259                         self->close();
260                         break;
261         }
262 }
263
264 void PaClient::init()
265 {
266         m_ml = pa_threaded_mainloop_new();
267         if (!m_ml) {
268                 qCritical("PaClient: failed to create mainloop");
269                 return;
270         }
271
272         pa_threaded_mainloop_set_name(m_ml, "PaClient mainloop");
273
274         m_mlapi = pa_threaded_mainloop_get_api(m_ml);
275
276         lock();
277
278         m_ctx = pa_context_new(m_mlapi, "Mixer");
279         if (!m_ctx) {
280                 qCritical("PaClient: failed to create context");
281                 pa_threaded_mainloop_free(m_ml);
282                 return;
283         }
284         pa_context_set_state_callback(m_ctx, context_state_cb, this);
285
286         if (pa_context_connect(m_ctx, 0, (pa_context_flags_t)0, 0) < 0) {
287                 qCritical("PaClient: failed to connect");
288                 pa_context_unref(m_ctx);
289                 pa_threaded_mainloop_free(m_ml);
290                 return;
291         }
292
293         if (pa_threaded_mainloop_start(m_ml) != 0) {
294                 qCritical("PaClient: failed to start mainloop");
295                 pa_context_unref(m_ctx);
296                 pa_threaded_mainloop_free(m_ml);
297                 return;
298         }
299
300         unlock();
301
302         m_init = true;
303 }
304
305 void PaClient::addOneControlState(int type, int index, const pa_cvolume *cvolume)
306 {
307         pa_cvolume *cvolume_new = new pa_cvolume;
308         cvolume_new->channels = cvolume->channels;
309         for (int i = 0; i < cvolume->channels; i++)
310                 cvolume_new->values[i] = cvolume->values[i];
311         if (type == C_SINK)
312                 m_sink_states.insert(index, cvolume_new);
313         else if (type == C_SOURCE)
314                 m_source_states.insert(index, cvolume_new);
315 }
316
317 QHash<int, pa_cvolume *> PaClient::sink_states(void)
318 {
319         return m_sink_states;
320 }
321
322 QHash<int, pa_cvolume *> PaClient::source_states(void)
323 {
324         return m_source_states;
325 }