diff --git a/po/POTFILES.in b/po/POTFILES.in index 7a914f1d7..c92665d9b 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -170,6 +170,7 @@ src/pulsecore/thread-mq.c src/pulsecore/thread-posix.c src/pulsecore/thread-win32.c src/pulsecore/time-smoother.c +src/pulsecore/time-smoother_2.c src/pulsecore/tokenizer.c src/pulsecore/x11prop.c src/pulsecore/x11wrap.c diff --git a/src/meson.build b/src/meson.build index 59a9b16bf..bbf83d7b1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -62,6 +62,7 @@ libpulsecommon_sources = [ 'pulsecore/strlist.c', 'pulsecore/tagstruct.c', 'pulsecore/time-smoother.c', + 'pulsecore/time-smoother_2.c', 'pulsecore/tokenizer.c', 'pulsecore/usergroup.c', 'pulsecore/sndfile-util.c', @@ -141,6 +142,7 @@ libpulsecommon_headers = [ 'pulsecore/tagstruct.h', 'pulsecore/thread.h', 'pulsecore/time-smoother.h', + 'pulsecore/time-smoother_2.h', 'pulsecore/tokenizer.h', 'pulsecore/usergroup.h', 'pulsecore/sndfile-util.h', diff --git a/src/pulsecore/time-smoother_2.c b/src/pulsecore/time-smoother_2.c new file mode 100644 index 000000000..8f4447e0c --- /dev/null +++ b/src/pulsecore/time-smoother_2.c @@ -0,0 +1,408 @@ +/*** + 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 + Lesser 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 . +***/ + +/* The code in this file is based on the theoretical background found at + * https://www.freedesktop.org/software/pulseaudio/misc/rate_estimator.odt. + * The theory has never been reviewed, so it may be inaccurate in places. */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include "time-smoother_2.h" + +struct pa_smoother_2 { + /* Values set when the smoother is created */ + pa_usec_t smoother_window_time; + uint32_t rate; + uint32_t frame_size; + + /* USB hack parameters */ + bool usb_hack; + bool enable_usb_hack; + uint32_t hack_threshold; + + /* Smoother state */ + bool init; + bool paused; + + /* Current byte count start value */ + double start_pos; + /* System time corresponding to start_pos */ + pa_usec_t start_time; + /* Conversion factor between time domains */ + double time_factor; + + /* Used if the smoother is paused while still in init state */ + pa_usec_t fixup_time; + + /* Time offset for USB devices */ + int64_t time_offset; + + /* Various time stamps */ + pa_usec_t resume_time; + pa_usec_t pause_time; + pa_usec_t smoother_start_time; + pa_usec_t last_time; + + /* Variables used for Kalman filter */ + double time_variance; + double time_factor_variance; + double kalman_variance; + + /* Variables used for low pass filter */ + double drift_filter; + double drift_filter_1; +}; + +/* Create new smoother */ +pa_smoother_2* pa_smoother_2_new(pa_usec_t window, pa_usec_t time_stamp, uint32_t frame_size, uint32_t rate) { + pa_smoother_2 *s; + + pa_assert(window > 0); + + s = pa_xnew(pa_smoother_2, 1); + s->enable_usb_hack = false; + s->usb_hack = false; + s->hack_threshold = 0; + s->smoother_window_time = window; + s->rate = rate; + s->frame_size = frame_size; + + pa_smoother_2_reset(s, time_stamp); + + return s; +} + +/* Free the smoother */ +void pa_smoother_2_free(pa_smoother_2* s) { + + pa_assert(s); + + pa_xfree(s); +} + +void pa_smoother_2_set_rate(pa_smoother_2 *s, pa_usec_t time_stamp, uint32_t rate) { + + pa_assert(s); + pa_assert(rate > 0); + + /* If the rate has changed, data in the smoother will be invalid, + * therefore also reset the smoother */ + if (rate != s->rate) { + s->rate = rate; + pa_smoother_2_reset(s, time_stamp); + } +} + +void pa_smoother_2_set_sample_spec(pa_smoother_2 *s, pa_usec_t time_stamp, pa_sample_spec *spec) { + size_t frame_size; + + pa_assert(s); + pa_assert(pa_sample_spec_valid(spec)); + + /* If the sample spec has changed, data in the smoother will be invalid, + * therefore also reset the smoother */ + frame_size = pa_frame_size(spec); + if (frame_size != s->frame_size || spec->rate != s->rate) { + s->frame_size = frame_size; + s->rate = spec->rate; + pa_smoother_2_reset(s, time_stamp); + } +} + +/* Add a new data point and re-calculate time conversion factor */ +void pa_smoother_2_put(pa_smoother_2 *s, pa_usec_t time_stamp, int64_t byte_count) { + double byte_difference, iteration_time; + double time_delta_system, time_delta_card, drift, filter_constant, filter_constant_1; + double temp, filtered_time_delta_card, expected_time_delta_card; + + pa_assert(s); + + /* Smoother is paused, nothing to do */ + if (s->paused) + return; + + /* Initial setup or resume */ + if PA_UNLIKELY((s->init)) { + s->resume_time = time_stamp; + + /* We have no data yet, nothing to do */ + if (byte_count <= 0) + return; + + /* Now we are playing/recording. + * Get fresh time stamps and save the start count */ + s->start_pos = (double)byte_count; + s->last_time = time_stamp; + s->start_time = time_stamp; + s->smoother_start_time = time_stamp; + + s->usb_hack = s->enable_usb_hack; + s->init = false; + return; + } + + /* Duration of last iteration */ + iteration_time = (double)time_stamp - s->last_time; + + /* Don't go backwards in time */ + if (iteration_time <= 0) + return; + + /* Wait at least 100 ms before starting calculations, otherwise the + * impact of the offset error will slow down convergence */ + if (time_stamp < s->smoother_start_time + 100 * PA_USEC_PER_MSEC) + return; + + /* Time difference in system time domain */ + time_delta_system = time_stamp - s->start_time; + + /* Number of bytes played since start_time */ + byte_difference = (double)byte_count - s->start_pos; + + /* Time difference in soundcard time domain. Don't use + * pa_bytes_to_usec() here because byte_difference need not + * be on a sample boundary */ + time_delta_card = byte_difference / s->frame_size / s->rate * PA_USEC_PER_SEC; + filtered_time_delta_card = time_delta_card; + + /* Prediction of measurement */ + expected_time_delta_card = time_delta_system * s->time_factor; + + /* Filtered variance of card time measurements */ + s->time_variance = 0.9 * s->time_variance + 0.1 * (time_delta_card - expected_time_delta_card) * (time_delta_card - expected_time_delta_card); + + /* Kalman filter, will only be used when the time factor has converged good enough, + * the value of 100 corresponds to a change rate of approximately 10e-6 per second. */ + if (s->time_factor_variance < 100) { + filtered_time_delta_card = (time_delta_card * s->kalman_variance + expected_time_delta_card * s->time_variance) / (s->kalman_variance + s->time_variance); + s->kalman_variance = s->kalman_variance * s->time_variance / (s->kalman_variance + s->time_variance) + s->time_variance / 4 + 500; + } + + /* This is a horrible hack which is necessary because USB sinks seem to fix up + * the reported delay by some millisecondsconds shortly after startup. This is + * an artifact, the real latency does not change on the reported jump. If the + * change is not caught or if the hack is triggered inadvertently, it will lead to + * prolonged convergence time and decreased stability of the reported latency. + * Since the fix up will occur within the first seconds, it is disabled later to + * avoid false triggers. When run as batch device, the threshold for the hack must + * be lower (1000) than for timer based scheduling (2000). */ + if (s->usb_hack && time_stamp - s->smoother_start_time < 5 * PA_USEC_PER_SEC) { + if ((time_delta_system - filtered_time_delta_card / s->time_factor) > (double)s->hack_threshold) { + /* Recalculate initial conditions */ + temp = time_stamp - time_delta_card - s->start_time; + s->start_time += temp; + s->smoother_start_time += temp; + s->time_offset = -temp; + + /* Reset time factor variance */ + s->time_factor_variance = 10000; + + pa_log_debug("USB Hack, start time corrected by %0.2f usec", temp); + s->usb_hack = false; + return; + } + } + + /* Parameter for lowpass filters with time constants of smoother_window_time + * and smoother_window_time/8 */ + temp = (double)s->smoother_window_time / 6.2831853; + filter_constant = iteration_time / (iteration_time + temp / 8.0); + filter_constant_1 = iteration_time / (iteration_time + temp); + + /* Temporarily save the current time factor */ + temp = s->time_factor; + + /* Calculate geometric series */ + drift = (s->drift_filter_1 + 1.0) * (1.5 - filtered_time_delta_card / time_delta_system); + + /* 2nd order lowpass */ + s->drift_filter = (1 - filter_constant) * s->drift_filter + filter_constant * drift; + s->drift_filter_1 = (1 - filter_constant) * s->drift_filter_1 + filter_constant * s->drift_filter; + + /* Calculate time conversion factor, filter again */ + s->time_factor = (1 - filter_constant_1) * s->time_factor + filter_constant_1 * (s->drift_filter_1 + 3) / (s->drift_filter_1 + 1) / 2; + + /* Filtered variance of time factor derivative, used as measure for the convergence of the time factor */ + temp = (s->time_factor - temp) / iteration_time * 10000000000000; + s->time_factor_variance = (1 - filter_constant_1) * s->time_factor_variance + filter_constant_1 * temp * temp; + + /* Calculate new start time and corresponding sample count after window time */ + if (time_stamp > s->smoother_start_time + s->smoother_window_time) { + s->start_pos += ((double)byte_count - s->start_pos) / (time_stamp - s->start_time) * iteration_time; + s->start_time += (pa_usec_t)iteration_time; + } + + /* Save current system time */ + s->last_time = time_stamp; +} + +/* Calculate the current latency. For a source, the sign must be inverted */ +int64_t pa_smoother_2_get_delay(pa_smoother_2 *s, pa_usec_t time_stamp, size_t byte_count) { + int64_t now, delay; + + pa_assert(s); + + /* If we do not have a valid frame size and rate, just return 0 */ + if (!s->frame_size || !s->rate) + return 0; + + /* Smoother is paused or has been resumed but no new data has been received */ + if (s->paused || s->init) { + delay = (int64_t)((double)byte_count * PA_USEC_PER_SEC / s->frame_size / s->rate); + return delay - pa_smoother_2_get(s, time_stamp); + } + + /* Convert system time difference to soundcard time difference */ + now = (time_stamp - s->start_time - s->time_offset) * s->time_factor; + + /* Don't use pa_bytes_to_usec(), u->start_pos needs not be on a sample boundary */ + return (int64_t)(((double)byte_count - s->start_pos) / s->frame_size / s->rate * PA_USEC_PER_SEC) - now; +} + +/* Convert system time to sound card time */ +pa_usec_t pa_smoother_2_get(pa_smoother_2 *s, pa_usec_t time_stamp) { + pa_usec_t current_time; + + pa_assert(s); + + /* If we do not have a valid frame size and rate, just return 0 */ + if (!s->frame_size || !s->rate) + return 0; + + /* Sound card time at start_time */ + current_time = (pa_usec_t)(s->start_pos / s->frame_size / s->rate * PA_USEC_PER_SEC); + + /* If the smoother has not started, just return system time since resume */ + if (!s->start_time) { + if (time_stamp >= s->resume_time) + current_time = time_stamp - s->resume_time; + else + current_time = 0; + + /* If we are paused return the sound card time at pause_time */ + } else if (s->paused) + current_time += (s->pause_time - s->start_time - s->time_offset - s->fixup_time) * s->time_factor; + + /* If we are initializing, add the time since resume to the card time at pause_time */ + else if (s->init) { + current_time += (s->pause_time - s->start_time - s->time_offset - s->fixup_time) * s->time_factor; + current_time += (time_stamp - s->resume_time) * s->time_factor; + + /* Smoother is running, calculate current sound card time */ + } else + current_time += (time_stamp - s->start_time - s->time_offset) * s->time_factor; + + return current_time; +} + +/* Convert a time interval from sound card time to system time */ +pa_usec_t pa_smoother_2_translate(pa_smoother_2 *s, pa_usec_t time_difference) { + + pa_assert(s); + + /* If not started yet, return the time difference */ + if (!s->start_time) + return time_difference; + + return (pa_usec_t)(time_difference / s->time_factor); +} + +/* Enable USB hack */ +void pa_smoother_2_usb_hack_enable(pa_smoother_2 *s, bool enable, pa_usec_t offset) { + + pa_assert(s); + + s->enable_usb_hack = enable; + s->hack_threshold = offset; +} + +/* Reset the smoother */ +void pa_smoother_2_reset(pa_smoother_2 *s, pa_usec_t time_stamp) { + + pa_assert(s); + + /* Reset variables for time estimation */ + s->drift_filter = 1.0; + s->drift_filter_1 = 1.0; + s->time_factor = 1.0; + s->start_pos = 0; + s->init = true; + s->time_offset = 0; + s->time_factor_variance = 10000.0; + s->kalman_variance = 10000000.0; + s->time_variance = 100000.0; + s->start_time = 0; + s->last_time = 0; + s->smoother_start_time = 0; + s->usb_hack = false; + s->pause_time = time_stamp; + s->fixup_time = 0; + s->resume_time = time_stamp; + s->paused = false; + + /* Set smoother to paused if rate or frame size are invalid */ + if (!s->frame_size || !s->rate) + s->paused = true; +} + +/* Pause the smoother */ +void pa_smoother_2_pause(pa_smoother_2 *s, pa_usec_t time_stamp) { + + pa_assert(s); + + /* Smoother is already paused, nothing to do */ + if (s->paused) + return; + + /* If we are in init state, add the pause time to the fixup time */ + if (s->init) + s->fixup_time += s->resume_time - s->pause_time; + else + s->fixup_time = 0; + + s->smoother_start_time = 0; + s->resume_time = time_stamp; + s->pause_time = time_stamp; + s->time_factor_variance = 10000.0; + s->kalman_variance = 10000000.0; + s->time_variance = 100000.0; + s->init = true; + s->paused = true; +} + +/* Resume the smoother */ +void pa_smoother_2_resume(pa_smoother_2 *s, pa_usec_t time_stamp) { + + pa_assert(s); + + if (!s->paused) + return; + + /* Keep smoother paused if rate or frame size is not set */ + if (!s->frame_size || !s->rate) + return; + + s->resume_time = time_stamp; + s->paused = false; +} diff --git a/src/pulsecore/time-smoother_2.h b/src/pulsecore/time-smoother_2.h new file mode 100644 index 000000000..57fc1e31c --- /dev/null +++ b/src/pulsecore/time-smoother_2.h @@ -0,0 +1,53 @@ +#ifndef foopulsetimesmoother2hfoo +#define foopulsetimesmoother2hfoo + +/*** + 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 + Lesser 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 . +***/ + +#include + +typedef struct pa_smoother_2 pa_smoother_2; + +/* Create new smoother */ +pa_smoother_2* pa_smoother_2_new(pa_usec_t window, pa_usec_t time_stamp, uint32_t frame_size, uint32_t rate); +/* Free the smoother */ +void pa_smoother_2_free(pa_smoother_2* s); +/* Reset the smoother */ +void pa_smoother_2_reset(pa_smoother_2 *s, pa_usec_t time_stamp); +/* Pause the smoother */ +void pa_smoother_2_pause(pa_smoother_2 *s, pa_usec_t time_stamp); +/* Resume the smoother */ +void pa_smoother_2_resume(pa_smoother_2 *s, pa_usec_t time_stamp); + +/* Add a new data point and re-calculate time conversion factor */ +void pa_smoother_2_put(pa_smoother_2 *s, pa_usec_t time_stamp, int64_t byte_count); + +/* Calculate the current latency. For a source, the sign of the result must be inverted */ +int64_t pa_smoother_2_get_delay(pa_smoother_2 *s, pa_usec_t time_stamp, size_t byte_count); +/* Convert system time since start to sound card time */ +pa_usec_t pa_smoother_2_get(pa_smoother_2 *s, pa_usec_t time_stamp); +/* Convert a time interval from sound card time to system time */ +pa_usec_t pa_smoother_2_translate(pa_smoother_2 *s, pa_usec_t time_difference); + +/* Enable USB hack, only used for alsa sinks */ +void pa_smoother_2_usb_hack_enable(pa_smoother_2 *s, bool enable, pa_usec_t offset); +/* Set sample rate */ +void pa_smoother_2_set_rate(pa_smoother_2 *s, pa_usec_t time_stamp, uint32_t rate); +/* Set rate and frame size */ +void pa_smoother_2_set_sample_spec(pa_smoother_2 *s, pa_usec_t time_stamp, pa_sample_spec *spec); + +#endif