2 * The Ursary Audio Controls
4 * (c) 2014--2020 Martin Mares <mj@ucw.cz>
10 #include <ucw/clists.h>
11 #include <ucw/daemon.h>
13 #include <ucw/mainloop.h>
15 #include <ucw/stkstring.h>
29 * rotary red button green button
30 * 0 sink PCH mute use headphones
34 * 4 MPD mute MPD play/pause
35 * 5 Albireo MPV mute MPD stop
36 * 6 Albireo other mute MPD prev
37 * 7 other machines mute MPD next
43 #define PCH_SINK "alsa_output.pci-0000_00_1f.3.analog-stereo"
45 /*** Sink controls ***/
47 static double volume_from_pa(pa_volume_t vol)
49 return (double) vol / PA_VOLUME_NORM;
52 static pa_volume_t volume_to_pa(double vol)
54 return vol * PA_VOLUME_NORM + 0.0001;
57 static void update_ring_from_sink(int ring, const char *sink_name)
59 struct pulse_sink *s = pulse_sink_by_name(sink_name);
62 noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
63 noct_set_button(ring, 0);
69 noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f);
70 noct_set_button(ring, 1);
74 double vol = CLAMP(volume_from_pa(s->volume), 0, 1);
75 noct_set_ring(ring, RING_MODE_LEFT, CLAMP((int)(0x7f * vol), 12, 0x7f));
76 noct_set_button(ring, 0);
79 static void update_button_from_port(int button, const char *sink_name, const char *port_name)
81 struct pulse_sink *s = pulse_sink_by_name(sink_name);
84 noct_set_button(button, 0);
88 noct_set_button(button, !strcmp(s->active_port, port_name));
91 static void update_sink_from_rotary(int delta, const char *sink_name)
93 struct pulse_sink *s = pulse_sink_by_name(sink_name);
97 double vol = volume_from_pa(s->volume) + delta * 0.02;
98 pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, 1));
99 if (pavol == s->volume)
102 pa_cvolume_set(&cvol, s->channels, pavol);
104 DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]);
105 pulse_sink_set_volume(s->idx, &cvol);
108 static void update_sink_mute_from_button(int on, const char *sink_name)
113 struct pulse_sink *s = pulse_sink_by_name(sink_name);
117 DBG("## Setting mute of sink %s to %d", s->name, !s->mute);
118 pulse_sink_set_mute(s->idx, !s->mute);
121 static void update_port_from_button(int on, const char *sink_name, const char *port1, const char *port2)
126 struct pulse_sink *s = pulse_sink_by_name(sink_name);
130 const char *port = port1;
131 if (!strcmp(s->active_port, port1))
134 DBG("## Setting port of sink %s to %s", s->name, port);
135 pulse_sink_set_port(s->idx, port);
138 /*** Client controls ***/
146 static struct client_map client_map[] = {
147 { 4, "Music Player Daemon", "albireo", },
148 { 5, "mpv", "albireo", },
149 { 6, NULL, "albireo", },
153 #define CLIENT_MAP_SIZE ARRAY_SIZE(client_map)
155 struct group_config {
167 static struct group_config group_config[NUM_GROUPS] = {
168 [4] = { .enabled = 1, .range = 1.5 },
169 [5] = { .enabled = 1, .range = 1.5 },
170 [6] = { .enabled = 1, .range = 1.5 },
171 [7] = { .enabled = 1, .range = 1.5 },
174 static struct group_state group_state[NUM_GROUPS];
176 static void calc_groups(void)
178 bzero(group_state, sizeof(group_state));
180 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
182 s->noct_group_idx = -1;
184 if (s->client_idx < 0 || s->sink_idx < 0)
187 struct pulse_client *c = pulse_client_by_idx(s->client_idx);
191 for (uns i=0; i < CLIENT_MAP_SIZE; i++)
193 struct client_map *cm = &client_map[i];
194 if ((!cm->client || !strcmp(cm->client, c->name)) &&
195 (!cm->host || !strcmp(cm->host, c->host)))
198 struct group_state *gs = &group_state[g];
199 DBG("@@ Client #%d, sink input #%d -> group %d", s->client_idx, s->idx, g);
200 s->noct_group_idx = g;
201 gs->volume = MAX(gs->volume, s->volume);
202 gs->have_muted[!!s->mute] = 1;
209 static void update_groups(void)
213 for (uns i=0; i < NUM_GROUPS; i++)
215 struct group_config *gc = &group_config[i];
216 struct group_state *gs = &group_state[i];
220 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);
222 if (!gs->have_muted[0] && !gs->have_muted[1])
224 noct_set_ring(i, RING_MODE_LEFT, 0);
225 noct_set_button(i, 0);
227 else if (!gs->have_muted[0])
229 noct_set_ring(i, RING_MODE_SINGLE_ON, 0x7f);
230 noct_set_button(i, 1);
234 double vol = CLAMP(volume_from_pa(gs->volume), 0, gc->range);
235 int val = 0x7f * vol / gc->range;
236 val = CLAMP(val, 12, 0x7f);
237 noct_set_ring(i, RING_MODE_LEFT, val);
238 noct_set_button(i, 0);
243 static void update_group_from_rotary(int i, int delta)
247 struct group_config *gc = &group_config[i];
248 struct group_state *gs = &group_state[i];
253 double vol = volume_from_pa(gs->volume) + delta*0.02;
254 pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, gc->range));
256 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
258 if (s->noct_group_idx == i && s->volume != pavol)
260 DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol);
262 pa_cvolume_set(&cvol, s->channels, pavol);
263 pulse_sink_input_set_volume(s->idx, &cvol);
268 static void update_group_from_button(int i, int on)
274 struct group_config *gc = &group_config[i];
275 struct group_state *gs = &group_state[i];
280 if (!gs->have_muted[0] && !gs->have_muted[1])
282 uns mute = !gs->have_muted[1];
284 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
286 if (s->noct_group_idx == i)
288 DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute);
289 pulse_sink_input_set_mute(s->idx, mute);
294 #if 0 // Not used at the moment
296 static int find_touched_client(void)
300 for (uns i=0; i < NUM_GROUPS; i++)
301 if (group_config[i].enabled && noct_rotary_touched[i])
312 /*** Default sink controls ***/
314 #if 0 // Not mapped to any button at the moment
316 static const char *get_client_sink(int i)
318 const char *sink = NULL;
320 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
321 if (s->noct_group_idx == i)
323 struct pulse_sink *sk = (s->sink_idx >= 0) ? pulse_sink_by_idx(s->sink_idx) : NULL;
324 const char *ss = sk ? sk->name : NULL;
327 else if (strcmp(sink, ss))
333 static void update_default_sink(void)
335 int i = find_touched_client();
338 sink = get_client_sink(i);
340 sink = pulse_default_sink_name ? : "?";
342 if (!strcmp(sink, "ursarium"))
344 noct_set_button(8, 1);
345 noct_set_button(9, 0);
346 noct_set_button(10, 0);
348 else if (!strcmp(sink, "catarium"))
350 noct_set_button(8, 0);
351 noct_set_button(9, 1);
352 noct_set_button(10, 0);
354 else if (!strcmp(sink, "compress"))
356 noct_set_button(8, 0);
357 noct_set_button(9, 0);
358 noct_set_button(10, 1);
362 noct_set_button(8, 0);
363 noct_set_button(9, 0);
364 noct_set_button(10, 0);
368 static void update_default_sink_from_button(int button, int on)
373 int i = find_touched_client();
376 sink = get_client_sink(i);
378 sink = pulse_default_sink_name ? : "?";
380 const char *switch_to = NULL;
383 if (!strcmp(sink, "ursarium"))
384 switch_to = "burrow";
386 switch_to = "ursarium";
388 else if (button == 9)
390 if (!strcmp(sink, "catarium"))
391 switch_to = "burrow";
393 switch_to = "catarium";
395 else if (button == 10)
397 if (!strcmp(sink, "compress"))
398 switch_to = "burrow";
400 switch_to = "compress";
408 struct pulse_sink *sk = pulse_sink_by_name(switch_to);
412 CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list)
413 if (s->noct_group_idx == i)
415 DBG("Moving input #%d to sink #%d", s->idx, sk->idx);
416 pulse_sink_input_move(s->idx, sk->idx);
421 DBG("Switching default sink to %s", switch_to);
422 pulse_server_set_default_sink(switch_to);
428 /*** MPD controls ***/
430 static bool mpd_flash_state;
432 static void mpd_flash_timeout(struct main_timer *t)
434 mpd_flash_state ^= 1;
435 noct_set_button(12, mpd_flash_state);
436 timer_add_rel(t, 500);
439 static struct main_timer mpd_flash_timer = {
440 .handler = mpd_flash_timeout,
443 static void update_mpd(void)
445 const char *state = mpd_get_player_state();
446 if (!strcmp(state, "play"))
448 noct_set_button(12, 1);
449 timer_del(&mpd_flash_timer);
451 else if (!strcmp(state, "pause"))
453 if (!timer_is_active(&mpd_flash_timer))
456 mpd_flash_timeout(&mpd_flash_timer);
461 noct_set_button(12, 0);
462 timer_del(&mpd_flash_timer);
466 static void mpd_button_timeout(struct main_timer *t)
473 static void update_mpd_from_button(int button UNUSED, int on)
475 static struct main_timer mpd_button_timer = {
476 .handler = mpd_button_timeout,
479 const char *state = mpd_get_player_state();
483 if (timer_is_active(&mpd_button_timer))
485 timer_del(&mpd_button_timer);
486 if (!strcmp(state, "play"))
491 else if (!strcmp(state, "pause"))
500 if (!strcmp(state, "stop"))
507 DBG("MPD starting button timer");
508 timer_add_rel(&mpd_button_timer, 1000);
512 /*** Main update routines ***/
514 static struct main_timer update_timer;
515 static timestamp_t last_touch_time;
524 static enum update_state update_state;
526 static bool want_sleep_p(void)
528 CLIST_FOR_EACH(struct pulse_sink *, s, pulse_sink_list)
534 static void do_update(struct main_timer *t)
536 DBG("## UPDATE in state %u", update_state);
540 if (!noct_is_ready())
542 DBG("## UPDATE: Nocturn is not ready");
543 update_state = US_OFFLINE;
546 else if (update_state == US_OFFLINE)
548 DBG("## UPDATE: Going online");
549 update_state = US_ONLINE;
553 if (!pulse_is_ready())
555 DBG("## UPDATE: Pulse is not online");
556 if (update_state != US_PULSE_DEAD)
558 update_state = US_PULSE_DEAD;
560 for (int i=0; i<8; i++)
561 noct_set_button(i, 1);
565 else if (update_state == US_PULSE_DEAD)
567 DBG("## UPDATE: Waking up from the dead");
568 update_state = US_ONLINE;
577 bool want_sleep = want_sleep_p();
579 last_touch_time = main_get_now();
580 timestamp_t since_touch = last_touch_time ? main_get_now() - last_touch_time : 0;
581 timestamp_t sleep_in = 5000;
582 if (since_touch >= sleep_in)
584 DBG("UPDATE: Sleeping");
585 if (update_state == US_ONLINE)
587 update_state = US_SLEEPING;
589 noct_set_ring(8, RING_MODE_LEFT, 127);
595 if (update_state == US_SLEEPING)
597 DBG("UPDATE: Waking up");
598 update_state = US_ONLINE;
603 timestamp_t t = sleep_in - since_touch + 10;
604 DBG("UPDATE: Scheduling sleep in %d ms", (int) t);
605 timer_add_rel(&update_timer, t);
610 update_ring_from_sink(0, PCH_SINK);
611 update_button_from_port(8, PCH_SINK, "analog-output-headphones");
614 update_default_sink();
619 void schedule_update(void)
621 timer_add_rel(&update_timer, 10);
624 static bool prepare_notify(void)
626 if (!pulse_is_ready())
628 DBG("## NOTIFY: Pulse is not online");
632 last_touch_time = main_get_now();
633 if (update_state == US_SLEEPING)
635 DBG("## NOTIFY: Scheduling wakeup");
642 void notify_rotary(int rotary, int delta)
644 if (!prepare_notify())
650 update_sink_from_rotary(delta, PCH_SINK);
653 update_group_from_rotary(rotary, delta);
657 void notify_button(int button, int on)
659 if (!prepare_notify())
665 update_sink_mute_from_button(on, PCH_SINK);
668 update_port_from_button(on, PCH_SINK, "analog-output-lineout", "analog-output-headphones");
673 update_default_sink_from_button(button, on);
677 update_mpd_from_button(button, on);
692 update_group_from_button(button, on);
696 void notify_touch(int rotary, int on UNUSED)
698 if (!prepare_notify())
701 // Rotary touches switch meaning of LEDs, this is handled inside display updates
702 if (rotary >= 4 && rotary < 8)
706 /*** Main entry point ***/
711 static struct opt_section options = {
713 OPT_HELP("Control console for the Ursary"),
715 OPT_HELP("Options:"),
717 OPT_BOOL('d', "debug", debug, 0, "\tEnable debugging mode (no fork etc.)"),
718 OPT_BOOL(0, "no-fork", no_fork, 0, "\tDo not fork\n"),
723 static void sigterm_handler(struct main_signal *ms UNUSED)
728 static void daemon_body(struct daemon_params *dp)
731 update_timer.handler = do_update;
738 static struct main_signal term_sig = {
740 .handler = sigterm_handler,
742 signal_add(&term_sig);
744 msg(L_INFO, "Ursary daemon starting");
746 msg(L_INFO, "Ursary daemon shut down");
750 int main(int argc UNUSED, char **argv)
752 opt_parse(&options, argv+1);
756 struct daemon_params dp = {
757 .flags = ((debug || no_fork) ? DAEMON_FLAG_SIMULATE : 0),
758 .pid_file = "/run/ursaryd.pid",
759 .run_as_user = "ursary",
766 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
767 ls->levels = ~(1U << L_DEBUG);
768 log_set_default_stream(ls);
771 daemon_run(&dp, daemon_body);