[pulseaudio-discuss] [PATCH] loopback: Calculate and track minimum possible latency

Peter Meerwald-Stadler pmeerw at pmeerw.net
Mon Apr 3 17:32:40 UTC 2017


> With the current code, the user can request any end-to-end latency. Because there
> is no protection against underruns, setting the latency too small will result in
> repetitive underruns.
> 
> This patch tries to mitigate the problem by calculating the minimum possible latency
> for the current combination of source and sink. The actual calculation has been put
> in a separate function so it can easily be changed. To keep the values up to date,
> changes in the latency ranges have to be tracked.

nitpicking below

> ---
>  src/modules/module-loopback.c | 213 ++++++++++++++++++++++++++++++++++++++++--
>  1 file changed, 204 insertions(+), 9 deletions(-)
> 
> diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
> index 9b4ea49..4a65cc3 100644
> --- a/src/modules/module-loopback.c
> +++ b/src/modules/module-loopback.c
> @@ -65,10 +65,14 @@ PA_MODULE_USAGE(
>  
>  #define DEFAULT_ADJUST_TIME_USEC (10*PA_USEC_PER_SEC)
>  
> +typedef struct loopback_msg loopback_msg;
> +
>  struct userdata {
>      pa_core *core;
>      pa_module *module;
>  
> +    loopback_msg *msg;
> +
>      pa_sink_input *sink_input;
>      pa_source_output *source_output;
>  
> @@ -90,6 +94,11 @@ struct userdata {
>      pa_usec_t max_sink_latency;
>      pa_usec_t configured_sink_latency;
>      pa_usec_t configured_source_latency;
> +    int64_t source_latency_offset;
> +    int64_t sink_latency_offset;
> +    pa_usec_t minimum_latency;
> +
> +    bool fixed_alsa_source;
>  
>      /* Used for sink input and source output snapshots */
>      struct {
> @@ -110,6 +119,7 @@ struct userdata {
>      struct {
>          int64_t recv_counter;
>          pa_usec_t effective_source_latency;
> +        pa_usec_t minimum_latency;
>  
>          /* Various booleans */
>          bool in_pop;
> @@ -120,6 +130,14 @@ struct userdata {
>      } output_thread_info;
>  };
>  
> +struct loopback_msg {
> +    pa_msgobject parent;
> +    struct userdata *userdata;
> +};
> +
> +PA_DEFINE_PRIVATE_CLASS(loopback_msg, pa_msgobject);
> +#define LOOPBACK_MSG(o) (loopback_msg_cast(o))
> +
>  static const char* const valid_modargs[] = {
>      "source",
>      "sink",
> @@ -142,13 +160,19 @@ enum {
>      SINK_INPUT_MESSAGE_REWIND,
>      SINK_INPUT_MESSAGE_LATENCY_SNAPSHOT,
>      SINK_INPUT_MESSAGE_SOURCE_CHANGED,
> -    SINK_INPUT_MESSAGE_SET_EFFECTIVE_SOURCE_LATENCY
> +    SINK_INPUT_MESSAGE_SET_EFFECTIVE_SOURCE_LATENCY,

I think it is good pratice to end enumerations with , (comma) so that 
adding to the list changes one line and not two

> +    SINK_INPUT_MESSAGE_UPDATE_MIN_LATENCY
>  };
>  
>  enum {
>      SOURCE_OUTPUT_MESSAGE_LATENCY_SNAPSHOT = PA_SOURCE_OUTPUT_MESSAGE_MAX
>  };
>  
> +enum {
> +    LOOPBACK_MESSAGE_SOURCE_LATENCY_RANGE_CHANGED,
> +    LOOPBACK_MESSAGE_SINK_LATENCY_RANGE_CHANGED
> +};
> +
>  static void enable_adjust_timer(struct userdata *u, bool enable);
>  
>  /* Called from main context */
> @@ -239,7 +263,7 @@ static void adjust_rates(struct userdata *u) {
>      /* Latency at base rate */
>      latency_at_optimum_rate = current_source_sink_latency + current_buffer_latency * old_rate / base_rate;
>  
> -    final_latency = u->latency;
> +    final_latency = PA_MAX(u->latency, u->minimum_latency);
>      latency_difference = (int32_t)((int64_t)latency_at_optimum_rate - final_latency);
>  
>      pa_log_debug("Loopback overall latency is %0.2f ms + %0.2f ms + %0.2f ms = %0.2f ms",
> @@ -303,18 +327,77 @@ 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). */
> +        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

two spaces before until

> +         * data is pushed. */
> +        u->minimum_latency += u->min_source_latency;
> +
> +    /* Multiply by 1.1 as a safety margin for delays related 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;

really set to 0? this doesn't go well with the comment above

> +
> +    /* 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) {
> +static void update_latency_boundaries(struct userdata *u, pa_source *source, pa_sink *sink, bool print_msg) {
> +    const char *s;
>  
>      if (source) {
>          /* Source latencies */
> +        u->fixed_alsa_source = false;
>          if (source->flags & PA_SOURCE_DYNAMIC_LATENCY)
>              pa_source_get_latency_range(source, &u->min_source_latency, &u->max_source_latency);
>          else {
>              u->min_source_latency = pa_source_get_fixed_latency(source);
>              u->max_source_latency = u->min_source_latency;
> +            if ((s = pa_proplist_gets(source->proplist, PA_PROP_DEVICE_API))) {
> +                if (pa_streq(s, "alsa"))
> +                    u->fixed_alsa_source = true;
> +            }
>          }
> +        /* Source offset */
> +        u->source_latency_offset = source->port_latency_offset;
> +
>          /* Latencies below 2.5 ms cause problems, limit source latency if possible */
>          if (u->max_source_latency >= MIN_DEVICE_LATENCY)
>              u->min_source_latency = PA_MAX(u->min_source_latency, MIN_DEVICE_LATENCY);
> @@ -330,21 +413,27 @@ static void update_latency_boundaries(struct userdata *u, pa_source *source, pa_
>              u->min_sink_latency = pa_sink_get_fixed_latency(sink);
>              u->max_sink_latency = u->min_sink_latency;
>          }
> +        /* Sink offset */
> +        u->sink_latency_offset = sink->port_latency_offset;
> +
>          /* Latencies below 2.5 ms cause problems, limit sink latency if possible */
>          if (u->max_sink_latency >= MIN_DEVICE_LATENCY)
>              u->min_sink_latency = PA_MAX(u->min_sink_latency, MIN_DEVICE_LATENCY);
>          else
>              u->min_sink_latency = u->max_sink_latency;
>      }
> +
> +    update_minimum_latency(u, sink, print_msg);
>  }
>  
>  /* Called from output context
>   * Sets the memblockq to the configured latency corrected by latency_offset_usec */
>  static void memblockq_adjust(struct userdata *u, pa_usec_t latency_offset_usec, bool allow_push) {
>      size_t current_memblockq_length, requested_memblockq_length, buffer_correction;
> -    pa_usec_t requested_buffer_latency;
> +    pa_usec_t requested_buffer_latency, final_latency;
>  
> -    requested_buffer_latency = PA_CLIP_SUB(u->latency, latency_offset_usec);
> +    final_latency = PA_MAX(u->latency, u->output_thread_info.minimum_latency);
> +    requested_buffer_latency = PA_CLIP_SUB(final_latency, latency_offset_usec);
>      requested_memblockq_length = pa_usec_to_bytes(requested_buffer_latency, &u->sink_input->sample_spec);
>      current_memblockq_length = pa_memblockq_get_length(u->memblockq);
>  
> @@ -447,6 +536,14 @@ static void set_source_output_latency(struct userdata *u, pa_source *source) {
>  
>      requested_latency = u->latency / 3;
>  
> +    /* Normally we try to configure sink and source latency equally. If the
> +     * sink latency cannot match the requested source latency try to set the
> +     * source latency to a smaller value to avoid underruns */
> +    if (u->min_sink_latency > requested_latency) {
> +        latency = PA_MAX(u->latency, u->minimum_latency);
> +        requested_latency = (latency - u->min_sink_latency) / 2;
> +    }
> +
>      latency = PA_CLAMP(requested_latency , u->min_source_latency, u->max_source_latency);
>      u->configured_source_latency = pa_source_output_set_requested_latency(u->source_output, latency);
>      if (u->configured_source_latency != requested_latency)
> @@ -529,7 +626,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 */
> -    update_latency_boundaries(u, dest, NULL);
> +    update_latency_boundaries(u, dest, u->sink_input->sink, true);
>      set_source_output_latency(u, dest);
>      update_effective_source_latency(u, dest, u->sink_input->sink);
>  
> @@ -576,6 +673,18 @@ static void source_output_suspend_cb(pa_source_output *o, bool suspended) {
>      update_adjust_timer(u);
>  }
>  
> +/* Called from input thread context */
> +static void update_source_latency_range_cb(pa_source_output *i) {
> +    struct userdata *u;
> +
> +    pa_source_output_assert_ref(i);
> +    pa_source_output_assert_io_context(i);
> +    pa_assert_se(u = i->userdata);
> +
> +    /* Source latency may have changed */
> +    pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), LOOPBACK_MESSAGE_SOURCE_LATENCY_RANGE_CHANGED, NULL, 0, NULL, NULL);
> +}
> +
>  /* Called from output thread context */
>  static int sink_input_pop_cb(pa_sink_input *i, size_t nbytes, pa_memchunk *chunk) {
>      struct userdata *u;
> @@ -719,7 +828,9 @@ static int sink_input_process_msg_cb(pa_msgobject *obj, int code, void *data, in
>  
>          case SINK_INPUT_MESSAGE_REWIND:
>  
> -            pa_memblockq_seek(u->memblockq, -offset, PA_SEEK_RELATIVE, true);
> +            /* Do not try to rewind if no data was pushed yet */
> +            if (u->output_thread_info.push_called)
> +                pa_memblockq_seek(u->memblockq, -offset, PA_SEEK_RELATIVE, true);
>  
>              u->output_thread_info.recv_counter -= offset;
>  
> @@ -751,6 +862,12 @@ static int sink_input_process_msg_cb(pa_msgobject *obj, int code, void *data, in
>              u->output_thread_info.effective_source_latency = (pa_usec_t)offset;
>  
>              return 0;
> +
> +        case SINK_INPUT_MESSAGE_UPDATE_MIN_LATENCY:
> +
> +            u->output_thread_info.minimum_latency = (pa_usec_t)offset;
> +
> +            return 0;
>      }
>  
>      return pa_sink_input_process_msg(obj, code, data, offset, chunk);
> @@ -765,6 +882,14 @@ static void set_sink_input_latency(struct userdata *u, pa_sink *sink) {
>  
>      requested_latency = u->latency / 3;
>  
> +    /* Normally we try to configure sink and source latency equally. If the
> +     * source latency cannot match the requested sink latency try to set the
> +     * sink latency to a smaller value to avoid underruns */
> +    if (u->min_source_latency > requested_latency) {
> +        latency = PA_MAX(u->latency, u->minimum_latency);
> +        requested_latency = (latency - u->min_source_latency) / 2;
> +    }
> +
>      latency = PA_CLAMP(requested_latency , u->min_sink_latency, u->max_sink_latency);
>      u->configured_sink_latency = pa_sink_input_set_requested_latency(u->sink_input, latency);
>      if (u->configured_sink_latency != requested_latency)
> @@ -870,7 +995,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 */
> -    update_latency_boundaries(u, NULL, dest);
> +    update_latency_boundaries(u, NULL, dest, true);
>      set_sink_input_latency(u, dest);
>      update_effective_source_latency(u, u->source_output->source, dest);
>  
> @@ -925,6 +1050,67 @@ static void sink_input_suspend_cb(pa_sink_input *i, bool suspended) {
>      update_adjust_timer(u);
>  }
>  
> +/* Called from output thread context */
> +static void update_sink_latency_range_cb(pa_sink_input *i) {
> +    struct userdata *u;
> +
> +    pa_sink_input_assert_ref(i);
> +    pa_sink_input_assert_io_context(i);
> +    pa_assert_se(u = i->userdata);
> +
> +    /* Sink latency may have changed */
> +    pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(u->msg), LOOPBACK_MESSAGE_SINK_LATENCY_RANGE_CHANGED, NULL, 0, NULL, NULL);
> +}
> +
> +/* Called from main context */
> +static int loopback_process_msg_cb(pa_msgobject *o, int code, void *userdata, int64_t offset, pa_memchunk *chunk) {
> +    struct loopback_msg *msg;
> +    struct userdata *u;
> +    pa_usec_t current_latency;
> +
> +    pa_assert(o);
> +    pa_assert_ctl_context();
> +
> +    msg = LOOPBACK_MSG(o);
> +    pa_assert_se(u = msg->userdata);
> +
> +    switch (code) {
> +
> +        case LOOPBACK_MESSAGE_SOURCE_LATENCY_RANGE_CHANGED:
> +
> +            update_effective_source_latency(u, u->source_output->source, u->sink_input->sink);
> +            current_latency = pa_source_get_requested_latency(u->source_output->source);
> +            if (current_latency > u->configured_source_latency) {
> +                /* The minimum latency has changed to a value larger than the configured latency. so

, so

> +                 * the source latency has been increased. The case that the minimum latency changes
> +                 * back to a smaller value is not handled because this never happens with the current
> +                 * source implementations */

.

> +                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, false);
> +            }
> +
> +            return 0;
> +
> +        case LOOPBACK_MESSAGE_SINK_LATENCY_RANGE_CHANGED:
> +
> +            current_latency = pa_sink_get_requested_latency(u->sink_input->sink);
> +            if (current_latency > u->configured_sink_latency) {
> +                /* The minimum latency has changed to a value larger than the configured latency, so
> +                 * the sink latency has been increased. The case that the minimum latency changes back
> +                 * to a smaller value is not handled because this never happens with the current sink
> +                 * implementations */
> +                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, false);
> +            }
> +
> +            return 0;
> +    }
> +
> +    return 0;
> +}
> +
>  int pa__init(pa_module *m) {
>      pa_modargs *ma = NULL;
>      struct userdata *u;
> @@ -1102,6 +1288,8 @@ int pa__init(pa_module *m) {
>      u->sink_input->may_move_to = sink_input_may_move_to_cb;
>      u->sink_input->moving = sink_input_moving_cb;
>      u->sink_input->suspend = sink_input_suspend_cb;
> +    u->sink_input->update_sink_latency_range = update_sink_latency_range_cb;
> +    u->sink_input->update_sink_fixed_latency = update_sink_latency_range_cb;
>      u->sink_input->userdata = u;
>  
>      pa_source_output_new_data_init(&source_output_data);
> @@ -1150,9 +1338,11 @@ int pa__init(pa_module *m) {
>      u->source_output->may_move_to = source_output_may_move_to_cb;
>      u->source_output->moving = source_output_moving_cb;
>      u->source_output->suspend = source_output_suspend_cb;
> +    u->source_output->update_source_latency_range = update_source_latency_range_cb;
> +    u->source_output->update_source_fixed_latency = update_source_latency_range_cb;
>      u->source_output->userdata = u;
>  
> -    update_latency_boundaries(u, u->source_output->source, u->sink_input->sink);
> +    update_latency_boundaries(u, u->source_output->source, u->sink_input->sink, true);
>      set_sink_input_latency(u, u->sink_input->sink);
>      set_source_output_latency(u, u->source_output->source);
>  
> @@ -1193,6 +1383,11 @@ int pa__init(pa_module *m) {
>              && (n = pa_proplist_gets(u->source_output->source->proplist, PA_PROP_DEVICE_ICON_NAME)))
>          pa_proplist_sets(u->sink_input->proplist, PA_PROP_MEDIA_ICON_NAME, n);
>  
> +    /* Setup message handler for main thread */
> +    u->msg = pa_msgobject_new(loopback_msg);
> +    u->msg->parent.process_msg = loopback_process_msg_cb;
> +    u->msg->userdata = u;
> +
>      /* The output thread is not yet running, set effective_source_latency directly */
>      update_effective_source_latency(u, u->source_output->source, NULL);
>  
> -- 
> 2.10.1
> 
> _______________________________________________
> pulseaudio-discuss mailing list
> pulseaudio-discuss at lists.freedesktop.org
> https://lists.freedesktop.org/mailman/listinfo/pulseaudio-discuss
> 

-- 

Peter Meerwald-Stadler
Mobile: +43 664 24 44 418


More information about the pulseaudio-discuss mailing list