diff --git a/src/.gitignore b/src/.gitignore index 1157c9fdc..65a7e1072 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -71,6 +71,7 @@ proplist-test queue-test remix-test resampler-test +resampler-rewind-test rtpoll-test rtstutter sig2str-test diff --git a/src/tests/meson.build b/src/tests/meson.build index acd301037..42f60538c 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -70,6 +70,8 @@ if get_option('daemon') [ check_dep, libpulse_dep, libpulsecommon_dep, libpulsecore_dep ] ], [ 'resampler-test', 'resampler-test.c', [ libpulse_dep, libpulsecommon_dep, libpulsecore_dep, libintl_dep ] ], + [ 'resampler-rewind-test', 'resampler-rewind-test.c', + [ libpulse_dep, libpulsecommon_dep, libpulsecore_dep, libintl_dep, libm_dep ] ], [ 'rtpoll-test', 'rtpoll-test.c', [ check_dep, libpulse_dep, libpulsecommon_dep, libpulsecore_dep ] ], [ 'smoother-test', 'smoother-test.c', diff --git a/src/tests/resampler-rewind-test.c b/src/tests/resampler-rewind-test.c new file mode 100644 index 000000000..158a84214 --- /dev/null +++ b/src/tests/resampler-rewind-test.c @@ -0,0 +1,437 @@ +/*** + This file is part of PulseAudio. + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, + or (at your option) any later version. + + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, see . +***/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MEMBLOCKQ_MAXLENGTH (16*1024*1024) +#define PA_SILENCE_MAX (pa_page_size()*16) +#define MAX_MATCHING_PERIOD 500 + +static pa_memblock *silence_memblock_new(pa_mempool *pool, uint8_t c) { + pa_memblock *b; + size_t length; + void *data; + + pa_assert(pool); + + length = PA_MIN(pa_mempool_block_size_max(pool), PA_SILENCE_MAX); + + b = pa_memblock_new(pool, length); + + data = pa_memblock_acquire(b); + memset(data, c, length); + pa_memblock_release(b); + + pa_memblock_set_is_silence(b, true); + + return b; +} + +/* Calculate number of history bytes needed for the rewind */ +static size_t calculate_resampler_history_bytes(pa_resampler *r, size_t in_rewind_frames) { + size_t history_frames, history_max, matching_period, total_frames, remainder; + double delay; + + if (!r) + return 0; + + /* Initialize some variables, cut off full seconds from the rewind */ + total_frames = 0; + in_rewind_frames = in_rewind_frames % r->i_ss.rate; + history_max = (uint64_t) PA_RESAMPLER_MAX_DELAY_USEC * r->i_ss.rate * 3 / PA_USEC_PER_SEC / 2; + + /* Get the current internal delay of the resampler */ + delay = pa_resampler_get_delay(r, false); + + /* Calculate the matchiung period */ + matching_period = r->i_ss.rate / pa_resampler_get_gcd(r); + pa_log_debug("Integral period length is %lu input frames", matching_period); + + /* If the delay is larger than the length of the history queue, we can only + * replay as much as we have. */ + if ((size_t)delay >= history_max) { + history_frames = history_max; + return history_frames * r->i_fz; + } + + /* Initially set the history to 3 times the resampler delay. Use at least 2 ms. */ + history_frames = (size_t)(delay * 3.0); + history_frames = PA_MAX(history_frames, r->i_ss.rate / 500); + + /* Check how the rewind fits into multiples of the matching period. */ + remainder = (in_rewind_frames + history_frames) % matching_period; + + /* If possible, use between 2 and 3 times the resampler delay */ + if (remainder < (size_t)delay && history_frames - remainder <= history_max) + total_frames = in_rewind_frames + history_frames - remainder; + + /* Else, try above 3 times the delay */ + else if (history_frames + matching_period - remainder <= history_max) + total_frames = in_rewind_frames + history_frames + matching_period - remainder; + + if (total_frames != 0) + /* We found a perfect match. */ + history_frames = total_frames - in_rewind_frames; + else { + /* Try to use 2.5 times the delay. */ + history_frames = PA_MIN((size_t)(delay * 2.5), history_max); + pa_log_debug("No usable integral matching period"); + } + + return history_frames * r->i_fz; +} + +static float compare_blocks(const pa_sample_spec *ss, const pa_memchunk *chunk_a, const pa_memchunk *chunk_b) { + float *a, *b, max_diff = 0; + unsigned i; + + a = pa_memblock_acquire(chunk_a->memblock); + b = pa_memblock_acquire(chunk_b->memblock); + a += chunk_a->index / pa_frame_size(ss); + b += chunk_b->index / pa_frame_size(ss); + + for (i = 0; i < chunk_a->length / pa_frame_size(ss); i++) { + if (fabs(a[i] - b[i]) > max_diff) + max_diff = fabs(a[i] - b[i]); + } + + pa_memblock_release(chunk_a->memblock); + pa_memblock_release(chunk_b->memblock); + + return max_diff; +} + +static pa_memblock* generate_block(pa_mempool *pool, const pa_sample_spec *ss, unsigned frequency, double amplitude, size_t nr_of_samples) { + pa_memblock *r; + float *d; + float val; + unsigned i; + int n; + float t, dt, dt_period; + + pa_assert(frequency); + pa_assert(nr_of_samples); + pa_assert(ss->channels == 1); + pa_assert(ss->format == PA_SAMPLE_FLOAT32NE); + + pa_assert_se(r = pa_memblock_new(pool, pa_frame_size(ss) * nr_of_samples)); + d = pa_memblock_acquire(r); + + /* Generate square wave with given length, frequency and sample rate. */ + val = amplitude; + t = 0; + n = 1; + dt = 1.0 / ss->rate; + dt_period = 1.0 / frequency; + for (i=0; i < nr_of_samples; i++) { + d[i] = val; + + if ((int)(2 * t / dt_period) >= n) { + n++; + if (val >= amplitude) + val = - amplitude; + else + val = amplitude; + } + + t += dt; + } + + pa_memblock_release(r); + + return r; +} + +static void help(const char *argv0) { + printf("%s [options]\n\n" + "-h, --help Show this help\n" + "-v, --verbose Print debug messages\n" + " --from-rate=SAMPLERATE From sample rate in Hz (defaults to 44100)\n" + " --to-rate=SAMPLERATE To sample rate in Hz (defaults to 44100)\n" + " --resample-method=METHOD Resample method (defaults to auto)\n" + " --frequency=unsigned Frequency of square wave\n" + " --samples=unsigned Number of samples for square wave\n" + " --rewind=unsigned Number of output samples to rewind\n" + "\n" + "This test generates samples for a square wave of given frequency, number of samples\n" + "and input sample rate. Then this input data is resampled to the output rate, rewound\n" + "by rewind samples and the rewound part is processed again. Then output is compared to\n" + "the result of the first pass.\n" + "\n" + "See --dump-resample-methods for possible values of resample methods.\n", + argv0); +} + +enum { + ARG_VERSION = 256, + ARG_FROM_SAMPLERATE, + ARG_TO_SAMPLERATE, + ARG_FREQUENCY, + ARG_SAMPLES, + ARG_REWIND, + ARG_RESAMPLE_METHOD, + ARG_DUMP_RESAMPLE_METHODS +}; + +static void dump_resample_methods(void) { + int i; + + for (i = 0; i < PA_RESAMPLER_MAX; i++) + if (pa_resample_method_supported(i)) + printf("%s\n", pa_resample_method_to_string(i)); + +} + +int main(int argc, char *argv[]) { + pa_mempool *pool = NULL; + pa_sample_spec a, b; + pa_resample_method_t method; + int ret = 1, c; + unsigned samples, frequency, rewind; + unsigned crossover_freq = 120; + pa_resampler *resampler; + pa_memchunk in_chunk, out_chunk, rewound_chunk, silence_chunk; + pa_usec_t ts; + pa_memblockq *history_queue = NULL; + size_t in_rewind_size, in_frame_size, history_size, out_rewind_size, old_length, in_resampler_buffer, n_out_expected; + float max_diff; + double delay_before, delay_after, delay_expected; + + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"verbose", 0, NULL, 'v'}, + {"version", 0, NULL, ARG_VERSION}, + {"from-rate", 1, NULL, ARG_FROM_SAMPLERATE}, + {"to-rate", 1, NULL, ARG_TO_SAMPLERATE}, + {"frequency", 1, NULL, ARG_FREQUENCY}, + {"samples", 1, NULL, ARG_SAMPLES}, + {"rewind", 1, NULL, ARG_REWIND}, + {"resample-method", 1, NULL, ARG_RESAMPLE_METHOD}, + {"dump-resample-methods", 0, NULL, ARG_DUMP_RESAMPLE_METHODS}, + {NULL, 0, NULL, 0} + }; + + setlocale(LC_ALL, ""); +#ifdef ENABLE_NLS + bindtextdomain(GETTEXT_PACKAGE, PULSE_LOCALEDIR); +#endif + + pa_log_set_level(PA_LOG_WARN); + if (!getenv("MAKE_CHECK")) + pa_log_set_level(PA_LOG_INFO); + + a.channels = b.channels = 1; + a.rate = 48000; + b.rate = 44100; + a.format = b.format = PA_SAMPLE_FLOAT32NE; + + method = PA_RESAMPLER_AUTO; + frequency = 1000; + samples = 5000; + rewind = 2500; + + while ((c = getopt_long(argc, argv, "hv", long_options, NULL)) != -1) { + + switch (c) { + case 'h' : + help(argv[0]); + ret = 0; + goto quit; + + case 'v': + pa_log_set_level(PA_LOG_DEBUG); + break; + + case ARG_VERSION: + printf("%s %s\n", argv[0], PACKAGE_VERSION); + ret = 0; + goto quit; + + case ARG_DUMP_RESAMPLE_METHODS: + dump_resample_methods(); + ret = 0; + goto quit; + + case ARG_FROM_SAMPLERATE: + a.rate = (uint32_t) atoi(optarg); + break; + + case ARG_TO_SAMPLERATE: + b.rate = (uint32_t) atoi(optarg); + break; + + case ARG_FREQUENCY: + frequency = (unsigned) atoi(optarg); + break; + + case ARG_SAMPLES: + samples = (unsigned) atoi(optarg); + break; + + case ARG_REWIND: + rewind = (unsigned) atoi(optarg); + break; + + case ARG_RESAMPLE_METHOD: + if (*optarg == '\0' || pa_streq(optarg, "help")) { + dump_resample_methods(); + ret = 0; + goto quit; + } + method = pa_parse_resample_method(optarg); + break; + + default: + goto quit; + } + } + + pa_log_info("=== Square wave %u Hz, %u samples. Resampling using %s from %u Hz to %u Hz, rewinding %u output samples.", frequency, + samples, pa_resample_method_to_string(method), a.rate, b.rate, rewind); + + ret = 0; + pa_assert_se(pool = pa_mempool_new(PA_MEM_TYPE_PRIVATE, 0, true)); + + pa_log_debug("Compilation CFLAGS: %s", PA_CFLAGS); + + /* Setup resampler */ + ts = pa_rtclock_now(); + pa_assert_se(resampler = pa_resampler_new(pool, &a, NULL, &b, NULL, crossover_freq, method, 0)); + pa_log_info("Init took %llu usec", (long long unsigned)(pa_rtclock_now() - ts)); + + /* Generate input data */ + in_chunk.memblock = generate_block(pool, &a, frequency, 0.5, samples); + in_chunk.length = pa_memblock_get_length(in_chunk.memblock); + in_chunk.index = 0; + in_frame_size = pa_frame_size(&a); + + /* First, resample the full block */ + ts = pa_rtclock_now(); + pa_resampler_run(resampler, &in_chunk, &out_chunk); + if (!out_chunk.memblock) { + pa_memblock_unref(in_chunk.memblock); + pa_log_warn("Resampling did not return any output data"); + ret = 1; + goto quit; + } + + pa_log_info("resampling took %llu usec.", (long long unsigned)(pa_rtclock_now() - ts)); + if (rewind > out_chunk.length / pa_frame_size(&b)) { + pa_log_warn("Specified number of frames to rewind (%u) larger than number of output frames (%lu), aborting.", rewind, out_chunk.length / pa_frame_size(&b)); + ret = 1; + goto quit1; + } + + /* Get delay after first resampling pass */ + delay_before = pa_resampler_get_delay(resampler, true); + + /* Create and prepare history queue */ + silence_chunk.memblock = silence_memblock_new(pool, 0); + silence_chunk.length = pa_frame_align(pa_memblock_get_length(silence_chunk.memblock), &a); + silence_chunk.index = 0; + history_queue = pa_memblockq_new("Test-Queue", 0, MEMBLOCKQ_MAXLENGTH, 0, &a, 0, 1, samples * in_frame_size, &silence_chunk); + pa_memblock_unref(silence_chunk.memblock); + + pa_memblockq_push(history_queue, &in_chunk); + pa_memblockq_drop(history_queue, samples * in_frame_size); + + in_rewind_size = pa_resampler_request(resampler, rewind * pa_frame_size(&b)); + out_rewind_size = rewind * pa_frame_size(&b); + pa_log_debug("Have to rewind %lu input frames", in_rewind_size / in_frame_size); + ts = pa_rtclock_now(); + + /* Now rewind the resampler */ + pa_memblockq_rewind(history_queue, in_rewind_size); + history_size = calculate_resampler_history_bytes(resampler, in_rewind_size / in_frame_size); + pa_log_debug("History is %lu frames.", history_size / in_frame_size); + pa_resampler_rewind(resampler, out_rewind_size, history_queue, history_size); + + pa_log_info("Rewind took %llu usec.", (long long unsigned)(pa_rtclock_now() - ts)); + ts = pa_rtclock_now(); + + /* Re-run the resampler */ + old_length = in_chunk.length; + in_chunk.length = in_rewind_size; + in_chunk.index = old_length - in_chunk.length; + pa_resampler_run(resampler, &in_chunk, &rewound_chunk); + if (!rewound_chunk.memblock) { + pa_log_warn("Resampler did not return output data for rewind"); + ret = 1; + goto quit1; + } + + /* Get delay after rewind */ + delay_after = pa_resampler_get_delay(resampler, true); + + /* Calculate expected delay */ + n_out_expected = pa_resampler_result(resampler, in_rewind_size + history_size) / pa_frame_size(&b); + delay_expected = delay_before + (double)(in_rewind_size + history_size) / (double)in_frame_size - n_out_expected * (double)a.rate / (double)b.rate; + + /* Check for leftover samples in the resampler buffer */ + in_resampler_buffer = lround((delay_after - delay_expected) * (double)b.rate / (double)a.rate); + if (in_resampler_buffer != 0) { + pa_log_debug("%li output frames still in resampler buffer", in_resampler_buffer); + } + + pa_log_info("Second resampler run took %llu usec.", (long long unsigned)(pa_rtclock_now() - ts)); + pa_log_debug("Got %lu output frames", rewound_chunk.length / pa_frame_size(&b)); + old_length = out_chunk.length; + out_chunk.length = rewound_chunk.length; + out_chunk.index = old_length - out_chunk.length; + + max_diff = compare_blocks(&b, &out_chunk, &rewound_chunk); + pa_log_info("Maximum difference is %.*g", 6, max_diff); + + pa_memblock_unref(rewound_chunk.memblock); + +quit1: + pa_memblock_unref(in_chunk.memblock); + pa_memblock_unref(out_chunk.memblock); + + pa_resampler_free(resampler); + if (history_queue) + pa_memblockq_free(history_queue); + +quit: + if (pool) + pa_mempool_unref(pool); + + return ret; +}