[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