1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
//! A standalone plugin target that directly connects to the system's audio and MIDI ports instead
//! of relying on a plugin host. This is mostly useful for quickly testing GUI changes.
use clap::{CommandFactory, FromArgMatches};
use self::backend::Backend;
use self::config::WrapperConfig;
use self::wrapper::{Wrapper, WrapperError};
use super::util::setup_logger;
use crate::prelude::Plugin;
mod backend;
mod config;
mod context;
mod wrapper;
/// Open an NIH-plug plugin as a standalone application. If the plugin has an editor, this will open
/// the editor and block until the editor is closed. Otherwise this will block until SIGINT is
/// received. This is mainly useful for quickly testing plugin GUIs. In order to use this, you will
/// first need to make your plugin's main struct `pub` and expose a `lib` artifact in addition to
/// your plugin's `cdylib`:
///
/// ```toml
/// # Cargo.toml
///
/// [lib]
/// # The `lib` artifact is needed for the standalone target
/// crate-type = ["cdylib", "lib"]
/// ```
///
/// You can then create a `src/main.rs` file that calls this function:
///
/// ```ignore
/// // src/main.rs
///
/// use nih_plug::prelude::*;
///
/// use plugin_name::PluginName;
///
/// fn main() {
/// nih_export_standalone::<PluginName>();
/// }
/// ```
///
/// By default this will connect to the 'default' audio and MIDI ports. Use the command line options
/// to change this. `--help` lists all available options.
///
/// If the wrapped plugin fails to initialize or throws an error during audio processing, then this
/// function will return `false`.
pub fn nih_export_standalone<P: Plugin>() -> bool {
// TODO: If the backend fails to initialize then the standalones will exit normally instead of
// with an error code. This should probably be changed.
nih_export_standalone_with_args::<P, _>(std::env::args())
}
/// The same as [`nih_export_standalone()`], but with the arguments taken from an iterator instead
/// of using [`std::env::args()`].
pub fn nih_export_standalone_with_args<P: Plugin, Args: IntoIterator<Item = String>>(
args: Args,
) -> bool {
setup_logger();
// Instead of parsing this directly, we need to take a bit of a roundabout approach to get the
// plugin's name and vendor in here since they'd otherwise be taken from NIH-plug's own
// `Cargo.toml` file.
let config = WrapperConfig::from_arg_matches(
&WrapperConfig::command()
.name(P::NAME)
.author(P::VENDOR)
.get_matches_from(args),
)
.unwrap_or_else(|err| err.exit());
match config.backend {
config::BackendType::Auto => {
let result = backend::Jack::new::<P>(config.clone()).map(|backend| {
nih_log!("Using the JACK backend");
run_wrapper::<P, _>(backend, config.clone())
});
#[cfg(target_os = "linux")]
let result = result.or_else(|_| {
match backend::CpalMidir::new::<P>(config.clone(), cpal::HostId::Alsa) {
Ok(backend) => {
nih_log!("Using the ALSA backend");
Ok(run_wrapper::<P, _>(backend, config.clone()))
}
Err(err) => {
nih_error!(
"Could not initialize either the JACK or the ALSA backends, falling \
back to the dummy audio backend: {err:#}"
);
Err(())
}
}
});
#[cfg(target_os = "macos")]
let result = result.or_else(|_| {
match backend::CpalMidir::new::<P>(config.clone(), cpal::HostId::CoreAudio) {
Ok(backend) => {
nih_log!("Using the CoreAudio backend");
Ok(run_wrapper::<P, _>(backend, config.clone()))
}
Err(err) => {
nih_error!(
"Could not initialize either the JACK or the CoreAudio backends, \
falling back to the dummy audio backend: {err:#}"
);
Err(())
}
}
});
#[cfg(target_os = "windows")]
let result = result.or_else(|_| {
match backend::CpalMidir::new::<P>(config.clone(), cpal::HostId::Wasapi) {
Ok(backend) => {
nih_log!("Using the WASAPI backend");
Ok(run_wrapper::<P, _>(backend, config.clone()))
}
Err(err) => {
nih_error!(
"Could not initialize either the JACK or the WASAPI backends, falling \
back to the dummy audio backend: {err:#}"
);
Err(())
}
}
});
result.unwrap_or_else(|_| {
nih_error!("Falling back to the dummy audio backend, audio and MIDI will not work");
run_wrapper::<P, _>(backend::Dummy::new::<P>(config.clone()), config)
})
}
config::BackendType::Jack => match backend::Jack::new::<P>(config.clone()) {
Ok(backend) => run_wrapper::<P, _>(backend, config),
Err(err) => {
nih_error!("Could not initialize the JACK backend: {:#}", err);
false
}
},
#[cfg(target_os = "linux")]
config::BackendType::Alsa => {
match backend::CpalMidir::new::<P>(config.clone(), cpal::HostId::Alsa) {
Ok(backend) => run_wrapper::<P, _>(backend, config),
Err(err) => {
nih_error!("Could not initialize the ALSA backend: {:#}", err);
false
}
}
}
#[cfg(target_os = "macos")]
config::BackendType::CoreAudio => {
match backend::CpalMidir::new::<P>(config.clone(), cpal::HostId::CoreAudio) {
Ok(backend) => run_wrapper::<P, _>(backend, config),
Err(err) => {
nih_error!("Could not initialize the CoreAudio backend: {:#}", err);
false
}
}
}
#[cfg(target_os = "windows")]
config::BackendType::Wasapi => {
match backend::CpalMidir::new::<P>(config.clone(), cpal::HostId::Wasapi) {
Ok(backend) => run_wrapper::<P, _>(backend, config),
Err(err) => {
nih_error!("Could not initialize the WASAPI backend: {:#}", err);
false
}
}
}
config::BackendType::Dummy => {
run_wrapper::<P, _>(backend::Dummy::new::<P>(config.clone()), config)
}
}
}
fn run_wrapper<P: Plugin, B: Backend<P>>(backend: B, config: WrapperConfig) -> bool {
let wrapper = match Wrapper::<P, _>::new(backend, config) {
Ok(wrapper) => wrapper,
Err(err) => {
print_error(err);
return false;
}
};
// TODO: Add a repl while the application is running to interact with parameters
match wrapper.run() {
Ok(()) => true,
Err(err) => {
print_error(err);
false
}
}
}
fn print_error(error: WrapperError) {
match error {
WrapperError::InitializationFailed => {
nih_error!("The plugin failed to initialize");
}
}
}