]> mj.ucw.cz Git - ursary.git/blob - ursaryd.c
Generalized client groups
[ursary.git] / ursaryd.c
1 /*
2  *      The Ursary Audio Controls
3  *
4  *      (c) 2014 Martin Mares <mj@ucw.cz>
5  */
6
7 #undef LOCAL_DEBUG
8
9 #include <ucw/lib.h>
10 #include <ucw/clists.h>
11 #include <ucw/daemon.h>
12 #include <ucw/log.h>
13 #include <ucw/mainloop.h>
14 #include <ucw/opt.h>
15 #include <ucw/stkstring.h>
16
17 #include <signal.h>
18 #include <stdio.h>
19 #include <string.h>
20 #include <stdlib.h>
21 #include <syslog.h>
22
23 #include "ursaryd.h"
24
25 /*
26  *      Map of all controls
27  *
28  *              rotary          red button      green button
29  *      0       sink Ursarium   mute            select as default (or assign to client selected by touch)
30  *      1       sink Catarium   mute            dtto
31  *      2       sink Compress   -               dtto
32  *      3       -               -               -
33  *      4       MPD             mute            play/pause/stop
34  *      5       Albireo         mute            -
35  *      6       Ogion           mute            -
36  *      7       Ursula/Havrana  mute            -
37  *
38  *      center  all sinks
39  *      slider  -
40  */
41
42 /*** Sink controls ***/
43
44 static double volume_from_pa(pa_volume_t vol)
45 {
46   return (double) vol / PA_VOLUME_NORM;
47 }
48
49 static pa_volume_t volume_to_pa(double vol)
50 {
51   return vol * PA_VOLUME_NORM + 0.0001;
52 }
53
54 static void update_ring_from_sink(int ring, const char *sink_name)
55 {
56   struct pulse_sink *s = pulse_sink_by_name(sink_name);
57   if (!s)
58     {
59       noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
60       noct_set_button(ring, 0);
61       return;
62     }
63
64   if (s->mute)
65     {
66       noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
67       noct_set_button(ring, 1);
68       return;
69     }
70
71   double vol = CLAMP(volume_from_pa(s->volume), 0, 1);
72   noct_set_ring(ring, RING_MODE_LEFT, CLAMP((int)(0x7f * vol), 12, 0x7f));
73   noct_set_button(ring, 0);
74 }
75
76 static void update_sink_from_rotary(int delta, const char *sink_name)
77 {
78   struct pulse_sink *s = pulse_sink_by_name(sink_name);
79   if (!s)
80     return;
81
82   double vol = volume_from_pa(s->volume) + delta * 0.02;
83   pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, 1));
84   if (pavol == s->volume)
85     return;
86   pa_cvolume cvol;
87   pa_cvolume_set(&cvol, s->channels, pavol);
88
89   DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]);
90   pulse_sink_set_volume(s->idx, &cvol);
91 }
92
93 static void update_sink_mute_from_button(int on, const char *sink_name)
94 {
95   if (!on)
96     return;
97
98   struct pulse_sink *s = pulse_sink_by_name(sink_name);
99   if (!s)
100     return;
101
102   DBG("## Setting mute of sink %s to %d", s->name, !s->mute);
103   pulse_sink_set_mute(s->idx, !s->mute);
104 }
105
106 /*** Client controls ***/
107
108 struct client_map {
109   int group;
110   const char *client;
111   const char *host;
112 };
113
114 static struct client_map client_map[] = {
115   { 4, "Music Player Daemon",   "albireo",      },
116   { 5, NULL,                    "albireo",      },
117   { 6, NULL,                    "ogion",        },
118   { 7, NULL,                    "ursula",       },
119   { 7, NULL,                    "havrana",      },
120 };
121
122 #define CLIENT_MAP_SIZE ARRAY_SIZE(client_map)
123
124 struct group_config {
125   bool enabled;
126   double range;
127 };
128
129 struct group_state {
130   double volume;
131   bool have_muted[2];
132 };
133
134 #define NUM_GROUPS 9
135
136 static struct group_config group_config[NUM_GROUPS] = {
137   [4] = { .enabled = 1, .range = 1.5 },
138   [5] = { .enabled = 1, .range = 1.5 },
139   [6] = { .enabled = 1, .range = 1.5 },
140   [7] = { .enabled = 1, .range = 1.5 },
141 };
142
143 static struct group_state group_state[NUM_GROUPS];
144
145 static void calc_groups(void)
146 {
147   bzero(group_state, sizeof(group_state));
148
149   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
150     {
151       s->noct_group_idx = -1;
152
153       if (s->client_idx < 0 || s->sink_idx < 0)
154         continue;
155
156       struct pulse_client *c = pulse_client_by_idx(s->client_idx);
157       if (!c)
158         continue;
159
160       for (uns i=0; i < CLIENT_MAP_SIZE; i++)
161         {
162           struct client_map *cm = &client_map[i];
163           if ((!cm->client || !strcmp(cm->client, c->name)) &&
164               (!cm->host || !strcmp(cm->host, c->host)))
165             {
166               int g = cm->group;
167               struct group_state *gs = &group_state[g];
168               DBG("@@ Client #%d, sink input #%d -> group %d", s->client_idx, s->idx, g);
169               s->noct_group_idx = g;
170               gs->volume = MAX(gs->volume, s->volume);
171               gs->have_muted[!!s->mute] = 1;
172               break;
173             }
174         }
175     }
176 }
177
178 static void update_groups(void)
179 {
180   calc_groups();
181
182   for (uns i=0; i < NUM_GROUPS; i++)
183     {
184       struct group_config *gc = &group_config[i];
185       struct group_state *gs = &group_state[i];
186       if (!gc->enabled)
187         continue;
188
189       DBG("@@ Group #%d: mute=%d/%d volume=%.3f/%.3f", i, gs->have_muted[0], gs->have_muted[1], volume_from_pa(gs->volume), gc->range);
190
191       if (!gs->have_muted[0] && !gs->have_muted[1])
192         {
193           noct_set_ring(i, RING_MODE_LEFT, 0);
194           noct_set_button(i, 0);
195         }
196       else if (!gs->have_muted[0])
197         {
198           noct_set_ring(i, RING_MODE_SINGLE_ON, 0x7f);
199           noct_set_button(i, 1);
200         }
201       else
202         {
203           double vol = CLAMP(volume_from_pa(gs->volume), 0, gc->range);
204           int val = 0x7f * vol / gc->range;
205           val = CLAMP(val, 12, 0x7f);
206           noct_set_ring(i, RING_MODE_LEFT, val);
207           noct_set_button(i, 0);
208         }
209     }
210 }
211
212 static void update_group_from_rotary(int i, int delta)
213 {
214   if (i >= NUM_GROUPS)
215     return;
216   struct group_config *gc = &group_config[i];
217   struct group_state *gs = &group_state[i];
218   if (!gc->enabled)
219     return;
220
221   calc_groups();
222   double vol = volume_from_pa(gs->volume) + delta*0.02;
223   pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, gc->range));
224
225   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
226     {
227       if (s->noct_group_idx == i && s->volume != pavol)
228         {
229           DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol);
230           pa_cvolume cvol;
231           pa_cvolume_set(&cvol, s->channels, pavol);
232           pulse_sink_input_set_volume(s->idx, &cvol);
233         }
234     }
235 }
236
237 static void update_group_from_button(int i, int on)
238 {
239   if (!on)
240     return;
241   if (i >= NUM_GROUPS)
242     return;
243   struct group_config *gc = &group_config[i];
244   struct group_state *gs = &group_state[i];
245   if (!gc->enabled)
246     return;
247
248   calc_groups();
249   if (!gs->have_muted[0] && !gs->have_muted[1])
250     return;
251   uns mute = !gs->have_muted[1];
252
253   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
254     {
255       if (s->noct_group_idx == i)
256         {
257           DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute);
258           pulse_sink_input_set_mute(s->idx, mute);
259         }
260     }
261 }
262
263 static int find_touched_client(void)
264 {
265   int touched = -1;
266
267   for (uns i=0; i < NUM_GROUPS; i++)
268     if (group_config[i].enabled && noct_rotary_touched[i])
269       {
270         if (touched >= 0)
271           return -1;
272         touched = i;
273       }
274   return touched;
275 }
276
277 /*** Default sink controls ***/
278
279 static const char *get_client_sink(int i)
280 {
281   const char *sink = NULL;
282
283   CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
284     if (s->noct_group_idx == i)
285       {
286         struct pulse_sink *sk = (s->sink_idx >= 0) ? pulse_sink_by_idx(s->sink_idx) : NULL;
287         const char *ss = sk ? sk->name : NULL;
288         if (!sink)
289           sink = ss;
290         else if (strcmp(sink, ss))
291           sink = "?";
292       }
293   return sink ? : "?";
294 }
295
296 static void update_default_sink(void)
297 {
298   int i = find_touched_client();
299   const char *sink;
300   if (i >= 0)
301     sink = get_client_sink(i);
302   else
303     sink = pulse_default_sink_name ? : "?";
304
305   if (!strcmp(sink, "ursarium"))
306     {
307       noct_set_button(8, 1);
308       noct_set_button(9, 0);
309       noct_set_button(10, 0);
310     }
311   else if (!strcmp(sink, "catarium"))
312     {
313       noct_set_button(8, 0);
314       noct_set_button(9, 1);
315       noct_set_button(10, 0);
316     }
317   else if (!strcmp(sink, "compress"))
318     {
319       noct_set_button(8, 0);
320       noct_set_button(9, 0);
321       noct_set_button(10, 1);
322     }
323   else
324     {
325       noct_set_button(8, 0);
326       noct_set_button(9, 0);
327       noct_set_button(10, 0);
328     }
329 }
330
331 static void update_default_sink_from_button(int button, int on)
332 {
333   if (!on)
334     return;
335
336   int i = find_touched_client();
337   const char *sink;
338   if (i >= 0)
339     sink = get_client_sink(i);
340   else
341     sink = pulse_default_sink_name ? : "?";
342
343   const char *switch_to = NULL;
344   if (button == 8)
345     {
346       if (!strcmp(sink, "ursarium"))
347         switch_to = "burrow";
348       else
349         switch_to = "ursarium";
350     }
351   else if (button == 9)
352     {
353       if (!strcmp(sink, "catarium"))
354         switch_to = "burrow";
355       else
356         switch_to = "catarium";
357     }
358   else if (button == 10)
359     {
360       if (!strcmp(sink, "compress"))
361         switch_to = "burrow";
362       else
363         switch_to = "compress";
364     }
365
366   if (!switch_to)
367     return;
368
369   if (i >= 0)
370     {
371       struct pulse_sink *sk = pulse_sink_by_name(switch_to);
372       if (!sk)
373         return;
374
375       CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
376         if (s->noct_group_idx == i)
377           {
378             DBG("Moving input #%d to sink #%d", s->idx, sk->idx);
379             pulse_sink_input_move(s->idx, sk->idx);
380           }
381     }
382   else
383     {
384       DBG("Switching default sink to %s", switch_to);
385       pulse_server_set_default_sink(switch_to);
386     }
387 }
388
389 /*** MPD controls ***/
390
391 static bool mpd_flash_state;
392
393 static void mpd_flash_timeout(struct main_timer *t)
394 {
395   mpd_flash_state ^= 1;
396   noct_set_button(12, mpd_flash_state);
397   timer_add_rel(t, 500);
398 }
399
400 static struct main_timer mpd_flash_timer = {
401   .handler = mpd_flash_timeout,
402 };
403
404 static void update_mpd(void)
405 {
406   const char *state = mpd_get_player_state();
407   if (!strcmp(state, "play"))
408     {
409       noct_set_button(12, 1);
410       timer_del(&mpd_flash_timer);
411     }
412   else if (!strcmp(state, "pause"))
413     {
414       if (!timer_is_active(&mpd_flash_timer))
415         {
416           mpd_flash_state = 1;
417           mpd_flash_timeout(&mpd_flash_timer);
418         }
419     }
420   else
421     {
422       noct_set_button(12, 0);
423       timer_del(&mpd_flash_timer);
424     }
425 }
426
427 static void mpd_button_timeout(struct main_timer *t)
428 {
429   DBG("MPD stop");
430   timer_del(t);
431   mpd_stop();
432 }
433
434 static void update_mpd_from_button(int button UNUSED, int on)
435 {
436   static struct main_timer mpd_button_timer = {
437     .handler = mpd_button_timeout,
438   };
439
440   const char *state = mpd_get_player_state();
441
442   if (!on)
443     {
444       if (timer_is_active(&mpd_button_timer))
445         {
446           timer_del(&mpd_button_timer);
447           if (!strcmp(state, "play"))
448             {
449               DBG("MPD pause");
450               mpd_pause(1);
451             }
452           else if (!strcmp(state, "pause"))
453             {
454               DBG("MPD resume");
455               mpd_pause(0);
456             }
457         }
458       return;
459     }
460
461   if (!strcmp(state, "stop"))
462     {
463       DBG("MPD play");
464       mpd_play();
465     }
466   else
467     {
468       DBG("MPD starting button timer");
469       timer_add_rel(&mpd_button_timer, 1000);
470     }
471 }
472
473 /*** Main update routines ***/
474
475 static struct main_timer update_timer;
476 static timestamp_t last_touch_time;
477
478 enum update_state {
479   US_OFFLINE,
480   US_ONLINE,
481   US_SLEEPING,
482   US_PULSE_DEAD,
483 };
484
485 static enum update_state update_state;
486
487 static bool want_sleep_p(void)
488 {
489   CLIST_FOR_EACH(struct pulse_sink *, s, pulse_sink_list)
490     if (!s->suspended)
491       return 0;
492   return 1;
493 }
494
495 static void do_update(struct main_timer *t)
496 {
497   DBG("## UPDATE in state %u", update_state);
498   timer_del(t);
499
500   // Nocturn dead?
501   if (!noct_is_ready())
502     {
503       DBG("## UPDATE: Nocturn is not ready");
504       update_state = US_OFFLINE;
505       return;
506     }
507   else if (update_state == US_OFFLINE)
508     {
509       DBG("## UPDATE: Going online");
510       update_state = US_ONLINE;
511     }
512
513   // Pulse dead?
514   if (!pulse_is_ready())
515     {
516       DBG("## UPDATE: Pulse is not online");
517       if (update_state != US_PULSE_DEAD)
518         {
519           update_state = US_PULSE_DEAD;
520           noct_clear();
521           for (int i=0; i<8; i++)
522             noct_set_button(i, 1);
523         }
524       return;
525     }
526   else if (update_state == US_PULSE_DEAD)
527     {
528       DBG("## UPDATE: Waking up from the dead");
529       update_state = US_ONLINE;
530       noct_clear();
531     }
532
533 #ifdef LOCAL_DEBUG
534   pulse_dump();
535 #endif
536
537   // Sleeping?
538   bool want_sleep = want_sleep_p();
539   if (!want_sleep)
540     last_touch_time = main_get_now();
541   timestamp_t since_touch = last_touch_time ? main_get_now() - last_touch_time : 0;
542   timestamp_t sleep_in = 5000;
543   if (since_touch >= sleep_in)
544     {
545       DBG("UPDATE: Sleeping");
546       if (update_state == US_ONLINE)
547         {
548           update_state = US_SLEEPING;
549           noct_clear();
550           noct_set_ring(8, RING_MODE_LEFT, 127);
551         }
552       return;
553     }
554   else
555     {
556       if (update_state == US_SLEEPING)
557         {
558           DBG("UPDATE: Waking up");
559           update_state = US_ONLINE;
560           noct_clear();
561         }
562       if (want_sleep)
563         {
564           timestamp_t t = sleep_in - since_touch + 10;
565           DBG("UPDATE: Scheduling sleep in %d ms", (int) t);
566           timer_add_rel(&update_timer, t);
567         }
568     }
569
570   // Everything normal
571   update_ring_from_sink(0, "ursarium");
572   update_ring_from_sink(1, "catarium");
573   update_groups();
574   update_default_sink();
575   update_mpd();
576 }
577
578 void schedule_update(void)
579 {
580   timer_add_rel(&update_timer, 10);
581 }
582
583 static bool prepare_notify(void)
584 {
585   if (!pulse_is_ready())
586     {
587       DBG("## NOTIFY: Pulse is not online");
588       return 0;
589     }
590
591   last_touch_time = main_get_now();
592   if (update_state == US_SLEEPING)
593     {
594       DBG("## NOTIFY: Scheduling wakeup");
595       schedule_update();
596     }
597
598   return 1;
599 }
600
601 void notify_rotary(int rotary, int delta)
602 {
603   if (!prepare_notify())
604     return;
605
606   switch (rotary)
607     {
608     case 0:
609       update_sink_from_rotary(delta, "ursarium");
610       break;
611     case 1:
612       update_sink_from_rotary(delta, "catarium");
613       break;
614     case 8:
615       update_sink_from_rotary(delta, "ursarium");
616       update_sink_from_rotary(delta, "catarium");
617       break;
618     default:
619       update_group_from_rotary(rotary, delta);
620     }
621 }
622
623 void notify_button(int button, int on)
624 {
625   if (!prepare_notify())
626     return;
627
628   switch (button)
629     {
630     case 0:
631       update_sink_mute_from_button(on, "ursarium");
632       break;
633     case 1:
634       update_sink_mute_from_button(on, "catarium");
635       break;
636     case 8:
637     case 9:
638     case 10:
639       update_default_sink_from_button(button, on);
640       break;
641     case 12:
642       update_mpd_from_button(button, on);
643       break;
644     default:
645       update_group_from_button(button, on);
646     }
647 }
648
649 void notify_touch(int rotary, int on UNUSED)
650 {
651   if (!prepare_notify())
652     return;
653
654   // Rotary touches switch meaning of LEDs, this is handled inside display updates
655   if (rotary >= 4 && rotary < 8)
656     schedule_update();
657 }
658
659 /*** Main entry point ***/
660
661 static int debug;
662
663 static struct opt_section options = {
664   OPT_ITEMS {
665     OPT_HELP("Control console for the Ursary"),
666     OPT_HELP(""),
667     OPT_HELP("Options:"),
668     OPT_HELP_OPTION,
669     OPT_BOOL('d', "debug", debug, 0, "\tEnable debugging mode (no fork etc.)"),
670     OPT_END
671   }
672 };
673
674 static void sigterm_handler(struct main_signal *ms UNUSED)
675 {
676   main_shut_down();
677 }
678
679 static void daemon_body(struct daemon_params *dp)
680 {
681   main_init();
682   update_timer.handler = do_update;
683
684   noct_init();
685   pulse_init();
686   mpd_init();
687
688   static struct main_signal term_sig = {
689     .signum = SIGTERM,
690     .handler = sigterm_handler,
691   };
692   signal_add(&term_sig);
693
694   msg(L_INFO, "Ursary daemon starting");
695   main_loop();
696   msg(L_INFO, "Ursary daemon shut down");
697   daemon_exit(dp);
698 }
699
700 int main(int argc UNUSED, char **argv)
701 {
702   opt_parse(&options, argv+1);
703   unsetenv("DISPLAY");
704   unsetenv("HOME");
705
706   struct daemon_params dp = {
707     .flags = (debug ? DAEMON_FLAG_SIMULATE : 0),
708     .pid_file = "/run/ursaryd.pid",
709     .run_as_user = "ursary",
710   };
711   daemon_init(&dp);
712
713   log_init(argv[0]);
714   if (!debug)
715     {
716       struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
717       ls->levels = ~(1U << L_DEBUG);
718       log_set_default_stream(ls);
719     }
720
721   daemon_run(&dp, daemon_body);
722   return 0;
723 }