Implement the "volume sharing" feature.
[profile/ivi/pulseaudio-panda.git] / src / pulsecore / sink.c
index 773123d..d713be1 100644 (file)
@@ -226,8 +226,14 @@ pa_sink* pa_sink_new(
     pa_return_null_if_fail(pa_channel_map_valid(&data->channel_map));
     pa_return_null_if_fail(data->channel_map.channels == data->sample_spec.channels);
 
-    if (!data->volume_is_set)
+    /* FIXME: There should probably be a general function for checking whether
+     * the sink volume is allowed to be set, like there is for sink inputs. */
+    pa_assert(!data->volume_is_set || !(flags & PA_SINK_SHARE_VOLUME_WITH_MASTER));
+
+    if (!data->volume_is_set) {
         pa_cvolume_reset(&data->volume, data->sample_spec.channels);
+        data->save_volume = FALSE;
+    }
 
     pa_return_null_if_fail(pa_cvolume_valid(&data->volume));
     pa_return_null_if_fail(pa_cvolume_compatible(&data->volume, &data->sample_spec));
@@ -447,6 +453,7 @@ void pa_sink_put(pa_sink* s) {
     pa_assert_ctl_context();
 
     pa_assert(s->state == PA_SINK_INIT);
+    pa_assert(!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER) || s->input_to_master);
 
     /* The following fields must be initialized properly when calling _put() */
     pa_assert(s->asyncmsgq);
@@ -456,22 +463,43 @@ void pa_sink_put(pa_sink* s) {
      * special exception we allow volume related flags to be set
      * between _new() and _put(). */
 
-    if (!(s->flags & PA_SINK_HW_VOLUME_CTRL))
+    /* XXX: Currently decibel volume is disabled for all sinks that use volume
+     * sharing. When the master sink supports decibel volume, it would be good
+     * to have the flag also in the filter sink, but currently we don't do that
+     * so that the flags of the filter sink never change when it's moved from
+     * a master sink to another. One solution for this problem would be to
+     * remove user-visible volume altogether from filter sinks when volume
+     * sharing is used, but the current approach was easier to implement... */
+    if (!(s->flags & PA_SINK_HW_VOLUME_CTRL) && !(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER))
         s->flags |= PA_SINK_DECIBEL_VOLUME;
 
     if ((s->flags & PA_SINK_DECIBEL_VOLUME) && s->core->flat_volumes)
         s->flags |= PA_SINK_FLAT_VOLUME;
 
-    /* We assume that if the sink implementor changed the default
-     * volume he did so in real_volume, because that is the usual
-     * place where he is supposed to place his changes.  */
-    s->reference_volume = s->real_volume;
+    if (s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER) {
+        pa_sink *root_sink = s->input_to_master->sink;
+
+        while (root_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)
+            root_sink = root_sink->input_to_master->sink;
+
+        s->reference_volume = root_sink->reference_volume;
+        pa_cvolume_remap(&s->reference_volume, &root_sink->channel_map, &s->channel_map);
+
+        s->real_volume = root_sink->real_volume;
+        pa_cvolume_remap(&s->real_volume, &root_sink->channel_map, &s->channel_map);
+    } else
+        /* We assume that if the sink implementor changed the default
+         * volume he did so in real_volume, because that is the usual
+         * place where he is supposed to place his changes.  */
+        s->reference_volume = s->real_volume;
 
     s->thread_info.soft_volume = s->soft_volume;
     s->thread_info.soft_muted = s->muted;
     pa_sw_cvolume_multiply(&s->thread_info.current_hw_volume, &s->soft_volume, &s->real_volume);
 
-    pa_assert((s->flags & PA_SINK_HW_VOLUME_CTRL) || (s->base_volume == PA_VOLUME_NORM && s->flags & PA_SINK_DECIBEL_VOLUME));
+    pa_assert((s->flags & PA_SINK_HW_VOLUME_CTRL)
+              || (s->base_volume == PA_VOLUME_NORM
+                  && ((s->flags & PA_SINK_DECIBEL_VOLUME || (s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)))));
     pa_assert(!(s->flags & PA_SINK_DECIBEL_VOLUME) || s->n_volume_steps == PA_VOLUME_NORM+1);
     pa_assert(!(s->flags & PA_SINK_DYNAMIC_LATENCY) == (s->thread_info.fixed_latency != 0));
     pa_assert(!(s->flags & PA_SINK_LATENCY) == !(s->monitor_source->flags & PA_SOURCE_LATENCY));
@@ -1196,47 +1224,61 @@ pa_usec_t pa_sink_get_latency_within_thread(pa_sink *s) {
     return usec;
 }
 
-static pa_cvolume* cvolume_remap_minimal_impact(
-        pa_cvolume *v,
-        const pa_cvolume *template,
-        const pa_channel_map *from,
-        const pa_channel_map *to) {
+/* Called from the main thread (and also from the IO thread while the main
+ * thread is waiting).
+ *
+ * When a sink uses volume sharing, it never has the PA_SINK_FLAT_VOLUME flag
+ * set. Instead, flat volume mode is detected by checking whether the root sink
+ * has the flag set. */
+pa_bool_t pa_sink_flat_volume_enabled(pa_sink *s) {
+    pa_sink_assert_ref(s);
 
-    pa_cvolume t;
+    while (s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)
+        s = s->input_to_master->sink;
 
-    pa_assert(v);
-    pa_assert(template);
-    pa_assert(from);
-    pa_assert(to);
+    return (s->flags & PA_SINK_FLAT_VOLUME);
+}
 
-    pa_return_val_if_fail(pa_cvolume_compatible_with_channel_map(v, from), NULL);
-    pa_return_val_if_fail(pa_cvolume_compatible_with_channel_map(template, to), NULL);
+/* Called from main context. */
+static void compute_reference_ratio(pa_sink_input *i) {
+    unsigned c = 0;
+    pa_cvolume remapped;
 
-    /* Much like pa_cvolume_remap(), but tries to minimize impact when
-     * mapping from sink input to sink volumes:
-     *
-     * If template is a possible remapping from v it is used instead
-     * of remapping anew.
+    pa_assert(i);
+    pa_assert(pa_sink_flat_volume_enabled(i->sink));
+
+    /*
+     * Calculates the reference ratio from the sink's reference
+     * volume. This basically calculates:
      *
-     * If the channel maps don't match we set an all-channel volume on
-     * the sink to ensure that changing a volume on one stream has no
-     * effect that cannot be compensated for in another stream that
-     * does not have the same channel map as the sink. */
+     * i->reference_ratio = i->volume / i->sink->reference_volume
+     */
 
-    if (pa_channel_map_equal(from, to))
-        return v;
+    remapped = i->sink->reference_volume;
+    pa_cvolume_remap(&remapped, &i->sink->channel_map, &i->channel_map);
 
-    t = *template;
-    if (pa_cvolume_equal(pa_cvolume_remap(&t, to, from), v)) {
-        *v = *template;
-        return v;
-    }
+    i->reference_ratio.channels = i->sample_spec.channels;
 
-    pa_cvolume_set(v, to->channels, pa_cvolume_max(v));
-    return v;
+    for (c = 0; c < i->sample_spec.channels; c++) {
+
+        /* We don't update when the sink volume is 0 anyway */
+        if (remapped.values[c] <= PA_VOLUME_MUTED)
+            continue;
+
+        /* Don't update the reference ratio unless necessary */
+        if (pa_sw_volume_multiply(
+                    i->reference_ratio.values[c],
+                    remapped.values[c]) == i->volume.values[c])
+            continue;
+
+        i->reference_ratio.values[c] = pa_sw_volume_divide(
+                i->volume.values[c],
+                remapped.values[c]);
+    }
 }
 
-/* Called from main context */
+/* Called from main context. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. */
 static void compute_reference_ratios(pa_sink *s) {
     uint32_t idx;
     pa_sink_input *i;
@@ -1244,44 +1286,18 @@ static void compute_reference_ratios(pa_sink *s) {
     pa_sink_assert_ref(s);
     pa_assert_ctl_context();
     pa_assert(PA_SINK_IS_LINKED(s->state));
-    pa_assert(s->flags & PA_SINK_FLAT_VOLUME);
+    pa_assert(pa_sink_flat_volume_enabled(s));
 
     PA_IDXSET_FOREACH(i, s->inputs, idx) {
-        unsigned c;
-        pa_cvolume remapped;
-
-        /*
-         * Calculates the reference volume from the sink's reference
-         * volume. This basically calculates:
-         *
-         * i->reference_ratio = i->volume / s->reference_volume
-         */
-
-        remapped = s->reference_volume;
-        pa_cvolume_remap(&remapped, &s->channel_map, &i->channel_map);
-
-        i->reference_ratio.channels = i->sample_spec.channels;
-
-        for (c = 0; c < i->sample_spec.channels; c++) {
+        compute_reference_ratio(i);
 
-            /* We don't update when the sink volume is 0 anyway */
-            if (remapped.values[c] <= PA_VOLUME_MUTED)
-                continue;
-
-            /* Don't update the reference ratio unless necessary */
-            if (pa_sw_volume_multiply(
-                        i->reference_ratio.values[c],
-                        remapped.values[c]) == i->volume.values[c])
-                continue;
-
-            i->reference_ratio.values[c] = pa_sw_volume_divide(
-                    i->volume.values[c],
-                    remapped.values[c]);
-        }
+        if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER))
+            compute_reference_ratios(i->origin_sink);
     }
 }
 
-/* Called from main context */
+/* Called from main context. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. */
 static void compute_real_ratios(pa_sink *s) {
     pa_sink_input *i;
     uint32_t idx;
@@ -1289,12 +1305,24 @@ static void compute_real_ratios(pa_sink *s) {
     pa_sink_assert_ref(s);
     pa_assert_ctl_context();
     pa_assert(PA_SINK_IS_LINKED(s->state));
-    pa_assert(s->flags & PA_SINK_FLAT_VOLUME);
+    pa_assert(pa_sink_flat_volume_enabled(s));
 
     PA_IDXSET_FOREACH(i, s->inputs, idx) {
         unsigned c;
         pa_cvolume remapped;
 
+        if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)) {
+            /* The origin sink uses volume sharing, so this input's real ratio
+             * is handled as a special case - the real ratio must be 0 dB, and
+             * as a result i->soft_volume must equal i->volume_factor. */
+            pa_cvolume_reset(&i->real_ratio, i->real_ratio.channels);
+            i->soft_volume = i->volume_factor;
+
+            compute_real_ratios(i->origin_sink);
+
+            continue;
+        }
+
         /*
          * This basically calculates:
          *
@@ -1335,23 +1363,144 @@ static void compute_real_ratios(pa_sink *s) {
     }
 }
 
-/* Called from main thread */
-static void compute_real_volume(pa_sink *s) {
+static pa_cvolume *cvolume_remap_minimal_impact(
+        pa_cvolume *v,
+        const pa_cvolume *template,
+        const pa_channel_map *from,
+        const pa_channel_map *to) {
+
+    pa_cvolume t;
+
+    pa_assert(v);
+    pa_assert(template);
+    pa_assert(from);
+    pa_assert(to);
+    pa_assert(pa_cvolume_compatible_with_channel_map(v, from));
+    pa_assert(pa_cvolume_compatible_with_channel_map(template, to));
+
+    /* Much like pa_cvolume_remap(), but tries to minimize impact when
+     * mapping from sink input to sink volumes:
+     *
+     * If template is a possible remapping from v it is used instead
+     * of remapping anew.
+     *
+     * If the channel maps don't match we set an all-channel volume on
+     * the sink to ensure that changing a volume on one stream has no
+     * effect that cannot be compensated for in another stream that
+     * does not have the same channel map as the sink. */
+
+    if (pa_channel_map_equal(from, to))
+        return v;
+
+    t = *template;
+    if (pa_cvolume_equal(pa_cvolume_remap(&t, to, from), v)) {
+        *v = *template;
+        return v;
+    }
+
+    pa_cvolume_set(v, to->channels, pa_cvolume_max(v));
+    return v;
+}
+
+/* Called from main thread. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. */
+static void get_maximum_input_volume(pa_sink *s, pa_cvolume *max_volume, const pa_channel_map *channel_map) {
     pa_sink_input *i;
     uint32_t idx;
 
     pa_sink_assert_ref(s);
+    pa_assert(max_volume);
+    pa_assert(channel_map);
+    pa_assert(pa_sink_flat_volume_enabled(s));
+
+    PA_IDXSET_FOREACH(i, s->inputs, idx) {
+        pa_cvolume remapped;
+
+        if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)) {
+            get_maximum_input_volume(i->origin_sink, max_volume, channel_map);
+
+            /* Ignore this input. The origin sink uses volume sharing, so this
+             * input's volume will be set to be equal to the root sink's real
+             * volume. Obviously this input's current volume must not then
+             * affect what the root sink's real volume will be. */
+            continue;
+        }
+
+        remapped = i->volume;
+        cvolume_remap_minimal_impact(&remapped, max_volume, &i->channel_map, channel_map);
+        pa_cvolume_merge(max_volume, max_volume, &remapped);
+    }
+}
+
+/* Called from main thread. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. */
+static pa_bool_t has_inputs(pa_sink *s) {
+    pa_sink_input *i;
+    uint32_t idx;
+
+    pa_sink_assert_ref(s);
+
+    PA_IDXSET_FOREACH(i, s->inputs, idx) {
+        if (!i->origin_sink || !(i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER) || has_inputs(i->origin_sink))
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
+/* Called from main thread. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. */
+static void update_real_volume(pa_sink *s, const pa_cvolume *new_volume, pa_channel_map *channel_map) {
+    pa_sink_input *i;
+    uint32_t idx;
+
+    pa_sink_assert_ref(s);
+    pa_assert(new_volume);
+    pa_assert(channel_map);
+
+    s->real_volume = *new_volume;
+    pa_cvolume_remap(&s->real_volume, channel_map, &s->channel_map);
+
+    PA_IDXSET_FOREACH(i, s->inputs, idx) {
+        if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)) {
+            if (pa_sink_flat_volume_enabled(s)) {
+                pa_cvolume old_volume = i->volume;
+
+                /* Follow the root sink's real volume. */
+                i->volume = *new_volume;
+                pa_cvolume_remap(&i->volume, channel_map, &i->channel_map);
+                compute_reference_ratio(i);
+
+                /* The volume changed, let's tell people so */
+                if (!pa_cvolume_equal(&old_volume, &i->volume)) {
+                    if (i->volume_changed)
+                        i->volume_changed(i);
+
+                    pa_subscription_post(i->core, PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_CHANGE, i->index);
+                }
+            }
+
+            update_real_volume(i->origin_sink, new_volume, channel_map);
+        }
+    }
+}
+
+/* Called from main thread. Only called for the root sink in shared volume
+ * cases. */
+static void compute_real_volume(pa_sink *s) {
+    pa_sink_assert_ref(s);
     pa_assert_ctl_context();
     pa_assert(PA_SINK_IS_LINKED(s->state));
-    pa_assert(s->flags & PA_SINK_FLAT_VOLUME);
+    pa_assert(pa_sink_flat_volume_enabled(s));
+    pa_assert(!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER));
 
     /* This determines the maximum volume of all streams and sets
      * s->real_volume accordingly. */
 
-    if (pa_idxset_isempty(s->inputs)) {
-        /* In the special case that we have no sink input we leave the
+    if (!has_inputs(s)) {
+        /* In the special case that we have no sink inputs we leave the
          * volume unmodified. */
-        s->real_volume = s->reference_volume;
+        update_real_volume(s, &s->reference_volume, &s->channel_map);
         return;
     }
 
@@ -1359,20 +1508,16 @@ static void compute_real_volume(pa_sink *s) {
 
     /* First let's determine the new maximum volume of all inputs
      * connected to this sink */
-    PA_IDXSET_FOREACH(i, s->inputs, idx) {
-        pa_cvolume remapped;
-
-        remapped = i->volume;
-        cvolume_remap_minimal_impact(&remapped, &s->real_volume, &i->channel_map, &s->channel_map);
-        pa_cvolume_merge(&s->real_volume, &s->real_volume, &remapped);
-    }
+    get_maximum_input_volume(s, &s->real_volume, &s->channel_map);
+    update_real_volume(s, &s->real_volume, &s->channel_map);
 
     /* Then, let's update the real ratios/soft volumes of all inputs
      * connected to this sink */
     compute_real_ratios(s);
 }
 
-/* Called from main thread */
+/* Called from main thread. Only called for the root sink in shared volume
+ * cases, except for internal recursive calls. */
 static void propagate_reference_volume(pa_sink *s) {
     pa_sink_input *i;
     uint32_t idx;
@@ -1380,14 +1525,23 @@ static void propagate_reference_volume(pa_sink *s) {
     pa_sink_assert_ref(s);
     pa_assert_ctl_context();
     pa_assert(PA_SINK_IS_LINKED(s->state));
-    pa_assert(s->flags & PA_SINK_FLAT_VOLUME);
+    pa_assert(pa_sink_flat_volume_enabled(s));
 
     /* This is called whenever the sink volume changes that is not
      * caused by a sink input volume change. We need to fix up the
      * sink input volumes accordingly */
 
     PA_IDXSET_FOREACH(i, s->inputs, idx) {
-        pa_cvolume old_volume, remapped;
+        pa_cvolume old_volume;
+
+        if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)) {
+            propagate_reference_volume(i->origin_sink);
+
+            /* Since the origin sink uses volume sharing, this input's volume
+             * needs to be updated to match the root sink's real volume, but
+             * that will be done later in update_shared_real_volume(). */
+            continue;
+        }
 
         old_volume = i->volume;
 
@@ -1395,9 +1549,9 @@ static void propagate_reference_volume(pa_sink *s) {
          *
          * i->volume := s->reference_volume * i->reference_ratio  */
 
-        remapped = s->reference_volume;
-        pa_cvolume_remap(&remapped, &s->channel_map, &i->channel_map);
-        pa_sw_cvolume_multiply(&i->volume, &remapped, &i->reference_ratio);
+        i->volume = s->reference_volume;
+        pa_cvolume_remap(&i->volume, &s->channel_map, &i->channel_map);
+        pa_sw_cvolume_multiply(&i->volume, &i->volume, &i->reference_ratio);
 
         /* The volume changed, let's tell people so */
         if (!pa_cvolume_equal(&old_volume, &i->volume)) {
@@ -1410,6 +1564,54 @@ static void propagate_reference_volume(pa_sink *s) {
     }
 }
 
+/* Called from main thread. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. The return value indicates
+ * whether any reference volume actually changed. */
+static pa_bool_t update_reference_volume(pa_sink *s, const pa_cvolume *v, const pa_channel_map *channel_map, pa_bool_t save) {
+    pa_cvolume volume;
+    pa_bool_t reference_volume_changed;
+    pa_sink_input *i;
+    uint32_t idx;
+
+    pa_sink_assert_ref(s);
+    pa_assert(PA_SINK_IS_LINKED(s->state));
+    pa_assert(v);
+    pa_assert(channel_map);
+    pa_assert(pa_cvolume_valid(v));
+
+    volume = *v;
+    pa_cvolume_remap(&volume, channel_map, &s->channel_map);
+
+    reference_volume_changed = !pa_cvolume_equal(&volume, &s->reference_volume);
+    s->reference_volume = volume;
+
+    s->save_volume = (!reference_volume_changed && s->save_volume) || save;
+
+    if (reference_volume_changed)
+        pa_subscription_post(s->core, PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_CHANGE, s->index);
+    else if (!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER))
+        /* If the root sink's volume doesn't change, then there can't be any
+         * changes in the other sinks in the sink tree either.
+         *
+         * It's probably theoretically possible that even if the root sink's
+         * volume changes slightly, some filter sink doesn't change its volume
+         * due to rounding errors. If that happens, we still want to propagate
+         * the changed root sink volume to the sinks connected to the
+         * intermediate sink that didn't change its volume. This theoretical
+         * possiblity is the reason why we have that !(s->flags &
+         * PA_SINK_SHARE_VOLUME_WITH_MASTER) condition. Probably nobody would
+         * notice even if we returned here FALSE always if
+         * reference_volume_changed is FALSE. */
+        return FALSE;
+
+    PA_IDXSET_FOREACH(i, s->inputs, idx) {
+        if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER))
+            update_reference_volume(i->origin_sink, v, channel_map, FALSE);
+    }
+
+    return TRUE;
+}
+
 /* Called from main thread */
 void pa_sink_set_volume(
         pa_sink *s,
@@ -1417,14 +1619,14 @@ void pa_sink_set_volume(
         pa_bool_t send_msg,
         pa_bool_t save) {
 
-    pa_cvolume old_reference_volume;
-    pa_bool_t reference_changed;
+    pa_cvolume new_reference_volume;
+    pa_sink *root_sink = s;
 
     pa_sink_assert_ref(s);
     pa_assert_ctl_context();
     pa_assert(PA_SINK_IS_LINKED(s->state));
     pa_assert(!volume || pa_cvolume_valid(volume));
-    pa_assert(volume || (s->flags & PA_SINK_FLAT_VOLUME));
+    pa_assert(volume || pa_sink_flat_volume_enabled(s));
     pa_assert(!volume || volume->channels == 1 || pa_cvolume_compatible(volume, &s->sample_spec));
 
     /* make sure we don't change the volume when a PASSTHROUGH input is connected */
@@ -1445,76 +1647,82 @@ void pa_sink_set_volume(
         }
     }
 
+    /* In case of volume sharing, the volume is set for the root sink first,
+     * from which it's then propagated to the sharing sinks. */
+    while (root_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)
+        root_sink = root_sink->input_to_master->sink;
+
     /* As a special exception we accept mono volumes on all sinks --
      * even on those with more complex channel maps */
 
+    if (volume) {
+        if (pa_cvolume_compatible(volume, &s->sample_spec))
+            new_reference_volume = *volume;
+        else {
+            new_reference_volume = s->reference_volume;
+            pa_cvolume_scale(&new_reference_volume, pa_cvolume_max(volume));
+        }
+
+        pa_cvolume_remap(&new_reference_volume, &s->channel_map, &root_sink->channel_map);
+    }
+
     /* If volume is NULL we synchronize the sink's real and reference
      * volumes with the stream volumes. If it is not NULL we update
      * the reference_volume with it. */
 
-    old_reference_volume = s->reference_volume;
-
     if (volume) {
-
-        if (pa_cvolume_compatible(volume, &s->sample_spec))
-            s->reference_volume = *volume;
-        else
-            pa_cvolume_scale(&s->reference_volume, pa_cvolume_max(volume));
-
-        if (s->flags & PA_SINK_FLAT_VOLUME) {
-            /* OK, propagate this volume change back to the inputs */
-            propagate_reference_volume(s);
-
-            /* And now recalculate the real volume */
-            compute_real_volume(s);
-        } else
-            s->real_volume = s->reference_volume;
+        if (update_reference_volume(root_sink, &new_reference_volume, &root_sink->channel_map, save)) {
+            if (pa_sink_flat_volume_enabled(root_sink)) {
+                /* OK, propagate this volume change back to the inputs */
+                propagate_reference_volume(root_sink);
+
+                /* And now recalculate the real volume */
+                compute_real_volume(root_sink);
+            } else
+                update_real_volume(root_sink, &root_sink->reference_volume, &root_sink->channel_map);
+        }
 
     } else {
-        pa_assert(s->flags & PA_SINK_FLAT_VOLUME);
+        pa_assert(pa_sink_flat_volume_enabled(root_sink));
 
         /* Ok, let's determine the new real volume */
-        compute_real_volume(s);
+        compute_real_volume(root_sink);
 
         /* Let's 'push' the reference volume if necessary */
-        pa_cvolume_merge(&s->reference_volume, &s->reference_volume, &s->real_volume);
+        pa_cvolume_merge(&new_reference_volume, &s->reference_volume, &root_sink->real_volume);
+        update_reference_volume(root_sink, &new_reference_volume, &root_sink->channel_map, save);
 
-        /* We need to fix the reference ratios of all streams now that
-         * we changed the reference volume */
-        compute_reference_ratios(s);
+        /* Now that the reference volume is updated, we can update the streams'
+         * reference ratios. */
+        compute_reference_ratios(root_sink);
     }
 
-    reference_changed = !pa_cvolume_equal(&old_reference_volume, &s->reference_volume);
-    s->save_volume = (!reference_changed && s->save_volume) || save;
-
-    if (s->set_volume) {
+    if (root_sink->set_volume) {
         /* If we have a function set_volume(), then we do not apply a
          * soft volume by default. However, set_volume() is free to
-         * apply one to s->soft_volume */
+         * apply one to root_sink->soft_volume */
 
-        pa_cvolume_reset(&s->soft_volume, s->sample_spec.channels);
-        if (!(s->flags & PA_SINK_SYNC_VOLUME))
-            s->set_volume(s);
+        pa_cvolume_reset(&root_sink->soft_volume, root_sink->sample_spec.channels);
+        if (!(root_sink->flags & PA_SINK_SYNC_VOLUME))
+            root_sink->set_volume(root_sink);
         else
             send_msg = TRUE;
 
     } else
         /* If we have no function set_volume(), then the soft volume
-         * becomes the virtual volume */
-        s->soft_volume = s->real_volume;
+         * becomes the real volume */
+        root_sink->soft_volume = root_sink->real_volume;
 
-    /* This tells the sink that soft and/or virtual volume changed */
+    /* This tells the sink that soft volume and/or real volume changed */
     if (send_msg)
-        pa_assert_se(pa_asyncmsgq_send(s->asyncmsgq, PA_MSGOBJECT(s), PA_SINK_MESSAGE_SET_VOLUME_SYNCED, NULL, 0, NULL) == 0);
-
-    if (reference_changed)
-        pa_subscription_post(s->core, PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_CHANGE, s->index);
+        pa_assert_se(pa_asyncmsgq_send(root_sink->asyncmsgq, PA_MSGOBJECT(root_sink), PA_SINK_MESSAGE_SET_SHARED_VOLUME, NULL, 0, NULL) == 0);
 }
 
 /* Called from the io thread if sync volume is used, otherwise from the main thread.
  * Only to be called by sink implementor */
 void pa_sink_set_soft_volume(pa_sink *s, const pa_cvolume *volume) {
     pa_sink_assert_ref(s);
+    pa_assert(!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER));
     if (s->flags & PA_SINK_SYNC_VOLUME)
         pa_sink_assert_io_context(s);
     else
@@ -1531,12 +1739,14 @@ void pa_sink_set_soft_volume(pa_sink *s, const pa_cvolume *volume) {
         s->thread_info.soft_volume = s->soft_volume;
 }
 
+/* Called from the main thread. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. */
 static void propagate_real_volume(pa_sink *s, const pa_cvolume *old_real_volume) {
     pa_sink_input *i;
     uint32_t idx;
-    pa_cvolume old_reference_volume;
 
     pa_sink_assert_ref(s);
+    pa_assert(old_real_volume);
     pa_assert_ctl_context();
     pa_assert(PA_SINK_IS_LINKED(s->state));
 
@@ -1545,20 +1755,18 @@ static void propagate_real_volume(pa_sink *s, const pa_cvolume *old_real_volume)
      * reference volume and then rebuild the stream volumes based on
      * i->real_ratio which should stay fixed. */
 
-    if (old_real_volume && pa_cvolume_equal(old_real_volume, &s->real_volume))
-        return;
-
-    old_reference_volume = s->reference_volume;
+    if (!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)) {
+        if (pa_cvolume_equal(old_real_volume, &s->real_volume))
+            return;
 
-    /* 1. Make the real volume the reference volume */
-    s->reference_volume = s->real_volume;
+        /* 1. Make the real volume the reference volume */
+        update_reference_volume(s, &s->real_volume, &s->channel_map, TRUE);
+    }
 
-    if (s->flags & PA_SINK_FLAT_VOLUME) {
+    if (pa_sink_flat_volume_enabled(s)) {
 
         PA_IDXSET_FOREACH(i, s->inputs, idx) {
-            pa_cvolume old_volume, remapped;
-
-            old_volume = i->volume;
+            pa_cvolume old_volume = i->volume;
 
             /* 2. Since the sink's reference and real volumes are equal
              * now our ratios should be too. */
@@ -1572,9 +1780,9 @@ static void propagate_real_volume(pa_sink *s, const pa_cvolume *old_real_volume)
              * i->volume = s->reference_volume * i->reference_ratio
              *
              * This is identical to propagate_reference_volume() */
-            remapped = s->reference_volume;
-            pa_cvolume_remap(&remapped, &s->channel_map, &i->channel_map);
-            pa_sw_cvolume_multiply(&i->volume, &remapped, &i->reference_ratio);
+            i->volume = s->reference_volume;
+            pa_cvolume_remap(&i->volume, &s->channel_map, &i->channel_map);
+            pa_sw_cvolume_multiply(&i->volume, &i->volume, &i->reference_ratio);
 
             /* Notify if something changed */
             if (!pa_cvolume_equal(&old_volume, &i->volume)) {
@@ -1584,16 +1792,17 @@ static void propagate_real_volume(pa_sink *s, const pa_cvolume *old_real_volume)
 
                 pa_subscription_post(i->core, PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_CHANGE, i->index);
             }
+
+            if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER))
+                propagate_real_volume(i->origin_sink, old_real_volume);
         }
     }
 
     /* Something got changed in the hardware. It probably makes sense
      * to save changed hw settings given that hw volume changes not
      * triggered by PA are almost certainly done by the user. */
-    s->save_volume = TRUE;
-
-    if (!pa_cvolume_equal(&old_reference_volume, &s->reference_volume))
-        pa_subscription_post(s->core, PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_CHANGE, s->index);
+    if (!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER))
+        s->save_volume = TRUE;
 }
 
 /* Called from io thread */
