para_play, implementation.
authorAndre Noll <maan@systemlinux.org>
Thu, 5 Apr 2012 23:18:23 +0000 (01:18 +0200)
committerAndre Noll <maan@systemlinux.org>
Sun, 18 Nov 2012 19:28:29 +0000 (20:28 +0100)
This replaces the dummy implementation added in the previous commit
by a real implementation.

para_play makes use of all new concepts that have been previously
introduced. In particular, it employs the newly added single key
mode of the i9e subsystem and uses task notifications to shut down
the writer node when the user stops playback.

error.h
m4/gengetopt/play.m4
play.c
play.cmd

diff --git a/error.h b/error.h
index fd2e01a..981b2b7 100644 (file)
--- a/error.h
+++ b/error.h
@@ -51,7 +51,10 @@ extern const char **para_errlist[];
 
 
 #define PLAY_ERRORS \
-       PARA_ERROR(PLAY_SYNTAX, "play syntax error"), \
+       PARA_ERROR(PLAY_SYNTAX, "para_play: syntax error"), \
+       PARA_ERROR(NO_VALID_FILES, "no valid file found in playlist"), \
+       PARA_ERROR(TERM_RQ, "user termination request"), \
+       PARA_ERROR(BAD_PLAY_CMD, "invalid command"), \
 
 
 #define FLACDEC_FILTER_ERRORS \
index 9bdc767..57f954c 100644 (file)
@@ -2,6 +2,8 @@ args "--unamed-opts=audio_file --no-handle-version --conf-parser --no-handle-hel
 include(header.m4)
 define(CURRENT_PROGRAM,para_play)
 define(DEFAULT_CONFIG_FILE,~/.paraslash/play.conf)
+define(DEFAULT_HISTORY_FILE,~/.paraslash/play.history)
+
 <qu>
 #########################
 section "General options"
@@ -9,3 +11,28 @@ section "General options"
 </qu>
 include(loglevel.m4)
 include(config_file.m4)
+include(history_file.m4)
+<qu>
+
+###############################
+section "Options for para_play"
+###############################
+
+option "randomize" z
+#~~~~~~~~~~~~~~~~~~~
+"randomize playlist at startup."
+flag off
+
+option "key_map" k
+#~~~~~~~~~~~~~~~~~
+"Map key k to a command."
+
+string typestr = "key:command [args]"
+optional
+multiple
+details = "
+       This option may be given multiple times, one for each key
+       mapping. Example:
+               5:jmp 50
+"
+</qu>
diff --git a/play.c b/play.c
index fc33f68..7537ddd 100644 (file)
--- a/play.c
+++ b/play.c
@@ -7,6 +7,9 @@
 /** \file play.c Paraslash's standalone player. */
 
 #include <regex.h>
+#include <sys/time.h>
+#include <fnmatch.h>
+#include <signal.h>
 
 #include "para.h"
 #include "list.h"
 #include "write_common.h"
 #include "fd.h"
 
-static struct play_args_info conf;
+/**
+ * Besides playback tasks which correspond to the receiver/filter/writer nodes,
+ * para_play creates two further tasks: The play task and the i9e task. It is
+ * important whether a function can be called in the context of para_play or
+ * i9e or both. As a rule, all command handlers are called only in i9e context via
+ * the line handler (input mode) or the key handler (command mode) below.
+ *
+ * Playlist handling is done exclusively in play context.
+ */
+
+/**
+ * Describes a request to change the state of para_play.
+ *
+ * There is only one variable of this type: \a rq of the global play task
+ * structure. Command handlers only set this variable and the post_select()
+ * function of the play task investigates its value during each iteration of
+ * the scheduler run and performs the actual work.
+ */
+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;
+       /*
+               bg: read lines at prompt, fg: display status and wait
+               for keystroke.
+       */
+       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;
+};
 
 /** Initialize the array of errors for para_play. */
 INIT_PLAY_ERRLISTS;
@@ -33,34 +103,71 @@ INIT_PLAY_ERRLISTS;
 /* Activate the afh receiver. */
 extern void afh_recv_init(struct receiver *r);
 #undef AFH_RECEIVER
+/** Initialization code for a receiver struct. */
 #define AFH_RECEIVER {.name = "afh", .init = afh_recv_init},
+/** This expands to the array of all receivers. */
 DEFINE_RECEIVER_ARRAY;
 
