Pipewire: Support modifying sink volume instead of stream volume
When using Pipewire as an output plugin and changing the volume in MPD the stream volume is modified. If the computer is both used as a music player and for other things, this is problematic.
Either the general volume (sink volume) is on a normal level (e.g. 10%), then MPD is limited to 100% stream volume and there is no possibility to listen to music a bit louder than when sitting in front of the computer (overall (obviously simplified) 10% * 100% = 10%).
Alternatively, the sink volume is loud per default (e.g. 100%) and all used applications have a low stream value (e.g. 10%), then MPD has the full dynamic range. However, as soon as a new application outputs some sounds, all neighbors are informed about that, as the (new) stream value default is 100% (resulting in 100% * 100% = 100%). ;-)
Therefore, there should be an option to switch between controlling sink volume and stream volume when using Pipewire.
I wrote a patch how this feature could be implemented. If MPD's config file contains a Pipewire audio_output section with the entry mixer_volume "sink", then the sink volume is used and modified. For mixer_volume "stream" (and as default value) the behavior is as before.
However, I guess this patch is not mergeable, as
- proper error handling is missing
- test depth is low
- it uses a system call to read and set the volume
- it depends on Wireplumber being used
- documentation is missing
- currently does not respect an explicitly set target (device? sink?)
Solving 1. and 5. would be possible, but makes most sense after 3. and 4. is solved. I will implicitly work on 2. during the next days (I just put the code on the living room's MPD). However, 3. seems to be the tricky one. Pipewire's library documentation is hardly comprehensible, the source of the currently used tool wpctl is quite complex and Wireplumber specific.
So the most promising way forward would be to follow the pw-cli example in pw-cli's source code and call the necessary library functions from MPD. However, I will probably not work on that due to other tasks.
If anyone wants to continue, here is the patch:
diff '--color=auto' -ruN mpd-0.23.8-old/src/mixer/plugins/PipeWireMixerPlugin.cxx mpd-0.23.8-new/src/mixer/plugins/PipeWireMixerPlugin.cxx
--- mpd-0.23.8-old/src/mixer/plugins/PipeWireMixerPlugin.cxx 2022-07-09 01:05:38.000000000 +0200
+++ mpd-0.23.8-new/src/mixer/plugins/PipeWireMixerPlugin.cxx 2022-08-09 17:07:44.000000000 +0200
@@ -24,17 +24,32 @@
#include <cmath>
+#include "config/Block.hxx"
+#include <cstring>
+#include <memory>
+// #include "Log.hxx"
+// #include "util/Domain.hxx"
+
+// static constexpr Domain pipewire_mixer_domain("pipewire_mixer");
+
class PipeWireMixer final : public Mixer {
PipeWireOutput &output;
int volume = 100;
+ bool sink_volume = false;
+
public:
PipeWireMixer(PipeWireOutput &_output,
- MixerListener &_listener) noexcept
+ MixerListener &_listener,
+ const ConfigBlock &block) noexcept
:Mixer(pipewire_mixer_plugin, _listener),
output(_output)
{
+ const char *const volume_name(
+ block.GetBlockValue("mixer_volume", nullptr));
+ if (volume_name != nullptr)
+ sink_volume = !strcmp(volume_name, "sink");
}
~PipeWireMixer() noexcept override;
@@ -65,27 +80,60 @@
pm.OnVolumeChanged(new_volume);
}
+static std::string
+exec(const char* cmd) {
+ std::array<char, 128> buffer;
+ std::string result;
+ std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
+ if (!pipe) {
+ throw std::runtime_error("popen() failed!");
+ }
+ while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
+ result += buffer.data();
+ }
+ return result;
+}
+
int
PipeWireMixer::GetVolume()
{
+ /* If global volume is active, use the sink's volume internally. */
+ if (sink_volume) {
+ // FmtWarning(pipewire_mixer_domain, "Old mixer volume: {}", volume);
+ std::string vol = exec("/bin/wpctl get-volume @DEFAULT_AUDIO_SINK@");
+ // vol contains e.g. "Volume: 0.78" whereas e.g. "78" is needed
+ size_t index_first_digit = vol.find_first_of(' ') + 1;
+ volume = round(std::stof(vol.substr(index_first_digit)) * 100.f);
+ // FmtWarning(pipewire_mixer_domain, "New volume of mixer from default audio sink: {}", volume);
+ }
+
return volume;
}
void
PipeWireMixer::SetVolume(unsigned new_volume)
{
- pipewire_output_set_volume(output, float(new_volume) * 0.01f);
+ if (sink_volume) {
+ // FmtWarning(pipewire_mixer_domain, "In SetVolume: {}", float(new_volume) * 0.01f);
+
+ std::string setCmd("/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ ");
+ setCmd = setCmd + std::to_string(float(new_volume) * 0.01f);
+ std::string ret = exec(setCmd.c_str());
+ } else
+ pipewire_output_set_volume(output, float(new_volume) * 0.01f);
+
volume = new_volume;
}
static Mixer *
pipewire_mixer_init([[maybe_unused]] EventLoop &event_loop, AudioOutput &ao,
MixerListener &listener,
- const ConfigBlock &)
+ const ConfigBlock &block)
{
auto &po = (PipeWireOutput &)ao;
- auto *pm = new PipeWireMixer(po, listener);
+ auto *pm = new PipeWireMixer(po, listener, block);
pipewire_output_set_mixer(po, *pm);
+
return pm;
}
@@ -98,3 +146,24 @@
pipewire_mixer_init,
true,
};
diff '--color=auto' -ruN mpd-0.23.8-old/src/mixer/plugins/PipeWireMixerPlugin.hxx mpd-0.23.8-new/src/mixer/plugins/PipeWireMixerPlugin.hxx
--- mpd-0.23.8-old/src/mixer/plugins/PipeWireMixerPlugin.hxx 2022-07-09 01:05:38.000000000 +0200
+++ mpd-0.23.8-new/src/mixer/plugins/PipeWireMixerPlugin.hxx 2022-08-09 07:36:10.000000000 +0200
@@ -20,6 +20,8 @@
#ifndef MPD_PIPEWIRE_MIXER_PLUGIN_HXX
#define MPD_PIPEWIRE_MIXER_PLUGIN_HXX
+#include <string>
+
struct MixerPlugin;
class PipeWireMixer;
@@ -28,4 +30,7 @@
void
pipewire_mixer_on_change(PipeWireMixer &pm, float new_volume) noexcept;
+static std::string
+exec(const char* cmd);
+
#endif
Calling an external program is not acceptable. If you ever find out how to do it with a library call instead, we can talk about merging.