blob: 441f7ffa27145f4e16fb7dd8adde291fae8ece38 [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::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(())
}
}