blob: 601165ec60c3b4c8205948fce77c37b085b1c145 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! `max98390d` module implements the required initialization workflows for sound
//! cards that use max98390d smart amp.
//! It currently supports boot time calibration for max98390d.
#![deny(missing_docs)]
mod settings;
use std::time::Duration;
use std::{fs, path::Path};
use cros_alsa::{Card, IntControl, SwitchControl};
use dsm::{CalibData, Error, Result, SpeakerStatus, TempConverter, ZeroPlayer, DSM};
use crate::Amp;
use settings::{AmpCalibSettings, DeviceSettings};
/// Amp volume mode emulation used by set_volume().
#[derive(PartialEq, Clone, Copy)]
enum VolumeMode {
/// Low mode protects the speaker by limiting its output volume if the
/// calibration has not been completed successfully.
Low = 138,
/// High mode removes the speaker output volume limitation after
/// having successfully completed the calibration.
High = 148,
}
/// It implements the Max98390 functions of boot time calibration.
#[derive(Debug)]
pub struct Max98390 {
card: Card,
setting: AmpCalibSettings,
}
impl Amp for Max98390 {
/// Performs max98390d boot time calibration.
///
/// # Errors
///
/// If the amplifier fails to complete the calibration.
fn boot_time_calibration(&mut self) -> Result<()> {
if !Path::new(&self.setting.dsm_param).exists() {
return Err(Error::MissingDSMParam);
}
let mut dsm = DSM::new(
&self.card.name(),
self.setting.num_channels(),
Self::rdc_to_ohm,
Self::TEMP_UPPER_LIMIT_CELSIUS,
Self::TEMP_LOWER_LIMIT_CELSIUS,
);
dsm.set_temp_converter(TempConverter::new(
Self::dsm_unit_to_celsius,
Self::celsius_to_dsm_unit,
));
self.set_volume(VolumeMode::Low)?;
let calib = match dsm.check_speaker_over_heated_workflow()? {
SpeakerStatus::Hot(previous_calib) => previous_calib,
SpeakerStatus::Cold => self
.do_calibration()?
.iter()
.enumerate()
.map(|(ch, calib_data)| dsm.decide_calibration_value_workflow(ch, *calib_data))
.collect::<Result<Vec<_>>>()?,
};
self.apply_calibration_value(calib)?;
self.set_volume(VolumeMode::High)?;
Ok(())
}
}
impl Max98390 {
const TEMP_UPPER_LIMIT_CELSIUS: f32 = 40.0;
const TEMP_LOWER_LIMIT_CELSIUS: f32 = 0.0;
const RDC_CALIB_WARM_UP_TIME: Duration = Duration::from_millis(300);
/// Creates an `Max98390`.
/// # Arguments
///
/// * `card_name` - card name.
/// * `config_path` - config file path.
///
/// # Results
///
/// * `Max98390` - It implements the Max98390 functions of boot time calibration.
///
/// # Errors
///
/// * If `Card` creation from sound card name fails.
pub fn new(card_name: &str, config_path: &Path) -> Result<Self> {
let conf = fs::read_to_string(config_path)
.map_err(|e| Error::FileIOFailed(config_path.to_path_buf(), e))?;
let settings = DeviceSettings::from_yaml_str(&conf)?;
Ok(Self {
card: Card::new(card_name)?,
setting: settings.amp_calibrations,
})
}
/// Sets the card volume control to given VolumeMode.
fn set_volume(&mut self, mode: VolumeMode) -> Result<()> {
for control in &self.setting.controls {
self.card
.control_by_name::<IntControl>(&control.volume_ctrl)?
.set(mode as i32)?;
}
Ok(())
}
/// Applies the calibration value to the amp.
fn apply_calibration_value(&mut self, calib: Vec<CalibData>) -> Result<()> {
for (ch, &CalibData { rdc, temp }) in calib.iter().enumerate() {
self.card
.control_by_name::<IntControl>(&self.setting.controls[ch].rdc_ctrl)?
.set(rdc)?;
self.card
.control_by_name::<IntControl>(&self.setting.controls[ch].temp_ctrl)?
.set(Self::celsius_to_dsm_unit(temp))?;
}
Ok(())
}
/// Triggers the amplifier calibration and reads the calibrated rdc and ambient_temp value
/// from the mixer control.
/// To get accurate calibration results, the main thread calibrates the amplifier while
/// the `zero_player` starts another thread to play zeros to the speakers.
fn do_calibration(&mut self) -> Result<Vec<CalibData>> {
let mut zero_player: ZeroPlayer = Default::default();
zero_player.start(Self::RDC_CALIB_WARM_UP_TIME)?;
// Playback of zeros is started for Self::RDC_CALIB_WARM_UP_TIME, and the main thread
// can start the calibration.
let setting = &self.setting;
let card = &mut self.card;
let calib = setting
.controls
.iter()
.map(|control| {
card.control_by_name::<SwitchControl>(&control.calib_ctrl)?
.on()?;
let rdc = card
.control_by_name::<IntControl>(&control.rdc_ctrl)?
.get()?;
let temp = card
.control_by_name::<IntControl>(&control.temp_ctrl)?
.get()?;
card.control_by_name::<SwitchControl>(&control.calib_ctrl)?
.off()?;
Ok(CalibData {
rdc,
temp: Self::dsm_unit_to_celsius(temp),
})
})
.collect::<Result<Vec<CalibData>>>()?;
zero_player.stop()?;
Ok(calib)
}
/// Converts the ambient temperature from celsius to the DSM unit.
#[inline]
fn celsius_to_dsm_unit(celsius: f32) -> i32 {
(celsius * ((1 << 12) as f32) / 100.0) as i32
}
/// Converts the ambient temperature from DSM unit to celsius.
#[inline]
fn dsm_unit_to_celsius(temp: i32) -> f32 {
temp as f32 * 100.0 / (1 << 12) as f32
}
/// Converts the calibrated value to real DC resistance in ohm unit.
#[inline]
fn rdc_to_ohm(x: i32) -> f32 {
3.66 * (1 << 20) as f32 / x as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn celsius_to_dsm_unit() {
assert_eq!(
Max98390::celsius_to_dsm_unit(Max98390::TEMP_UPPER_LIMIT_CELSIUS),
1638
);
assert_eq!(
Max98390::celsius_to_dsm_unit(Max98390::TEMP_LOWER_LIMIT_CELSIUS),
0
);
}
#[test]
fn dsm_unit_to_celsius() {
assert_eq!(
Max98390::dsm_unit_to_celsius(1638).round(),
Max98390::TEMP_UPPER_LIMIT_CELSIUS
);
assert_eq!(
Max98390::dsm_unit_to_celsius(0),
Max98390::TEMP_LOWER_LIMIT_CELSIUS
);
}
#[test]
fn rdc_to_ohm() {
assert_eq!(Max98390::rdc_to_ohm(1123160), 3.416956);
assert_eq!(Max98390::rdc_to_ohm(1157049), 3.3168762);
}
}