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