[pulseaudio-discuss] [PATCH 2/3] stream-interaction: Update interaction logic.

Tanu Kaskinen tanuk at iki.fi
Tue Aug 7 12:27:48 UTC 2018


On Fri, 2018-07-13 at 11:21 +0300, Juho Hämäläinen wrote:
> Stream interaction groups now have hashmaps for all trigger and interaction
> states and roles. We track both trigger streams and interaction streams
> separately, so that applying interaction (ducking or cork and muting) is
> relatively simple.

You forgot the "why" part from the commit message. (Based on our
earlier discussion the goal is to make the code easier to understand.)

> Signed-off-by: Juho Hämäläinen <jusa at hilvi.org>
> ---
>  src/modules/stream-interaction.c | 388 ++++++++++++++++++++-------------------
>  1 file changed, 200 insertions(+), 188 deletions(-)
> 
> diff --git a/src/modules/stream-interaction.c b/src/modules/stream-interaction.c
> index d538e88..c30a8c0 100644
> --- a/src/modules/stream-interaction.c
> +++ b/src/modules/stream-interaction.c
> @@ -42,10 +42,13 @@
>  
>  struct group {
>      char *name;
> -    pa_idxset *trigger_roles;
> -    pa_idxset *interaction_roles;
> +    pa_hashmap *trigger_roles;
> +    pa_hashmap *interaction_roles;
> +    pa_hashmap *trigger_state;
>      pa_hashmap *interaction_state;

Could you add comments about what these hashmaps contain and maybe
something about how they are used? I'm reading
update_trigger_streams(), and I have some trouble understanding it. One
obstacle to understanding is that I don't know what trigger_state
contains, except that the keys are sink inputs. I could look elsewhere
in the code to figure it out, but it would be nice to have a reference
here in the struct definition.

The hashmaps seem to be used as sets, i.e. you don't care about the
values, you just want to be able to look up the keys. I suggest using
the key as the value, that way you can use PA_HASHMAP_FOREACH instead
of PA_HASHMAP_FOREACH_KV.

>      pa_volume_t volume;
> +    bool any_role;
> +    bool active;
>  
>      PA_LLIST_FIELDS(struct group);
>  };
> @@ -55,211 +58,228 @@ struct userdata {
>      PA_LLIST_HEAD(struct group, groups);
>      bool global:1;
>      bool duck:1;
> -    pa_hook_slot
> -        *sink_input_put_slot,
> -        *sink_input_unlink_slot,
> -        *sink_input_move_start_slot,
> -        *sink_input_move_finish_slot,
> -        *sink_input_state_changed_slot,
> -        *sink_input_mute_changed_slot,
> -        *sink_input_proplist_changed_slot;
>  };
>  
> -static const char *get_trigger_role(struct userdata *u, pa_sink_input *i, struct group *g) {
> -    const char *role, *trigger_role;
> -    uint32_t role_idx;
> +static const char *sink_input_role(pa_sink_input *sink_input) {
> +    const char *role;
>  
> -    if (!(role = pa_proplist_gets(i->proplist, PA_PROP_MEDIA_ROLE)))
> -        role = STREAM_INTERACTION_NO_ROLE;
> +    pa_assert(sink_input);
>  
> -    if (g == NULL) {
> -        /* get it from all groups */
> -        PA_LLIST_FOREACH(g, u->groups) {
> -            PA_IDXSET_FOREACH(trigger_role, g->trigger_roles, role_idx) {
> -                if (pa_streq(role, trigger_role))
> -                    return trigger_role;
> -            }
> -        }
> -    } else {
> -        PA_IDXSET_FOREACH(trigger_role, g->trigger_roles, role_idx) {
> -            if (pa_streq(role, trigger_role))
> -                return trigger_role;
> -        }
> -    }
> +    if (!(role = pa_proplist_gets(sink_input->proplist, PA_PROP_MEDIA_ROLE)))
> +        role = STREAM_INTERACTION_NO_ROLE;
>  
> -    return NULL;
> +    return role;
>  }
>  
> -static const char *find_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_input *ignore, struct group *g) {
> -    pa_sink_input *j;
> -    uint32_t idx;
> -    const char *trigger_role;
> +/* Return true if group state changes between
> + * active and inactive. */
> +static bool update_group_active(struct userdata *u, struct group *g) {
> +    void *state = NULL;
> +    bool new_active = false;
> +    pa_sink_input *sink_input = NULL;
> +    void *value;
>  
>      pa_assert(u);
> -    pa_sink_assert_ref(s);
> -
> -    for (j = PA_SINK_INPUT(pa_idxset_first(s->inputs, &idx)); j; j = PA_SINK_INPUT(pa_idxset_next(s->inputs, &idx))) {
> +    pa_assert(g);
>  
> -        if (j == ignore)
> -            continue;
> +    if (pa_hashmap_size(g->trigger_state) > 0) {
> +        PA_HASHMAP_FOREACH_KV(sink_input, value, g->trigger_state, state) {
> +            if (!sink_input->muted &&
> +                sink_input->state != PA_SINK_INPUT_CORKED) {
> +                new_active = true;
> +                break;
> +            }
> +        }
> +    }

The pa_hashmap_size() check doesn't seem to do anything useful. If the
hashmap is empty, the iteration loop won't run anyway.

>  
> -        trigger_role = get_trigger_role(u, j, g);
> -        if (trigger_role && !j->muted && j->state != PA_SINK_INPUT_CORKED)
> -            return trigger_role;
> +    if (new_active != g->active) {
> +        pa_log_debug("Group '%s' is now %sactive.", g->name, new_active ? "" : "in");
> +        g->active = new_active;
> +        return true;
>      }
>  
> -    return NULL;
> +    return false;
>  }
>  
> -static const char *find_global_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_input *ignore, struct group *g) {
> -    const char *trigger_role = NULL;
> +/* Identify trigger streams and add or remove the streams from
> + * state hashmap. Proplist change when changing media.role may
> + * result in already existing stream to gain or lose trigger role
> + * status. Returns true if the handled sink-input should be ignored
> + * in interaction applying phase. */
> +static bool update_trigger_streams(struct userdata *u, struct group *g, pa_sink_input *sink_input,
> +                                   bool put, bool unlink, bool proplist) {
> +    const char *role = NULL;
> +    bool proplist_changed = false;

I think renaming "proplist" to "proplist_changed" and
"proplist_changed" to "trigger_status_changed" would improve the
readability.

> +    bool can_ignore = false;
>  
>      pa_assert(u);
> +    pa_assert(g);
> +    pa_assert(sink_input);
> +
> +    if (proplist) {
> +        bool in_trigger_state;
> +        bool in_trigger_roles;
> +        role = sink_input_role(sink_input);
> +
> +        in_trigger_state = !!pa_hashmap_get(g->trigger_state, sink_input);
> +        in_trigger_roles = !!pa_hashmap_get(g->trigger_roles, role);
> +
> +        /* If the sink-input is already both pointer in trigger_state hashmap
> +         * and role in trigger_roles, or neither, proplist value important to
> +         * us (media.role) didn't change, so no need to update anything. */
> +        proplist_changed = ((in_trigger_state && in_trigger_roles) ||
> +                            (!in_trigger_state && !in_trigger_roles));
> +    }
>  
> -    if (u->global) {
> -        uint32_t idx;
> -        PA_IDXSET_FOREACH(s, u->core->sinks, idx)
> -            if ((trigger_role = find_trigger_stream(u, s, ignore, g)))
> -                break;
> -    } else
> -        trigger_role = find_trigger_stream(u, s, ignore, g);
> +    if (proplist_changed || unlink) {
> +        if (pa_hashmap_remove(g->trigger_state, sink_input)) {
> +            can_ignore = true;
> +            pa_log_debug("Stream with role '%s' removed from group '%s'", sink_input_role(sink_input), g->name);

You could set the role variable unconditionally in the beginning of the
function, then sink_input_role() wouldn't have to be called multiple
times.

I suggest adding "triggers" to the end of the log message to make it
clear that a trigger stream was removed, not a target stream. That is:

"Stream with role '%s' removed from group '%s' triggers."

> +        }
> +    }
> +
> +    if (proplist_changed || put) {
> +        if (!role)
> +            role = sink_input_role(sink_input);
>  
> -    return trigger_role;
> +        if (pa_hashmap_get(g->trigger_roles, role)) {
> +            pa_hashmap_put(g->trigger_state, sink_input, PA_INT_TO_PTR(1));
> +            can_ignore = true;
> +            pa_log_debug("Stream with role '%s' added to group '%s'", role, g->name);
> +        }
> +    }
> +
> +    /* If proplist changed we need to include this stream into consideration
> +     * when applying interaction roles, as the sink-input may have gained or
> +     * lost trigger or interaction role. */
> +    if (proplist_changed)
> +        can_ignore = false;

I have trouble fully understanding what streams should be ignored and
why.

When something happens, the interactions are updated for each group. A
trigger stream can't be a target stream at the same time (within the
group, that is - being a trigger in one group and target in another is
probably possible), so ignoring makes sense for those, and indeed, you
set can_ignore to true only for new and removed trigger streams.
However, if a stream changes its role from a target to a trigger, then
its interaction need to be updated. Therefore, not all trigger streams
are ignored. Since not all trigger streams are ignored, what's the
point of having the complexity of ignoring any streams? Why not just
run the update process for all streams?

> +
> +    return can_ignore;
>  }
>  
> -static void cork_or_duck(struct userdata *u, pa_sink_input *i, const char *interaction_role,  const char *trigger_role, bool interaction_applied, struct group *g) {
> +static void cork_or_duck(struct userdata *u, struct group *g,
> +                         pa_sink_input *sink_input, const char *interaction_role) {
>  
> -    if (u->duck && !interaction_applied) {
> +    if (u->duck) {
>          pa_cvolume vol;
> -        vol.channels = 1;
> -        vol.values[0] = g->volume;
> +        pa_cvolume_set(&vol, 1, g->volume);
>  
> -        pa_log_debug("Found a '%s' stream of '%s' that ducks a '%s' stream.", trigger_role, g->name, interaction_role);
> -        pa_sink_input_add_volume_factor(i, g->name, &vol);
> +        pa_log_debug("Group '%s' ducks a '%s' stream.", g->name, interaction_role);
> +        pa_sink_input_add_volume_factor(sink_input, g->name, &vol);
>  
> -    } else if (!u->duck) {
> -        pa_log_debug("Found a '%s' stream that corks/mutes a '%s' stream.", trigger_role, interaction_role);
> -        pa_sink_input_set_mute(i, true, false);
> -        pa_sink_input_send_event(i, PA_STREAM_EVENT_REQUEST_CORK, NULL);
> +    } else {
> +        pa_log_debug("Group '%s' corks/mutes a '%s' stream.", g->name, interaction_role);
> +        if (!sink_input->muted)
> +            pa_sink_input_set_mute(sink_input, true, false);
> +        if (sink_input->state != PA_SINK_INPUT_CORKED)
> +            pa_sink_input_send_event(sink_input, PA_STREAM_EVENT_REQUEST_CORK, NULL);
>      }
>  }
>  
> -static void uncork_or_unduck(struct userdata *u, pa_sink_input *i, const char *interaction_role, bool corked, struct group *g) {
> +static void uncork_or_unduck(struct userdata *u, struct group *g,
> +                             pa_sink_input *sink_input, const char *interaction_role) {
>  
>      if (u->duck) {
> -       pa_log_debug("In '%s', found a '%s' stream that should be unducked", g->name, interaction_role);
> -       pa_sink_input_remove_volume_factor(i, g->name);
> -    }
> -    else if (corked || i->muted) {
> -       pa_log_debug("Found a '%s' stream that should be uncorked/unmuted.", interaction_role);
> -       if (i->muted)
> -          pa_sink_input_set_mute(i, false, false);
> -       if (corked)
> -          pa_sink_input_send_event(i, PA_STREAM_EVENT_REQUEST_UNCORK, NULL);
> +        pa_log_debug("In '%s', found a '%s' stream that should be unducked", g->name, interaction_role);
> +        pa_sink_input_remove_volume_factor(sink_input, g->name);
> +    } else {
> +        pa_log_debug("In '%s' found a '%s' stream that should be uncorked/unmuted.", g->name, interaction_role);
> +        if (sink_input->muted)
> +           pa_sink_input_set_mute(sink_input, false, false);
> +        if (sink_input->state == PA_SINK_INPUT_CORKED)
> +            pa_sink_input_send_event(sink_input, PA_STREAM_EVENT_REQUEST_UNCORK, NULL);
>      }
>  }
>  
> -static inline void apply_interaction_to_sink(struct userdata *u, pa_sink *s, const char *new_trigger, pa_sink_input *ignore, bool new_stream, struct group *g) {
> -    pa_sink_input *j;
> -    uint32_t idx, role_idx;
> -    const char *interaction_role;
> +static void apply_interaction_to_sink_input(struct userdata *u, struct group *g,
> +                                            pa_sink_input *sink_input, bool unlink) {
>      bool trigger = false;
> +    bool interaction_applied;
> +    const char *role = STREAM_INTERACTION_ANY_ROLE;
>  
>      pa_assert(u);
> -    pa_sink_assert_ref(s);
> -
> -    for (j = PA_SINK_INPUT(pa_idxset_first(s->inputs, &idx)); j; j = PA_SINK_INPUT(pa_idxset_next(s->inputs, &idx))) {
> -        bool corked, interaction_applied;
> -        const char *role;
>  
> -        if (j == ignore)
> -            continue;
> -
> -        if (!(role = pa_proplist_gets(j->proplist, PA_PROP_MEDIA_ROLE)))
> -            role = STREAM_INTERACTION_NO_ROLE;
> -
> -        PA_IDXSET_FOREACH(interaction_role, g->interaction_roles, role_idx) {
> -            if ((trigger = pa_streq(role, interaction_role)))
> -                break;
> -            if ((trigger = (pa_streq(interaction_role, STREAM_INTERACTION_ANY_ROLE) &&
> -                            !get_trigger_role(u, j, g))))
> -               break;
> -        }
> -        if (!trigger)
> -            continue;
> -
> -        /* Some applications start their streams corked, so the stream is uncorked by */
> -        /* the application only after sink_input_put() was called. If a new stream turns */
> -        /* up, act as if it was not corked. In the case of module-role-cork this will */
> -        /* only mute the stream because corking is reverted later by the application */
> -        corked = (j->state == PA_SINK_INPUT_CORKED);
> -        if (new_stream && corked)
> -            corked = false;
> -        interaction_applied = !!pa_hashmap_get(g->interaction_state, j);
> +    if (pa_hashmap_get(g->trigger_state, sink_input))
> +        return;

This means "don't do anything on trigger streams", right? What if the
stream was previously a target stream and changed its role to a
trigger, shouldn't it be uncorked/unducked?

>  
> -        if (new_trigger && ((!corked && !j->muted) || u->duck)) {
> -            if (!interaction_applied)
> -                pa_hashmap_put(g->interaction_state, j, PA_INT_TO_PTR(1));
> +    if (!g->any_role) {
> +        role = sink_input_role(sink_input);
> +        trigger = !!pa_hashmap_get(g->interaction_roles, role);
> +    }
>  
> -            cork_or_duck(u, j, role, new_trigger, interaction_applied, g);
> +    if (!g->any_role && !trigger)
> +        return;

I think this would be easier to read:

    if (g->any_role)
        trigger = true;
    else {
        role = sink_input_role(sink_input);
        trigger = !!pa_hashmap_get(g->interaction_roles, role);
    }

    if (!trigger)
        return;

-- 
Tanu

https://www.patreon.com/tanuk
https://liberapay.com/tanuk


More information about the pulseaudio-discuss mailing list