-/* FIXME: This is needed by the amp filter. */
-char *stat_item_values[NUM_STAT_ITEMS] = {NULL};
-
 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};
+
+/** Iterate over all files in the playlist. */
+#define FOR_EACH_PLAYLIST_FILE(i) for (i = 0; i < conf.inputs_num; i++)
+static struct play_args_info conf;
+
+static struct sched sched = {.max_fileno = 0};
+static struct play_task play_task;
+static struct receiver *afh_recv;
+
+static void check_afh_receiver_or_die(void)
+{
+       int i;
+
+       FOR_EACH_RECEIVER(i) {
+               struct receiver *r = receivers + i;
+               if (strcmp(r->name, "afh"))
+                       continue;
+               afh_recv = r;
+               return;
+       }
+       PARA_EMERG_LOG("fatal: afh receiver not found\n");
+       exit(EXIT_FAILURE);
+}
+
+/** Description to be included in the --detailed-help output. */
+#define PP_DESC \
+"para_play is a command line audio player.\n" \
+"\n" \
+"It operates either in command mode or in insert mode. In insert mode it\n" \
+"presents a prompt and allows to enter para_play commands like stop, play, pause\n" \
+"etc. In command mode, the current audio file is shown and the program reads\n" \
+"single key strokes from stdin. Keys may be mapped to para_play commands.\n" \
+"Whenever a mapped key is pressed, the associated command is executed.\n" \
+
 __noreturn static void print_help_and_die(void)
 {
        int d = conf.detailed_help_given;
        const char **p = d? play_args_info_detailed_help
                : play_args_info_help;
 
-       printf_or_die("%s\n\n", PLAY_CMDLINE_PARSER_PACKAGE "-"
-               PLAY_CMDLINE_PARSER_VERSION);
+//     printf_or_die("%s\n\n", PLAY_CMDLINE_PARSER_PACKAGE "-"
+//             PLAY_CMDLINE_PARSER_VERSION);
+
        printf_or_die("%s\n\n", play_args_info_usage);
+       if (d)
+               printf_or_die("%s\n", PP_DESC);
        for (; *p; p++)
                printf_or_die("%s\n", *p);
-       print_filter_helps(d);
-       print_writer_helps(d);
        exit(0);
 }
 
 static void parse_config_or_die(int argc, char *argv[])
 {
-       int ret;
+       int i, ret;
        char *config_file;
        struct play_cmdline_parser_params params = {
                .override = 0,
@@ -93,6 +200,13 @@ static void parse_config_or_die(int argc, char *argv[])
                params.check_required = 1;
                play_cmdline_parser_config_file(config_file, &conf, &params);
        }
+       for (i = 0; i < conf.key_map_given; i++) {
+               char *s = strchr(conf.key_map_arg[i] + 1, ':');
+               if (s)
+                       continue;
+               PARA_EMERG_LOG("invalid key map arg: %s\n", conf.key_map_arg[i]);
+               goto err;
+       }
        free(config_file);
        return;
 err:
@@ -100,13 +214,1070 @@ err:
        exit(EXIT_FAILURE);
 }
 
