blob: 353402264dba95e8414c946fcefccd7bbf75af3f [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.
use std::fmt;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use audio_streams::SampleFormat;
use cros_alsa::{Card, IntControl, SwitchControl};
use libcras::{CrasClient, CrasNodeType};
use sys_util::{error, info};
use crate::{
datastore::Datastore,
error::{Error, Result},
settings::AmpCalibSettings,
vpd::VPD,
};
const CALI_ERROR_UPPER_LIMIT: f32 = 0.3;
const CALI_ERROR_LOWER_LIMIT: f32 = 0.03;
const FRAMES_PER_BUFFER: usize = 256;
const FRAME_RATE: u32 = 48000;
const NUM_CHANNELS: usize = 2;
const FORMAT: SampleFormat = SampleFormat::S16LE;
const DURATION_MS: u32 = 1000;
const WARM_UP_DURATION_MS: u32 = 300;
/// Amp volume mode emulation used by set_volume().
#[derive(PartialEq)]
pub enum VolumeMode {
/// Low mode protects the speaker by limiting its output volume if the
/// calibration has not been completed successfully.
Low,
/// High mode removes the speaker output volume limitation after
/// having successfully completed the calibration.
High,
}
/// It implements the amplifier boot time calibration flow.
pub struct AmpCalibration<'a> {
card: &'a mut Card,
setting: AmpCalibSettings,
}
impl<'a> fmt::Debug for AmpCalibration<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AmpCalibration")
.field("snd_card_id", &self.card.name())
.field("amp_calib", &self.setting)
.finish()
}
}
impl<'a> AmpCalibration<'a> {
/// Creates an `AmpCalibration`.
/// # Arguments
///
/// * `card` - `&Card`.
/// * `setting` - `AmpCalibSettings`.
///
/// # Results
///
/// * `AmpCalibration` - It implements the amplifier boot time calibration flow.
///
/// # Errors
///
/// * If `Card` creation from sound card name fails.
pub fn new(card: &mut Card, setting: AmpCalibSettings) -> Result<AmpCalibration> {
let amp = AmpCalibration { card, setting };
Ok(amp)
}
/// Sets card volume control to given VolumeMode.
pub fn set_volume(&mut self, mode: VolumeMode) -> Result<()> {
match mode {
VolumeMode::High => self
.card
.control_by_name::<IntControl>(&self.setting.amp.volume_ctrl)?
.set(self.setting.amp.volume_high_limit)?,
VolumeMode::Low => self
.card
.control_by_name::<IntControl>(&self.setting.amp.volume_ctrl)?
.set(self.setting.amp.volume_low_limit)?,
}
Ok(())
}
/// The implementation of max98390d boot time calibration logic.
///
/// The boot time calibration logic includes the following steps:
/// * Gets results from `do_calibration`.
/// * Decides whether the new calibration result should replace the stored value.
/// * Applies a good calibration value.
pub fn run(&mut self) -> Result<()> {
let vpd = VPD::from_file(&self.setting.rdc_vpd, &self.setting.temp_vpd)?;
let (rdc_cali, temp_cali) = self.do_calibration()?;
let datastore = match Datastore::from_file(self.card.name(), &self.setting.calib_file) {
Ok(sci_calib) => Some(sci_calib),
Err(e) => {
info!("failure in Datastore::from_file: {}", e);
None
}
};
// Given that rdc_cali is the inverse of hardware real_rdc, the result of `rdc_diff`
// equals to transforming `rdc`s to `real_rdc`s and calculating the relative difference
// from the `real_rdc`s.
let rdc_diff = |x: i32, x_ref: i32| (x - x_ref).abs() as f32 / x as f32;
let diff: f32 = match datastore {
None => rdc_diff(rdc_cali, vpd.dsm_calib_r0),
Some(d) => match d {
Datastore::UseVPD => rdc_diff(rdc_cali, vpd.dsm_calib_r0),
Datastore::DSM { rdc, .. } => rdc_diff(rdc_cali, rdc),
},
};
if !self.validate_temperature(temp_cali) {
info!("invalid temperature: {}.", temp_cali);
return match datastore {
None => Err(Error::InvalidTemperature(temp_cali)),
Some(d) => self.apply_datastore(d),
};
}
if diff > CALI_ERROR_UPPER_LIMIT {
return Err(Error::LargeCalibrationDiff(rdc_cali, temp_cali));
} else if diff < CALI_ERROR_LOWER_LIMIT {
match datastore {
None => Datastore::UseVPD.save(self.card.name(), &self.setting.calib_file)?,
Some(d) => self.apply_datastore(d)?,
}
} else {
info!("apply boot time calibration values.");
self.card
.control_by_name::<IntControl>(&self.setting.amp.rdc_ctrl)?
.set(rdc_cali)?;
self.card
.control_by_name::<IntControl>(&self.setting.amp.temp_ctrl)?
.set(temp_cali)?;
Datastore::DSM {
rdc: rdc_cali,
ambient_temp: temp_cali,
}
.save(self.card.name(), &self.setting.calib_file)?;
}
Ok(())
}
fn apply_datastore(&mut self, d: Datastore) -> Result<()> {
info!("apply datastore values.");
match d {
Datastore::UseVPD => Ok(()),
Datastore::DSM { rdc, ambient_temp } => {
self.card
.control_by_name::<IntControl>(&self.setting.amp.rdc_ctrl)?
.set(rdc)?;
self.card
.control_by_name::<IntControl>(&self.setting.amp.temp_ctrl)?
.set(ambient_temp)?;
Ok(())
}
}
}
fn validate_temperature(&self, temp: i32) -> bool {
temp < self.setting.amp.temp_upper_limit && temp > self.setting.amp.temp_lower_limit
}
/// 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 another thread plays zeros to the speakers.
fn do_calibration(&mut self) -> Result<(i32, i32)> {
// The playback worker uses `playback_started` to notify the main thread that playback
// of zeros has started.
let playback_started = Arc::new((Mutex::new(false), Condvar::new()));
// Shares `calib_finished` to the playback worker and uses it to notify the worker when
// the calibration is finished.
let calib_finished = Arc::new(AtomicBool::new(false));
let handle =
AmpCalibration::run_play_zero_worker(playback_started.clone(), calib_finished.clone())?;
// Waits until zero playback starts or timeout.
let mut timeout = Duration::from_millis(1000);
let (lock, cvar) = &*playback_started;
let mut started = lock.lock()?;
while !*started {
let start_time = Instant::now();
started = cvar.wait_timeout(started, timeout)?.0;
if *started {
break;
} else {
let elapsed = start_time.elapsed();
if elapsed > timeout {
return Err(Error::StartPlaybackTimeout);
} else {
// Spurious wakes. Decrements the sleep duration by the amount slept.
timeout -= start_time.elapsed();
}
}
}
// Playback of zeros is started, and the main thread can start the calibration.
self.card
.control_by_name::<SwitchControl>(&self.setting.amp.calib_ctrl)?
.on()?;
let rdc = self
.card
.control_by_name::<IntControl>(&self.setting.amp.rdc_ctrl)?
.get()?;
let temp = self
.card
.control_by_name::<IntControl>(&self.setting.amp.temp_ctrl)?
.get()?;
self.card
.control_by_name::<SwitchControl>(&self.setting.amp.calib_ctrl)?
.off()?;
// Notifies the play_zero_worker that the calibration is finished.
calib_finished.store(true, Ordering::Relaxed);
// If play_zero_worker has error during the calibration, returns an error to keep the volume
// low to protect the speaker.
match handle.join() {
Ok(res) => {
if let Err(e) = res {
error!("run_play_zero_worker has error: {}", e);
return Err(e);
}
}
Err(e) => {
error!("run_play_zero_worker panics: {:?}", e);
return Err(Error::WorkerPanics);
}
}
Ok((rdc, temp))
}
// Creates a thread to play zeros to the internal speakers.
fn run_play_zero_worker(
playback_started: Arc<(Mutex<bool>, Condvar)>,
calib_finished: Arc<AtomicBool>,
) -> Result<JoinHandle<Result<()>>> {
let mut cras_client = CrasClient::new().map_err(Error::CrasClientFailed)?;
// TODO(b/155007305): Implement cras_client.wait_node_change and use it here.
let node = cras_client
.output_nodes()
.find(|node| node.node_type == CrasNodeType::CRAS_NODE_TYPE_INTERNAL_SPEAKER)
.ok_or(Error::InternalSpeakerNotFound)?;
let handle = thread::spawn(move || -> Result<()> {
let local_buffer = [0u8; FRAMES_PER_BUFFER * NUM_CHANNELS * 2];
let iterations = (FRAME_RATE * DURATION_MS) / FRAMES_PER_BUFFER as u32 / 1000;
let warm_up_iterations =
(FRAME_RATE * WARM_UP_DURATION_MS) / FRAMES_PER_BUFFER as u32 / 1000;
let (_control, mut stream) = cras_client
.new_pinned_playback_stream(
node.iodev_index,
NUM_CHANNELS,
FORMAT,
FRAME_RATE,
FRAMES_PER_BUFFER,
)
.map_err(|e| Error::NewPlayStreamFailed(e))?;
// Plays zeros for at most DURATION_MS.
for i in 0..iterations {
if calib_finished.load(Ordering::Relaxed) {
break;
}
let mut buffer = stream
.next_playback_buffer()
.map_err(|e| Error::NextPlaybackBufferFailed(e))?;
let _write_frames = buffer.write(&local_buffer).map_err(Error::PlaybackFailed)?;
// Notifies the main thread to start the calibration.
// The mute playing time need to be longer than WARM_UP_DURATION_MS to get rdc properly.
if i == warm_up_iterations {
let (lock, cvar) = &*playback_started;
let mut started = lock.lock()?;
*started = true;
cvar.notify_one();
}
// The playback_started lock is unlocked here when `started` goes out of scope.
}
// Returns an error if the calibration is not finished before playback stops.
if !calib_finished.load(Ordering::Relaxed) {
return Err(Error::CalibrationTimeout);
}
Ok(())
});
Ok(handle)
}
/// Skips max98390d boot time calibration when the speaker may be hot.
///
/// If datastore exists, applies the stored value and sets volume to high.
/// If datastore does not exist, sets volume to low.
pub fn hot_speaker_workflow(&mut self) -> Result<()> {
if let Ok(sci_calib) = Datastore::from_file(self.card.name(), &self.setting.calib_file) {
self.apply_datastore(sci_calib)?;
self.set_volume(VolumeMode::High)?;
return Ok(());
}
info!("no datastore, set volume low");
self.set_volume(VolumeMode::Low)
}
}