diff options
Diffstat (limited to '')
-rw-r--r-- | src/tools/pw-cat.c | 1971 |
1 files changed, 1971 insertions, 0 deletions
diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c new file mode 100644 index 0000000..068fac1 --- /dev/null +++ b/src/tools/pw-cat.c @@ -0,0 +1,1971 @@ +/* PipeWire - pw-cat + * + * 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 <signal.h> +#include <fcntl.h> +#include <getopt.h> +#include <unistd.h> +#include <assert.h> +#include <ctype.h> +#include <locale.h> + +#include <sndfile.h> + +#include <spa/param/audio/layout.h> +#include <spa/param/audio/format-utils.h> +#include <spa/param/audio/type-info.h> +#include <spa/param/props.h> +#include <spa/utils/result.h> +#include <spa/utils/string.h> +#include <spa/utils/json.h> +#include <spa/debug/types.h> + +#include <pipewire/pipewire.h> +#include <pipewire/i18n.h> +#include <pipewire/extensions/metadata.h> + +#include "config.h" + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION +#include <libavformat/avformat.h> +#include <libavcodec/avcodec.h> +#endif + +#include "midifile.h" +#include "dsffile.h" + +#define DEFAULT_MEDIA_TYPE "Audio" +#define DEFAULT_MIDI_MEDIA_TYPE "Midi" +#define DEFAULT_MEDIA_CATEGORY_PLAYBACK "Playback" +#define DEFAULT_MEDIA_CATEGORY_RECORD "Capture" +#define DEFAULT_MEDIA_ROLE "Music" +#define DEFAULT_TARGET "auto" +#define DEFAULT_LATENCY_PLAY "100ms" +#define DEFAULT_LATENCY_REC "none" +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_FORMAT "s16" +#define DEFAULT_VOLUME 1.0 +#define DEFAULT_QUALITY 4 + +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 channelmap { + int n_channels; + int channels[SPA_AUDIO_MAX_CHANNELS]; +}; + +struct data { + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct spa_hook core_listener; + + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct spa_source *timer; + + enum mode mode; + bool verbose; +#define TYPE_PCM 0 +#define TYPE_MIDI 1 +#define TYPE_DSD 2 +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION +#define TYPE_ENCODED 3 +#endif + int data_type; + const char *remote_name; + const char *media_type; + const char *media_category; + const char *media_role; + const char *channel_map; + const char *format; + const char *target; + const char *latency; + struct pw_properties *props; + + const char *filename; + SNDFILE *file; + + unsigned int bitrate; + unsigned int rate; + int channels; + struct channelmap channelmap; + unsigned int stride; + enum unit latency_unit; + unsigned int latency_value; + int quality; + + enum spa_audio_format spa_format; + + float volume; + bool volume_is_set; + + fill_fn fill; + + struct spa_io_position *position; + bool drained; + uint64_t clock_time; + + struct { + struct midi_file *file; + struct midi_file_info info; + } midi; + struct { + struct dsf_file *file; + struct dsf_file_info info; + struct dsf_layout layout; + } dsf; + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + FILE *encoded_file; + AVFormatContext *fmt_context; + AVStream *astream; + AVCodecContext *ctx; + enum AVSampleFormat sfmt; +#endif +}; + +#define STR_FMTS "(ulaw|alaw|u8|s8|s16|s32|f32|f64)" + +static const struct format_info { + const char *name; + int sf_format; + uint32_t spa_format; + uint32_t width; +} format_info[] = { + { "ulaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ULAW, 1 }, + { "alaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ALAW, 1 }, + { "s8", SF_FORMAT_PCM_S8, SPA_AUDIO_FORMAT_S8, 1 }, + { "u8", SF_FORMAT_PCM_U8, SPA_AUDIO_FORMAT_U8, 1 }, + { "s16", SF_FORMAT_PCM_16, SPA_AUDIO_FORMAT_S16, 2 }, + { "s24", SF_FORMAT_PCM_24, SPA_AUDIO_FORMAT_S24, 3 }, + { "s32", SF_FORMAT_PCM_32, SPA_AUDIO_FORMAT_S32, 4 }, + { "f32", SF_FORMAT_FLOAT, SPA_AUDIO_FORMAT_F32, 4 }, + { "f64", SF_FORMAT_DOUBLE, SPA_AUDIO_FORMAT_F32, 8 }, +}; + +static const struct format_info *format_info_by_name(const char *str) +{ + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) + if (spa_streq(str, i->name)) + return i; + return NULL; +} + +static const struct format_info *format_info_by_sf_format(int format) +{ + int sub_type = (format & SF_FORMAT_SUBMASK); + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) + if (i->sf_format == sub_type) + return i; + return NULL; +} + +static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames) +{ + sf_count_t rn; + + rn = sf_read_raw(d->file, dest, n_frames * d->stride); + return (int)rn / d->stride; +} + +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; +} + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION +static int encoded_playback_fill(struct data *d, void *dest, unsigned int n_frames) +{ + int ret, size = 0; + uint8_t buffer[16384] = { 0 }; + + ret = fread(buffer, 1, 16384, d->encoded_file); + if (ret > 0) { + memcpy(dest, buffer, ret); + size = ret; + } + + return (int)size; +} + +static int avcodec_ctx_to_info(struct data *data, AVCodecContext *ctx, struct spa_audio_info *info) +{ + int32_t profile; + + switch (ctx->codec_id) { + case AV_CODEC_ID_VORBIS: + info->media_subtype = SPA_MEDIA_SUBTYPE_vorbis; + info->info.vorbis.rate = data->rate; + info->info.vorbis.channels = data->channels; + break; + case AV_CODEC_ID_MP3: + info->media_subtype = SPA_MEDIA_SUBTYPE_mp3; + info->info.mp3.rate = data->rate; + info->info.mp3.channels = data->channels; + break; + case AV_CODEC_ID_AAC: + info->media_subtype = SPA_MEDIA_SUBTYPE_aac; + info->info.aac.rate = data->rate; + info->info.aac.channels = data->channels; + info->info.aac.bitrate = data->bitrate; + info->info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_RAW; + break; + case AV_CODEC_ID_WMAV1: + case AV_CODEC_ID_WMAV2: + case AV_CODEC_ID_WMAPRO: + case AV_CODEC_ID_WMAVOICE: + case AV_CODEC_ID_WMALOSSLESS: + info->media_subtype = SPA_MEDIA_SUBTYPE_wma; + switch (ctx->codec_tag) { + /* TODO see if these hex constants can be replaced by named constants from FFmpeg */ + case 0x161: + profile = SPA_AUDIO_WMA_PROFILE_WMA9; + break; + case 0x162: + profile = SPA_AUDIO_WMA_PROFILE_WMA9_PRO; + break; + case 0x163: + profile = SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS; + break; + case 0x166: + profile = SPA_AUDIO_WMA_PROFILE_WMA10; + break; + case 0x167: + profile = SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS; + break; + default: + fprintf(stderr, "error: invalid WMA profile\n"); + return -EINVAL; + } + info->info.wma.rate = data->rate; + info->info.wma.channels = data->channels; + info->info.wma.bitrate = data->bitrate; + info->info.wma.block_align = ctx->block_align; + info->info.wma.profile = profile; + break; + case AV_CODEC_ID_FLAC: + info->media_subtype = SPA_MEDIA_SUBTYPE_flac; + info->info.flac.rate = data->rate; + info->info.flac.channels = data->channels; + break; + case AV_CODEC_ID_ALAC: + info->media_subtype = SPA_MEDIA_SUBTYPE_alac; + info->info.alac.rate = data->rate; + info->info.alac.channels = data->channels; + break; + case AV_CODEC_ID_APE: + info->media_subtype = SPA_MEDIA_SUBTYPE_ape; + info->info.ape.rate = data->rate; + info->info.ape.channels = data->channels; + break; + case AV_CODEC_ID_RA_144: + case AV_CODEC_ID_RA_288: + info->media_subtype = SPA_MEDIA_SUBTYPE_ra; + info->info.ra.rate = data->rate; + info->info.ra.channels = data->channels; + break; + case AV_CODEC_ID_AMR_NB: + info->media_subtype = SPA_MEDIA_SUBTYPE_amr; + info->info.amr.rate = data->rate; + info->info.amr.channels = data->channels; + info->info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_NB; + break; + case AV_CODEC_ID_AMR_WB: + info->media_subtype = SPA_MEDIA_SUBTYPE_amr; + info->info.amr.rate = data->rate; + info->info.amr.channels = data->channels; + info->info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB; + break; + default: + fprintf(stderr, "Unsupported encoded media subtype\n"); + return -EINVAL; + } + return 0; +} +#endif + +static inline fill_fn +playback_fill_fn(uint32_t fmt) +{ + switch (fmt) { + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_ULAW: + case SPA_AUDIO_FORMAT_ALAW: + return sf_playback_fill_x8; + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_S16_BE: + /* sndfile check */ + if (sizeof(int16_t) != sizeof(short)) + return NULL; + return sf_playback_fill_s16; + case SPA_AUDIO_FORMAT_S32_LE: + case SPA_AUDIO_FORMAT_S32_BE: + /* sndfile check */ + if (sizeof(int32_t) != sizeof(int)) + return NULL; + return sf_playback_fill_s32; + case SPA_AUDIO_FORMAT_F32_LE: + case SPA_AUDIO_FORMAT_F32_BE: + /* sndfile check */ + if (sizeof(float) != 4) + return NULL; + return sf_playback_fill_f32; + case SPA_AUDIO_FORMAT_F64_LE: + case SPA_AUDIO_FORMAT_F64_BE: + if (sizeof(double) != 8) + return NULL; + return sf_playback_fill_f64; +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + case SPA_AUDIO_FORMAT_ENCODED: + return encoded_playback_fill; +#endif + default: + break; + } + return NULL; +} + +static int sf_record_fill_x8(struct data *d, void *src, unsigned int n_frames) +{ + sf_count_t rn; + + rn = sf_write_raw(d->file, src, n_frames * d->stride); + return (int)rn / d->stride; +} + +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 +record_fill_fn(uint32_t fmt) +{ + switch (fmt) { + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_ULAW: + case SPA_AUDIO_FORMAT_ALAW: + return sf_record_fill_x8; + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_S16_BE: + /* sndfile check */ + if (sizeof(int16_t) != sizeof(short)) + return NULL; + return sf_record_fill_s16; + case SPA_AUDIO_FORMAT_S32_LE: + case SPA_AUDIO_FORMAT_S32_BE: + /* sndfile check */ + if (sizeof(int32_t) != sizeof(int)) + return NULL; + return sf_record_fill_s32; + case SPA_AUDIO_FORMAT_F32_LE: + case SPA_AUDIO_FORMAT_F32_BE: + /* sndfile check */ + if (sizeof(float) != 4) + return NULL; + return sf_record_fill_f32; + case SPA_AUDIO_FORMAT_F64_LE: + case SPA_AUDIO_FORMAT_F64_BE: + /* sndfile check */ + if (sizeof(double) != 8) + return NULL; + return sf_record_fill_f64; + default: + break; + } + return NULL; +} + +static int channelmap_from_sf(struct channelmap *map) +{ + static const enum spa_audio_channel table[] = { + [SF_CHANNEL_MAP_MONO] = SPA_AUDIO_CHANNEL_MONO, + [SF_CHANNEL_MAP_LEFT] = SPA_AUDIO_CHANNEL_FL, /* libsndfile distinguishes left and front-left, which we don't */ + [SF_CHANNEL_MAP_RIGHT] = SPA_AUDIO_CHANNEL_FR, + [SF_CHANNEL_MAP_CENTER] = SPA_AUDIO_CHANNEL_FC, + [SF_CHANNEL_MAP_FRONT_LEFT] = SPA_AUDIO_CHANNEL_FL, + [SF_CHANNEL_MAP_FRONT_RIGHT] = SPA_AUDIO_CHANNEL_FR, + [SF_CHANNEL_MAP_FRONT_CENTER] = SPA_AUDIO_CHANNEL_FC, + [SF_CHANNEL_MAP_REAR_CENTER] = SPA_AUDIO_CHANNEL_RC, + [SF_CHANNEL_MAP_REAR_LEFT] = SPA_AUDIO_CHANNEL_RL, + [SF_CHANNEL_MAP_REAR_RIGHT] = SPA_AUDIO_CHANNEL_RR, + [SF_CHANNEL_MAP_LFE] = SPA_AUDIO_CHANNEL_LFE, + [SF_CHANNEL_MAP_FRONT_LEFT_OF_CENTER] = SPA_AUDIO_CHANNEL_FLC, + [SF_CHANNEL_MAP_FRONT_RIGHT_OF_CENTER] = SPA_AUDIO_CHANNEL_FRC, + [SF_CHANNEL_MAP_SIDE_LEFT] = SPA_AUDIO_CHANNEL_SL, + [SF_CHANNEL_MAP_SIDE_RIGHT] = SPA_AUDIO_CHANNEL_SR, + [SF_CHANNEL_MAP_TOP_CENTER] = SPA_AUDIO_CHANNEL_TC, + [SF_CHANNEL_MAP_TOP_FRONT_LEFT] = SPA_AUDIO_CHANNEL_TFL, + [SF_CHANNEL_MAP_TOP_FRONT_RIGHT] = SPA_AUDIO_CHANNEL_TFR, + [SF_CHANNEL_MAP_TOP_FRONT_CENTER] = SPA_AUDIO_CHANNEL_TFC, + [SF_CHANNEL_MAP_TOP_REAR_LEFT] = SPA_AUDIO_CHANNEL_TRL, + [SF_CHANNEL_MAP_TOP_REAR_RIGHT] = SPA_AUDIO_CHANNEL_TRR, + [SF_CHANNEL_MAP_TOP_REAR_CENTER] = SPA_AUDIO_CHANNEL_TRC + }; + int i; + + for (i = 0; i < map->n_channels; i++) { + if (map->channels[i] >= 0 && map->channels[i] < (int) SPA_N_ELEMENTS(table)) + map->channels[i] = table[map->channels[i]]; + else + map->channels[i] = SPA_AUDIO_CHANNEL_UNKNOWN; + } + return 0; +} +struct mapping { + const char *name; + unsigned int channels; + unsigned int values[32]; +}; + +static const struct mapping maps[] = +{ + { "mono", SPA_AUDIO_LAYOUT_Mono }, + { "stereo", SPA_AUDIO_LAYOUT_Stereo }, + { "surround-21", SPA_AUDIO_LAYOUT_2_1 }, + { "quad", SPA_AUDIO_LAYOUT_Quad }, + { "surround-22", SPA_AUDIO_LAYOUT_2_2 }, + { "surround-40", SPA_AUDIO_LAYOUT_4_0 }, + { "surround-31", SPA_AUDIO_LAYOUT_3_1 }, + { "surround-41", SPA_AUDIO_LAYOUT_4_1 }, + { "surround-50", SPA_AUDIO_LAYOUT_5_0 }, + { "surround-51", SPA_AUDIO_LAYOUT_5_1 }, + { "surround-51r", SPA_AUDIO_LAYOUT_5_1R }, + { "surround-70", SPA_AUDIO_LAYOUT_7_0 }, + { "surround-71", SPA_AUDIO_LAYOUT_7_1 }, +}; + +static unsigned int find_channel(const char *name) +{ + int i; + + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static int parse_channelmap(const char *channel_map, struct channelmap *map) +{ + int i, nch; + char **ch; + + SPA_FOR_EACH_ELEMENT_VAR(maps, m) { + if (spa_streq(m->name, channel_map)) { + map->n_channels = m->channels; + spa_memcpy(map->channels, &m->values, + map->n_channels * sizeof(unsigned int)); + return 0; + } + } + + ch = pw_split_strv(channel_map, ",", SPA_AUDIO_MAX_CHANNELS, &nch); + if (ch == NULL) + return -1; + + map->n_channels = nch; + for (i = 0; i < map->n_channels; i++) { + int c = find_channel(ch[i]); + map->channels[i] = c; + } + pw_free_strv(ch); + return 0; +} + +static int channelmap_default(struct channelmap *map, int n_channels) +{ + switch(n_channels) { + case 1: + parse_channelmap("mono", map); + break; + case 2: + parse_channelmap("stereo", map); + break; + case 3: + parse_channelmap("surround-21", map); + break; + case 4: + parse_channelmap("quad", map); + break; + case 5: + parse_channelmap("surround-50", map); + break; + case 6: + parse_channelmap("surround-51", map); + break; + case 7: + parse_channelmap("surround-70", map); + break; + case 8: + parse_channelmap("surround-71", map); + break; + default: + n_channels = 0; + break; + } + map->n_channels = n_channels; + return 0; +} + +static void channelmap_print(struct channelmap *map) +{ + int i; + + for (i = 0; i < map->n_channels; i++) { + const char *name = spa_debug_type_find_name(spa_type_audio_channel, map->channels[i]); + if (name == NULL) + name = ":UNK"; + printf("%s%s", spa_debug_type_short_name(name), i + 1 < map->n_channels ? "," : ""); + } +} + +static void on_core_info(void *userdata, const struct pw_core_info *info) +{ + struct data *data = userdata; + + if (data->verbose) + printf("remote %"PRIu32" is named \"%s\"\n", + info->id, info->name); +} + +static void on_core_error(void *userdata, uint32_t id, int seq, int res, const char *message) +{ + struct data *data = userdata; + + fprintf(stderr, "remote error: id=%"PRIu32" seq:%d res:%d (%s): %s\n", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) + pw_main_loop_quit(data->loop); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .info = on_core_info, + .error = on_core_error, +}; + +static void +on_state_changed(void *userdata, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct data *data = userdata; + int ret; + + if (data->verbose) + printf("stream state changed %s -> %s\n", + pw_stream_state_as_string(old), + pw_stream_state_as_string(state)); + + switch (state) { + case PW_STREAM_STATE_STREAMING: + if (!data->volume_is_set) { + ret = pw_stream_set_control(data->stream, + SPA_PROP_volume, 1, &data->volume, + 0); + if (data->verbose) + printf("stream set volume to %.3f - %s\n", data->volume, + ret == 0 ? "success" : "FAILED"); + + data->volume_is_set = true; + } + if (data->verbose) { + struct timespec timeout = {0, 1}, interval = {1, 0}; + struct pw_loop *l = pw_main_loop_get_loop(data->loop); + pw_loop_update_timer(l, data->timer, &timeout, &interval, false); + printf("stream node %"PRIu32"\n", + pw_stream_get_node_id(data->stream)); + } + break; + case PW_STREAM_STATE_PAUSED: + if (data->verbose) { + struct timespec timeout = {0, 0}, interval = {0, 0}; + struct pw_loop *l = pw_main_loop_get_loop(data->loop); + pw_loop_update_timer(l, data->timer, &timeout, &interval, false); + } + break; + case PW_STREAM_STATE_ERROR: + printf("stream node %"PRIu32" error: %s\n", + pw_stream_get_node_id(data->stream), + error); + pw_main_loop_quit(data->loop); + break; + default: + break; + } +} + +static void +on_io_changed(void *userdata, uint32_t id, void *data, uint32_t size) +{ + struct data *d = userdata; + + switch (id) { + case SPA_IO_Position: + d->position = data; + break; + default: + break; + } +} + +static void +on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) +{ + struct data *data = userdata; + struct spa_audio_info info = { 0 }; + int err; + + if (data->verbose) + printf("stream param change: %s\n", + spa_debug_type_find_name(spa_type_param, id)); + + if (id != SPA_PARAM_Format || param == NULL) + return; + + if ((err = spa_format_parse(param, &info.media_type, &info.media_subtype)) < 0) + return; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_dsd) + return; + + if (spa_format_audio_dsd_parse(param, &info.info.dsd) < 0) + return; + + data->dsf.layout.interleave = info.info.dsd.interleave, + data->dsf.layout.channels = info.info.dsd.channels; + data->dsf.layout.lsb = info.info.dsd.bitorder == SPA_PARAM_BITORDER_lsb; + + data->stride = data->dsf.layout.channels * SPA_ABS(data->dsf.layout.interleave); + + if (data->verbose) { + printf("DSD: channels:%d bitorder:%s interleave:%d stride:%d\n", + data->dsf.layout.channels, + data->dsf.layout.lsb ? "lsb" : "msb", + data->dsf.layout.interleave, + data->stride); + } +} + +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_frames = SPA_MIN(n_frames, (int)b->requested); + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + n_fill_frames = data->fill(data, p, n_frames); + + if (n_fill_frames > 0 || n_frames == 0) { + d->chunk->offset = 0; + if (data->data_type == TYPE_ENCODED) { + d->chunk->stride = 0; + // encoded_playback_fill returns number of bytes + // read and not number of frames like other + // functions for raw audio. + d->chunk->size = n_fill_frames; + b->size = n_fill_frames; + } else { + d->chunk->stride = data->stride; + d->chunk->size = n_fill_frames * data->stride; + b->size = n_frames; + } + have_data = true; + } else if (n_fill_frames < 0) { + fprintf(stderr, "fill error %d\n", n_fill_frames); + } else { + if (data->verbose) + printf("drain start\n"); + } +#else + n_fill_frames = data->fill(data, p, n_frames); + + if (n_fill_frames > 0 || n_frames == 0) { + d->chunk->offset = 0; + d->chunk->stride = data->stride; + d->chunk->size = n_fill_frames * data->stride; + have_data = true; + b->size = n_frames; + } else if (n_fill_frames < 0) { + fprintf(stderr, "fill error %d\n", n_fill_frames); + } else { + if (data->verbose) + printf("drain start\n"); + } +#endif + } 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, + .io_changed = on_io_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); +} + +static void do_print_delay(void *userdata, uint64_t expirations) +{ + struct data *data = userdata; + struct pw_time time; + pw_stream_get_time_n(data->stream, &time, sizeof(time)); + printf("stream time: now:%"PRIi64" rate:%u/%u ticks:%"PRIu64 + " delay:%"PRIi64" queued:%"PRIu64 + " buffered:%"PRIi64" buffers:%u avail:%u\n", + time.now, + time.rate.num, time.rate.denom, + time.ticks, time.delay, time.queued, time.buffered, + time.queued_buffers, time.avail_buffers); +} + +enum { + OPT_VERSION = 1000, + OPT_MEDIA_TYPE, + OPT_MEDIA_CATEGORY, + OPT_MEDIA_ROLE, + OPT_TARGET, + OPT_LATENCY, + OPT_RATE, + OPT_CHANNELS, + OPT_CHANNELMAP, + OPT_FORMAT, + OPT_VOLUME, +}; + +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, 'p' }, + { "midi", no_argument, NULL, 'm' }, + + { "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 }, + { "properties", required_argument, NULL, 'P' }, + + { "rate", required_argument, NULL, OPT_RATE }, + { "channels", required_argument, NULL, OPT_CHANNELS }, + { "channel-map", required_argument, NULL, OPT_CHANNELMAP }, + { "format", required_argument, NULL, OPT_FORMAT }, + { "volume", required_argument, NULL, OPT_VOLUME }, + { "quality", required_argument, NULL, 'q' }, + + { 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" + " -h, --help Show this help\n" + " --version Show version\n" + " -v, --verbose Enable verbose operations\n" + "\n"), name); + + 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 serial or name (default %s)\n" + " 0 means don't link\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" + " -P --properties Set node properties\n" + "\n"), + DEFAULT_MEDIA_TYPE, + DEFAULT_MEDIA_CATEGORY_PLAYBACK, + DEFAULT_MEDIA_ROLE, + DEFAULT_TARGET, DEFAULT_LATENCY_PLAY); + + fprintf(fp, + _(" --rate Sample rate (req. for rec) (default %u)\n" + " --channels Number of channels (req. for rec) (default %u)\n" + " --channel-map Channel map\n" + " one of: \"stereo\", \"surround-51\",... or\n" + " comma separated list of channel names: eg. \"FL,FR\"\n" + " --format Sample format %s (req. for rec) (default %s)\n" + " --volume Stream volume 0-1.0 (default %.3f)\n" + " -q --quality Resampler quality (0 - 15) (default %d)\n" + "\n"), + DEFAULT_RATE, + DEFAULT_CHANNELS, + STR_FMTS, DEFAULT_FORMAT, + DEFAULT_VOLUME, + DEFAULT_QUALITY); + + if (spa_streq(name, "pw-cat")) { + fputs( + _(" -p, --playback Playback mode\n" + " -r, --record Recording mode\n" + " -m, --midi Midi mode\n" + " -d, --dsd DSD mode\n" +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + " -o, --encoded Encoded mode\n" +#endif + "\n"), fp); + } +} + +static int midi_play(struct data *d, void *src, unsigned int n_frames) +{ + int res; + struct spa_pod_builder b; + struct spa_pod_frame f; + uint32_t first_frame, last_frame; + bool have_data = false; + + spa_zero(b); + spa_pod_builder_init(&b, src, n_frames); + + spa_pod_builder_push_sequence(&b, &f, 0); + + first_frame = d->clock_time; + last_frame = first_frame + d->position->clock.duration; + d->clock_time = last_frame; + + while (1) { + uint32_t frame; + struct midi_event ev; + + res = midi_file_next_time(d->midi.file, &ev.sec); + if (res <= 0) { + if (have_data) + break; + return res; + } + + frame = ev.sec * d->position->clock.rate.denom; + if (frame < first_frame) + frame = 0; + else if (frame < last_frame) + frame -= first_frame; + else + break; + + midi_file_read_event(d->midi.file, &ev); + + if (d->verbose) + midi_file_dump_event(stdout, &ev); + + if (ev.data[0] == 0xff) + continue; + + spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, ev.data, ev.size); + have_data = true; + } + spa_pod_builder_pop(&b, &f); + + return b.state.offset; +} + +static int midi_record(struct data *d, void *src, unsigned int n_frames) +{ + struct spa_pod *pod; + struct spa_pod_control *c; + uint32_t frame; + + frame = d->clock_time; + d->clock_time += d->position->clock.duration; + + if ((pod = spa_pod_from_data(src, n_frames, 0, n_frames)) == NULL) + return 0; + if (!spa_pod_is_sequence(pod)) + return 0; + + SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) { + struct midi_event ev; + + if (c->type != SPA_CONTROL_Midi) + continue; + + ev.track = 0; + ev.sec = (frame + c->offset) / (float) d->position->clock.rate.denom; + ev.data = SPA_POD_BODY(&c->value), + ev.size = SPA_POD_BODY_SIZE(&c->value); + + if (d->verbose) + midi_file_dump_event(stdout, &ev); + + midi_file_write_event(d->midi.file, &ev); + } + return 0; +} + +static int setup_midifile(struct data *data) +{ + if (data->mode == mode_record) { + spa_zero(data->midi.info); + data->midi.info.format = 0; + data->midi.info.ntracks = 1; + data->midi.info.division = 0; + } + + data->midi.file = midi_file_open(data->filename, + data->mode == mode_playback ? "r" : "w", + &data->midi.info); + if (data->midi.file == NULL) { + fprintf(stderr, "midifile: can't read midi file '%s': %m\n", data->filename); + return -errno; + } + + if (data->verbose) + printf("midifile: opened file \"%s\" format %08x ntracks:%d div:%d\n", + data->filename, + data->midi.info.format, data->midi.info.ntracks, + data->midi.info.division); + + data->fill = data->mode == mode_playback ? midi_play : midi_record; + data->stride = 1; + + return 0; +} + +struct dsd_layout_info { + uint32_t type; + struct spa_audio_layout_info info; +}; +static const struct dsd_layout_info dsd_layouts[] = { + { 1, { SPA_AUDIO_LAYOUT_Mono, }, }, + { 2, { SPA_AUDIO_LAYOUT_Stereo, }, }, + { 3, { SPA_AUDIO_LAYOUT_2FC }, }, + { 4, { SPA_AUDIO_LAYOUT_Quad }, }, + { 5, { SPA_AUDIO_LAYOUT_3_1 }, }, + { 6, { SPA_AUDIO_LAYOUT_5_0R }, }, + { 7, { SPA_AUDIO_LAYOUT_5_1R }, }, +}; + +static int dsf_play(struct data *d, void *src, unsigned int n_frames) +{ + return dsf_file_read(d->dsf.file, src, n_frames, &d->dsf.layout); +} + +static int setup_dsffile(struct data *data) +{ + if (data->mode == mode_record) + return -ENOTSUP; + + data->dsf.file = dsf_file_open(data->filename, "r", &data->dsf.info); + if (data->dsf.file == NULL) { + fprintf(stderr, "dsffile: can't read dsf file '%s': %m\n", data->filename); + return -errno; + } + + if (data->verbose) + printf("dsffile: opened file \"%s\" channels:%d rate:%d samples:%"PRIu64" bitorder:%s\n", + data->filename, + data->dsf.info.channels, data->dsf.info.rate, + data->dsf.info.samples, + data->dsf.info.lsb ? "lsb" : "msb"); + + data->fill = dsf_play; + + return 0; +} + +static int stdout_record(struct data *d, void *src, unsigned int n_frames) +{ + return fwrite(src, d->stride, n_frames, stdout); +} + +static int stdin_play(struct data *d, void *src, unsigned int n_frames) +{ + return fread(src, d->stride, n_frames, stdin); +} + +static int setup_pipe(struct data *data) +{ + const struct format_info *info; + + if (data->format == NULL) + data->format = DEFAULT_FORMAT; + if (data->channels == 0) + data->channels = DEFAULT_CHANNELS; + if (data->rate == 0) + data->rate = DEFAULT_RATE; + if (data->channelmap.n_channels == 0) + channelmap_default(&data->channelmap, data->channels); + + info = format_info_by_name(data->format); + if (info == NULL) + return -EINVAL; + + data->spa_format = info->spa_format; + data->stride = info->width * data->channels; + data->fill = data->mode == mode_playback ? stdin_play : stdout_record; + + if (data->verbose) + printf("PIPE: rate=%u channels=%u fmt=%s samplesize=%u stride=%u\n", + data->rate, data->channels, + info->name, info->width, data->stride); + + return 0; +} + +static int fill_properties(struct data *data) +{ + static const char * const table[] = { + [SF_STR_TITLE] = PW_KEY_MEDIA_TITLE, + [SF_STR_COPYRIGHT] = PW_KEY_MEDIA_COPYRIGHT, + [SF_STR_SOFTWARE] = PW_KEY_MEDIA_SOFTWARE, + [SF_STR_ARTIST] = PW_KEY_MEDIA_ARTIST, + [SF_STR_COMMENT] = PW_KEY_MEDIA_COMMENT, + [SF_STR_DATE] = PW_KEY_MEDIA_DATE + }; + + SF_INFO sfi; + SF_FORMAT_INFO fi; + int res; + unsigned c; + const char *s, *t; + + for (c = 0; c < SPA_N_ELEMENTS(table); c++) { + if (table[c] == NULL) + continue; + + if ((s = sf_get_string(data->file, c)) == NULL || + *s == '\0') + continue; + + pw_properties_set(data->props, table[c], s); + } + + spa_zero(sfi); + if ((res = sf_command(data->file, SFC_GET_CURRENT_SF_INFO, &sfi, sizeof(sfi)))) { + pw_log_error("sndfile: %s", sf_error_number(res)); + return -EIO; + } + + spa_zero(fi); + fi.format = sfi.format; + if (sf_command(data->file, SFC_GET_FORMAT_INFO, &fi, sizeof(fi)) == 0 && fi.name) + pw_properties_set(data->props, PW_KEY_MEDIA_FORMAT, fi.name); + + s = pw_properties_get(data->props, PW_KEY_MEDIA_TITLE); + t = pw_properties_get(data->props, PW_KEY_MEDIA_ARTIST); + if (s && t) + pw_properties_setf(data->props, PW_KEY_MEDIA_NAME, + "'%s' / '%s'", s, t); + + return 0; +} +static void format_from_filename(SF_INFO *info, const char *filename) +{ + int i, count = 0; + int format = -1; + +#if __BYTE_ORDER == __BIG_ENDIAN + info->format |= SF_ENDIAN_BIG; +#else + info->format |= SF_ENDIAN_LITTLE; +#endif + + if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0) + count = 0; + + for (i = 0; i < count; i++) { + SF_FORMAT_INFO fi; + + spa_zero(fi); + fi.format = i; + if (sf_command(NULL, SFC_GET_FORMAT_MAJOR, &fi, sizeof(fi)) != 0) + continue; + + if (spa_strendswith(filename, fi.extension)) { + format = fi.format; + break; + } + } + if (format == -1) + format = SF_FORMAT_WAV; + if (format == SF_FORMAT_WAV && info->channels > 2) + format = SF_FORMAT_WAVEX; + + info->format |= format; + + if (format == SF_FORMAT_OGG || format == SF_FORMAT_FLAC) + info->format = (info->format & ~SF_FORMAT_ENDMASK) | SF_ENDIAN_FILE; + if (format == SF_FORMAT_OGG) + info->format = (info->format & ~SF_FORMAT_SUBMASK) | SF_FORMAT_VORBIS; +} + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION +static int setup_encodedfile(struct data *data) +{ + int ret; + int bits_per_sample; + int num_channels; + char path[256] = { 0 }; + + /* We do not support record with encoded media */ + if (data->mode == mode_record) { + return -EINVAL; + } + + strcpy(path, "file:"); + strcat(path, data->filename); + + data->fmt_context = NULL; + ret = avformat_open_input(&data->fmt_context, path, NULL, NULL); + if (ret < 0) { + fprintf(stderr, "Failed to open input\n"); + return -EINVAL; + } + + avformat_find_stream_info (data->fmt_context, NULL); + + data->ctx = avcodec_alloc_context3(NULL); + if (!data->ctx) { + fprintf(stderr, "Could not allocate audio codec context\n"); + avformat_close_input(&data->fmt_context); + return -EINVAL; + } + + // We expect only one stream with audio + data->astream = data->fmt_context->streams[0]; + avcodec_parameters_to_context (data->ctx, data->astream->codecpar); + + if (data->ctx->codec_type != AVMEDIA_TYPE_AUDIO) { + fprintf(stderr, "Not an audio file\n"); + avformat_close_input(&data->fmt_context); + return -EINVAL; + } + + printf("Number of streams: %d Codec id: %x\n", data->fmt_context->nb_streams, + data->ctx->codec_id); + + /* FFmpeg 5.1 (which contains libavcodec 59.37.100) introduced + * a new channel layout API and deprecated the old one. */ +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 37, 100) + num_channels = data->ctx->ch_layout.nb_channels; +#else + num_channels = data->ctx->channels; +#endif + + data->rate = data->ctx->sample_rate; + data->channels = num_channels; + data->sfmt = data->ctx->sample_fmt; + data->stride = 1; // Don't care + + bits_per_sample = av_get_bits_per_sample(data->ctx->codec_id); + data->bitrate = bits_per_sample ? + data->ctx->sample_rate * num_channels * bits_per_sample : data->ctx->bit_rate; + + data->spa_format = SPA_AUDIO_FORMAT_ENCODED; + data->fill = playback_fill_fn(data->spa_format); + + if (data->verbose) + printf("Opened file \"%s\" sample format %08x channels:%d rate:%d bitrate: %d\n", + data->filename, data->ctx->sample_fmt, data->channels, + data->rate, data->bitrate); + + if (data->fill == NULL) { + fprintf(stderr, "Unhandled encoded format %d\n", data->spa_format); + avformat_close_input(&data->fmt_context); + return -EINVAL; + } + + avformat_close_input(&data->fmt_context); + + data->encoded_file = fopen(data->filename, "rb"); + if (!data->encoded_file) { + fprintf(stderr, "Failed to open file\n"); + return -EINVAL; + } + + return 0; +} +#endif + +static int setup_sndfile(struct data *data) +{ + const struct format_info *fi = NULL; + SF_INFO info; + + spa_zero(info); + /* for record, you fill in the info first */ + if (data->mode == mode_record) { + if (data->format == NULL) + data->format = DEFAULT_FORMAT; + if (data->channels == 0) + data->channels = DEFAULT_CHANNELS; + if (data->rate == 0) + data->rate = DEFAULT_RATE; + if (data->channelmap.n_channels == 0) + channelmap_default(&data->channelmap, data->channels); + + if ((fi = format_info_by_name(data->format)) == NULL) { + fprintf(stderr, "error: unknown format \"%s\"\n", data->format); + return -EINVAL; + } + memset(&info, 0, sizeof(info)); + info.samplerate = data->rate; + info.channels = data->channels; + info.format = fi->sf_format; + format_from_filename(&info, data->filename); + } + + data->file = sf_open(data->filename, + data->mode == mode_playback ? SFM_READ : SFM_WRITE, + &info); + if (!data->file) { + fprintf(stderr, "sndfile: failed to open audio file \"%s\": %s\n", + data->filename, sf_strerror(NULL)); + return -EIO; + } + + if (data->verbose) + printf("sndfile: opened file \"%s\" format %08x channels:%d rate:%d\n", + data->filename, info.format, info.channels, info.samplerate); + if (data->channels > 0 && info.channels != data->channels) { + fprintf(stderr, "sndfile: given channels (%u) don't match file channels (%d)\n", + data->channels, info.channels); + return -EINVAL; + } + + data->rate = info.samplerate; + data->channels = info.channels; + + if (data->mode == mode_playback) { + if (data->channelmap.n_channels == 0) { + bool def = false; + + if (sf_command(data->file, SFC_GET_CHANNEL_MAP_INFO, + data->channelmap.channels, + sizeof(data->channelmap.channels[0]) * data->channels)) { + data->channelmap.n_channels = data->channels; + if (channelmap_from_sf(&data->channelmap) < 0) + data->channelmap.n_channels = 0; + } + if (data->channelmap.n_channels == 0) { + channelmap_default(&data->channelmap, data->channels); + def = true; + } + if (data->verbose) { + printf("sndfile: using %s channel map: ", def ? "default" : "file"); + channelmap_print(&data->channelmap); + printf("\n"); + } + } + fill_properties(data); + + /* try native format first, else decode to float */ + if ((fi = format_info_by_sf_format(info.format)) == NULL) + fi = format_info_by_sf_format(SF_FORMAT_FLOAT); + + } + if (fi == NULL) + return -EIO; + + if (data->verbose) + printf("PCM: fmt:%s rate:%u channels:%u width:%u\n", + fi->name, data->rate, data->channels, fi->width); + + /* we read and write S24 as S32 with sndfile */ + if (fi->spa_format == SPA_AUDIO_FORMAT_S24) + fi = format_info_by_sf_format(SF_FORMAT_PCM_32); + + data->spa_format = fi->spa_format; + data->stride = fi->width * data->channels; + data->fill = data->mode == mode_playback ? + playback_fill_fn(data->spa_format) : + record_fill_fn(data->spa_format); + + if (data->fill == NULL) { + fprintf(stderr, "PCM: unhandled format %d\n", data->spa_format); + return -EINVAL; + } + return 0; +} + +static int setup_properties(struct data *data) +{ + const char *s; + unsigned int nom = 0; + + if (data->quality >= 0) + pw_properties_setf(data->props, "resample.quality", "%d", data->quality); + + if (data->rate) + pw_properties_setf(data->props, PW_KEY_NODE_RATE, "1/%u", data->rate); + + data->latency_unit = unit_none; + + s = data->latency; + while (*s && isdigit(*s)) + s++; + if (!*s) + data->latency_unit = unit_samples; + else if (spa_streq(s, "none")) + data->latency_unit = unit_none; + else if (spa_streq(s, "s") || spa_streq(s, "sec") || spa_streq(s, "secs")) + data->latency_unit = unit_sec; + else if (spa_streq(s, "ms") || spa_streq(s, "msec") || spa_streq(s, "msecs")) + data->latency_unit = unit_msec; + else if (spa_streq(s, "us") || spa_streq(s, "usec") || spa_streq(s, "usecs")) + data->latency_unit = unit_usec; + else if (spa_streq(s, "ns") || spa_streq(s, "nsec") || spa_streq(s, "nsecs")) + data->latency_unit = unit_nsec; + else { + fprintf(stderr, "error: bad latency value %s (bad unit)\n", data->latency); + return -EINVAL; + } + data->latency_value = atoi(data->latency); + if (!data->latency_value && data->latency_unit != unit_none) { + fprintf(stderr, "error: bad latency value %s (is zero)\n", data->latency); + return -EINVAL; + } + + 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:%d latency:%u (%.3fs)\n", + data->rate, nom, data->rate ? (double)nom/data->rate : 0.0f); + if (nom) + pw_properties_setf(data->props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data->rate); + + return 0; +} + +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, ret; + enum pw_stream_flags flags = 0; + + setlocale(LC_ALL, ""); + pw_init(&argc, &argv); + + flags |= PW_STREAM_FLAG_AUTOCONNECT; + + prog = argv[0]; + if ((prog = strrchr(argv[0], '/')) != NULL) + prog++; + else + prog = argv[0]; + + /* prime the mode from the program name */ + if (spa_streq(prog, "pw-play")) { + data.mode = mode_playback; + data.data_type = TYPE_PCM; + } else if (spa_streq(prog, "pw-record")) { + data.mode = mode_record; + data.data_type = TYPE_PCM; + } else if (spa_streq(prog, "pw-midiplay")) { + data.mode = mode_playback; + data.data_type = TYPE_MIDI; + } else if (spa_streq(prog, "pw-midirecord")) { + data.mode = mode_record; + data.data_type = TYPE_MIDI; + } else if (spa_streq(prog, "pw-dsdplay")) { + data.mode = mode_playback; + data.data_type = TYPE_DSD; + } else + data.mode = mode_none; + + /* negative means no volume adjustment */ + data.volume = -1.0; + data.quality = -1; + data.props = pw_properties_new( + PW_KEY_APP_NAME, prog, + PW_KEY_NODE_NAME, prog, + NULL); + + if (data.props == NULL) { + fprintf(stderr, "error: pw_properties_new() failed: %m\n"); + goto error_no_props; + } + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + while ((c = getopt_long(argc, argv, "hvprmdoR:q:P:", long_options, NULL)) != -1) { +#else + while ((c = getopt_long(argc, argv, "hvprmdR:q:P:", long_options, NULL)) != -1) { +#endif + + switch (c) { + + case 'h': + show_usage(prog, false); + return EXIT_SUCCESS; + + case OPT_VERSION: + printf("%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 'm': + data.data_type = TYPE_MIDI; + break; + + case 'd': + data.data_type = TYPE_DSD; + break; + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + case 'o': + data.data_type = TYPE_ENCODED; + break; +#endif + + case 'R': + data.remote_name = optarg; + break; + + case 'q': + data.quality = atoi(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 'P': + pw_properties_update_string(data.props, optarg, strlen(optarg)); + break; + + case OPT_TARGET: + data.target = optarg; + if (spa_streq(data.target, "0")) { + data.target = NULL; + flags &= ~PW_STREAM_FLAG_AUTOCONNECT; + } + 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_CHANNELMAP: + data.channel_map = optarg; + break; + + case OPT_FORMAT: + data.format = optarg; + break; + + case OPT_VOLUME: + data.volume = atof(optarg); + break; + default: + 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) { + switch (data.data_type) { + case TYPE_MIDI: + data.media_type = DEFAULT_MIDI_MEDIA_TYPE; + break; + default: + data.media_type = DEFAULT_MEDIA_TYPE; + break; + } + } + 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.latency) + data.latency = data.mode == mode_playback ? + DEFAULT_LATENCY_PLAY : + DEFAULT_LATENCY_REC; + if (data.channel_map != NULL) { + if (parse_channelmap(data.channel_map, &data.channelmap) < 0) { + fprintf(stderr, "error: can parse channel-map \"%s\"\n", data.channel_map); + goto error_usage; + + } else { + if (data.channels > 0 && data.channelmap.n_channels != data.channels) { + fprintf(stderr, "error: channels and channel-map incompatible\n"); + goto error_usage; + } + data.channels = data.channelmap.n_channels; + } + } + if (data.volume < 0) + data.volume = DEFAULT_VOLUME; + + if (optind >= argc) { + fprintf(stderr, "error: filename or - argument missing\n"); + goto error_usage; + } + data.filename = argv[optind++]; + + pw_properties_set(data.props, PW_KEY_MEDIA_TYPE, data.media_type); + pw_properties_set(data.props, PW_KEY_MEDIA_CATEGORY, data.media_category); + pw_properties_set(data.props, PW_KEY_MEDIA_ROLE, data.media_role); + pw_properties_set(data.props, PW_KEY_MEDIA_FILENAME, data.filename); + pw_properties_set(data.props, PW_KEY_MEDIA_NAME, data.filename); + pw_properties_set(data.props, PW_KEY_TARGET_OBJECT, data.target); + + /* 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: %m\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); + + data.context = pw_context_new(l, + pw_properties_new( + PW_KEY_CONFIG_NAME, "client-rt.conf", + NULL), + 0); + if (!data.context) { + fprintf(stderr, "error: pw_context_new() failed: %m\n"); + goto error_no_context; + } + + data.core = pw_context_connect(data.context, + pw_properties_new( + PW_KEY_REMOTE_NAME, data.remote_name, + NULL), + 0); + if (!data.core) { + fprintf(stderr, "error: pw_context_connect() failed: %m\n"); + goto error_ctx_connect_failed; + } + pw_core_add_listener(data.core, &data.core_listener, &core_events, &data); + + if (spa_streq(data.filename, "-")) { + ret = setup_pipe(&data); + } else { + switch (data.data_type) { + case TYPE_PCM: + ret = setup_sndfile(&data); + break; + case TYPE_MIDI: + ret = setup_midifile(&data); + break; + case TYPE_DSD: + ret = setup_dsffile(&data); + break; +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + case TYPE_ENCODED: + ret = setup_encodedfile(&data); + break; +#endif + default: + ret = -ENOTSUP; + break; + } + } + if (ret < 0) { + fprintf(stderr, "error: open failed: %s\n", spa_strerror(ret)); + switch (ret) { + case -EIO: + goto error_bad_file; + case -EINVAL: + default: + goto error_usage; + } + } + ret = setup_properties(&data); + + switch (data.data_type) { +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + case TYPE_ENCODED: + { + struct spa_audio_info info; + + spa_zero(info); + info.media_type = SPA_MEDIA_TYPE_audio; + + ret = avcodec_ctx_to_info(&data, data.ctx, &info); + if (ret < 0) { + if (data.encoded_file) { + fclose(data.encoded_file); + } + goto error_bad_file; + } + params[0] = spa_format_audio_build(&b, SPA_PARAM_EnumFormat, &info); + break; + } +#endif + case TYPE_PCM: + { + struct spa_audio_info_raw info; + info = SPA_AUDIO_INFO_RAW_INIT( + .flags = data.channelmap.n_channels ? 0 : SPA_AUDIO_FLAG_UNPOSITIONED, + .format = data.spa_format, + .rate = data.rate, + .channels = data.channels); + + if (data.channelmap.n_channels) + memcpy(info.position, data.channelmap.channels, data.channels * sizeof(int)); + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + break; + } + case TYPE_MIDI: + params[0] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + + pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); + break; + case TYPE_DSD: + { + struct spa_audio_info_dsd info; + + spa_zero(info); + info.channels = data.dsf.info.channels; + info.rate = data.dsf.info.rate / 8; + + SPA_FOR_EACH_ELEMENT_VAR(dsd_layouts, i) { + if (i->type != data.dsf.info.channel_type) + continue; + info.channels = i->info.n_channels; + memcpy(info.position, i->info.position, + info.channels * sizeof(uint32_t)); + } + params[0] = spa_format_audio_dsd_build(&b, SPA_PARAM_EnumFormat, &info); + break; + } + } + + data.stream = pw_stream_new(data.core, prog, data.props); + data.props = NULL; + + if (data.stream == NULL) { + fprintf(stderr, "error: failed to create stream: %m\n"); + goto error_no_stream; + } + pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data); + + if (data.verbose) + printf("connecting %s stream; target=%s\n", + data.mode == mode_playback ? "playback" : "record", + data.target); + + if (data.verbose) + data.timer = pw_loop_add_timer(l, do_print_delay, &data); + + ret = pw_stream_connect(data.stream, + data.mode == mode_playback ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT, + PW_ID_ANY, + flags | + PW_STREAM_FLAG_MAP_BUFFERS, + params, 1); + if (ret < 0) { + fprintf(stderr, "error: failed connect: %s\n", spa_strerror(ret)); + 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); + +#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION + if (data.encoded_file) + fclose(data.encoded_file); +#endif + + /* we're returning OK only if got to the point to drain */ + if (data.drained) + exit_code = EXIT_SUCCESS; + +error_connect_fail: + if (data.stream) { + spa_hook_remove(&data.stream_listener); + pw_stream_destroy(data.stream); + } +error_no_stream: +error_bad_file: + spa_hook_remove(&data.core_listener); + pw_core_disconnect(data.core); +error_ctx_connect_failed: + pw_context_destroy(data.context); +error_no_context: + pw_main_loop_destroy(data.loop); +error_no_props: +error_no_main_loop: + pw_properties_free(data.props); + if (data.file) + sf_close(data.file); + if (data.midi.file) + midi_file_close(data.midi.file); + pw_deinit(); + return exit_code; + +error_usage: + show_usage(prog, true); + return EXIT_FAILURE; +} |