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 --- src/modules/module-loopback.c | 118 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 15 deletions(-)
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c index 9ca84b9d..12193716 100644 --- a/src/modules/module-loopback.c +++ b/src/modules/module-loopback.c @@ -58,6 +58,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) @@ -104,6 +106,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; @@ -112,8 +121,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 { @@ -190,6 +204,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); @@ -233,24 +248,26 @@ 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(new_rate_1 - base_rate) < abs(new_rate_2 - base_rate)) @@ -258,7 +275,37 @@ static uint32_t rate_controller( 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(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. @@ -361,9 +408,12 @@ static void adjust_rates(struct userdata *u) { pa_log_info("Underrun counter: %u", u->underrun_counter); } - /* Calculate real adjust time */ + /* Reset drift compensation parameters if source or sink changed, else calculate real adjust time */ now = pa_rtclock_now(); - if (!u->source_sink_changed) { + if (u->source_sink_changed) { + u->drift_compensation_rate = 0; + u->drift_filter = 0; + } else { u->adjust_counter++; u->real_adjust_time_sum += now - u->adjust_time_stamp; u->real_adjust_time = u->real_adjust_time_sum / u->adjust_counter; @@ -387,11 +437,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, @@ -399,12 +449,19 @@ 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); /* 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); @@ -422,6 +479,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); @@ -718,6 +781,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); @@ -736,6 +800,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 @@ -903,6 +969,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; } @@ -1103,6 +1173,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); @@ -1121,6 +1192,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; @@ -1235,31 +1308,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); @@ -1386,6 +1470,10 @@ int pa__init(pa_module *m) { u->source_sink_changed = true; u->real_adjust_time_sum = 0; u->adjust_counter = 0; + 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) { -- 2.14.1 _______________________________________________ pulseaudio-discuss mailing list pulseaudio-discuss@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/pulseaudio-discuss