@@ -1613,6 +1822,8 @@ const pa_cvolume *pa_sink_get_volume(pa_sink *s, pa_bool_t force_refresh) {
     if (s->refresh_volume || force_refresh) {
         struct pa_cvolume old_real_volume;
 
+        pa_assert(!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER));
+
         old_real_volume = s->real_volume;
 
         if (!(s->flags & PA_SINK_SYNC_VOLUME) && s->get_volume)
@@ -1620,25 +1831,27 @@ const pa_cvolume *pa_sink_get_volume(pa_sink *s, pa_bool_t force_refresh) {
 
         pa_assert_se(pa_asyncmsgq_send(s->asyncmsgq, PA_MSGOBJECT(s), PA_SINK_MESSAGE_GET_VOLUME, NULL, 0, NULL) == 0);
 
+        update_real_volume(s, &s->real_volume, &s->channel_map);
         propagate_real_volume(s, &old_real_volume);
     }
 
     return &s->reference_volume;
 }
 
-/* Called from main thread */
+/* Called from main thread. In volume sharing cases, only the root sink may
+ * call this. */
 void pa_sink_volume_changed(pa_sink *s, const pa_cvolume *new_real_volume) {
     pa_cvolume old_real_volume;
 
     pa_sink_assert_ref(s);
     pa_assert_ctl_context();
     pa_assert(PA_SINK_IS_LINKED(s->state));
+    pa_assert(!(s->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER));
 
     /* The sink implementor may call this if the volume changed to make sure everyone is notified */
 
     old_real_volume = s->real_volume;
-    s->real_volume = *new_real_volume;
-
+    update_real_volume(s, new_real_volume, &s->channel_map);
     propagate_real_volume(s, &old_real_volume);
 }
 
