[pulseaudio-discuss,v2,2/4] stream-interaction: Add support for fading in case of ducking/unducking

Submitted by Sangchul Lee on April 28, 2017, 1:37 p.m.

Details

Message ID 1493386680-5563-3-git-send-email-sangchul1011@gmail.com
State New
Headers show
Series "role-ducking: Introduction of fading effect" ( rev: 1 ) in PulseAudio

Not browsing as part of any series.

Commit Message

Sangchul Lee April 28, 2017, 1:37 p.m.
Ducking/unducking with fading can be applied according to variables
for fading duration. In case of a stream that should be affected by
more than two groups of fading simultaneously, the lowest volume
among the groups will be applied.

Signed-off-by: Sangchul Lee <sc11.lee@samsung.com>
---
 src/modules/stream-interaction.c | 268 +++++++++++++++++++++++++++++++++------
 1 file changed, 231 insertions(+), 37 deletions(-)

Patch hide | download patch | download mbox

diff --git a/src/modules/stream-interaction.c b/src/modules/stream-interaction.c
index 32df9fb..d8e2a6b 100644
--- a/src/modules/stream-interaction.c
+++ b/src/modules/stream-interaction.c
@@ -34,12 +34,20 @@ 
 
 #include "stream-interaction.h"
 
+struct fade_durations {
+    long out;
+    long in;
+};
+
 struct group {
     char *name;
     pa_idxset *trigger_roles;
     pa_idxset *interaction_roles;
     pa_idxset *interacted_inputs;
     pa_volume_t volume;
+    pa_idxset *triggered_inputs;
+    struct fade_durations fade_durs;
+    bool fade_needed:1;
 };
 
 struct userdata {
@@ -58,33 +66,76 @@  struct userdata {
         *sink_input_proplist_changed_slot;
 };
 
-static const char *get_trigger_role(struct userdata *u, pa_sink_input *i, struct group *g) {
+static const char *get_trigger_role_with_update_fade(struct userdata *u, pa_sink_input *i, bool new_stream, struct group *g) {
     const char *role, *trigger_role;
-    uint32_t role_idx;
+    uint32_t role_idx, trigger_idx;
+    void *si;
+
+    pa_assert(u);
+    pa_assert(i);
+    pa_assert(g);
 
     if (!(role = pa_proplist_gets(i->proplist, PA_PROP_MEDIA_ROLE)))
         role = "no_role";
 
-    if (g == NULL) {
-        /* get it from all groups */
-        uint32_t j;
-        for (j = 0; j < u->n_groups; j++) {
-            PA_IDXSET_FOREACH(trigger_role, u->groups[j]->trigger_roles, role_idx) {
-                if (pa_streq(role, trigger_role))
-                    return trigger_role;
+    if (u->duck && new_stream)
+        g->fade_needed = true;
+
+    PA_IDXSET_FOREACH(trigger_role, g->trigger_roles, role_idx) {
+        if (pa_streq(role, trigger_role)) {
+            if (u->duck && (g->fade_durs.in > 0 || g->fade_durs.out > 0)) {
+                if (new_stream) {
+                    /* Update fade state for this group which is for determining that
+                     * the incoming stream should be ducked with fade in or just be ducked */
+                    PA_IDXSET_FOREACH(si, g->triggered_inputs, trigger_idx) {
+                        if (i == si) {
+                            g->fade_needed = false;
+                            break;
+                        }
+                    }
+                }
+                pa_idxset_put(g->triggered_inputs, i, NULL);
             }
-        }
-    } else {
-        PA_IDXSET_FOREACH(trigger_role, g->trigger_roles, role_idx) {
-            if (pa_streq(role, trigger_role))
-                return trigger_role;
+            return trigger_role;
         }
     }
 
     return NULL;
 }
 
-static const char *find_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_input *ignore, struct group *g) {
+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;
+
+    pa_assert(u);
+    pa_assert(i);
+    pa_assert(g);
+
+    if (!(role = pa_proplist_gets(i->proplist, PA_PROP_MEDIA_ROLE)))
+        role = "no_role";
+
+    PA_IDXSET_FOREACH(trigger_role, g->trigger_roles, role_idx)
+        if (pa_streq(role, trigger_role))
+            return trigger_role;
+
+    return NULL;
+}
+
+static const char *get_trigger_role_all_groups(struct userdata *u, pa_sink_input *i) {
+    const char *found = NULL;
+    uint32_t j;
+
+    pa_assert(u);
+    pa_assert(i);
+
+    for (j = 0; j < u->n_groups; j++)
+        if ((found = get_trigger_role(u, i, u->groups[j])))
+            return found;
+
+    return found;
+}
+
+static const char *find_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_input *ignore, bool new_stream, struct group *g) {
     pa_sink_input *j;
     uint32_t idx;
     const char *trigger_role;
@@ -96,7 +147,10 @@  static const char *find_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_i
         if (j == ignore)
             continue;
 
-        trigger_role = get_trigger_role(u, j, g);
+        if (u->duck && (g->fade_durs.in > 0 || g->fade_durs.out > 0))
+            trigger_role = get_trigger_role_with_update_fade(u, j, new_stream, g);
+        else
+            trigger_role = get_trigger_role(u, j, g);
         if (trigger_role && !j->muted && pa_sink_input_get_state(j) != PA_SINK_INPUT_CORKED)
             return trigger_role;
     }
@@ -104,7 +158,7 @@  static const char *find_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_i
     return NULL;
 }
 
-static const char *find_global_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_input *ignore, struct group *g) {
+static const char *find_global_trigger_stream(struct userdata *u, pa_sink *s, pa_sink_input *ignore, bool new_stream, struct group *g) {
     const char *trigger_role = NULL;
 
     pa_assert(u);
@@ -112,26 +166,76 @@  static const char *find_global_trigger_stream(struct userdata *u, pa_sink *s, pa
     if (u->global) {
         uint32_t idx;
         PA_IDXSET_FOREACH(s, u->core->sinks, idx)
-            if ((trigger_role = find_trigger_stream(u, s, ignore, g)))
+            if ((trigger_role = find_trigger_stream(u, s, ignore, new_stream, g)))
                 break;
     } else
-        trigger_role = find_trigger_stream(u, s, ignore, g);
+        trigger_role = find_trigger_stream(u, s, ignore, new_stream, g);
 
     return trigger_role;
 }
 
-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 bool is_affected_by_other_groups(struct userdata *u, pa_sink_input *s, struct group *skip_g, pa_volume_t *affected_min_vol) {
+    bool ret = false;
+    uint32_t i;
+    pa_sink_input *si;
+    pa_volume_t vol = PA_VOLUME_MAX;
+
+    pa_assert(u);
+
+    for (i = 0; i < u->n_groups; i++) {
+        uint32_t idx;
+        if (u->groups[i] == skip_g)
+            continue;
+        PA_IDXSET_FOREACH(si, u->groups[i]->interacted_inputs, idx)
+            if (si == s) {
+                if (u->groups[i]->volume < vol)
+                    vol = u->groups[i]->volume;
+                ret = true;
+            }
+    }
+    if (ret)
+        *affected_min_vol = vol;
+
+    return ret;
+}
+
+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) {
 
     if (u->duck && !interaction_applied) {
-        pa_cvolume vol;
-        vol.channels = 1;
-        vol.values[0] = g->volume;
+        if (g->fade_durs.in || g->fade_durs.out) {
+            bool affected = false;
+            pa_volume_t min_vol;
+            pa_cvolume_ramp vol_ramp;
+
+            pa_cvolume_ramp_set(&vol_ramp, i->volume.channels, PA_VOLUME_RAMP_TYPE_LINEAR, g->fade_durs.out, g->volume);
+            if (!g->fade_needed) {
+                pa_cvolume vol;
+                vol.channels = 1;
+                vol.values[0] = g->volume;
+                pa_log_debug("Found a '%s' stream(%u) that should be ducked by '%s'.", interaction_role, i->index, g->name);
+
+                pa_sink_input_add_volume_factor(i, g->name, &vol);
+                pa_sink_input_set_volume_ramp(i, &vol_ramp, false);
+            } else {
+                affected = is_affected_by_other_groups(u, i, g, &min_vol);
+                pa_cvolume_ramp_set(&vol_ramp, i->volume.channels, PA_VOLUME_RAMP_TYPE_LINEAR,
+                                    g->fade_durs.out, affected ? MIN(g->volume, min_vol) : g->volume);
+                pa_log_debug("Found a '%s' stream(%u) that should be ducked with fade-out(%lums) by '%s'.",
+                             interaction_role, i->index, g->fade_durs.out, g->name);
 
-        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_sink_input_set_volume_ramp(i, &vol_ramp, true);
+            }
+        } else {
+            pa_cvolume vol;
+            vol.channels = 1;
+            vol.values[0] = g->volume;
+
+            pa_log_debug("Found a '%s' stream of '%s' that ducks a '%s' stream(%u).", trigger_role, g->name, interaction_role, i->index);
+            pa_sink_input_add_volume_factor(i, 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_log_debug("Found a '%s' stream that corks/mutes a '%s' stream(%u).", trigger_role, interaction_role, i->index);
         pa_sink_input_set_mute(i, true, false);
         pa_sink_input_send_event(i, PA_STREAM_EVENT_REQUEST_CORK, NULL);
     }
@@ -140,11 +244,26 @@  static void cork_or_duck(struct userdata *u, pa_sink_input *i, const char *inter
 static void uncork_or_unduck(struct userdata *u, pa_sink_input *i, const char *interaction_role, bool corked, struct group *g) {
 
     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);
+        if (g->fade_durs.in || g->fade_durs.out) {
+            bool affected = false;
+            pa_volume_t min_vol;
+            pa_cvolume_ramp vol_ramp;
+
+            pa_log_debug("In '%s', found a '%s' stream(%u) that should be unducked with fade-in(%lums)",
+                         g->name, interaction_role, i->index, g->fade_durs.in);
+            pa_sink_input_remove_volume_factor(i, g->name);
+            affected = is_affected_by_other_groups(u, i, g, &min_vol);
+            pa_cvolume_ramp_set(&vol_ramp, i->volume.channels, PA_VOLUME_RAMP_TYPE_LINEAR,
+                                g->fade_durs.in, affected ? min_vol : PA_VOLUME_NORM);
+
+            pa_sink_input_set_volume_ramp(i, &vol_ramp, true);
+        } else {
+            pa_log_debug("In '%s', found a '%s' stream(%u) that should be unducked", g->name, interaction_role, i->index);
+            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);
+       pa_log_debug("Found a '%s' stream(%u) that should be uncorked/unmuted.", interaction_role, i->index);
        if (i->muted)
           pa_sink_input_set_mute(i, false, false);
        if (corked)
@@ -244,15 +363,19 @@  static pa_hook_result_t process(struct userdata *u, pa_sink_input *i, bool creat
     pa_assert(u);
     pa_sink_input_assert_ref(i);
 
-    if (!create)
-        for (j = 0; j < u->n_groups; j++)
+    if (!create) {
+        for (j = 0; j < u->n_groups; j++) {
             pa_idxset_remove_by_data(u->groups[j]->interacted_inputs, i, NULL);
+            if (u->duck)
+                pa_idxset_remove_by_data(u->groups[j]->triggered_inputs, i, NULL);
+        }
+    }
 
     if (!i->sink)
         return PA_HOOK_OK;
 
     for (j = 0; j < u->n_groups; j++) {
-        trigger_role = find_global_trigger_stream(u, i->sink, create ? NULL : i, u->groups[j]);
+        trigger_role = find_global_trigger_stream(u, i->sink, create ? NULL : i, new_stream, u->groups[j]);
         apply_interaction(u, i->sink, trigger_role, create ? NULL : i, new_stream, u->groups[j]);
     }
 
@@ -290,7 +413,7 @@  static pa_hook_result_t sink_input_state_changed_cb(pa_core *core, pa_sink_input
     pa_core_assert_ref(core);
     pa_sink_input_assert_ref(i);
 
-    if (PA_SINK_INPUT_IS_LINKED(pa_sink_input_get_state(i)) && get_trigger_role(u, i, NULL))
+    if (PA_SINK_INPUT_IS_LINKED(pa_sink_input_get_state(i)) && get_trigger_role_all_groups(u, i))
         return process(u, i, true, false);
 
     return PA_HOOK_OK;
@@ -300,7 +423,7 @@  static pa_hook_result_t sink_input_mute_changed_cb(pa_core *core, pa_sink_input
     pa_core_assert_ref(core);
     pa_sink_input_assert_ref(i);
 
-    if (PA_SINK_INPUT_IS_LINKED(pa_sink_input_get_state(i)) && get_trigger_role(u, i, NULL))
+    if (PA_SINK_INPUT_IS_LINKED(pa_sink_input_get_state(i)) && get_trigger_role_all_groups(u, i))
         return process(u, i, true, false);
 
     return PA_HOOK_OK;
@@ -343,9 +466,12 @@  int pa_stream_interaction_init(pa_module *m, const char* const v_modargs[]) {
 
     if (u->duck) {
         const char *volumes;
+        const char *durations;
         uint32_t group_count_tr = 0;
         uint32_t group_count_du = 0;
         uint32_t group_count_vol = 0;
+        uint32_t group_count_fd_out = 0;
+        uint32_t group_count_fd_in = 0;
 
         roles = pa_modargs_get_value(ma, "trigger_roles", NULL);
         if (roles) {
@@ -374,9 +500,28 @@  int pa_stream_interaction_init(pa_module *m, const char* const v_modargs[]) {
                 pa_xfree(n);
             }
         }
+        durations = pa_modargs_get_value(ma, "fade_out", NULL);
+        if (durations) {
+            const char *split_state = NULL;
+            char *n = NULL;
+            while ((n = pa_split(durations, "/", &split_state))) {
+                group_count_fd_out++;
+                pa_xfree(n);
+            }
+        }
+        durations = pa_modargs_get_value(ma, "fade_in", NULL);
+        if (durations) {
+            const char *split_state = NULL;
+            char *n = NULL;
+            while ((n = pa_split(durations, "/", &split_state))) {
+                group_count_fd_in++;
+                pa_xfree(n);
+            }
+        }
 
         if ((group_count_tr > 1 || group_count_du > 1 || group_count_vol > 1) &&
-            ((group_count_tr != group_count_du) || (group_count_tr != group_count_vol))) {
+            ((group_count_tr != group_count_du) || (group_count_tr != group_count_vol) ||
+            (group_count_fd_out > group_count_tr) || (group_count_fd_in > group_count_tr))) {
             pa_log("Invalid number of groups");
             goto fail;
         }
@@ -391,8 +536,10 @@  int pa_stream_interaction_init(pa_module *m, const char* const v_modargs[]) {
         u->groups[i]->trigger_roles = pa_idxset_new(NULL, NULL);
         u->groups[i]->interaction_roles = pa_idxset_new(NULL, NULL);
         u->groups[i]->interacted_inputs = pa_idxset_new(NULL, NULL);
-        if (u->duck)
+        if (u->duck) {
+            u->groups[i]->triggered_inputs = pa_idxset_new(NULL, NULL);
             u->groups[i]->name = pa_sprintf_malloc("ducking_group_%u", i);
+        }
     }
 
     roles = pa_modargs_get_value(ma, "trigger_roles", NULL);
@@ -462,6 +609,7 @@  int pa_stream_interaction_init(pa_module *m, const char* const v_modargs[]) {
 
     if (u->duck) {
         const char *volumes;
+        const char *durations;
         u->groups[0]->volume = pa_sw_volume_from_dB(-20);
         if ((volumes = pa_modargs_get_value(ma, "volume", NULL))) {
             const char *group_split_state = NULL;
@@ -482,6 +630,50 @@  int pa_stream_interaction_init(pa_module *m, const char* const v_modargs[]) {
                 pa_xfree(n);
             }
         }
+
+        u->groups[0]->fade_durs.out = 0L;
+        durations = pa_modargs_get_value(ma, "fade_out", NULL);
+        if (durations) {
+            const char *group_split_state = NULL;
+            char *n = NULL;
+            i = 0;
+            while ((n = pa_split(durations, "/", &group_split_state))) {
+                if (n[0] != '\0') {
+                    if (pa_atol(n, &(u->groups[i++]->fade_durs.out))) {
+                        pa_log("Failed to pa_atol() for fade out");
+                        pa_xfree(n);
+                        goto fail;
+                    }
+                } else {
+                    pa_log("empty fade out duration");
+                    pa_xfree(n);
+                    goto fail;
+                }
+                pa_xfree(n);
+            }
+        }
+
+        u->groups[0]->fade_durs.in = 0L;
+        durations = pa_modargs_get_value(ma, "fade_in", NULL);
+        if (durations) {
+            const char *group_split_state = NULL;
+            char *n = NULL;
+            i = 0;
+            while ((n = pa_split(durations, "/", &group_split_state))) {
+                if (n[0] != '\0') {
+                    if (pa_atol(n, &(u->groups[i++]->fade_durs.in))) {
+                        pa_log("Failed to pa_atol() for fade in");
+                        pa_xfree(n);
+                        goto fail;
+                    }
+                } else {
+                    pa_log("empty fade in duration");
+                    pa_xfree(n);
+                    goto fail;
+                }
+                pa_xfree(n);
+            }
+        }
     }
 
     if (pa_modargs_get_value_boolean(ma, "global", &global) < 0) {
@@ -529,8 +721,10 @@  void pa_stream_interaction_done(pa_module *m) {
             pa_idxset_free(u->groups[j]->trigger_roles, pa_xfree);
             pa_idxset_free(u->groups[j]->interaction_roles, pa_xfree);
             pa_idxset_free(u->groups[j]->interacted_inputs, pa_xfree);
-            if (u->duck)
+            if (u->duck) {
+                pa_idxset_free(u->groups[j]->triggered_inputs, pa_xfree);
                 pa_xfree(u->groups[j]->name);
+            }
             pa_xfree(u->groups[j]);
         }
         pa_xfree(u->groups);