1pub use crate::client::PluginFormat;
7use crate::client::{default_watchdog, PluginHost};
8use crate::protocol::{Event, ParamValue, PluginInfo};
9use base64::Engine;
10use ferraus_core::{AudioBuffer, Error, ErrorKind, OpCtx, Result};
11use serde::{Deserialize, Serialize};
12use serde_json::{json, Value};
13use std::path::{Path, PathBuf};
14use std::time::Duration;
15
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
18pub struct PluginRef {
19 pub format: PluginFormat,
20 pub path: PathBuf,
22 pub plugin_id: Option<String>,
24 pub name: String,
25 #[serde(default)]
26 pub vendor: String,
27 #[serde(default)]
28 pub version: String,
29}
30
31impl PluginRef {
32 pub fn from_path(path: impl Into<PathBuf>, plugin_id: Option<&str>) -> PluginRef {
34 let path = path.into();
35 let name = path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "plugin".into());
36 PluginRef {
37 format: PluginFormat::from_path(&path).unwrap_or(PluginFormat::Clap),
38 path,
39 plugin_id: plugin_id.map(|s| s.to_string()),
40 name,
41 vendor: String::new(),
42 version: String::new(),
43 }
44 }
45
46 pub fn resolve(query: &str, sound_paths: &[PathBuf]) -> Result<PluginRef> {
50 crate::scan::resolve_plugin(query, sound_paths, default_watchdog())
51 }
52
53 fn label(&self) -> &str {
54 &self.name
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct MidiEvent {
61 pub frame: u64,
62 pub kind: MidiEventKind,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq)]
66pub enum MidiEventKind {
67 NoteOn {
69 channel: u8,
70 key: u8,
71 velocity: f32,
72 },
73 NoteOff {
74 channel: u8,
75 key: u8,
76 velocity: f32,
77 },
78 Raw([u8; 3]),
80}
81
82impl MidiEvent {
83 pub fn note_on(frame: u64, channel: u8, key: u8, velocity: f32) -> MidiEvent {
84 MidiEvent { frame, kind: MidiEventKind::NoteOn { channel, key, velocity } }
85 }
86
87 pub fn note_off(frame: u64, channel: u8, key: u8) -> MidiEvent {
88 MidiEvent { frame, kind: MidiEventKind::NoteOff { channel, key, velocity: 0.0 } }
89 }
90
91 fn to_wire(self) -> (u64, Event) {
92 let e = match self.kind {
93 MidiEventKind::NoteOn { channel, key, velocity } => Event::NoteOn { time: 0, channel, key, velocity },
94 MidiEventKind::NoteOff { channel, key, velocity } => Event::NoteOff { time: 0, channel, key, velocity },
95 MidiEventKind::Raw(data) => Event::Midi { time: 0, data },
96 };
97 (self.frame, e)
98 }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq)]
103pub enum Tail {
104 Auto,
107 Seconds(f64),
108}
109
110#[derive(Clone, Debug)]
112pub struct PluginOptions {
113 pub params: Vec<(String, String)>,
116 pub preset: Option<String>,
118 pub state: Option<Vec<u8>>,
120 pub block_size: u32,
121 pub tail: Tail,
122 pub watchdog: Duration,
123 pub slots: u32,
125 pub progress: Option<fn(f64)>,
127}
128
129impl Default for PluginOptions {
130 fn default() -> Self {
131 PluginOptions {
132 params: Vec::new(),
133 preset: None,
134 state: None,
135 block_size: 512,
136 tail: Tail::Auto,
137 watchdog: default_watchdog(),
138 slots: 4,
139 progress: None,
140 }
141 }
142}
143
144#[derive(Clone, Debug)]
146pub struct PluginRun {
147 pub audio: AudioBuffer,
148 pub info: PluginInfo,
149 pub params: Vec<ParamValue>,
151 pub state: Option<Vec<u8>>,
153 pub latency: u32,
154 pub tail_frames: usize,
155 pub preset: Option<String>,
156}
157
158impl PluginRun {
159 pub fn record(&self, plugin: &PluginRef) -> Value {
161 let params: serde_json::Map<String, Value> = self
162 .params
163 .iter()
164 .map(|p| (p.name.clone(), json!({"id": p.id, "value": p.value, "text": p.text})))
165 .collect();
166 json!({
167 "name": if self.info.descriptor.name.is_empty() { plugin.name.clone() } else { self.info.descriptor.name.clone() },
168 "id": self.info.descriptor.id,
169 "path": plugin.path.display().to_string(),
170 "format": plugin.format.as_str(),
171 "version": self.info.descriptor.version,
172 "vendor": self.info.descriptor.vendor,
173 "params": params,
174 "preset": self.preset,
175 "state_b64": self.state.as_ref().map(|s| base64::engine::general_purpose::STANDARD.encode(s)),
176 "latency_samples": self.latency,
177 "tail_samples": self.tail_frames,
178 })
179 }
180
181 pub fn record_in(&self, plugin: &PluginRef, ctx: &mut OpCtx) {
184 ctx.plugins.push(self.record(plugin));
185 ctx.deterministic = Some(false);
186 }
187}
188
189pub struct PluginSession {
192 pub host: PluginHost,
193 pub info: PluginInfo,
194 pub plugin: PluginRef,
195}
196
197impl PluginSession {
198 pub fn open(plugin: &PluginRef, watchdog: Duration) -> Result<PluginSession> {
199 let mut host = PluginHost::spawn(plugin.format, plugin.label(), &plugin.path, watchdog)?;
200 let info = host.load(plugin.plugin_id.as_deref())?;
201 if !info.descriptor.name.is_empty() {
202 host.set_label(&info.descriptor.name);
203 }
204 Ok(PluginSession { host, info, plugin: plugin.clone() })
205 }
206
207 pub fn configure(&mut self, opts: &PluginOptions) -> Result<Vec<ParamValue>> {
209 if let Some(s) = &opts.state {
210 self.host.set_state(s)?;
211 }
212 if let Some(p) = &opts.preset {
213 self.host.load_preset(p)?;
214 }
215 self.host.set_params(&opts.params)
216 }
217
218 fn capture_state(&mut self) -> Result<Option<Vec<u8>>> {
219 if self.info.has_state {
220 Ok(Some(self.host.get_state()?))
221 } else {
222 Ok(None)
223 }
224 }
225}
226
227fn tail_frames(tail: Tail, reported: Option<u32>, sr: u32, instrument: bool) -> usize {
228 let secs = match tail {
229 Tail::Seconds(s) => s.max(0.0),
230 Tail::Auto => match reported {
231 Some(t) if t != u32::MAX => (t as f64 / sr as f64).min(30.0),
232 Some(_) => 2.0,
233 None if instrument => 0.0,
234 None => 2.0,
235 },
236 };
237 (secs * sr as f64).round() as usize
238}
239
240fn check_sr(sr: u32) -> Result<()> {
241 if !(8000..=768_000).contains(&sr) {
242 return Err(Error::usage(format!("sample rate {sr} Hz is out of range")));
243 }
244 Ok(())
245}
246
247fn validate_block(opts: &PluginOptions) -> Result<()> {
248 if !(16..=16384).contains(&opts.block_size) {
249 return Err(Error::usage(format!("block size {} is out of range (16..16384)", opts.block_size)));
250 }
251 Ok(())
252}
253
254pub fn render_instrument_with(
257 plugin: &PluginRef,
258 midi_events: &[MidiEvent],
259 sample_rate: u32,
260 duration: f64,
261 opts: &PluginOptions,
262) -> Result<PluginRun> {
263 check_sr(sample_rate)?;
264 validate_block(opts)?;
265 if !(duration.is_finite() && duration >= 0.0) {
266 return Err(Error::usage("render duration must be a positive number of seconds"));
267 }
268 let mut s = PluginSession::open(plugin, opts.watchdog)?;
269 let params = s.configure(opts)?;
270 let state = s.capture_state()?;
271 let (ci, co) = (s.info.main_in_channels, s.info.main_out_channels);
272 if co == 0 {
273 return Err(Error::new(
274 ErrorKind::Plugin,
275 "plugin_no_output",
276 format!("plugin '{}' has no audio output", s.plugin.name),
277 ));
278 }
279 let act = s.host.activate(sample_rate, opts.block_size, ci, co, opts.slots)?;
280 let latency = act.latency as usize;
281 let dur = (duration * sample_rate as f64).round() as usize;
282 let tail = tail_frames(opts.tail, act.tail, sample_rate, true);
283 let total = dur + tail + latency;
284 let events: Vec<(u64, Event)> = midi_events.iter().map(|e| e.to_wire()).collect();
285 let progress = opts.progress;
286 let out = s.host.run(&[], total, &events, &mut |f| {
287 if let Some(p) = progress {
288 p(f)
289 }
290 Ok(())
291 })?;
292 let data: Vec<Vec<f32>> = out.into_iter().map(|c| c[latency..].to_vec()).collect();
293 let info = s.info.clone();
294 s.host.shutdown();
295 Ok(PluginRun {
296 audio: AudioBuffer::from_planar(sample_rate, data),
297 info,
298 params,
299 state,
300 latency: act.latency,
301 tail_frames: tail,
302 preset: opts.preset.clone(),
303 })
304}
305
306pub fn render_instrument(
309 plugin: &PluginRef,
310 midi_events: &[MidiEvent],
311 sample_rate: u32,
312 duration: f64,
313 params: &[(String, String)],
314 state: Option<&[u8]>,
315) -> Result<AudioBuffer> {
316 let opts = PluginOptions { params: params.to_vec(), state: state.map(|s| s.to_vec()), ..Default::default() };
317 Ok(render_instrument_with(plugin, midi_events, sample_rate, duration, &opts)?.audio)
318}
319
320pub fn process_effect_with(plugin: &PluginRef, input: &AudioBuffer, opts: &PluginOptions) -> Result<PluginRun> {
323 process_effect_inner(plugin, input, opts, &mut |_| Ok(()))
324}
325
326pub(crate) fn process_effect_inner(
327 plugin: &PluginRef,
328 input: &AudioBuffer,
329 opts: &PluginOptions,
330 progress: &mut dyn FnMut(f64) -> Result<()>,
331) -> Result<PluginRun> {
332 check_sr(input.sample_rate)?;
333 validate_block(opts)?;
334 let mut s = PluginSession::open(plugin, opts.watchdog)?;
335 let params = s.configure(opts)?;
336 let state = s.capture_state()?;
337 let (ci, co) = (s.info.main_in_channels, s.info.main_out_channels);
338 if ci == 0 || co == 0 {
339 return Err(Error::new(
340 ErrorKind::Usage,
341 "plugin_not_effect",
342 format!(
343 "plugin '{}' has no audio {} (is it an instrument?)",
344 s.plugin.name,
345 if ci == 0 { "input" } else { "output" }
346 ),
347 )
348 .with_hint("render instruments with: ferraus render <file.mid> --instrument plugin:<name>"));
349 }
350 let act = s.host.activate(input.sample_rate, opts.block_size, ci, co, opts.slots)?;
351 let latency = act.latency as usize;
352 let tail = tail_frames(opts.tail, act.tail, input.sample_rate, false);
353 let adapted = input.with_channels(ci as usize);
354 let total = input.frames() + tail + latency;
355 let user_progress = opts.progress;
356 let out = s.host.run(&adapted.data, total, &[], &mut |f| {
357 if let Some(p) = user_progress {
358 p(f)
359 }
360 progress(f)
361 })?;
362 let data: Vec<Vec<f32>> = out.into_iter().map(|c| c[latency..].to_vec()).collect();
363 let info = s.info.clone();
364 s.host.shutdown();
365 Ok(PluginRun {
366 audio: AudioBuffer::from_planar(input.sample_rate, data),
367 info,
368 params,
369 state,
370 latency: act.latency,
371 tail_frames: tail,
372 preset: opts.preset.clone(),
373 })
374}
375
376pub fn process_effect(
379 plugin: &PluginRef,
380 input: &AudioBuffer,
381 params: &[(String, String)],
382 state: Option<&[u8]>,
383 tail_s: f64,
384) -> Result<AudioBuffer> {
385 let opts = PluginOptions {
386 params: params.to_vec(),
387 state: state.map(|s| s.to_vec()),
388 tail: Tail::Seconds(tail_s),
389 ..Default::default()
390 };
391 Ok(process_effect_with(plugin, input, &opts)?.audio)
392}
393
394pub fn read_state_file(path: &Path) -> Result<Vec<u8>> {
397 let bytes = std::fs::read(path).map_err(|e| {
398 Error::new(
399 ErrorKind::InputMissing,
400 "input_not_found",
401 format!("cannot read state file {}: {e}", path.display()),
402 )
403 })?;
404 if let Ok(v) = serde_json::from_slice::<Value>(&bytes) {
405 let b64 = v
406 .get("state_b64")
407 .or_else(|| v.pointer("/plugins/0/state_b64"))
408 .and_then(|s| s.as_str())
409 .ok_or_else(|| Error::usage(format!("{} is JSON but has no state_b64 field", path.display())))?;
410 return base64::engine::general_purpose::STANDARD
411 .decode(b64.trim())
412 .map_err(|e| Error::usage(format!("{}: state_b64 is not base64: {e}", path.display())));
413 }
414 Ok(bytes)
415}