pw-cat: add a container option and some --list options

Add a container option to override the extension check and force a
container when saving.

Add some more formats that are supported by libsndfile.

Add some options to list all supported formats, extensions/containers,
layouts and channel names.

Fixes #5117
This commit is contained in:
Wim Taymans 2026-02-09 13:44:40 +01:00
parent 0470f96887
commit a2df282086
2 changed files with 188 additions and 29 deletions

View file

@ -24,9 +24,9 @@ Play and record media with PipeWire
**pw-cat** is a simple tool for playing back or capturing raw or encoded
media files on a PipeWire server. It understands all audio file formats
supported by `libsndfile` for PCM capture and playback. When capturing
PCM, the filename extension is used to guess the file format with the
WAV file format as the default.
supported by `libsndfile` for PCM capture and playback. When no container
is specified for capturing PCM, the filename extension is used to guess
the file format with the WAV file format as the default.
It understands standard MIDI files and MIDI 2.0 clip files for playback
and recording. This tool will not render MIDI files, it will simply make
@ -37,8 +37,15 @@ DSD playback is supported with the DSF file format. This tool will only
work with native DSD capable hardware and will produce an error when no
such hardware was found.
When the *FILE* is - input and output will be raw data from STDIN and
STDOUT respectively.
When the *FILE* is - input will be from STDIN. If no format is specified,
libsndfile will attempt to parse and stream the format from STDIN. For
some formats, this is not possible and libsndfile will give an error.
Raw, MIDI and DSD formats are all streamable from STDIN.
When the *FILE* is - output will be to STDOUT. If no format is specified,
libsndfile is instructed to output the .au format, which is streamble and
preserves the format, rate and channels.
Raw and DSD formats are all streamable to STDOUT.
# OPTIONS
@ -87,6 +94,11 @@ DSD mode. *FILE* is a DSF file. If the tool is called under the name
render the DSD audio. You need a DSD capable device to play DSD content
or this program will exit with an error.
\par -s | \--sysex
SysEx mode. *FILE* is a File that contains a raw SysEx MIDI message.
If the tool is called under the name **pw-sysex** this is the default.
The File is read and sent as a MIDI control message into the graph.
\par \--media-type=VALUE
Set the media type property (default Audio/Midi depending on mode). The
media type is used by the session manager to select a suitable target to
@ -138,6 +150,17 @@ does not match the samplerate of the server, the data will be resampled.
Higher quality uses more CPU. Values between 0 and 15 are allowed, the
default quality is 4.
\par -a | \--raw
Raw samples will be read or written. The \--rate, \--format, \--channels
and \--channelmap can be used to specify the raw format.
\par -M | \--force-midi
Force midi format, one of "midi" or "ump", (default ump).
When reading or writing midi, for one of midi or UMP.
\par -n | \--sample-count=COUNT
Stop after COUNT samples.
\par \--rate=VALUE
The sample rate, default 48000.
@ -145,19 +168,38 @@ The sample rate, default 48000.
The number of channels, default 2.
\par \--channel-map=VALUE
The channelmap. Possible values include: **mono**, **stereo**,
The channelmap. Possible values include are either a channel layout
such as **mono**, **stereo**,
**surround-21**, **quad**, **surround-22**, **surround-40**,
**surround-31**, **surround-41**, **surround-50**, **surround-51**,
**surround-51r**, **surround-70**, **surround-71** or a comma separated
list of channel names: **FL**, **FR**, **FC**, **LFE**, **SL**, **SR**,
**FLC**, **FRC**, **RC**, **RL**, **RR**, **TC**, **TFL**, **TFC**,
**TFR**, **TRL**, **TRC**, **TRR**, **RLC**, **RRC**, **FLW**, **FRW**,
**LFE2**, **FLH**, **FCH**, **FRH**, **TFLC**, **TFRC**, **TSL**,
**TSR**, **LLFR**, **RLFE**, **BC**, **BLC**, **BRC**
or comma separated array of channel names such as **FL,FR**.
See \--list-layouts and \--list-channel-names to get a complete
list of possible values.
\par \--list-layouts
List all known channel layouts. One of these can be used as the
\--channel-map value.
\par \--list-channel-names
List all known channel names. An array of these can be used as the
\--channel-map value.
\par \--format=VALUE
The sample format to use. One of: **u8**, **s8**, **s16** (default),
**s24**, **s32**, **f32**, **f64**.
The sample format to use. Some possible values include: **u8**, **s8**,
**s16** (default), **s24**, **s32**, **f32**, **f64**. See
\--list-formats to get a complete list of values.
\par \--list-formats
List all known format values.
\par \--container=VALUE
Specify the container to use when saving. This is usually guessed from
the filename extension but can be specified explicitly. When using
STDOUT and no container is specified, the AU container will be used.
Then using a filename and the container was not specified and it could
not be derived from the filename, the WAV container is used.
\par \--list-containers
List all known container values.
\par \--volume=VALUE
The stream volume, default 1.000. Depending on the locale you have

