audio_processor_standalone/
options.rs

1// Augmented Audio: Audio libraries and applications
2// Copyright (c) 2022 Pedro Tacla Yamada
3//
4// The MIT License (MIT)
5//
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to deal
8// in the Software without restriction, including without limitation the rights
9// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10// copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12//
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15//
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22// THE SOFTWARE.
23use std::ffi::OsString;
24
25use clap::ArgMatches;
26
27pub enum RenderingOptions {
28    Online {
29        input_file: Option<String>,
30        input_device: Option<String>,
31        output_device: Option<String>,
32    },
33    Offline {
34        input_file: String,
35        output_file: String,
36    },
37}
38
39impl RenderingOptions {
40    pub fn input_device(&self) -> Option<String> {
41        if let RenderingOptions::Online { input_device, .. } = &self {
42            input_device.clone()
43        } else {
44            None
45        }
46    }
47
48    pub fn output_device(&self) -> Option<String> {
49        if let RenderingOptions::Online { output_device, .. } = &self {
50            output_device.clone()
51        } else {
52            None
53        }
54    }
55}
56
57pub struct MidiOptions {
58    pub input_file: Option<String>,
59}
60
61pub struct Options {
62    midi: MidiOptions,
63    rendering: RenderingOptions,
64    list_hosts: bool,
65    list_input_devices: bool,
66    list_output_devices: bool,
67}
68
69impl Options {
70    pub fn rendering(&self) -> &RenderingOptions {
71        &self.rendering
72    }
73
74    pub fn midi(&self) -> &MidiOptions {
75        &self.midi
76    }
77
78    pub fn list_hosts(&self) -> bool {
79        self.list_hosts
80    }
81
82    pub fn list_input_devices(&self) -> bool {
83        self.list_input_devices
84    }
85
86    pub fn list_output_devices(&self) -> bool {
87        self.list_output_devices
88    }
89}
90
91pub struct ParseOptionsParams {
92    pub supports_midi: bool,
93}
94
95pub fn parse_options(params: ParseOptionsParams) -> Options {
96    parse_options_from(params, std::env::args_os())
97}
98
99fn parse_options_from<I, T>(params: ParseOptionsParams, args: I) -> Options
100where
101    I: IntoIterator<Item = T>,
102    T: Into<OsString> + Clone,
103{
104    let supports_midi = params.supports_midi;
105
106    let app = clap::App::new("audio-processor-standalone");
107    let mut app = app
108        .arg(clap::Arg::from_usage(
109            "-i, --input-file=[INPUT_PATH] 'An input audio file to process'",
110        ))
111        .arg(clap::Arg::from_usage(
112            "-o, --output-file=[OUTPUT_PATH] 'If specified, will render offline into this file (WAV)'",
113        ))
114        .arg(clap::Arg::from_usage(
115            "--input-device=[INPUT_DEVICE] 'The input device to use'",
116        ))
117        .arg(clap::Arg::from_usage(
118            "--output-device=[OUTPUT_DEVICE] 'The output device to use'",
119        ))
120        .arg(clap::Arg::from_usage(
121            "--list-hosts 'List the audio hosts available'",
122        ))
123        .arg(clap::Arg::from_usage(
124            "--list-input-devices 'List the input devices available'",
125        ))
126        .arg(clap::Arg::from_usage(
127            "--list-output-devices 'List the output devices available'",
128        ));
129
130    if supports_midi {
131        app = app
132            .arg(clap::Arg::from_usage(
133                "--midi-input-file=[MIDI_INPUT_FILE] 'If specified, this MIDI file will be passed through the processor'",
134            ));
135    }
136
137    let matches = app.get_matches_from(args);
138
139    let midi_options = parse_midi_options(&matches);
140    let rendering = parse_rendering_options(&matches);
141
142    Options {
143        midi: midi_options,
144        rendering,
145        list_hosts: matches.is_present("list-hosts"),
146        list_input_devices: matches.is_present("list-input-devices"),
147        list_output_devices: matches.is_present("list-output-devices"),
148    }
149}
150
151fn parse_midi_options(matches: &ArgMatches) -> MidiOptions {
152    MidiOptions {
153        input_file: matches.value_of("midi-input-file").map(|s| s.into()),
154    }
155}
156
157fn parse_rendering_options(matches: &ArgMatches) -> RenderingOptions {
158    if matches.is_present("output-file") {
159        if !matches.is_present("input-file") {
160            log::error!("Please specify `--input-file`");
161            std::process::exit(1);
162        }
163
164        let input_path = matches.value_of("input-file").map(|s| s.into()).unwrap();
165        let output_path = matches.value_of("output-file").map(|s| s.into()).unwrap();
166
167        RenderingOptions::Offline {
168            input_file: input_path,
169            output_file: output_path,
170        }
171    } else {
172        RenderingOptions::Online {
173            input_file: matches.value_of("input-file").map(|s| s.into()),
174            input_device: matches.value_of("input-device").map(|s| s.into()),
175            output_device: matches.value_of("output-device").map(|s| s.into()),
176        }
177    }
178}
179
180#[cfg(test)]
181mod test {
182    use super::*;
183
184    #[test]
185    fn test_parse_empty_options() {
186        let options = parse_options_from::<Vec<String>, String>(
187            ParseOptionsParams {
188                supports_midi: false,
189            },
190            vec![],
191        );
192        assert!(options.midi().input_file.is_none());
193        assert!(matches!(
194            options.rendering(),
195            RenderingOptions::Online { .. }
196        ));
197    }
198
199    #[test]
200    fn test_parse_list_hosts() {
201        let options = parse_options_from::<Vec<String>, String>(
202            ParseOptionsParams {
203                supports_midi: false,
204            },
205            vec!["program".into(), "--list-hosts".into()],
206        );
207        assert!(options.list_hosts());
208    }
209
210    #[test]
211    fn test_parse_list_input_devices() {
212        let options = parse_options_from::<Vec<String>, String>(
213            ParseOptionsParams {
214                supports_midi: false,
215            },
216            vec!["program".into(), "--list-input-devices".into()],
217        );
218        assert!(options.list_input_devices());
219    }
220
221    #[test]
222    fn test_parse_list_output_devices() {
223        let options = parse_options_from::<Vec<String>, String>(
224            ParseOptionsParams {
225                supports_midi: false,
226            },
227            vec!["program".into(), "--list-output-devices".into()],
228        );
229        assert!(options.list_output_devices());
230    }
231
232    #[test]
233    fn test_parse_online_options() {
234        let options = parse_options_from::<Vec<String>, String>(
235            ParseOptionsParams {
236                supports_midi: false,
237            },
238            vec![
239                "program".into(),
240                "--input-file".into(),
241                "test.mp3".into(),
242                "--input-device".into(),
243                "Microphone".into(),
244                "--output-device".into(),
245                "Default".into(),
246            ],
247        );
248        assert!(options.midi().input_file.is_none());
249        assert!(matches!(
250            options.rendering(),
251            RenderingOptions::Online { .. }
252        ));
253        match options.rendering() {
254            RenderingOptions::Online {
255                input_file,
256                input_device,
257                output_device,
258            } => {
259                assert_eq!(input_file.as_ref().unwrap(), "test.mp3");
260                assert_eq!(input_device.as_ref().unwrap(), "Microphone");
261                assert_eq!(output_device.as_ref().unwrap(), "Default");
262            }
263            _ => {}
264        }
265    }
266
267    #[test]
268    fn test_parse_midi_options() {
269        let options = parse_options_from::<Vec<String>, String>(
270            ParseOptionsParams {
271                supports_midi: true,
272            },
273            vec![
274                "program".into(),
275                "--midi-input-file".into(),
276                "bach.mid".into(),
277            ],
278        );
279        assert!(options.midi().input_file.is_some());
280        assert_eq!(options.midi().input_file.as_ref().unwrap(), "bach.mid")
281    }
282
283    #[test]
284    fn test_parse_offline_options() {
285        let options = parse_options_from::<Vec<String>, String>(
286            ParseOptionsParams {
287                supports_midi: false,
288            },
289            vec![
290                "program".into(),
291                "--input-file".into(),
292                "test.mp3".into(),
293                "--output-file".into(),
294                "test.wav".into(),
295            ],
296        );
297        assert!(options.midi().input_file.is_none());
298        assert!(matches!(
299            options.rendering(),
300            RenderingOptions::Offline { .. }
301        ));
302        match options.rendering() {
303            RenderingOptions::Offline {
304                input_file,
305                output_file,
306            } => {
307                assert_eq!(input_file, "test.mp3");
308                assert_eq!(output_file, "test.wav");
309            }
310            _ => {}
311        }
312    }
313}