summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorg Chini <georg@chini.tk>2020-01-27 20:19:48 +0100
committerGeorg Chini <georg@chini.tk>2021-11-07 18:17:37 +0000
commit6a906e54cc8cd47ff2827b9ef3b04ff1a8eab99f (patch)
treecef3856f45cdf6daecb62ef85718616aa134ac43
parent285427ce30c966cf149acef7096aec10c2eaa769 (diff)
loopback: Optimize adaptive re-sampling
The current code assumes that the time domains of source and sink are equal. This leads to a saw-tooth characteristics of the resulting end to end latency. This patch adds an iterative calculation of an optimum rate which accounts for the difference between the source and sink time domains, thereby massively enhancing the latency stability. Theoretical background for the calculation can be found at https://www.freedesktop.org/software/pulseaudio/misc/rate_estimator.odt Part-of: <https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/merge_requests/56>
-rw-r--r--src/modules/module-loopback.c121
1 files changed, 105 insertions, 16 deletions
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index 0f6467fff..d2eec9b72 100644
--- a/src/modules/module-loopback.c
+++ b/src/modules/module-loopback.c
@@ -59,6 +59,8 @@ PA_MODULE_USAGE(
#define DEFAULT_LATENCY_MSEC 200
+#define FILTER_PARAMETER 0.125
+
#define MEMBLOCKQ_MAXLENGTH (1024*1024*32)
#define MIN_DEVICE_LATENCY (2.5*PA_USEC_PER_MSEC)
@@ -106,6 +108,13 @@ struct userdata {
int64_t sink_latency_offset;
pa_usec_t minimum_latency;
+ /* State variable of the latency controller */
+ int32_t last_latency_difference;
+
+ /* Filter varables used for 2nd order filter */
+ double drift_filter;
+ double drift_compensation_rate;
+
/* lower latency limit found by underruns */
pa_usec_t underrun_latency_limit;
@@ -114,8 +123,13 @@ struct userdata {
uint32_t underrun_counter;
uint32_t adjust_counter;
+ /* Various booleans */
bool fixed_alsa_source;
bool source_sink_changed;
+ bool underrun_occured;
+ bool source_latency_offset_changed;
+ bool sink_latency_offset_changed;
+ bool initial_adjust_pending;
/* Used for sink input and source output snapshots */
struct {
@@ -195,6 +209,7 @@ enum {
LOOPBACK_MESSAGE_SOURCE_LATENCY_RANGE_CHANGED,
LOOPBACK_MESSAGE_SINK_LATENCY_RANGE_CHANGED,
LOOPBACK_MESSAGE_UNDERRUN,
+ LOOPBACK_MESSAGE_ADJUST_DONE,
};
static void enable_adjust_timer(struct userdata *u, bool enable);
@@ -241,32 +256,64 @@ static void teardown(struct userdata *u) {
/* rate controller, called from main context
* - maximum deviation from base rate is less than 1%
* - controller step size is limited to 2.01‰
+ * - will calculate an optimum rate
* - exhibits hunting with USB or Bluetooth sources
*/
static uint32_t rate_controller(
struct userdata *u,
uint32_t base_rate, uint32_t old_rate,
- int32_t latency_difference_usec) {
+ int32_t latency_difference_at_optimum_rate,
+ int32_t latency_difference_at_base_rate) {
- uint32_t new_rate, new_rate_1, new_rate_2;
- double min_cycles_1, min_cycles_2;
+ double new_rate, new_rate_1, new_rate_2;
+ double min_cycles_1, min_cycles_2, drift_rate, latency_drift;
/* Calculate next rate that is not more than 2‰ away from the last rate */
- min_cycles_1 = (double)abs(latency_difference_usec) / u->real_adjust_time / 0.002 + 1;
- new_rate_1 = old_rate + base_rate * (double)latency_difference_usec / min_cycles_1 / u->real_adjust_time;
+ min_cycles_1 = (double)abs(latency_difference_at_optimum_rate) / u->real_adjust_time / 0.002 + 1;
+ new_rate_1 = old_rate + base_rate * (double)latency_difference_at_optimum_rate / min_cycles_1 / u->real_adjust_time;
/* Calculate best rate to correct the current latency offset, limit at
* 1% difference from base_rate */
- min_cycles_2 = (double)abs(latency_difference_usec) / u->real_adjust_time / 0.01 + 1;
- new_rate_2 = (double)base_rate * (1.0 + (double)latency_difference_usec / min_cycles_2 / u->real_adjust_time);
+ min_cycles_2 = (double)abs(latency_difference_at_optimum_rate) / u->real_adjust_time / 0.01 + 1;
+ new_rate_2 = (double)base_rate * (1.0 + (double)latency_difference_at_optimum_rate / min_cycles_2 / u->real_adjust_time);
/* Choose the rate that is nearer to base_rate */
- if (abs((int)new_rate_1 - (int)base_rate) < abs((int)new_rate_2 - (int)base_rate))
+ if (abs((int)(new_rate_1 - base_rate)) < abs((int)(new_rate_2 - base_rate)))
new_rate = new_rate_1;
else
new_rate = new_rate_2;
- return new_rate;
+ /* Calculate rate difference between source and sink. Skip calculation
+ * after a source/sink change, an underrun or latency offset change */
+
+ if (!u->underrun_occured && !u->source_sink_changed && !u->source_latency_offset_changed && !u->sink_latency_offset_changed) {
+ /* Latency difference between last iterations */
+ latency_drift = latency_difference_at_base_rate - u->last_latency_difference;
+
+ /* Calculate frequency difference between source and sink */
+ drift_rate = latency_drift * old_rate / u->real_adjust_time + old_rate - base_rate;
+
+ /* The maximum accepted sample rate difference between source and
+ * sink is 1% of the base rate. If the result is larger, something
+ * went wrong, so do not use it. Pass in 0 instead to allow the
+ * filter to decay. */
+ if (abs((int)drift_rate) > base_rate / 100)
+ drift_rate = 0;
+
+ /* 2nd order lowpass filter */
+ u->drift_filter = (1 - FILTER_PARAMETER) * u->drift_filter + FILTER_PARAMETER * drift_rate;
+ u->drift_compensation_rate = (1 - FILTER_PARAMETER) * u->drift_compensation_rate + FILTER_PARAMETER * u->drift_filter;
+ }
+
+ /* Use drift compensation. Though not likely, the rate might exceed the maximum allowed rate now. */
+ new_rate = new_rate + u->drift_compensation_rate + 0.5;
+
+ if (new_rate > base_rate * 101 / 100)
+ return base_rate * 101 / 100;
+ else if (new_rate < base_rate * 99 / 100)
+ return base_rate * 99 / 100;
+ else
+ return (int)new_rate;
}
/* Called from main thread.
@@ -372,13 +419,17 @@ static void adjust_rates(struct userdata *u) {
/* Calculate real adjust time if source or sink did not change and if the system has
* not been suspended. If the time between two calls is more than 5% longer than the
* configured adjust time, we assume that the system has been sleeping and skip the
- * calculation for this iteration. */
+ * calculation for this iteration. When source or sink changed or the system has been
+ * sleeping, we need to reset the parameters for drift compensation. */
now = pa_rtclock_now();
time_passed = now - u->adjust_time_stamp;
if (!u->source_sink_changed && time_passed < u->adjust_time * 1.05) {
u->adjust_counter++;
u->real_adjust_time_sum += time_passed;
u->real_adjust_time = u->real_adjust_time_sum / u->adjust_counter;
+ } else {
+ u->drift_compensation_rate = 0;
+ u->drift_filter = 0;
}
u->adjust_time_stamp = now;
@@ -399,11 +450,11 @@ static void adjust_rates(struct userdata *u) {
/* Current latency */
current_latency = current_source_sink_latency + current_buffer_latency;
- /* Latency at base rate */
- latency_at_optimum_rate = current_source_sink_latency + current_buffer_latency * old_rate / base_rate;
+ /* Latency at optimum rate and latency difference */
+ latency_at_optimum_rate = current_source_sink_latency + current_buffer_latency * old_rate / (u->drift_compensation_rate + base_rate);
final_latency = PA_MAX(u->latency, u->minimum_latency);
- latency_difference = (int32_t)(latency_at_optimum_rate - final_latency);
+ latency_difference = (int32_t)(current_latency - final_latency);
pa_log_debug("Loopback overall latency is %0.2f ms + %0.2f ms + %0.2f ms = %0.2f ms",
(double) u->latency_snapshot.sink_latency / PA_USEC_PER_MSEC,
@@ -411,7 +462,7 @@ static void adjust_rates(struct userdata *u) {
(double) u->latency_snapshot.source_latency / PA_USEC_PER_MSEC,
(double) current_latency / PA_USEC_PER_MSEC);
- pa_log_debug("Loopback latency at base rate is %0.2f ms", (double)latency_at_optimum_rate / PA_USEC_PER_MSEC);
+ pa_log_debug("Loopback latency at optimum rate is %0.2f ms", (double)latency_at_optimum_rate / PA_USEC_PER_MSEC);
/* Drop or insert samples if fast_adjust_threshold_msec was specified and the latency difference is too large. */
if (u->fast_adjust_threshold > 0 && abs(latency_difference) > u->fast_adjust_threshold) {
@@ -419,15 +470,22 @@ static void adjust_rates(struct userdata *u) {
pa_asyncmsgq_send(u->sink_input->sink->asyncmsgq, PA_MSGOBJECT(u->sink_input), SINK_INPUT_MESSAGE_FAST_ADJUST, NULL, current_source_sink_latency, NULL);
- /* Skip real adjust time calculation on next iteration. */
+ /* Skip real adjust time calculation and reset drift compensation parameters on next iteration. */
u->source_sink_changed = true;
return;
}
/* Calculate new rate */
- new_rate = rate_controller(u, base_rate, old_rate, latency_difference);
+ new_rate = rate_controller(u, base_rate, old_rate, (int32_t)(latency_at_optimum_rate - final_latency), latency_difference);
+
+ /* Save current latency difference at new rate for next cycle and reset flags */
+ u->last_latency_difference = current_source_sink_latency + current_buffer_latency * old_rate / new_rate - final_latency;
+ /* Set variables that may change between calls of adjust_rate() */
u->source_sink_changed = false;
+ u->underrun_occured = false;
+ u->source_latency_offset_changed = false;
+ u->sink_latency_offset_changed = false;
/* Set rate */
pa_sink_input_set_rate(u->sink_input, new_rate);
@@ -445,6 +503,12 @@ static void time_callback(pa_mainloop_api *a, pa_time_event *e, const struct tim
/* Restart timer right away */
pa_core_rttime_restart(u->core, u->time_event, pa_rtclock_now() + u->adjust_time);
+ /* If the initial latency adjustment has not been done yet, we have to skip
+ * adjust_rates(). The estimation of the optimum rate cannot be done in that
+ * situation */
+ if (u->initial_adjust_pending)
+ return;
+
/* Get sink and source latency snapshot */
pa_asyncmsgq_send(u->sink_input->sink->asyncmsgq, PA_MSGOBJECT(u->sink_input), SINK_INPUT_MESSAGE_LATENCY_SNAPSHOT, NULL, 0, NULL);
pa_asyncmsgq_send(u->source_output->source->asyncmsgq, PA_MSGOBJECT(u->source_output), SOURCE_OUTPUT_MESSAGE_LATENCY_SNAPSHOT, NULL, 0, NULL);
@@ -745,6 +809,7 @@ static void source_output_moving_cb(pa_source_output *o, pa_source *dest) {
/* Set latency and calculate latency limits */
u->underrun_latency_limit = 0;
+ u->initial_adjust_pending = true;
update_latency_boundaries(u, dest, u->sink_input->sink);
set_source_output_latency(u, dest);
update_effective_source_latency(u, dest, u->sink_input->sink);
@@ -763,6 +828,8 @@ static void source_output_moving_cb(pa_source_output *o, pa_source *dest) {
u->underrun_counter = 0;
u->source_sink_changed = true;
+ u->underrun_occured = false;
+ u->source_latency_offset_changed = false;
/* Send a mesage to the output thread that the source has changed.
* If the sink is invalid here during a profile switching situation
@@ -938,6 +1005,10 @@ static int sink_input_process_msg_cb(pa_msgobject *obj, int code, void *data, in
* might lead to a gap in the stream */
memblockq_adjust(u, time_delta, true);
+ /* Notify main thread when the initial adjustment is done. */
+ if (u->output_thread_info.pop_called)
+ pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), LOOPBACK_MESSAGE_ADJUST_DONE, NULL, 0, NULL, NULL);
+
u->output_thread_info.pop_adjust = false;
u->output_thread_info.push_called = true;
}
@@ -1155,6 +1226,7 @@ static void sink_input_moving_cb(pa_sink_input *i, pa_sink *dest) {
/* Set latency and calculate latency limits */
u->underrun_latency_limit = 0;
+ u->initial_adjust_pending = true;
update_latency_boundaries(u, NULL, dest);
set_sink_input_latency(u, dest);
update_effective_source_latency(u, u->source_output->source, dest);
@@ -1173,6 +1245,8 @@ static void sink_input_moving_cb(pa_sink_input *i, pa_sink *dest) {
u->underrun_counter = 0;
u->source_sink_changed = true;
+ u->underrun_occured = false;
+ u->sink_latency_offset_changed = false;
u->output_thread_info.pop_called = false;
u->output_thread_info.first_pop_done = false;
@@ -1300,31 +1374,42 @@ static int loopback_process_msg_cb(pa_msgobject *o, int code, void *userdata, in
case LOOPBACK_MESSAGE_UNDERRUN:
u->underrun_counter++;
+ u->underrun_occured = true;
pa_log_debug("Underrun detected, counter incremented to %u", u->underrun_counter);
return 0;
+ case LOOPBACK_MESSAGE_ADJUST_DONE:
+
+ u->initial_adjust_pending = false;
+
+ return 0;
+
}
return 0;
}
+/* Called from main thread */
static pa_hook_result_t sink_port_latency_offset_changed_cb(pa_core *core, pa_sink *sink, struct userdata *u) {
if (sink != u->sink_input->sink)
return PA_HOOK_OK;
+ u->sink_latency_offset_changed = true;
u->sink_latency_offset = sink->port_latency_offset;
update_minimum_latency(u, sink, true);
return PA_HOOK_OK;
}
+/* Called from main thread */
static pa_hook_result_t source_port_latency_offset_changed_cb(pa_core *core, pa_source *source, struct userdata *u) {
if (source != u->source_output->source)
return PA_HOOK_OK;
+ u->source_latency_offset_changed = true;
u->source_latency_offset = source->port_latency_offset;
update_minimum_latency(u, u->sink_input->sink, true);
@@ -1459,6 +1544,10 @@ int pa__init(pa_module *m) {
u->real_adjust_time_sum = 0;
u->adjust_counter = 0;
u->fast_adjust_threshold = fast_adjust_threshold * PA_USEC_PER_MSEC;
+ u->underrun_occured = false;
+ u->source_latency_offset_changed = false;
+ u->sink_latency_offset_changed = false;
+ u->initial_adjust_pending = true;
adjust_time_sec = DEFAULT_ADJUST_TIME_USEC / PA_USEC_PER_SEC;
if (pa_modargs_get_value_u32(ma, "adjust_time", &adjust_time_sec) < 0) {