Skip to main content

nice_plug/wrapper/
standalone.rs

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