[pulseaudio-discuss] [PATCH 08/22] loopback: Implement underrun protection

Georg Chini georg at chini.tk
Mon Feb 13 12:02:05 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.

---
 src/modules/module-loopback.c | 199 ++++++++++++++++++++++++++++--------------
 1 file changed, 132 insertions(+), 67 deletions(-)

diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
index c94f060..b84a26a 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,7 +180,8 @@ enum {
 
 enum {
     LOOPBACK_MESSAGE_SOURCE_LATENCY_CHANGED,
-    LOOPBACK_MESSAGE_SINK_LATENCY_CHANGED
+    LOOPBACK_MESSAGE_SINK_LATENCY_CHANGED,
+    LOOPBACK_MESSAGE_UNDERRUN
 };
 
 static void enable_adjust_timer(struct userdata *u, bool enable);
@@ -233,10 +243,76 @@ 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). In practice it turns out that we need two fragments to
+             * be sure it works. */
+            u->minimum_latency += 2 * u->core->default_fragment_size_msec * PA_USEC_PER_MSEC;
+
+        else if (u->min_sink_latency >= u->min_source_latency)
+            /* Initially there will be one sink latency sent to the sink while one half of the sink
+             * latency (or one source latency) remains in the memblockq. If the source latency is
+             * smaller than half the sink latency, it will deliver data at least twice within one
+             * sink latency, thereby filling the queue to more than the sink latency. If it is
+             * larger, but not as large as the sink latency, the source will provide more data once
+             * before the sink needs to be refilled. This is sufficient, because there is still
+             * one source latency in the queue. If the source latency is much larger, the situation
+             * is similar to the last case. */
+            u->minimum_latency += PA_MAX(u->min_source_latency, u->min_sink_latency / 2);
+
+        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 = 2 * u->min_source_latency;
+
+        /* Add 2 ms of safety margin */
+        u->minimum_latency += 2 * 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;
@@ -244,6 +320,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;
@@ -328,65 +424,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, bool print_msg) {
-
-    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). In practice it turns out that we need two fragments to
-         * be sure it works. */
-        u->minimum_latency += 2 * u->core->default_fragment_size_msec * PA_USEC_PER_MSEC;
-
-    else if (u->min_sink_latency >= u->min_source_latency)
-        /* Initially there will be one sink latency sent to the sink while one half of the sink
-         * latency (or one source latency) remains in the memblockq. If the source latency is
-         * smaller than half the sink latency, it will deliver data at least twice within one
-         * sink latency, thereby filling the queue to more than the sink latency. If it is
-         * larger, but not as large as the sink latency, the source will provide more data once
-         * before the sink needs to be refilled. This is sufficient, because there is still
-         * one source latency in the queue. If the source latency is much larger, the situation
-         * is similar to the last case. */
-        u->minimum_latency += PA_MAX(u->min_source_latency, u->min_sink_latency / 2);
-
-    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 = 2 * u->min_source_latency;
-
-    /* Add 2 ms of safety margin */
-    u->minimum_latency += 2 * 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 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, bool print_msg) {
@@ -628,6 +665,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, true);
     set_source_output_latency(u, dest);
     get_effective_source_latency(u, dest, u->sink_input->sink);
@@ -640,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.  Also, the current
@@ -800,15 +842,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;
@@ -982,6 +1027,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, true);
     set_sink_input_latency(u, dest);
     get_effective_source_latency(u, u->source_output->source, dest);
@@ -994,6 +1040,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;
+
     /* Send a message to the output thread that the sink has changed */
     pa_asyncmsgq_send(dest->asyncmsgq, PA_MSGOBJECT(u->sink_input), SINK_INPUT_MESSAGE_SINK_CHANGED, NULL, 0, NULL);
 
@@ -1076,6 +1126,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, true);
+                /* We re-start counting when the latency has changed */
+                u->iteration_counter = 0;
+                u->underrun_counter = 0;
             }
 
             return 0;
@@ -1090,9 +1143,18 @@ 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, true);
+                /* 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;
@@ -1227,6 +1289,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.10.1



More information about the pulseaudio-discuss mailing list