diff options
Diffstat (limited to 'sound_card_init/dsm/src')
-rw-r--r-- | sound_card_init/dsm/src/datastore.rs | 72 | ||||
-rw-r--r-- | sound_card_init/dsm/src/error.rs | 128 | ||||
-rw-r--r-- | sound_card_init/dsm/src/lib.rs | 335 | ||||
-rw-r--r-- | sound_card_init/dsm/src/utils.rs | 82 | ||||
-rw-r--r-- | sound_card_init/dsm/src/vpd.rs | 41 | ||||
-rw-r--r-- | sound_card_init/dsm/src/zero_player.rs | 209 |
6 files changed, 867 insertions, 0 deletions
diff --git a/sound_card_init/dsm/src/datastore.rs b/sound_card_init/dsm/src/datastore.rs new file mode 100644 index 00000000..f0180cc2 --- /dev/null +++ b/sound_card_init/dsm/src/datastore.rs @@ -0,0 +1,72 @@ +// 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::fs::{remove_file, File}; +use std::io::{prelude::*, BufReader, BufWriter}; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use sys_util::info; + +use crate::error::{Error, Result}; + +/// `Datastore`, which stores and reads calibration values in yaml format. +#[derive(Debug, Deserialize, Serialize, Copy, Clone)] +pub enum Datastore { + /// Indicates using values in VPD. + UseVPD, + DSM { + rdc: i32, + temp: i32, + }, +} + +impl Datastore { + /// The dir of datastore. + pub const DATASTORE_DIR: &'static str = "/var/lib/sound_card_init"; + + /// Creates a `Datastore` and initializes its fields from the datastore file. + pub fn from_file(snd_card: &str, channel: usize) -> Result<Datastore> { + let path = Self::path(snd_card, channel); + let reader = + BufReader::new(File::open(&path).map_err(|e| Error::FileIOFailed(path.to_owned(), e))?); + let datastore: Datastore = + serde_yaml::from_reader(reader).map_err(|e| Error::SerdeError(path.to_owned(), e))?; + Ok(datastore) + } + + /// Saves a `Datastore` to file. + pub fn save(&self, snd_card: &str, channel: usize) -> Result<()> { + let path = Self::path(snd_card, channel); + + let mut writer = BufWriter::new( + File::create(&path).map_err(|e| Error::FileIOFailed(path.to_owned(), e))?, + ); + writer + .write( + serde_yaml::to_string(self) + .map_err(|e| Error::SerdeError(path.to_owned(), e))? + .as_bytes(), + ) + .map_err(|e| Error::FileIOFailed(path.to_owned(), e))?; + writer + .flush() + .map_err(|e| Error::FileIOFailed(path.to_owned(), e))?; + info!("update Datastore {}: {:?}", path.to_string_lossy(), self); + Ok(()) + } + + /// Deletes the datastore file. + pub fn delete(snd_card: &str, channel: usize) -> Result<()> { + let path = Self::path(snd_card, channel); + remove_file(&path).map_err(|e| Error::FileIOFailed(path.to_owned(), e))?; + info!("datastore: {:#?} is deleted.", path); + Ok(()) + } + + fn path(snd_card: &str, channel: usize) -> PathBuf { + PathBuf::from(Self::DATASTORE_DIR) + .join(snd_card) + .join(format!("calib_{}", channel)) + } +} diff --git a/sound_card_init/dsm/src/error.rs b/sound_card_init/dsm/src/error.rs new file mode 100644 index 00000000..4b6e8dc2 --- /dev/null +++ b/sound_card_init/dsm/src/error.rs @@ -0,0 +1,128 @@ +// 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::any::Any; +use std::error; +use std::fmt; +use std::io; +use std::num::ParseIntError; +use std::path::PathBuf; +use std::sync::PoisonError; +use std::time; + +use remain::sorted; + +use crate::CalibData; + +pub type Result<T> = std::result::Result<T, Error>; + +#[sorted] +#[derive(Debug)] +pub enum Error { + AlsaCardError(cros_alsa::CardError), + AlsaControlError(cros_alsa::ControlError), + AlsaControlTLVError(cros_alsa::ControlTLVError), + CalibrationTimeout, + CrasClientFailed(libcras::Error), + DeserializationFailed(String, serde_yaml::Error), + DSMParamUpdateFailed(cros_alsa::ControlTLVError), + FileIOFailed(PathBuf, io::Error), + InternalSpeakerNotFound, + InvalidDatastore, + InvalidDSMParam, + InvalidShutDownTime, + InvalidTemperature(f32), + LargeCalibrationDiff(CalibData), + MissingDSMParam, + MutexPoisonError, + NewPlayStreamFailed(libcras::BoxError), + NextPlaybackBufferFailed(libcras::BoxError), + PlaybackFailed(io::Error), + SerdeError(PathBuf, serde_yaml::Error), + StartPlaybackTimeout, + SystemTimeError(time::SystemTimeError), + UnsupportedSoundCard(String), + VPDParseFailed(String, ParseIntError), + WorkerPanics(Box<dyn Any + Send + 'static>), + ZeroPlayerIsNotRunning, + ZeroPlayerIsRunning, +} + +impl PartialEq for Error { + // Implement eq for more Error when needed. + fn eq(&self, other: &Error) -> bool { + match (self, other) { + (Error::InvalidDSMParam, Error::InvalidDSMParam) => true, + _ => false, + } + } +} + +impl From<cros_alsa::CardError> for Error { + fn from(err: cros_alsa::CardError) -> Error { + Error::AlsaCardError(err) + } +} + +impl From<cros_alsa::ControlError> for Error { + fn from(err: cros_alsa::ControlError) -> Error { + Error::AlsaControlError(err) + } +} + +impl From<cros_alsa::ControlTLVError> for Error { + fn from(err: cros_alsa::ControlTLVError) -> Error { + Error::AlsaControlTLVError(err) + } +} + +impl<T> From<PoisonError<T>> for Error { + fn from(_: PoisonError<T>) -> Error { + Error::MutexPoisonError + } +} + +impl error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Error::*; + match self { + AlsaCardError(e) => write!(f, "AlsaCardError: {}", e), + AlsaControlError(e) => write!(f, "AlsaControlError: {}", e), + AlsaControlTLVError(e) => write!(f, "AlsaControlTLVError: {}", e), + CalibrationTimeout => write!(f, "calibration is not finished in time"), + DSMParamUpdateFailed(e) => write!(f, "failed to update DsmParam, err: {}", e), + CrasClientFailed(e) => write!(f, "failed to create cras client: {}", e), + DeserializationFailed(file_path, e) => { + write!(f, "failed to parse {}: {}", file_path, e) + } + FileIOFailed(file_path, e) => write!(f, "{:#?}: {}", file_path, e), + InvalidShutDownTime => write!(f, "invalid shutdown time"), + InternalSpeakerNotFound => write!(f, "internal speaker is not found in cras"), + InvalidTemperature(temp) => write!( + f, + "invalid calibration temperature: {}, and there is no datastore", + temp + ), + InvalidDatastore => write!(f, "invalid datastore format"), + InvalidDSMParam => write!(f, "invalid dsm param from kcontrol"), + LargeCalibrationDiff(calib) => { + write!(f, "calibration difference is too large, calib: {:?}", calib) + } + MissingDSMParam => write!(f, "missing dsm_param.bin"), + MutexPoisonError => write!(f, "mutex is poisoned"), + NewPlayStreamFailed(e) => write!(f, "{}", e), + NextPlaybackBufferFailed(e) => write!(f, "{}", e), + PlaybackFailed(e) => write!(f, "{}", e), + SerdeError(file_path, e) => write!(f, "{:?}: {}", file_path, e), + StartPlaybackTimeout => write!(f, "playback is not started in time"), + SystemTimeError(e) => write!(f, "{}", e), + UnsupportedSoundCard(name) => write!(f, "unsupported sound card: {}", name), + VPDParseFailed(file_path, e) => write!(f, "failed to parse vpd {}: {}", file_path, e), + WorkerPanics(e) => write!(f, "run_play_zero_worker panics: {:#?}", e), + ZeroPlayerIsNotRunning => write!(f, "zero player is not running"), + ZeroPlayerIsRunning => write!(f, "zero player is running"), + } + } +} diff --git a/sound_card_init/dsm/src/lib.rs b/sound_card_init/dsm/src/lib.rs new file mode 100644 index 00000000..0b3ec64c --- /dev/null +++ b/sound_card_init/dsm/src/lib.rs @@ -0,0 +1,335 @@ +// 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. +//! `dsm` crate implements the required initialization workflows for smart amps. + +mod datastore; +mod error; +pub mod utils; +mod vpd; +mod zero_player; + +use std::{ + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use libcras::{CrasClient, CrasNodeType}; +use sys_util::{error, info}; + +use crate::datastore::Datastore; +pub use crate::error::{Error, Result}; +use crate::utils::{run_time, shutdown_time}; +use crate::vpd::VPD; +pub use crate::zero_player::ZeroPlayer; + +#[derive(Debug, Clone, Copy)] +/// `CalibData` represents the calibration data. +pub struct CalibData { + /// The DC resistance of the speaker is DSM unit. + pub rdc: i32, + /// The ambient temperature in celsius unit at which the rdc is measured. + pub temp: f32, +} + +/// `TempConverter` converts the temperature value between celsius and unit in VPD::dsm_calib_temp. +pub struct TempConverter { + vpd_to_celsius: fn(i32) -> f32, + celsius_to_vpd: fn(f32) -> i32, +} + +impl Default for TempConverter { + fn default() -> Self { + let vpd_to_celsius = |x: i32| x as f32; + let celsius_to_vpd = |x: f32| x.round() as i32; + Self { + vpd_to_celsius, + celsius_to_vpd, + } + } +} + +impl TempConverter { + /// Creates a `TempConverter` + /// + /// # Arguments + /// + /// * `vpd_to_celsius` - function to convert VPD::dsm_calib_temp to celsius unit` + /// * `celsius_to_vpd` - function to convert celsius unit to VPD::dsm_calib_temp` + /// # Results + /// + /// * `TempConverter` - it converts the temperature value between celsius and unit in VPD::dsm_calib_temp. + pub fn new(vpd_to_celsius: fn(i32) -> f32, celsius_to_vpd: fn(f32) -> i32) -> Self { + Self { + vpd_to_celsius, + celsius_to_vpd, + } + } +} + +/// `SpeakerStatus` are the possible return results of +/// DSM::check_speaker_over_heated_workflow. +pub enum SpeakerStatus { + ///`SpeakerStatus::Cold` means the speakers are not overheated and the Amp can + /// trigger the boot time calibration. + Cold, + /// `SpeakerStatus::Hot(Vec<CalibData>)` means the speakers may be too hot for calibration. + /// The boot time calibration should be skipped and the Amp should use the previous + /// calibration values returned by the enum. + Hot(Vec<CalibData>), +} + +/// `DSM`, which implements the required initialization workflows for smart amps. +pub struct DSM { + snd_card: String, + num_channels: usize, + temp_converter: TempConverter, + rdc_to_ohm: fn(i32) -> f32, + temp_upper_limit: f32, + temp_lower_limit: f32, +} + +impl DSM { + const SPEAKER_COOL_DOWN_TIME: Duration = Duration::from_secs(180); + const CALI_ERROR_UPPER_LIMIT: f32 = 0.3; + const CALI_ERROR_LOWER_LIMIT: f32 = 0.03; + + /// Creates a `DSM` + /// + /// # Arguments + /// + /// * `snd_card` - `sound card name`. + /// * `num_channels` - `number of channels`. + /// * `rdc_to_ohm` - `fn(rdc: i32) -> f32 to convert the CalibData::rdc to ohm unit`. + /// * `temp_upper_limit` - the high limit of the valid ambient temperature in dsm unit. + /// * `temp_lower_limit` - the low limit of the valid ambient temperature in dsm unit. + /// + /// # Results + /// + /// * `DSM` - It implements the required initialization workflows for smart amps. + pub fn new( + snd_card: &str, + num_channels: usize, + rdc_to_ohm: fn(i32) -> f32, + temp_upper_limit: f32, + temp_lower_limit: f32, + ) -> Self { + Self { + snd_card: snd_card.to_owned(), + num_channels, + rdc_to_ohm, + temp_converter: TempConverter::default(), + temp_upper_limit, + temp_lower_limit, + } + } + + /// Sets self.temp_converter to the given temp_converter. + /// + /// # Arguments + /// + /// * `temp_converter` - the convert function to use. + pub fn set_temp_converter(&mut self, temp_converter: TempConverter) { + self.temp_converter = temp_converter; + } + + /// Checks whether the speakers are overheated or not according to the previous shutdown time. + /// The boot time calibration should be skipped when the speakers may be too hot + /// and the Amp should use the previous calibration value returned by the + /// SpeakerStatus::Hot(Vec<CalibData>). + /// + /// # Results + /// + /// * `SpeakerStatus::Cold` - which means the speakers are not overheated and the Amp can + /// trigger the boot time calibration. + /// * `SpeakerStatus::Hot(Vec<CalibData>)` - when the speakers may be too hot. The boot + /// time calibration should be skipped and the Amp should use the previous calibration values + /// returned by the enum. + /// + /// # Errors + /// + /// * The speakers are overheated and there are no previous calibration values stored. + /// * Cannot determine whether the speakers are overheated as previous shutdown time record is + /// invalid. + pub fn check_speaker_over_heated_workflow(&self) -> Result<SpeakerStatus> { + if self.is_first_boot() { + return Ok(SpeakerStatus::Cold); + } + match self.is_speaker_over_heated() { + Ok(overheated) => { + if overheated { + let calib: Vec<CalibData> = (0..self.num_channels) + .map(|ch| -> Result<CalibData> { self.get_previous_calibration_value(ch) }) + .collect::<Result<Vec<CalibData>>>()?; + info!("the speakers are hot, the boot time calibration should be skipped"); + return Ok(SpeakerStatus::Hot(calib)); + } + Ok(SpeakerStatus::Cold) + } + Err(err) => { + // We cannot assume the speakers are not replaced or not overheated + // when the shutdown time file is invalid; therefore we can not use the datastore + // value anymore and we can not trigger boot time calibration. + for ch in 0..self.num_channels { + if let Err(e) = Datastore::delete(&self.snd_card, ch) { + error!("error delete datastore: {}", e); + } + } + Err(err) + } + } + } + + /// Decides a good calibration value and updates the stored value according to the following + /// logic: + /// * Returns the previous value if the ambient temperature is not within a valid range. + /// * Returns Error::LargeCalibrationDiff if rdc difference is larger than + /// `CALI_ERROR_UPPER_LIMIT`. + /// * Returns the previous value if the rdc difference is smaller than `CALI_ERROR_LOWER_LIMIT`. + /// * Returns the boot time calibration value and updates the datastore value if the rdc. + /// difference is between `CALI_ERROR_UPPER_LIMIT` and `CALI_ERROR_LOWER_LIMIT`. + /// + /// # Arguments + /// + /// * `card` - `&Card`. + /// * `channel` - `channel number`. + /// * `calib_data` - `boot time calibrated data`. + /// + /// # Results + /// + /// * `CalibData` - the calibration data to be applied according to the deciding logic. + /// + /// # Errors + /// + /// * VPD does not exist. + /// * rdc difference is larger than `CALI_ERROR_UPPER_LIMIT`. + /// * Failed to update Datastore. + pub fn decide_calibration_value_workflow( + &self, + channel: usize, + calib_data: CalibData, + ) -> Result<CalibData> { + if calib_data.temp < self.temp_lower_limit || calib_data.temp > self.temp_upper_limit { + info!("invalid temperature: {}.", calib_data.temp); + return self + .get_previous_calibration_value(channel) + .map_err(|_| Error::InvalidTemperature(calib_data.temp)); + } + let (datastore_exist, previous_calib) = match self.get_previous_calibration_value(channel) { + Ok(previous_calib) => (true, previous_calib), + Err(e) => { + info!("{}, use vpd as previous calibration value", e); + (false, self.get_vpd_calibration_value(channel)?) + } + }; + + let diff = { + let calib_rdc_ohm = (self.rdc_to_ohm)(calib_data.rdc); + let previous_rdc_ohm = (self.rdc_to_ohm)(previous_calib.rdc); + (calib_rdc_ohm - previous_rdc_ohm) / previous_rdc_ohm + }; + if diff > Self::CALI_ERROR_UPPER_LIMIT { + Err(Error::LargeCalibrationDiff(calib_data)) + } else if diff < Self::CALI_ERROR_LOWER_LIMIT { + if !datastore_exist { + Datastore::UseVPD.save(&self.snd_card, channel)?; + } + Ok(previous_calib) + } else { + Datastore::DSM { + rdc: calib_data.rdc, + temp: (self.temp_converter.celsius_to_vpd)(calib_data.temp), + } + .save(&self.snd_card, channel)?; + Ok(calib_data) + } + } + + /// Gets the calibration values from vpd. + /// + /// # Results + /// + /// * `Vec<CalibData>` - the calibration values in vpd. + /// + /// # Errors + /// + /// * Failed to read vpd. + pub fn get_all_vpd_calibration_value(&self) -> Result<Vec<CalibData>> { + (0..self.num_channels) + .map(|ch| self.get_vpd_calibration_value(ch)) + .collect::<Result<Vec<_>>>() + } + + /// Blocks until the internal speakers are ready. + /// + /// # Errors + /// + /// * Failed to wait the internal speakers to be ready. + pub fn wait_for_speakers_ready(&self) -> Result<()> { + let find_speaker = || -> Result<()> { + let cras_client = CrasClient::new().map_err(Error::CrasClientFailed)?; + let _node = cras_client + .output_nodes() + .find(|node| node.node_type == CrasNodeType::CRAS_NODE_TYPE_INTERNAL_SPEAKER) + .ok_or(Error::InternalSpeakerNotFound)?; + Ok(()) + }; + // TODO(b/155007305): Implement cras_client.wait_node_change and use it here. + const RETRY: usize = 3; + const RETRY_INTERVAL: Duration = Duration::from_millis(500); + for _ in 0..RETRY { + match find_speaker() { + Ok(_) => return Ok(()), + Err(e) => error!("retry on finding speaker: {}", e), + }; + thread::sleep(RETRY_INTERVAL); + } + Err(Error::InternalSpeakerNotFound) + } + + fn is_first_boot(&self) -> bool { + !run_time::exists(&self.snd_card) + } + + // If (Current time - the latest CRAS shutdown time) < cool_down_time, we assume that + // the speakers may be overheated. + fn is_speaker_over_heated(&self) -> Result<bool> { + let last_run = run_time::from_file(&self.snd_card)?; + let last_shutdown = shutdown_time::from_file()?; + if last_shutdown < last_run { + return Err(Error::InvalidShutDownTime); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(Error::SystemTimeError)?; + + let elapsed = now + .checked_sub(last_shutdown) + .ok_or(Error::InvalidShutDownTime)?; + + if elapsed < Self::SPEAKER_COOL_DOWN_TIME { + return Ok(true); + } + Ok(false) + } + + fn get_previous_calibration_value(&self, ch: usize) -> Result<CalibData> { + let sci_calib = Datastore::from_file(&self.snd_card, ch)?; + match sci_calib { + Datastore::UseVPD => self.get_vpd_calibration_value(ch), + Datastore::DSM { rdc, temp } => Ok(CalibData { + rdc, + temp: (self.temp_converter.vpd_to_celsius)(temp), + }), + } + } + + fn get_vpd_calibration_value(&self, channel: usize) -> Result<CalibData> { + let vpd = VPD::new(channel)?; + Ok(CalibData { + rdc: vpd.dsm_calib_r0, + temp: (self.temp_converter.vpd_to_celsius)(vpd.dsm_calib_temp), + }) + } +} diff --git a/sound_card_init/dsm/src/utils.rs b/sound_card_init/dsm/src/utils.rs new file mode 100644 index 00000000..64f6c972 --- /dev/null +++ b/sound_card_init/dsm/src/utils.rs @@ -0,0 +1,82 @@ +// 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. +//! It contains common utils shared within sound_card_init. +#![deny(missing_docs)] + +use std::fs::File; +use std::io::{prelude::*, BufReader, BufWriter}; +use std::path::PathBuf; +use std::time::Duration; + +use crate::datastore::Datastore; +use crate::error::{Error, Result}; + +fn duration_from_file(path: &PathBuf) -> Result<Duration> { + let reader = + BufReader::new(File::open(&path).map_err(|e| Error::FileIOFailed(path.clone(), e))?); + serde_yaml::from_reader(reader).map_err(|e| Error::SerdeError(path.clone(), e)) +} + +/// The utils to parse CRAS shutdown time file. +pub mod shutdown_time { + use super::*; + // The path of CRAS shutdown time file. + const SHUTDOWN_TIME_FILE: &str = "/var/lib/cras/stop"; + + /// Reads the unix time from CRAS shutdown time file. + pub fn from_file() -> Result<Duration> { + duration_from_file(&PathBuf::from(SHUTDOWN_TIME_FILE)) + } +} + +/// The utils to create and parse sound_card_init run time file. +pub mod run_time { + use std::time::SystemTime; + + use super::*; + // The filename of sound_card_init run time file. + const RUN_TIME_FILE: &str = "run"; + + /// Returns the sound_card_init run time file existence. + pub fn exists(snd_card: &str) -> bool { + run_time_file(snd_card).exists() + } + + /// Reads the unix time from sound_card_init run time file. + pub fn from_file(snd_card: &str) -> Result<Duration> { + duration_from_file(&run_time_file(snd_card)) + } + + /// Saves the current unix time to sound_card_init run time file. + pub fn now_to_file(snd_card: &str) -> Result<()> { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(t) => to_file(snd_card, t), + Err(e) => Err(Error::SystemTimeError(e)), + } + } + + /// Saves the unix time to sound_card_init run time file. + pub fn to_file(snd_card: &str, duration: Duration) -> Result<()> { + let path = run_time_file(snd_card); + let mut writer = + BufWriter::new(File::create(&path).map_err(|e| Error::FileIOFailed(path.clone(), e))?); + writer + .write_all( + serde_yaml::to_string(&duration) + .map_err(|e| Error::SerdeError(path.clone(), e))? + .as_bytes(), + ) + .map_err(|e| Error::FileIOFailed(path.clone(), e))?; + writer + .flush() + .map_err(|e| Error::FileIOFailed(path.clone(), e))?; + Ok(()) + } + + fn run_time_file(snd_card: &str) -> PathBuf { + PathBuf::from(Datastore::DATASTORE_DIR) + .join(snd_card) + .join(RUN_TIME_FILE) + } +} diff --git a/sound_card_init/dsm/src/vpd.rs b/sound_card_init/dsm/src/vpd.rs new file mode 100644 index 00000000..b00864cc --- /dev/null +++ b/sound_card_init/dsm/src/vpd.rs @@ -0,0 +1,41 @@ +// 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::fs::File; +use std::io::prelude::*; +use std::io::BufReader; +use std::path::PathBuf; + +use crate::error::{Error, Result}; + +const VPD_DIR: &str = "/sys/firmware/vpd/ro/vpdfile"; + +/// `VPD`, which represents the amplifier factory calibration values. +#[derive(Default, Debug)] +pub struct VPD { + pub dsm_calib_r0: i32, + pub dsm_calib_temp: i32, +} + +impl VPD { + /// Creates a `VPD` and initializes its fields from VPD_DIR/dsm_calib_r0_{channel}. + /// # Arguments + /// + /// * `channel` - channel number. + pub fn new(channel: usize) -> Result<VPD> { + let mut vpd: VPD = Default::default(); + vpd.dsm_calib_r0 = read_vpd_files(&format!("dsm_calib_r0_{}", channel))?; + vpd.dsm_calib_temp = read_vpd_files(&format!("dsm_calib_temp_{}", channel))?; + Ok(vpd) + } +} + +fn read_vpd_files(file: &str) -> Result<i32> { + let path = PathBuf::from(VPD_DIR).with_file_name(file); + let io_err = |e| Error::FileIOFailed(path.to_owned(), e); + let mut reader = BufReader::new(File::open(&path).map_err(io_err)?); + let mut line = String::new(); + reader.read_line(&mut line).map_err(io_err)?; + line.parse::<i32>() + .map_err(|e| Error::VPDParseFailed(path.to_string_lossy().to_string(), e)) +} diff --git a/sound_card_init/dsm/src/zero_player.rs b/sound_card_init/dsm/src/zero_player.rs new file mode 100644 index 00000000..441f7ffa --- /dev/null +++ b/sound_card_init/dsm/src/zero_player.rs @@ -0,0 +1,209 @@ +// 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::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; + +use audio_streams::SampleFormat; +use libcras::{CrasClient, CrasNodeType}; +use sys_util::error; + +use crate::error::{Error, Result}; + +/// `ZeroPlayer` provides the functionality to play zeros sample in the background thread. +#[derive(Default)] +pub struct ZeroPlayer { + thread_info: Option<PlayZeroWorkerInfo>, +} + +impl Drop for ZeroPlayer { + fn drop(&mut self) { + if self.thread_info.is_some() { + if let Err(e) = self.stop() { + error!("{}", e); + } + } + } +} + +impl ZeroPlayer { + /// It takes about 400 ms to get CRAS_NODE_TYPE_INTERNAL_SPEAKER during the boot time. + const TIMEOUT: Duration = Duration::from_millis(1000); + + /// Returns whether the ZeroPlayer is running. + pub fn running(&self) -> bool { + self.thread_info.is_some() + } + + /// Starts to play zeros for at most `max_playback_time`. + /// This function blocks and returns until playback has started for `min_playback_time`. + /// This function must be called when self.running() returns false. + /// + /// # Arguments + /// + /// * `min_playback_time` - It blocks and returns until playback has started for + /// `min_playback_time`. + /// + /// # Errors + /// + /// * If it's called when the `ZeroPlayer` is already running. + /// * Failed to find internal speakers. + /// * Failed to start the background thread. + pub fn start(&mut self, min_playback_time: Duration) -> Result<()> { + if self.running() { + return Err(Error::ZeroPlayerIsRunning); + } + self.thread_info = Some(PlayZeroWorkerInfo::new(min_playback_time)); + if let Some(thread_info) = &mut self.thread_info { + // Block until playback of zeros has started for min_playback_time or timeout. + let (lock, cvar) = &*(thread_info.ready); + let result = cvar.wait_timeout_while( + lock.lock()?, + min_playback_time + ZeroPlayer::TIMEOUT, + |&mut is_ready| !is_ready, + )?; + if result.1.timed_out() { + return Err(Error::StartPlaybackTimeout); + } + } + Ok(()) + } + + /// Stops playing zeros in the background thread. + /// This function must be called when self.running() returns true. + /// + /// # Errors + /// + /// * If it's called again when the `ZeroPlayer` is not running. + /// * Failed to play zeros to internal speakers via CRAS client. + /// * Failed to join the background thread. + pub fn stop(&mut self) -> Result<()> { + match self.thread_info.take() { + Some(mut thread_info) => Ok(thread_info.destroy()?), + None => Err(Error::ZeroPlayerIsNotRunning), + } + } +} + +// Audio thread book-keeping data +struct PlayZeroWorkerInfo { + thread: Option<JoinHandle<Result<()>>>, + // Uses `thread_run` to notify the background thread to stop. + thread_run: Arc<AtomicBool>, + // The background thread uses `ready` to notify the main thread that playback + // of zeros has started for min_playback_time. + ready: Arc<(Mutex<bool>, Condvar)>, +} + +impl Drop for PlayZeroWorkerInfo { + fn drop(&mut self) { + if let Err(e) = self.destroy() { + error!("{}", e); + } + } +} + +impl PlayZeroWorkerInfo { + // Spawns the PlayZeroWorker. + fn new(min_playback_time: Duration) -> Self { + let thread_run = Arc::new(AtomicBool::new(false)); + let ready = Arc::new((Mutex::new(false), Condvar::new())); + let mut worker = PlayZeroWorker::new(min_playback_time, thread_run.clone(), ready.clone()); + Self { + thread: Some(thread::spawn(move || -> Result<()> { + worker.run()?; + Ok(()) + })), + thread_run, + ready, + } + } + + // Joins the PlayZeroWorker. + fn destroy(&mut self) -> Result<()> { + self.thread_run.store(false, Ordering::Relaxed); + if let Some(handle) = self.thread.take() { + let res = handle.join().map_err(Error::WorkerPanics)?; + return match res { + Err(e) => Err(e), + Ok(_) => Ok(()), + }; + } + Ok(()) + } +} + +struct PlayZeroWorker { + min_playback_time: Duration, + // Uses `thread_run` to notify the background thread to stop. + thread_run: Arc<AtomicBool>, + // The background thread uses `ready` to notify the main thread that playback + // of zeros has started for min_playback_time. + ready: Arc<(Mutex<bool>, Condvar)>, +} + +impl PlayZeroWorker { + const FRAMES_PER_BUFFER: usize = 256; + const FRAME_RATE: u32 = 48000; + const NUM_CHANNELS: usize = 2; + const FORMAT: SampleFormat = SampleFormat::S16LE; + + fn new( + min_playback_time: Duration, + thread_run: Arc<AtomicBool>, + ready: Arc<(Mutex<bool>, Condvar)>, + ) -> Self { + Self { + min_playback_time, + thread_run, + ready, + } + } + + fn run(&mut self) -> 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 local_buffer = + vec![0u8; Self::FRAMES_PER_BUFFER * Self::NUM_CHANNELS * Self::FORMAT.sample_bytes()]; + let min_playback_iterations = (Self::FRAME_RATE + * self.min_playback_time.as_millis() as u32) + / Self::FRAMES_PER_BUFFER as u32 + / 1000; + let (_control, mut stream) = cras_client + .new_pinned_playback_stream( + node.iodev_index, + Self::NUM_CHANNELS, + Self::FORMAT, + Self::FRAME_RATE, + Self::FRAMES_PER_BUFFER, + ) + .map_err(|e| Error::NewPlayStreamFailed(e))?; + + let mut iter = 0; + self.thread_run.store(true, Ordering::Relaxed); + while self.thread_run.load(Ordering::Relaxed) { + 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 that playback of zeros has started for min_playback_time. + if iter == min_playback_iterations { + let (lock, cvar) = &*self.ready; + let mut is_ready = lock.lock()?; + *is_ready = true; + cvar.notify_one(); + } + iter += 1; + } + Ok(()) + } +} |