use std::fmt;
use std::path::PathBuf;
use cpal::{Error, ErrorKind};
use crate::output::SampleType;
pub mod atome;
pub mod internal;
pub mod params;
#[cfg(all(feature = "au", target_vendor = "apple"))]
pub mod au;
#[cfg(all(feature = "au3", target_vendor = "apple"))]
pub mod au3;
#[cfg(feature = "vst")]
pub mod vst;
#[cfg(feature = "vst3")]
pub mod vst3;
#[cfg(any(
feature = "vst3",
all(any(feature = "au", feature = "au3"), target_vendor = "apple")
))]
mod host;
pub use atome::Effect;
pub use internal::InternalPlugin;
pub use params::{ParamError, ParamKind, ParamSpec, Params, Value};
macro_rules! have {
(au) => {
cfg!(all(feature = "au", target_vendor = "apple"))
};
(au3) => {
cfg!(all(feature = "au3", target_vendor = "apple"))
};
(vst3) => {
cfg!(feature = "vst3")
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PluginFormat {
Internal,
Au,
Au3,
Vst,
Vst3,
}
impl PluginFormat {
pub fn is_available(self) -> bool {
match self {
Self::Internal => true,
Self::Au => have!(au),
Self::Au3 => have!(au3),
Self::Vst => false,
Self::Vst3 => have!(vst3),
}
}
fn unavailable(self) -> String {
match self {
Self::Internal => unreachable!("internal plugins are always available"),
Self::Au => "AU v2 hosting needs the 'au' feature, on macOS or iOS".into(),
Self::Au3 => "AU v3 hosting needs the 'au3' feature, on macOS or iOS".into(),
Self::Vst => "VST 2.4 hosting is not implemented — there is no backend behind the \
'vst' feature. Use VST 3 ('vst3') instead."
.into(),
Self::Vst3 => "VST 3 hosting needs the 'vst3' feature".into(),
}
}
}
enum PluginInner {
Internal(InternalPlugin),
#[cfg(all(feature = "au", target_vendor = "apple"))]
Au(au::Loaded),
#[cfg(all(feature = "au3", target_vendor = "apple", not(feature = "au")))]
Au3(au3::Loaded),
#[cfg(feature = "vst3")]
Vst3(vst3::Loaded),
}
pub struct Plugin {
pub name: String,
pub path: PathBuf,
pub buffer_size: usize,
pub sample_rate: usize,
pub channels: usize,
pub params: String,
pub format: PluginFormat,
inner: Option<PluginInner>,
}
impl Plugin {
pub fn new(
name: String,
path: PathBuf,
buffer_size: usize,
sample_rate: usize,
channels: usize,
params: String,
format: PluginFormat,
) -> Self {
Self {
name,
path,
buffer_size,
sample_rate,
channels,
params,
format,
inner: None,
}
}
pub fn internal<F>(name: impl Into<String>, process: F) -> Self
where
F: Fn(&mut [f32], u16) + Send + Sync + 'static,
{
Self::from_internal(InternalPlugin::new(name, process))
}
pub fn from_internal(plugin: InternalPlugin) -> Self {
Self {
name: plugin.name().to_string(),
path: PathBuf::new(),
buffer_size: 0,
sample_rate: 0,
channels: 0,
params: String::new(),
format: PluginFormat::Internal,
inner: Some(PluginInner::Internal(plugin)),
}
}
pub fn is_loaded(&self) -> bool {
self.inner.is_some()
}
pub fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if self.inner.is_some() {
return Ok(());
}
let format = self.detect_format();
if !format.is_available() {
return Err(format.unavailable().into());
}
self.inner = Some(match format {
PluginFormat::Internal => {
let plugin = match internal::get(&self.name) {
Some(plugin) => plugin,
None => {
let built_in = atome::create(
&self.name,
self.sample_rate as u32,
&Params::new(),
)
.map_err(|_| {
format!(
"'{}' is neither a registered internal plugin nor one of the \
built-ins ({}). Register one with \
atome::plugins::internal::register, or build it directly with \
Plugin::internal",
self.name,
atome::kinds().join(", "),
)
})?;
match built_in.inner {
Some(PluginInner::Internal(plugin)) => plugin,
_ => unreachable!("a built-in is always an internal plugin"),
}
}
};
PluginInner::Internal(plugin)
}
#[cfg(all(feature = "au", target_vendor = "apple"))]
PluginFormat::Au => PluginInner::Au(au::load(
&self.path,
self.channels,
self.sample_rate as f64,
self.buffer_size,
)?),
#[cfg(all(feature = "au3", target_vendor = "apple", feature = "au"))]
PluginFormat::Au3 => PluginInner::Au(au3::load(
&self.path,
self.channels,
self.sample_rate as f64,
self.buffer_size,
)?),
#[cfg(all(feature = "au3", target_vendor = "apple", not(feature = "au")))]
PluginFormat::Au3 => PluginInner::Au3(au3::load(
&self.path,
self.channels,
self.sample_rate as f64,
self.buffer_size,
)?),
#[cfg(feature = "vst3")]
PluginFormat::Vst3 => PluginInner::Vst3(vst3::load(
&self.path,
self.channels,
self.sample_rate as f64,
self.buffer_size,
)?),
#[allow(unreachable_patterns)]
other => return Err(other.unavailable().into()),
});
if !self.params.trim().is_empty() {
let params = Params::parse(&self.params)?;
self.set_params(¶ms)?;
}
Ok(())
}
fn detect_format(&self) -> PluginFormat {
if self.format != PluginFormat::Internal {
return self.format;
}
let name = self
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
if name.ends_with(".vst3") {
PluginFormat::Vst3
} else if name.ends_with(".component") {
PluginFormat::Au
} else if name.ends_with(".appex") {
PluginFormat::Au3
} else {
PluginFormat::Internal
}
}
pub fn apply<S: SampleType>(&mut self, buffer: &mut [S], channels: u16) -> Result<(), Error> {
let Some(inner) = &mut self.inner else {
return Ok(());
};
match inner {
PluginInner::Internal(plugin) => apply_internal(plugin, buffer, channels),
#[cfg(all(feature = "au", target_vendor = "apple"))]
PluginInner::Au(plugin) => plugin.process(buffer, channels).map_err(host_error),
#[cfg(all(feature = "au3", target_vendor = "apple", not(feature = "au")))]
PluginInner::Au3(plugin) => plugin.process(buffer, channels).map_err(host_error),
#[cfg(feature = "vst3")]
PluginInner::Vst3(plugin) => plugin.process(buffer, channels).map_err(host_error),
}
}
pub fn latency(&self) -> usize {
match &self.inner {
Some(PluginInner::Internal(plugin)) => plugin.latency(),
_ => 0,
}
}
pub fn param_schema(&self) -> Vec<ParamSpec> {
match &self.inner {
None => Vec::new(),
Some(PluginInner::Internal(plugin)) => {
plugin.effect().map(Effect::schema).unwrap_or_default()
}
#[cfg(all(feature = "au", target_vendor = "apple"))]
Some(PluginInner::Au(plugin)) => plugin.param_schema(),
#[cfg(all(feature = "au3", target_vendor = "apple", not(feature = "au")))]
Some(PluginInner::Au3(plugin)) => plugin.param_schema(),
#[cfg(feature = "vst3")]
Some(PluginInner::Vst3(plugin)) => plugin.param_schema(),
}
}
pub fn params(&self) -> Params {
match &self.inner {
None => Params::new(),
Some(PluginInner::Internal(plugin)) => {
plugin.effect().map(atome::params_of).unwrap_or_default()
}
#[cfg(all(feature = "au", target_vendor = "apple"))]
Some(PluginInner::Au(plugin)) => plugin.params(),
#[cfg(all(feature = "au3", target_vendor = "apple", not(feature = "au")))]
Some(PluginInner::Au3(plugin)) => plugin.params(),
#[cfg(feature = "vst3")]
Some(PluginInner::Vst3(plugin)) => plugin.params(),
}
}
pub fn set_params(&mut self, params: &Params) -> Result<(), ParamError> {
match &mut self.inner {
None => {
if params.is_empty() {
return Ok(());
}
Err(ParamError::Unsupported {
message: format!(
"'{}' is not loaded, so it has no parameters to set — call load() first",
self.name
),
})
}
Some(PluginInner::Internal(plugin)) => match plugin.effect_mut() {
Some(effect) => atome::apply(effect, params),
None if params.is_empty() => Ok(()),
None => Err(ParamError::Unsupported {
message: format!(
"'{}' is a plain function, which has no parameters. Build it from an \
Effect — see atome::plugins::atome — if it needs them",
self.name
),
}),
},
#[cfg(all(feature = "au", target_vendor = "apple"))]
Some(PluginInner::Au(plugin)) => plugin.set_params(params),
#[cfg(all(feature = "au3", target_vendor = "apple", not(feature = "au")))]
Some(PluginInner::Au3(plugin)) => plugin.set_params(params),
#[cfg(feature = "vst3")]
Some(PluginInner::Vst3(plugin)) => plugin.set_params(params),
}
}
pub fn set_param(
&mut self,
key: &str,
value: impl Into<Value>,
) -> Result<(), ParamError> {
self.set_params(&Params::new().with(key, value))
}
pub fn set_params_str(&mut self, params: &str) -> Result<(), ParamError> {
self.set_params(&Params::parse(params)?)
}
pub fn with_params(mut self, params: &str) -> Result<Self, ParamError> {
self.set_params_str(params)?;
Ok(self)
}
pub fn reset(&mut self) {
if let Some(PluginInner::Internal(plugin)) = &mut self.inner {
plugin.reset();
}
}
}
fn apply_internal<S: SampleType>(
plugin: &mut InternalPlugin,
buffer: &mut [S],
channels: u16,
) -> Result<(), Error> {
if channels == 0 || buffer.len() % channels as usize != 0 {
return Err(Error::with_message(
ErrorKind::InvalidInput,
format!(
"{} samples is not a whole number of {channels}-channel frames",
buffer.len()
),
));
}
let mut scratch: Vec<f32> = buffer.iter().map(|sample| sample.to_f32()).collect();
plugin.process(&mut scratch, channels);
for (sample, processed) in buffer.iter_mut().zip(scratch) {
*sample = S::from_f32(processed);
}
Ok(())
}
#[cfg(any(
feature = "vst3",
all(any(feature = "au", feature = "au3"), target_vendor = "apple")
))]
fn host_error(error: truce_rack_core::error::Error) -> Error {
Error::with_message(ErrorKind::BackendError, error.to_string())
}
impl Clone for Plugin {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
path: self.path.clone(),
buffer_size: self.buffer_size,
sample_rate: self.sample_rate,
channels: self.channels,
params: self.params.clone(),
format: self.format,
inner: match &self.inner {
Some(PluginInner::Internal(plugin)) => Some(PluginInner::Internal(plugin.clone())),
_ => None,
},
}
}
}
impl fmt::Debug for Plugin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Plugin")
.field("name", &self.name)
.field("path", &self.path)
.field("buffer_size", &self.buffer_size)
.field("sample_rate", &self.sample_rate)
.field("channels", &self.channels)
.field("params", &self.params)
.field("format", &self.format)
.field("loaded", &self.is_loaded())
.finish()
}
}
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.path == other.path
&& self.buffer_size == other.buffer_size
&& self.sample_rate == other.sample_rate
&& self.channels == other.channels
&& self.params == other.params
&& self.format == other.format
}
}
impl Eq for Plugin {}
#[cfg(test)]
mod tests {
use super::*;
fn descriptor(name: &str, format: PluginFormat) -> Plugin {
Plugin::new(
name.into(),
PathBuf::new(),
512,
48_000,
2,
String::new(),
format,
)
}
#[test]
fn an_unloaded_plugin_passes_audio_through() {
let mut plugin = descriptor("never loaded", PluginFormat::Vst3);
let mut buffer = [0.25_f32, -0.5];
plugin.apply(&mut buffer, 2).expect("pass-through");
assert_eq!(buffer, [0.25, -0.5]);
assert!(!plugin.is_loaded());
}
#[test]
fn internal_plugins_are_loaded_on_construction() {
let plugin = Plugin::internal("gain", |_: &mut [f32], _| {});
assert!(plugin.is_loaded());
assert_eq!(plugin.format, PluginFormat::Internal);
}
#[test]
fn applies_to_a_non_float_engine() {
let mut plugin = Plugin::internal("silence", |buffer: &mut [f32], _| {
buffer.fill(0.0);
});
let mut buffer = [i16::MAX, i16::MIN, 1234];
plugin.apply(&mut buffer, 1).expect("processed");
assert_eq!(buffer, [0, 0, 0]);
}
#[test]
fn round_trips_a_float_engine_untouched() {
let mut plugin = Plugin::internal("noop", |_: &mut [f32], _| {});
let mut buffer = [0.123_f32, -0.789, 1.0, -1.0];
let before = buffer;
plugin.apply(&mut buffer, 2).expect("processed");
assert_eq!(buffer, before);
}
#[test]
fn the_plugin_sees_the_interleaving_it_was_told_about() {
let mut plugin = Plugin::internal("right only", |buffer: &mut [f32], channels| {
for frame in buffer.chunks_mut(channels as usize) {
frame[0] = 0.0;
}
});
let mut buffer = [1.0_f32, 1.0, 1.0, 1.0];
plugin.apply(&mut buffer, 2).expect("processed");
assert_eq!(buffer, [0.0, 1.0, 0.0, 1.0]);
}
#[test]
fn refuses_a_partial_frame() {
let mut plugin = Plugin::internal("noop", |_: &mut [f32], _| {});
let mut buffer = [0.0_f32; 3];
assert!(plugin.apply(&mut buffer, 2).is_err());
}
#[test]
fn refuses_zero_channels() {
let mut plugin = Plugin::internal("noop", |_: &mut [f32], _| {});
let mut buffer = [0.0_f32; 4];
assert!(plugin.apply(&mut buffer, 0).is_err());
}
#[test]
fn loads_an_internal_plugin_by_name() {
internal::register("load by name", |buffer: &mut [f32], _| buffer.fill(1.0));
let mut plugin = descriptor("load by name", PluginFormat::Internal);
assert!(!plugin.is_loaded());
plugin.load().expect("registered");
assert!(plugin.is_loaded());
let mut buffer = [0.0_f32; 2];
plugin.apply(&mut buffer, 2).expect("processed");
assert_eq!(buffer, [1.0, 1.0]);
internal::unregister("load by name");
}
#[test]
fn an_unregistered_name_is_an_error() {
let mut plugin = descriptor("nothing registered under this", PluginFormat::Internal);
let error = plugin.load().expect_err("not registered");
let message = error.to_string();
assert!(message.contains("nothing registered under this"), "{message}");
assert!(message.contains("compressor"), "{message}");
}
#[test]
fn a_built_in_loads_by_name() {
let mut plugin = descriptor("compressor", PluginFormat::Internal);
plugin.load().expect("compressor is a built-in");
assert!(plugin.is_loaded());
assert_eq!(plugin.params().number("ratio"), Some(4.0));
}
#[test]
fn a_registered_name_shadows_a_built_in() {
internal::register("reverb", |buffer: &mut [f32], _| buffer.fill(0.25));
let mut plugin = descriptor("reverb", PluginFormat::Internal);
plugin.load().expect("registered");
let mut buffer = [0.0_f32; 4];
plugin.apply(&mut buffer, 2).expect("processes");
assert_eq!(buffer, [0.25; 4]);
internal::unregister("reverb");
}
#[test]
fn the_descriptors_parameters_are_applied_on_load() {
let mut plugin = Plugin::new(
"delay".into(),
PathBuf::new(),
512,
48_000,
2,
r#"{ "time_ms": 375, "feedback": 0.6 }"#.into(),
PluginFormat::Internal,
);
plugin.load().expect("a built-in with parameters");
assert_eq!(plugin.params().number("time_ms"), Some(375.0));
}
#[test]
fn bad_parameters_in_the_descriptor_fail_the_load() {
let mut plugin = Plugin::new(
"delay".into(),
PathBuf::new(),
512,
48_000,
2,
"time_ms: 999999".into(),
PluginFormat::Internal,
);
let error = plugin.load().expect_err("999999 ms is outside the range");
assert!(error.to_string().contains("time_ms"), "{error}");
}
#[test]
fn a_plain_function_has_no_parameters_to_set() {
let mut plugin = Plugin::internal("noop", |_: &mut [f32], _| {});
assert!(plugin.params().is_empty());
assert!(plugin.param_schema().is_empty());
assert!(plugin.set_params(&Params::new()).is_ok());
assert!(matches!(
plugin.set_param("anything", 1.0),
Err(ParamError::Unsupported { .. })
));
}
#[test]
fn an_unloaded_plugin_has_nothing_to_set_parameters_on() {
let mut plugin = descriptor("not loaded", PluginFormat::Vst3);
assert!(plugin.params().is_empty());
assert!(matches!(
plugin.set_param("ratio", 4.0),
Err(ParamError::Unsupported { .. })
));
}
#[test]
fn parameters_round_trip_through_a_string() {
let mut original = atome::reverb(48_000);
original
.set_params_str("room_size: 0.8, damping: 0.3, mix: 0.45")
.expect("valid");
let saved = original.params().to_string();
let mut restored = atome::reverb(48_000);
restored.set_params_str(&saved).expect("its own output");
assert_eq!(restored.params(), original.params(), "saved as {saved}");
}
#[test]
fn loading_twice_is_a_no_op() {
internal::register("loaded twice", |_: &mut [f32], _| {});
let mut plugin = descriptor("loaded twice", PluginFormat::Internal);
plugin.load().expect("first");
plugin.load().expect("second");
internal::unregister("loaded twice");
}
#[test]
fn a_declared_latency_reaches_the_engine() {
let lookahead = InternalPlugin::new("lookahead", |_: &mut [f32], _| {}).with_latency(128);
assert_eq!(Plugin::from_internal(lookahead).latency(), 128);
assert_eq!(descriptor("unloaded", PluginFormat::Vst3).latency(), 0);
}
#[test]
fn cloning_keeps_an_internal_plugin_working() {
let plugin = Plugin::internal("shared", |buffer: &mut [f32], _| buffer.fill(0.5));
let mut clone = plugin.clone();
assert!(clone.is_loaded());
let mut buffer = [0.0_f32; 2];
clone.apply(&mut buffer, 2).expect("processed");
assert_eq!(buffer, [0.5, 0.5]);
}
#[test]
fn equality_ignores_whether_a_plugin_is_loaded() {
internal::register("compared", |_: &mut [f32], _| {});
let unloaded = descriptor("compared", PluginFormat::Internal);
let mut loaded = unloaded.clone();
loaded.load().expect("registered");
assert_eq!(unloaded, loaded);
internal::unregister("compared");
}
#[test]
fn a_path_overrides_a_defaulted_internal_format() {
let plugin = Plugin::new(
"Pro-Q".into(),
"/Library/Audio/Plug-Ins/VST3/Pro-Q 3.vst3".into(),
512,
48_000,
2,
String::new(),
PluginFormat::Internal,
);
assert_eq!(plugin.detect_format(), PluginFormat::Vst3);
}
#[test]
fn bundle_suffixes_map_to_their_formats() {
for (path, expected) in [
("/x/Thing.vst3", PluginFormat::Vst3),
("/x/Thing.component", PluginFormat::Au),
("/x/Thing.appex", PluginFormat::Au3),
("/x/Thing", PluginFormat::Internal),
] {
let plugin = Plugin::new(
"Thing".into(),
path.into(),
512,
48_000,
2,
String::new(),
PluginFormat::Internal,
);
assert_eq!(plugin.detect_format(), expected, "{path}");
}
}
#[test]
fn an_explicit_format_is_taken_at_its_word() {
let plugin = Plugin::new(
"Thing".into(),
"/x/Thing.vst3".into(),
512,
48_000,
2,
String::new(),
PluginFormat::Au,
);
assert_eq!(plugin.detect_format(), PluginFormat::Au);
}
#[test]
fn internal_is_available_everywhere() {
assert!(PluginFormat::Internal.is_available());
}
#[test]
fn vst2_is_never_available() {
assert!(!PluginFormat::Vst.is_available());
let mut plugin = descriptor("some vst2", PluginFormat::Vst);
let error = plugin.load().expect_err("no VST2 backend");
assert!(error.to_string().contains("not implemented"));
}
#[test]
fn an_unavailable_format_names_its_feature() {
for format in [PluginFormat::Au, PluginFormat::Au3, PluginFormat::Vst3] {
if format.is_available() {
continue;
}
let mut plugin = descriptor("absent", format);
let error = plugin.load().expect_err("feature is off");
let message = error.to_string();
assert!(
message.contains("feature"),
"{format:?} should say which feature is missing, said: {message}"
);
}
}
}