pub use crate::client::PluginFormat;
use crate::client::{default_watchdog, PluginHost};
use crate::protocol::{Event, ParamValue, PluginInfo};
use base64::Engine;
use ferraus_core::{AudioBuffer, Error, ErrorKind, OpCtx, Result};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PluginRef {
pub format: PluginFormat,
pub path: PathBuf,
pub plugin_id: Option<String>,
pub name: String,
#[serde(default)]
pub vendor: String,
#[serde(default)]
pub version: String,
}
impl PluginRef {
pub fn from_path(path: impl Into<PathBuf>, plugin_id: Option<&str>) -> PluginRef {
let path = path.into();
let name = path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "plugin".into());
PluginRef {
format: PluginFormat::from_path(&path).unwrap_or(PluginFormat::Clap),
path,
plugin_id: plugin_id.map(|s| s.to_string()),
name,
vendor: String::new(),
version: String::new(),
}
}
pub fn resolve(query: &str, sound_paths: &[PathBuf]) -> Result<PluginRef> {
crate::scan::resolve_plugin(query, sound_paths, default_watchdog())
}
fn label(&self) -> &str {
&self.name
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MidiEvent {
pub frame: u64,
pub kind: MidiEventKind,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MidiEventKind {
NoteOn {
channel: u8,
key: u8,
velocity: f32,
},
NoteOff {
channel: u8,
key: u8,
velocity: f32,
},
Raw([u8; 3]),
}
impl MidiEvent {
pub fn note_on(frame: u64, channel: u8, key: u8, velocity: f32) -> MidiEvent {
MidiEvent { frame, kind: MidiEventKind::NoteOn { channel, key, velocity } }
}
pub fn note_off(frame: u64, channel: u8, key: u8) -> MidiEvent {
MidiEvent { frame, kind: MidiEventKind::NoteOff { channel, key, velocity: 0.0 } }
}
fn to_wire(self) -> (u64, Event) {
let e = match self.kind {
MidiEventKind::NoteOn { channel, key, velocity } => Event::NoteOn { time: 0, channel, key, velocity },
MidiEventKind::NoteOff { channel, key, velocity } => Event::NoteOff { time: 0, channel, key, velocity },
MidiEventKind::Raw(data) => Event::Midi { time: 0, data },
};
(self.frame, e)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Tail {
Auto,
Seconds(f64),
}
#[derive(Clone, Debug)]
pub struct PluginOptions {
pub params: Vec<(String, String)>,
pub preset: Option<String>,
pub state: Option<Vec<u8>>,
pub block_size: u32,
pub tail: Tail,
pub watchdog: Duration,
pub slots: u32,
pub progress: Option<fn(f64)>,
}
impl Default for PluginOptions {
fn default() -> Self {
PluginOptions {
params: Vec::new(),
preset: None,
state: None,
block_size: 512,
tail: Tail::Auto,
watchdog: default_watchdog(),
slots: 4,
progress: None,
}
}
}
#[derive(Clone, Debug)]
pub struct PluginRun {
pub audio: AudioBuffer,
pub info: PluginInfo,
pub params: Vec<ParamValue>,
pub state: Option<Vec<u8>>,
pub latency: u32,
pub tail_frames: usize,
pub preset: Option<String>,
}
impl PluginRun {
pub fn record(&self, plugin: &PluginRef) -> Value {
let params: serde_json::Map<String, Value> = self
.params
.iter()
.map(|p| (p.name.clone(), json!({"id": p.id, "value": p.value, "text": p.text})))
.collect();
json!({
"name": if self.info.descriptor.name.is_empty() { plugin.name.clone() } else { self.info.descriptor.name.clone() },
"id": self.info.descriptor.id,
"path": plugin.path.display().to_string(),
"format": plugin.format.as_str(),
"version": self.info.descriptor.version,
"vendor": self.info.descriptor.vendor,
"params": params,
"preset": self.preset,
"state_b64": self.state.as_ref().map(|s| base64::engine::general_purpose::STANDARD.encode(s)),
"latency_samples": self.latency,
"tail_samples": self.tail_frames,
})
}
pub fn record_in(&self, plugin: &PluginRef, ctx: &mut OpCtx) {
ctx.plugins.push(self.record(plugin));
ctx.deterministic = Some(false);
}
}
pub struct PluginSession {
pub host: PluginHost,
pub info: PluginInfo,
pub plugin: PluginRef,
}
impl PluginSession {
pub fn open(plugin: &PluginRef, watchdog: Duration) -> Result<PluginSession> {
let mut host = PluginHost::spawn(plugin.format, plugin.label(), &plugin.path, watchdog)?;
let info = host.load(plugin.plugin_id.as_deref())?;
if !info.descriptor.name.is_empty() {
host.set_label(&info.descriptor.name);
}
Ok(PluginSession { host, info, plugin: plugin.clone() })
}
pub fn configure(&mut self, opts: &PluginOptions) -> Result<Vec<ParamValue>> {
if let Some(s) = &opts.state {
self.host.set_state(s)?;
}
if let Some(p) = &opts.preset {
self.host.load_preset(p)?;
}
self.host.set_params(&opts.params)
}
fn capture_state(&mut self) -> Result<Option<Vec<u8>>> {
if self.info.has_state {
Ok(Some(self.host.get_state()?))
} else {
Ok(None)
}
}
}
fn tail_frames(tail: Tail, reported: Option<u32>, sr: u32, instrument: bool) -> usize {
let secs = match tail {
Tail::Seconds(s) => s.max(0.0),
Tail::Auto => match reported {
Some(t) if t != u32::MAX => (t as f64 / sr as f64).min(30.0),
Some(_) => 2.0,
None if instrument => 0.0,
None => 2.0,
},
};
(secs * sr as f64).round() as usize
}
fn check_sr(sr: u32) -> Result<()> {
if !(8000..=768_000).contains(&sr) {
return Err(Error::usage(format!("sample rate {sr} Hz is out of range")));
}
Ok(())
}
fn validate_block(opts: &PluginOptions) -> Result<()> {
if !(16..=16384).contains(&opts.block_size) {
return Err(Error::usage(format!("block size {} is out of range (16..16384)", opts.block_size)));
}
Ok(())
}
pub fn render_instrument_with(
plugin: &PluginRef,
midi_events: &[MidiEvent],
sample_rate: u32,
duration: f64,
opts: &PluginOptions,
) -> Result<PluginRun> {
check_sr(sample_rate)?;
validate_block(opts)?;
if !(duration.is_finite() && duration >= 0.0) {
return Err(Error::usage("render duration must be a positive number of seconds"));
}
let mut s = PluginSession::open(plugin, opts.watchdog)?;
let params = s.configure(opts)?;
let state = s.capture_state()?;
let (ci, co) = (s.info.main_in_channels, s.info.main_out_channels);
if co == 0 {
return Err(Error::new(
ErrorKind::Plugin,
"plugin_no_output",
format!("plugin '{}' has no audio output", s.plugin.name),
));
}
let act = s.host.activate(sample_rate, opts.block_size, ci, co, opts.slots)?;
let latency = act.latency as usize;
let dur = (duration * sample_rate as f64).round() as usize;
let tail = tail_frames(opts.tail, act.tail, sample_rate, true);
let total = dur + tail + latency;
let events: Vec<(u64, Event)> = midi_events.iter().map(|e| e.to_wire()).collect();
let progress = opts.progress;
let out = s.host.run(&[], total, &events, &mut |f| {
if let Some(p) = progress {
p(f)
}
Ok(())
})?;
let data: Vec<Vec<f32>> = out.into_iter().map(|c| c[latency..].to_vec()).collect();
let info = s.info.clone();
s.host.shutdown();
Ok(PluginRun {
audio: AudioBuffer::from_planar(sample_rate, data),
info,
params,
state,
latency: act.latency,
tail_frames: tail,
preset: opts.preset.clone(),
})
}
pub fn render_instrument(
plugin: &PluginRef,
midi_events: &[MidiEvent],
sample_rate: u32,
duration: f64,
params: &[(String, String)],
state: Option<&[u8]>,
) -> Result<AudioBuffer> {
let opts = PluginOptions { params: params.to_vec(), state: state.map(|s| s.to_vec()), ..Default::default() };
Ok(render_instrument_with(plugin, midi_events, sample_rate, duration, &opts)?.audio)
}
pub fn process_effect_with(plugin: &PluginRef, input: &AudioBuffer, opts: &PluginOptions) -> Result<PluginRun> {
process_effect_inner(plugin, input, opts, &mut |_| Ok(()))
}
pub(crate) fn process_effect_inner(
plugin: &PluginRef,
input: &AudioBuffer,
opts: &PluginOptions,
progress: &mut dyn FnMut(f64) -> Result<()>,
) -> Result<PluginRun> {
check_sr(input.sample_rate)?;
validate_block(opts)?;
let mut s = PluginSession::open(plugin, opts.watchdog)?;
let params = s.configure(opts)?;
let state = s.capture_state()?;
let (ci, co) = (s.info.main_in_channels, s.info.main_out_channels);
if ci == 0 || co == 0 {
return Err(Error::new(
ErrorKind::Usage,
"plugin_not_effect",
format!(
"plugin '{}' has no audio {} (is it an instrument?)",
s.plugin.name,
if ci == 0 { "input" } else { "output" }
),
)
.with_hint("render instruments with: ferraus render <file.mid> --instrument plugin:<name>"));
}
let act = s.host.activate(input.sample_rate, opts.block_size, ci, co, opts.slots)?;
let latency = act.latency as usize;
let tail = tail_frames(opts.tail, act.tail, input.sample_rate, false);
let adapted = input.with_channels(ci as usize);
let total = input.frames() + tail + latency;
let user_progress = opts.progress;
let out = s.host.run(&adapted.data, total, &[], &mut |f| {
if let Some(p) = user_progress {
p(f)
}
progress(f)
})?;
let data: Vec<Vec<f32>> = out.into_iter().map(|c| c[latency..].to_vec()).collect();
let info = s.info.clone();
s.host.shutdown();
Ok(PluginRun {
audio: AudioBuffer::from_planar(input.sample_rate, data),
info,
params,
state,
latency: act.latency,
tail_frames: tail,
preset: opts.preset.clone(),
})
}
pub fn process_effect(
plugin: &PluginRef,
input: &AudioBuffer,
params: &[(String, String)],
state: Option<&[u8]>,
tail_s: f64,
) -> Result<AudioBuffer> {
let opts = PluginOptions {
params: params.to_vec(),
state: state.map(|s| s.to_vec()),
tail: Tail::Seconds(tail_s),
..Default::default()
};
Ok(process_effect_with(plugin, input, &opts)?.audio)
}
pub fn read_state_file(path: &Path) -> Result<Vec<u8>> {
let bytes = std::fs::read(path).map_err(|e| {
Error::new(
ErrorKind::InputMissing,
"input_not_found",
format!("cannot read state file {}: {e}", path.display()),
)
})?;
if let Ok(v) = serde_json::from_slice::<Value>(&bytes) {
let b64 = v
.get("state_b64")
.or_else(|| v.pointer("/plugins/0/state_b64"))
.and_then(|s| s.as_str())
.ok_or_else(|| Error::usage(format!("{} is JSON but has no state_b64 field", path.display())))?;
return base64::engine::general_purpose::STANDARD
.decode(b64.trim())
.map_err(|e| Error::usage(format!("{}: state_b64 is not base64: {e}", path.display())));
}
Ok(bytes)
}