|
@@ -1,44 +1,184 @@
|
|
use super::AudioFilter;
|
|
use super::AudioFilter;
|
|
use super::{Mixer, MixerConfig};
|
|
use super::{Mixer, MixerConfig};
|
|
|
|
+use std;
|
|
use std::error::Error;
|
|
use std::error::Error;
|
|
|
|
|
|
use alsa;
|
|
use alsa;
|
|
|
|
|
|
|
|
+const SND_CTL_TLV_DB_GAIN_MUTE: i64 = -9999999;
|
|
|
|
+
|
|
|
|
+#[derive(Clone)]
|
|
|
|
+struct AlsaMixerVolumeParams {
|
|
|
|
+ min: i64,
|
|
|
|
+ max: i64,
|
|
|
|
+ range: f64,
|
|
|
|
+ min_db: alsa::mixer::MilliBel,
|
|
|
|
+ max_db: alsa::mixer::MilliBel,
|
|
|
|
+ has_switch: bool,
|
|
|
|
+}
|
|
|
|
+
|
|
#[derive(Clone)]
|
|
#[derive(Clone)]
|
|
pub struct AlsaMixer {
|
|
pub struct AlsaMixer {
|
|
config: MixerConfig,
|
|
config: MixerConfig,
|
|
|
|
+ params: AlsaMixerVolumeParams,
|
|
}
|
|
}
|
|
|
|
|
|
impl AlsaMixer {
|
|
impl AlsaMixer {
|
|
- fn map_volume(&self, set_volume: Option<u16>) -> Result<(u16), Box<dyn Error>> {
|
|
|
|
- let mixer = alsa::mixer::Mixer::new(&self.config.card, false)?;
|
|
|
|
- let sid = alsa::mixer::SelemId::new(&*self.config.mixer, self.config.index);
|
|
|
|
|
|
+ fn pvol<T>(&self, vol: T, min: T, max: T) -> f64
|
|
|
|
+ where
|
|
|
|
+ T: std::ops::Sub + Copy,
|
|
|
|
+ f64: std::convert::From<<T as std::ops::Sub>::Output>,
|
|
|
|
+ {
|
|
|
|
+ f64::from(vol - min) / f64::from(max - min)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ fn init_mixer(mut config: MixerConfig) -> Result<AlsaMixer, Box<dyn Error>> {
|
|
|
|
+ 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(
|
|
let selem = mixer.find_selem(&sid).expect(
|
|
format!(
|
|
format!(
|
|
- "Couldn't find simple mixer control for {}",
|
|
|
|
- self.config.mixer
|
|
|
|
|
|
+ "Couldn't find simple mixer control for {},{}",
|
|
|
|
+ &config.mixer, &config.index,
|
|
)
|
|
)
|
|
.as_str(),
|
|
.as_str(),
|
|
);
|
|
);
|
|
let (min, max) = selem.get_playback_volume_range();
|
|
let (min, max) = selem.get_playback_volume_range();
|
|
- let range = (max - min) as f64;
|
|
|
|
-
|
|
|
|
- let new_vol: u16;
|
|
|
|
-
|
|
|
|
- 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);
|
|
|
|
- selem
|
|
|
|
- .set_playback_volume_all(alsa_volume)
|
|
|
|
- .expect("Couldn't set alsa volume");
|
|
|
|
- new_vol = vol;
|
|
|
|
- } 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);
|
|
|
|
|
|
+ let (min_db, max_db) = selem.get_playback_db_range();
|
|
|
|
+ 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
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ 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 if !config.mapped_volume {
|
|
|
|
+ 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 {
|
|
|
|
+ min: min,
|
|
|
|
+ max: max,
|
|
|
|
+ range: (max - min) as f64,
|
|
|
|
+ min_db: min_db,
|
|
|
|
+ max_db: max_db,
|
|
|
|
+ has_switch: has_switch,
|
|
|
|
+ },
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ fn map_volume(&self, set_volume: Option<u16>) -> Result<u16, Box<dyn Error>> {
|
|
|
|
+ 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).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(-SND_CTL_TLV_DB_GAIN_MUTE));
|
|
|
|
+
|
|
|
|
+ let mut new_vol: u16 = 0;
|
|
|
|
+ trace!("Current alsa volume: {}{:?}", cur_vol, cur_vol_db);
|
|
|
|
+
|
|
|
|
+ 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
|
|
|
|
+ // 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(
|
|
|
|
+ cur_vol as f64,
|
|
|
|
+ self.params.min as f64,
|
|
|
|
+ self.params.max as f64
|
|
|
|
+ ),
|
|
|
|
+ cur_vol
|
|
|
|
+ );
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
Ok(new_vol)
|
|
Ok(new_vol)
|
|
@@ -52,7 +192,7 @@ impl Mixer for AlsaMixer {
|
|
"Setting up new mixer: card:{} mixer:{} index:{}",
|
|
"Setting up new mixer: card:{} mixer:{} index:{}",
|
|
config.card, config.mixer, config.index
|
|
config.card, config.mixer, config.index
|
|
);
|
|
);
|
|
- AlsaMixer { config: config }
|
|
|
|
|
|
+ AlsaMixer::init_mixer(config).expect("Error setting up mixer!")
|
|
}
|
|
}
|
|
|
|
|
|
fn start(&self) {}
|
|
fn start(&self) {}
|