Skip to main content

phosphor_plugin/
lib.rs

1//! Plugin API definitions for Phosphor.
2//!
3//! This crate defines the trait that all plugins (instruments, effects,
4//! analyzers) must implement. Built-in DSP and third-party plugins
5//! use the same interface — no special casing.
6
7use std::fmt;
8
9/// Plugin category — determines where it appears in the UI and how it's routed.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(C)]
12pub enum PluginCategory {
13    /// Generates audio from MIDI input (synths, samplers).
14    Instrument,
15    /// Processes audio (filters, delays, reverbs, compressors).
16    Effect,
17    /// Reads audio for display (spectrum analyzer, oscilloscope).
18    Analyzer,
19    /// Utility (gain, panner, test tone generator).
20    Utility,
21}
22
23impl fmt::Display for PluginCategory {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::Instrument => write!(f, "Instrument"),
27            Self::Effect => write!(f, "Effect"),
28            Self::Analyzer => write!(f, "Analyzer"),
29            Self::Utility => write!(f, "Utility"),
30        }
31    }
32}
33
34/// Metadata about a plugin.
35#[derive(Debug, Clone)]
36pub struct PluginInfo {
37    pub name: String,
38    pub version: String,
39    pub author: String,
40    pub category: PluginCategory,
41}
42
43/// A MIDI event with a sample-accurate offset within the current buffer.
44#[derive(Debug, Clone, Copy)]
45pub struct MidiEvent {
46    /// Sample offset within the buffer (0 = start of buffer).
47    pub sample_offset: u32,
48    /// MIDI status byte.
49    pub status: u8,
50    /// First data byte.
51    pub data1: u8,
52    /// Second data byte.
53    pub data2: u8,
54}
55
56/// Parameter descriptor for plugin parameters.
57#[derive(Debug, Clone)]
58pub struct ParameterInfo {
59    pub name: String,
60    pub min: f32,
61    pub max: f32,
62    pub default: f32,
63    pub unit: String,
64}
65
66/// The core plugin trait. Every synth, effect, and utility implements this.
67pub trait Plugin: Send {
68    /// Plugin metadata.
69    fn info(&self) -> PluginInfo;
70
71    /// Called once when the plugin is loaded. Preallocate everything here.
72    fn init(&mut self, sample_rate: f64, max_buffer_size: usize);
73
74    /// Process audio. Called from the audio thread — must be real-time safe.
75    ///
76    /// - `inputs`: input audio buffers (one slice per channel). Empty for instruments.
77    /// - `outputs`: output audio buffers to write into (one slice per channel).
78    /// - `midi_events`: MIDI events for this buffer, sorted by `sample_offset`.
79    fn process(
80        &mut self,
81        inputs: &[&[f32]],
82        outputs: &mut [&mut [f32]],
83        midi_events: &[MidiEvent],
84    );
85
86    /// Number of parameters this plugin exposes.
87    fn parameter_count(&self) -> usize;
88
89    /// Get info about a parameter.
90    fn parameter_info(&self, index: usize) -> Option<ParameterInfo>;
91
92    /// Get current parameter value (0.0..1.0 normalized).
93    fn get_parameter(&self, index: usize) -> f32;
94
95    /// Set parameter value. Clamped to 0.0..1.0.
96    fn set_parameter(&mut self, index: usize, value: f32);
97
98    /// Reset internal state (clear delay lines, reset envelopes, etc).
99    fn reset(&mut self);
100}
101
102/// Clamp a parameter value to the valid range.
103#[inline]
104pub fn clamp_parameter(value: f32) -> f32 {
105    value.clamp(0.0, 1.0)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn clamp_parameter_bounds() {
114        assert_eq!(clamp_parameter(0.5), 0.5);
115        assert_eq!(clamp_parameter(-1.0), 0.0);
116        assert_eq!(clamp_parameter(2.0), 1.0);
117        assert_eq!(clamp_parameter(0.0), 0.0);
118        assert_eq!(clamp_parameter(1.0), 1.0);
119    }
120
121    #[test]
122    fn clamp_parameter_nan_handling() {
123        // NaN.clamp returns NaN in Rust — we should be aware of this
124        let result = clamp_parameter(f32::NAN);
125        assert!(result.is_nan(), "NaN input produces NaN — callers must validate");
126    }
127
128    #[test]
129    fn plugin_category_display() {
130        assert_eq!(format!("{}", PluginCategory::Instrument), "Instrument");
131        assert_eq!(format!("{}", PluginCategory::Effect), "Effect");
132    }
133
134    #[test]
135    fn midi_event_is_copy() {
136        let event = MidiEvent {
137            sample_offset: 0,
138            status: 0x90,
139            data1: 60,
140            data2: 100,
141        };
142        let copy = event;
143        assert_eq!(copy.status, event.status);
144    }
145}