audio_processor_standalone/lib.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.
23//! # Augmented Audio: Audio Processor Standalone
24//! [](https://crates.io/crates/audio-processor-standalone)
25//! [](https://docs.rs/audio-processor-standalone/)
26//! - - -
27//! This is part of <https://github.com/yamadapc/augmented-audio>. Please review its goals. This
28//! crate builds upon [`audio_processor_traits::AudioProcessor`](https://docs.rs/audio-processor-traits/latest/audio_processor_traits/trait.AudioProcessor.html).
29//!
30//! Provides a stand-alone audio-processor runner for [`AudioProcessor`](https://docs.rs/audio-processor-traits/latest/audio_processor_traits/trait.AudioProcessor.html)
31//! implementations.
32//!
33//! ## Navigating the documentation
34//! * Look at exported functions & macros; the structs/traits are for more advanced/internal usage.
35//! * Start with [`audio_processor_main`] and [`audio_processor_main_with_midi`]
36//! * There are plenty examples in the `augmented-audio` repository
37//!
38//! The gist of it is:
39//!
40//! 1. Implement [`AudioProcessor`](https://docs.rs/audio-processor-traits/latest/audio_processor_traits/trait.AudioProcessor.html)
41//! or [`SimpleAudioProcessor`](https://docs.rs/audio-processor-traits/latest/audio_processor_traits/trait.SimpleAudioProcessor.html)
42//! from [`audio_processor_traits`](https://docs.rs/audio-processor-traits)
43//! 2. Call `audio_processor_main(processor)`
44//! 3. You now have a CLI for rendering online (CPAL, use your mic) or offline (pass a file through your processor & write
45//! the results to a `.wav`)
46//!
47//! A VST may also be generated through the `standalone_vst` module and by enabling the `vst`
48//! feature flag.
49//!
50//! ## Example usage
51//!
52//! Declare the `AudioProcessor`:
53//!
54//! ```rust
55//! use audio_processor_traits::{AudioBuffer, AudioContext, AudioProcessor};
56//!
57//! struct GainProcessor {}
58//!
59//! impl GainProcessor { fn new() -> Self { GainProcessor {} }}
60//!
61//! impl AudioProcessor for GainProcessor {
62//! type SampleType = f32;
63//! fn process(&mut self, _context: &mut AudioContext, data: &mut AudioBuffer<Self::SampleType>) {
64//! for channel in data.channels_mut() {
65//! for sample in channel {
66//! *sample = *sample * 0.4;
67//! }
68//! }
69//! }
70//! }
71//! ```
72//!
73//! Declare the main function:
74//!
75//! ```ignore
76//! fn main() {
77//! let processor = GainProcessor::new();
78//! audio_processor_standalone::audio_processor_main(processor);
79//! }
80//! ```
81//!
82//! ## Usage of the command-line
83//! ```ignore
84//! audio-processor-standalone
85//!
86//! USAGE:
87//! my-crate [OPTIONS]
88//!
89//! FLAGS:
90//! -h, --help Prints help information
91//! -V, --version Prints version information
92//!
93//! OPTIONS:
94//! -i, --input-file <INPUT_PATH> An input audio file to process
95//! --midi-input-file <MIDI_INPUT_FILE> If specified, this MIDI file will be passed through the processor
96//! -o, --output-file <OUTPUT_PATH> If specified, will render offline into this file (WAV)
97//! ```
98
99use basedrop::Handle;
100use cpal::traits::HostTrait;
101
102use crate::standalone_cpal::StandaloneStartOptions;
103use crate::standalone_processor::StandaloneOptions;
104use audio_processor_traits::{AudioProcessor, MidiEventHandler};
105use options::{ParseOptionsParams, RenderingOptions};
106#[doc(inline)]
107pub use standalone_cpal::{
108 audio_processor_start, audio_processor_start_with_midi, standalone_start,
109 standalone_start_with, StandaloneHandles,
110};
111#[doc(inline)]
112pub use standalone_processor::{
113 StandaloneAudioOnlyProcessor, StandaloneProcessor, StandaloneProcessorImpl,
114};
115
116/// Options handling for standalone processor
117pub mod options;
118/// Online standalone implementation using CPAL
119pub mod standalone_cpal;
120/// Internal wrapper types
121pub mod standalone_processor;
122
123/// Offline rendering implementation (offline functionality will not work on iOS for now)
124#[cfg(not(target_os = "ios"))]
125pub mod offline;
126
127/// VST support (VST is not compiled for iOS)
128#[cfg(all(feature = "vst", not(target_os = "ios")))]
129pub mod standalone_vst;
130
131#[cfg(feature = "clap")]
132pub mod standalone_clap;
133
134/// A default main function for an [`AudioProcessor`] and [`MidiEventHandler`].
135///
136/// Run an [`AudioProcessor`] / [`MidiEventHandler`] as a stand-alone cpal app and forward MIDI
137/// messages received on all inputs to it. Same as `audio_processor_main`, but requires
138/// [`MidiEventHandler`] to support MIDI.
139///
140/// Will internally create [`cpal::Stream`], [`audio_processor_standalone_midi::MidiHost`] and park the current thread. If the thread
141/// is unparked the function will exit and the audio/MIDI threads will stop once these structures
142/// are dropped.
143pub fn audio_processor_main_with_midi<
144 Processor: AudioProcessor<SampleType = f32> + MidiEventHandler + Send + 'static,
145>(
146 audio_processor: Processor,
147 handle: &Handle,
148) {
149 let app = StandaloneProcessorImpl::new(audio_processor);
150 standalone_main(app, Some(handle));
151}
152
153/// A default main function for an [`AudioProcessor`].
154///
155/// Will support running it, based on CLI options, as:
156///
157/// * CPAL audio app processing audio from default input device and outputting it
158/// * CPAL audio app processing an audio input file (MP3)
159/// * Offline rendering into a WAV file
160///
161pub fn audio_processor_main<Processor: AudioProcessor<SampleType = f32> + Send + 'static>(
162 audio_processor: Processor,
163) {
164 // Options are parsed twice which isn't good
165 let options = options::parse_options(ParseOptionsParams {
166 supports_midi: false,
167 });
168 let app = StandaloneAudioOnlyProcessor::new(
169 audio_processor,
170 StandaloneOptions {
171 input_device: options.rendering().input_device(),
172 output_device: options.rendering().output_device(),
173 ..Default::default()
174 },
175 );
176 standalone_main(app, None);
177}
178
179/// Internal main function used by `audio_processor_main`.
180fn standalone_main<SP: StandaloneProcessor>(mut app: SP, handle: Option<&Handle>) {
181 let options = options::parse_options(ParseOptionsParams {
182 supports_midi: app.supports_midi(),
183 });
184
185 if handle_list_commands(&options) {
186 return;
187 }
188
189 match options.rendering() {
190 RenderingOptions::Online { .. } => {
191 log::info!("Starting stand-alone online rendering with default IO config");
192 let _handles = standalone_start_with::<SP, cpal::Host>(
193 app,
194 StandaloneStartOptions {
195 handle: handle.cloned(),
196 ..StandaloneStartOptions::default()
197 },
198 );
199 std::thread::park();
200 }
201 #[cfg(not(target_os = "ios"))]
202 RenderingOptions::Offline {
203 input_file: input_path,
204 output_file: output_path,
205 } => {
206 #[cfg(feature = "midi")]
207 let midi_input_file = options.midi().input_file.as_ref().map(|midi_input_file| {
208 let file_contents =
209 std::fs::read(midi_input_file).expect("Failed to read input MIDI file");
210 let (_, midi_file) =
211 augmented_midi::parse_midi_file::<String, Vec<u8>>(&file_contents)
212 .expect("Failed to parse input MIDI file");
213 midi_file
214 });
215
216 offline::run_offline_render(offline::OfflineRenderOptions {
217 app,
218 handle,
219 input_path,
220 output_path,
221 #[cfg(feature = "midi")]
222 midi_input_file,
223 });
224 }
225 #[cfg(target_os = "ios")]
226 _ => {
227 log::error!("Offline rendering is unsupported on iOS");
228 std::process::exit(1)
229 }
230 }
231}
232
233fn handle_list_commands(options: &options::Options) -> bool {
234 if options.list_hosts() {
235 for host in cpal::available_hosts() {
236 println!("{:?}", host.name());
237 }
238 return true;
239 }
240
241 if options.list_input_devices() || options.list_output_devices() {
242 use cpal::traits::DeviceTrait;
243
244 let host = cpal::default_host();
245 let devices = if options.list_input_devices() {
246 host.input_devices()
247 } else {
248 host.output_devices()
249 };
250 for device in devices.expect("Failed to list devices") {
251 println!("{:?}", device.name().expect("Failed to get device name"));
252 }
253 return true;
254 }
255
256 false
257}