/* SPDX-License-Identifier: GPL-2.0 */ /** \file play.c Standalone command line player. * * para_play(1) employs the buffer tree API (see \ref buffer_tree.h) and the * paraslash scheduler (see \ref sched.h), to set up a buffer tree linking * the afh receiver, the corresponding decoder and the default writer to * play audio files encoded in any of the supported audio formats. * * In insert mode, interactive command line editing is implemented by employing * the i9e API defined in \ref interactive.h. A substantial amount of the * code of this file is the command handlers, which are executed from the * i9e subsystem when a bound key is pressed or a subcommand has been entered * in insert mode. */ /** \cond doxygen_ignore */ #include #include #include "recv_cmd.lsg.h" #include "filter_cmd.lsg.h" #include "play_cmd.lsg.h" #include "write_cmd.lsg.h" #include "play.lsg.h" #include "para.h" #include "lsu.h" #include "list.h" #include "error.h" #include "buffer_tree.h" #include "string.h" #include "sched.h" #include "filter.h" #include "afh.h" #include "recv.h" #include "write.h" #include "fd.h" #include "interactive.h" DEFINE_PARA_ERRLIST; static struct lls_parse_result *play_lpr; #define CMD_PTR (lls_cmd(0, play_suite)) #define OPT_RESULT(_name) \ (lls_opt_result(LSG_PLAY_PARA_PLAY_OPT_ ## _name, play_lpr)) #define OPT_GIVEN(_name) (lls_opt_given(OPT_RESULT(_name))) #define OPT_UINT32_VAL(_name) (lls_uint32_val(0, OPT_RESULT(_name))) #define OPT_STRING_VAL(_name) (lls_string_val(0, OPT_RESULT(_name))) /* * Describes a request to change the state of para_play. * * The play task structure contains an integer of this type, which is set by * command handlers to indicate that a request has arrived. The post_monitor() * function of the play task investigates the value during each iteration * of the scheduler loop to dispatch the pending request. */ enum state_change_request_type { /* Everybody is happy. */ CRT_NONE, /* Stream must be repositioned (com_jmp(), com_ff()). */ CRT_REPOS, /* New file should be loaded (com_next()). */ CRT_FILE_CHANGE, /* Someone wants us for dead (com_quit()). */ CRT_TERM_RQ }; struct play_task { struct task *task; /* A bit array of invalid files (those will be skipped). */ bool *invalid; /* The file which is currently open. */ unsigned current_file; /* When to update the status again. */ struct timeval next_update; /* Root of the buffer tree for command and status output. */ struct btr_node *btrn; /* The decoding machinery. */ struct receiver_node rn; struct filter_node fn; struct writer_node wn; /* See comment to enum state_change_request_type above. */ enum state_change_request_type rq; /* only relevant if rq == CRT_FILE_CHANGE */ unsigned next_file; /* * True when in command mode, i.e. when we display the current status * and wait for keystrokes. False if we operate in insert mode, * reading complete input lines at the prompt. */ bool background; /* We have the *intention* to play. Set by com_play(). */ bool playing; /* as returned by afh_recv->open() */ int audio_format_num; /* retrieved via the btr exec mechanism */ long unsigned start_chunk; long unsigned seconds; long unsigned num_chunks; char *afhi_txt; }; struct play_command_info { int (*handler)(struct lls_parse_result *lpr); }; static int loglevel = LL_WARNING; /* The log function which writes log messages to stderr. */ INIT_STDERR_LOGGING(loglevel); char *stat_item_values[NUM_STAT_ITEMS] = {NULL}; static struct sched *sched; static struct play_task play_task, *pt = &play_task; #define AFH_RECV_CMD (lls_cmd(LSG_RECV_CMD_CMD_AFH, recv_cmd_suite)) #define AFH_RECV ((struct receiver *)lls_user_data(AFH_RECV_CMD)) static unsigned *shuffle_map; static const char *get_playlist_file(unsigned idx) { return lls_input(shuffle_map[idx], play_lpr); } static void handle_help_flags(void) { char *help; if (OPT_GIVEN(DETAILED_HELP)) help = lls_long_help(CMD_PTR); else if (OPT_GIVEN(HELP) || lls_num_inputs(play_lpr) == 0) help = lls_short_help(CMD_PTR); else return; printf("%s\n", help); free(help); exit(EXIT_SUCCESS); } static void parse_config_or_die(int argc, char *argv[]) { int i, ret; unsigned num_kmas; char *errctx; ret = lls(lls_parse(argc, argv, CMD_PTR, &play_lpr, &errctx)); if (ret < 0) { if (errctx) PARA_EMERG_LOG("%s\n", errctx); free(errctx); PARA_EMERG_LOG("failed to parse command line options: %s\n", para_strerror(-ret)); exit(EXIT_FAILURE); } loglevel = OPT_UINT32_VAL(LOGLEVEL); version_handle_flag("play", OPT_GIVEN(VERSION)); handle_help_flags(); /* also handles the zero-arg case */ ret = lsu_merge_config_file_options(OPT_STRING_VAL(CONFIG_FILE), "play.conf", &play_lpr, CMD_PTR, play_suite, 0 /* flags */); if (ret < 0) { PARA_EMERG_LOG("failed to parse config file: %s\n", para_strerror(-ret)); exit(EXIT_FAILURE); } loglevel = OPT_UINT32_VAL(LOGLEVEL); num_kmas = OPT_GIVEN(KEY_MAP); for (i = 0; i < num_kmas; i++) { const char *kma = lls_string_val(i, OPT_RESULT(KEY_MAP)); if (*kma && strchr(kma + 1, ':')) continue; PARA_EMERG_LOG("invalid key map arg: %s\n", kma); exit(EXIT_FAILURE); } } static char get_playback_state(void) { switch (pt->rq) { case CRT_NONE: return pt->playing? 'P' : 'U'; case CRT_REPOS: return 'R'; case CRT_FILE_CHANGE: return 'F'; case CRT_TERM_RQ: return 'X'; } assert(false); }; /* returns number of milliseconds */ static long unsigned get_play_time(void) { char state = get_playback_state(); long unsigned result; if (state != 'P' && state != 'U') return 0; if (pt->num_chunks == 0 || pt->seconds == 0) return 0; /* where the stream started (in milliseconds) */ result = 1000ULL * pt->start_chunk * pt->seconds / pt->num_chunks; if (pt->wn.btrn) { /* Add the uptime of the writer node */ struct timeval diff = {.tv_sec = 0}, wstime; btr_get_node_start(pt->wn.btrn, &wstime); if (wstime.tv_sec > 0) tv_diff(now, &wstime, &diff); result += tv2ms(&diff); } result = PARA_MIN(result, pt->seconds * 1000); result = PARA_MAX(result, 0UL); return result; } static void wipe_receiver_node(void) { PARA_NOTICE_LOG("cleaning up receiver node\n"); btr_remove_node(&pt->rn.btrn); AFH_RECV->close(&pt->rn); lls_free_parse_result(pt->rn.lpr, AFH_RECV_CMD); memset(&pt->rn, 0, sizeof(struct receiver_node)); } /* returns: 0 not eof, 1: eof, < 0: fatal error. */ static int get_playback_error(void) { int err; if (!pt->wn.task) return 1; err = task_status(pt->wn.task); if (err >= 0) return 0; if (task_status(pt->fn.task) >= 0) return 0; if (task_status(pt->rn.task) >= 0) return 0; if (err == -E_EOF) return 1; return err; } static int eof_cleanup(void) { const struct filter *decoder; const struct writer *w = writer_get(-1); /* default writer */ int ret; ret = get_playback_error(); if (ret == 0) return ret; PARA_NOTICE_LOG("cleaning up wn/fn nodes\n"); task_reap(&pt->wn.task); w->close(&pt->wn); btr_remove_node(&pt->wn.btrn); lls_free_parse_result(pt->wn.lpr, WRITE_CMD(pt->wn.wid)); memset(&pt->wn, 0, sizeof(struct writer_node)); decoder = filter_get(pt->fn.filter_num); task_reap(&pt->fn.task); if (decoder && decoder->close) decoder->close(&pt->fn); btr_remove_node(&pt->fn.btrn); lls_free_parse_result(pt->fn.lpr, FILTER_CMD(pt->fn.filter_num)); free(pt->fn.conf); memset(&pt->fn, 0, sizeof(struct filter_node)); task_reap(&pt->rn.task); btr_remove_node(&pt->rn.btrn); /* * On eof (ret > 0), we do not wipe the receiver node struct until a * new file is loaded because we still need it for jumping around when * paused. */ if (ret < 0) wipe_receiver_node(); return ret; } static int shuffle_compare(__a_unused const void *a, __a_unused const void *b) { return para_random(100) - 50; } static void init_shuffle_map(void) { unsigned n, num_inputs = lls_num_inputs(play_lpr); shuffle_map = arr_alloc(num_inputs, sizeof(unsigned)); for (n = 0; n < num_inputs; n++) shuffle_map[n] = n; if (!OPT_GIVEN(RANDOMIZE)) return; srandom(time(NULL)); qsort(shuffle_map, num_inputs, sizeof(unsigned), shuffle_compare); } static struct btr_node *new_recv_btrn(struct receiver_node *rn) { return btr_new_node(&(struct btr_node_description) EMBRACE(.name = lls_command_name(AFH_RECV_CMD), .context = rn, .handler = AFH_RECV->execute)); } static int open_new_file(void) { int ret; const char *path = get_playlist_file(pt->next_file); char *tmp = para_strdup(path), *errctx; char *argv[] = {"play", "-f", tmp, "-b", "0", NULL}; PARA_NOTICE_LOG("next file: %s\n", path); wipe_receiver_node(); pt->start_chunk = 0; pt->rn.btrn = new_recv_btrn(&pt->rn); ret = lls(lls_parse(ARRAY_SIZE(argv) - 1, argv, AFH_RECV_CMD, &pt->rn.lpr, &errctx)); free(tmp); assert(ret >= 0); pt->rn.receiver = AFH_RECV; ret = AFH_RECV->open(&pt->rn); if (ret < 0) { PARA_ERROR_LOG("could not open %s\n", path); goto fail; } pt->audio_format_num = ret; free(pt->afhi_txt); ret = btr_exec_up(pt->rn.btrn, "afhi", &pt->afhi_txt); if (ret < 0) pt->afhi_txt = make_message("[afhi command failed]\n"); ret = btr_exec_up(pt->rn.btrn, "seconds_total", &tmp); if (ret < 0) pt->seconds = 1; else { int32_t x; ret = para_atoi32(tmp, &x); pt->seconds = ret < 0? 1 : x; free(tmp); tmp = NULL; } ret = btr_exec_up(pt->rn.btrn, "chunks_total", &tmp); if (ret < 0) pt->num_chunks = 1; else { int32_t x; ret = para_atoi32(tmp, &x); pt->num_chunks = ret < 0? 1 : x; free(tmp); tmp = NULL; } return 1; fail: wipe_receiver_node(); return ret; } static int load_file(void) { const char *af; char *tmp, buf[20]; int ret; const struct filter *decoder; static struct lls_parse_result *filter_lpr, *writer_lpr; btr_remove_node(&pt->rn.btrn); if (!pt->rn.receiver || pt->next_file != pt->current_file) { ret = open_new_file(); if (ret < 0) return ret; } else { pt->rn.btrn = new_recv_btrn(&pt->rn); sprintf(buf, "repos %lu", pt->start_chunk); ret = btr_exec_up(pt->rn.btrn, buf, &tmp); if (ret < 0) PARA_CRIT_LOG("repos failed: %s\n", para_strerror(-ret)); freep(&tmp); } if (!pt->playing) return 0; /* set up decoding filter */ af = audio_format_name(pt->audio_format_num); tmp = make_message("%sdec", af); ret = filter_setup(tmp, &pt->fn.conf, &filter_lpr); freep(&tmp); if (ret < 0) goto fail; pt->fn.filter_num = ret; pt->fn.lpr = filter_lpr; decoder = filter_get(ret); pt->fn.btrn = btr_new_node(&(struct btr_node_description) EMBRACE(.name = filter_name(pt->fn.filter_num), .parent = pt->rn.btrn, .handler = decoder->execute, .context = &pt->fn)); if (decoder->open) decoder->open(&pt->fn); PARA_INFO_LOG("buffer tree:\n"); btr_log_tree(pt->rn.btrn, LL_INFO); /* setup default writer */ pt->wn.wid = check_writer_arg_or_die(NULL, &writer_lpr); pt->wn.lpr = writer_lpr; /* success, register tasks */ pt->rn.task = task_register( &(struct task_info) { .name = lls_command_name(AFH_RECV_CMD), .pre_monitor = AFH_RECV->pre_monitor, .post_monitor = AFH_RECV->post_monitor, .context = &pt->rn }, sched); sprintf(buf, "%s decoder", af); pt->fn.task = task_register( &(struct task_info) { .name = buf, .pre_monitor = decoder->pre_monitor, .post_monitor = decoder->post_monitor, .context = &pt->fn }, sched); register_writer_node(&pt->wn, pt->fn.btrn, sched); return 1; fail: wipe_receiver_node(); return ret; } static int next_valid_file(void) { int i, j = pt->current_file; unsigned num_inputs = lls_num_inputs(play_lpr); if (j == num_inputs - 1) { switch (OPT_UINT32_VAL(END_OF_PLAYLIST)) { case EOP_LOOP: break; case EOP_STOP: pt->playing = false; return 0; case EOP_QUIT: return -E_EOP; } } for (i = 0; i < num_inputs; i++) { j = (j + 1) % num_inputs; if (!pt->invalid[j]) return j; } return -E_NO_VALID_FILES; } static int load_next_file(void) { int ret; again: if (pt->rq == CRT_NONE) { pt->start_chunk = 0; ret = next_valid_file(); if (ret < 0) return ret; pt->next_file = ret; } else if (pt->rq == CRT_REPOS) pt->next_file = pt->current_file; ret = load_file(); if (ret < 0) { PARA_ERROR_LOG("%s: marking file as invalid\n", para_strerror(-ret)); pt->invalid[pt->next_file] = true; pt->rq = CRT_NONE; goto again; } pt->current_file = pt->next_file; pt->rq = CRT_NONE; return ret; } static void kill_stream(void) { if (pt->wn.task) task_notify(pt->wn.task, E_EOF); } /* only called from com_prev(), nec. only if we have readline */ static int previous_valid_file(void) { int i, j = pt->current_file; unsigned num_inputs = lls_num_inputs(play_lpr); for (i = 0; i < num_inputs; i++) { j--; if (j < 0) j = num_inputs - 1; if (!pt->invalid[j]) return j; } return -E_NO_VALID_FILES; } /* * Define the default (internal) key mappings and helper functions to get the * key sequence or the command from a key id, which is what we obtain from * i9e/readline when the key is pressed. * * In some of these helper functions we could return pointers to the constant * arrays defined below. However, for others we can not, so let's better be * consistent and allocate all returned strings on the heap. */ #define INTERNAL_KEYMAP_ENTRIES \ KEYMAP_ENTRY("^", "jmp 0"), \ KEYMAP_ENTRY("1", "jmp 10"), \ KEYMAP_ENTRY("2", "jmp 21"), \ KEYMAP_ENTRY("3", "jmp 32"), \ KEYMAP_ENTRY("4", "jmp 43"), \ KEYMAP_ENTRY("5", "jmp 54"), \ KEYMAP_ENTRY("6", "jmp 65"), \ KEYMAP_ENTRY("7", "jmp 76"), \ KEYMAP_ENTRY("8", "jmp 87"), \ KEYMAP_ENTRY("9", "jmp 98"), \ KEYMAP_ENTRY("+", "next"), \ KEYMAP_ENTRY("-", "prev"), \ KEYMAP_ENTRY(":", "bg"), \ KEYMAP_ENTRY("i", "info"), \ KEYMAP_ENTRY("l", "ls"), \ KEYMAP_ENTRY("s", "play"), \ KEYMAP_ENTRY("p", "pause"), \ KEYMAP_ENTRY("q", "quit"), \ KEYMAP_ENTRY("?", "help"), \ KEYMAP_ENTRY("\033[D", "ff -10"), \ KEYMAP_ENTRY("\033[C", "ff 10"), \ KEYMAP_ENTRY("\033[A", "ff 60"), \ KEYMAP_ENTRY("\033[B", "ff -60"), \ #define KEYMAP_ENTRY(a, b) a static const char *default_keyseqs[] = {INTERNAL_KEYMAP_ENTRIES}; #undef KEYMAP_ENTRY #define KEYMAP_ENTRY(a, b) b static const char *default_commands[] = {INTERNAL_KEYMAP_ENTRIES}; #undef KEYMAP_ENTRY #define NUM_INTERNALLY_MAPPED_KEYS ARRAY_SIZE(default_commands) #define NUM_MAPPED_KEYS (NUM_INTERNALLY_MAPPED_KEYS + OPT_GIVEN(KEY_MAP)) #define FOR_EACH_MAPPED_KEY(i) for (i = 0; i < NUM_MAPPED_KEYS; i++) static inline bool is_internal_key(int key) { return key < NUM_INTERNALLY_MAPPED_KEYS; } /* for internal keys, the key id is just the array index. */ static inline int get_internal_key_map_idx(int key) { assert(is_internal_key(key)); return key; } /* * For user-defined keys, we have to subtract NUM_INTERNALLY_MAPPED_KEYS. The * difference is the index to the array of user defined key maps. */ static inline int get_user_key_map_idx(int key) { assert(!is_internal_key(key)); return key - NUM_INTERNALLY_MAPPED_KEYS; } static inline int get_key_map_idx(int key) { return is_internal_key(key)? get_internal_key_map_idx(key) : get_user_key_map_idx(key); } static inline const char *get_user_key_map_arg(int key) { return lls_string_val(get_user_key_map_idx(key), OPT_RESULT(KEY_MAP)); } static inline char *get_internal_key_map_seq(int key) { return para_strdup(default_keyseqs[get_internal_key_map_idx(key)]); } static char *get_key_map_seq(int key) { const char *kma, *p; char *result; int len; if (is_internal_key(key)) return get_internal_key_map_seq(key); kma = get_user_key_map_arg(key); p = strchr(kma + 1, ':'); assert(p); /* We checked earlier that kma contains a colon */ len = p - kma; result = alloc(len + 1); memcpy(result, kma, len); result[len] = '\0'; return result; } static char *get_key_map_seq_safe(int key) { const char hex[] = "0123456789abcdef"; char *seq = get_key_map_seq(key), *sseq; size_t n, len = strlen(seq); if (len == 1 && isprint(*seq)) return seq; sseq = alloc(2 + 2 * len + 1); sseq[0] = '0'; sseq[1] = 'x'; for (n = 0; n < len; n++) { uint8_t val = (seq[n] & 0xf0) >> 4; sseq[2 + 2 * n] = hex[val]; val = seq[n] & 0xf; sseq[2 + 2 * n + 1] = hex[val]; } free(seq); sseq[2 + 2 * n] = '\0'; return sseq; } static inline char *get_internal_key_map_cmd(int key) { return para_strdup(default_commands[get_internal_key_map_idx(key)]); } static char *get_user_key_map_cmd(int key) { const char *kma = get_user_key_map_arg(key); const char *p = strchr(kma + 1, ':'); if (!p) return NULL; return para_strdup(p + 1); } static char *get_key_map_cmd(int key) { return is_internal_key(key)? get_internal_key_map_cmd(key) : get_user_key_map_cmd(key); } static char **get_mapped_keyseqs(void) { char **result; int i; result = arr_alloc(NUM_MAPPED_KEYS + 1, sizeof(char *)); FOR_EACH_MAPPED_KEY(i) { char *seq = get_key_map_seq(i); result[i] = seq; } result[i] = NULL; return result; } static const struct i9e_completer pp_completers[]; I9E_DUMMY_COMPLETER(jmp); I9E_DUMMY_COMPLETER(next); I9E_DUMMY_COMPLETER(prev); I9E_DUMMY_COMPLETER(fg); I9E_DUMMY_COMPLETER(bg); I9E_DUMMY_COMPLETER(ls); I9E_DUMMY_COMPLETER(info); I9E_DUMMY_COMPLETER(play); I9E_DUMMY_COMPLETER(pause); I9E_DUMMY_COMPLETER(tasks); I9E_DUMMY_COMPLETER(quit); I9E_DUMMY_COMPLETER(ff); static void help_completer(struct i9e_completion_info *ci, struct i9e_completion_result *cr) { char * const opts[] = {LSG_PLAY_CMD_HELP_OPTS, NULL}; if (ci->word[0] == '-') { i9e_complete_option(opts, ci, cr); return; } cr->matches = i9e_complete_commands(ci->word, pp_completers); } static const struct i9e_completer pp_completers[] = { #define LSG_PLAY_CMD_CMD(_name) {.name = #_name, \ .completer = _name ## _completer} LSG_PLAY_CMD_SUBCOMMANDS #undef LSG_PLAY_CMD_CMD {.name = NULL} }; static void attach_stdout(const char *name) { if (pt->btrn) return; pt->btrn = btr_new_node(&(struct btr_node_description) EMBRACE(.name = name)); i9e_attach_to_stdout(pt->btrn); } static void detach_stdout(void) { btr_remove_node(&pt->btrn); } #define EXPORT_PLAY_CMD_HANDLER(_cmd) \ const struct play_command_info lsg_play_cmd_com_ ## _cmd ## _user_data = { \ .handler = com_ ## _cmd \ }; static int com_quit(__a_unused struct lls_parse_result *lpr) { pt->rq = CRT_TERM_RQ; return 0; } EXPORT_PLAY_CMD_HANDLER(quit); static int com_help(struct lls_parse_result *lpr) { int i, ret; char *buf; size_t sz; unsigned n; const struct lls_opt_result *r = lls_opt_result(LSG_PLAY_CMD_HELP_OPT_LONG, lpr); bool long_help = lls_opt_given(r); if (!pt->background) { FOR_EACH_MAPPED_KEY(i) { bool internal = is_internal_key(i); int idx = get_key_map_idx(i); char *seq = get_key_map_seq_safe(i); char *kmc = get_key_map_cmd(i); sz = xasprintf(&buf, "%s key #%d: %s -> %s\n", internal? "internal" : "user-defined", idx, seq, kmc); btr_add_output(buf, sz, pt->btrn); free(seq); free(kmc); } return 0; } ret = lsu_com_help(long_help, lpr, play_cmd_suite, NULL, &buf, &n); if (ret < 0) { PARA_WARNING_LOG("%s", buf); /* no \n needed */ free(buf); } else btr_add_output(buf, n, pt->btrn); return ret; } EXPORT_PLAY_CMD_HANDLER(help); static int com_info(__a_unused struct lls_parse_result *lpr) { char *buf; size_t sz; static char dflt[] = "[no information available]"; sz = xasprintf(&buf, "playlist_pos: %u\npath: %s\n", pt->current_file, get_playlist_file(pt->current_file)); btr_add_output(buf, sz, pt->btrn); buf = pt->afhi_txt? pt->afhi_txt : dflt; btr_add_output_dont_free(buf, strlen(buf), pt->btrn); return 0; } EXPORT_PLAY_CMD_HANDLER(info); static void list_file(int num) { char *buf; size_t sz; sz = xasprintf(&buf, "%s %4d %s\n", num == pt->current_file? "*" : " ", num, get_playlist_file(num)); btr_add_output(buf, sz, pt->btrn); } static int com_tasks(__a_unused struct lls_parse_result *lpr) { static char state; char *buf; size_t sz; buf = get_task_list(sched); btr_add_output(buf, strlen(buf), pt->btrn); state = get_playback_state(); sz = xasprintf(&buf, "state: %c\n", state); btr_add_output(buf, sz, pt->btrn); return 0; } EXPORT_PLAY_CMD_HANDLER(tasks); static int com_ls(__a_unused struct lls_parse_result *lpr) { int i; unsigned num_inputs = lls_num_inputs(play_lpr); for (i = 0; i < num_inputs; i++) list_file(i); return 0; } EXPORT_PLAY_CMD_HANDLER(ls); static int com_play(struct lls_parse_result *lpr) { int32_t x; int ret; char state, *errctx; ret = lls(lls_check_arg_count(lpr, 0, 1, &errctx)); if (ret < 0) { if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); return ret; } state = get_playback_state(); if (lls_num_inputs(lpr) == 0) { if (state == 'P') return 0; pt->next_file = pt->current_file; pt->rq = CRT_REPOS; pt->playing = true; return 0; } ret = para_atoi32(lls_input(0, lpr), &x); if (ret < 0) return ret; if (x < 0 || x >= lls_num_inputs(play_lpr)) return -ERRNO_TO_PARA_ERROR(EINVAL); kill_stream(); pt->next_file = x; pt->rq = CRT_FILE_CHANGE; return 0; } EXPORT_PLAY_CMD_HANDLER(play); static int com_pause(__a_unused struct lls_parse_result *lpr) { char state; uint64_t ms; unsigned long cn; /* chunk num */ state = get_playback_state(); pt->playing = false; if (state != 'P') return 0; ms = get_play_time(); pt->playing = false; cn = 0; if (pt->seconds > 0) cn = ms * pt->num_chunks / pt->seconds / 1000 + 1; cn = PARA_MIN(cn, pt->num_chunks); pt->start_chunk = cn; pt->rq = CRT_REPOS; kill_stream(); return 0; } EXPORT_PLAY_CMD_HANDLER(pause); static int com_prev(__a_unused struct lls_parse_result *lpr) { int ret; ret = previous_valid_file(); if (ret < 0) return ret; kill_stream(); pt->next_file = ret; pt->rq = CRT_FILE_CHANGE; pt->start_chunk = 0; return 0; } EXPORT_PLAY_CMD_HANDLER(prev); static int com_next(__a_unused struct lls_parse_result *lpr) { int ret; ret = next_valid_file(); if (ret < 0) return ret; kill_stream(); pt->next_file = ret; pt->rq = CRT_FILE_CHANGE; pt->start_chunk = 0; return 0; } EXPORT_PLAY_CMD_HANDLER(next); static int com_fg(__a_unused struct lls_parse_result *lpr) { pt->background = false; return 0; } EXPORT_PLAY_CMD_HANDLER(fg); static int com_bg(__a_unused struct lls_parse_result *lpr) { pt->background = true; return 0; } EXPORT_PLAY_CMD_HANDLER(bg); static int com_jmp(struct lls_parse_result *lpr) { int32_t percent; int ret; char *errctx; ret = lls(lls_check_arg_count(lpr, 1, 1, &errctx)); if (ret < 0) { if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); return ret; } ret = para_atoi32(lls_input(0, lpr), &percent); if (ret < 0) return ret; if (percent < 0 || percent > 100) return -ERRNO_TO_PARA_ERROR(EINVAL); if (percent == 100) return com_next(NULL); if (pt->playing && !pt->fn.btrn) return 0; pt->start_chunk = (uint64_t)percent * pt->num_chunks / 100; if (!pt->playing) return 0; pt->rq = CRT_REPOS; kill_stream(); return 0; } EXPORT_PLAY_CMD_HANDLER(jmp); static int com_ff(struct lls_parse_result *lpr) { int32_t seconds; char *errctx; int ret; ret = lls(lls_check_arg_count(lpr, 1, 1, &errctx)); if (ret < 0) { if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); return ret; } ret = para_atoi32(lls_input(0, lpr), &seconds); if (ret < 0) return ret; if (pt->playing && !pt->fn.btrn) return 0; seconds += (get_play_time() + 500) / 1000; seconds = PARA_MIN(seconds, (typeof(seconds))pt->seconds - 4); seconds = PARA_MAX(seconds, 0); pt->start_chunk = (uint64_t)pt->num_chunks * seconds / pt->seconds; pt->start_chunk = PARA_MIN(pt->start_chunk, pt->num_chunks - 1); pt->start_chunk = PARA_MAX(pt->start_chunk, 0UL); if (!pt->playing) return 0; pt->rq = CRT_REPOS; kill_stream(); return 0; } EXPORT_PLAY_CMD_HANDLER(ff); static int run_command(char *line) { int ret, argc; char **argv = NULL; char *errctx = NULL; const struct play_command_info *pci; struct lls_parse_result *lpr; const struct lls_command *cmd; attach_stdout(__func__); ret = create_argv(line, " ", &argv); if (ret < 0) goto out; if (ret == 0) goto out; argc = ret; ret = lls(lls_lookup_subcmd(argv[0], play_cmd_suite, &errctx)); if (ret < 0) goto out; cmd = lls_cmd(ret, play_cmd_suite); ret = lls(lls_parse(argc, argv, cmd, &lpr, &errctx)); if (ret < 0) goto out; pci = lls_user_data(cmd); ret = pci->handler(lpr); lls_free_parse_result(lpr, cmd); out: if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); free_argv(argv); return ret; } static int play_i9e_line_handler(char *line) { return run_command(line); } static int play_i9e_key_handler(int key) { int idx = get_key_map_idx(key); char *seq = get_key_map_seq(key); char *cmd = get_key_map_cmd(key); bool internal = is_internal_key(key); PARA_NOTICE_LOG("pressed %d: %s key #%d (%s -> %s)\n", key, internal? "internal" : "user-defined", idx, seq, cmd); run_command(cmd); free(seq); free(cmd); pt->next_update = *now; return 0; } static struct i9e_client_info ici = { .fds = {0, 1, 2}, .prompt = "para_play> ", .line_handler = play_i9e_line_handler, .key_handler = play_i9e_key_handler, .completers = pp_completers, }; static void sigint_handler(int sig) { pt->background = true; i9e_signal_dispatch(sig); } /* * We start with para_log() set to the standard log function which writes to * stderr. Once the i9e subsystem has been initialized, we switch to the i9e * log facility. */ static void session_open(void) { int ret; char *history_file; struct sigaction act; PARA_NOTICE_LOG("\n%s\n", version_text("play")); if (OPT_GIVEN(HISTORY_FILE)) history_file = para_strdup(OPT_STRING_VAL(HISTORY_FILE)); else { char *dot_para = create_dot_paraslash(); history_file = make_message("%s/play.history", dot_para); free(dot_para); } ici.history_file = history_file; ici.loglevel = loglevel; act.sa_handler = sigint_handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, NULL); act.sa_handler = i9e_signal_dispatch; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGWINCH, &act, NULL); sched = sched_new(i9e_poll); ici.bound_keyseqs = get_mapped_keyseqs(); pt->btrn = ici.producer = btr_new_node(&(struct btr_node_description) EMBRACE(.name = __func__)); ret = i9e_open(&ici, sched); if (ret < 0) goto out; para_log = i9e_log; return; out: free(history_file); if (ret >= 0) return; PARA_EMERG_LOG("fatal: %s\n", para_strerror(-ret)); exit(EXIT_FAILURE); } static void session_update_time_string(char *str, unsigned len) { if (pt->background) return; if (pt->btrn) { if (btr_get_output_queue_size(pt->btrn) > 0) return; if (btr_get_input_queue_size(pt->btrn) > 0) return; } i9e_print_status_bar(str, len); } static void play_pre_monitor(struct sched *s, __a_unused void *context) { char state; sched_monitor_readfd(STDIN_FILENO, s); state = get_playback_state(); if (state == 'R' || state == 'F' || state == 'X') return sched_min_delay(s); sched_request_barrier_or_min_delay(&pt->next_update, s); } static unsigned get_time_string(char **result) { int seconds, length; char state = get_playback_state(); /* do not return anything if things are about to change */ if (state != 'P' && state != 'U') { *result = NULL; return 0; } length = pt->seconds; if (length == 0) return xasprintf(result, "0:00 [0:00] (0%%/0:00)"); seconds = (get_play_time() + 500) / 1000; return xasprintf(result, "#%u: %d:%02d [%d:%02d] (%d%%/%d:%02d) %s", pt->current_file, seconds / 60, seconds % 60, (length - seconds) / 60, (length - seconds) % 60, length? (seconds * 100 + length / 2) / length : 0, length / 60, length % 60, get_playlist_file(pt->current_file) ); } static int play_post_monitor(__a_unused struct sched *s, __a_unused void *context) { int ret, i9e_error; if (pt->background) detach_stdout(); else attach_stdout(__func__); i9e_error = i9e_get_error(); ret = eof_cleanup(); if (pt->rq == CRT_TERM_RQ || i9e_error < 0) /* com_quit() or CTRL+D */ kill_stream(); /* terminate receiver/filter/writer */ if ((ret < 0 || pt->rq == CRT_TERM_RQ) && i9e_error >= 0) i9e_signal_dispatch(SIGTERM); /* terminate the i9e task */ if (ret < 0) /* unexpected error from the writer node */ return ret; if (ret != 0 && i9e_error < 0) /* eof, and i9e has died */ return i9e_error; if (!pt->wn.btrn && !pt->fn.btrn) { char state = get_playback_state(); if (state == 'P' || state == 'R' || state == 'F') { PARA_NOTICE_LOG("state: %c\n", state); ret = load_next_file(); if (ret < 0) { PARA_ERROR_LOG("%s\n", para_strerror(-ret)); pt->rq = CRT_TERM_RQ; return 1; } pt->next_update = *now; } } if (tv_diff(now, &pt->next_update, NULL) >= 0) { char *str; unsigned len = get_time_string(&str); struct timeval delay = {.tv_sec = 0, .tv_usec = 100 * 1000}; if (str && len > 0) session_update_time_string(str, len); free(str); tv_add(now, &delay, &pt->next_update); } return 1; } /** \endcond * * The main function of para_play(1). * * \param argc Options are defined in the play lopsub suite. * \param argv Subcommands (stop, pause, play, etc.) are realized as lopsub * subcommands. These are defined in the play_cmd suite file. * * para_play(1) submits various tasks to the paraslash scheduler. The receiver, * filter and writer tasks require one task each to maintain their underlying * buffer tree node. These tasks only exist when an audio file is playing. * * The i9e task, which is submitted and maintained by the i9e subsystem, * either reads single key strokes (command mode) or a complete input line * (insert mode), then calls the corresponding command handler implemented * in this file, for example as com_stop(). The command handlers modify the * request field of the central play task structure but do not perform any * real work. Rather, the request is acted upon in play context, i.e. in * the post-monitor function of the play task. * * \return EXIT_FAILURE or EXIT_SUCCESS. */ int main(int argc, char *argv[]) { int ret, i; unsigned num_inputs; parse_config_or_die(argc, argv); session_open(); num_inputs = lls_num_inputs(play_lpr); init_shuffle_map(); pt->invalid = arr_zalloc(num_inputs, sizeof(*pt->invalid)); pt->rq = CRT_FILE_CHANGE; pt->playing = true; pt->task = task_register(&(struct task_info){ .name = "play", .pre_monitor = play_pre_monitor, .post_monitor = play_post_monitor, .context = pt, }, sched); ret = schedule(sched); sched_shutdown(sched); i9e_close(); wipe_receiver_node(); para_log = stderr_log; free(ici.history_file); FOR_EACH_MAPPED_KEY(i) free(get_key_map_seq(i)); free(ici.bound_keyseqs); free(pt->afhi_txt); lls_free_parse_result(play_lpr, CMD_PTR); if (ret < 0) PARA_ERROR_LOG("%s\n", para_strerror(-ret)); return ret < 0? EXIT_FAILURE : EXIT_SUCCESS; }