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//! [![crates.io](https://img.shields.io/crates/v/audio-processor-standalone.svg)](https://crates.io/crates/audio-processor-standalone)
25//! [![docs.rs](https://docs.rs/audio-processor-standalone/badge.svg)](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}