X-Git-Url: http://git.tuebingen.mpg.de/?p=paraslash.git;a=blobdiff_plain;f=play.c;h=fb72bae43e3a921f8acc6d85082e29d0ed19f20f;hp=c9cc56abb5374831979cb510bcc89ed337425257;hb=24758c5f;hpb=002e302f18b73e57e8a5ff5e9dc97c68bb85216d diff --git a/play.c b/play.c index c9cc56ab..fb72bae4 100644 --- a/play.c +++ b/play.c @@ -1,368 +1,1280 @@ /* - * Copyright (C) 2005-2006 Andre Noll + * Copyright (C) 2012-2013 Andre Noll * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA. + * Licensed under the GPL v2. For licencing details see COPYING. */ -/* - * Based in parts on aplay.c from the alsa-utils-1.0.8 package, - * Copyright (c) by Jaroslav Kysela , which is - * based on the vplay program by Michael Beck. - */ +/** \file play.c Paraslash's standalone player. */ + +#include +#include +#include -#define WAV_HEADER_LEN 44 -#include /* gettimeofday */ #include "para.h" -#include "fd.h" +#include "list.h" #include "play.cmdline.h" -#include -#include "string.h" +#include "filter.cmdline.h" #include "error.h" +#include "ggo.h" +#include "buffer_tree.h" +#include "version.h" +#include "string.h" +#include "sched.h" +#include "filter.h" +#include "afh.h" +#include "recv.h" +#include "write.h" +#include "write_common.h" +#include "fd.h" -#define FORMAT SND_PCM_FORMAT_S16_LE +/** + * 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. + */ -struct private_alsa_data { - snd_pcm_t *handle; - size_t bytes_per_frame; +/** + * 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 writer_node { - struct writer *writer; - void *private_data; +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; }; -struct writer { - int (*open)(struct writer_node **); - int (*write)(char *data, size_t nbytes, struct writer_node *); - void (*close)(struct writer_node *); - void (*shutdown)(struct writer_node *); +/** Initialize the array of errors for para_play. */ +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; + +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_args_info_usage); + if (d) + printf_or_die("%s\n", PP_DESC); + for (; *p; p++) + printf_or_die("%s\n", *p); + exit(0); +} + +static void parse_config_or_die(int argc, char *argv[]) +{ + int i, ret; + char *config_file; + struct play_cmdline_parser_params params = { + .override = 0, + .initialize = 1, + .check_required = 0, + .check_ambiguity = 0, + .print_errors = 1 + }; + + if (play_cmdline_parser_ext(argc, argv, &conf, ¶ms)) + exit(EXIT_FAILURE); + HANDLE_VERSION_FLAG("play", conf); + if (conf.help_given || conf.detailed_help_given) + print_help_and_die(); + loglevel = get_loglevel_by_name(conf.loglevel_arg); + if (conf.config_file_given) + config_file = para_strdup(conf.config_file_arg); + else { + char *home = para_homedir(); + config_file = make_message("%s/.paraslash/play.conf", home); + free(home); + } + ret = file_exists(config_file); + if (conf.config_file_given && !ret) { + PARA_EMERG_LOG("can not read config file %s\n", config_file); + goto err; + } + if (ret) { + params.initialize = 0; + params.check_required = 1; + play_cmdline_parser_config_file(config_file, &conf, ¶ms); + } + 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: + free(config_file); + 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); }; -#define NUM_WRITERS 1 -static struct writer writers[NUM_WRITERS]; -#define FOR_EACH_WRITER(i) for (i = 0; i < NUM_WRITERS, i++) -static struct writer_node **writer_nodes; +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 unsigned char *audiobuf; -static struct timeval *start_time; -static struct gengetopt_args_info conf; +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)); +} -INIT_PLAY_ERRLISTS; +/* 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; +} -void para_log(__a_unused int ll, const char* fmt,...) +static int eof_cleanup(struct play_task *pt) { - va_list argp; + 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)); - va_start(argp, fmt); - vfprintf(stderr, fmt, argp); - va_end(argp); + 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; } -/** - * read WAV_HEADER_LEN bytes from stdin to audio buffer - * - * \return -E_READ_HDR on errors and on eof before WAV_HEADER_LEN could be - * read. A positive return value indicates success. - */ -static int read_wav_header(void) +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) { - ssize_t ret, count = 0; + int ret; + char *tmp, *path = conf.inputs[pt->next_file], *afh_recv_conf[] = + {"play", "-f", path, "-b", "0", NULL}; - while (count < WAV_HEADER_LEN) { - ret = read(STDIN_FILENO, audiobuf + count, WAV_HEADER_LEN - count); - if (ret <= 0) - return -E_READ_HDR; - count += ret; + 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: + wipe_receiver_node(pt); + 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" + /* - * open and prepare the PCM handle for writing + * 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. * - * Install PCM software and hardware configuration. Exit on errors. + * 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. */ -static int alsa_open(void **private_data) -{ - snd_pcm_hw_params_t *hwparams; - snd_pcm_sw_params_t *swparams; - snd_pcm_uframes_t buffer_size, xfer_align, start_threshold, - stop_threshold; - unsigned buffer_time = 0; - int err; - snd_pcm_info_t *info; - snd_output_t *log; - snd_pcm_uframes_t period_size; - struct private_alsa_data *pad = para_malloc(sizeof(struct private_alsa_data)); - *private_data = pad; - - snd_pcm_info_alloca(&info); - if (snd_output_stdio_attach(&log, stderr, 0) < 0) - return -E_ALSA_LOG; - err = snd_pcm_open(&pad->handle, conf.device_arg, - SND_PCM_STREAM_PLAYBACK, 0); - if (err < 0) - return -E_PCM_OPEN; - if ((err = snd_pcm_info(pad->handle, info)) < 0) - return -E_SND_PCM_INFO; - - snd_pcm_hw_params_alloca(&hwparams); - snd_pcm_sw_params_alloca(&swparams); - if (snd_pcm_hw_params_any(pad->handle, hwparams) < 0) - return -E_BROKEN_CONF; - if (snd_pcm_hw_params_set_access(pad->handle, hwparams, - SND_PCM_ACCESS_RW_INTERLEAVED) < 0) - return -E_ACCESS_TYPE; - if (snd_pcm_hw_params_set_format(pad->handle, hwparams, FORMAT) < 0) - return -E_SAMPLE_FORMAT; - if (snd_pcm_hw_params_set_channels(pad->handle, hwparams, - conf.channels_arg) < 0) - return -E_CHANNEL_COUNT; - if (snd_pcm_hw_params_set_rate_near(pad->handle, hwparams, - (unsigned int*) &conf.sample_rate_arg, 0) < 0) - return -E_SET_RATE; - err = snd_pcm_hw_params_get_buffer_time_max(hwparams, &buffer_time, 0); - if (err < 0 || !buffer_time) - return -E_GET_BUFFER_TIME; - PARA_DEBUG_LOG("buffer time: %d\n", buffer_time); - if (snd_pcm_hw_params_set_buffer_time_near(pad->handle, hwparams, - &buffer_time, 0) < 0) - return -E_SET_BUFFER_TIME; - if (snd_pcm_hw_params(pad->handle, hwparams) < 0) - return -E_HW_PARAMS; - snd_pcm_hw_params_get_period_size(hwparams, &period_size, 0); - snd_pcm_hw_params_get_buffer_size(hwparams, &buffer_size); - PARA_DEBUG_LOG("buffer size: %lu, period_size: %lu\n", buffer_size, period_size); - if (period_size == buffer_size) - return -E_BAD_PERIOD; - snd_pcm_sw_params_current(pad->handle, swparams); - err = snd_pcm_sw_params_get_xfer_align(swparams, &xfer_align); - if (err < 0 || !xfer_align) - return -E_GET_XFER; - snd_pcm_sw_params_set_avail_min(pad->handle, swparams, period_size); - /* round to closest transfer boundary */ - start_threshold = (buffer_size / xfer_align) * xfer_align; - if (start_threshold < 1) - start_threshold = 1; - if (snd_pcm_sw_params_set_start_threshold(pad->handle, swparams, - start_threshold) < 0) - return -E_START_THRESHOLD; - stop_threshold = buffer_size; - if (snd_pcm_sw_params_set_stop_threshold(pad->handle, swparams, - stop_threshold) < 0) - return -E_STOP_THRESHOLD; - if (snd_pcm_sw_params_set_xfer_align(pad->handle, swparams, - xfer_align) < 0) - return -E_SET_XFER; - if (snd_pcm_sw_params(pad->handle, swparams) < 0) - return -E_SW_PARAMS; - pad->bytes_per_frame = snd_pcm_format_physical_width(FORMAT) - * conf.channels_arg / 8; - return period_size * pad->bytes_per_frame; + +#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; } -/** - * push out pcm frames - * \param data pointer do data to be written - * \param nbytes number of bytes (not frames) - * - * \return Number of bytes written, -E_ALSA_WRITE on errors. +/* 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 int alsa_write(char *data, size_t nbytes, void *private_data) -{ - struct private_alsa_data *pad = private_data; - size_t frames = nbytes / pad->bytes_per_frame; - unsigned char *d = data; - snd_pcm_sframes_t r, result = 0; - - while (frames > 0) { - /* write interleaved frames */ - r = snd_pcm_writei(pad->handle, d, frames); - if (r < 0) - PARA_ERROR_LOG("write error: %s\n", snd_strerror(r)); - if (r == -EAGAIN || (r >= 0 && r < frames)) - snd_pcm_wait(pad->handle, 1); - else if (r == -EPIPE) - snd_pcm_prepare(pad->handle); - else if (r < 0) - return -E_ALSA_WRITE; - if (r > 0) { - result += r; - frames -= r; - d += r * pad->bytes_per_frame; - } +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); } - return result * pad->bytes_per_frame; + result[i] = NULL; + return result; } -static void alsa_close(void *private_data) +#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) { - struct private_alsa_data *pad = private_data; - snd_pcm_drain(pad->handle); - snd_pcm_close(pad->handle); - snd_config_update_free_global(); - free(pad); + result->matches = i9e_complete_commands(ci->word, pp_completers); } -void alsa_writer_init(struct writer *w) +static struct i9e_completer pp_completers[] = {PLAY_COMPLETERS {.name = NULL}}; + +static void attach_stdout(struct play_task *pt, const char *name) { - w->open = alsa_open; - w->write = alsa_write; - w->close = alsa_close; - w->shutdown = NULL; /* nothing to do */ + 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); +} -/** - * check if current time is later than start_time - * \param diff pointer to write remaining time to - * - * If start_time was not given, or current time is later than given - * start_time, return 0. Otherwise, return 1 and write the time - * difference between current time and start_time to diff. diff may be - * NULL. - * - */ -static int start_time_in_future(struct timeval *diff) +static int com_quit(struct play_task *pt, int argc, __a_unused char **argv) { - struct timeval now; + 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 (!conf.start_time_given) + 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; - gettimeofday(&now, NULL); - return tv_diff(start_time, &now, diff) > 0? 1 : 0; + } + return -E_BAD_PLAY_CMD; } -/** - * sleep until time given at command line - * - * This is called if the initial buffer is filled. It returns - * immediately if no start_time was given at the command line - * or if the given start time is in the past. - * - */ -static void do_initial_delay(struct timeval *delay) +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) { - do - para_select(1, NULL, NULL, delay); - while (start_time_in_future(delay)); + 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); } -/** - * play raw pcm data - * \param loaded number of bytes already loaded - * - * If start_time was given, prebuffer data until buffer is full or - * start_time is reached. In any case, do not start playing before - * start_time. - * - * \return positive on success, negative on errors. - */ -static int play_pcm(size_t loaded) +static int com_tasks(struct play_task *pt, int argc, __a_unused char **argv) { - size_t bufsize, written = 0, prebuf_size; - unsigned char *p; - struct timeval delay; - void *private_data; - int chunk_bytes, ret = alsa_open(&private_data); + static char state; + char *buf; + size_t sz; - if (ret < 0) - goto out; - chunk_bytes = ret; - bufsize = (conf.bufsize_arg * 1024 / chunk_bytes) * chunk_bytes; - audiobuf = para_realloc(audiobuf, bufsize); - prebuf_size = conf.prebuffer_arg * bufsize / 100; -again: - if (!written) { - if (loaded < prebuf_size) - goto read; - if (start_time && start_time_in_future(&delay)) { - do_initial_delay(&delay); - start_time = NULL; + 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); } } - p = audiobuf; - while (loaded >= chunk_bytes) { - ret = alsa_write(p, chunk_bytes, private_data); - if (ret < 0) - goto out; - p += ret; - written += ret; - loaded -= ret; + 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; } - if (loaded && p != audiobuf) - memmove(audiobuf, p, loaded); -read: - ret = read(STDIN_FILENO, audiobuf + loaded, bufsize - loaded); + 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) { - ret = -E_READ_STDIN; - goto out; + PARA_ERROR_LOG("parse error: %s\n", para_strerror(-ret)); + return 0; } - if (ret) { - loaded += ret; - goto again; + 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; } - ret = 1; + PARA_WARNING_LOG("invalid command: %s\n", argv[0]); + ret = 0; out: - alsa_close(private_data); + free_argv(argv); return ret; } -/** - * test if audio buffer contains a valid wave header +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. * - * \return If not, return 0, otherwise, store number of channels and sample rate - * in struct conf and return WAV_HEADER_LEN. + * 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 size_t check_wave(void) +static int session_post_select(__a_unused struct sched *s, struct task *t) { - unsigned char *a = audiobuf; - if (a[0] != 'R' || a[1] != 'I' || a[2] != 'F' || a[3] != 'F') - return WAV_HEADER_LEN; - conf.channels_arg = (unsigned) a[22]; - conf.sample_rate_arg = a[24] + (a[25] << 8) + (a[26] << 16) + (a[27] << 24); + 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; } -int main(int argc, char *argv[]) +#else /* HAVE_READLINE */ + +static int session_post_select(struct sched *s, struct task *t) { - struct timeval tv; - int ret; + 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) +{ +} - cmdline_parser(argc, argv, &conf); - if (conf.start_time_given) { - ret = -E_PLAY_SYNTAX; - if (sscanf(conf.start_time_arg, "%lu:%lu", - &tv.tv_sec, &tv.tv_usec) != 2) - goto out; - start_time = &tv; +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; } - /* call init for each supported writer */ - alsa_writer_init(&writers[0]); - /* one for each given writer */ - writer_nodes = para_calloc(2 * sizeof(struct writer_node)); + 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] + ); +} - audiobuf = para_malloc(WAV_HEADER_LEN); - ret = read_wav_header(); +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; - ret = play_pcm(check_wave()); + 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: - free(audiobuf); + 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(); + + clock_get_realtime(now); + sched.default_timeout.tv_sec = 5; + + parse_config_or_die(argc, argv); + 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; + PARA_ERROR_LOG("%s\n", para_strerror(-ret)); + return ret < 0? EXIT_FAILURE : EXIT_SUCCESS; }