resampler: Precompute some common filter coefficients

While this is quite fast on x86 (order of a few microseconds), the
computation can take a few milliseconds on ARM (measured at 1.9ms (32000
-> 48000) and 3.3ms (32000 -> 44100) on a Cortex A53).

Let's precompute some common rates so that we can avoid this overhead on
each stream (or any other audioconvert) instantiation. The approach
taken here is to write a little program to create the resampler
instance, and run that on the host at compile-time to generate some
common rate conversions.
This commit is contained in:
Arun Raghavan 2024-08-07 12:30:10 -04:00
parent 46f89d8009
commit 70a7bae5d7
5 changed files with 260 additions and 1 deletions

View file

@ -50,6 +50,7 @@ pipewire_confdatadir = pipewire_datadir / 'pipewire'
modules_install_dir = pipewire_libdir / pipewire_name
cc = meson.get_compiler('c')
cc_native = meson.get_compiler('c', native: true)
if cc.has_header('features.h') and cc.get_define('__GLIBC__', prefix: '#include <features.h>') != ''
# glibc ld.so interprets ${LIB} in a library loading path with an
@ -112,6 +113,8 @@ cc_flags = common_flags + [
]
add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c')
cc_flags_native = cc_native.get_supported_arguments(cc_flags)
have_cpp = add_languages('cpp', native: false, required : false)
if have_cpp
@ -276,6 +279,7 @@ configure_file(input : 'Makefile.in',
# Find dependencies
mathlib = cc.find_library('m', required : false)
mathlib_native = cc_native.find_library('m', required : false)
rt_lib = cc.find_library('rt', required : false) # clock_gettime
dl_lib = cc.find_library('dl', required : false)
pthread_lib = dependency('threads')

View file

@ -84,6 +84,10 @@ option('audioconvert',
description: 'Enable audioconvert spa plugin integration',
type: 'feature',
value: 'enabled')
option('resampler-precomp-tuples',
description: 'Array of "inrate,outrate[,quality]" tuples to precompute resampler coefficients for',
type: 'array',
value: [ '32000,44100', '32000,48000', '48000,44100', '44100,48000' ])
option('bluez5',
description: 'Enable bluez5 spa plugin integration',
type: 'feature',

View file

@ -105,10 +105,40 @@ if have_neon
simd_dependencies += audioconvert_neon
endif
sparesampledumpcoeffs_sources = [
'resample-native.c',
'resample-native-c.c',
'spa-resample-dump-coeffs.c',
]
sparesampledumpcoeffs = executable(
'spa-resample-dump-coeffs',
sparesampledumpcoeffs_sources,
c_args : [ cc_flags_native, '-DRESAMPLE_DISABLE_PRECOMP' ],
dependencies : [ spa_dep, mathlib_native ],
install : false,
native : true,
)
precomptuples = []
foreach tuple : get_option('resampler-precomp-tuples')
precomptuples += '-t ' + tuple
endforeach
resample_native_precomp_h = custom_target(
'resample-native-precomp.h',
output : 'resample-native-precomp.h',
capture : true,
command : [
sparesampledumpcoeffs,
] + precomptuples
)
audioconvert_lib = static_library('audioconvert',
['fmt-ops.c',
'channelmix-ops.c',
'peaks-ops.c',
resample_native_precomp_h,
'resample-native.c',
'resample-peaks.c',
'wavfile.c',

View file

@ -7,6 +7,9 @@
#include <spa/param/audio/format.h>
#include "resample-native-impl.h"
#ifndef RESAMPLE_DISABLE_PRECOMP
#include "resample-native-precomp.h"
#endif
struct quality {
uint32_t n_taps;
@ -375,7 +378,25 @@ int resample_native_init(struct resample *r)
for (c = 0; c < r->channels; c++)
d->history[c] = SPA_PTROFF(d->hist_mem, c * history_stride, float);
build_filter(d->filter, d->filter_stride, n_taps, n_phases, scale);
#ifndef RESAMPLE_DISABLE_PRECOMP
/* See if we have precomputed coefficients */
for (c = 0; precomp_coeffs[c].filter; c++) {
if (precomp_coeffs[c].in_rate == r->i_rate &&
precomp_coeffs[c].out_rate == r->o_rate &&
precomp_coeffs[c].quality == r->quality)
break;
}
if (precomp_coeffs[c].filter) {
spa_log_debug(r->log, "using precomputed filter for %u->%u(%u)",
r->i_rate, r->o_rate, r->quality);
spa_memcpy(d->filter, precomp_coeffs[c].filter, filter_size);
} else {
#endif
build_filter(d->filter, d->filter_stride, n_taps, n_phases, scale);
#ifndef RESAMPLE_DISABLE_PRECOMP
}
#endif
d->info = find_resample_info(SPA_AUDIO_FORMAT_F32, r->cpu_flags);
if (SPA_UNLIKELY(d->info == NULL)) {

View file

@ -0,0 +1,200 @@
/* Spa */
/* SPDX-FileCopyrightText: Copyright © 2024 Arun Raghavan <arun@asymptotic.io> */
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <getopt.h>
#include <spa/support/log-impl.h>
#include <spa/utils/result.h>
#include <spa/utils/string.h>
SPA_LOG_IMPL(logger);
#include "resample.h"
#include "resample-native-impl.h"
#define OPTIONS "ht:"
static const struct option long_options[] = {
{ "help", no_argument, NULL, 'h'},
{ "tuple", required_argument, NULL, 't' },
{ 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]\n", name);
fprintf(fp,
" -h, --help Show this help\n"
"\n"
" -t --tuple Sample rate tuple (as \"in_rate,out_rate[,quality]\")\n"
"\n");
}
static void parse_tuple(const char *arg, int *in, int *out, int *quality)
{
char tuple[256];
char *token;
strncpy(tuple, arg, sizeof(tuple) - 1);
*in = 0;
*out = 0;
token = strtok(tuple, ",");
if (!token || !spa_atoi32(token, in, 10))
return;
token = strtok(NULL, ",");
if (!token || !spa_atoi32(token, out, 10))
return;
token = strtok(NULL, ",");
if (!token) {
*quality = RESAMPLE_DEFAULT_QUALITY;
} else if (!spa_atoi32(token, quality, 10)) {
*quality = -1;
return;
}
/* first, second now contain zeroes on error, or the numbers on success,
* third contains a quality or -1 on error, default value if unspecified */
}
#define PREFIX "__precomp_coeff"
static void dump_header(void)
{
printf("/* This is a generated file, see spa-resample-dump-coeffs.c */");
printf("\n#include <stdint.h>\n");
printf("\n#include <stdlib.h>\n");
printf("\n");
printf("struct resample_coeffs {\n");
printf("\tuint32_t in_rate;\n");
printf("\tuint32_t out_rate;\n");
printf("\tint quality;\n");
printf("\tconst float *filter;\n");
printf("};\n");
}
static void dump_footer(const uint32_t *ins, const uint32_t *outs, const int *qualities)
{
printf("\n");
printf("static const struct resample_coeffs precomp_coeffs[] = {\n");
while (*ins && *outs) {
printf("\t{ .in_rate = %u, .out_rate = %u, .quality = %u, "
".filter = %s_%u_%u_%u },\n",
*ins, *outs, *qualities, PREFIX, *ins, *outs, *qualities);
ins++;
outs++;
qualities++;
}
printf("\t{ .in_rate = 0, .out_rate = 0, .quality = 0, .filter = NULL },\n");
printf("};\n");
}
static void dump_coeffs(unsigned int in_rate, unsigned int out_rate, int quality)
{
struct resample r = { 0, };
struct native_data *d;
unsigned int i, filter_size;
int ret;
r.log = &logger.log;
r.i_rate = in_rate;
r.o_rate = out_rate;
r.quality = quality;
r.channels = 1; /* irrelevant for generated taps */
if ((ret = resample_native_init(&r)) < 0) {
fprintf(stderr, "can't init converter: %s\n", spa_strerror(ret));
return;
}
d = r.data;
filter_size = d->filter_stride * (d->n_phases + 1);
printf("\n");
printf("static const float %s_%u_%u_%u[] = {", PREFIX, in_rate, out_rate, quality);
for (i = 0; i < filter_size; i++) {
printf("%a", d->filter[i]);
if (i != filter_size - 1)
printf(",");
}
printf("};\n");
if (r.free)
r.free(&r);
}
int main(int argc, char* argv[])
{
unsigned int ins[256] = { 0, }, outs[256] = { 0, };
int qualities[256] = { 0, };
int in_rate = 0, out_rate = 0, quality = 0;
int c, longopt_index = 0, i = 0;
while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) {
switch (c) {
case 'h':
show_usage(argv[0], false);
return EXIT_SUCCESS;
case 't':
parse_tuple(optarg, &in_rate, &out_rate, &quality);
if (in_rate <= 0) {
fprintf(stderr, "error: bad input rate %d\n", in_rate);
goto error;
}
if (out_rate <= 0) {
fprintf(stderr, "error: bad output rate %d\n", out_rate);
goto error;
}
if (quality < 0 || quality > 14) {
fprintf(stderr, "error: bad quality value %s\n", optarg);
goto error;
}
ins[i] = in_rate;
outs[i] = out_rate;
qualities[i] = quality;
i++;
break;
default:
fprintf(stderr, "error: unknown option\n");
goto error_usage;
}
}
if (optind != argc) {
fprintf(stderr, "error: got %d extra argument(s))\n",
optind - argc);
goto error_usage;
}
if (in_rate == 0) {
fprintf(stderr, "error: input rate must be specified\n");
goto error;
}
if (out_rate == 0) {
fprintf(stderr, "error: input rate must be specified\n");
goto error;
}
dump_header();
while (i--) {
dump_coeffs(ins[i], outs[i], qualities[i]);
}
dump_footer(ins, outs, qualities);
return EXIT_SUCCESS;
error_usage:
show_usage(argv[0], true);
error:
return EXIT_FAILURE;
}