View file

@ -120,6 +120,7 @@ struct data {
const char *media_role;
const char *channel_map;
const char *format;
const char *container;
const char *target;
const char *latency;
struct pw_properties *props;
@ -194,8 +195,6 @@ struct data {
uint64_t samples_processed;
};
#define STR_FMTS "(ulaw|alaw|u8|s8|s16|s32|f32|f64)"
static const struct format_info {
const char *name;
int sf_format;
@ -217,6 +216,29 @@ static const struct format_info {
{ "mp3", SF_FORMAT_MPEG_LAYER_III, SPA_AUDIO_FORMAT_F32, 1 },
{ "vorbis", SF_FORMAT_VORBIS, SPA_AUDIO_FORMAT_F32, 1 },
{ "opus", SF_FORMAT_OPUS, SPA_AUDIO_FORMAT_F32, 1 },
{ "ima-adpcm", SF_FORMAT_IMA_ADPCM, SPA_AUDIO_FORMAT_F32, 1 },
{ "ms-adpcm", SF_FORMAT_MS_ADPCM, SPA_AUDIO_FORMAT_F32, 1 },
{ "nms-adpcm-16", SF_FORMAT_NMS_ADPCM_16, SPA_AUDIO_FORMAT_F32, 1 },
{ "nms-adpcm-24", SF_FORMAT_NMS_ADPCM_24, SPA_AUDIO_FORMAT_F32, 1 },
{ "nms-adpcm-32", SF_FORMAT_NMS_ADPCM_32, SPA_AUDIO_FORMAT_F32, 1 },
{ "alac-16", SF_FORMAT_ALAC_16, SPA_AUDIO_FORMAT_F32, 1 },
{ "alac-20", SF_FORMAT_ALAC_20, SPA_AUDIO_FORMAT_F32, 1 },
{ "alac-24", SF_FORMAT_ALAC_24, SPA_AUDIO_FORMAT_F32, 1 },
{ "alac-32", SF_FORMAT_ALAC_32, SPA_AUDIO_FORMAT_F32, 1 },
{ "gsm610", SF_FORMAT_GSM610, SPA_AUDIO_FORMAT_F32, 1 },
{ "g721-32", SF_FORMAT_G721_32, SPA_AUDIO_FORMAT_F32, 1 },
{ "g723-24", SF_FORMAT_G723_24, SPA_AUDIO_FORMAT_F32, 1 },
{ "g723-40", SF_FORMAT_G723_40, SPA_AUDIO_FORMAT_F32, 1 },
{ "dwvw-12", SF_FORMAT_DWVW_12, SPA_AUDIO_FORMAT_F32, 1 },
{ "dwvw-16", SF_FORMAT_DWVW_16, SPA_AUDIO_FORMAT_F32, 1 },
{ "dwvw-24", SF_FORMAT_DWVW_24, SPA_AUDIO_FORMAT_F32, 1 },
{ "vox", SF_FORMAT_VOX_ADPCM, SPA_AUDIO_FORMAT_F32, 1 },
{ "dpcm-16", SF_FORMAT_DPCM_16, SPA_AUDIO_FORMAT_F32, 1 },
{ "dpcm-8", SF_FORMAT_DPCM_8, SPA_AUDIO_FORMAT_F32, 1 },
};
static const struct format_info *format_info_by_name(const char *str)
@ -236,6 +258,14 @@ static const struct format_info *format_info_by_sf_format(int format)
return NULL;
}
static void list_formats(struct data *d)
{
fprintf(stdout, _("Supported formats:\n"));
SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
fprintf(stdout, " %s\n", i->name);
}
static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames, bool *null_frame)
{
sf_count_t rn;
@ -714,6 +744,34 @@ static int parse_channelmap(const char *channel_map, struct spa_audio_layout_inf
return 0;
}
static void list_layouts(struct data *d)
{
fprintf(stderr, _("Supported channel layouts:\n"));
SPA_FOR_EACH_ELEMENT_VAR(spa_type_audio_layout_info, i) {
if (i->name == NULL)
break;
fprintf(stdout, " %s: [", i->name);
for (uint32_t j = 0; j < i->layout.n_channels; j++)
fprintf(stdout, "%s%s", j == 0 ? " " : ", ",
spa_type_audio_channel_to_short_name(i->layout.position[j]));
fprintf(stdout, " ]\n");
}
fprintf(stderr, _("Supported channel layout aliases:\n"));
SPA_FOR_EACH_ELEMENT_VAR(maps, m)
fprintf(stdout, _(" %s -> %s\n"), m->name, m->alias);
}
static void list_channel_names(struct data *d)
{
fprintf(stderr, _("Supported channel names:\n"));
SPA_FOR_EACH_ELEMENT_VAR(spa_type_audio_channel, i) {
if (i->name == NULL || SPA_AUDIO_CHANNEL_IS_AUX(i->type))
break;
fprintf(stdout, " %s\n", spa_type_short_name(i->name));
}
fprintf(stderr, " AUX0 ... AUX4095\n");
}
static int channelmap_default(struct spa_audio_layout_info *map, int n_channels)
{
switch(n_channels) {
@ -1054,6 +1112,11 @@ enum {
OPT_CHANNELMAP,
OPT_FORMAT,
OPT_VOLUME,
OPT_CONTAINER,
OPT_LISTFORMATS,
OPT_LISTCONTAINERS,
OPT_LISTLAYOUTS,
OPT_LISTCHANNELNAMES,
};
#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
@ -1088,13 +1151,18 @@ static const struct option long_options[] = {
{ "rate", required_argument, NULL, OPT_RATE },
{ "channels", required_argument, NULL, OPT_CHANNELS },
{ "channel-map", required_argument, NULL, OPT_CHANNELMAP },
{ "list-layouts", no_argument, NULL, OPT_LISTLAYOUTS },
{ "list-channel-names", no_argument, NULL, OPT_LISTCHANNELNAMES },
{ "format", required_argument, NULL, OPT_FORMAT },
{ "list-formats", no_argument, NULL, OPT_LISTFORMATS },
{ "container", required_argument, NULL, OPT_CONTAINER },
{ "list-containers", no_argument, NULL, OPT_LISTCONTAINERS },
{ "volume", required_argument, NULL, OPT_VOLUME },
{ "quality", required_argument, NULL, 'q' },
{ "raw", no_argument, NULL, 'a' },
{ "raw", no_argument, NULL, 'a' },
{ "force-midi", required_argument, NULL, 'M' },
{ "sample-count", required_argument, NULL, 'n' },
{ "midi-clip", no_argument, NULL, 'c' },
{ "midi-clip", no_argument, NULL, 'c' },
{ NULL, 0, NULL, 0 }
};
@ -1131,12 +1199,17 @@ static void show_usage(const char *name, bool is_error)
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"
_(" --rate Sample rate (default %u)\n"
" --channels Number of channels (default %u)\n"
" --channel-map Channel map\n"
" one of: \"Stereo\", \"5.1\",... or\n"
" a channel layout: \"Stereo\", \"5.1\",... or\n"
" comma separated list of channel names: eg. \"FL,FR\"\n"
" --format Sample format %s (req. for rec) (default %s)\n"
" --list-layouts List supported channel layouts\n"
" --list-channel-names List supported channel maps\n"
" --format Sample format (default %s)\n"
" --list-formats List supported sample formats\n"
" --container Container format\n"
" --list-containers List supported containers and extensions\n"
" --volume Stream volume 0-1.0 (default %.3f)\n"
" -q --quality Resampler quality (0 - 15) (default %d)\n"
" -a, --raw RAW mode\n"
@ -1145,7 +1218,7 @@ static void show_usage(const char *name, bool is_error)
"\n"),
DEFAULT_RATE,
DEFAULT_CHANNELS,
STR_FMTS, DEFAULT_FORMAT,
DEFAULT_FORMAT,
DEFAULT_VOLUME,
DEFAULT_QUALITY);
@ -1679,11 +1752,20 @@ static int fill_properties(struct data *data)
return 0;
}
static void format_from_filename(SF_INFO *info, const char *filename)
static void format_from_filename(SF_INFO *info, const char *filename, const char *container)
{
int i, count = 0;
int format = -1;
const char *extension;
if (spa_streq(filename, "-"))
extension = container ? container : "au";
else if (container)
extension = container;
else
extension = filename;
fprintf(stderr, "%s\n", filename);
if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0)
count = 0;
@ -1695,7 +1777,7 @@ static void format_from_filename(SF_INFO *info, const char *filename)
if (sf_command(NULL, SFC_GET_FORMAT_MAJOR, &fi, sizeof(fi)) != 0)
continue;
if (spa_strendswith(filename, fi.extension)) {
if (spa_strendswith(extension, fi.extension)) {
format = fi.format;
break;
}
@ -1712,7 +1794,7 @@ static void format_from_filename(SF_INFO *info, const char *filename)
if (sf_command(NULL, SFC_GET_SIMPLE_FORMAT, &fi, sizeof(fi)) != 0)
continue;
if (spa_strendswith(filename, fi.extension)) {
if (spa_strendswith(extension, fi.extension)) {
format = fi.format;
info->format = 0;
break;
@ -1720,7 +1802,7 @@ static void format_from_filename(SF_INFO *info, const char *filename)
}
}
if (format == -1)
format = spa_streq(filename, "-") ? SF_FORMAT_AU : SF_FORMAT_WAV;
format = SF_FORMAT_WAV;
if (format == SF_FORMAT_WAV && info->channels > 2)
format = SF_FORMAT_WAVEX;
@ -1738,6 +1820,26 @@ static void format_from_filename(SF_INFO *info, const char *filename)
info->format |= format;
}
static void list_containers(struct data *d)
{
int i, count = 0;
fprintf(stderr, _("Supported containers and extensions:\n"));
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;
fprintf(stderr, " %s: %s\n", fi.extension, fi.name);
}
}
#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
static int setup_encodedfile(struct data *data)
{
@ -1859,7 +1961,7 @@ static int setup_sndfile(struct data *data)
info.samplerate = data->rate;
info.channels = data->channels;
info.format = fi->sf_format;
format_from_filename(&info, data->filename);
format_from_filename(&info, data->filename, data->container);
}
data->sndfile.file = sf_open(data->filename,
@ -2220,6 +2322,9 @@ int main(int argc, char *argv[])
case OPT_FORMAT:
data.format = optarg;
break;
case OPT_CONTAINER:
data.container = optarg;
break;
case OPT_VOLUME:
if (!spa_atof(optarg, &data.volume))
@ -2231,6 +2336,18 @@ int main(int argc, char *argv[])
case 'c':
data.data_type = TYPE_MIDI2;
break;
case OPT_LISTFORMATS:
list_formats(&data);
return EXIT_SUCCESS;
case OPT_LISTCONTAINERS:
list_containers(&data);
return EXIT_SUCCESS;
case OPT_LISTLAYOUTS:
list_layouts(&data);
return EXIT_SUCCESS;
case OPT_LISTCHANNELNAMES:
list_channel_names(&data);
return EXIT_SUCCESS;
default:
goto error_usage;
}