[pulseaudio-discuss] [PATCH 1/6] Implement the "volume sharing" feature.

Colin Guthrie gmane at colin.guthr.ie
Mon Feb 28 03:38:01 PST 2011


Hiya,

Some quick comments but these are to go on top rather than change this
as I've already merged it :)

'Twas brillig, and Tanu Kaskinen at 24/02/11 14:16 did gyre and gimble:
> When we have a filter sink that does some processing, currently the
> benefits of the flat volume feature are not really available. That's
> because if you have a music player that is connected to the filter sink,
> the hardware sink doesn't have any idea of the music player's stream
> volume.
> 
> This problem is solved by this "volume sharing" feature. The volume
> sharing feature works so that the filter sinks that want to avoid the
> previously described problem declare that they don't want to have
> independent volume, but they follow the master sink volume instead.
> The PA_SINK_SHARE_VOLUME_WITH_MASTER sink flag is used for that
> declaration. Then the volume logic is changed so that the hardware
> sink calculates its real volume using also the streams connected to the
> filter sink in addition to the streams that are connected directly to
> the hardware sink. Basically we're trying to create an illusion that
> from volume point of view all streams are connected directly to the
> hardware sink.
> 
> For that illusion to work, the volumes of the filter sinks and their
> virtual streams have to be managed carefully according to a set of
> rules:
> 
> If a filter sink follows the hardware sink volume, then the filter sink's
>  * reference_volume always equals the hw sink's reference_volume
>  * real_volume always equals the hw sink's real_volume
>  * soft_volume is always 0dB (ie. no soft volume)
> 
> If a filter sink doesn't follow the hardware sink volume, then the filter
> sink's
>  * reference_volume can be whatever (completely independent from the hw sink)
>  * real_volume always equals reference_volume
>  * soft_volume always equals real_volume (and reference_volume)
> 
> If a filter sink follows the hardware sink volume, and the hardware sink
> supports flat volume, then the filter sink's virtual stream's
>  * volume always equals the hw sink's real_volume
>  * reference_ratio is calculated normally from the stream volume and the hw
>    sink's reference_volume
>  * real_ratio always equals 0dB (follows from the first point)
>  * soft_volume always equals volume_factor (follows from the previous point)
> 
> If a filter sink follows the hardware sink volume, and the hardware sink
> doesn't support flat volume, then the filter sink's virtual stream's
>  * volume is always 0dB
>  * reference_ratio is always 0dB
>  * real_ratio is always 0dB
>  * soft_volume always equals volume_factor
> 
> If a filter sink doesn't follow the hardware sink volume, then the filter
> sink's virtual stream is handled as a regular stream.
> 
> Since the volumes of the virtual streams are controlled by a set of rules,
> the user is not allowed to change the virtual streams' volumes. It would
> probably also make sense to forbid changing the filter sinks' volume, but
> that's not strictly necessary, and currently changing a filter sink's volume
> changes actually the hardware sink's volume, and from there it propagates to
> all filter sinks ("funny" effects are expected when adjusting a single
> channel in cases where all sinks don't have the same channel maps).
> ---
>  src/pulse/def.h                 |   11 +
>  src/pulsecore/protocol-native.c |    2 +-
>  src/pulsecore/sink-input.c      |  204 +++++++++++++--
>  src/pulsecore/sink.c            |  555 ++++++++++++++++++++++++++++-----------
>  src/pulsecore/sink.h            |    4 +
>  5 files changed, 591 insertions(+), 185 deletions(-)
> 
> diff --git a/src/pulse/def.h b/src/pulse/def.h
> index 4a8da13..f6de7b3 100644
> --- a/src/pulse/def.h
> +++ b/src/pulse/def.h
> @@ -749,6 +749,16 @@ typedef enum pa_sink_flags {
>      /**< The HW volume changes are syncronized with SW volume.
>       * \since 1.0 */
>  
> +/** \cond fulldocs */
> +    /* PRIVATE: Server-side values -- do not try to use these at client-side.
> +     * The server will filter out these flags anyway, so you should never see
> +     * these flags in sinks. */
> +
> +    PA_SINK_SHARE_VOLUME_WITH_MASTER = 0x0400U,
> +    /**< This sink shares the volume with the master sink (used by some filter
> +     * sinks). */
> +/** \endcond */


Out of curiosity, is there any problem letting clients "see" these
flags. I can understand not letting them adjust/set them, but do we
really want to hide them away?

>  } pa_sink_flags_t;
>  
>  /** \cond fulldocs */
> @@ -762,6 +772,7 @@ typedef enum pa_sink_flags {
>  #define PA_SINK_DYNAMIC_LATENCY PA_SINK_DYNAMIC_LATENCY
>  #define PA_SINK_PASSTHROUGH PA_SINK_PASSTHROUGH
>  #define PA_SINK_SYNC_VOLUME PA_SINK_SYNC_VOLUME
> +#define PA_SINK_SHARE_VOLUME_WITH_MASTER PA_SINK_SHARE_VOLUME_WITH_MASTER

Could we have a PA_SINK_SERVER_SIDE_FLAGS define here that current is just:

#define PA_SINK_SERVER_SIDE_FLAGS PA_SINK_SHARE_VOLUME_WITH_MASTER

but could extend to:

#define PA_SINK_SERVER_SIDE_FLAGS
PA_SINK_SHARE_VOLUME_WITH_MASTER|PA_SINK_SOME_SECRET_FLAG


in the future.....


>  /** \endcond */
>  
> diff --git a/src/pulsecore/protocol-native.c b/src/pulsecore/protocol-native.c
> index 9257524..c812a3e 100644
> --- a/src/pulsecore/protocol-native.c
> +++ b/src/pulsecore/protocol-native.c
> @@ -2902,7 +2902,7 @@ static void sink_fill_tagstruct(pa_native_connection *c, pa_tagstruct *t, pa_sin
>          PA_TAG_STRING, sink->monitor_source ? sink->monitor_source->name : NULL,
>          PA_TAG_USEC, pa_sink_get_latency(sink),
>          PA_TAG_STRING, sink->driver,
> -        PA_TAG_U32, sink->flags,
> +        PA_TAG_U32, sink->flags & ~PA_SINK_SHARE_VOLUME_WITH_MASTER,
>          PA_TAG_INVALID);


.... and then use PA_SINK_SERVER_SIDE_FLAGS here instead?

It'll just make grepping the code easier and aid future readability I think.

> @@ -1282,6 +1300,156 @@ int pa_sink_input_start_move(pa_sink_input *i) {
>      return 0;
>  }
>  
> +/* Called from main context. If i has an origin sink that uses volume sharing,
> + * then also the origin sink and all streams connected to it need to update
> + * their volume - this function does all that by using recursion. */
> +static void update_volume_due_to_moving(pa_sink_input *i, pa_sink *dest) {
> +    pa_cvolume old_volume;
> +
> +    pa_assert(i);
> +    pa_assert(dest);
> +    pa_assert(i->sink); /* The destination sink should already be set. */

If it should already be set, do you mean that dest==i->sink (in which
case that's what the assert should say) or should the comment read "The
previous sink should already be set"?

> +    /* If i->sink == dest, then recursion has finished, and we can finally call
> +     * pa_sink_set_volume(), which will do the rest of the updates. */
> +    if ((i->sink == dest) && pa_sink_flat_volume_enabled(i->sink))
> +        pa_sink_set_volume(i->sink, NULL, FALSE, i->save_volume);
> +}


Judging from this comment at the tail of that function, I'm guessing
that the comment in the above hunk needs a little clarification. The
rest of the comments in this function are awesomely detailed! :)

>  /* Called from main context */
>  int pa_sink_input_finish_move(pa_sink_input *i, pa_sink *dest, pa_bool_t save) {
>      pa_resampler *new_resampler;
> @@ -1355,17 +1523,7 @@ int pa_sink_input_finish_move(pa_sink_input *i, pa_sink *dest, pa_bool_t save) {
>      }
>      pa_sink_update_status(dest);
>  
> -    if (i->sink->flags & PA_SINK_FLAT_VOLUME) {
> -        pa_cvolume remapped;
> -
> -        /* Make relative volumes absolute */
> -        remapped = dest->reference_volume;
> -        pa_cvolume_remap(&remapped, &dest->channel_map, &i->channel_map);
> -        pa_sw_cvolume_multiply(&i->volume, &i->reference_ratio, &remapped);
> -
> -        /* We might need to update the sink's volume if we are in flat volume mode. */
> -        pa_sink_set_volume(i->sink, NULL, FALSE, i->save_volume);
> -    }
> +    update_volume_due_to_moving(i, dest);
>  
>      pa_assert_se(pa_asyncmsgq_send(i->sink->asyncmsgq, PA_MSGOBJECT(i->sink), PA_SINK_MESSAGE_FINISH_MOVE, i, 0, NULL) == 0);
>  
> @@ -1374,9 +1532,6 @@ int pa_sink_input_finish_move(pa_sink_input *i, pa_sink *dest, pa_bool_t save) {
>      /* Notify everyone */
>      pa_hook_fire(&i->core->hooks[PA_CORE_HOOK_SINK_INPUT_MOVE_FINISH], i);
>  
> -    if (i->volume_changed)
> -        i->volume_changed(i);
> -
>      pa_subscription_post(i->core, PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_CHANGE, i->index);
>  
>      return 0;

The volume_changed now fires before the hook. I don't think this will
really matter tho' (especially considering the current usage of that hook).




> @@ -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)) {

I presume that removing the intermediate "remapped" variable here cannot
introduce any crazy race conditions?



> @@ -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;
> +

I guess there is no need for a pa_assert_se() here? We know this will be
OK from setup checks etc?


>      /* 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) {

Smells like these two "if (volume)" blocks could be squashed
together.... unless this is for readability?


> @@ -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);

Again "remapped" intermediate variable removed. Same race possibility
question as above.



> +/* 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);
> +    }
> +}
> +

Is PA_SINK_MESSAGE_SET_VOLUME_SYNCED rather than
PA_SINK_MESSAGE_SET_SHARED_VOLUME correct here?

I'm guessing from the below hunks that it is but it's getting a bit
confusing... with the message defines here...

>  /* 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;
>          }

The above changes make me think my previous comment does not require any
action. Just included for context.



This wasn't a massively thorough review, but it's the best I can likely
do without properly getting a handle on all the volume code - so sorry
about that.

In the mean time I've pushed all the changes I have and we can all go
mental on fixing it up :)

Cheers

Col


-- 

Colin Guthrie
gmane(at)colin.guthr.ie
http://colin.guthr.ie/

Day Job:
  Tribalogic Limited [http://www.tribalogic.net/]
Open Source:
  Mageia Contributor [http://www.mageia.org/]
  PulseAudio Hacker [http://www.pulseaudio.org/]
  Trac Hacker [http://trac.edgewall.org/]




More information about the pulseaudio-discuss mailing list