Skip to content

Commit 1f7e8fc

Browse files
committed
alsa mixer: make volume computation more robust
1 parent 1ebce19 commit 1f7e8fc

2 files changed

Lines changed: 77 additions & 54 deletions

File tree

src/alsa_mixer.rs

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
1+
use color_eyre::eyre::{self, Context, eyre};
12
use librespot_playback::mixer::{Mixer, MixerConfig};
23
use log::error;
3-
use std::error::Error;
4+
use std::sync::{Arc, Mutex, MutexGuard};
45

5-
#[derive(Clone)]
66
pub struct AlsaMixer {
7-
pub device: String,
8-
pub mixer: String,
9-
pub linear_scaling: bool,
7+
pub mixer: Arc<Mutex<alsa::Mixer>>,
8+
pub config: MixerConfig,
109
}
1110

1211
impl AlsaMixer {
13-
fn set_volume_with_err(&self, volume: u16) -> Result<(), Box<dyn Error>> {
14-
let mixer = alsa::mixer::Mixer::new(&self.device, false)?;
15-
16-
let selem_id = alsa::mixer::SelemId::new(&self.mixer, 0);
17-
let elem = mixer.find_selem(&selem_id).ok_or_else(|| {
18-
format!(
19-
"Couldn't find selem with name '{}'.",
20-
selem_id.get_name().unwrap_or("unnamed")
12+
fn get_selem<'a>(
13+
&'a self,
14+
lock: &'a MutexGuard<'a, alsa::Mixer>,
15+
) -> eyre::Result<alsa::mixer::Selem<'a>> {
16+
let selem_id = alsa::mixer::SelemId::new(&self.config.control, self.config.index);
17+
let selem = lock.find_selem(&selem_id).ok_or_else(|| {
18+
eyre!(
19+
"No control with name '{}' in alsa device '{}'",
20+
self.config.control,
21+
self.config.device,
2122
)
2223
})?;
23-
24+
Ok(selem)
25+
}
26+
fn set_volume_with_err(&self, volume: u16) -> eyre::Result<()> {
27+
let lock = self.mixer.lock().expect("lock shouldn't be poisoned");
28+
let elem = self.get_selem(&lock)?;
2429
let (min, max) = elem.get_playback_volume_range();
2530

2631
let volume_steps = (max - min) as f64;
27-
let normalised_volume = if self.linear_scaling {
32+
let normalised_volume = if matches!(
33+
self.config.volume_ctrl,
34+
librespot_playback::config::VolumeCtrl::Linear
35+
) {
2836
(((volume as f64) / (u16::MAX as f64)) * volume_steps) as i64 + min
2937
} else {
3038
((volume as f64 + 1.0).log((u16::MAX as f64) + 1.0) * volume_steps).floor() as i64 + min
@@ -33,39 +41,41 @@ impl AlsaMixer {
3341
elem.set_playback_volume_all(normalised_volume)?;
3442
Ok(())
3543
}
44+
45+
fn get_volume_with_err(&self) -> eyre::Result<u16> {
46+
let lock = self.mixer.lock().expect("lock shouldn't be poisoned");
47+
let elem = self.get_selem(&lock)?;
48+
let (min, max) = elem.get_playback_volume_range();
49+
let volume_steps = (max - min) as f64;
50+
let vol = elem.get_playback_volume(alsa::mixer::SelemChannelId::mono())?;
51+
let normalized_volume = if matches!(
52+
self.config.volume_ctrl,
53+
librespot_playback::config::VolumeCtrl::Linear
54+
) {
55+
((vol - min) as f64 * u16::MAX as f64 / volume_steps).floor() as u16
56+
} else {
57+
((u16::MAX as f64 + 1.0).powf(((vol - min) as f64) / volume_steps) - 1.0).floor() as u16
58+
};
59+
Ok(normalized_volume)
60+
}
3661
}
3762

3863
impl Mixer for AlsaMixer {
39-
fn open(_: MixerConfig) -> Result<AlsaMixer, librespot_core::Error> {
64+
fn open(config: MixerConfig) -> Result<AlsaMixer, librespot_core::Error> {
65+
let mixer = alsa::Mixer::new(&config.device, false)
66+
.wrap_err("failed to open mixer")
67+
.map_err(librespot_core::Error::invalid_argument)?;
4068
Ok(AlsaMixer {
41-
device: "default".to_string(),
42-
mixer: "Master".to_string(),
43-
linear_scaling: false,
69+
mixer: Arc::new(Mutex::new(mixer)),
70+
config,
4471
})
4572
}
4673

4774
fn volume(&self) -> u16 {
48-
let selem_id = alsa::mixer::SelemId::new(&self.mixer, 0);
49-
let mixer = alsa::mixer::Mixer::new(&self.device, false).ok();
50-
let vol = mixer
51-
.as_ref()
52-
.and_then(|mixer| mixer.find_selem(&selem_id))
53-
.and_then(|elem| {
54-
let (min, max) = elem.get_playback_volume_range();
55-
elem.get_playback_volume(alsa::mixer::SelemChannelId::mono())
56-
.ok()
57-
.map(|volume| {
58-
(((volume - min) as f64 / (max - min) as f64) * (u16::MAX as f64)).floor()
59-
as u16
60-
})
61-
});
62-
match vol {
63-
Some(vol) => vol,
64-
None => {
65-
error!(
66-
"Couldn't read volume from alsa device with name \"{}\".",
67-
self.device
68-
);
75+
match self.get_volume_with_err() {
76+
Ok(vol) => vol,
77+
Err(err) => {
78+
error!("failed to get volume from alsa device: {err:?}");
6979
0
7080
}
7181
}
@@ -74,7 +84,7 @@ impl Mixer for AlsaMixer {
7484
fn set_volume(&self, volume: u16) {
7585
match self.set_volume_with_err(volume) {
7686
Ok(_) => (),
77-
Err(e) => error!("Couldn't set volume: {:?}", e),
87+
Err(err) => error!("Couldn't set volume of alsa device: {err:?}"),
7888
}
7989
}
8090
}

src/setup.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,35 @@ pub(crate) fn initial_state(
2525
}
2626
#[cfg(feature = "alsa_backend")]
2727
config::VolumeController::Alsa | config::VolumeController::AlsaLinear => {
28-
let audio_device = config.audio_device.clone();
29-
let control_device = config.alsa_config.control.clone();
30-
let mixer = config.alsa_config.mixer.clone();
28+
let device = config
29+
.alsa_config
30+
.mixer
31+
.as_deref()
32+
.or(config.audio_device.as_deref())
33+
.unwrap_or("default")
34+
.to_string();
35+
let control = config
36+
.alsa_config
37+
.control
38+
.as_deref()
39+
.unwrap_or("Master")
40+
.to_string();
3141
info!("Using alsa volume controller.");
32-
let linear = matches!(
42+
use librespot_playback::config::VolumeCtrl;
43+
let volume_ctrl = if matches!(
3344
config.volume_controller,
3445
config::VolumeController::AlsaLinear
35-
);
36-
Arc::new(alsa_mixer::AlsaMixer {
37-
device: control_device
38-
.clone()
39-
.or_else(|| audio_device.clone())
40-
.unwrap_or_else(|| "default".to_string()),
41-
mixer: mixer.clone().unwrap_or_else(|| "Master".to_string()),
42-
linear_scaling: linear,
43-
})
46+
) {
47+
VolumeCtrl::Linear
48+
} else {
49+
VolumeCtrl::Log(0.0) /* this value is ignored */
50+
};
51+
Arc::new(alsa_mixer::AlsaMixer::open(MixerConfig {
52+
device,
53+
control,
54+
index: 0,
55+
volume_ctrl,
56+
})?)
4457
}
4558
_ => {
4659
info!("Using software volume controller.");

0 commit comments

Comments
 (0)