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. The calculated minimum latency is used to limit the configured latency. The minimum latency is only a "best guess", so the actual minimum may be much larger (for example for USB devices) or much smaller than the calculated value. Port latency offsets are not yet handled properly, this will be done in a separate patch. --- src/modules/module-loopback.c | 199 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 8 deletions(-) diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c index 9b4ea49..e2cd986 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, + SINK_INPUT_MESSAGE_UPDATE_MIN_LATENCY }; enum { SOURCE_OUTPUT_MESSAGE_LATENCY_SNAPSHOT = PA_SOURCE_OUTPUT_MESSAGE_MAX }; +enum { + LOOPBACK_MESSAGE_SOURCE_LATENCY_CHANGED, + LOOPBACK_MESSAGE_SINK_LATENCY_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,85 @@ 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. */ + 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) { +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 +421,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); @@ -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_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; @@ -751,6 +860,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); @@ -870,7 +985,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 +1040,65 @@ 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_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_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 + * the source latency has been increased. The case that the minimum latency changes + * back to a smaller value is not handled because this is currently not implemented */ + 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_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 is currently not implemented */ + 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 +1276,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 +1326,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 +1371,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