From 70a7bae5d7f4822d9bcc852f7e6280e06a27cf41 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Wed, 7 Aug 2024 12:30:10 -0400 Subject: [PATCH] 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. --- meson.build | 4 + meson_options.txt | 4 + spa/plugins/audioconvert/meson.build | 30 +++ spa/plugins/audioconvert/resample-native.c | 23 +- .../audioconvert/spa-resample-dump-coeffs.c | 200 ++++++++++++++++++ 5 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 spa/plugins/audioconvert/spa-resample-dump-coeffs.c diff --git a/meson.build b/meson.build index 3012b38d5..fbeeaae13 100644 --- a/meson.build +++ b/meson.build @@ -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 ') != '' # 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') diff --git a/meson_options.txt b/meson_options.txt index be334ff5d..f3e60b252 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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', diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build index d18f64b53..e6a461824 100644 --- a/spa/plugins/audioconvert/meson.build +++ b/spa/plugins/audioconvert/meson.build @@ -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', diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c index 0183d3684..3a85f06c8 100644 --- a/spa/plugins/audioconvert/resample-native.c +++ b/spa/plugins/audioconvert/resample-native.c @@ -7,6 +7,9 @@ #include #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)) { diff --git a/spa/plugins/audioconvert/spa-resample-dump-coeffs.c b/spa/plugins/audioconvert/spa-resample-dump-coeffs.c new file mode 100644 index 000000000..154792fc9 --- /dev/null +++ b/spa/plugins/audioconvert/spa-resample-dump-coeffs.c @@ -0,0 +1,200 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2024 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +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 \n"); + printf("\n#include \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; +}