[pulseaudio-discuss] [PATCH 1/2] loopback: Implement underrun protection
Georg Chini
georg at chini.tk
Tue Apr 11 20:43:32 UTC 2017
The latency controller will try to adjust to the configured latency regardless
of underruns. If the configured latency is set too small, it will lead to
periodically occuring underruns. Therefore an underrun protection is implemented
which will increase the target latency if too many underruns are detected.
Underruns are tracked and if more than 3 underruns occur, the target latency
is increased in increments of 5 ms. One underrun per hour is accepted.
The protection ensures, that independent from the configured latency the
module will converge to a stable latency if the configured latency is too
small.
The print_msg argument to update_minimum_latency() had to be re-introduced,
because there is one place where the message should not be logged.
---
src/modules/module-loopback.c | 185 +++++++++++++++++++++++++++++-------------
1 file changed, 127 insertions(+), 58 deletions(-)
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index 2a0d7075..217b517f 100644
--- a/src/modules/module-loopback.c
+++ b/src/modules/module-loopback.c
@@ -98,6 +98,13 @@ struct userdata {
int64_t sink_latency_offset;
pa_usec_t minimum_latency;
+ /* lower latency limit found by underruns */
+ pa_usec_t underrun_latency_limit;
+
+ /* Various counters */
+ uint32_t iteration_counter;
+ uint32_t underrun_counter;
+
bool fixed_alsa_source;
/* Used for sink input and source output snapshots */
@@ -118,6 +125,8 @@ struct userdata {
/* Output thread variables */
struct {
int64_t recv_counter;
+
+ /* Copied from main thread */
pa_usec_t effective_source_latency;
pa_usec_t minimum_latency;
@@ -171,6 +180,7 @@ enum {
enum {
LOOPBACK_MESSAGE_SOURCE_LATENCY_RANGE_CHANGED,
LOOPBACK_MESSAGE_SINK_LATENCY_RANGE_CHANGED,
+ LOOPBACK_MESSAGE_UNDERRUN,
};
static void enable_adjust_timer(struct userdata *u, bool enable);
@@ -232,10 +242,68 @@ static uint32_t rate_controller(
return new_rate;
}
+/* Called from main thread.
+ * It has been a matter of discussion how to correctly calculate the minimum
+ * latency that module-loopback can deliver with a given source and sink.
+ * The calculation has been placed in a separate function so that the definition
+ * can easily be changed. The resulting estimate is not very exact because it
+ * depends on the reported latency ranges. In cases were the lower bounds of
+ * source and sink latency are not reported correctly (USB) the result will
+ * be wrong. */
+static void update_minimum_latency(struct userdata *u, pa_sink *sink, bool print_msg) {
+
+ if (u->underrun_latency_limit)
+ /* If we already detected a real latency limit because of underruns, use it */
+ u->minimum_latency = u->underrun_latency_limit;
+
+ else {
+ /* Calculate latency limit from latency ranges */
+
+ u->minimum_latency = u->min_sink_latency;
+ if (u->fixed_alsa_source)
+ /* If we are using an alsa source with fixed latency, we will get a wakeup when
+ * one fragment is filled, and then we empty the source buffer, so the source
+ * latency never grows much beyond one fragment (assuming that the CPU doesn't
+ * cause a bottleneck). */
+ u->minimum_latency += u->core->default_fragment_size_msec * PA_USEC_PER_MSEC;
+
+ else
+ /* In all other cases the source will deliver new data at latest after one source latency.
+ * Make sure there is enough data available that the sink can keep on playing until new
+ * data is pushed. */
+ u->minimum_latency += u->min_source_latency;
+
+ /* Multiply by 1.1 as a safety margin for delays that are proportional to the buffer sizes */
+ u->minimum_latency *= 1.1;
+
+ /* Add 1.5 ms as a safety margin for delays not related to the buffer sizes */
+ u->minimum_latency += 1.5 * PA_USEC_PER_MSEC;
+ }
+
+ /* Add the latency offsets */
+ if (-(u->sink_latency_offset + u->source_latency_offset) <= (int64_t)u->minimum_latency)
+ u->minimum_latency += u->sink_latency_offset + u->source_latency_offset;
+ else
+ u->minimum_latency = 0;
+
+ /* If the sink is valid, send a message to update the minimum latency to
+ * the output thread, else set the variable directly */
+ if (sink)
+ pa_asyncmsgq_send(sink->asyncmsgq, PA_MSGOBJECT(u->sink_input), SINK_INPUT_MESSAGE_UPDATE_MIN_LATENCY, NULL, u->minimum_latency, NULL);
+ else
+ u->output_thread_info.minimum_latency = u->minimum_latency;
+
+ if (print_msg) {
+ pa_log_info("Minimum possible end to end latency: %0.2f ms", (double)u->minimum_latency / PA_USEC_PER_MSEC);
+ if (u->latency < u->minimum_latency)
+ pa_log_warn("Configured latency of %0.2f ms is smaller than minimum latency, using minimum instead", (double)u->latency / PA_USEC_PER_MSEC);
+ }
+}
+
/* Called from main context */
static void adjust_rates(struct userdata *u) {
size_t buffer;
- uint32_t old_rate, base_rate, new_rate;
+ uint32_t old_rate, base_rate, new_rate, run_hours;
int32_t latency_difference;
pa_usec_t current_buffer_latency, snapshot_delay, current_source_sink_latency, current_latency, latency_at_optimum_rate;
pa_usec_t final_latency;
@@ -243,6 +311,26 @@ static void adjust_rates(struct userdata *u) {
pa_assert(u);
pa_assert_ctl_context();
+ /* Runtime and counters since last change of source or sink
+ * or source/sink latency */
+ run_hours = u->iteration_counter * u->adjust_time / PA_USEC_PER_SEC / 3600;
+ u->iteration_counter +=1;
+
+ /* If we are seeing underruns then the latency is too small */
+ if (u->underrun_counter > 2) {
+ u->underrun_latency_limit = PA_MAX(u->latency, u->minimum_latency) + 5 * PA_USEC_PER_MSEC;
+ u->underrun_latency_limit = PA_CLIP_SUB((int64_t)u->underrun_latency_limit, u->sink_latency_offset + u->source_latency_offset);
+ update_minimum_latency(u, u->sink_input->sink, false);
+ pa_log_warn("Too many underruns, increasing latency to %0.2f ms", (double)u->minimum_latency / PA_USEC_PER_MSEC);
+ u->underrun_counter = 0;
+ }
+
+ /* Allow one underrun per hour */
+ if (u->iteration_counter * u->adjust_time / PA_USEC_PER_SEC / 3600 > run_hours) {
+ u->underrun_counter = PA_CLIP_SUB(u->underrun_counter, 1u);
+ pa_log_info("Underrun counter: %u", u->underrun_counter);
+ }
+
/* Rates and latencies*/
old_rate = u->sink_input->sample_spec.rate;
base_rate = u->source_output->sample_spec.rate;
@@ -327,54 +415,6 @@ static void update_adjust_timer(struct userdata *u) {
enable_adjust_timer(u, true);
}
-/* Called from main thread.
- * It has been a matter of discussion how to correctly calculate the minimum
- * latency that module-loopback can deliver with a given source and sink.
- * The calculation has been placed in a separate function so that the definition
- * can easily be changed. The resulting estimate is not very exact because it
- * depends on the reported latency ranges. In cases were the lower bounds of
- * source and sink latency are not reported correctly (USB) the result will
- * be wrong. */
-static void update_minimum_latency(struct userdata *u, pa_sink *sink) {
-
- u->minimum_latency = u->min_sink_latency;
- if (u->fixed_alsa_source)
- /* If we are using an alsa source with fixed latency, we will get a wakeup when
- * one fragment is filled, and then we empty the source buffer, so the source
- * latency never grows much beyond one fragment (assuming that the CPU doesn't
- * cause a bottleneck). */
- u->minimum_latency += u->core->default_fragment_size_msec * PA_USEC_PER_MSEC;
-
- else
- /* In all other cases the source will deliver new data at latest after one source latency.
- * Make sure there is enough data available that the sink can keep on playing until new
- * data is pushed. */
- u->minimum_latency += u->min_source_latency;
-
- /* Multiply by 1.1 as a safety margin for delays that are proportional to the buffer sizes */
- u->minimum_latency *= 1.1;
-
- /* Add 1.5 ms as a safety margin for delays not related to the buffer sizes */
- u->minimum_latency += 1.5 * PA_USEC_PER_MSEC;
-
- /* Add the latency offsets */
- if (-(u->sink_latency_offset + u->source_latency_offset) <= (int64_t)u->minimum_latency)
- u->minimum_latency += u->sink_latency_offset + u->source_latency_offset;
- else
- u->minimum_latency = 0;
-
- /* If the sink is valid, send a message to update the minimum latency to
- * the output thread, else set the variable directly */
- if (sink)
- pa_asyncmsgq_send(sink->asyncmsgq, PA_MSGOBJECT(u->sink_input), SINK_INPUT_MESSAGE_UPDATE_MIN_LATENCY, NULL, u->minimum_latency, NULL);
- else
- u->output_thread_info.minimum_latency = u->minimum_latency;
-
- pa_log_info("Minimum possible end to end latency: %0.2f ms", (double)u->minimum_latency / PA_USEC_PER_MSEC);
- if (u->latency < u->minimum_latency)
- pa_log_warn("Configured latency of %0.2f ms is smaller than minimum latency, using minimum instead", (double)u->latency / PA_USEC_PER_MSEC);
-}
-
/* Called from main thread
* Calculates minimum and maximum possible latency for source and sink */
static void update_latency_boundaries(struct userdata *u, pa_source *source, pa_sink *sink) {
@@ -421,7 +461,7 @@ static void update_latency_boundaries(struct userdata *u, pa_source *source, pa_
u->min_sink_latency = u->max_sink_latency;
}
- update_minimum_latency(u, sink);
+ update_minimum_latency(u, sink, true);
}
/* Called from output context
@@ -624,6 +664,7 @@ static void source_output_moving_cb(pa_source_output *o, pa_source *dest) {
pa_sink_input_set_property(u->sink_input, PA_PROP_DEVICE_ICON_NAME, n);
/* Set latency and calculate latency limits */
+ u->underrun_latency_limit = 0;
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);
@@ -637,6 +678,10 @@ static void source_output_moving_cb(pa_source_output *o, pa_source *dest) {
update_adjust_timer(u);
+ /* Reset counters */
+ u->iteration_counter = 0;
+ u->underrun_counter = 0;
+
/* Send a mesage to the output thread that the source has changed.
* If the sink is invalid here during a profile switching situation
* we can safely set push_called to false directly. */
@@ -814,15 +859,18 @@ static int sink_input_process_msg_cb(pa_msgobject *obj, int code, void *data, in
/* Is this the end of an underrun? Then let's start things
* right-away */
- if (!u->output_thread_info.in_pop &&
- u->sink_input->sink->thread_info.state != PA_SINK_SUSPENDED &&
+ if (u->sink_input->sink->thread_info.state != PA_SINK_SUSPENDED &&
u->sink_input->thread_info.underrun_for > 0 &&
pa_memblockq_is_readable(u->memblockq)) {
- pa_log_debug("Requesting rewind due to end of underrun.");
- pa_sink_input_request_rewind(u->sink_input,
- (size_t) (u->sink_input->thread_info.underrun_for == (size_t) -1 ? 0 : u->sink_input->thread_info.underrun_for),
- false, true, false);
+ pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), LOOPBACK_MESSAGE_UNDERRUN, NULL, 0, NULL, NULL);
+ /* If called from within the pop callback skip the rewind */
+ if (!u->output_thread_info.in_pop) {
+ pa_log_debug("Requesting rewind due to end of underrun.");
+ pa_sink_input_request_rewind(u->sink_input,
+ (size_t) (u->sink_input->thread_info.underrun_for == (size_t) -1 ? 0 : u->sink_input->thread_info.underrun_for),
+ false, true, false);
+ }
}
u->output_thread_info.recv_counter += (int64_t) chunk->length;
@@ -998,6 +1046,7 @@ static void sink_input_moving_cb(pa_sink_input *i, pa_sink *dest) {
pa_source_output_set_property(u->source_output, PA_PROP_MEDIA_ICON_NAME, n);
/* Set latency and calculate latency limits */
+ u->underrun_latency_limit = 0;
update_latency_boundaries(u, NULL, dest);
set_sink_input_latency(u, dest);
update_effective_source_latency(u, u->source_output->source, dest);
@@ -1011,6 +1060,10 @@ static void sink_input_moving_cb(pa_sink_input *i, pa_sink *dest) {
update_adjust_timer(u);
+ /* Reset counters */
+ u->iteration_counter = 0;
+ u->underrun_counter = 0;
+
u->output_thread_info.pop_called = false;
u->output_thread_info.first_pop_done = false;
@@ -1096,6 +1149,9 @@ static int loopback_process_msg_cb(pa_msgobject *o, int code, void *userdata, in
pa_log_warn("Source minimum latency increased to %0.2f ms", (double)current_latency / PA_USEC_PER_MSEC);
u->configured_source_latency = current_latency;
update_latency_boundaries(u, u->source_output->source, u->sink_input->sink);
+ /* We re-start counting when the latency has changed */
+ u->iteration_counter = 0;
+ u->underrun_counter = 0;
}
return 0;
@@ -1111,9 +1167,19 @@ static int loopback_process_msg_cb(pa_msgobject *o, int code, void *userdata, in
pa_log_warn("Sink minimum latency increased to %0.2f ms", (double)current_latency / PA_USEC_PER_MSEC);
u->configured_sink_latency = current_latency;
update_latency_boundaries(u, u->source_output->source, u->sink_input->sink);
+ /* We re-start counting when the latency has changed */
+ u->iteration_counter = 0;
+ u->underrun_counter = 0;
}
return 0;
+
+ case LOOPBACK_MESSAGE_UNDERRUN:
+
+ u->underrun_counter++;
+
+ return 0;
+
}
return 0;
@@ -1125,7 +1191,7 @@ static pa_hook_result_t sink_port_latency_offset_changed_cb(pa_core *core, pa_si
return PA_HOOK_OK;
u->sink_latency_offset = sink->port_latency_offset;
- update_minimum_latency(u, sink);
+ update_minimum_latency(u, sink, true);
return PA_HOOK_OK;
}
@@ -1136,7 +1202,7 @@ static pa_hook_result_t source_port_latency_offset_changed_cb(pa_core *core, pa_
return PA_HOOK_OK;
u->source_latency_offset = source->port_latency_offset;
- update_minimum_latency(u, u->sink_input->sink);
+ update_minimum_latency(u, u->sink_input->sink, true);
return PA_HOOK_OK;
}
@@ -1242,6 +1308,9 @@ int pa__init(pa_module *m) {
u->output_thread_info.pop_called = false;
u->output_thread_info.pop_adjust = false;
u->output_thread_info.push_called = false;
+ u->iteration_counter = 0;
+ u->underrun_counter = 0;
+ u->underrun_latency_limit = 0;
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.11.0
More information about the pulseaudio-discuss
mailing list