1 // Copyright 2019 The Chromium OS Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 use std::error;
5 use std::fmt;
6 use std::path::PathBuf;
7
8 use audio_streams::SampleFormat;
9 use getopts::{self, Matches, Options};
10
11 #[derive(Debug)]
12 pub enum Error {
13 GetOpts(getopts::Fail),
14 InvalidArgument(String, String, String),
15 InvalidFiletype(String),
16 MissingArgument(String),
17 MissingCommand,
18 MissingFilename,
19 UnknownCommand(String),
20 }
21
22 impl error::Error for Error {}
23
24 impl fmt::Display for Error {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result25 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26 use Error::*;
27 match self {
28 GetOpts(e) => write!(f, "Getopts Error: {}", e),
29 InvalidArgument(flag, value, error_msg) => {
30 write!(f, "Invalid {} argument '{}': {}", flag, value, error_msg)
31 }
32 InvalidFiletype(extension) => write!(
33 f,
34 "Invalid file extension '{}'. Supported types are 'wav' and 'raw'",
35 extension
36 ),
37 MissingArgument(subcommand) => write!(f, "Missing argument for {}", subcommand),
38 MissingCommand => write!(f, "A command must be provided"),
39 MissingFilename => write!(f, "A file name must be provided"),
40 UnknownCommand(s) => write!(f, "Unknown command '{}'", s),
41 }
42 }
43 }
44
45 type Result<T> = std::result::Result<T, Error>;
46
47 /// The different types of commands that can be given to cras_tests.
48 /// Any options for those commands are passed as parameters to the enum values.
49 #[derive(Debug, PartialEq)]
50 pub enum Command {
51 Capture(AudioOptions),
52 Playback(AudioOptions),
53 Control(ControlCommand),
54 }
55
56 impl Command {
parse<T: AsRef<str>>(args: &[T]) -> Result<Option<Self>>57 pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Option<Self>> {
58 let program_name = args.get(0).map(|s| s.as_ref()).unwrap_or("cras_tests");
59 let remaining_args = args.get(2..).unwrap_or(&[]);
60 match args.get(1).map(|s| s.as_ref()) {
61 None => {
62 show_usage(program_name);
63 Err(Error::MissingCommand)
64 }
65 Some("help") => {
66 show_usage(program_name);
67 Ok(None)
68 }
69 Some("capture") => Ok(
70 AudioOptions::parse(program_name, "capture", remaining_args)?.map(Command::Capture),
71 ),
72 Some("playback") => Ok(
73 AudioOptions::parse(program_name, "playback", remaining_args)?
74 .map(Command::Playback),
75 ),
76 Some("control") => {
77 Ok(ControlCommand::parse(program_name, remaining_args)?.map(Command::Control))
78 }
79 Some(s) => {
80 show_usage(program_name);
81 Err(Error::UnknownCommand(s.to_string()))
82 }
83 }
84 }
85 }
86
87 #[derive(Debug, PartialEq)]
88 pub enum FileType {
89 Raw,
90 Wav,
91 }
92
93 impl fmt::Display for FileType {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result94 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95 match self {
96 FileType::Raw => write!(f, "raw data"),
97 FileType::Wav => write!(f, "WAVE"),
98 }
99 }
100 }
101
show_usage(program_name: &str)102 fn show_usage(program_name: &str) {
103 eprintln!("Usage: {} [command] <command args>", program_name);
104 eprintln!("\nCommands:\n");
105 eprintln!("capture - Capture to a file from CRAS");
106 eprintln!("playback - Playback to CRAS from a file");
107 eprintln!("control - Get and set server settings");
108 eprintln!("\nhelp - Print help message");
109 }
110
show_audio_command_usage(program_name: &str, command: &str, opts: &Options)111 fn show_audio_command_usage(program_name: &str, command: &str, opts: &Options) {
112 let brief = format!("Usage: {} {} [options] [filename]", program_name, command);
113 eprint!("{}", opts.usage(&brief));
114 }
115
116 /// The possible command line options that can be passed to the 'playback' and
117 /// 'capture' commands. Optional values will be `Some(_)` only if a value was
118 /// explicitly provided by the user.
119 ///
120 /// This struct will be passed to `playback()` and `capture()`.
121 #[derive(Debug, PartialEq)]
122 pub enum LoopbackType {
123 PreDsp,
124 PostDsp,
125 }
126
127 #[derive(Debug, PartialEq)]
128 pub struct AudioOptions {
129 pub file_name: PathBuf,
130 pub loopback_type: Option<LoopbackType>,
131 pub file_type: FileType,
132 pub buffer_size: Option<usize>,
133 pub num_channels: Option<usize>,
134 pub format: Option<SampleFormat>,
135 pub frame_rate: Option<u32>,
136 }
137
get_u32_param(matches: &Matches, option_name: &str) -> Result<Option<u32>>138 fn get_u32_param(matches: &Matches, option_name: &str) -> Result<Option<u32>> {
139 matches.opt_get::<u32>(option_name).map_err(|e| {
140 let argument = matches.opt_str(option_name).unwrap_or_default();
141 Error::InvalidArgument(option_name.to_string(), argument, e.to_string())
142 })
143 }
144
get_usize_param(matches: &Matches, option_name: &str) -> Result<Option<usize>>145 fn get_usize_param(matches: &Matches, option_name: &str) -> Result<Option<usize>> {
146 matches.opt_get::<usize>(option_name).map_err(|e| {
147 let argument = matches.opt_str(option_name).unwrap_or_default();
148 Error::InvalidArgument(option_name.to_string(), argument, e.to_string())
149 })
150 }
151
152 impl AudioOptions {
parse<T: AsRef<str>>( program_name: &str, command_name: &str, args: &[T], ) -> Result<Option<Self>>153 fn parse<T: AsRef<str>>(
154 program_name: &str,
155 command_name: &str,
156 args: &[T],
157 ) -> Result<Option<Self>> {
158 let mut opts = Options::new();
159 opts.optopt("b", "buffer_size", "Buffer size in frames", "SIZE")
160 .optopt("c", "channels", "Number of channels", "NUM")
161 .optopt(
162 "f",
163 "format",
164 "Sample format (U8, S16_LE, S24_LE, or S32_LE)",
165 "FORMAT",
166 )
167 .optopt("r", "rate", "Audio frame rate (Hz)", "RATE")
168 .optflag("h", "help", "Print help message");
169
170 if command_name == "capture" {
171 opts.optopt(
172 "",
173 "loopback",
174 "Capture from loopback device ('pre_dsp' or 'post_dsp')",
175 "DEVICE",
176 );
177 }
178
179 let args = args.iter().map(|s| s.as_ref());
180 let matches = match opts.parse(args) {
181 Ok(m) => m,
182 Err(e) => {
183 show_audio_command_usage(program_name, command_name, &opts);
184 return Err(Error::GetOpts(e));
185 }
186 };
187 if matches.opt_present("h") {
188 show_audio_command_usage(program_name, command_name, &opts);
189 return Ok(None);
190 }
191
192 let loopback_type = if matches.opt_defined("loopback") {
193 match matches.opt_str("loopback").as_deref() {
194 Some("pre_dsp") => Some(LoopbackType::PreDsp),
195 Some("post_dsp") => Some(LoopbackType::PostDsp),
196 Some(s) => {
197 return Err(Error::InvalidArgument(
198 "loopback".to_string(),
199 s.to_string(),
200 "Loopback type must be 'pre_dsp' or 'post_dsp'".to_string(),
201 ))
202 }
203 None => None,
204 }
205 } else {
206 None
207 };
208
209 let file_name = match matches.free.get(0) {
210 None => {
211 show_audio_command_usage(program_name, command_name, &opts);
212 return Err(Error::MissingFilename);
213 }
214 Some(file_name) => PathBuf::from(file_name),
215 };
216
217 let extension = file_name
218 .extension()
219 .map(|s| s.to_string_lossy().into_owned());
220 let file_type = match extension.as_deref() {
221 Some("wav") | Some("wave") => FileType::Wav,
222 Some("raw") | None => FileType::Raw,
223 Some(extension) => return Err(Error::InvalidFiletype(extension.to_string())),
224 };
225
226 let buffer_size = get_usize_param(&matches, "buffer_size")?;
227 let num_channels = get_usize_param(&matches, "channels")?;
228 let frame_rate = get_u32_param(&matches, "rate")?;
229 let format = match matches.opt_str("format").as_deref() {
230 Some("U8") => Some(SampleFormat::U8),
231 Some("S16_LE") => Some(SampleFormat::S16LE),
232 Some("S24_LE") => Some(SampleFormat::S24LE),
233 Some("S32_LE") => Some(SampleFormat::S32LE),
234 Some(s) => {
235 show_audio_command_usage(program_name, command_name, &opts);
236 return Err(Error::InvalidArgument(
237 "format".to_string(),
238 s.to_string(),
239 "Format must be 'U8', 'S16_LE', 'S24_LE', or 'S32_LE'".to_string(),
240 ));
241 }
242 None => None,
243 };
244
245 Ok(Some(AudioOptions {
246 loopback_type,
247 file_name,
248 file_type,
249 buffer_size,
250 num_channels,
251 format,
252 frame_rate,
253 }))
254 }
255 }
256
show_control_command_usage(program_name: &str)257 fn show_control_command_usage(program_name: &str) {
258 eprintln!("Usage: {} control [command] <command args>", program_name);
259 eprintln!("");
260 eprintln!("Commands:");
261 let commands = [
262 ("help", "", "Print help message"),
263 ("", "", ""),
264 ("get_volume", "", "Get the system volume (0 - 100)"),
265 (
266 "set_volume",
267 "VOLUME",
268 "Set the system volume to VOLUME (0 - 100)",
269 ),
270 ("get_mute", "", "Get the system mute state (true or false)"),
271 (
272 "set_mute",
273 "MUTE",
274 "Set the system mute state to MUTE (true or false)",
275 ),
276 ("", "", ""),
277 ("list_output_devices", "", "Print list of output devices"),
278 ("list_input_devices", "", "Print list of input devices"),
279 ("list_output_nodes", "", "Print list of output nodes"),
280 ("list_input_nodes", "", "Print list of input nodes"),
281 (
282 "dump_audio_debug_info",
283 "",
284 "Print stream info, device info, and audio thread log.",
285 ),
286 ];
287 for command in &commands {
288 let command_string = format!("{} {}", command.0, command.1);
289 eprintln!("\t{: <23} {}", command_string, command.2);
290 }
291 }
292
293 #[derive(Debug, PartialEq)]
294 pub enum ControlCommand {
295 GetSystemVolume,
296 SetSystemVolume(u32),
297 GetSystemMute,
298 SetSystemMute(bool),
299 ListOutputDevices,
300 ListInputDevices,
301 ListOutputNodes,
302 ListInputNodes,
303 DumpAudioDebugInfo,
304 }
305
306 impl ControlCommand {
parse<T: AsRef<str>>(program_name: &str, args: &[T]) -> Result<Option<Self>>307 fn parse<T: AsRef<str>>(program_name: &str, args: &[T]) -> Result<Option<Self>> {
308 let mut args = args.iter().map(|s| s.as_ref());
309 match args.next() {
310 Some("help") => {
311 show_control_command_usage(program_name);
312 Ok(None)
313 }
314 Some("get_volume") => Ok(Some(ControlCommand::GetSystemVolume)),
315 Some("set_volume") => {
316 let volume_str = args
317 .next()
318 .ok_or_else(|| Error::MissingArgument("set_volume".to_string()))?;
319
320 let volume = volume_str.parse::<u32>().map_err(|e| {
321 Error::InvalidArgument(
322 "set_volume".to_string(),
323 volume_str.to_string(),
324 e.to_string(),
325 )
326 })?;
327
328 Ok(Some(ControlCommand::SetSystemVolume(volume)))
329 }
330 Some("get_mute") => Ok(Some(ControlCommand::GetSystemMute)),
331 Some("set_mute") => {
332 let mute_str = args
333 .next()
334 .ok_or_else(|| Error::MissingArgument("set_mute".to_string()))?;
335
336 let mute = mute_str.parse::<bool>().map_err(|e| {
337 Error::InvalidArgument(
338 "set_mute".to_string(),
339 mute_str.to_string(),
340 e.to_string(),
341 )
342 })?;
343 Ok(Some(ControlCommand::SetSystemMute(mute)))
344 }
345 Some("list_output_devices") => Ok(Some(ControlCommand::ListOutputDevices)),
346 Some("list_input_devices") => Ok(Some(ControlCommand::ListInputDevices)),
347 Some("list_output_nodes") => Ok(Some(ControlCommand::ListOutputNodes)),
348 Some("list_input_nodes") => Ok(Some(ControlCommand::ListInputNodes)),
349 Some("dump_audio_debug_info") => Ok(Some(ControlCommand::DumpAudioDebugInfo)),
350 Some(s) => {
351 show_control_command_usage(program_name);
352 Err(Error::UnknownCommand(s.to_string()))
353 }
354 None => {
355 show_control_command_usage(program_name);
356 Err(Error::MissingCommand)
357 }
358 }
359 }
360 }
361
362 #[cfg(test)]
363 mod tests {
364 use super::*;
365
366 #[test]
parse_command()367 fn parse_command() {
368 let command = Command::parse(&["cras_tests", "playback", "output.wav"])
369 .unwrap()
370 .unwrap();
371 assert_eq!(
372 command,
373 Command::Playback(AudioOptions {
374 file_name: PathBuf::from("output.wav"),
375 loopback_type: None,
376 file_type: FileType::Wav,
377 frame_rate: None,
378 num_channels: None,
379 format: None,
380 buffer_size: None,
381 })
382 );
383 let command = Command::parse(&["cras_tests", "capture", "input.raw"])
384 .unwrap()
385 .unwrap();
386 assert_eq!(
387 command,
388 Command::Capture(AudioOptions {
389 file_name: PathBuf::from("input.raw"),
390 loopback_type: None,
391 file_type: FileType::Raw,
392 frame_rate: None,
393 num_channels: None,
394 format: None,
395 buffer_size: None,
396 })
397 );
398
399 let command = Command::parse(&[
400 "cras_tests",
401 "playback",
402 "-r",
403 "44100",
404 "output.wave",
405 "-c",
406 "2",
407 ])
408 .unwrap()
409 .unwrap();
410 assert_eq!(
411 command,
412 Command::Playback(AudioOptions {
413 file_name: PathBuf::from("output.wave"),
414 loopback_type: None,
415 file_type: FileType::Wav,
416 frame_rate: Some(44100),
417 num_channels: Some(2),
418 format: None,
419 buffer_size: None,
420 })
421 );
422
423 let command =
424 Command::parse(&["cras_tests", "playback", "-r", "44100", "output", "-c", "2"])
425 .unwrap()
426 .unwrap();
427 assert_eq!(
428 command,
429 Command::Playback(AudioOptions {
430 file_name: PathBuf::from("output"),
431 loopback_type: None,
432 file_type: FileType::Raw,
433 frame_rate: Some(44100),
434 num_channels: Some(2),
435 format: None,
436 buffer_size: None,
437 })
438 );
439
440 assert!(Command::parse(&["cras_tests"]).is_err());
441 assert!(Command::parse(&["cras_tests", "capture"]).is_err());
442 assert!(Command::parse(&["cras_tests", "capture", "input.mp3"]).is_err());
443 assert!(Command::parse(&["cras_tests", "capture", "input.ogg"]).is_err());
444 assert!(Command::parse(&["cras_tests", "capture", "input.flac"]).is_err());
445 assert!(Command::parse(&["cras_tests", "playback"]).is_err());
446 assert!(Command::parse(&["cras_tests", "loopback"]).is_err());
447 assert!(Command::parse(&["cras_tests", "loopback", "file.ogg"]).is_err());
448 assert!(Command::parse(&["cras_tests", "filename.wav"]).is_err());
449 assert!(Command::parse(&["cras_tests", "filename.wav", "capture"]).is_err());
450 assert!(Command::parse(&["cras_tests", "help"]).is_ok());
451 assert!(Command::parse(&[
452 "cras_tests",
453 "-c",
454 "2",
455 "playback",
456 "output.wav",
457 "-r",
458 "44100"
459 ])
460 .is_err());
461 }
462 }
463