mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-04-18 06:46:28 -04:00
tools: add pw-audioconvert
Takes an input file, processes it with audioconvert and writes to an output file. Can be used to test all audioconvert features such as resample, channelmix, filter-graph, format conversion, dither, etc. Boilerplate written by Claude.
This commit is contained in:
parent
b4457b871f
commit
54aba261d2
5 changed files with 744 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ Manual pages:
|
||||||
|
|
||||||
- \subpage page_man_pipewire_1
|
- \subpage page_man_pipewire_1
|
||||||
- \subpage page_man_pipewire-pulse_1
|
- \subpage page_man_pipewire-pulse_1
|
||||||
|
- \subpage page_man_pw-audioconvert_1
|
||||||
- \subpage page_man_pw-cat_1
|
- \subpage page_man_pw-cat_1
|
||||||
- \subpage page_man_pw-cli_1
|
- \subpage page_man_pw-cli_1
|
||||||
- \subpage page_man_pw-config_1
|
- \subpage page_man_pw-config_1
|
||||||
|
|
|
||||||
62
doc/dox/programs/pw-audioconvert.1.md
Normal file
62
doc/dox/programs/pw-audioconvert.1.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
\page page_man_pw-audioconvert_1 pw-audioconvert
|
||||||
|
|
||||||
|
The PipeWire audioconvert utility
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
**pw-audioconvert** \[*OPTIONS*\] *INFILE* *OUTFILE*
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
Use the PipeWire audioconvert to convert input file to output file,
|
||||||
|
following the given options.
|
||||||
|
|
||||||
|
This is useful only for doing audio conversion but also apply effects
|
||||||
|
on the audio using a filter-graph.
|
||||||
|
|
||||||
|
It understands all audio file formats supported by `libsndfile` for input
|
||||||
|
and output. The filename extension is used to guess the output file
|
||||||
|
container and format with the WAV file format as the default.
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
\par -r RATE | \--rate=RATE
|
||||||
|
Output sample rate. Default the same as the input sample rate.
|
||||||
|
|
||||||
|
\par -f FORMAT | \--format=FORMAT
|
||||||
|
Output sample format (s8 | s16 | s32 | f32 | f64). Default the same
|
||||||
|
as the input format.
|
||||||
|
|
||||||
|
\par -b BLOCKSIZE | \--blocksize=BLOCKSIZE
|
||||||
|
Number of samples per iteration (default 4096)
|
||||||
|
|
||||||
|
\par -P PROPERTIES | \--properties=PROPERTIES
|
||||||
|
Set extra stream properties as a JSON object. One can also use @filename to
|
||||||
|
read the JSON object with properties from filename.
|
||||||
|
|
||||||
|
\par -c CHANNELS | \--channels=CHANNELS
|
||||||
|
The number of output channels, default the same as the input.
|
||||||
|
|
||||||
|
\par \--channel-map=VALUE
|
||||||
|
The channelmap. Possible values include are either a predefined channel layout
|
||||||
|
such as **Mono**, **Stereo**, **2.1**, **Quad**, **2.2**, **5.1**,
|
||||||
|
or comma separated array of channel names such as **FL,FR**.
|
||||||
|
|
||||||
|
\par -h
|
||||||
|
Show help.
|
||||||
|
|
||||||
|
\par -v
|
||||||
|
Verbose operation.
|
||||||
|
|
||||||
|
# EXAMPLES
|
||||||
|
|
||||||
|
**pw-audioconvert** -r 48000 -f s32 in.wav out.wav
|
||||||
|
|
||||||
|
# AUTHORS
|
||||||
|
|
||||||
|
The PipeWire Developers <$(PACKAGE_BUGREPORT)>;
|
||||||
|
PipeWire is available from <$(PACKAGE_URL)>
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
\ref page_man_pipewire_1 "pipewire(1)"
|
||||||
|
|
@ -100,6 +100,7 @@ manpage_docs = [
|
||||||
'dox/config/libpipewire-modules.7.md',
|
'dox/config/libpipewire-modules.7.md',
|
||||||
'dox/programs/pipewire-pulse.1.md',
|
'dox/programs/pipewire-pulse.1.md',
|
||||||
'dox/programs/pipewire.1.md',
|
'dox/programs/pipewire.1.md',
|
||||||
|
'dox/programs/pw-audioconvert.1.md',
|
||||||
'dox/programs/pw-cat.1.md',
|
'dox/programs/pw-cat.1.md',
|
||||||
'dox/programs/pw-cli.1.md',
|
'dox/programs/pw-cli.1.md',
|
||||||
'dox/programs/pw-config.1.md',
|
'dox/programs/pw-config.1.md',
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,14 @@ if build_pw_cat
|
||||||
summary({'Build pw-cat with FFmpeg integration': build_pw_cat_with_ffmpeg}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool')
|
summary({'Build pw-cat with FFmpeg integration': build_pw_cat_with_ffmpeg}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if sndfile_dep.found()
|
||||||
|
executable('pw-audioconvert',
|
||||||
|
'pw-audioconvert.c',
|
||||||
|
install: true,
|
||||||
|
dependencies : [pipewire_dep, sndfile_dep, mathlib],
|
||||||
|
)
|
||||||
|
endif
|
||||||
|
|
||||||
build_avb_virtual = get_option('avb-virtual').require(
|
build_avb_virtual = get_option('avb-virtual').require(
|
||||||
host_machine.system() == 'linux',
|
host_machine.system() == 'linux',
|
||||||
error_message: 'AVB support is only available on Linux'
|
error_message: 'AVB support is only available on Linux'
|
||||||
|
|
|
||||||
672
src/tools/pw-audioconvert.c
Normal file
672
src/tools/pw-audioconvert.c
Normal file
|
|
@ -0,0 +1,672 @@
|
||||||
|
/* PipeWire - pw-filter-graph */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */
|
||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
#include <sndfile.h>
|
||||||
|
|
||||||
|
#include <spa/param/audio/format-utils.h>
|
||||||
|
#include <spa/param/audio/layout-types.h>
|
||||||
|
#include <spa/param/audio/raw-json.h>
|
||||||
|
#include <spa/node/node.h>
|
||||||
|
#include <spa/node/io.h>
|
||||||
|
#include <spa/utils/names.h>
|
||||||
|
#include <spa/utils/string.h>
|
||||||
|
#include <spa/utils/result.h>
|
||||||
|
#include <spa/debug/file.h>
|
||||||
|
|
||||||
|
#include <pipewire/pipewire.h>
|
||||||
|
|
||||||
|
#define MAX_SAMPLES 4096u
|
||||||
|
|
||||||
|
enum {
|
||||||
|
OPT_CHANNELMAP = 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct data {
|
||||||
|
bool verbose;
|
||||||
|
int rate;
|
||||||
|
int format;
|
||||||
|
uint32_t blocksize;
|
||||||
|
int out_channels;
|
||||||
|
const char *channel_map;
|
||||||
|
struct pw_properties *props;
|
||||||
|
|
||||||
|
const char *iname;
|
||||||
|
SF_INFO iinfo;
|
||||||
|
SNDFILE *ifile;
|
||||||
|
|
||||||
|
const char *oname;
|
||||||
|
SF_INFO oinfo;
|
||||||
|
SNDFILE *ofile;
|
||||||
|
|
||||||
|
struct pw_main_loop *loop;
|
||||||
|
struct pw_context *context;
|
||||||
|
|
||||||
|
struct spa_handle *handle;
|
||||||
|
struct spa_node *node;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define STR_FMTS "(s8|s16|s32|f32|f64)"
|
||||||
|
|
||||||
|
#define OPTIONS "hvr:f:b:P:c:"
|
||||||
|
static const struct option long_options[] = {
|
||||||
|
{ "help", no_argument, NULL, 'h' },
|
||||||
|
{ "verbose", no_argument, NULL, 'v' },
|
||||||
|
|
||||||
|
{ "rate", required_argument, NULL, 'r' },
|
||||||
|
{ "format", required_argument, NULL, 'f' },
|
||||||
|
{ "blocksize", required_argument, NULL, 'b' },
|
||||||
|
{ "properties", required_argument, NULL, 'P' },
|
||||||
|
{ "channels", required_argument, NULL, 'c' },
|
||||||
|
{ "channel-map", required_argument, NULL, OPT_CHANNELMAP },
|
||||||
|
|
||||||
|
{ 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] <infile> <outfile>\n", name);
|
||||||
|
fprintf(fp,
|
||||||
|
" -h, --help Show this help\n"
|
||||||
|
" -v --verbose Be verbose\n"
|
||||||
|
"\n");
|
||||||
|
fprintf(fp,
|
||||||
|
" -r --rate Output sample rate (default as input)\n"
|
||||||
|
" -f --format Output sample format %s (default as input)\n"
|
||||||
|
" -b --blocksize Number of samples per iteration (default %u)\n"
|
||||||
|
" -P --properties Set node properties (optional)\n"
|
||||||
|
" Use @filename to read from file\n"
|
||||||
|
" -c --channels Output channel count\n"
|
||||||
|
" --channel-map Output channel layout (e.g. \"stereo\", \"5.1\",\n"
|
||||||
|
" \"FL,FR,FC,LFE,SL,SR\")\n",
|
||||||
|
STR_FMTS, MAX_SAMPLES);
|
||||||
|
fprintf(fp, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline const char *
|
||||||
|
sf_fmt_to_str(int fmt)
|
||||||
|
{
|
||||||
|
switch(fmt & SF_FORMAT_SUBMASK) {
|
||||||
|
case SF_FORMAT_PCM_S8:
|
||||||
|
return "s8";
|
||||||
|
case SF_FORMAT_PCM_16:
|
||||||
|
return "s16";
|
||||||
|
case SF_FORMAT_PCM_24:
|
||||||
|
return "s24";
|
||||||
|
case SF_FORMAT_PCM_32:
|
||||||
|
return "s32";
|
||||||
|
case SF_FORMAT_FLOAT:
|
||||||
|
return "f32";
|
||||||
|
case SF_FORMAT_DOUBLE:
|
||||||
|
return "f64";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
sf_str_to_fmt(const char *str)
|
||||||
|
{
|
||||||
|
if (!str)
|
||||||
|
return -1;
|
||||||
|
if (spa_streq(str, "s8"))
|
||||||
|
return SF_FORMAT_PCM_S8;
|
||||||
|
if (spa_streq(str, "s16"))
|
||||||
|
return SF_FORMAT_PCM_16;
|
||||||
|
if (spa_streq(str, "s24"))
|
||||||
|
return SF_FORMAT_PCM_24;
|
||||||
|
if (spa_streq(str, "s32"))
|
||||||
|
return SF_FORMAT_PCM_32;
|
||||||
|
if (spa_streq(str, "f32"))
|
||||||
|
return SF_FORMAT_FLOAT;
|
||||||
|
if (spa_streq(str, "f64"))
|
||||||
|
return SF_FORMAT_DOUBLE;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_channelmap(const char *channel_map, struct spa_audio_layout_info *map)
|
||||||
|
{
|
||||||
|
if (spa_audio_layout_info_parse_name(map, sizeof(*map), channel_map) >= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
spa_audio_parse_position_n(channel_map, strlen(channel_map),
|
||||||
|
map->position, SPA_N_ELEMENTS(map->position), &map->n_channels);
|
||||||
|
return map->n_channels > 0 ? 0 : -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int channelmap_default(struct spa_audio_layout_info *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("2.1", map); break;
|
||||||
|
case 4: parse_channelmap("Quad", map); break;
|
||||||
|
case 5: parse_channelmap("5.0", map); break;
|
||||||
|
case 6: parse_channelmap("5.1", map); break;
|
||||||
|
case 7: parse_channelmap("7.0", map); break;
|
||||||
|
case 8: parse_channelmap("7.1", map); break;
|
||||||
|
default: n_channels = 0; break;
|
||||||
|
}
|
||||||
|
map->n_channels = n_channels;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int open_input(struct data *d)
|
||||||
|
{
|
||||||
|
d->ifile = sf_open(d->iname, SFM_READ, &d->iinfo);
|
||||||
|
if (d->ifile == NULL) {
|
||||||
|
fprintf(stderr, "error: failed to open input file \"%s\": %s\n",
|
||||||
|
d->iname, sf_strerror(NULL));
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
if (d->verbose)
|
||||||
|
fprintf(stdout, "input '%s': channels:%d rate:%d format:%s\n",
|
||||||
|
d->iname, d->iinfo.channels, d->iinfo.samplerate,
|
||||||
|
sf_fmt_to_str(d->iinfo.format));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int open_output(struct data *d, int channels)
|
||||||
|
{
|
||||||
|
int i, count = 0, format = -1;
|
||||||
|
|
||||||
|
d->oinfo.channels = channels;
|
||||||
|
d->oinfo.samplerate = d->rate > 0 ? d->rate : d->iinfo.samplerate;
|
||||||
|
d->oinfo.format = d->format > 0 ? d->format : (d->iinfo.format & SF_FORMAT_SUBMASK);
|
||||||
|
|
||||||
|
/* try to guess the format from the extension */
|
||||||
|
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(d->oname, fi.extension)) {
|
||||||
|
format = fi.format;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (format == -1)
|
||||||
|
format = d->iinfo.format & ~SF_FORMAT_SUBMASK;
|
||||||
|
if (format == SF_FORMAT_WAV && d->oinfo.channels > 2)
|
||||||
|
format = SF_FORMAT_WAVEX;
|
||||||
|
|
||||||
|
d->oinfo.format |= format;
|
||||||
|
|
||||||
|
d->ofile = sf_open(d->oname, SFM_WRITE, &d->oinfo);
|
||||||
|
if (d->ofile == NULL) {
|
||||||
|
fprintf(stderr, "error: failed to open output file \"%s\": %s\n",
|
||||||
|
d->oname, sf_strerror(NULL));
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
sf_command(d->ofile, SFC_SET_CLIPPING, NULL, 1);
|
||||||
|
|
||||||
|
if (d->verbose)
|
||||||
|
fprintf(stdout, "output '%s': channels:%d rate:%d format:%s\n",
|
||||||
|
d->oname, d->oinfo.channels, d->oinfo.samplerate,
|
||||||
|
sf_fmt_to_str(d->oinfo.format));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int setup_convert_direction(struct spa_node *node,
|
||||||
|
enum spa_direction direction, struct spa_audio_info_raw *info)
|
||||||
|
{
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
uint8_t buffer[1024];
|
||||||
|
struct spa_pod *param, *format;
|
||||||
|
int res;
|
||||||
|
|
||||||
|
/* set port config to convert mode */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
param = spa_pod_builder_add_object(&b,
|
||||||
|
SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
|
||||||
|
SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(direction),
|
||||||
|
SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert));
|
||||||
|
|
||||||
|
res = spa_node_set_param(node, SPA_PARAM_PortConfig, 0, param);
|
||||||
|
if (res < 0)
|
||||||
|
return res;
|
||||||
|
|
||||||
|
/* set format on port 0 */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
format = spa_format_audio_raw_build(&b, SPA_PARAM_Format, info);
|
||||||
|
|
||||||
|
res = spa_node_port_set_param(node, direction, 0,
|
||||||
|
SPA_PARAM_Format, 0, format);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int do_filter(struct data *d)
|
||||||
|
{
|
||||||
|
void *iface;
|
||||||
|
struct spa_node *node;
|
||||||
|
int in_channels = d->iinfo.channels;
|
||||||
|
int out_channels;
|
||||||
|
int in_rate = d->iinfo.samplerate;
|
||||||
|
int out_rate = d->rate > 0 ? d->rate : in_rate;
|
||||||
|
uint32_t blocksize = d->blocksize > 0 ? d->blocksize : MAX_SAMPLES;
|
||||||
|
int res;
|
||||||
|
struct spa_audio_info_raw in_info, out_info;
|
||||||
|
struct spa_audio_layout_info in_layout, out_layout;
|
||||||
|
|
||||||
|
/* determine output channels */
|
||||||
|
out_channels = d->out_channels > 0 ? d->out_channels : in_channels;
|
||||||
|
|
||||||
|
/* set up input channel layout */
|
||||||
|
spa_zero(in_layout);
|
||||||
|
channelmap_default(&in_layout, in_channels);
|
||||||
|
|
||||||
|
/* set up output channel layout */
|
||||||
|
spa_zero(out_layout);
|
||||||
|
if (d->channel_map != NULL) {
|
||||||
|
if (parse_channelmap(d->channel_map, &out_layout) < 0) {
|
||||||
|
fprintf(stderr, "error: can't parse channel-map '%s'\n",
|
||||||
|
d->channel_map);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
if (d->out_channels > 0 &&
|
||||||
|
out_layout.n_channels != (uint32_t)d->out_channels) {
|
||||||
|
fprintf(stderr, "error: channel-map has %u channels "
|
||||||
|
"but -c specifies %d\n",
|
||||||
|
out_layout.n_channels, d->out_channels);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
out_channels = out_layout.n_channels;
|
||||||
|
} else {
|
||||||
|
channelmap_default(&out_layout, out_channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open the output file */
|
||||||
|
res = open_output(d, out_channels);
|
||||||
|
if (res < 0)
|
||||||
|
return res;
|
||||||
|
|
||||||
|
/* calculate output buffer size accounting for resampling */
|
||||||
|
uint32_t out_blocksize = (uint32_t)((uint64_t)blocksize *
|
||||||
|
out_rate / in_rate) + 64;
|
||||||
|
|
||||||
|
uint32_t quant_limit = SPA_ROUND_UP_N(SPA_MAX(out_blocksize, blocksize), 4096);
|
||||||
|
|
||||||
|
pw_properties_setf(d->props, "clock.quantum-limit", "%u", quant_limit);
|
||||||
|
pw_properties_set(d->props, "convert.direction", "output");
|
||||||
|
|
||||||
|
d->handle = pw_context_load_spa_handle(d->context,
|
||||||
|
SPA_NAME_AUDIO_CONVERT, &d->props->dict);
|
||||||
|
|
||||||
|
if (d->handle == NULL) {
|
||||||
|
fprintf(stderr, "can't load %s: %m\n", SPA_NAME_AUDIO_CONVERT);
|
||||||
|
return -errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = spa_handle_get_interface(d->handle,
|
||||||
|
SPA_TYPE_INTERFACE_Node, &iface);
|
||||||
|
if (res < 0 || iface == NULL) {
|
||||||
|
fprintf(stderr, "can't get Node interface: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
node = d->node = iface;
|
||||||
|
|
||||||
|
/* build input format: interleaved F32 */
|
||||||
|
spa_zero(in_info);
|
||||||
|
in_info.format = SPA_AUDIO_FORMAT_F32;
|
||||||
|
in_info.rate = in_rate;
|
||||||
|
in_info.channels = in_channels;
|
||||||
|
for (uint32_t i = 0; i < in_layout.n_channels &&
|
||||||
|
i < SPA_AUDIO_MAX_CHANNELS; i++)
|
||||||
|
in_info.position[i] = in_layout.position[i];
|
||||||
|
|
||||||
|
/* build output format: interleaved F32 */
|
||||||
|
spa_zero(out_info);
|
||||||
|
out_info.format = SPA_AUDIO_FORMAT_F32;
|
||||||
|
out_info.rate = out_rate;
|
||||||
|
out_info.channels = out_channels;
|
||||||
|
for (uint32_t i = 0; i < out_layout.n_channels &&
|
||||||
|
i < SPA_AUDIO_MAX_CHANNELS; i++)
|
||||||
|
out_info.position[i] = out_layout.position[i];
|
||||||
|
|
||||||
|
/* set up convert directions */
|
||||||
|
res = setup_convert_direction(node, SPA_DIRECTION_INPUT, &in_info);
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "can't set input format: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
res = setup_convert_direction(node, SPA_DIRECTION_OUTPUT, &out_info);
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "can't set output format: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* send Start command */
|
||||||
|
{
|
||||||
|
struct spa_command cmd =
|
||||||
|
SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
|
||||||
|
res = spa_node_send_command(node, &cmd);
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "can't start node: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d->verbose)
|
||||||
|
fprintf(stdout, "convert: in:%dch@%dHz -> out:%dch@%dHz "
|
||||||
|
"blocksize:%u\n",
|
||||||
|
in_channels, in_rate,
|
||||||
|
out_channels, out_rate, blocksize);
|
||||||
|
|
||||||
|
/* process audio */
|
||||||
|
{
|
||||||
|
float ibuf[blocksize * in_channels] SPA_ALIGNED(64);
|
||||||
|
float obuf[out_blocksize * out_channels] SPA_ALIGNED(64);
|
||||||
|
|
||||||
|
struct spa_chunk in_chunk, out_chunk;
|
||||||
|
struct spa_data in_sdata, out_sdata;
|
||||||
|
struct spa_buffer in_buffer, out_buffer;
|
||||||
|
struct spa_buffer *in_buffers[1] = { &in_buffer };
|
||||||
|
struct spa_buffer *out_buffers[1] = { &out_buffer };
|
||||||
|
struct spa_io_buffers in_io, out_io;
|
||||||
|
size_t read_total = 0, written_total = 0;
|
||||||
|
|
||||||
|
/* setup input buffer */
|
||||||
|
spa_zero(in_chunk);
|
||||||
|
spa_zero(in_sdata);
|
||||||
|
in_sdata.type = SPA_DATA_MemPtr;
|
||||||
|
in_sdata.flags = SPA_DATA_FLAG_READABLE;
|
||||||
|
in_sdata.fd = -1;
|
||||||
|
in_sdata.maxsize = sizeof(ibuf);
|
||||||
|
in_sdata.data = ibuf;
|
||||||
|
in_sdata.chunk = &in_chunk;
|
||||||
|
|
||||||
|
spa_zero(in_buffer);
|
||||||
|
in_buffer.datas = &in_sdata;
|
||||||
|
in_buffer.n_datas = 1;
|
||||||
|
|
||||||
|
res = spa_node_port_use_buffers(node,
|
||||||
|
SPA_DIRECTION_INPUT, 0, 0,
|
||||||
|
in_buffers, 1);
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "can't set input buffers: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
goto stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* setup output buffer */
|
||||||
|
spa_zero(out_chunk);
|
||||||
|
spa_zero(out_sdata);
|
||||||
|
out_sdata.type = SPA_DATA_MemPtr;
|
||||||
|
out_sdata.flags = SPA_DATA_FLAG_READWRITE;
|
||||||
|
out_sdata.fd = -1;
|
||||||
|
out_sdata.maxsize = sizeof(obuf);
|
||||||
|
out_sdata.data = obuf;
|
||||||
|
out_sdata.chunk = &out_chunk;
|
||||||
|
|
||||||
|
spa_zero(out_buffer);
|
||||||
|
out_buffer.datas = &out_sdata;
|
||||||
|
out_buffer.n_datas = 1;
|
||||||
|
|
||||||
|
res = spa_node_port_use_buffers(node,
|
||||||
|
SPA_DIRECTION_OUTPUT, 0, 0,
|
||||||
|
out_buffers, 1);
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "can't set output buffers: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
goto stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* setup IO */
|
||||||
|
res = spa_node_port_set_io(node, SPA_DIRECTION_INPUT, 0,
|
||||||
|
SPA_IO_Buffers, &in_io, sizeof(in_io));
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "can't set input IO: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
goto stop;
|
||||||
|
}
|
||||||
|
res = spa_node_port_set_io(node, SPA_DIRECTION_OUTPUT, 0,
|
||||||
|
SPA_IO_Buffers, &out_io, sizeof(out_io));
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "can't set output IO: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
goto stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* process loop */
|
||||||
|
while (true) {
|
||||||
|
sf_count_t n_read;
|
||||||
|
|
||||||
|
n_read = sf_readf_float(d->ifile, ibuf, blocksize);
|
||||||
|
|
||||||
|
read_total += n_read;
|
||||||
|
|
||||||
|
in_chunk.offset = 0;
|
||||||
|
in_chunk.size = n_read * in_channels * sizeof(float);
|
||||||
|
in_chunk.stride = 0;
|
||||||
|
|
||||||
|
out_chunk.offset = 0;
|
||||||
|
out_chunk.size = 0;
|
||||||
|
out_chunk.stride = 0;
|
||||||
|
|
||||||
|
in_io.status = n_read > 0 ? SPA_STATUS_HAVE_DATA : SPA_STATUS_DRAINED;
|
||||||
|
in_io.buffer_id = 0;
|
||||||
|
out_io.status = SPA_STATUS_NEED_DATA;
|
||||||
|
out_io.buffer_id = 0;
|
||||||
|
|
||||||
|
res = spa_node_process(node);
|
||||||
|
if (res < 0) {
|
||||||
|
fprintf(stderr, "process error: %s\n",
|
||||||
|
spa_strerror(res));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_io.status == SPA_STATUS_HAVE_DATA &&
|
||||||
|
out_io.buffer_id == 0) {
|
||||||
|
uint32_t out_frames = out_chunk.size /
|
||||||
|
(out_channels * sizeof(float));
|
||||||
|
if (out_frames > 0)
|
||||||
|
written_total += sf_writef_float(
|
||||||
|
d->ofile, obuf,
|
||||||
|
out_frames);
|
||||||
|
}
|
||||||
|
if (n_read == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (d->verbose)
|
||||||
|
fprintf(stdout, "read %zu samples, wrote %zu samples\n",
|
||||||
|
read_total, written_total);
|
||||||
|
}
|
||||||
|
|
||||||
|
res = 0;
|
||||||
|
|
||||||
|
stop:
|
||||||
|
{
|
||||||
|
struct spa_command cmd =
|
||||||
|
SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend);
|
||||||
|
spa_node_send_command(node, &cmd);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *read_file(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f;
|
||||||
|
long size;
|
||||||
|
char *buf;
|
||||||
|
|
||||||
|
f = fopen(path, "r");
|
||||||
|
if (f == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
size = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
|
||||||
|
buf = malloc(size + 1);
|
||||||
|
if (buf == NULL) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((long)fread(buf, 1, size, f) != size) {
|
||||||
|
free(buf);
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
buf[size] = '\0';
|
||||||
|
fclose(f);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
int c;
|
||||||
|
int longopt_index = 0, ret;
|
||||||
|
struct data data;
|
||||||
|
struct spa_error_location loc;
|
||||||
|
char *file_content = NULL, *str;
|
||||||
|
|
||||||
|
spa_zero(data);
|
||||||
|
data.props = pw_properties_new(NULL, NULL);
|
||||||
|
|
||||||
|
pw_init(&argc, &argv);
|
||||||
|
|
||||||
|
while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) {
|
||||||
|
switch (c) {
|
||||||
|
case 'h':
|
||||||
|
show_usage(argv[0], false);
|
||||||
|
ret = EXIT_SUCCESS;
|
||||||
|
goto done;
|
||||||
|
case 'v':
|
||||||
|
data.verbose = true;
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
ret = atoi(optarg);
|
||||||
|
if (ret <= 0) {
|
||||||
|
fprintf(stderr, "error: bad rate %s\n", optarg);
|
||||||
|
goto error_usage;
|
||||||
|
}
|
||||||
|
data.rate = ret;
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
ret = sf_str_to_fmt(optarg);
|
||||||
|
if (ret < 0) {
|
||||||
|
fprintf(stderr, "error: bad format %s\n", optarg);
|
||||||
|
goto error_usage;
|
||||||
|
}
|
||||||
|
data.format = ret;
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
ret = atoi(optarg);
|
||||||
|
if (ret <= 0) {
|
||||||
|
fprintf(stderr, "error: bad blocksize %s\n", optarg);
|
||||||
|
goto error_usage;
|
||||||
|
}
|
||||||
|
data.blocksize = ret;
|
||||||
|
break;
|
||||||
|
case 'P':
|
||||||
|
if (optarg[0] == '@') {
|
||||||
|
file_content = read_file(optarg + 1);
|
||||||
|
if (file_content == NULL) {
|
||||||
|
fprintf(stderr, "error: can't read graph file '%s': %m\n",
|
||||||
|
optarg + 1);
|
||||||
|
ret = EXIT_FAILURE;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
str = file_content;
|
||||||
|
} else {
|
||||||
|
str = optarg;
|
||||||
|
}
|
||||||
|
if (pw_properties_update_string_checked(data.props, str, strlen(str), &loc) < 0) {
|
||||||
|
spa_debug_file_error_location(stderr, &loc,
|
||||||
|
"error: syntax error in --properties: %s",
|
||||||
|
loc.reason);
|
||||||
|
goto error_usage;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'c':
|
||||||
|
ret = atoi(optarg);
|
||||||
|
if (ret <= 0) {
|
||||||
|
fprintf(stderr, "error: bad channel count %s\n", optarg);
|
||||||
|
goto error_usage;
|
||||||
|
}
|
||||||
|
data.out_channels = ret;
|
||||||
|
break;
|
||||||
|
case OPT_CHANNELMAP:
|
||||||
|
data.channel_map = optarg;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "error: unknown option '%c'\n", c);
|
||||||
|
goto error_usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (optind + 1 >= argc) {
|
||||||
|
fprintf(stderr, "error: filename arguments missing\n");
|
||||||
|
goto error_usage;
|
||||||
|
}
|
||||||
|
data.iname = argv[optind++];
|
||||||
|
data.oname = argv[optind++];
|
||||||
|
|
||||||
|
data.loop = pw_main_loop_new(NULL);
|
||||||
|
if (data.loop == NULL) {
|
||||||
|
fprintf(stderr, "error: can't create main loop: %m\n");
|
||||||
|
ret = EXIT_FAILURE;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
|
||||||
|
if (data.context == NULL) {
|
||||||
|
fprintf(stderr, "error: can't create context: %m\n");
|
||||||
|
ret = EXIT_FAILURE;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open_input(&data) < 0) {
|
||||||
|
ret = EXIT_FAILURE;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = do_filter(&data);
|
||||||
|
|
||||||
|
done:
|
||||||
|
if (data.ifile)
|
||||||
|
sf_close(data.ifile);
|
||||||
|
if (data.ofile)
|
||||||
|
sf_close(data.ofile);
|
||||||
|
if (data.props)
|
||||||
|
pw_properties_free(data.props);
|
||||||
|
if (data.handle)
|
||||||
|
pw_unload_spa_handle(data.handle);
|
||||||
|
if (data.context)
|
||||||
|
pw_context_destroy(data.context);
|
||||||
|
if (data.loop)
|
||||||
|
pw_main_loop_destroy(data.loop);
|
||||||
|
free(file_content);
|
||||||
|
pw_deinit();
|
||||||
|
|
||||||
|
return ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||||
|
|
||||||
|
error_usage:
|
||||||
|
show_usage(argv[0], true);
|
||||||
|
ret = EXIT_FAILURE;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue