[GH-ISSUE #1162] Implement generic dummy mixer #536

Open
opened 2026-02-27 19:31:10 +03:00 by kerem · 3 comments
Owner

Originally created by @sqozz on GitHub (May 11, 2023).
Original GitHub issue: https://github.com/librespot-org/librespot/issues/1162

Is your feature request related to a problem? Please describe.
I route librespot into pulseaudio to mix it with other sources. The result is written into a socket which is consumed by snapcast. From a users perspective it would be nice if the volume slider in Spotify/the Spotify App could control the general master volume of snapcast.

Describe the solution you'd like
I would like to control the Spotify volume slider without adjusting the actual volume. The volume regulation should happen by an external script which receives the selected volume (I think onevent already receives the selected volume) and e.g. does http requests to something to adjust the volume.

Describe alternatives you've considered
I considered (ab)using pulseaudio for that purpose. Unfortunately it seems like the PA backend is not supported for the --mixer option. I also tried to make use of --mixer softvol --volume-ctrl fixed --initial-volume 100 (Which is described here) but this just disabled volume control via Spotify completely and therefore an onevent-script cannot receive the volume change.

Additional context
Supporting pulseaudio as mixer might be a viable solution but I think a much more generic solution would be a "dummy" volume regulation which receives the selected level but does not adjust the audio stream.

Originally created by @sqozz on GitHub (May 11, 2023). Original GitHub issue: https://github.com/librespot-org/librespot/issues/1162 **Is your feature request related to a problem? Please describe.** I route librespot into pulseaudio to mix it with other sources. The result is written into a socket which is consumed by [snapcast](https://github.com/badaix/snapcast). From a users perspective it would be nice if the volume slider in Spotify/the Spotify App could control the general master volume of snapcast. **Describe the solution you'd like** I would like to control the Spotify volume slider without adjusting the actual volume. The volume regulation should happen by an external script which receives the selected volume (I think `onevent` already receives the selected volume) and e.g. does http requests to something to adjust the volume. **Describe alternatives you've considered** I considered (ab)using pulseaudio for that purpose. Unfortunately it seems like the PA backend is not supported for the `--mixer` option. I also tried to make use of `--mixer softvol --volume-ctrl fixed --initial-volume 100` (Which is described [here](https://github.com/librespot-org/librespot/wiki/Options#mixer)) but this just disabled volume control via Spotify completely and therefore an `onevent`-script cannot receive the volume change. **Additional context** Supporting pulseaudio as mixer might be a viable solution but I think a much more generic solution would be a "dummy" volume regulation which receives the selected level but does not adjust the audio stream.
Author
Owner

@kingosticks commented on GitHub (May 11, 2023):

When you specify --volume-ctrl fixed we do not advertise the ability to change the volume and the UI will likely be disabled in a Connect controller, as you saw.

We already have librespot::playback::mixer::NoOpVolume but it's not exposed through the --mixer option. Adding it will probably be enough to make this use case work. Give it a try?

<!-- gh-comment-id:1543907450 --> @kingosticks commented on GitHub (May 11, 2023): When you specify `--volume-ctrl fixed` we do not advertise the ability to change the volume and the UI will likely be disabled in a Connect controller, as you saw. We already have `librespot::playback::mixer::NoOpVolume` but it's not exposed through the `--mixer` option. Adding it will probably be enough to make this use case work. Give it a try?
Author
Owner

@sqozz commented on GitHub (May 12, 2023):

We already have librespot::playback::mixer::NoOpVolume but it's not exposed through the --mixer option. Adding it will probably be enough to make this use case work. Give it a try?

Yes this sounds like it would actually cover my needs. I tried to come up with some code changes but never programmed in rust and it is not even close to be suitable as a PR so I will just attach it as patch here:

diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs
index 0a8b8d6..b7c35cb 100644
--- a/playback/src/mixer/mod.rs
+++ b/playback/src/mixer/mod.rs
@@ -30,6 +30,8 @@ impl VolumeGetter for NoOpVolume {

 pub mod softmixer;
 use self::softmixer::SoftMixer;
+pub mod noopmixer;
+use self::noopmixer::NoOpMixer;

 #[cfg(feature = "alsa-backend")]
 pub mod alsamixer;
@@ -63,6 +65,7 @@ fn mk_sink<M: Mixer + 'static>(config: MixerConfig) -> Box<dyn Mixer> {

 pub const MIXERS: &[(&str, MixerFn)] = &[
     (SoftMixer::NAME, mk_sink::<SoftMixer>), // default goes first
+    (NoOpMixer::NAME, mk_sink::<NoOpMixer>),
     #[cfg(feature = "alsa-backend")]
     (AlsaMixer::NAME, mk_sink::<AlsaMixer>),
 ];
diff --git a/playback/src/mixer/noopmixer.rs b/playback/src/mixer/noopmixer.rs
new file mode 100644
index 0000000..867c950
--- /dev/null
+++ b/playback/src/mixer/noopmixer.rs
@@ -0,0 +1,49 @@
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::Arc;
+
+use super::VolumeGetter;
+use super::{MappedCtrl, VolumeCtrl};
+use super::{Mixer, MixerConfig};
+
+#[derive(Clone)]
+pub struct NoOpMixer {
+    // There is no AtomicF64, so we store the f64 as bits in a u64 field.
+    // It's much faster than a Mutex<f64>.
+    volume: Arc<AtomicU64>,
+    volume_ctrl: VolumeCtrl,
+}
+
+impl Mixer for NoOpMixer {
+    fn open(config: MixerConfig) -> Self {
+        let volume_ctrl = config.volume_ctrl;
+        info!("Mixing with NoOp mixer");
+
+        Self {
+            volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))),
+            volume_ctrl,
+        }
+    }
+
+    fn volume(&self) -> u16 {
+        let mapped_volume = f64::from_bits(self.volume.load(Ordering::Relaxed));
+        self.volume_ctrl.as_unmapped(mapped_volume)
+    }
+
+    fn set_volume(&self, volume: u16) {
+        let mapped_volume = self.volume_ctrl.to_mapped(volume);
+        self.volume
+            .store(mapped_volume.to_bits(), Ordering::Relaxed)
+    }
+}
+
+impl NoOpMixer {
+    pub const NAME: &'static str = "noop";
+}
+
+struct NoOpVolume(Arc<AtomicU64>);
+
+impl VolumeGetter for NoOpVolume {
+    fn attenuation_factor(&self) -> f64 {
+        f64::from_bits(self.0.load(Ordering::Relaxed))
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index e4727ba..a6deb46 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -290,7 +290,7 @@ fn get_setup() -> Setup {
     // Options that have different descriptions
     // depending on what backends were enabled at build time.
     #[cfg(feature = "alsa-backend")]
-    const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol}. Defaults to softvol.";
+    const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol|noop}. Defaults to softvol.";
     #[cfg(not(feature = "alsa-backend"))]
     const MIXER_TYPE_DESC: &str = "Not supported by the included audio backend(s).";
     #[cfg(any(
@@ -789,10 +789,10 @@ fn get_setup() -> Setup {
         }
     }

-    #[cfg(feature = "alsa-backend")]
+    //#[cfg(feature = "alsa-backend")]
     let mixer_type = opt_str(MIXER_TYPE);
-    #[cfg(not(feature = "alsa-backend"))]
-    let mixer_type: Option<String> = None;
+    //#[cfg(not(feature = "alsa-backend"))]
+    //let mixer_type: Option<String> = None;

     let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| {
         invalid_error_msg(

It somehow works but the whole "softvol as default" logic for --mixer is now broken. If somebody has hints or wants to pick up my patch I'd be very happy :) My snippet is CC-0 so no mention, credit, anything needed.

<!-- gh-comment-id:1546437417 --> @sqozz commented on GitHub (May 12, 2023): > We already have `librespot::playback::mixer::NoOpVolume` but it's not exposed through the `--mixer` option. Adding it will probably be enough to make this use case work. Give it a try? Yes this sounds like it would actually cover my needs. I tried to come up with some code changes but never programmed in rust and it is not even close to be suitable as a PR so I will just attach it as patch here: ```patch diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 0a8b8d6..b7c35cb 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -30,6 +30,8 @@ impl VolumeGetter for NoOpVolume { pub mod softmixer; use self::softmixer::SoftMixer; +pub mod noopmixer; +use self::noopmixer::NoOpMixer; #[cfg(feature = "alsa-backend")] pub mod alsamixer; @@ -63,6 +65,7 @@ fn mk_sink<M: Mixer + 'static>(config: MixerConfig) -> Box<dyn Mixer> { pub const MIXERS: &[(&str, MixerFn)] = &[ (SoftMixer::NAME, mk_sink::<SoftMixer>), // default goes first + (NoOpMixer::NAME, mk_sink::<NoOpMixer>), #[cfg(feature = "alsa-backend")] (AlsaMixer::NAME, mk_sink::<AlsaMixer>), ]; diff --git a/playback/src/mixer/noopmixer.rs b/playback/src/mixer/noopmixer.rs new file mode 100644 index 0000000..867c950 --- /dev/null +++ b/playback/src/mixer/noopmixer.rs @@ -0,0 +1,49 @@ +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +use super::VolumeGetter; +use super::{MappedCtrl, VolumeCtrl}; +use super::{Mixer, MixerConfig}; + +#[derive(Clone)] +pub struct NoOpMixer { + // There is no AtomicF64, so we store the f64 as bits in a u64 field. + // It's much faster than a Mutex<f64>. + volume: Arc<AtomicU64>, + volume_ctrl: VolumeCtrl, +} + +impl Mixer for NoOpMixer { + fn open(config: MixerConfig) -> Self { + let volume_ctrl = config.volume_ctrl; + info!("Mixing with NoOp mixer"); + + Self { + volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))), + volume_ctrl, + } + } + + fn volume(&self) -> u16 { + let mapped_volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); + self.volume_ctrl.as_unmapped(mapped_volume) + } + + fn set_volume(&self, volume: u16) { + let mapped_volume = self.volume_ctrl.to_mapped(volume); + self.volume + .store(mapped_volume.to_bits(), Ordering::Relaxed) + } +} + +impl NoOpMixer { + pub const NAME: &'static str = "noop"; +} + +struct NoOpVolume(Arc<AtomicU64>); + +impl VolumeGetter for NoOpVolume { + fn attenuation_factor(&self) -> f64 { + f64::from_bits(self.0.load(Ordering::Relaxed)) + } +} diff --git a/src/main.rs b/src/main.rs index e4727ba..a6deb46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -290,7 +290,7 @@ fn get_setup() -> Setup { // Options that have different descriptions // depending on what backends were enabled at build time. #[cfg(feature = "alsa-backend")] - const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol}. Defaults to softvol."; + const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol|noop}. Defaults to softvol."; #[cfg(not(feature = "alsa-backend"))] const MIXER_TYPE_DESC: &str = "Not supported by the included audio backend(s)."; #[cfg(any( @@ -789,10 +789,10 @@ fn get_setup() -> Setup { } } - #[cfg(feature = "alsa-backend")] + //#[cfg(feature = "alsa-backend")] let mixer_type = opt_str(MIXER_TYPE); - #[cfg(not(feature = "alsa-backend"))] - let mixer_type: Option<String> = None; + //#[cfg(not(feature = "alsa-backend"))] + //let mixer_type: Option<String> = None; let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { invalid_error_msg( ``` It somehow works but the whole "softvol as default" logic for `--mixer` is now broken. If somebody has hints or wants to pick up my patch I'd be very happy :) My snippet is CC-0 so no mention, credit, anything needed.
Author
Owner

@martignlo commented on GitHub (Mar 13, 2025):

I'm also very interested in this feature. My use case is to listen for volume change events and execute a command (ir-send to increase/decrease the volume of the amplifier).

I'm currently using softvol, but the result is that the volume is adjusted on the amplifier, but also in librespot.

Any chance the NoOpMixer can be added to the list of mixers?

<!-- gh-comment-id:2722659749 --> @martignlo commented on GitHub (Mar 13, 2025): I'm also very interested in this feature. My use case is to listen for volume change events and execute a command (`ir-send` to increase/decrease the volume of the amplifier). I'm currently using `softvol`, but the result is that the volume is adjusted on the amplifier, but also in librespot. Any chance the `NoOpMixer` can be added to the list of mixers?
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/librespot#536
No description provided.