summaryrefslogtreecommitdiffstats
path: root/src/tools/pw-cat.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/tools/pw-cat.c1971
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;
+}