use crate::graph::Processor;
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Category {
Source,
Effect,
Mixer,
Dynamics,
Filter,
Modulator,
Utility,
}
impl Category {
pub fn label(self) -> &'static str {
match self {
Category::Source => "Source",
Category::Effect => "Effect",
Category::Mixer => "Mixer",
Category::Dynamics => "Dynamics",
Category::Filter => "Filter",
Category::Modulator => "Modulator",
Category::Utility => "Utility",
}
}
}
pub struct ProcessorEntry {
pub tag: &'static str,
pub name: &'static str,
pub category: Category,
factory: Box<dyn Fn() -> Box<dyn Processor> + Send + Sync>,
}
impl ProcessorEntry {
pub fn create(&self) -> Box<dyn Processor> {
(self.factory)()
}
}
pub struct Registry {
entries: HashMap<&'static str, ProcessorEntry>,
by_category: HashMap<Category, Vec<&'static str>>,
}
impl Registry {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
by_category: HashMap::new(),
}
}
pub fn register<F>(&mut self, tag: &'static str, name: &'static str, category: Category, f: F)
where
F: Fn() -> Box<dyn Processor> + Send + Sync + 'static,
{
self.by_category.entry(category).or_default().push(tag);
self.entries.insert(
tag,
ProcessorEntry {
tag,
name,
category,
factory: Box::new(f),
},
);
}
pub fn create(&self, tag: &str) -> Option<Box<dyn Processor>> {
self.entries.get(tag).map(|e| e.create())
}
pub fn get(&self, tag: &str) -> Option<&ProcessorEntry> {
self.entries.get(tag)
}
pub fn tags(&self) -> Vec<&'static str> {
let mut tags: Vec<_> = self.entries.keys().copied().collect();
tags.sort();
tags
}
pub fn tags_in(&self, category: Category) -> &[&'static str] {
self.by_category
.get(&category)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn categories(&self) -> Vec<Category> {
let mut cats: Vec<_> = self.by_category.keys().copied().collect();
cats.sort_by_key(|c| c.label());
cats
}
pub fn standard() -> Self {
use crate::dsp;
let mut r = Self::new();
r.register("osc", "Oscillator", Category::Source, || {
Box::new(dsp::Oscillator::new(dsp::Waveform::Sine))
});
r.register("noi", "Noise", Category::Source, || {
Box::new(dsp::Noise::new())
});
r.register("kick", "Kick Drum", Category::Source, || {
Box::new(dsp::KickSynth::new(0))
});
r.register("snr", "Snare Drum", Category::Source, || {
Box::new(dsp::SnareSynth::new(0))
});
r.register("hat", "Hi-Hat", Category::Source, || {
Box::new(dsp::HatSynth::new(0))
});
r.register("syn", "Analog Voice", Category::Source, || {
Box::new(dsp::analog_voice(0, 64))
});
r.register("ldv", "Lead Voice", Category::Source, || {
Box::new(dsp::lead_voice(0, 64))
});
r.register("dly", "Stereo Delay", Category::Effect, || {
Box::new(dsp::StereoDelay::new(250.0, 0.4, 0.3))
});
r.register("dst", "Distortion", Category::Effect, || {
Box::new(dsp::Distortion::new())
});
r.register("vrb", "Plate Reverb", Category::Effect, || {
Box::new(dsp::PlateReverb::new(0.5, 0.5, 0.3))
});
r.register("peq", "Parametric EQ", Category::Effect, || {
Box::new(dsp::ParametricEq::new())
});
r.register("lpf", "Low-Pass Filter", Category::Filter, || {
Box::new(dsp::BiquadFilter::new(
dsp::FilterType::LowPass,
1000.0,
0.707,
))
});
r.register("hpf", "High-Pass Filter", Category::Filter, || {
Box::new(dsp::BiquadFilter::new(
dsp::FilterType::HighPass,
200.0,
0.707,
))
});
r.register("bpf", "Band-Pass Filter", Category::Filter, || {
Box::new(dsp::BiquadFilter::new(
dsp::FilterType::BandPass,
1000.0,
1.0,
))
});
r.register("env", "ADSR Envelope", Category::Modulator, || {
Box::new(dsp::Adsr::new(0.01, 0.1, 0.7, 0.3))
});
r.register("geq", "Graphic EQ", Category::Filter, || {
Box::new(dsp::GraphicEq::new())
});
r.register("wav", "Wavetable", Category::Source, || {
Box::new(dsp::Wavetable::new())
});
r.register("lfo", "LFO", Category::Modulator, || {
Box::new(dsp::Lfo::new(1.0))
});
r.register("lim", "Limiter", Category::Dynamics, || {
Box::new(dsp::Limiter::new(-0.3, 100.0))
});
r.register("com", "Compressor", Category::Dynamics, || {
Box::new(dsp::Compressor::new(-18.0, 4.0, 10.0, 150.0))
});
r.register("vol", "Volume", Category::Utility, || {
Box::new(dsp::StereoGain::new(1.0))
});
r.register("pan", "Stereo Pan", Category::Utility, || {
Box::new(dsp::StereoPan::new(0.0))
});
r.register("gain", "Mono Gain", Category::Utility, || {
Box::new(dsp::MonoGain::new(1.0))
});
r.register("mix", "Stereo Mixer", Category::Mixer, || {
Box::new(dsp::StereoMixer::new(2))
});
r.register("xfade", "Mono Crossfade", Category::Mixer, || {
Box::new(dsp::MonoCrossfade::new(0.5))
});
r
}
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_registry_has_all_tags() {
let reg = Registry::standard();
let expected = [
"osc", "noi", "kick", "snr", "hat", "syn", "ldv", "dly", "dst", "vrb", "peq", "geq",
"lpf", "hpf", "bpf", "env", "wav", "lfo", "lim", "com", "vol", "pan", "gain", "mix",
"xfade",
];
for tag in &expected {
assert!(reg.get(tag).is_some(), "missing tag: {tag}");
}
assert_eq!(reg.tags().len(), expected.len());
}
#[test]
fn create_returns_processor() {
let reg = Registry::standard();
let osc = reg.create("osc").unwrap();
assert!(!osc.info().name.is_empty());
assert!(osc.info().sig.is_source());
}
#[test]
fn create_unknown_returns_none() {
let reg = Registry::standard();
assert!(reg.create("xyz").is_none());
}
#[test]
fn categories_are_populated() {
let reg = Registry::standard();
assert!(!reg.tags_in(Category::Source).is_empty());
assert!(!reg.tags_in(Category::Effect).is_empty());
assert!(!reg.tags_in(Category::Filter).is_empty());
assert!(!reg.tags_in(Category::Utility).is_empty());
assert!(!reg.tags_in(Category::Mixer).is_empty());
assert!(!reg.tags_in(Category::Modulator).is_empty());
}
#[test]
fn all_processors_construct_without_panic() {
let reg = Registry::standard();
for tag in reg.tags() {
let proc = reg.create(tag).unwrap();
let info = proc.info();
assert!(!info.name.is_empty(), "empty name for tag: {tag}");
assert!(
info.sig.outputs > 0 || info.sig.inputs > 0,
"zero I/O for {tag}"
);
}
}
#[test]
fn created_processor_params_are_valid() {
let reg = Registry::standard();
for tag in reg.tags() {
let proc = reg.create(tag).unwrap();
for p in proc.params() {
assert!(p.min <= p.default, "{tag}/{}: min > default", p.name);
assert!(p.default <= p.max, "{tag}/{}: default > max", p.name);
}
}
}
}