+static char get_playback_state(struct play_task *pt)
+{
+       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);
+};
+
+static long unsigned get_play_time(struct play_task *pt)
+{
+       char state = get_playback_state(pt);
+       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 seconds) */
+       result = 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 += diff.tv_sec;
+       }
+       result = PARA_MIN(result, pt->seconds);
+       result = PARA_MAX(result, 0UL);
+       return result;
+}
+
+static void wipe_receiver_node(struct play_task *pt)
+{
+       PARA_NOTICE_LOG("cleaning up receiver node\n");
+       btr_remove_node(&pt->rn.btrn);
+       afh_recv->close(&pt->rn);
+       afh_recv->free_config(pt->rn.conf);
+       memset(&pt->rn, 0, sizeof(struct receiver_node));
+}
+
+/* returns: 0 not eof, 1: eof, < 0: fatal error.  */
+static int get_playback_error(struct play_task *pt)
+{
+       int err = pt->wn.task.error;
+
+       if (err >= 0)
+               return 0;
+       if (pt->fn.task.error >= 0)
+               return 0;
+       if (pt->rn.task.error >= 0)
+               return 0;
+       if (err == -E_BTR_EOF || err == -E_RECV_EOF || err == -E_EOF
+                       || err == -E_WRITE_COMMON_EOF)
+               return 1;
+       return err;
+}
+
+static int eof_cleanup(struct play_task *pt)
+{
+       struct writer *w = writers + DEFAULT_WRITER;
+       struct filter *decoder = filters + pt->fn.filter_num;
+       int ret;
+
+       ret = get_playback_error(pt);
+       if (ret == 0)
+               return ret;
+       PARA_NOTICE_LOG("cleaning up wn/fn nodes\n");
+       w->close(&pt->wn);
+       btr_remove_node(&pt->wn.btrn);
+       w->free_config(pt->wn.conf);
+       memset(&pt->wn, 0, sizeof(struct writer_node));
+
+       decoder->close(&pt->fn);
+       btr_remove_node(&pt->fn.btrn);
+       free(pt->fn.conf);
+       memset(&pt->fn, 0, sizeof(struct filter_node));
+
+       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(pt);
+       return ret;
+}
+
+static int shuffle_compare(__a_unused const void *a, __a_unused const void *b)
+{
+       return para_random(100) - 50;
+}
+
+static void shuffle(char **base, size_t num)
+{
+       srandom(now->tv_sec);
+       qsort(base, num, sizeof(char *), shuffle_compare);
+}
+
+static struct btr_node *new_recv_btrn(struct receiver_node *rn)
+{
+       return btr_new_node(&(struct btr_node_description)
+               EMBRACE(.name = afh_recv->name, .context = rn,
+                       .handler = afh_recv->execute));
+}
+
+static int open_new_file(struct play_task *pt)
+{
+       int ret;
+       char *tmp, *path = conf.inputs[pt->next_file], *afh_recv_conf[] =
+               {"play", "-f", path, "-b", "0", NULL};
+
+       PARA_NOTICE_LOG("next file: %s\n", path);
+       wipe_receiver_node(pt);
+       pt->start_chunk = 0;
+       pt->rn.btrn = new_recv_btrn(&pt->rn);
+       pt->rn.conf = afh_recv->parse_config(ARRAY_SIZE(afh_recv_conf) - 1,
+               afh_recv_conf);
+       assert(pt->rn.conf);
+       pt->rn.receiver = afh_recv;
+       ret = afh_recv->open(&pt->rn);
+       if (ret < 0) {
+               PARA_ERROR_LOG("could not open %s: %s\n", path,
+                       para_strerror(-ret));
+               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;
+       }
+       pt->rn.task.pre_select = afh_recv->pre_select;
+       pt->rn.task.post_select = afh_recv->post_select;
+       sprintf(pt->rn.task.status, "%s receiver node", afh_recv->name);
+       return 1;
+fail:
+       wipe_receiver_node(pt);
+       return ret;
+}
+
+static int load_file(struct play_task *pt)
+{
+       const char *af;
+       char *tmp;
+       int ret;
+       struct filter *decoder;
+
+       btr_remove_node(&pt->rn.btrn);
+       if (!pt->rn.receiver || pt->next_file != pt->current_file) {
+               ret = open_new_file(pt);
+               if (ret < 0)
+                       return ret;
+       } else {
+               char buf[20];
+               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 = check_filter_arg(tmp, &pt->fn.conf);
+       freep(&tmp);
+       if (ret < 0)
+               goto fail;
+       pt->fn.filter_num = ret;
+       decoder = filters + ret;
+       pt->fn.task.pre_select = decoder->pre_select;
+       pt->fn.task.post_select = decoder->post_select;
+       sprintf(pt->fn.task.status, "%s decoder", af);
+       pt->fn.btrn = btr_new_node(&(struct btr_node_description)
+               EMBRACE(.name = decoder->name, .parent = pt->rn.btrn,
+                       .handler = decoder->execute, .context = &pt->fn));
+       decoder->open(&pt->fn);
+
+       /* setup default writer */
+       pt->wn.conf = check_writer_arg_or_die(NULL, &pt->wn.writer_num);
+       pt->wn.task.error = 0;
+
+       /* success, register tasks */
+       register_task(&sched, &pt->rn.task);
+       register_task(&sched, &pt->fn.task);
+       register_writer_node(&pt->wn, pt->fn.btrn, &sched);
+       return 1;
+fail:
+       afh_recv->close(&pt->rn);
+       btr_remove_node(&pt->rn.btrn);
+       afh_recv->free_config(pt->rn.conf);
+       return ret;
+}
+
+static int next_valid_file(struct play_task *pt)
+{
+       int i, j = pt->current_file;
+
+       FOR_EACH_PLAYLIST_FILE(i) {
+               j = (j + 1) % conf.inputs_num;
+               if (!pt->invalid[j])
+                       return j;
+       }
+       return -E_NO_VALID_FILES;
+}
+
+static int load_next_file(struct play_task *pt)
+{
+       int ret;
+
+again:
+       if (pt->rq == CRT_NONE || pt->rq == CRT_FILE_CHANGE) {
+               pt->start_chunk = 0;
+               ret = next_valid_file(pt);
+               if (ret < 0)
+                       return ret;
+               pt->next_file = ret;
+       } else if (pt->rq == CRT_REPOS)
+               pt->next_file = pt->current_file;
+       ret = load_file(pt);
+       if (ret < 0) {
+               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(struct play_task *pt)
+{
+       task_notify(&pt->wn.task, E_EOF);
+}
+
+#ifdef HAVE_READLINE
+
+/* only called from com_prev(), nec. only if we have readline */
+static int previous_valid_file(struct play_task *pt)
+{
+       int i, j = pt->current_file;
+
+       FOR_EACH_PLAYLIST_FILE(i) {
+               j--;
+               if (j < 0)
+                       j = conf.inputs_num - 1;
+               if (!pt->invalid[j])
+                       return j;
+       }
+       return -E_NO_VALID_FILES;
+}
+
+#include "interactive.h"
+
+/*
+ * 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 + conf.key_map_given)
+#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 char *get_user_key_map_arg(int key)
+{
+       return conf.key_map_arg[get_user_key_map_idx(key)];
+}
+
+static inline char *get_internal_key_map_seq(int key)
+{
+       return para_strdup(default_keyseqs[get_internal_key_map_idx(key)]);
+}
+
+static char *get_user_key_map_seq(int key)
+{
+       const char *kma = get_user_key_map_arg(key);
+       const char *p = strchr(kma + 1, ':');
+       char *result;
+       int len;
+
+       if (!p)
+               return NULL;
+       len = p - kma;
+       result = para_malloc(len + 1);
+       memcpy(result, kma, len);
+       result[len] = '\0';
+       return result;
+}
+
+static char *get_key_map_seq(int key)
+{
+       return is_internal_key(key)?
+               get_internal_key_map_seq(key) : get_user_key_map_seq(key);
+}
+
+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 = para_malloc((NUM_MAPPED_KEYS + 1) * sizeof(char *));
+       FOR_EACH_MAPPED_KEY(i) {
+               int idx = get_key_map_idx(i);
+               char *seq = get_key_map_seq(i);
+               char *cmd = get_key_map_cmd(i);
+               bool internal = is_internal_key(i);
+               PARA_DEBUG_LOG("%s key sequence #%d: %s -> %s\n",
+                       internal? "internal" : "user-defined",
+                       idx, seq, cmd);
+               result[i] = seq;
+               free(cmd);
+       }
+       result[i] = NULL;
+       return result;
+}
+
+#include "play_completion.h"
+
+
+/* defines one command of para_play */
+struct pp_command {
+       const char *name;
+       int (*handler)(struct play_task *, int, char**);
+       const char *description;
+       const char *usage;
+       const char *help;
+};
+
+#include "play_command_list.h"
+static struct pp_command pp_cmds[] = {DEFINE_PLAY_CMD_ARRAY};
+#define FOR_EACH_COMMAND(c) for (c = 0; pp_cmds[c].name; c++)
+
+#include "play_completion.h"
+static 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(stop);
+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 *result)
+{
+       result->matches = i9e_complete_commands(ci->word, pp_completers);
+}
+
+static struct i9e_completer pp_completers[] = {PLAY_COMPLETERS {.name = NULL}};
+
+static void attach_stdout(struct play_task *pt, 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(struct play_task *pt)
+{
+       btr_remove_node(&pt->btrn);
+}
+
+static int com_quit(struct play_task *pt, int argc, __a_unused char **argv)
+{
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+       pt->rq = CRT_TERM_RQ;
+       return 0;
+}
+
+static int com_help(struct play_task *pt, int argc, char **argv)
+{
+       int i;
+       char *buf;
+       size_t sz;
+
+       if (argc > 2)
+               return -E_PLAY_SYNTAX;
+       if (argc < 2) {
+               if (pt->background)
+                       FOR_EACH_COMMAND(i) {
+                               sz = xasprintf(&buf, "%s\t%s\n", pp_cmds[i].name,
+                                       pp_cmds[i].description);
+                               btr_add_output(buf, sz, pt->btrn);
+                       }
+               else {
+                       FOR_EACH_MAPPED_KEY(i) {
+                               bool internal = is_internal_key(i);
+                               int idx = get_key_map_idx(i);
+                               char *seq = get_key_map_seq(i);
+                               char *cmd = get_key_map_cmd(i);
+                               sz = xasprintf(&buf,
+                                       "%s key #%d: %s -> %s\n",
+                                       internal? "internal" : "user-defined",
+                                       idx, seq, cmd);
+                               btr_add_output(buf, sz, pt->btrn);
+                               free(seq);
+                               free(cmd);
+                       }
+               }
+               return 0;
+       }
+       FOR_EACH_COMMAND(i) {
+               if (strcmp(pp_cmds[i].name, argv[1]))
+                       continue;
+               sz = xasprintf(&buf,
+                       "NAME\n\t%s -- %s\n"
+                       "SYNOPSIS\n\t%s\n"
+                       "DESCRIPTION\n%s\n",
+                       argv[1],
+                       pp_cmds[i].description,
+                       pp_cmds[i].usage,
+                       pp_cmds[i].help
+               );
+               btr_add_output(buf, sz, pt->btrn);
+               return 0;
+       }
+       return -E_BAD_PLAY_CMD;
+}
+
+static int com_info(struct play_task *pt, int argc, __a_unused char **argv)
+{
+       char *buf;
+       size_t sz;
+       static char dflt[] = "[no information available]";
+
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+       sz = xasprintf(&buf, "playlist_pos: %u\npath: %s\n",
+               pt->current_file, conf.inputs[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;
+}
+
+static void list_file(struct play_task *pt, int num)
+{
+       char *buf;
+       size_t sz;
+
+       sz = xasprintf(&buf, "%s %4u %s\n", num == pt->current_file?
+               "*" : " ", num, conf.inputs[num]);
+       btr_add_output(buf, sz, pt->btrn);
+}
+
+static int com_tasks(struct play_task *pt, int argc, __a_unused char **argv)
+{
+       static char state;
+       char *buf;
+       size_t sz;
+
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+
+       buf = get_task_list(&sched);
+       btr_add_output(buf, strlen(buf), pt->btrn);
+       state = get_playback_state(pt);
+       sz = xasprintf(&buf, "state: %c\n", state);
+       btr_add_output(buf, sz, pt->btrn);
+       return 0;
+}
+
+static int com_ls(struct play_task *pt, int argc, char **argv)
+{
+       int i, j, ret;
+
+       if (argc == 1) {
+               FOR_EACH_PLAYLIST_FILE(i)
+                       list_file(pt, i);
+               return 0;
+       }
+       for (j = 1; j < argc; j++) {
+               FOR_EACH_PLAYLIST_FILE(i) {
+                       ret = fnmatch(argv[j], conf.inputs[i], 0);
+                       if (ret == 0) /* match */
+                               list_file(pt, i);
+               }
+       }
+       return 0;
+}
+
+static int com_play(struct play_task *pt, int argc, char **argv)
+{
+       int32_t x;
+       int ret;
+       char state;
+
+       if (argc > 2)
+               return -E_PLAY_SYNTAX;
+       state = get_playback_state(pt);
+       if (argc == 1) {
+               if (state == 'P')
+                       return 0;
+               pt->next_file = pt->current_file;
+               pt->rq = CRT_REPOS;
+               pt->playing = true;
+               return 0;
+       }
+       ret = para_atoi32(argv[1], &x);
+       if (ret < 0)
+               return ret;
+       if (x < 0 || x >= conf.inputs_num)
+               return -ERRNO_TO_PARA_ERROR(EINVAL);
+       kill_stream(pt);
+       pt->next_file = x;
+       pt->rq = CRT_FILE_CHANGE;
+       return 0;
+}
+
+static int com_pause(struct play_task *pt, int argc, __a_unused char **argv)
+{
+       char state;
+       long unsigned seconds, ss;
+
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+       state = get_playback_state(pt);
+       pt->playing = false;
+       if (state != 'P')
+               return 0;
+       seconds = get_play_time(pt);
+       pt->playing = false;
+       ss = 0;
+       if (pt->seconds > 0)
+               ss = seconds * pt->num_chunks / pt->seconds + 1;
+       ss = PARA_MAX(ss, 0UL);
+       ss = PARA_MIN(ss, pt->num_chunks);
+       pt->start_chunk = ss;
+       kill_stream(pt);
+       return 0;
+}
+
+static int com_prev(struct play_task *pt, int argc, __a_unused char **argv)
+
+{
+       int ret;
+
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+       ret = previous_valid_file(pt);
+       if (ret < 0)
+               return ret;
+       kill_stream(pt);
+       pt->next_file = ret;
+       pt->rq = CRT_FILE_CHANGE;
+       return 0;
+}
+
+static int com_next(struct play_task *pt, int argc, __a_unused char **argv)
+{
+       int ret;
+
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+       ret = next_valid_file(pt);
+       if (ret < 0)
+               return ret;
+       kill_stream(pt);
+       pt->next_file = ret;
+       pt->rq = CRT_FILE_CHANGE;
+       return 0;
+}
+
+static int com_fg(struct play_task *pt, int argc, __a_unused char **argv)
+{
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+       pt->background = false;
+       return 0;
+}
+
+static int com_bg(struct play_task *pt, int argc, __a_unused char **argv)
+{
+       if (argc != 1)
+               return -E_PLAY_SYNTAX;
+       pt->background = true;
+       return 0;
+}
+
+static int com_jmp(struct play_task *pt, int argc, char **argv)
+{
+       int32_t percent;
+       int ret;
+
+       if (argc != 2)
+               return -E_PLAY_SYNTAX;
+       ret = para_atoi32(argv[1], &percent);
+       if (ret < 0)
+               return ret;
+       if (percent < 0 || percent > 100)
+               return -ERRNO_TO_PARA_ERROR(EINVAL);
+       if (pt->playing && !pt->fn.btrn)
+               return 0;
+       pt->start_chunk = percent * pt->num_chunks / 100;
+       if (!pt->playing)
+               return 0;
+       pt->rq = CRT_REPOS;
+       kill_stream(pt);
+       return 0;
+}
+
+static int com_ff(struct play_task *pt, int argc, char **argv)
+{
+       int32_t seconds;
+       int ret;
+
+       if (argc != 2)
+               return -E_PLAY_SYNTAX;
+       ret = para_atoi32(argv[1], &seconds);
+       if (ret < 0)
+               return ret;
+       if (pt->playing && !pt->fn.btrn)
+               return 0;
+       seconds += get_play_time(pt);
+       seconds = PARA_MIN(seconds, (typeof(seconds))pt->seconds - 4);
+       seconds = PARA_MAX(seconds, 0);
+       pt->start_chunk = 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(pt);
+       return 0;
+}
+
+static int run_command(char *line, struct play_task *pt)
+{
+       int i, ret, argc;
+       char **argv = NULL;
+
+       attach_stdout(pt, __FUNCTION__);
+       ret = create_argv(line, " ", &argv);
+       if (ret < 0) {
+               PARA_ERROR_LOG("parse error: %s\n", para_strerror(-ret));
+               return 0;
+       }
+       if (ret == 0)
+               goto out;
+       argc = ret;
+       FOR_EACH_COMMAND(i) {
+               if (strcmp(pp_cmds[i].name, argv[0]))
+                       continue;
+               ret = pp_cmds[i].handler(pt, argc, argv);
+               if (ret < 0)
+                       PARA_WARNING_LOG("%s: %s\n", pt->background?
+                               "" : argv[0], para_strerror(-ret));
+               ret = 1;
+               goto out;
+       }
+       PARA_WARNING_LOG("invalid command: %s\n", argv[0]);
+       ret = 0;
+out:
+       free_argv(argv);
+       return ret;
+}
+
+static int play_i9e_line_handler(char *line)
+{
+       struct play_task *pt = &play_task;
+       int ret;
+
+       if (line == NULL || !*line)
+               return 0;
+       ret = run_command(line, pt);
+       if (ret < 0)
+               return ret;
+       return 0;
+}
+
+static int play_i9e_key_handler(int key)
+{
+       struct play_task *pt = &play_task;
+       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, pt);
+       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)
+{
+       play_task.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(__a_unused struct play_task *pt)
+{
+       int ret;
+       char *history_file;
+       struct sigaction act;
+
+       PARA_NOTICE_LOG("\n%s\n", VERSION_TEXT("play"));
+       if (conf.history_file_given)
+               history_file = para_strdup(conf.history_file_arg);
+       else {
+               char *home = para_homedir();
+               history_file = make_message("%s/.paraslash/play.history",
+                       home);
+               free(home);
+       }
+       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.select_function = i9e_select;
+
+       ici.bound_keyseqs = get_mapped_keyseqs();
+       pt->btrn = ici.producer = btr_new_node(&(struct btr_node_description)
+               EMBRACE(.name = __FUNCTION__));
+       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(struct play_task *pt, 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;
+       }
+       ie9_print_status_bar(str, len);
+}
+
+/*
+ * If we are about to die we must call i9e_close() to reset the terminal.
+ * However, i9e_close() must be called in *this* context, i.e. from
+ * play_task.post_select() rather than from i9e_post_select(), because
+ * otherwise i9e would access freed memory upon return. So the play task must
+ * stay alive until the i9e task terminates.
+ *
+ * We achieve this by sending a fake SIGTERM signal via i9e_signal_dispatch()
+ * and reschedule. In the next iteration, i9e->post_select returns an error and
+ * terminates. Subsequent calls to i9e_get_error() then return negative and we
+ * are allowed to call i9e_close() and terminate as well.
+ */
+static int session_post_select(__a_unused struct sched *s, struct task *t)
+{
+       struct play_task *pt = container_of(t, struct play_task, task);
+       int ret;
+
+       if (pt->background)
+               detach_stdout(pt);
+       else
+               attach_stdout(pt, __FUNCTION__);
+       ret = i9e_get_error();
+       if (ret < 0) {
+               kill_stream(pt);
+               i9e_close();
+               para_log = stderr_log;
+               free(ici.history_file);
+               return ret;
+       }
+       if (get_playback_state(pt) == 'X')
+               i9e_signal_dispatch(SIGTERM);
+       return 0;
+}
+
+#else /* HAVE_READLINE */
+
+static int session_post_select(struct sched *s, struct task *t)
+{
+       struct play_task *pt = container_of(t, struct play_task, task);
+       char c;
+
+       if (!FD_ISSET(STDIN_FILENO, &s->rfds))
+               return 0;
+       if (read(STDIN_FILENO, &c, 1))
+               do_nothing;
+       kill_stream(pt);
+       return 1;
+}
+
+static void session_open(__a_unused struct play_task *pt)
+{
+}
+
+static void session_update_time_string(__a_unused struct play_task *pt,
+               char *str, __a_unused unsigned len)
+{
+       printf("\r%s     ", str);
+       fflush(stdout);
+}
+#endif /* HAVE_READLINE */
+
+static void play_pre_select(struct sched *s, struct task *t)
+{
+       struct play_task *pt = container_of(t, struct play_task, task);
+       char state;
+
+       para_fd_set(STDIN_FILENO, &s->rfds, &s->max_fileno);
+       state = get_playback_state(pt);
+       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(struct play_task *pt, char **result)
+{
+       int seconds, length;
+       char state = get_playback_state(pt);
+
+       /* 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(pt);
+       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,
+               conf.inputs[pt->current_file]
+       );
+}
+
+static void play_post_select(struct sched *s, struct task *t)
+{
+       struct play_task *pt = container_of(t, struct play_task, task);
+       int ret;
+
+       ret = eof_cleanup(pt);
+       if (ret < 0) {
+               pt->rq = CRT_TERM_RQ;
+               return;
+       }
+       ret = session_post_select(s, t);
+       if (ret < 0)
+               goto out;
+       if (!pt->wn.btrn && !pt->fn.btrn) {
+               char state = get_playback_state(pt);
+               if (state == 'P' || state == 'R' || state == 'F') {
+                       PARA_NOTICE_LOG("state: %c\n", state);
+                       ret = load_next_file(pt);
+                       if (ret < 0) {
+                               PARA_ERROR_LOG("%s\n", para_strerror(-ret));
+                               pt->rq = CRT_TERM_RQ;
+                               ret = 1;
+                               goto out;
+                       }
+                       pt->next_update = *now;
+               }
+       }
+       if (tv_diff(now, &pt->next_update, NULL) >= 0) {
+               char *str;
+               unsigned len = get_time_string(pt, &str);
+               struct timeval delay = {.tv_sec = 0, .tv_usec = 100 * 1000};
+               if (str && len > 0)
+                       session_update_time_string(pt, str, len);
+               free(str);
+               tv_add(now, &delay, &pt->next_update);
+       }
+       ret = 1;
+out:
+       t->error = ret;
+}
+
+/**
+ * The main function of para_play.
+ *
+ * \param argc Standard.
+ * \param argv Standard.
+ *
+ * \return \p EXIT_FAILURE or \p EXIT_SUCCESS.
+ */
 int main(int argc, char *argv[])
 {
+       int ret;
+       struct play_task *pt = &play_task;
+
        /* needed this early to make help work */
        recv_init();
        filter_init();
        writer_init();
 
+       gettimeofday(now, NULL);
+       sched.default_timeout.tv_sec = 5;
+
        parse_config_or_die(argc, argv);
-       return 0;
+       if (conf.inputs_num == 0)
+               print_help_and_die();
+       check_afh_receiver_or_die();
+
+       session_open(pt);
+       if (conf.randomize_given)
+               shuffle(conf.inputs, conf.inputs_num);
+       pt->invalid = para_calloc(sizeof(*pt->invalid) * conf.inputs_num);
+       pt->rq = CRT_FILE_CHANGE;
+       pt->current_file = conf.inputs_num - 1;
+       pt->playing = true;
+       pt->task.pre_select = play_pre_select;
+       pt->task.post_select = play_post_select;
+       sprintf(pt->task.status, "play task");
+       register_task(&sched, &pt->task);
+       ret = schedule(&sched);
+       if (ret < 0)
+               PARA_ERROR_LOG("%s\n", para_strerror(-ret));
+       return ret < 0? EXIT_FAILURE : EXIT_SUCCESS;
 }
index c0e8d7f..459ad8c 100644 (file)
--- a/play.cmd
+++ b/play.cmd
@@ -2,6 +2,75 @@ BN: play
 SF: play.c
 SN: list of commands
 ---
+N: help
+D: Display command list or help for given command.
+U: help [command]
+H: This command acts differently depending on whether it is executed in command
+H: mode or in insert mode.  In command mode, the list of keybindings is printed.
+H: In insert mode, if no command is given, the list of commands is shown.
+H: Otherwise, the help for the given command is printed.
+---
+N: next
+D: Load next file.
+U: next
+H: Closes the current file and loads the next file of the playlist.
+---
+N: prev
+D: Load previous file.
+U: prev
+H: Closes the current file and loads the previous file of the playlist.
+---
+N: fg
+D: Enter command mode.
+U: fg
+H: In this mode, file name and play time are displayed.  Hit CTRL+C to switch to
+H: input mode.
+---
+N: bg
+D: Enter input mode.
+U: bg
+H: Only useful if called in command mode via a key binding. The default key
+H: bindings map this command to the colon key, so pressing : in command mode
+H: activates insert mode.
+---
+N: jmp
+D: Jump to position in current file.
+U: jmp <percent>
+H: The <percent> argument should be an integer between 0 and 100.
+---
+N: ff
+D: Jump forwards or backwards.
+U: ff <seconds>
+H: Negative values mean to jmp backwards the given amount of seconds.
+---
+N: ls
+D: List playlist.
+U: ls
+H: This prints all paths of the playlist. The currently active file is
+H: marked with an asterisk.
+---
+N: info
+D: Print information about the current file.
+U: info
+H: This is the audio file selector info.
+---
+N: play
+D: Start or resume playing.
+U: play [<num>]
+H: Without <num>, starts playing at the current position. Otherwise, the
+H: corresponding file is loaded and playback is started.
+---
+N: pause
+D: Stop playing.
+U: pause
+H: When paused, it is still possible to jump around in the file via the jmp and ff
+H: comands.
+---
+N: tasks
+D: Print list of active tasks.
+U: tasks
+H: Mainly useful for debugging.
+---
 N: quit
 D: Exit para_play.
 U: quit