@@ -1853,10 +2066,27 @@ static void sync_input_volumes_within_thread(pa_sink *s) {
 
         if (!pa_atomic_load(&i->before_ramping_v))
             i->thread_info.soft_volume = i->soft_volume;
+
         pa_sink_input_request_rewind(i, 0, TRUE, FALSE, FALSE);
     }
 }
 
+/* Called from the IO thread. Only called for the root sink in volume sharing
+ * cases, except for internal recursive calls. */
+static void set_shared_volume_within_thread(pa_sink *s) {
+    pa_sink_input *i = NULL;
+    void *state = NULL;
+
+    pa_sink_assert_ref(s);
+
+    PA_MSGOBJECT(s)->process_msg(PA_MSGOBJECT(s), PA_SINK_MESSAGE_SET_VOLUME_SYNCED, NULL, 0, NULL);
+
+    PA_HASHMAP_FOREACH(i, s->thread_info.inputs, state) {
+        if (i->origin_sink && (i->origin_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER))
+            set_shared_volume_within_thread(i->origin_sink);
+    }
+}
+
 /* Called from IO thread, except when it is not */
 int pa_sink_process_msg(pa_msgobject *o, int code, void *userdata, int64_t offset, pa_memchunk *chunk) {
     pa_sink *s = PA_SINK(o);
@@ -1913,7 +2143,7 @@ int pa_sink_process_msg(pa_msgobject *o, int code, void *userdata, int64_t offse
 
             /* In flat volume mode we need to update the volume as
              * well */
-            return o->process_msg(o, PA_SINK_MESSAGE_SET_VOLUME_SYNCED, NULL, 0, NULL);
+            return o->process_msg(o, PA_SINK_MESSAGE_SET_SHARED_VOLUME, NULL, 0, NULL);
         }
 
         case PA_SINK_MESSAGE_REMOVE_INPUT: {
@@ -1956,7 +2186,7 @@ int pa_sink_process_msg(pa_msgobject *o, int code, void *userdata, int64_t offse
 
             /* In flat volume mode we need to update the volume as
              * well */
-            return o->process_msg(o, PA_SINK_MESSAGE_SET_VOLUME_SYNCED, NULL, 0, NULL);
+            return o->process_msg(o, PA_SINK_MESSAGE_SET_SHARED_VOLUME, NULL, 0, NULL);
         }
 
         case PA_SINK_MESSAGE_START_MOVE: {
@@ -2001,7 +2231,7 @@ int pa_sink_process_msg(pa_msgobject *o, int code, void *userdata, int64_t offse
 
             /* In flat volume mode we need to update the volume as
              * well */
-            return o->process_msg(o, PA_SINK_MESSAGE_SET_VOLUME_SYNCED, NULL, 0, NULL);
+            return o->process_msg(o, PA_SINK_MESSAGE_SET_SHARED_VOLUME, NULL, 0, NULL);
         }
 
         case PA_SINK_MESSAGE_FINISH_MOVE: {
@@ -2042,9 +2272,17 @@ int pa_sink_process_msg(pa_msgobject *o, int code, void *userdata, int64_t offse
                 pa_sink_request_rewind(s, nbytes);
             }
 
-            /* In flat volume mode we need to update the volume as
-             * well */
-            return o->process_msg(o, PA_SINK_MESSAGE_SET_VOLUME_SYNCED, NULL, 0, NULL);
+            return o->process_msg(o, PA_SINK_MESSAGE_SET_SHARED_VOLUME, NULL, 0, NULL);
+        }
+
+        case PA_SINK_MESSAGE_SET_SHARED_VOLUME: {
+            pa_sink *root_sink = s;
+
+            while (root_sink->flags & PA_SINK_SHARE_VOLUME_WITH_MASTER)
+                root_sink = root_sink->input_to_master->sink;
+
+            set_shared_volume_within_thread(root_sink);
+            return 0;
         }
 
         case PA_SINK_MESSAGE_SET_VOLUME_SYNCED:
@@ -2062,9 +2300,6 @@ int pa_sink_process_msg(pa_msgobject *o, int code, void *userdata, int64_t offse
                 pa_sink_request_rewind(s, (size_t) -1);
             }
 
-            if (!(s->flags & PA_SINK_FLAT_VOLUME))
-                return 0;
-
             /* Fall through ... */
 
         case PA_SINK_MESSAGE_SYNC_VOLUMES: