blob: 59e9ec26b2144d39b7bfb50ed875a3ab5863a755 [file] [log] [blame]
// Copyright 2019 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::error;
use std::fmt;
use std::path::PathBuf;
use audio_streams::SampleFormat;
use getopts::{self, Matches, Options};
#[derive(Debug)]
pub enum Error {
GetOpts(getopts::Fail),
InvalidArgument(String, String, String),
InvalidFiletype(String),
MissingArgument(String),
MissingCommand,
MissingFilename,
UnknownCommand(String),
}
impl error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Error::*;
match self {
GetOpts(e) => write!(f, "Getopts Error: {}", e),
InvalidArgument(flag, value, error_msg) => {
write!(f, "Invalid {} argument '{}': {}", flag, value, error_msg)
}
InvalidFiletype(extension) => write!(
f,
"Invalid file extension '{}'. Supported types are 'wav' and 'raw'",
extension
),
MissingArgument(subcommand) => write!(f, "Missing argument for {}", subcommand),
MissingCommand => write!(f, "A command must be provided"),
MissingFilename => write!(f, "A file name must be provided"),
UnknownCommand(s) => write!(f, "Unknown command '{}'", s),
}
}
}
type Result<T> = std::result::Result<T, Error>;
/// The different types of commands that can be given to cras_tests.
/// Any options for those commands are passed as parameters to the enum values.
#[derive(Debug, PartialEq)]
pub enum Command {
Capture(AudioOptions),
Playback(AudioOptions),
Control(ControlCommand),
}
impl Command {
pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Option<Self>> {
let program_name = args.get(0).map(|s| s.as_ref()).unwrap_or("cras_tests");
let remaining_args = args.get(2..).unwrap_or(&[]);
match args.get(1).map(|s| s.as_ref()) {
None => {
show_usage(program_name);
Err(Error::MissingCommand)
}
Some("help") => {
show_usage(program_name);
Ok(None)
}
Some("capture") => Ok(
AudioOptions::parse(program_name, "capture", remaining_args)?.map(Command::Capture),
),
Some("playback") => Ok(
AudioOptions::parse(program_name, "playback", remaining_args)?
.map(Command::Playback),
),
Some("control") => {
Ok(ControlCommand::parse(program_name, remaining_args)?.map(Command::Control))
}
Some(s) => {
show_usage(program_name);
Err(Error::UnknownCommand(s.to_string()))
}
}
}
}
#[derive(Debug, PartialEq)]
pub enum FileType {
Raw,
Wav,
}
impl fmt::Display for FileType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FileType::Raw => write!(f, "raw data"),
FileType::Wav => write!(f, "WAVE"),
}
}
}
fn show_usage(program_name: &str) {
eprintln!("Usage: {} [command] <command args>", program_name);
eprintln!("\nCommands:\n");
eprintln!("capture - Capture to a file from CRAS");
eprintln!("playback - Playback to CRAS from a file");
eprintln!("control - Get and set server settings");
eprintln!("\nhelp - Print help message");
}
fn show_audio_command_usage(program_name: &str, command: &str, opts: &Options) {
let brief = format!("Usage: {} {} [options] [filename]", program_name, command);
eprint!("{}", opts.usage(&brief));
}
/// The possible command line options that can be passed to the 'playback' and
/// 'capture' commands. Optional values will be `Some(_)` only if a value was
/// explicitly provided by the user.
///
/// This struct will be passed to `playback()` and `capture()`.
#[derive(Debug, PartialEq)]
pub enum LoopbackType {
PreDsp,
PostDsp,
}
#[derive(Debug, PartialEq)]
pub struct AudioOptions {
pub file_name: PathBuf,
pub loopback_type: Option<LoopbackType>,
pub file_type: FileType,
pub buffer_size: Option<usize>,
pub num_channels: Option<usize>,
pub format: Option<SampleFormat>,
pub frame_rate: Option<u32>,
}
fn get_u32_param(matches: &Matches, option_name: &str) -> Result<Option<u32>> {
matches.opt_get::<u32>(option_name).map_err(|e| {
let argument = matches.opt_str(option_name).unwrap_or_default();
Error::InvalidArgument(option_name.to_string(), argument, e.to_string())
})
}
fn get_usize_param(matches: &Matches, option_name: &str) -> Result<Option<usize>> {
matches.opt_get::<usize>(option_name).map_err(|e| {
let argument = matches.opt_str(option_name).unwrap_or_default();
Error::InvalidArgument(option_name.to_string(), argument, e.to_string())
})
}
impl AudioOptions {
fn parse<T: AsRef<str>>(
program_name: &str,
command_name: &str,
args: &[T],
) -> Result<Option<Self>> {
let mut opts = Options::new();
opts.optopt("b", "buffer_size", "Buffer size in frames", "SIZE")
.optopt("c", "channels", "Number of channels", "NUM")
.optopt(
"f",
"format",
"Sample format (U8, S16_LE, S24_LE, or S32_LE)",
"FORMAT",
)
.optopt("r", "rate", "Audio frame rate (Hz)", "RATE")
.optflag("h", "help", "Print help message");
if command_name == "capture" {
opts.optopt(
"",
"loopback",
"Capture from loopback device ('pre_dsp' or 'post_dsp')",
"DEVICE",
);
}
let args = args.iter().map(|s| s.as_ref());
let matches = match opts.parse(args) {
Ok(m) => m,
Err(e) => {
show_audio_command_usage(program_name, command_name, &opts);
return Err(Error::GetOpts(e));
}
};
if matches.opt_present("h") {
show_audio_command_usage(program_name, command_name, &opts);
return Ok(None);
}
let loopback_type = if matches.opt_defined("loopback") {
match matches.opt_str("loopback").as_deref() {
Some("pre_dsp") => Some(LoopbackType::PreDsp),
Some("post_dsp") => Some(LoopbackType::PostDsp),
Some(s) => {
return Err(Error::InvalidArgument(
"loopback".to_string(),
s.to_string(),
"Loopback type must be 'pre_dsp' or 'post_dsp'".to_string(),
))
}
None => None,
}
} else {
None
};
let file_name = match matches.free.get(0) {
None => {
show_audio_command_usage(program_name, command_name, &opts);
return Err(Error::MissingFilename);
}
Some(file_name) => PathBuf::from(file_name),
};
let extension = file_name
.extension()
.map(|s| s.to_string_lossy().into_owned());
let file_type = match extension.as_deref() {
Some("wav") | Some("wave") => FileType::Wav,
Some("raw") | None => FileType::Raw,
Some(extension) => return Err(Error::InvalidFiletype(extension.to_string())),
};
let buffer_size = get_usize_param(&matches, "buffer_size")?;
let num_channels = get_usize_param(&matches, "channels")?;
let frame_rate = get_u32_param(&matches, "rate")?;
let format = match matches.opt_str("format").as_deref() {
Some("U8") => Some(SampleFormat::U8),
Some("S16_LE") => Some(SampleFormat::S16LE),
Some("S24_LE") => Some(SampleFormat::S24LE),
Some("S32_LE") => Some(SampleFormat::S32LE),
Some(s) => {
show_audio_command_usage(program_name, command_name, &opts);
return Err(Error::InvalidArgument(
"format".to_string(),
s.to_string(),
"Format must be 'U8', 'S16_LE', 'S24_LE', or 'S32_LE'".to_string(),
));
}
None => None,
};
Ok(Some(AudioOptions {
loopback_type,
file_name,
file_type,
buffer_size,
num_channels,
format,
frame_rate,
}))
}
}
fn show_control_command_usage(program_name: &str) {
eprintln!("Usage: {} control [command] <command args>", program_name);
eprintln!("");
eprintln!("Commands:");
let commands = [
("help", "", "Print help message"),
("", "", ""),
("get_volume", "", "Get the system volume (0 - 100)"),
(
"set_volume",
"VOLUME",
"Set the system volume to VOLUME (0 - 100)",
),
("get_mute", "", "Get the system mute state (true or false)"),
(
"set_mute",
"MUTE",
"Set the system mute state to MUTE (true or false)",
),
("", "", ""),
("list_output_devices", "", "Print list of output devices"),
("list_input_devices", "", "Print list of input devices"),
("list_output_nodes", "", "Print list of output nodes"),
("list_input_nodes", "", "Print list of input nodes"),
(
"dump_audio_debug_info",
"",
"Print stream info, device info, and audio thread log.",
),
];
for command in &commands {
let command_string = format!("{} {}", command.0, command.1);
eprintln!("\t{: <23} {}", command_string, command.2);
}
}
#[derive(Debug, PartialEq)]
pub enum ControlCommand {
GetSystemVolume,
SetSystemVolume(u32),
GetSystemMute,
SetSystemMute(bool),
ListOutputDevices,
ListInputDevices,
ListOutputNodes,
ListInputNodes,
DumpAudioDebugInfo,
}
impl ControlCommand {
fn parse<T: AsRef<str>>(program_name: &str, args: &[T]) -> Result<Option<Self>> {
let mut args = args.iter().map(|s| s.as_ref());
match args.next() {
Some("help") => {
show_control_command_usage(program_name);
Ok(None)
}
Some("get_volume") => Ok(Some(ControlCommand::GetSystemVolume)),
Some("set_volume") => {
let volume_str = args
.next()
.ok_or_else(|| Error::MissingArgument("set_volume".to_string()))?;
let volume = volume_str.parse::<u32>().map_err(|e| {
Error::InvalidArgument(
"set_volume".to_string(),
volume_str.to_string(),
e.to_string(),
)
})?;
Ok(Some(ControlCommand::SetSystemVolume(volume)))
}
Some("get_mute") => Ok(Some(ControlCommand::GetSystemMute)),
Some("set_mute") => {
let mute_str = args
.next()
.ok_or_else(|| Error::MissingArgument("set_mute".to_string()))?;
let mute = mute_str.parse::<bool>().map_err(|e| {
Error::InvalidArgument(
"set_mute".to_string(),
mute_str.to_string(),
e.to_string(),
)
})?;
Ok(Some(ControlCommand::SetSystemMute(mute)))
}
Some("list_output_devices") => Ok(Some(ControlCommand::ListOutputDevices)),
Some("list_input_devices") => Ok(Some(ControlCommand::ListInputDevices)),
Some("list_output_nodes") => Ok(Some(ControlCommand::ListOutputNodes)),
Some("list_input_nodes") => Ok(Some(ControlCommand::ListInputNodes)),
Some("dump_audio_debug_info") => Ok(Some(ControlCommand::DumpAudioDebugInfo)),
Some(s) => {
show_control_command_usage(program_name);
Err(Error::UnknownCommand(s.to_string()))
}
None => {
show_control_command_usage(program_name);
Err(Error::MissingCommand)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_command() {
let command = Command::parse(&["cras_tests", "playback", "output.wav"])
.unwrap()
.unwrap();
assert_eq!(
command,
Command::Playback(AudioOptions {
file_name: PathBuf::from("output.wav"),
loopback_type: None,
file_type: FileType::Wav,
frame_rate: None,
num_channels: None,
format: None,
buffer_size: None,
})
);
let command = Command::parse(&["cras_tests", "capture", "input.raw"])
.unwrap()
.unwrap();
assert_eq!(
command,
Command::Capture(AudioOptions {
file_name: PathBuf::from("input.raw"),
loopback_type: None,
file_type: FileType::Raw,
frame_rate: None,
num_channels: None,
format: None,
buffer_size: None,
})
);
let command = Command::parse(&[
"cras_tests",
"playback",
"-r",
"44100",
"output.wave",
"-c",
"2",
])
.unwrap()
.unwrap();
assert_eq!(
command,
Command::Playback(AudioOptions {
file_name: PathBuf::from("output.wave"),
loopback_type: None,
file_type: FileType::Wav,
frame_rate: Some(44100),
num_channels: Some(2),
format: None,
buffer_size: None,
})
);
let command =
Command::parse(&["cras_tests", "playback", "-r", "44100", "output", "-c", "2"])
.unwrap()
.unwrap();
assert_eq!(
command,
Command::Playback(AudioOptions {
file_name: PathBuf::from("output"),
loopback_type: None,
file_type: FileType::Raw,
frame_rate: Some(44100),
num_channels: Some(2),
format: None,
buffer_size: None,
})
);
assert!(Command::parse(&["cras_tests"]).is_err());
assert!(Command::parse(&["cras_tests", "capture"]).is_err());
assert!(Command::parse(&["cras_tests", "capture", "input.mp3"]).is_err());
assert!(Command::parse(&["cras_tests", "capture", "input.ogg"]).is_err());
assert!(Command::parse(&["cras_tests", "capture", "input.flac"]).is_err());
assert!(Command::parse(&["cras_tests", "playback"]).is_err());
assert!(Command::parse(&["cras_tests", "loopback"]).is_err());
assert!(Command::parse(&["cras_tests", "loopback", "file.ogg"]).is_err());
assert!(Command::parse(&["cras_tests", "filename.wav"]).is_err());
assert!(Command::parse(&["cras_tests", "filename.wav", "capture"]).is_err());
assert!(Command::parse(&["cras_tests", "help"]).is_ok());
assert!(Command::parse(&[
"cras_tests",
"-c",
"2",
"playback",
"output.wav",
"-r",
"44100"
])
.is_err());
}
}