/* SPDX-License-Identifier: GPL-2.0 */ /** \file write.c Paraslash's standalone wav/raw player. * * This file contains the main function of para_write(1), which is conceptually * similar to para_recv(1) in that it employs the scheduler and sets up a * simple buffer tree. The tree combines the stdin buffer tree node with * one of the paraslash writers. * * \cond doxygen_ignore */ #include #include #include "write_cmd.lsg.h" #include "write.lsg.h" #include "para.h" #include "string.h" #include "list.h" #include "sched.h" #include "stdin.h" #include "buffer_tree.h" #include "write.h" #include "fd.h" #include "error.h" #include "portable_io.h" DEFINE_PARA_ERRLIST; #define CMD_PTR (lls_cmd(0, write_suite)) #define OPT_RESULT(_name, _lpr) \ (lls_opt_result(LSG_WRITE_PARA_WRITE_OPT_ ## _name, _lpr)) #define OPT_GIVEN(_name, _lpr) (lls_opt_given(OPT_RESULT(_name, _lpr))) #define OPT_UINT32_VAL(_name, _lpr) (lls_uint32_val(0, OPT_RESULT(_name, _lpr))) static int loglevel; INIT_STDERR_LOGGING(loglevel) /* Length of a standard wav header. */ #define WAV_HEADER_LEN 44 /* The possible states of a check_wav instance. */ enum check_wav_state { /* Initial state, less than WAV_HEADER_LEN bytes available. */ CWS_NEED_HEADER, /* Wav hader was detected. */ CWS_HAVE_HEADER, /* First part of the stream did not look like a wav header. */ CWS_NO_HEADER, }; struct check_wav_context { enum check_wav_state state; struct btr_node *btrn; size_t min_iqs; struct lls_parse_result *lpr; /* Extracted from the wav header.*/ unsigned channels; unsigned sample_format; unsigned sample_rate; }; /* * If no data is available and the buffer tree node is not in error state, * the function does nothing. Otherwise it requests a minimal timeout. */ void check_wav_pre_monitor(struct sched *s, struct check_wav_context *cwc) { int ret = btr_node_status(cwc->btrn, cwc->min_iqs, BTR_NT_INTERNAL); if (ret != 0) sched_min_delay(s); } static int check_wav_exec(const struct btr_node *btrn, const char *cmd, char **result) { struct check_wav_context *cwc = btr_context(btrn); int val, header_val, given, arg; header_val = cwc->channels; arg = OPT_UINT32_VAL(CHANNELS, cwc->lpr); given = OPT_GIVEN(CHANNELS, cwc->lpr); if (!strcmp(cmd, "channels")) goto out; header_val = cwc->sample_rate; arg = OPT_UINT32_VAL(SAMPLE_RATE, cwc->lpr); given = OPT_GIVEN(SAMPLE_RATE, cwc->lpr); if (!strcmp(cmd, "sample_rate")) goto out; header_val = cwc->sample_format; arg = OPT_UINT32_VAL(SAMPLE_FORMAT, cwc->lpr); given = OPT_GIVEN(SAMPLE_FORMAT, cwc->lpr); if (!strcmp(cmd, "sample_format")) goto out; return -ERRNO_TO_PARA_ERROR(ENOTSUP); out: if (given) val = arg; else { switch (cwc->state) { case CWS_HAVE_HEADER: val = header_val; break; case CWS_NO_HEADER: /* * No wav header available and no value specified at * the command line. Maybe one of our parent nodes * knows. */ if (btr_exec_up(btr_parent(cwc->btrn), cmd, result) >= 0) return 1; /* Use default value */ val = arg; break; default: return -E_BTR_NAVAIL; } } *result = make_message("%d", val); return 1; } /* * Filter out the wav header, pushdown everything else. * * This function looks at the first WAV_HEADER_SIZE bytes of the input queue. * If they look like a wav header, the function extracts the information * of interest and swallows this part of the stream. Otherwise it is pushed * down to all children. In either case the rest of the input is pushed down * as well. * * Once the first part has been processed this way, the state of the instance * changes from CWS_NEED_HEADER to CWS_HAVE_HEADER or CWS_NO_HEADER. */ static int check_wav_post_monitor(struct check_wav_context *cwc) { struct btr_node *btrn = cwc->btrn; unsigned char *a; size_t sz; int ret; uint16_t format_code, bps; /* bits per sample */ const char *sample_formats[] = {SAMPLE_FORMATS}; if (!btrn) return 0; ret = btr_node_status(btrn, cwc->min_iqs, BTR_NT_INTERNAL); if (ret <= 0) goto out; if (cwc->state != CWS_NEED_HEADER) goto pushdown; btr_merge(btrn, cwc->min_iqs); sz = btr_next_buffer(btrn, (char **)&a); if (sz < cwc->min_iqs) /* file size less than WAV_HEADER_SIZE */ goto pushdown; cwc->min_iqs = 0; /* * The default byte ordering assumed for WAVE data files is * little-endian. Files written using the big-endian byte ordering * scheme have the identifier RIFX instead of RIFF. */ if (a[0] != 'R' || a[1] != 'I' || a[2] != 'F' || (a[3] != 'F' && a[3] != 'X')) { PARA_NOTICE_LOG("wav header not found\n"); cwc->state = CWS_NO_HEADER; goto out; } PARA_INFO_LOG("found wav header\n"); cwc->state = CWS_HAVE_HEADER; /* Only set those values which have not already been set. */ cwc->channels = a[22]; format_code = read_u16(a + 20); if (format_code != 1 && format_code != 3) { cwc->state = CWS_NO_HEADER; goto out; } cwc->sample_rate = read_u32(a + 24); bps = read_u16(a + 34); if (bps != 8 && bps != 16 && bps != 32) { PARA_WARNING_LOG("%u bps not supported, assuming 16\n", bps); bps = 16; } if ((bps < 32 && format_code != 1) || (bps == 32 && format_code != 3)) { PARA_WARNING_LOG("invalid bps/format_code (%u/%u)\n", bps, format_code); cwc->state = CWS_NO_HEADER; goto out; } /* * 8-bit samples are stored as unsigned bytes, ranging from 0 * to 255. 16-bit samples are stored as 2's-complement signed * integers, ranging from -32768 to 32767. */ if (bps == 8) cwc->sample_format = SF_U8; else if (bps == 16) cwc->sample_format = (a[3] == 'F')? SF_S16_LE : SF_S16_BE; else /* 32 bit */ cwc->sample_format = (a[3] == 'F')? SF_FLOAT_LE : SF_FLOAT_BE; PARA_NOTICE_LOG("%uHz, %s, %s\n", cwc->sample_rate, cwc->channels == 1? "mono" : "stereo", sample_formats[cwc->sample_format]); btr_consume(btrn, WAV_HEADER_LEN); pushdown: btr_pushdown(btrn); out: if (ret < 0) btr_remove_node(&cwc->btrn); return ret; } /* * Allocate and set up a new check_wav context. * * This function returns the handle of the new context. It never returns NULL. */ static struct check_wav_context *check_wav_init(struct btr_node *parent, struct lls_parse_result *lpr) { struct check_wav_context *cwc = zalloc(sizeof(*cwc)); cwc->state = CWS_NEED_HEADER; cwc->min_iqs = WAV_HEADER_LEN; cwc->lpr = lpr; cwc->btrn = btr_new_node(&(struct btr_node_description){ .name = "check_wav", .parent = parent, .handler = check_wav_exec, .context = cwc }); return cwc; } /* * Dellocate all ressources of a check_wav instance. This function may only * be called after check_wav_post_monitor() has returned negative. */ static void check_wav_shutdown(struct check_wav_context *cwc) { free(cwc); } static void handle_help_flag(struct lls_parse_result *lpr) { char *help; if (OPT_GIVEN(DETAILED_HELP, lpr)) help = lls_long_help(CMD_PTR); else if (OPT_GIVEN(HELP, lpr)) help = lls_short_help(CMD_PTR); else return; printf("%s\n", help); free(help); print_writer_helps(OPT_GIVEN(DETAILED_HELP, lpr)); exit(EXIT_SUCCESS); } struct write_task { struct task *task; struct check_wav_context *cwc; }; static void write_pre_monitor(struct sched *s, void *context) { struct write_task *wt = context; check_wav_pre_monitor(s, wt->cwc); } static int write_post_monitor(__a_unused struct sched *s, void *context) { struct write_task *wt = context; return check_wav_post_monitor(wt->cwc); } static int setup_and_schedule(struct lls_parse_result *lpr) { int i, n, ret, writer_given = OPT_GIVEN(WRITER, lpr); struct writer_node *wns; struct sched *s = sched_new(NULL); struct write_task wt; struct stdin_task *sit = stdin_new(); stdin_register(sit, s); wt.cwc = check_wav_init(stdin_btrn(sit), lpr); wt.task = task_register(&(struct task_info) { .name = "write", .pre_monitor = write_pre_monitor, .post_monitor = write_post_monitor, .context = &wt, }, s); n = writer_given? writer_given : 1; wns = arr_zalloc(n, sizeof(*wns)); for (i = 0; i < n; i++) { const char *arg = i < writer_given? lls_string_val(i, OPT_RESULT(WRITER, lpr)) : NULL; wns[i].wid = check_writer_arg_or_die(arg, &wns[i].lpr); register_writer_node(wns + i, wt.cwc->btrn, s); } ret = schedule(s); if (ret >= 0) { int j, ts; for (j = 0; j < n; j++) { struct writer_node *wn = wns + j; ts = task_status(wn->task); assert(ts < 0); if (ts != -E_EOF) { const char *name = writer_name(wn->wid); PARA_ERROR_LOG("%s: %s\n", name, para_strerror(-ts)); if (ret >= 0) ret = ts; } } } for (i = n - 1; i >= 0; i--) { struct writer_node *wn = wns + i; writer_get(wn->wid)->close(wn); btr_remove_node(&wn->btrn); lls_free_parse_result(wns[i].lpr, lls_cmd(wn->wid, write_cmd_suite)); } stdin_free(sit); free(wns); check_wav_shutdown(wt.cwc); sched_shutdown(s); return ret; } /** \endcond * * The main function of para_write(1). * * \param argc Options are defined in the write lopsub suite. * * \param argv The write_cmd suite file defines the various writers and their * options. Each writer corresponds to a lopsub subcommand of this suite. * * Set up the stdin task, the check_wav task and the writer task, and combine * the corresponding buffer tree nodes to form a chain. The check wav task * becomes active as soon as the first chunk of data is read from stdin. If * the data looks like a wav header, the check_wav task parses the header, * and passes through subsequent data, waiting for the writer task calling * \ref btr_exec_up() to query the number of channels, the sample rate and * the sample format. * * \return EXIT_SUCCESS or EXIT_FAILURE */ int main(int argc, char *argv[]) { int ret; struct lls_parse_result *lpr; char *errctx; ret = lls(lls_parse(argc, argv, CMD_PTR, &lpr, &errctx)); if (ret < 0) goto out; loglevel = OPT_UINT32_VAL(LOGLEVEL, lpr); version_handle_flag("write", OPT_GIVEN(VERSION, lpr)); handle_help_flag(lpr); ret = setup_and_schedule(lpr); lls_free_parse_result(lpr, CMD_PTR); out: if (ret < 0) { if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); PARA_ERROR_LOG("%s\n", para_strerror(-ret)); } return ret < 0? EXIT_FAILURE : EXIT_SUCCESS; }