/* SPDX-License-Identifier: GPL-2.0 */ /** \file mixer.c A volume fader and alarm clock. * * The para_mixer(1) executable supports two interchangeable implementations * which implement the API defined in \ref mix.h. The alsa mixer (see \ref * alsa_mix.c) and the oss mixer (see \ref oss_mix.c) are based on the * corresponding low level mixer APIs provided by the operating system. On * FreeBSD and NetBSD only oss is supported while both are available on Linux, * with oss being a compatibility layer on top of alsa. * * This file contains the mixer-independent part, including the implementation * of all subcommands. * * para_mixer(1) does not employ the scheduler. It also does not use the * paraslash log facility but implements a custom log function which prefixes * log messages with the current date and time. */ /** \cond doxygen_ignore */ #include #include #include "mixer.lsg.h" #include "para.h" #include "lsu.h" #include "fd.h" #include "string.h" #include "mix.h" #include "error.h" DEFINE_PARA_ERRLIST; /* At least one of the two is defined if this file gets compiled. */ static const struct mixer *mixers[] = { #ifdef HAVE_ALSA &alsa_mixer, #endif #ifdef HAVE_OSS &oss_mixer, #endif }; #define NUM_SUPPORTED_MIXERS (ARRAY_SIZE(mixers)) #define FOR_EACH_MIXER(i) for ((i) = 0; (i) < NUM_SUPPORTED_MIXERS; (i)++) static struct lls_parse_result *lpr, *sub_lpr; #define CMD_PTR(_cmd) (lls_cmd(LSG_MIXER_CMD_ ## _cmd, mixer_suite)) #define OPT_RESULT(_cmd, _opt) (lls_opt_result( \ LSG_MIXER_ ## _cmd ## _OPT_ ## _opt, (LSG_MIXER_CMD_ ## _cmd == 0)? lpr : sub_lpr)) #define OPT_GIVEN(_cmd, _opt) (lls_opt_given(OPT_RESULT(_cmd, _opt))) #define OPT_STRING_VAL(_cmd, _opt) (lls_string_val(0, OPT_RESULT(_cmd, _opt))) #define OPT_UINT32_VAL(_cmd, _opt) (lls_uint32_val(0, OPT_RESULT(_cmd, _opt))) typedef int (*mixer_subcommand_handler_t)(const struct mixer *); #define EXPORT_CMD(_cmd) const mixer_subcommand_handler_t \ lsg_mixer_com_ ## _cmd ## _user_data = &com_ ## _cmd; static int loglevel; static __printf_2_3 void date_log(int ll, const char *fmt, ...) { va_list argp; time_t t1; struct tm *tm; if (ll < loglevel) return; time(&t1); tm = localtime(&t1); fprintf(stderr, "%d:%02d:%02d ", tm->tm_hour, tm->tm_min, tm->tm_sec); va_start(argp, fmt); vprintf(fmt, argp); va_end(argp); } __printf_2_3 void (*para_log)(int, const char*, ...) = date_log; static int set_channel(const struct mixer *m, struct mixer_handle *h, const char *channel) { PARA_DEBUG_LOG("using %s mixer channel\n", channel? channel : "default"); return m->set_channel(h, channel); } static void millisleep(int ms) { struct timespec ts; PARA_DEBUG_LOG("sleeping %dms\n", ms); if (ms < 0) return; ts.tv_sec = ms / 1000, ts.tv_nsec = (ms % 1000) * 1000 * 1000; nanosleep(&ts, NULL); } /* * This implements the inverse function of t -> t^alpha, scaled to the time * interval [0,T] and the range given by old_vol and new_vol. It returns the * amount of milliseconds until the given volume is reached. */ static unsigned volume_time(double vol, double old_vol, double new_vol, double T, double alpha) { double c, d, x; if (old_vol < new_vol) { c = old_vol; d = new_vol; } else { c = new_vol; d = old_vol; } x = T * exp(log(((vol - c) / (d - c))) / alpha); assert(x <= T); if (old_vol < new_vol) return x; else return T - x; } /* Fade to new volume in fade_time seconds. */ static int fade(const struct mixer *m, struct mixer_handle *h, uint32_t new_vol, uint32_t fade_time, bool verbose) { int i, T, old_vol, ret, slept, incr; double ms, alpha; uint32_t fe = OPT_UINT32_VAL(PARA_MIXER, FADE_EXPONENT); const char *ch = OPT_STRING_VAL(PARA_MIXER, MIXER_CHANNEL); if (fade_time <= 0 || fe >= 100) { PARA_NOTICE_LOG("setting %s to %u\n", ch, new_vol); ret = m->set(h, new_vol); if (ret < 0) return ret; goto sleep; } alpha = (100 - fe) / 100.0; ret = m->get(h); if (ret < 0) return ret; old_vol = ret; if (old_vol == new_vol) goto sleep; if (verbose) PARA_NOTICE_LOG("fading %s to %u in %u seconds\n", ch, new_vol, fade_time); incr = old_vol < new_vol? 1 : -1; T = fade_time * 1000; i = old_vol; slept = 0; do { ms = volume_time(i + incr, old_vol, new_vol, T, alpha); millisleep(ms - slept); slept = ms; i += incr; ret = m->set(h, i); if (ret < 0) return ret; } while (i != new_vol); return 1; sleep: sleep(fade_time); return ret; } static int open_mixer_and_set_channel(const struct mixer *m, struct mixer_handle **h) { int ret; ret = m->open(OPT_STRING_VAL(PARA_MIXER, MIXER_DEVICE), h); if (ret < 0) return ret; ret = set_channel(m, *h, OPT_STRING_VAL(PARA_MIXER, MIXER_CHANNEL)); if (ret == -E_BAD_CHANNEL) { char *channels = m->get_channels(*h); printf("Available channels: %s\n", channels); free(channels); } if (ret < 0) m->close(*h); return ret; } static int com_fade(const struct mixer *m) { uint32_t new_vol = OPT_UINT32_VAL(FADE, FADE_VOL); uint32_t fade_time = OPT_UINT32_VAL(FADE, FADE_TIME); struct mixer_handle *h; int ret; ret = open_mixer_and_set_channel(m, &h); if (ret < 0) return ret; ret = fade(m, h, new_vol, fade_time, true); m->close(h); return ret; } EXPORT_CMD(fade); static void run(const char *exe, const char *cmd) { int ret, status, fds[3] = {0, 0, 0}; pid_t pid; char *cmdline = make_message("%s %s", exe, cmd); PARA_INFO_LOG("%s\n", cmdline); ret = xexec(&pid, cmdline, fds); free(cmdline); if (ret < 0) { PARA_ERROR_LOG("%s\n", para_strerror(-ret)); goto fail; } do ret = waitpid(pid, &status, 0); while (ret == -1 && errno == EINTR); if (ret < 0) { PARA_ERROR_LOG("%s\n", strerror(errno)); goto fail; } if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) goto fail; return; fail: PARA_EMERG_LOG("command \"%s\" failed\n", cmd); exit(EXIT_FAILURE); } static char *get_status_item(const char *status_item_name) { pid_t pid; int ret, fds[3] = {0, 1, 0}; char *buf, *p, *item = NULL, *cmdline; cmdline = make_message("para_audioc -- stat --one-shot %s", status_item_name); ret = xexec(&pid, cmdline, fds); free(cmdline); if (ret < 0) { PARA_ERROR_LOG("%s\n", para_strerror(-ret)); return para_strdup(NULL); } buf = alloc(100); ret = read(fds[1], buf, 99); if (ret < 0) { PARA_ERROR_LOG("%s\n", strerror(errno)); goto free_buf; } buf[ret] = '\0'; p = strrchr(buf, '\n'); if (!p) goto free_buf; *p = '\0'; p = strchr(buf, ':'); if (!p || p[1] == '\0') goto free_buf; item = para_strdup(p + 2); free_buf: free(buf); close(fds[1]); do ret = waitpid(pid, NULL, 0); while (ret == -1 && errno == EINTR); return item? item : para_strdup(NULL); } static void client_cmd(const char *cmd) { run("para_client", cmd); } static void audioc_cmd(const char *cmd) { run("para_audioc", cmd); } static int set_initial_volume(const struct mixer *m, struct mixer_handle *h) { int i, ret; for (i = 0; i < OPT_GIVEN(SLEEP, IVOL); i++) { const char *val = lls_string_val(i, OPT_RESULT(SLEEP, IVOL)); char *p, *ch, *arg = para_strdup(val); int32_t iv; p = strchr(arg, ':'); if (p) { *p = '\0'; p++; ch = arg; } else { p = arg; ch = NULL; } ret = para_atoi32(p, &iv); if (ret < 0) { free(arg); return ret; } ret = m->set_channel(h, ch); if (!ch) ch = "default"; if (ret < 0) { PARA_WARNING_LOG("ignoring channel %s\n", ch); ret = 0; } else { PARA_INFO_LOG("%s: %d\n", ch, iv); ret = fade(m, h, iv, 3, false); } free(arg); if (ret < 0) return ret; } return set_channel(m, h, OPT_STRING_VAL(PARA_MIXER, MIXER_CHANNEL)); } static int change_mop(const char *mop, const struct mixer *m, struct mixer_handle *h) { int ret; char *current_mop, *status, *cmd; static bool initial_volume_set, playing; if (!mop || !*mop) return 0; current_mop = get_status_item("afs_mode"); status = get_status_item("status"); PARA_INFO_LOG("afs_mode: %s, status: %s\n", current_mop, status); playing = !strcmp(status, "playing"); if (strcmp(current_mop, mop)) { if (playing) { uint32_t old_vol; ret = m->get(h); if (ret < 0) goto free_status; old_vol = ret; ret = fade(m, h, 0, 3, false); if (ret < 0) goto free_status; audioc_cmd("off"); client_cmd("stop"); playing = false; audioc_cmd("on"); ret = m->set(h, old_vol); if (ret < 0) goto free_status; } else if (!strcmp(status, "paused")) client_cmd("next"); PARA_NOTICE_LOG("switching to %s\n", mop); cmd = make_message("select %s", mop); client_cmd(cmd); free(cmd); } if (!initial_volume_set) { initial_volume_set = true; ret = set_initial_volume(m, h); if (ret < 0) goto free_status; } if (!playing) client_cmd("play"); ret = 1; free_status: free(status); free(current_mop); return ret; } static int com_sleep(const struct mixer *m) { time_t t1, wake_time_epoch; struct tm *tm; int ret; const char *wake_time = OPT_STRING_VAL(SLEEP, WAKE_TIME); const char *initial_mood = OPT_STRING_VAL(SLEEP, INITIAL_MOOD); const char *fo_mood = OPT_STRING_VAL(SLEEP, FO_MOOD); const char *fi_mood = OPT_STRING_VAL(SLEEP, FI_MOOD); const char *sleep_mood = OPT_STRING_VAL(SLEEP, SLEEP_MOOD); int fit = OPT_UINT32_VAL(SLEEP, FI_TIME); int fot = OPT_UINT32_VAL(SLEEP, FO_TIME); int fiv = OPT_UINT32_VAL(SLEEP, FI_VOL); int fov = OPT_UINT32_VAL(SLEEP, FO_VOL); int32_t hour, min = 0; char *tmp, *wt; struct mixer_handle *h; wt = para_strdup(wake_time + (wake_time[0] == '+')); /* calculate wake time */ time(&t1); tmp = strchr(wt, ':'); if (tmp) { *tmp = '\0'; tmp++; ret = para_atoi32(tmp, &min); if (ret < 0) { free(wt); return ret; } } ret = para_atoi32(wt, &hour); free(wt); if (ret < 0) return ret; if (wake_time[0] == '+') { /* relative */ t1 += hour * 60 * 60 + min * 60; tm = localtime(&t1); } else { tm = localtime(&t1); if (tm->tm_hour > hour || (tm->tm_hour == hour && tm->tm_min> min)) { t1 += 86400; /* wake time is tomorrow */ tm = localtime(&t1); } tm->tm_hour = hour; tm->tm_min = min; tm->tm_sec = 0; } wake_time_epoch = mktime(tm); PARA_INFO_LOG("waketime: %d:%02d\n", tm->tm_hour, tm->tm_min); ret = open_mixer_and_set_channel(m, &h); if (ret < 0) return ret; ret = change_mop(initial_mood, m, h); if (ret < 0) goto close_mixer; if (ret > 0) /* playing initial mood */ sleep(OPT_UINT32_VAL(SLEEP, INITIAL_DELAY)); ret = change_mop(fo_mood, m, h); if (ret < 0) goto close_mixer; ret = fade(m, h, fov, ret > 0? fot : 3, ret > 0); if (ret < 0) goto close_mixer; ret = change_mop(sleep_mood, m, h); if (ret < 0) goto close_mixer; if (ret == 0) /* no sleep mood */ client_cmd("stop"); time(&t1); if (wake_time_epoch > t1 + fit) sleep(wake_time_epoch - t1 - fit); ret = change_mop(fi_mood, m, h); if (ret < 0) goto close_mixer; ret = fade(m, h, fiv, ret > 0? fit : 3, ret > 0); close_mixer: m->close(h); return ret; } EXPORT_CMD(sleep); static int com_snooze(const struct mixer *m) { uint32_t sovol = OPT_UINT32_VAL(SNOOZE, SO_VOL); uint32_t sotime = OPT_UINT32_VAL(SNOOZE, SO_TIME); uint32_t stime = OPT_UINT32_VAL(SNOOZE, SNOOZE_TIME); uint32_t sivol = OPT_UINT32_VAL(SNOOZE, SI_VOL); uint32_t sitime = OPT_UINT32_VAL(SNOOZE, SI_TIME); struct mixer_handle *h; int ret = open_mixer_and_set_channel(m, &h); if (ret < 0) return ret; ret = fade(m, h, sovol, sotime, true); if (ret < 0) goto close_mixer; client_cmd("pause"); PARA_NOTICE_LOG("%" PRIu32 " seconds snooze time...\n", stime); sleep(stime); client_cmd("play"); ret = fade(m, h, sivol, sitime, true); close_mixer: m->close(h); return ret; } EXPORT_CMD(snooze); static int com_set(const struct mixer *m) { struct mixer_handle *h; int ret; uint32_t val = OPT_UINT32_VAL(SET, VAL); ret = open_mixer_and_set_channel(m, &h); if (ret < 0) return ret; PARA_NOTICE_LOG("new value: %u\n", val); ret = m->set(h, val); m->close(h); return ret; } EXPORT_CMD(set); static const struct mixer *get_mixer_or_die(void) { int i; if (!OPT_GIVEN(PARA_MIXER, MIXER_API)) i = 0; /* default: use first mixer */ else FOR_EACH_MIXER(i) if (!strcmp(mixers[i]->name, OPT_STRING_VAL(PARA_MIXER, MIXER_API))) break; if (i < NUM_SUPPORTED_MIXERS) { PARA_NOTICE_LOG("using %s mixer API\n", mixers[i]->name); return mixers[i]; } printf("available mixer APIs: "); FOR_EACH_MIXER(i) printf("%s%s%s ", i == 0? "[" : "", mixers[i]->name, i == 0? "]" : ""); printf("\n"); exit(EXIT_FAILURE); } static void show_subcommands(void) { const struct lls_command *cmd; int i; printf("Subcommands:\n"); for (i = 1; (cmd = lls_cmd(i, mixer_suite)); i++) { const char *name = lls_command_name(cmd); const char *purpose = lls_purpose(cmd); printf("%-20s%s\n", name, purpose); } } static int com_help(__a_unused const struct mixer *m) { const struct lls_command *cmd; const struct lls_opt_result *r_l = OPT_RESULT(HELP, LONG); char *txt, *errctx; const char *name; int ret; ret = lls_check_arg_count(sub_lpr, 0, 1, NULL); if (ret < 0) return ret; if (lls_num_inputs(sub_lpr) == 0) { show_subcommands(); return 0; } name = lls_input(0, sub_lpr); ret = lls(lls_lookup_subcmd(name, mixer_suite, &errctx)); if (ret < 0) { if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); return ret; } cmd = lls_cmd(ret, mixer_suite); if (lls_opt_given(r_l)) txt = lls_long_help(cmd); else txt = lls_short_help(cmd); printf("%s", txt); free(txt); return 0; } EXPORT_CMD(help); static void handle_help_flags(void) { char *help; if (OPT_GIVEN(PARA_MIXER, DETAILED_HELP)) help = lls_long_help(CMD_PTR(PARA_MIXER)); else if (OPT_GIVEN(PARA_MIXER, HELP)) help = lls_short_help(CMD_PTR(PARA_MIXER)); else return; printf("%s", help); free(help); show_subcommands(); exit(EXIT_SUCCESS); } static int parse_and_merge_config_file(const struct lls_command *cmd) { int ret; struct lls_parse_result **lprp = (cmd == lls_cmd(0, mixer_suite))? &lpr : &sub_lpr; ret = lsu_merge_config_file_options(OPT_STRING_VAL(PARA_MIXER, CONFIG_FILE), "mixer.conf", lprp, cmd, mixer_suite, 0 /* flags */); if (ret < 0) return ret; loglevel = OPT_UINT32_VAL(PARA_MIXER, LOGLEVEL); return 1; } /** \endcond * * The main function of para_mixer(1). * * \param argc Options are defined in the mixer lopsub suite. * \param argv The mixer suite also contains the definitions of all subcommands. * * This function parses the command line, then calls the appropriate subcommand * handler via the lopsub user_data pointer. * * \return EXIT_SUCCESS or EXIT_FAILURE. */ int main(int argc, char *argv[]) { const struct lls_command *cmd = CMD_PTR(PARA_MIXER); int ret; char *errctx; const char *subcmd; const struct mixer *m; unsigned n; ret = lls(lls_parse(argc, argv, cmd, &lpr, &errctx)); if (ret < 0) goto fail; loglevel = OPT_UINT32_VAL(PARA_MIXER, LOGLEVEL); version_handle_flag("mixer", OPT_GIVEN(PARA_MIXER, VERSION)); handle_help_flags(); n = lls_num_inputs(lpr); if (n == 0) { show_subcommands(); ret = 0; goto free_lpr; } ret = parse_and_merge_config_file(cmd); if (ret < 0) goto free_lpr; subcmd = lls_input(0, lpr); ret = lls(lls_lookup_subcmd(subcmd, mixer_suite, &errctx)); if (ret < 0) goto fail; cmd = lls_cmd(ret, mixer_suite); ret = lls(lls_parse(n, argv + argc - n, cmd, &sub_lpr, &errctx)); if (ret < 0) goto free_lpr; ret = parse_and_merge_config_file(cmd); if (ret < 0) goto free_sub_lpr; m = get_mixer_or_die(); ret = (*(mixer_subcommand_handler_t *)(lls_user_data(cmd)))(m); free_sub_lpr: lls_free_parse_result(sub_lpr, cmd); free_lpr: lls_free_parse_result(lpr, CMD_PTR(PARA_MIXER)); if (ret >= 0) return EXIT_SUCCESS; fail: if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); PARA_EMERG_LOG("%s\n", para_strerror(-ret)); return EXIT_FAILURE; }