From 3dfad7f788c7f2999f2e6813a972758b3df16f97 Mon Sep 17 00:00:00 2001 From: ashthespy Date: Mon, 17 Sep 2018 17:28:54 +0200 Subject: [PATCH 1/4] Implement mapped volume for alsa mixer --- playback/src/mixer/alsamixer.rs | 113 ++++++++++++++++++++++++++------ playback/src/mixer/mod.rs | 2 + src/main.rs | 6 ++ 3 files changed, 102 insertions(+), 19 deletions(-) diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index a906c2e..5aba111 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -1,44 +1,119 @@ use super::AudioFilter; use super::{Mixer, MixerConfig}; +use std; use std::error::Error; use alsa; +#[derive(Clone)] +struct AlsaMixerVolumeParams { + min: i64, + max: i64, + range: f64, + min_db: alsa::mixer::MilliBel, + max_db: alsa::mixer::MilliBel, +} + #[derive(Clone)] pub struct AlsaMixer { config: MixerConfig, + params: AlsaMixerVolumeParams, } impl AlsaMixer { - fn map_volume(&self, set_volume: Option) -> Result<(u16), Box> { + fn pvol(&self, vol: T, min: T, max: T) -> f64 + where + T: std::ops::Sub + Copy, + f64: std::convert::From<::Output>, + { + f64::from(vol - min) / f64::from(max - min) + } + + fn init_mixer(mut config: MixerConfig) -> Result> { + let mixer = alsa::mixer::Mixer::new(&config.card, false)?; + let sid = alsa::mixer::SelemId::new(&config.mixer, config.index); + + let selem = mixer.find_selem(&sid).expect("Couldn't find SelemId"); + let (min, max) = selem.get_playback_volume_range(); + let (min_db, max_db) = selem.get_playback_db_range(); + + info!( + "Alsa min: {} ({:?}[dB]) -- max: {} ({:?}[dB])", + min, min_db, max, max_db + ); + + if config.mapped_volume && (max_db - min_db <= alsa::mixer::MilliBel(24)) { + warn!( + "Switching to linear volume mapping, control range: {:?}", + max_db - min_db + ); + config.mapped_volume = false; + } else { + info!("Using Alsa mapped volume: dB range: {:?}", max_db - min_db); + } + + Ok(AlsaMixer { + config: config, + params: AlsaMixerVolumeParams { + min: min, + max: max, + range: (max - min) as f64, + min_db: min_db, + max_db: max_db, + }, + }) + } + + fn map_volume(&self, set_volume: Option) -> Result<(u16), Box> { let mixer = alsa::mixer::Mixer::new(&self.config.card, false)?; let sid = alsa::mixer::SelemId::new(&*self.config.mixer, self.config.index); - let selem = mixer.find_selem(&sid).expect( - format!( - "Couldn't find simple mixer control for {}", - self.config.mixer - ) - .as_str(), - ); - let (min, max) = selem.get_playback_volume_range(); - let range = (max - min) as f64; + let selem = mixer.find_selem(&sid).expect("Couldn't find SelemId"); + let cur_vol = selem + .get_playback_volume(alsa::mixer::SelemChannelId::mono()) + .expect("Couldn't get current volume"); + let cur_vol_db = selem + .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) + .expect("Couldn't get current volume"); let new_vol: u16; + debug!("Current alsa volume: {}[i64] {:?}", cur_vol, cur_vol_db); if let Some(vol) = set_volume { - let alsa_volume: i64 = ((vol as f64 / 0xFFFF as f64) * range) as i64 + min; - debug!("Mapping volume {:?} ->> alsa {:?}", vol, alsa_volume); + let alsa_volume = if self.config.mapped_volume { + ((self.pvol(vol, 0x0000, 0xFFFF)).log10() * 6000.0).floor() as i64 + self.params.max + } else { + ((vol as f64 / 0xFFFF as f64) * self.params.range) as i64 + self.params.min + }; + debug!( + "Maping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [i64]", + self.pvol(vol, 0x0000, 0xFFFF) * 100.0, + vol, + self.pvol( + alsa_volume as f64, + self.params.min as f64, + self.params.max as f64 + ) * 100.0, + alsa_volume + ); selem .set_playback_volume_all(alsa_volume) .expect("Couldn't set alsa volume"); - new_vol = vol; + new_vol = vol; // Meh } else { - let cur_vol = selem - .get_playback_volume(alsa::mixer::SelemChannelId::mono()) - .expect("Couldn't get current volume"); - new_vol = (((cur_vol - min) as f64 / range) * 0xFFFF as f64) as u16; - debug!("Mapping volume {:?} <<- alsa {:?}", new_vol, cur_vol); + new_vol = + (((cur_vol - self.params.min) as f64 / self.params.range) * 0xFFFF as f64) as u16; + debug!( + "Maping volume [{:.3}%] {:?} [u16] <<- Alsa [{:.3}%] {:?} [i64]", + self.pvol(new_vol, 0x0000, 0xFFFF), + new_vol, + self.pvol( + cur_vol as f64, + self.params.min as f64, + self.params.max as f64 + ), + cur_vol + ); } Ok(new_vol) @@ -52,7 +127,7 @@ impl Mixer for AlsaMixer { "Setting up new mixer: card:{} mixer:{} index:{}", config.card, config.mixer, config.index ); - AlsaMixer { config: config } + AlsaMixer::init_mixer(config).expect("Error setting up mixer!") } fn start(&self) {} diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 4fc01b5..325c1e1 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -25,6 +25,7 @@ pub struct MixerConfig { pub card: String, pub mixer: String, pub index: u32, + pub mapped_volume: bool, } impl Default for MixerConfig { @@ -33,6 +34,7 @@ impl Default for MixerConfig { card: String::from("default"), mixer: String::from("PCM"), index: 0, + mapped_volume: true, } } } diff --git a/src/main.rs b/src/main.rs index 552a0fb..7803a9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -150,6 +150,11 @@ fn setup(args: &[String]) -> Setup { "Alsa mixer index, Index of the cards mixer. Defaults to 0", "MIXER_INDEX", ) + .optflag( + "", + "mixer-linear-volume", + "Disable alsa's mapped volume scale (cubic). Default false", + ) .optopt( "", "initial-volume", @@ -241,6 +246,7 @@ fn setup(args: &[String]) -> Setup { .opt_str("mixer-index") .map(|index| index.parse::().unwrap()) .unwrap_or(0), + mapped_volume: !matches.opt_present("mixer-linear-volume"), }; let use_audio_cache = !matches.opt_present("disable-audio-cache"); From 527a4ccbe275ed93cb99b62e184dcb4799d600c6 Mon Sep 17 00:00:00 2001 From: ashthespy Date: Sat, 22 Sep 2018 17:56:13 +0200 Subject: [PATCH 2/4] Better `alsamixer` volume mapping for hardware mixers --- playback/src/mixer/alsamixer.rs | 72 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 5aba111..7e1303d 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -36,10 +36,13 @@ impl AlsaMixer { let selem = mixer.find_selem(&sid).expect("Couldn't find SelemId"); let (min, max) = selem.get_playback_volume_range(); let (min_db, max_db) = selem.get_playback_db_range(); + let hw_mix = selem + .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) + .is_ok(); info!( - "Alsa min: {} ({:?}[dB]) -- max: {} ({:?}[dB])", - min, min_db, max, max_db + "Alsa min: {} ({:?}[dB]) -- max: {} ({:?}[dB]) HW: {:?}", + min, min_db, max, max_db, hw_mix ); if config.mapped_volume && (max_db - min_db <= alsa::mixer::MilliBel(24)) { @@ -48,8 +51,8 @@ impl AlsaMixer { max_db - min_db ); config.mapped_volume = false; - } else { - info!("Using Alsa mapped volume: dB range: {:?}", max_db - min_db); + } else if !config.mapped_volume { + info!("Using Alsa linear volume"); } Ok(AlsaMixer { @@ -74,37 +77,56 @@ impl AlsaMixer { .expect("Couldn't get current volume"); let cur_vol_db = selem .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) - .expect("Couldn't get current volume"); + .unwrap_or(alsa::mixer::MilliBel(9999)); let new_vol: u16; - debug!("Current alsa volume: {}[i64] {:?}", cur_vol, cur_vol_db); + info!("Current alsa volume: {}[i64] {:?}", cur_vol, cur_vol_db); if let Some(vol) = set_volume { - let alsa_volume = if self.config.mapped_volume { - ((self.pvol(vol, 0x0000, 0xFFFF)).log10() * 6000.0).floor() as i64 + self.params.max + if self.config.mapped_volume { + info!("Using mapped_volume"); + //TODO Check if min is not mute! + let vol_db = ((self.pvol(vol, 0x0000, 0xFFFF)).log10() * 6000.0).floor() as i64 + + self.params.max; + selem + .set_playback_volume_all(vol_db) + .expect("Couldn't set alsa dB volume"); + info!( + "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [dB]", + self.pvol(vol, 0x0000, 0xFFFF) * 100.0, + vol, + self.pvol( + vol_db as f64, + self.params.min_db.to_db() as f64, + self.params.max_db.to_db() as f64 + ) * 100.0, + vol_db as f64 / 100.0, + ); } else { - ((vol as f64 / 0xFFFF as f64) * self.params.range) as i64 + self.params.min + info!("Using linear vol"); + let alsa_volume = + ((vol as f64 / 0xFFFF as f64) * self.params.range) as i64 + self.params.min; + selem + .set_playback_volume_all(alsa_volume) + .expect("Couldn't set alsa raw volume"); + info!( + "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [i64]", + self.pvol(vol, 0x0000, 0xFFFF) * 100.0, + vol, + self.pvol( + alsa_volume as f64, + self.params.min as f64, + self.params.max as f64 + ) * 100.0, + alsa_volume + ); }; - debug!( - "Maping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [i64]", - self.pvol(vol, 0x0000, 0xFFFF) * 100.0, - vol, - self.pvol( - alsa_volume as f64, - self.params.min as f64, - self.params.max as f64 - ) * 100.0, - alsa_volume - ); - selem - .set_playback_volume_all(alsa_volume) - .expect("Couldn't set alsa volume"); new_vol = vol; // Meh } else { new_vol = (((cur_vol - self.params.min) as f64 / self.params.range) * 0xFFFF as f64) as u16; - debug!( - "Maping volume [{:.3}%] {:?} [u16] <<- Alsa [{:.3}%] {:?} [i64]", + info!( + "Mapping volume [{:.3}%] {:?} [u16] <<- Alsa [{:.3}%] {:?} [i64]", self.pvol(new_vol, 0x0000, 0xFFFF), new_vol, self.pvol( From 46328810cb5028df18658b488ec50f895a07388d Mon Sep 17 00:00:00 2001 From: ashthespy Date: Wed, 4 Mar 2020 15:46:32 +0100 Subject: [PATCH 3/4] Make `alsamixer` less verbose --- playback/src/mixer/alsamixer.rs | 138 ++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 58 deletions(-) diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 7e1303d..f007c54 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -5,6 +5,8 @@ use std::error::Error; use alsa; +const SND_CTL_TLV_DB_GAIN_MUTE: i64 = -9999999; + #[derive(Clone)] struct AlsaMixerVolumeParams { min: i64, @@ -29,11 +31,17 @@ impl AlsaMixer { f64::from(vol - min) / f64::from(max - min) } - fn init_mixer(mut config: MixerConfig) -> Result> { + fn init_mixer(mut config: MixerConfig) -> Result> { let mixer = alsa::mixer::Mixer::new(&config.card, false)?; let sid = alsa::mixer::SelemId::new(&config.mixer, config.index); - let selem = mixer.find_selem(&sid).expect("Couldn't find SelemId"); + let selem = mixer.find_selem(&sid).expect( + format!( + "Couldn't find simple mixer control for {},{}", + &config.mixer, &config.index, + ) + .as_str(), + ); let (min, max) = selem.get_playback_volume_range(); let (min_db, max_db) = selem.get_playback_db_range(); let hw_mix = selem @@ -41,7 +49,7 @@ impl AlsaMixer { .is_ok(); info!( - "Alsa min: {} ({:?}[dB]) -- max: {} ({:?}[dB]) HW: {:?}", + "Alsa Mixer info min: {} ({:?}[dB]) -- max: {} ({:?}[dB]) HW: {:?}", min, min_db, max, max_db, hw_mix ); @@ -55,6 +63,10 @@ impl AlsaMixer { info!("Using Alsa linear volume"); } + if min_db != alsa::mixer::MilliBel(SND_CTL_TLV_DB_GAIN_MUTE) { + debug!("Alsa min-db is not SND_CTL_TLV_DB_GAIN_MUTE!!"); + } + Ok(AlsaMixer { config: config, params: AlsaMixerVolumeParams { @@ -67,75 +79,85 @@ impl AlsaMixer { }) } - fn map_volume(&self, set_volume: Option) -> Result<(u16), Box> { + fn map_volume(&self, set_volume: Option) -> Result> { let mixer = alsa::mixer::Mixer::new(&self.config.card, false)?; let sid = alsa::mixer::SelemId::new(&*self.config.mixer, self.config.index); - let selem = mixer.find_selem(&sid).expect("Couldn't find SelemId"); + let selem = mixer.find_selem(&sid).unwrap(); let cur_vol = selem .get_playback_volume(alsa::mixer::SelemChannelId::mono()) .expect("Couldn't get current volume"); let cur_vol_db = selem .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) - .unwrap_or(alsa::mixer::MilliBel(9999)); + .unwrap_or(alsa::mixer::MilliBel(-SND_CTL_TLV_DB_GAIN_MUTE)); - let new_vol: u16; - info!("Current alsa volume: {}[i64] {:?}", cur_vol, cur_vol_db); + let mut new_vol: u16 = 0; + trace!("Current alsa volume: {}{:?}", cur_vol, cur_vol_db); - if let Some(vol) = set_volume { - if self.config.mapped_volume { - info!("Using mapped_volume"); - //TODO Check if min is not mute! - let vol_db = ((self.pvol(vol, 0x0000, 0xFFFF)).log10() * 6000.0).floor() as i64 - + self.params.max; - selem - .set_playback_volume_all(vol_db) - .expect("Couldn't set alsa dB volume"); - info!( - "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [dB]", - self.pvol(vol, 0x0000, 0xFFFF) * 100.0, - vol, + match set_volume { + Some(vol) => { + if self.config.mapped_volume { + // Cubic mapping ala alsamixer + // https://linux.die.net/man/1/alsamixer + // In alsamixer, the volume is mapped to a value that is more natural for a + // human ear. The mapping is designed so that the position in the interval is + // proportional to the volume as a human ear would perceive it, i.e. the + // position is the cubic root of the linear sample multiplication factor. For + // controls with a small range (24 dB or less), the mapping is linear in the dB + // values so that each step has the same size visually. TODO + // TODO: Check if min is not mute! + let vol_db = (self.pvol(vol, 0x0000, 0xFFFF).log10() * 6000.0).floor() as i64 + + self.params.max_db.0; + selem + .set_playback_db_all(alsa::mixer::MilliBel(vol_db), alsa::Round::Floor) + .expect("Couldn't set alsa dB volume"); + debug!( + "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [dB] - {} [i64]", + self.pvol(vol, 0x0000, 0xFFFF) * 100.0, + vol, + self.pvol( + vol_db as f64, + self.params.min as f64, + self.params.max as f64 + ) * 100.0, + vol_db as f64 / 100.0, + vol_db + ); + } else { + // Linear mapping + let alsa_volume = + ((vol as f64 / 0xFFFF as f64) * self.params.range) as i64 + self.params.min; + selem + .set_playback_volume_all(alsa_volume) + .expect("Couldn't set alsa raw volume"); + debug!( + "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [i64]", + self.pvol(vol, 0x0000, 0xFFFF) * 100.0, + vol, + self.pvol( + alsa_volume as f64, + self.params.min as f64, + self.params.max as f64 + ) * 100.0, + alsa_volume + ); + }; + } + None => { + new_vol = (((cur_vol - self.params.min) as f64 / self.params.range) * 0xFFFF as f64) + as u16; + debug!( + "Mapping volume [{:.3}%] {:?} [u16] <<- Alsa [{:.3}%] {:?} [i64]", + self.pvol(new_vol, 0x0000, 0xFFFF), + new_vol, self.pvol( - vol_db as f64, - self.params.min_db.to_db() as f64, - self.params.max_db.to_db() as f64 - ) * 100.0, - vol_db as f64 / 100.0, - ); - } else { - info!("Using linear vol"); - let alsa_volume = - ((vol as f64 / 0xFFFF as f64) * self.params.range) as i64 + self.params.min; - selem - .set_playback_volume_all(alsa_volume) - .expect("Couldn't set alsa raw volume"); - info!( - "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [i64]", - self.pvol(vol, 0x0000, 0xFFFF) * 100.0, - vol, - self.pvol( - alsa_volume as f64, + cur_vol as f64, self.params.min as f64, self.params.max as f64 - ) * 100.0, - alsa_volume + ), + cur_vol ); - }; - new_vol = vol; // Meh - } else { - new_vol = - (((cur_vol - self.params.min) as f64 / self.params.range) * 0xFFFF as f64) as u16; - info!( - "Mapping volume [{:.3}%] {:?} [u16] <<- Alsa [{:.3}%] {:?} [i64]", - self.pvol(new_vol, 0x0000, 0xFFFF), - new_vol, - self.pvol( - cur_vol as f64, - self.params.min as f64, - self.params.max as f64 - ), - cur_vol - ); + } } Ok(new_vol) From 9e7180feb4600248a507b40d1987bbe1a65507e2 Mon Sep 17 00:00:00 2001 From: ashthespy Date: Fri, 6 Mar 2020 20:56:01 +0100 Subject: [PATCH 4/4] Use mixer's mute switch if possible --- playback/src/mixer/alsamixer.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index f007c54..1e00cc8 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -14,6 +14,7 @@ struct AlsaMixerVolumeParams { range: f64, min_db: alsa::mixer::MilliBel, max_db: alsa::mixer::MilliBel, + has_switch: bool, } #[derive(Clone)] @@ -47,7 +48,10 @@ impl AlsaMixer { let hw_mix = selem .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) .is_ok(); - + let has_switch = selem.has_playback_switch(); + if min_db != alsa::mixer::MilliBel(SND_CTL_TLV_DB_GAIN_MUTE) { + warn!("Alsa min-db is not SND_CTL_TLV_DB_GAIN_MUTE!!"); + } info!( "Alsa Mixer info min: {} ({:?}[dB]) -- max: {} ({:?}[dB]) HW: {:?}", min, min_db, max, max_db, hw_mix @@ -75,6 +79,7 @@ impl AlsaMixer { range: (max - min) as f64, min_db: min_db, max_db: max_db, + has_switch: has_switch, }, }) } @@ -96,6 +101,22 @@ impl AlsaMixer { match set_volume { Some(vol) => { + if self.params.has_switch { + let is_muted = selem + .get_playback_switch(alsa::mixer::SelemChannelId::mono()) + .map(|b| b == 0) + .unwrap_or(false); + if vol == 0 { + debug!("Toggling mute::True"); + selem.set_playback_switch_all(0).expect("Can't switch mute"); + + return Ok(vol); + } else if is_muted { + debug!("Toggling mute::False"); + selem.set_playback_switch_all(1).expect("Can't reset mute"); + } + } + if self.config.mapped_volume { // Cubic mapping ala alsamixer // https://linux.die.net/man/1/alsamixer