mk-fg / python-pulse-control

Python high-level interface and ctypes-based bindings for PulseAudio (libpulse)
https://pypi.org/project/pulsectl/
MIT License
170 stars 36 forks source link

Setting volume on sink linear percentage but on sink source is cuberoot of percentage. #64

Closed baileyji closed 3 years ago

baileyji commented 3 years ago

It looks like the volume values are passing through different pathways internally depending on setting the value for a sink input vs a sink. E.g. to get levels set to 50% (.5) for the sink and each input one must do as follows:

  with Pulse('volume-setter') as pulse:
            source = pulse.sink_input_list()[0]
            pulse.volume_set_all_chans(source, np.cbrt(volume_pct))
            # Alternatively
            # vol = source[0].volume
            # vol.value_flat = np.cbrt(volume_pct)
            # pulse.volume_set(source[0], np.cbrt(volume_pct))

            pulse.volume_set_all_chans(pulse.sink_list()[0], volume_pct)
mk-fg commented 3 years ago

Yes, pulseaudio volumes are cubic iirc, and stream volumes are those, existing purely within pulseaudio.

While sink/source volumes are basically an exposed ALSA slider, which in turn is an exposed slider in the driver or some hardware register, and those can mean anything, depending on the driver (and decade it was written in), specific hardware, etc - so it can be cubic too, or quadratic, linear, hyperbolic, bathtub curve, or anything else. Suspect that cheap hardware DACs can also have same value on such slider be different between same-model soundcards wrt their analog output levels, due to tolerances and differences in hardware electronic components.

Don't think it's a bug here though, just how volumes in pulseaudio work, but maybe worth adding a note in the README, in case this causes unnecessary confusion, given that there's a section about it already, and some conversion is done here after all.

mk-fg commented 3 years ago

Oh, this seem to be already briefly addressed there:

See ``src/pulse/volume.h`` in pulseaudio sources for all the gory details
on the latter (e.g. how it relates to sound level in dB).

Which I think refers to these bits (should also be in generated pulse doxygen docs):

 * Volumes commonly span between muted (0%), and normal (100%). It is possible
 * to set volumes to higher than 100%, but clipping might occur.
 *
 * There is no single well-defined meaning attached to the 100% volume for a
 * sink input. In fact, it depends on the server configuration. With flat
 * volumes enabled, it means the maximum volume that the sound hardware is
 * capable of, which is usually so high that you absolutely must not set sink
 * input volume to 100% unless the the user explicitly requests that (note that
 * usually you shouldn't set the volume anyway if the user doesn't explicitly
 * request it, instead, let PulseAudio decide the volume for the sink input).
 * With flat volumes disabled the sink input volume is relative to the sink
 * volume, so 100% sink input volume means that the sink input is played at the
 * current sink volume level. In this case 100% is often a good default volume
 * for a sink input, although you still should let PulseAudio decide the
 * default volume. It is possible to figure out whether flat volume mode is in
 * effect for a given sink by calling pa_context_get_sink_info_by_name().
...
 * \section calc_sec Calculations
 *
 * The volumes in PulseAudio are cubic in nature and applications shouldn't
 * perform calculations with them directly. Instead, they should be converted
 * to and from either dB or a linear scale:
 *
 * \li dB - pa_sw_volume_from_dB() / pa_sw_volume_to_dB()
 * \li Linear - pa_sw_volume_from_linear() / pa_sw_volume_to_linear()
 *
 * For simple multiplication, pa_sw_volume_multiply() and
 * pa_sw_cvolume_multiply() can be used.
 *
 * It's often unknown what scale hardware volumes relate to. Don't use the
 * above functions on sink and source volumes, unless the sink or source in
 * question has the PA_SINK_DECIBEL_VOLUME or PA_SOURCE_DECIBEL_VOLUME flag
 * set. The conversion functions are rarely needed anyway, most of the time
 * it's sufficient to treat all volumes as opaque with a range from
 * PA_VOLUME_MUTED (0%) to PA_VOLUME_NORM (100%).
...

So not sure if there's anything else to add really, as that seem to be a good description already, and don't think it's important enough for most people to copy-paste into README here.