X-Git-Url: http://mj.ucw.cz/gitweb/?a=blobdiff_plain;f=ursaryd.c;h=66ac0c6cab25fd313accd0bf08c61c09d98f9da9;hb=c575ff2359fd1627c538616c8be44aea27314a51;hp=bb057e16a1f105278bbb048d4fe24487d55bfe99;hpb=298340146eefb4a9a62c2dd64b042be0e526e70a;p=ursary.git diff --git a/ursaryd.c b/ursaryd.c index bb057e1..66ac0c6 100644 --- a/ursaryd.c +++ b/ursaryd.c @@ -1,555 +1,618 @@ -#define LOCAL_DEBUG +/* + * The Ursary Audio Controls + * + * (c) 2014--2018 Martin Mares + */ + +#undef LOCAL_DEBUG #include #include +#include +#include #include +#include #include +#include #include #include #include - -#include +#include #include "ursaryd.h" -/*** Interface to PulseAudio ***/ - -static pa_context *pulse_ctx; -static struct main_timer pulse_connect_timer; - -static void pulse_dump(void); - -enum pulse_state { - PS_OFFLINE, - PS_SUBSCRIBE, - PS_GET_CLIENTS, - PS_GET_SINKS, - PS_GET_SINK_INPUTS, - PS_ONLINE, -}; - -static enum pulse_state pulse_state; -#define PULSE_STATE(s) do { pulse_state = s; DBG("Pulse: " #s); } while (0) +/* + * Map of all controls + * + * rotary red button green button + * 0 sink PCH mute - + * 1 - - - + * 2 - - - + * 3 - - - + * 4 MPD mute play/pause/stop + * 5 Albireo MPV mute - + * 6 Albireo other mute - + * 7 other machines mute - + * + * center - + * slider - + */ -// Tracking of currently running asynchronous operations -struct pulse_op { - cnode n; - pa_operation *o; - bool is_init; -}; +#define PCH_SINK "alsa_output.pci-0000_00_1f.3.analog-stereo" -static clist pulse_op_list; +/*** Sink controls ***/ -static struct pulse_op *pulse_op_new(void) +static double volume_from_pa(pa_volume_t vol) { - struct pulse_op *op = xmalloc_zero(sizeof(*op)); - clist_add_tail(&pulse_op_list, &op->n); - return op; + return (double) vol / PA_VOLUME_NORM; } -static void pulse_op_done(struct pulse_op *op) +static pa_volume_t volume_to_pa(double vol) { - if (op->o) - pa_operation_unref(op->o); - clist_remove(&op->n); - xfree(op); + return vol * PA_VOLUME_NORM + 0.0001; } -static void pulse_op_cancel_all(void) +static void update_ring_from_sink(int ring, const char *sink_name) { - struct pulse_op *op; - while (op = (struct pulse_op *) clist_head(&pulse_op_list)) + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) { - DBG("Pulse: Cancelling pending operation"); - pa_operation_cancel(op->o); - pulse_op_done(op); + noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); + noct_set_button(ring, 0); + return; } -} -#define PULSE_ASYNC_RUN(name, ...) do { struct pulse_op *_op = pulse_op_new(); _op->o = name(__VA_ARGS__, _op); } while (0) -#define PULSE_ASYNC_INIT_RUN(name, ...) do { struct pulse_op *_op = pulse_op_new(); _op->is_init = 1; _op->o = name(__VA_ARGS__, _op); } while (0) + if (s->mute) + { + noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); + noct_set_button(ring, 1); + return; + } -static void pulse_success_cb(pa_context *ctx UNUSED, int success, void *userdata) -{ - if (!success) - msg(L_ERROR, "Pulse: Failure reported"); - pulse_op_done(userdata); + double vol = CLAMP(volume_from_pa(s->volume), 0, 1); + noct_set_ring(ring, RING_MODE_LEFT, CLAMP((int)(0x7f * vol), 12, 0x7f)); + noct_set_button(ring, 0); } -static void pulse_dump_proplist(pa_proplist *pl UNUSED) +static void update_button_from_port(int button, const char *sink_name, const char *port_name) { -#if 0 - void *iterator = NULL; - const char *key; - - while (key = pa_proplist_iterate(pl, &iterator)) + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) { - const char *val = pa_proplist_gets(pl, key); - DBG(" %s = %s", key, val); + noct_set_button(button, 0); + return; } -#endif + + noct_set_button(button, !strcmp(s->active_port, port_name)); } -struct pulse_sink_input { - int idx; - char *name; - int client_idx; - int sink_idx; - uns channels; - uns volume; - uns mute; - int noct_client_idx; // Used by the high-level logic below -}; +static void update_sink_from_rotary(int delta, const char *sink_name) +{ + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) + return; -#define HASH_NODE struct pulse_sink_input -#define HASH_PREFIX(x) pulse_sink_input_##x -#define HASH_KEY_ATOMIC idx -#define HASH_WANT_CLEANUP -#define HASH_WANT_LOOKUP -#define HASH_WANT_REMOVE -#define HASH_ZERO_FILL -#include + double vol = volume_from_pa(s->volume) + delta * 0.02; + pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, 1)); + if (pavol == s->volume) + return; + pa_cvolume cvol; + pa_cvolume_set(&cvol, s->channels, pavol); -#define SET_STRING(_field, _val) do { if (!_field || strcmp(_field, _val)) { xfree(_field); _field = xstrdup(_val); } } while (0) + DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]); + pulse_sink_set_volume(s->idx, &cvol); +} -static void pulse_sink_input_cb(pa_context *ctx UNUSED, const pa_sink_input_info *i, int eol, void *userdata) +static void update_sink_mute_from_button(int on, const char *sink_name) { - struct pulse_op *op = userdata; - - if (eol) - { - if (op->is_init) - { - PULSE_STATE(PS_ONLINE); - schedule_update(); - } - pulse_op_done(op); - return; - } + if (!on) + return; - DBG("Pulse: SINK INPUT #%u: %s client=%d sink=%d chans=%d has_vol=%d vol_rw=%d volume=%u mute=%d", - i->index, i->name, i->client, i->sink, i->channel_map.channels, i->has_volume, i->volume_writable, i->volume.values[0], i->mute); - pulse_dump_proplist(i->proplist); + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) + return; - struct pulse_sink_input *s = pulse_sink_input_lookup(i->index); - SET_STRING(s->name, i->name); - s->client_idx = i->client; - s->sink_idx = i->sink; - s->channels = i->channel_map.channels; - s->volume = pa_cvolume_avg(&i->volume); - s->mute = i->mute; - schedule_update(); + DBG("## Setting mute of sink %s to %d", s->name, !s->mute); + pulse_sink_set_mute(s->idx, !s->mute); } -static void pulse_sink_input_gone(int idx) +static void update_port_from_button(int on, const char *sink_name, const char *port1, const char *port2) { - DBG("Pulse: REMOVE SINK INPUT #%d", idx); - struct pulse_sink_input *s = pulse_sink_input_lookup(idx); - pulse_sink_input_remove(s); - schedule_update(); + if (!on) + return; + + struct pulse_sink *s = pulse_sink_by_name(sink_name); + if (!s) + return; + + const char *port = port1; + if (!strcmp(s->active_port, port1)) + port = port2; + + DBG("## Setting port of sink %s to %s", s->name, port); + pulse_sink_set_port(s->idx, port); } -struct pulse_sink { - int idx; - char *name; - uns channels; - uns volume; - uns base_volume; - int mute; +/*** Client controls ***/ + +struct client_map { + int group; + const char *client; + const char *host; }; -#define HASH_NODE struct pulse_sink -#define HASH_PREFIX(x) pulse_sink_##x -#define HASH_KEY_ATOMIC idx -#define HASH_WANT_CLEANUP -#define HASH_WANT_LOOKUP -#define HASH_WANT_REMOVE -#define HASH_ZERO_FILL -#include +static struct client_map client_map[] = { + { 4, "Music Player Daemon", "albireo", }, + { 5, "mpv", "albireo", }, + { 6, NULL, "albireo", }, + { 7, NULL, NULL, }, +}; + +#define CLIENT_MAP_SIZE ARRAY_SIZE(client_map) + +struct group_config { + bool enabled; + double range; +}; + +struct group_state { + double volume; + bool have_muted[2]; +}; + +#define NUM_GROUPS 9 + +static struct group_config group_config[NUM_GROUPS] = { + [4] = { .enabled = 1, .range = 1.5 }, + [5] = { .enabled = 1, .range = 1.5 }, + [6] = { .enabled = 1, .range = 1.5 }, + [7] = { .enabled = 1, .range = 1.5 }, +}; + +static struct group_state group_state[NUM_GROUPS]; -static void pulse_sink_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) +static void calc_groups(void) { - struct pulse_op *op = userdata; + bzero(group_state, sizeof(group_state)); - if (eol) + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) { - if (op->is_init) + s->noct_group_idx = -1; + + if (s->client_idx < 0 || s->sink_idx < 0) + continue; + + struct pulse_client *c = pulse_client_by_idx(s->client_idx); + if (!c) + continue; + + for (uns i=0; i < CLIENT_MAP_SIZE; i++) { - PULSE_STATE(PS_GET_SINK_INPUTS); - PULSE_ASYNC_INIT_RUN(pa_context_get_sink_input_info_list, ctx, pulse_sink_input_cb); + struct client_map *cm = &client_map[i]; + if ((!cm->client || !strcmp(cm->client, c->name)) && + (!cm->host || !strcmp(cm->host, c->host))) + { + int g = cm->group; + struct group_state *gs = &group_state[g]; + DBG("@@ Client #%d, sink input #%d -> group %d", s->client_idx, s->idx, g); + s->noct_group_idx = g; + gs->volume = MAX(gs->volume, s->volume); + gs->have_muted[!!s->mute] = 1; + break; + } } - pulse_op_done(op); - return; } - - DBG("Pulse: SINK #%u: %s (%s) flags=%08x channels=%u volume=%u mute=%d base_vol=%u state=%u", - i->index, i->name, i->description, i->flags, i->channel_map.channels, i->volume.values[0], i->mute, i->base_volume, i->state); - pulse_dump_proplist(i->proplist); - - struct pulse_sink *s = pulse_sink_lookup(i->index); - SET_STRING(s->name, i->name); - s->channels = i->channel_map.channels; - s->volume = pa_cvolume_avg(&i->volume); - s->base_volume = i->base_volume; - s->mute = i->mute; - schedule_update(); } -static void pulse_sink_gone(int idx) +static void update_groups(void) { - DBG("Pulse: REMOVE SINK #%d", idx); - struct pulse_sink *s = pulse_sink_lookup(idx); - pulse_sink_remove(s); - schedule_update(); -} + calc_groups(); -static struct pulse_sink *pulse_sink_by_name(const char *name) -{ - HASH_FOR_ALL(pulse_sink, s) + for (uns i=0; i < NUM_GROUPS; i++) { - if (!strcmp(s->name, name)) - return s; - } - HASH_END_FOR; - return NULL; -} + struct group_config *gc = &group_config[i]; + struct group_state *gs = &group_state[i]; + if (!gc->enabled) + continue; -struct pulse_client { - int idx; - char *name; - char *host; -}; + 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); -#define HASH_NODE struct pulse_client -#define HASH_PREFIX(x) pulse_client_##x -#define HASH_KEY_ATOMIC idx -#define HASH_WANT_CLEANUP -#define HASH_WANT_LOOKUP -#define HASH_WANT_REMOVE -#define HASH_ZERO_FILL -#include + if (!gs->have_muted[0] && !gs->have_muted[1]) + { + noct_set_ring(i, RING_MODE_LEFT, 0); + noct_set_button(i, 0); + } + else if (!gs->have_muted[0]) + { + noct_set_ring(i, RING_MODE_SINGLE_ON, 0x7f); + noct_set_button(i, 1); + } + else + { + double vol = CLAMP(volume_from_pa(gs->volume), 0, gc->range); + int val = 0x7f * vol / gc->range; + val = CLAMP(val, 12, 0x7f); + noct_set_ring(i, RING_MODE_LEFT, val); + noct_set_button(i, 0); + } + } +} -static void pulse_client_cb(pa_context *ctx, const pa_client_info *i, int eol, void *userdata) +static void update_group_from_rotary(int i, int delta) { - struct pulse_op *op = userdata; + if (i >= NUM_GROUPS) + return; + struct group_config *gc = &group_config[i]; + struct group_state *gs = &group_state[i]; + if (!gc->enabled) + return; - if (eol) + calc_groups(); + double vol = volume_from_pa(gs->volume) + delta*0.02; + pa_volume_t pavol = volume_to_pa(CLAMP(vol, 0, gc->range)); + + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) { - if (op->is_init) + if (s->noct_group_idx == i && s->volume != pavol) { - PULSE_STATE(PS_GET_SINKS); - PULSE_ASYNC_INIT_RUN(pa_context_get_sink_info_list, ctx, pulse_sink_cb); + DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol); + pa_cvolume cvol; + pa_cvolume_set(&cvol, s->channels, pavol); + pulse_sink_input_set_volume(s->idx, &cvol); } - pulse_op_done(op); - return; } - - char *host = stk_strdup(pa_proplist_gets(i->proplist, "application.process.host") ? : "?"); - DBG("Pulse: CLIENT #%u: %s mod=%u drv=%s host=%s", - i->index, i->name, i->owner_module, i->driver, host); - pulse_dump_proplist(i->proplist); - - struct pulse_client *c = pulse_client_lookup(i->index); - SET_STRING(c->name, i->name); - SET_STRING(c->host, host); - schedule_update(); } -static void pulse_client_gone(int idx) +static void update_group_from_button(int i, int on) { - DBG("Pulse: REMOVE CLIENT #%d", idx); - struct pulse_client *c = pulse_client_lookup(idx); - pulse_client_remove(c); - schedule_update(); + if (!on) + return; + if (i >= NUM_GROUPS) + return; + struct group_config *gc = &group_config[i]; + struct group_state *gs = &group_state[i]; + if (!gc->enabled) + return; + + calc_groups(); + if (!gs->have_muted[0] && !gs->have_muted[1]) + return; + uns mute = !gs->have_muted[1]; + + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) + { + if (s->noct_group_idx == i) + { + DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute); + pulse_sink_input_set_mute(s->idx, mute); + } + } } -static void pulse_shutdown(void) +#if 0 // Not used at the moment + +static int find_touched_client(void) { - DBG("Pulse: Shutting down"); - pulse_client_cleanup(); - pulse_sink_cleanup(); - pulse_sink_input_cleanup(); + int touched = -1; + + for (uns i=0; i < NUM_GROUPS; i++) + if (group_config[i].enabled && noct_rotary_touched[i]) + { + if (touched >= 0) + return -1; + touched = i; + } + return touched; } -static void pulse_subscribe_done_cb(pa_context *ctx, int success, void *userdata) -{ - pulse_op_done(userdata); +#endif - if (!success) - msg(L_ERROR, "pa_context_subscribe failed: success=%d", success); +/*** Default sink controls ***/ - PULSE_STATE(PS_GET_CLIENTS); - PULSE_ASYNC_INIT_RUN(pa_context_get_client_info_list, ctx, pulse_client_cb); -} +#if 0 // Not mapped to any button at the moment -static void pulse_event_cb(pa_context *ctx, pa_subscription_event_type_t type, uint32_t idx, void *userdata UNUSED) +static const char *get_client_sink(int i) { - DBG("Pulse: SUBSCRIBE EVENT type=%08x idx=%u", type, idx); + const char *sink = NULL; - uns object = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; - uns action = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; - switch (object) - { - case PA_SUBSCRIPTION_EVENT_CLIENT: - if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE) - PULSE_ASYNC_RUN(pa_context_get_client_info, ctx, idx, pulse_client_cb); - else if (action == PA_SUBSCRIPTION_EVENT_REMOVE) - pulse_client_gone(idx); - break; - case PA_SUBSCRIPTION_EVENT_SINK: - if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE) - PULSE_ASYNC_RUN(pa_context_get_sink_info_by_index, ctx, idx, pulse_sink_cb); - else if (action == PA_SUBSCRIPTION_EVENT_REMOVE) - pulse_sink_gone(idx); - break; - case PA_SUBSCRIPTION_EVENT_SINK_INPUT: - if (action == PA_SUBSCRIPTION_EVENT_NEW || action == PA_SUBSCRIPTION_EVENT_CHANGE) - PULSE_ASYNC_RUN(pa_context_get_sink_input_info, ctx, idx, pulse_sink_input_cb); - else if (action == PA_SUBSCRIPTION_EVENT_REMOVE) - pulse_sink_input_gone(idx); - break; - } + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) + if (s->noct_group_idx == i) + { + struct pulse_sink *sk = (s->sink_idx >= 0) ? pulse_sink_by_idx(s->sink_idx) : NULL; + const char *ss = sk ? sk->name : NULL; + if (!sink) + sink = ss; + else if (strcmp(sink, ss)) + sink = "?"; + } + return sink ? : "?"; } -static void pulse_state_cb(pa_context *ctx, void *userdata UNUSED) +static void update_default_sink(void) { - int state = pa_context_get_state(ctx); - DBG("Pulse: State callback, new state = %d", state); - if (state == PA_CONTEXT_READY) + int i = find_touched_client(); + const char *sink; + if (i >= 0) + sink = get_client_sink(i); + else + sink = pulse_default_sink_name ? : "?"; + + if (!strcmp(sink, "ursarium")) { - if (pulse_state == PS_OFFLINE) - { - PULSE_STATE(PS_SUBSCRIBE); - pa_context_set_subscribe_callback(ctx, pulse_event_cb, NULL); - PULSE_ASYNC_INIT_RUN(pa_context_subscribe, ctx, PA_SUBSCRIPTION_MASK_ALL, pulse_subscribe_done_cb); - } + noct_set_button(8, 1); + noct_set_button(9, 0); + noct_set_button(10, 0); + } + else if (!strcmp(sink, "catarium")) + { + noct_set_button(8, 0); + noct_set_button(9, 1); + noct_set_button(10, 0); + } + else if (!strcmp(sink, "compress")) + { + noct_set_button(8, 0); + noct_set_button(9, 0); + noct_set_button(10, 1); } else { - if (pulse_state != PS_OFFLINE) - { - PULSE_STATE(PS_OFFLINE); - pulse_op_cancel_all(); - pulse_shutdown(); - schedule_update(); - } - if (state == PA_CONTEXT_FAILED && !timer_is_active(&pulse_connect_timer)) - timer_add_rel(&pulse_connect_timer, 2000); + noct_set_button(8, 0); + noct_set_button(9, 0); + noct_set_button(10, 0); } } -static void pulse_dump(void) +static void update_default_sink_from_button(int button, int on) { - HASH_FOR_ALL(pulse_client, c) + if (!on) + return; + + int i = find_touched_client(); + const char *sink; + if (i >= 0) + sink = get_client_sink(i); + else + sink = pulse_default_sink_name ? : "?"; + + const char *switch_to = NULL; + if (button == 8) { - DBG("## Client #%d: %s host=%s", c->idx, c->name, c->host); + if (!strcmp(sink, "ursarium")) + switch_to = "burrow"; + else + switch_to = "ursarium"; } - HASH_END_FOR; - - HASH_FOR_ALL(pulse_sink, s) + else if (button == 9) { - DBG("## Sink #%d: %s channels=%u volume=%u base_vol=%u mute=%u", - s->idx, s->name, s->channels, s->volume, s->base_volume, s->mute); + if (!strcmp(sink, "catarium")) + switch_to = "burrow"; + else + switch_to = "catarium"; + } + else if (button == 10) + { + if (!strcmp(sink, "compress")) + switch_to = "burrow"; + else + switch_to = "compress"; } - HASH_END_FOR; - HASH_FOR_ALL(pulse_sink_input, s) + if (!switch_to) + return; + + if (i >= 0) + { + struct pulse_sink *sk = pulse_sink_by_name(switch_to); + if (!sk) + return; + + CLIST_FOR_EACH(struct pulse_sink_input *, s, pulse_sink_input_list) + if (s->noct_group_idx == i) + { + DBG("Moving input #%d to sink #%d", s->idx, sk->idx); + pulse_sink_input_move(s->idx, sk->idx); + } + } + else { - DBG("## Sink input #%d: %s client=%d sink=%d channels=%u volume=%u mute=%u", - s->idx, s->name, s->client_idx, s->sink_idx, s->channels, s->volume, s->mute); + DBG("Switching default sink to %s", switch_to); + pulse_server_set_default_sink(switch_to); } - HASH_END_FOR; } -static void pulse_connect(struct main_timer *t) -{ - DBG("Pulse: Connecting"); - timer_del(t); +#endif - clist_init(&pulse_op_list); - pulse_client_init(); - pulse_sink_init(); - pulse_sink_input_init(); +/*** MPD controls ***/ - if (pulse_ctx) - pa_context_unref(pulse_ctx); - pulse_ctx = pa_context_new(&pmain_api, "ursaryd"); +static bool mpd_flash_state; - pa_context_set_state_callback(pulse_ctx, pulse_state_cb, NULL); - pa_context_connect(pulse_ctx, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL); -} - -static void pulse_init(void) +static void mpd_flash_timeout(struct main_timer *t) { - pmain_init(); - - pulse_connect_timer.handler = pulse_connect; - timer_add_rel(&pulse_connect_timer, 0); + mpd_flash_state ^= 1; + noct_set_button(12, mpd_flash_state); + timer_add_rel(t, 500); } -/*** High-level logic ***/ - -static struct main_timer update_timer; +static struct main_timer mpd_flash_timer = { + .handler = mpd_flash_timeout, +}; -static void update_ring_from_sink(int ring, const char *sink_name) +static void update_mpd(void) { - struct pulse_sink *s = pulse_sink_by_name(sink_name); - if (!s) + const char *state = mpd_get_player_state(); + if (!strcmp(state, "play")) { - noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); - noct_set_button(ring, 0); - return; + noct_set_button(12, 1); + timer_del(&mpd_flash_timer); } - - if (s->mute) + else if (!strcmp(state, "pause")) { - noct_set_ring(ring, RING_MODE_SINGLE_ON, 0x7f); - noct_set_button(ring, 1); - return; + if (!timer_is_active(&mpd_flash_timer)) + { + mpd_flash_state = 1; + mpd_flash_timeout(&mpd_flash_timer); + } + } + else + { + noct_set_button(12, 0); + timer_del(&mpd_flash_timer); } - - double vol = pa_sw_volume_to_linear(s->volume); - vol = CLAMP(vol, 0, 1); - int val = 0x7f * vol; - val = CLAMP(val, 12, 0x7f); - noct_set_ring(ring, RING_MODE_LEFT, val); - noct_set_button(ring, 0); } -struct client_map { - int rotary; - const char *client; - const char *host; - double range; -}; - -static struct client_map client_map[] = { - { 4, "Music Player Daemon", "albireo", 1 }, - { 5, "MPlayer", NULL, 1 }, - { 6, NULL, "ogion", 1 }, -}; - -#define NUM_CLIENTS ARRAY_SIZE(client_map) - -struct client_state { - double volume; - bool have_muted[2]; -}; - -static struct client_state client_state[NUM_CLIENTS]; - -static int find_client_by_rotary(int rotary) +static void mpd_button_timeout(struct main_timer *t) { - uns i; - for (i=0; i < NUM_CLIENTS; i++) - if (client_map[i].rotary == rotary) - return i; - return -1; + DBG("MPD stop"); + timer_del(t); + mpd_stop(); } -static void calc_clients(void) +static void update_mpd_from_button(int button UNUSED, int on) { - bzero(client_state, sizeof(client_state)); + static struct main_timer mpd_button_timer = { + .handler = mpd_button_timeout, + }; - HASH_FOR_ALL(pulse_sink_input, s) - { - s->noct_client_idx = -1; - - if (s->client_idx < 0 || s->sink_idx < 0) - continue; + const char *state = mpd_get_player_state(); - struct pulse_client *c = pulse_client_lookup(s->client_idx); - if (!c) - continue; - - for (uns i=0; i < NUM_CLIENTS; i++) + if (!on) + { + if (timer_is_active(&mpd_button_timer)) { - struct client_map *cm = &client_map[i]; - struct client_state *cs = &client_state[i]; - if ((!cm->client || !strcmp(cm->client, c->name)) && - (!cm->host || !strcmp(cm->host, c->host))) + timer_del(&mpd_button_timer); + if (!strcmp(state, "play")) { - // DBG("@@ Client #%d, sink input #%d -> rotary %d", s->client_idx, s->idx, cm->rotary); - s->noct_client_idx = i; - cs->volume = MAX(cs->volume, s->volume); - cs->have_muted[!!s->mute] = 1; - break; + DBG("MPD pause"); + mpd_pause(1); + } + else if (!strcmp(state, "pause")) + { + DBG("MPD resume"); + mpd_pause(0); } } + return; } - HASH_END_FOR; -} -static void update_clients(void) -{ - calc_clients(); - - for (uns i=0; i < NUM_CLIENTS; i++) + if (!strcmp(state, "stop")) { - struct client_map *cm = &client_map[i]; - struct client_state *cs = &client_state[i]; - if (!cs->have_muted[0] && !cs->have_muted[1]) - { - noct_set_ring(cm->rotary, RING_MODE_LEFT, 0); - noct_set_button(cm->rotary, 0); - } - else if (!cs->have_muted[0]) - { - noct_set_ring(cm->rotary, RING_MODE_SINGLE_ON, 0x7f); - noct_set_button(cm->rotary, 1); - } - else - { - double vol = pa_sw_volume_to_linear(cs->volume); - vol = CLAMP(vol, 0, cm->range); - int val = 0x7f * vol / cm->range; - val = CLAMP(val, 12, 0x7f); - noct_set_ring(cm->rotary, RING_MODE_LEFT, val); - noct_set_button(cm->rotary, 0); - } + DBG("MPD play"); + mpd_play(); + } + else + { + DBG("MPD starting button timer"); + timer_add_rel(&mpd_button_timer, 1000); } } +/*** Main update routines ***/ + +static struct main_timer update_timer; +static timestamp_t last_touch_time; + +enum update_state { + US_OFFLINE, + US_ONLINE, + US_SLEEPING, + US_PULSE_DEAD, +}; + +static enum update_state update_state; + +static bool want_sleep_p(void) +{ + CLIST_FOR_EACH(struct pulse_sink *, s, pulse_sink_list) + if (!s->suspended) + return 0; + return 1; +} + static void do_update(struct main_timer *t) { + DBG("## UPDATE in state %u", update_state); timer_del(t); + + // Nocturn dead? if (!noct_is_ready()) { DBG("## UPDATE: Nocturn is not ready"); + update_state = US_OFFLINE; return; } + else if (update_state == US_OFFLINE) + { + DBG("## UPDATE: Going online"); + update_state = US_ONLINE; + } - static bool dead; - if (pulse_state != PS_ONLINE) + // Pulse dead? + if (!pulse_is_ready()) { DBG("## UPDATE: Pulse is not online"); - for (int i=0; i<=8; i++) - noct_set_ring(i, RING_MODE_LEFT, 0); - for (int i=0; i<8; i++) + if (update_state != US_PULSE_DEAD) { - noct_set_button(i, 1); - noct_set_button(i+8, 0); + update_state = US_PULSE_DEAD; + noct_clear(); + for (int i=0; i<8; i++) + noct_set_button(i, 1); } - dead = 1; return; } - if (dead) + else if (update_state == US_PULSE_DEAD) { DBG("## UPDATE: Waking up from the dead"); - for (int i=0; i<=8; i++) - noct_set_ring(i, RING_MODE_LEFT, 0); - for (int i=0; i<16; i++) - noct_set_button(i, 0); - dead = 0; + update_state = US_ONLINE; + noct_clear(); } - DBG("## UPDATE"); +#ifdef LOCAL_DEBUG pulse_dump(); +#endif + + // Sleeping? + bool want_sleep = want_sleep_p(); + if (!want_sleep) + last_touch_time = main_get_now(); + timestamp_t since_touch = last_touch_time ? main_get_now() - last_touch_time : 0; + timestamp_t sleep_in = 5000; + if (since_touch >= sleep_in) + { + DBG("UPDATE: Sleeping"); + if (update_state == US_ONLINE) + { + update_state = US_SLEEPING; + noct_clear(); + noct_set_ring(8, RING_MODE_LEFT, 127); + } + return; + } + else + { + if (update_state == US_SLEEPING) + { + DBG("UPDATE: Waking up"); + update_state = US_ONLINE; + noct_clear(); + } + if (want_sleep) + { + timestamp_t t = sleep_in - since_touch + 10; + DBG("UPDATE: Scheduling sleep in %d ms", (int) t); + timer_add_rel(&update_timer, t); + } + } - update_ring_from_sink(0, "ursarium"); - update_ring_from_sink(1, "catarium"); - update_clients(); + // Everything normal + update_ring_from_sink(0, PCH_SINK); + update_button_from_port(8, PCH_SINK, "analog-output-headphones"); + update_groups(); +#if 0 + update_default_sink(); +#endif + update_mpd(); } void schedule_update(void) @@ -557,150 +620,152 @@ void schedule_update(void) timer_add_rel(&update_timer, 10); } -static void update_sink_from_rotary(int delta, const char *sink_name) +static bool prepare_notify(void) { - struct pulse_sink *s = pulse_sink_by_name(sink_name); - if (!s) - return; + if (!pulse_is_ready()) + { + DBG("## NOTIFY: Pulse is not online"); + return 0; + } - double vol = pa_sw_volume_to_linear(s->volume); - vol += delta * 0.02; - vol = CLAMP(vol, 0, 1); - pa_volume_t pavol = pa_sw_volume_from_linear(vol); - if (pavol == s->volume) - return; - pa_cvolume cvol; - pa_cvolume_set(&cvol, s->channels, pavol); + last_touch_time = main_get_now(); + if (update_state == US_SLEEPING) + { + DBG("## NOTIFY: Scheduling wakeup"); + schedule_update(); + } - DBG("## Setting volume of sink %s to %d", s->name, cvol.values[0]); - PULSE_ASYNC_RUN(pa_context_set_sink_volume_by_index, pulse_ctx, s->idx, &cvol, pulse_success_cb); + return 1; } -static void update_client_from_rotary(int rotary, int delta) +void notify_rotary(int rotary, int delta) { - int i = find_client_by_rotary(rotary); - if (i < 0) + if (!prepare_notify()) return; - struct client_map *cm = &client_map[i]; - struct client_state *cs = &client_state[i]; - calc_clients(); - double vol = pa_sw_volume_to_linear(cs->volume); - vol += delta * 0.02; - vol = CLAMP(vol, 0, cm->range); - pa_volume_t pavol = pa_sw_volume_from_linear(vol); - - HASH_FOR_ALL(pulse_sink_input, s) + switch (rotary) { - if (s->noct_client_idx == i && s->volume != pavol) - { - DBG("@@ Client #%d, sink input #%d: setting volume=%u", s->client_idx, s->idx, pavol); - pa_cvolume cvol; - pa_cvolume_set(&cvol, s->channels, pavol); - PULSE_ASYNC_RUN(pa_context_set_sink_input_volume, pulse_ctx, s->idx, &cvol, pulse_success_cb); - } + case 0: + update_sink_from_rotary(delta, PCH_SINK); + break; + default: + update_group_from_rotary(rotary, delta); } - HASH_END_FOR; } -void notify_rotary(int rotary, int delta) +void notify_button(int button, int on) { - if (pulse_state != PS_ONLINE) - { - DBG("## NOTIFY: Pulse is not online"); - return; - } + if (!prepare_notify()) + return; - switch (rotary) + switch (button) { case 0: - update_sink_from_rotary(delta, "ursarium"); - break; - case 1: - update_sink_from_rotary(delta, "catarium"); + update_sink_mute_from_button(on, PCH_SINK); break; case 8: - update_sink_from_rotary(delta, "ursarium"); - update_sink_from_rotary(delta, "catarium"); + update_port_from_button(on, PCH_SINK, "analog-output-lineout", "analog-output-headphones"); + break; +#if 0 + case 9: + case 10: + update_default_sink_from_button(button, on); + break; +#endif + case 12: + update_mpd_from_button(button, on); + break; + case 13: + if (on) + mpd_stop(); + break; + case 14: + if (on) + mpd_prev(); + break; + case 15: + if (on) + mpd_next(); break; default: - update_client_from_rotary(rotary, delta); + update_group_from_button(button, on); } } -static void update_sink_mute_from_button(int on, const char *sink_name) +void notify_touch(int rotary, int on UNUSED) { - if (!on) - return; - - struct pulse_sink *s = pulse_sink_by_name(sink_name); - if (!s) + if (!prepare_notify()) return; - DBG("## Setting mute of sink %s to %d", s->name, !s->mute); - PULSE_ASYNC_RUN(pa_context_set_sink_mute_by_index, pulse_ctx, s->idx, !s->mute, pulse_success_cb); + // Rotary touches switch meaning of LEDs, this is handled inside display updates + if (rotary >= 4 && rotary < 8) + schedule_update(); } -static void update_client_from_button(int button, int on) -{ - if (button >= 8 || !on) - return; - - int i = find_client_by_rotary(button); - if (i < 0) - return; - struct client_state *cs = &client_state[i]; +/*** Main entry point ***/ - calc_clients(); - if (!cs->have_muted[0] && !cs->have_muted[1]) - return; - uns mute = !cs->have_muted[1]; +static int debug; +static int no_fork; - HASH_FOR_ALL(pulse_sink_input, s) - { - if (s->noct_client_idx == i) - { - DBG("@@ Client #%d, sink input #%d: setting mute=%u", s->client_idx, s->idx, mute); - PULSE_ASYNC_RUN(pa_context_set_sink_input_mute, pulse_ctx, s->idx, mute, pulse_success_cb); - } - } - HASH_END_FOR; -} +static struct opt_section options = { + OPT_ITEMS { + OPT_HELP("Control console for the Ursary"), + OPT_HELP(""), + OPT_HELP("Options:"), + OPT_HELP_OPTION, + OPT_BOOL('d', "debug", debug, 0, "\tEnable debugging mode (no fork etc.)"), + OPT_BOOL(0, "no-fork", no_fork, 0, "\tDo not fork\n"), + OPT_END + } +}; -void notify_button(int button, int on) +static void sigterm_handler(struct main_signal *ms UNUSED) { - if (pulse_state != PS_ONLINE) - { - DBG("## NOTIFY: Pulse is not online"); - return; - } - - switch (button) - { - case 0: - update_sink_mute_from_button(on, "ursarium"); - break; - case 1: - update_sink_mute_from_button(on, "catarium"); - break; - default: - update_client_from_button(button, on); - } + main_shut_down(); } -int main(int argc UNUSED, char **argv) +static void daemon_body(struct daemon_params *dp) { - log_init(argv[0]); main_init(); update_timer.handler = do_update; noct_init(); - - msg(L_INFO, "Initializing PulseAudio"); pulse_init(); + mpd_init(); + + static struct main_signal term_sig = { + .signum = SIGTERM, + .handler = sigterm_handler, + }; + signal_add(&term_sig); - msg(L_INFO, "Entering main loop"); + msg(L_INFO, "Ursary daemon starting"); main_loop(); + msg(L_INFO, "Ursary daemon shut down"); + daemon_exit(dp); +} + +int main(int argc UNUSED, char **argv) +{ + opt_parse(&options, argv+1); + unsetenv("DISPLAY"); + unsetenv("HOME"); + + struct daemon_params dp = { + .flags = ((debug || no_fork) ? DAEMON_FLAG_SIMULATE : 0), + .pid_file = "/run/ursaryd.pid", + .run_as_user = "ursary", + }; + daemon_init(&dp); + + log_init(argv[0]); + if (!debug) + { + struct log_stream *ls = log_new_syslog("daemon", LOG_PID); + ls->levels = ~(1U << L_DEBUG); + log_set_default_stream(ls); + } + daemon_run(&dp, daemon_body); return 0; }