diff options
author | Pantelis Antoniou <pantelis.antoniou@konsulko.com> | 2020-01-17 16:06:04 +0200 |
---|---|---|
committer | Wim Taymans <wtaymans@redhat.com> | 2020-01-27 12:23:15 +0100 |
commit | 588e9562f91407aadb28cbfc0602c02ffd8ceedf (patch) | |
tree | 2a0abffbd100caff2172e4b8a11b6bbe362a16e4 | |
parent | 6ac9b7b3a7b0adb23754f9a3ca555e6db1689075 (diff) |
pwcat: simple native playback/record tool
pwcat is analogous to pacat of PulseAudio which implements
both playback and recording capability.
Only wav files are supported for now, and you can use the
handy pwplay and pwrecord aliases for easy use.
Playback a wav file
$ pwplay foo.wav
Record a wav file
$ pwrecord -r 44100 -c 1 -f s16 foo.wav
Signed-off-by: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
-rw-r--r-- | .gitlab-ci.yml | 1 | ||||
-rw-r--r-- | meson.build | 3 | ||||
-rw-r--r-- | meson_options.txt | 4 | ||||
-rw-r--r-- | src/tools/meson.build | 26 | ||||
-rw-r--r-- | src/tools/pwcat.c | 963 |
5 files changed, 997 insertions, 0 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bf07f042..1effaa57 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,6 +43,7 @@ build-container: vulkan-loader-devel which xmltoman + libsndfile-devel build: stage: build diff --git a/meson.build b/meson.build index 4c4e9b9b..92673b82 100644 --- a/meson.build +++ b/meson.build @@ -192,6 +192,9 @@ dl_lib = cc.find_library('dl', required : false) pthread_lib = dependency('threads') dbus_dep = dependency('dbus-1') sdl_dep = dependency('sdl2', required : false) +if get_option('pwcat') + sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : false) +endif if get_option('gstreamer') or get_option('pipewire-pulseaudio') glib_dep = dependency('glib-2.0', version : '>=2.32.0') diff --git a/meson_options.txt b/meson_options.txt index e8eece7f..4e727494 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -98,3 +98,7 @@ option('vulkan', description: 'Enable vulkan spa plugin integration', type: 'boolean', value: true) +option('pwcat', + description: 'Build pwcat/pwplay/pwrecord', + type: 'boolean', + value: true) diff --git a/src/tools/meson.build b/src/tools/meson.build index e9b6cefe..d2758e5b 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -16,3 +16,29 @@ executable('pipewire-dot', install: true, dependencies : [pipewire_dep], ) + +if get_option('pwcat') and sndfile_dep.found() + + pwcat_sources = [ + 'pwcat.c', + ] + + pwcat_aliases = [ + 'pwplay', + 'pwrecord', + ] + + executable('pwcat', + pwcat_sources, + c_args : [ '-D_GNU_SOURCE' ], + install: true, + dependencies : [sndfile_dep, pipewire_dep, mathlib], + ) + + foreach alias : pwcat_aliases + dst = join_paths(pipewire_bindir, alias) + cmd = 'ln -fs @0@ $DESTDIR@1@'.format('pwcat', dst) + meson.add_install_script('sh', '-c', cmd) + endforeach + +endif diff --git a/src/tools/pwcat.c b/src/tools/pwcat.c new file mode 100644 index 00000000..73d2150b --- /dev/null +++ b/src/tools/pwcat.c @@ -0,0 +1,963 @@ +/* PipeWire - pwcat + * + * Copyright © 2020 Konsulko Group + + * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdio.h> +#include <errno.h> +#include <time.h> +#include <math.h> +#include <sys/mman.h> +#include <signal.h> +#include <getopt.h> +#include <unistd.h> +#include <assert.h> +#include <ctype.h> + +#include <sndfile.h> + +#include <spa/param/audio/format-utils.h> +#include <spa/param/props.h> + +#include <pipewire/pipewire.h> + +#define DEFAULT_MEDIA_TYPE "Audio" +#define DEFAULT_MEDIA_CATEGORY_PLAYBACK "Playback" +#define DEFAULT_MEDIA_CATEGORY_RECORD "Capture" +#define DEFAULT_MEDIA_ROLE "Music" +#define DEFAULT_TARGET "auto" +#define DEFAULT_LATENCY "100ms" +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_FORMAT "s16" + +enum mode { + mode_none, + mode_playback, + mode_record +}; + +enum unit { + unit_none, + unit_samples, + unit_sec, + unit_msec, + unit_usec, + unit_nsec, +}; + +struct data; + +typedef int (*fill_fn)(struct data *d, void *dest, unsigned int n_frames); + +struct data { + struct pw_main_loop *loop; + struct pw_stream *stream; + + double accumulator; + + enum mode mode; + bool verbose; + const char *remote_name; + const char *media_type; + const char *media_category; + const char *media_role; + const char *target; + const char *latency; + + const char *filename; + SNDFILE *file; + SF_INFO info; + + unsigned int rate; + unsigned int channels; + unsigned int samplesize; + unsigned int stride; + enum unit latency_unit; + unsigned int latency_value; + + enum spa_audio_format spa_format; + + fill_fn fill; + + uint32_t target_id; + + bool drained; +}; + +static inline bool +sf_is_valid_type(int format) +{ + int type = (format & SF_FORMAT_TYPEMASK); + + return type == SF_FORMAT_WAV; +} + +static inline bool +sf_is_valid_subtype(int format) +{ + int sub_type = (format & SF_FORMAT_SUBMASK); + + return sub_type == SF_FORMAT_PCM_S8 || + sub_type == SF_FORMAT_PCM_16 || + sub_type == SF_FORMAT_PCM_32 || + sub_type == SF_FORMAT_FLOAT || + sub_type == SF_FORMAT_DOUBLE; +} + +static inline int +sf_str_to_fmt(const char *str) +{ + if (!str) + return -1; + + if (!strcmp(str, "s8")) + return SF_FORMAT_PCM_S8 | SF_FORMAT_WAV; + if (!strcmp(str, "s16")) + return SF_FORMAT_PCM_16 | SF_FORMAT_WAV; + if (!strcmp(str, "s32")) + return SF_FORMAT_PCM_32 | SF_FORMAT_WAV; + if (!strcmp(str, "f32")) + return SF_FORMAT_FLOAT | SF_FORMAT_WAV; + if (!strcmp(str, "f64")) + return SF_FORMAT_DOUBLE | SF_FORMAT_WAV; + + return -1; +} + +static inline const char * +sf_fmt_to_str(int format) +{ + int sub_type = (format & SF_FORMAT_SUBMASK); + + if (sub_type == SF_FORMAT_PCM_S8) + return "s8"; + if (sub_type == SF_FORMAT_PCM_16) + return "s16"; + if (sub_type == SF_FORMAT_PCM_32) + return "s32"; + if (sub_type == SF_FORMAT_FLOAT) + return "f32"; + if (sub_type == SF_FORMAT_DOUBLE) + return "f64"; + return "(invalid)"; +} + +#define STR_FMTS "(s8|s16|s32|f32|f64)" + +/* 0 = native, 1 = le, 2 = be */ +static inline int +sf_format_endianess(int format) +{ + switch (format & SF_FORMAT_TYPEMASK) { + case SF_FORMAT_WAV: + return 0; /* sf_readf_* return native format */ + default: + break; + } + return 0; /* native */ +} + +static inline enum spa_audio_format +sf_format_to_pw(int format) +{ + int endianess; + + endianess = sf_format_endianess(format); + if (endianess < 0) + return SPA_AUDIO_FORMAT_UNKNOWN; + + switch (format & SF_FORMAT_SUBMASK) { + case SF_FORMAT_PCM_S8: + return SPA_AUDIO_FORMAT_S8; + case SF_FORMAT_PCM_16: + return endianess == 1 ? SPA_AUDIO_FORMAT_S16_LE : + endianess == 2 ? SPA_AUDIO_FORMAT_S16_BE : + SPA_AUDIO_FORMAT_S16; + case SF_FORMAT_PCM_32: + return endianess == 1 ? SPA_AUDIO_FORMAT_S32_LE : + endianess == 2 ? SPA_AUDIO_FORMAT_S32_BE : + SPA_AUDIO_FORMAT_S32; + case SF_FORMAT_FLOAT: + return endianess == 1 ? SPA_AUDIO_FORMAT_F32_LE : + endianess == 2 ? SPA_AUDIO_FORMAT_F32_BE : + SPA_AUDIO_FORMAT_F32; + case SF_FORMAT_DOUBLE: + return endianess == 1 ? SPA_AUDIO_FORMAT_F64_LE : + endianess == 2 ? SPA_AUDIO_FORMAT_F64_BE : + SPA_AUDIO_FORMAT_F64; + default: + break; + } + + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static inline int +sf_format_samplesize(int format) +{ + int sub_type = (format & SF_FORMAT_SUBMASK); + + switch (sub_type) { + case SF_FORMAT_PCM_S8: + return 1; + case SF_FORMAT_PCM_16: + return 2; + case SF_FORMAT_PCM_32: + return 4; + case SF_FORMAT_FLOAT: + return 4; + case SF_FORMAT_DOUBLE: + return 8; + default: + break; + } + return -1; +} + +static int sf_playback_fill_s8(struct data *d, void *dest, unsigned int n_frames) +{ + sf_count_t rn; + + rn = sf_read_raw(d->file, dest, n_frames); + return (int)rn; +} + +static int sf_playback_fill_s16(struct data *d, void *dest, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(short) == sizeof(int16_t)); + rn = sf_readf_short(d->file, dest, n_frames); + return (int)rn; +} + +static int sf_playback_fill_s32(struct data *d, void *dest, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(int) == sizeof(int32_t)); + rn = sf_readf_int(d->file, dest, n_frames); + return (int)rn; +} + +static int sf_playback_fill_f32(struct data *d, void *dest, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(float) == 4); + rn = sf_readf_float(d->file, dest, n_frames); + return (int)rn; +} + +static int sf_playback_fill_f64(struct data *d, void *dest, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(double) == 8); + rn = sf_readf_double(d->file, dest, n_frames); + return (int)rn; +} + +static inline fill_fn +sf_fmt_playback_fill_fn(int format) +{ + enum spa_audio_format fmt = sf_format_to_pw(format); + int type = (format & SF_FORMAT_TYPEMASK); + + switch (fmt) { + case SPA_AUDIO_FORMAT_S8: + if (type == SF_FORMAT_WAV) + return sf_playback_fill_s8; + break; + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_S16_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(int16_t) != sizeof(short)) + return NULL; + return sf_playback_fill_s16; + } + break; + case SPA_AUDIO_FORMAT_S32_LE: + case SPA_AUDIO_FORMAT_S32_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(int32_t) != sizeof(int)) + return NULL; + return sf_playback_fill_s32; + } + break; + case SPA_AUDIO_FORMAT_F32_LE: + case SPA_AUDIO_FORMAT_F32_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(float) != 4) + return NULL; + return sf_playback_fill_f32; + } + break; + case SPA_AUDIO_FORMAT_F64_LE: + case SPA_AUDIO_FORMAT_F64_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(double) != 8) + return NULL; + return sf_playback_fill_f64; + } + break; + default: + break; + } + return NULL; +} + +static int sf_record_fill_s8(struct data *d, void *src, unsigned int n_frames) +{ + sf_count_t rn; + + rn = sf_write_raw(d->file, src, n_frames); + return (int)rn; +} + +static int sf_record_fill_s16(struct data *d, void *src, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(short) == sizeof(int16_t)); + rn = sf_writef_short(d->file, src, n_frames); + return (int)rn; +} + +static int sf_record_fill_s32(struct data *d, void *src, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(int) == sizeof(int32_t)); + rn = sf_writef_int(d->file, src, n_frames); + return (int)rn; +} + +static int sf_record_fill_f32(struct data *d, void *src, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(float) == 4); + rn = sf_writef_float(d->file, src, n_frames); + return (int)rn; +} + +static int sf_record_fill_f64(struct data *d, void *src, unsigned int n_frames) +{ + sf_count_t rn; + + assert(sizeof(double) == 8); + rn = sf_writef_double(d->file, src, n_frames); + return (int)rn; +} + +static inline fill_fn +sf_fmt_record_fill_fn(int format) +{ + enum spa_audio_format fmt = sf_format_to_pw(format); + int type = (format & SF_FORMAT_TYPEMASK); + + switch (fmt) { + case SPA_AUDIO_FORMAT_S8: + if (type == SF_FORMAT_WAV) + return sf_record_fill_s8; + break; + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_S16_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(int16_t) != sizeof(short)) + return NULL; + return sf_record_fill_s16; + } + break; + case SPA_AUDIO_FORMAT_S32_LE: + case SPA_AUDIO_FORMAT_S32_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(int32_t) != sizeof(int)) + return NULL; + return sf_record_fill_s32; + } + break; + case SPA_AUDIO_FORMAT_F32_LE: + case SPA_AUDIO_FORMAT_F32_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(float) != 4) + return NULL; + return sf_record_fill_f32; + } + break; + case SPA_AUDIO_FORMAT_F64_LE: + case SPA_AUDIO_FORMAT_F64_BE: + if (type == SF_FORMAT_WAV) { + /* sndfile check */ + if (sizeof(double) != 8) + return NULL; + return sf_record_fill_f64; + } + break; + default: + break; + } + return NULL; +} + +static void +on_state_changed(void *userdata, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct data *data = userdata; + + if (data->verbose) + printf("stream state changed %s -> %s\n", + pw_stream_state_as_string(old), + pw_stream_state_as_string(state)); +} + +static void +on_param_changed(void *userdata, uint32_t id, const struct spa_pod *format) +{ + struct data *data = userdata; + + if (data->verbose) + printf("stream param change: id=%"PRIu32"\n", + id); +} + +static void on_process(void *userdata) +{ + struct data *data = userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + struct spa_data *d; + int n_frames, n_fill_frames; + uint8_t *p; + bool have_data; + uint32_t offset, size; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) + return; + + buf = b->buffer; + d = &buf->datas[0]; + + have_data = false; + + if ((p = d->data) == NULL) + return; + + if (data->mode == mode_playback) { + + n_frames = d->maxsize / data->stride; + + n_fill_frames = data->fill(data, p, n_frames); + + if (n_fill_frames > 0) { + d->chunk->offset = 0; + d->chunk->stride = data->stride; + d->chunk->size = n_fill_frames * data->stride; + have_data = true; + } else if (n_fill_frames < 0) + fprintf(stderr, "fill error %d\n", n_fill_frames); + } else { + offset = SPA_MIN(d->chunk->offset, d->maxsize); + size = SPA_MIN(d->chunk->size, d->maxsize - offset); + + p += offset; + + n_frames = size / data->stride; + + n_fill_frames = data->fill(data, p, n_frames); + + have_data = true; + } + + if (have_data) { + pw_stream_queue_buffer(data->stream, b); + return; + } + + if (data->mode == mode_playback) + pw_stream_flush(data->stream, true); +} + +static void on_drained(void *userdata) +{ + struct data *data = userdata; + + if (data->verbose) + printf("stream drained\n"); + + data->drained = true; + pw_main_loop_quit(data->loop); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_state_changed, + .param_changed = on_param_changed, + .process = on_process, + .drained = on_drained +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->loop); +} + +enum { + OPT_VERSION = 1000, + OPT_MEDIA_TYPE, + OPT_MEDIA_CATEGORY, + OPT_MEDIA_ROLE, + OPT_TARGET, + OPT_LATENCY, + OPT_RATE, + OPT_CHANNELS, + OPT_FORMAT, +}; + +static const struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, OPT_VERSION}, + {"verbose", no_argument, NULL, 'v'}, + + {"record", no_argument, NULL, 'r'}, + {"playback", no_argument, NULL, 's'}, + + {"remote", required_argument, NULL, 'R'}, + + {"media-type", required_argument, NULL, OPT_MEDIA_TYPE }, + {"media-category", required_argument, NULL, OPT_MEDIA_CATEGORY }, + {"media-role", required_argument, NULL, OPT_MEDIA_ROLE }, + {"target", required_argument, NULL, OPT_TARGET }, + {"latency", required_argument, NULL, OPT_LATENCY }, + + {"rate", required_argument, NULL, OPT_RATE }, + {"channels", required_argument, NULL, OPT_CHANNELS }, + {"format", required_argument, NULL, OPT_FORMAT }, + + {NULL, 0, NULL, 0 } +}; + +static void show_usage(const char *name, bool is_error) +{ + FILE *fp; + + fp = is_error ? stderr : stdout; + + fprintf(fp, "%s [options] <file>\n", name); + + fprintf(fp, + " -h, --help Show this help\n" + " --version Show version\n" + "\n"); + + fprintf(fp, + " -r, --remote Remote daemon name\n" + " --media-type Set media type (default %s)\n" + " --media-category Set media category (default %s)\n" + " --media-role Set media role (default %s)\n" + " --target Set node target (default %s)\n" + " --latency Set node latency (default %s)\n" + " Xunit (unit = s, ms, us, ns)\n" + " or direct samples (256)\n" + " the rate is the one of the source file\n" + "\n", + DEFAULT_MEDIA_TYPE, + DEFAULT_MEDIA_CATEGORY_PLAYBACK, + DEFAULT_MEDIA_ROLE, + DEFAULT_TARGET, DEFAULT_LATENCY); + + fprintf(fp, + " --rate Sample rate (req. for rec) (default %u)\n" + " --channels Number of channels (req. for rec) (default %u)\n" + " --format Sample format %s (req. for rec) (default %s)\n" + "\n", + DEFAULT_RATE, DEFAULT_CHANNELS, STR_FMTS, DEFAULT_FORMAT); + + if (!strcmp(name, "pwcat")) { + fprintf(fp, + " -p, --playback Playback mode\n" + " -r, --record Recording mode\n" + "\n"); + } + + fprintf(fp, + " -v, --verbose Enable verbose operations\n" + "\n"); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + struct pw_loop *l; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + const char *prog; + int exit_code = EXIT_FAILURE, c, format = 0, ret; + struct pw_properties *props; + const char *s; + unsigned int nom; + + pw_init(&argc, &argv); + + prog = argv[0]; + if ((prog = strrchr(argv[0], '/')) != NULL) + prog++; + else + prog = argv[0]; + + /* prime the mode from the program name */ + if (!strcmp(prog, "pwplay")) + data.mode = mode_playback; + else if (!strcmp(prog, "pwrecord")) + data.mode = mode_record; + else + data.mode = mode_none; + + while ((c = getopt_long(argc, argv, "hvprR:", long_options, NULL)) != -1) { + + switch (c) { + + case 'h': + show_usage(prog, false); + return EXIT_SUCCESS; + + case OPT_VERSION: + fprintf(stdout, "%s\n" + "Compiled with libpipewire %s\n" + "Linked with libpipewire %s\n", + prog, + pw_get_headers_version(), + pw_get_library_version()); + return 0; + + case 'v': + data.verbose = true; + break; + + case 'p': + data.mode = mode_playback; + break; + + case 'r': + data.mode = mode_record; + break; + + case 'R': + data.remote_name = optarg; + break; + + case OPT_MEDIA_TYPE: + data.media_type = optarg; + break; + + case OPT_MEDIA_CATEGORY: + data.media_category = optarg; + break; + + case OPT_MEDIA_ROLE: + data.media_role = optarg; + break; + + case OPT_TARGET: + if (!strcmp(optarg, "auto")) { + data.target = optarg; + data.target_id = PW_ID_ANY; + break; + } + if (!isdigit(optarg[0])) { + fprintf(stderr, "error: bad target option \"%s\"\n", optarg); + goto error_usage; + } + break; + + case OPT_LATENCY: + data.latency = optarg; + break; + + case OPT_RATE: + ret = atoi(optarg); + if (ret <= 0) { + fprintf(stderr, "error: bad rate %d\n", ret); + goto error_usage; + } + data.rate = (unsigned int)ret; + break; + + case OPT_CHANNELS: + ret = atoi(optarg); + if (ret <= 0) { + fprintf(stderr, "error: bad channels %d\n", ret); + goto error_usage; + } + data.channels = (unsigned int)ret; + break; + + case OPT_FORMAT: + if (sf_str_to_fmt(optarg) == -1) { + fprintf(stderr, "error: unknown format \"%s\"\n", optarg); + goto error_usage; + } + format = sf_str_to_fmt(optarg); + break; + + default: + fprintf(stderr, "error: unknown option '%c'\n", c); + goto error_usage; + } + } + + if (data.mode == mode_none) { + fprintf(stderr, "error: one of the playback/record options must be provided\n"); + goto error_usage; + } + + if (!data.media_type) + data.media_type = DEFAULT_MEDIA_TYPE; + if (!data.media_category) + data.media_category = data.mode == mode_playback ? + DEFAULT_MEDIA_CATEGORY_PLAYBACK : + DEFAULT_MEDIA_CATEGORY_RECORD; + if (!data.media_role) + data.media_role = DEFAULT_MEDIA_ROLE; + if (!data.target) { + data.target = DEFAULT_TARGET; + data.target_id = PW_ID_ANY; + } + if (!data.latency) + data.latency = DEFAULT_LATENCY; + if (!data.rate) + data.rate = DEFAULT_RATE; + if (!data.channels) + data.rate = DEFAULT_CHANNELS; + if (data.mode == mode_record && !format) + format = sf_str_to_fmt(DEFAULT_FORMAT); + + if (optind >= argc) { + fprintf(stderr, "error: filename argument missing\n"); + goto error_usage; + } + data.filename = argv[optind++]; + + /* for record, you fill in the info first */ + if (data.mode == mode_record) { + memset(&data.info, 0, sizeof(data.info)); + data.info.samplerate = data.rate; + data.info.channels = data.channels; + data.info.format = format; + } + + data.file = sf_open(data.filename, + data.mode == mode_playback ? SFM_READ : SFM_WRITE, + &data.info); + if (!data.file) { + fprintf(stderr, "error: failed to open audio file \"%s\"n", + data.filename); + goto error_open_file; + } + + if (data.verbose) + printf("opened file \"%s\"\n", data.filename); + + format = data.info.format; + if (!sf_is_valid_type(format) || + !sf_is_valid_subtype(format) || + sf_format_samplesize(format) <= 0) { + fprintf(stderr, "error: Invalid file format, require WAV PCM8/16/32 or float/double\n"); + goto error_bad_file; + } + + if (data.mode == mode_playback) { + data.rate = data.info.samplerate; + data.channels = data.info.channels; + } + data.samplesize = sf_format_samplesize(format); + data.stride = data.samplesize * data.channels; + data.spa_format = sf_format_to_pw(format); + data.fill = data.mode == mode_playback ? + sf_fmt_playback_fill_fn(format) : + sf_fmt_record_fill_fn(format); + + data.latency_unit = unit_none; + s = data.latency; + while (*s && isdigit(*s)) + s++; + if (!*s) + data.latency_unit = unit_samples; + else if (!strcmp(s, "none")) + data.latency_unit = unit_none; + else if (!strcmp(s, "s") || !strcmp(s, "sec") || !strcmp(s, "secs")) + data.latency_unit = unit_sec; + else if (!strcmp(s, "ms") || !strcmp(s, "msec") || !strcmp(s, "msecs")) + data.latency_unit = unit_msec; + else if (!strcmp(s, "us") || !strcmp(s, "usec") || !strcmp(s, "usecs")) + data.latency_unit = unit_usec; + else if (!strcmp(s, "ns") || !strcmp(s, "nsec") || !strcmp(s, "nsecs")) + data.latency_unit = unit_nsec; + else { + fprintf(stderr, "error: bad latency value %s (bad unit)\n", data.latency); + goto error_bad_file; + } + data.latency_value = atoi(data.latency); + if (!data.latency_value) { + fprintf(stderr, "error: bad latency value %s (is zero)\n", data.latency); + goto error_bad_file; + } + + switch (data.latency_unit) { + case unit_sec: + nom = data.latency_value * data.rate; + break; + case unit_msec: + nom = nearbyint((data.latency_value * data.rate) / 1000.0); + break; + case unit_usec: + nom = nearbyint((data.latency_value * data.rate) / 1000000.0); + break; + case unit_nsec: + nom = nearbyint((data.latency_value * data.rate) / 1000000000.0); + break; + case unit_samples: + nom = data.latency_value; + break; + default: + nom = 0; + break; + } + + if (data.verbose) + printf("rate=%u channels=%u fmt=%s samplesize=%u stride=%u latency=%u (%.3fs)\n", + data.rate, data.channels, + sf_fmt_to_str(format), + data.samplesize, + data.stride, nom, (double)nom/data.rate); + + /* make a main loop. If you already have another main loop, you can add + * the fd of this pipewire mainloop to it. */ + data.loop = pw_main_loop_new(NULL); + if (!data.loop) { + fprintf(stderr, "error: pw_main_loop_new() failed\n"); + goto error_no_main_loop; + } + + l = pw_main_loop_get_loop(data.loop); + pw_loop_add_signal(l, SIGINT, do_quit, &data); + pw_loop_add_signal(l, SIGTERM, do_quit, &data); + + props = pw_properties_new( + PW_KEY_MEDIA_TYPE, data.media_type, + PW_KEY_MEDIA_CATEGORY, data.media_category, + PW_KEY_MEDIA_ROLE, data.media_role, + PW_KEY_APP_NAME, prog, + PW_KEY_NODE_NAME, prog, + NULL); + if (!props) { + fprintf(stderr, "error: pw_properties_new() failed\n"); + goto error_no_props; + } + + if (nom) + pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data.rate); + + data.stream = pw_stream_new_simple(l, + prog, + props, + &stream_events, + &data); + if (!data.stream) { + fprintf(stderr, "error: failed to create simple stream\n"); + goto error_no_stream; + } + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = data.spa_format, + .channels = data.channels, + .rate = data.rate )); + + ret = pw_stream_connect(data.stream, + data.mode == mode_playback ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT, + data.target_id, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, 1); + if (ret != 0) { + fprintf(stderr, "error: failed connect\n"); + goto error_connect_fail; + } + + if (data.verbose) { + const struct pw_properties *props; + void *pstate; + const char *key, *val; + + if ((props = pw_stream_get_properties(data.stream)) != NULL) { + printf("stream properties:\n"); + pstate = NULL; + while ((key = pw_properties_iterate(props, &pstate)) != NULL && + (val = pw_properties_get(props, key)) != NULL) { + printf("\t%s = \"%s\"\n", key, val); + } + } + } + + /* and wait while we let things run */ + pw_main_loop_run(data.loop); + + /* we're returning OK only if got to the point to drain */ + if (data.drained) + exit_code = EXIT_SUCCESS; + +error_connect_fail: + pw_stream_destroy(data.stream); + +error_no_stream: + pw_main_loop_destroy(data.loop); + +error_no_props: +error_no_main_loop: +error_bad_file: + + if (data.file) + sf_close(data.file); +error_open_file: + + return exit_code; + +error_usage: + show_usage(prog, true); + return EXIT_FAILURE; +} |