Add audio nodes to the patch bay
Also make the patch bay responsive.
This commit is contained in:
70
crates/synth-core/src/descriptor.rs
Normal file
70
crates/synth-core/src/descriptor.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Compile-time metadata descriptors for synth-core DSP components.
|
||||
//!
|
||||
//! Each component exposes a `DESCRIPTOR` constant that describes its display
|
||||
//! name, jack ports, and adjustable parameters. The synth-visualiser reads
|
||||
//! these descriptors to build the patch-bay UI, ensuring the visual
|
||||
//! representation stays in sync with the actual DSP API without any
|
||||
//! duplication.
|
||||
//!
|
||||
//! All fields use `'static` references so the data lives in read-only memory
|
||||
//! with zero run-time cost on both embedded and WASM targets.
|
||||
|
||||
/// Which direction a signal flows through a jack.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Input,
|
||||
Output,
|
||||
}
|
||||
|
||||
/// The kind of signal a jack carries.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SignalKind {
|
||||
/// Audio-rate signal (e.g. VCO output, filter output).
|
||||
Audio,
|
||||
/// Control-voltage signal (e.g. LFO output, envelope output, pitch CV).
|
||||
Cv,
|
||||
/// Boolean gate signal (e.g. ADSR gate input).
|
||||
Gate,
|
||||
}
|
||||
|
||||
/// Metadata for a single jack (input or output port) on a component.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct JackDescriptor {
|
||||
/// Short machine-readable routing key (e.g. `"cv_in"`).
|
||||
pub id: &'static str,
|
||||
/// Human-readable label shown next to the jack in the patch bay.
|
||||
pub label: &'static str,
|
||||
pub direction: Direction,
|
||||
pub signal: SignalKind,
|
||||
}
|
||||
|
||||
/// Metadata for a single adjustable parameter on a component.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ParamDescriptor {
|
||||
/// Short machine-readable identifier (e.g. `"freq_hz"`).
|
||||
pub id: &'static str,
|
||||
/// Human-readable label shown in the patch bay.
|
||||
pub label: &'static str,
|
||||
pub min: f32,
|
||||
pub max: f32,
|
||||
pub default: f32,
|
||||
/// Display unit string (e.g. `"Hz"`, `"s"`, `""`).
|
||||
pub unit: &'static str,
|
||||
}
|
||||
|
||||
/// Full compile-time descriptor for a DSP component type.
|
||||
///
|
||||
/// Stored as a `const` associated item on each component struct so that the
|
||||
/// patch-bay can query `Vco::DESCRIPTOR`, `Svf::DESCRIPTOR`, etc. without any
|
||||
/// runtime overhead.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ComponentDescriptor {
|
||||
/// Short type identifier used as a routing and serialisation key (e.g. `"vco"`).
|
||||
pub kind: &'static str,
|
||||
/// Human-readable component name shown in the palette (e.g. `"VCO"`).
|
||||
pub label: &'static str,
|
||||
/// Ordered list of jacks.
|
||||
pub jacks: &'static [JackDescriptor],
|
||||
/// Ordered list of tunable parameters.
|
||||
pub params: &'static [ParamDescriptor],
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! ADSR envelope generator.
|
||||
|
||||
use crate::{AudioProcessor, config::SampleRate};
|
||||
use crate::descriptor::{ComponentDescriptor, Direction, JackDescriptor, ParamDescriptor, SignalKind};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Stage { Idle, Attack, Decay, Sustain, Release }
|
||||
@@ -25,6 +26,21 @@ impl Adsr {
|
||||
}
|
||||
}
|
||||
|
||||
pub const DESCRIPTOR: ComponentDescriptor = ComponentDescriptor {
|
||||
kind: "adsr",
|
||||
label: "ADSR",
|
||||
jacks: &[
|
||||
JackDescriptor { id: "gate_in", label: "Gate", direction: Direction::Input, signal: SignalKind::Gate },
|
||||
JackDescriptor { id: "env_out", label: "Env", direction: Direction::Output, signal: SignalKind::Cv },
|
||||
],
|
||||
params: &[
|
||||
ParamDescriptor { id: "attack_s", label: "Attack", min: 0.001, max: 4.0, default: 0.01, unit: "s" },
|
||||
ParamDescriptor { id: "decay_s", label: "Decay", min: 0.001, max: 4.0, default: 0.1, unit: "s" },
|
||||
ParamDescriptor { id: "sustain", label: "Sustain", min: 0.0, max: 1.0, default: 0.7, unit: "" },
|
||||
ParamDescriptor { id: "release_s", label: "Release", min: 0.001, max: 8.0, default: 0.3, unit: "s" },
|
||||
],
|
||||
};
|
||||
|
||||
pub fn gate_on(&mut self) { self.stage = Stage::Attack; }
|
||||
pub fn gate_off(&mut self) { self.stage = Stage::Release; }
|
||||
pub fn is_idle(&self) -> bool { self.stage == Stage::Idle }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! - `Svf` — State-variable filter (LP / HP / BP / Notch)
|
||||
|
||||
use crate::{AudioProcessor, CVProcessor, config::SampleRate};
|
||||
use crate::descriptor::{ComponentDescriptor, Direction, JackDescriptor, ParamDescriptor, SignalKind};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FilterMode {
|
||||
@@ -30,6 +31,21 @@ impl Svf {
|
||||
Self { cutoff_hz, resonance, mode, sample_rate, low: 0.0, band: 0.0 }
|
||||
}
|
||||
|
||||
pub const DESCRIPTOR: ComponentDescriptor = ComponentDescriptor {
|
||||
kind: "svf",
|
||||
label: "Filter",
|
||||
jacks: &[
|
||||
JackDescriptor { id: "audio_in", label: "In", direction: Direction::Input, signal: SignalKind::Audio },
|
||||
JackDescriptor { id: "cv_in", label: "CV", direction: Direction::Input, signal: SignalKind::Cv },
|
||||
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
||||
],
|
||||
params: &[
|
||||
ParamDescriptor { id: "cutoff_hz", label: "Cutoff", min: 20.0, max: 20_000.0, default: 2_000.0, unit: "Hz" },
|
||||
ParamDescriptor { id: "resonance", label: "Res", min: 0.0, max: 1.0, default: 0.5, unit: "" },
|
||||
ParamDescriptor { id: "mode", label: "Mode", min: 0.0, max: 3.0, default: 0.0, unit: "" },
|
||||
],
|
||||
};
|
||||
|
||||
#[inline]
|
||||
fn process_sample(&mut self, input: f32) -> f32 {
|
||||
let f = 2.0 * libm::sinf(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! in the range –1.0 to +1.0.
|
||||
|
||||
use crate::{AudioProcessor, config::SampleRate, oscillator::Waveform};
|
||||
use crate::descriptor::{ComponentDescriptor, Direction, JackDescriptor, ParamDescriptor, SignalKind};
|
||||
|
||||
pub struct Lfo {
|
||||
pub waveform: Waveform,
|
||||
@@ -19,6 +20,19 @@ impl Lfo {
|
||||
Self { waveform, rate_hz, depth, phase: 0.0, sample_rate }
|
||||
}
|
||||
|
||||
pub const DESCRIPTOR: ComponentDescriptor = ComponentDescriptor {
|
||||
kind: "lfo",
|
||||
label: "LFO",
|
||||
jacks: &[
|
||||
JackDescriptor { id: "cv_out", label: "Out", direction: Direction::Output, signal: SignalKind::Cv },
|
||||
],
|
||||
params: &[
|
||||
ParamDescriptor { id: "rate_hz", label: "Rate", min: 0.01, max: 20.0, default: 2.0, unit: "Hz" },
|
||||
ParamDescriptor { id: "depth", label: "Depth", min: 0.0, max: 1.0, default: 0.5, unit: "" },
|
||||
ParamDescriptor { id: "waveform", label: "Wave", min: 0.0, max: 4.0, default: 0.0, unit: "" },
|
||||
],
|
||||
};
|
||||
|
||||
#[inline]
|
||||
fn next_sample(&mut self) -> f32 {
|
||||
let p = self.phase;
|
||||
|
||||
@@ -18,6 +18,7 @@ extern crate libm;
|
||||
extern crate alloc;
|
||||
|
||||
pub mod config;
|
||||
pub mod descriptor;
|
||||
pub mod math;
|
||||
pub mod oscillator;
|
||||
pub mod filter;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! Uses phase accumulation; bandlimited variants (BLEP/BLAMP) to follow.
|
||||
|
||||
use crate::{AudioProcessor, CVProcessor, config::SampleRate};
|
||||
use crate::descriptor::{ComponentDescriptor, Direction, JackDescriptor, ParamDescriptor, SignalKind};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Waveform {
|
||||
@@ -26,6 +27,19 @@ impl Vco {
|
||||
Self { waveform, freq_hz, phase: 0.0, sample_rate }
|
||||
}
|
||||
|
||||
pub const DESCRIPTOR: ComponentDescriptor = ComponentDescriptor {
|
||||
kind: "vco",
|
||||
label: "VCO",
|
||||
jacks: &[
|
||||
JackDescriptor { id: "cv_in", label: "CV", direction: Direction::Input, signal: SignalKind::Cv },
|
||||
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
||||
],
|
||||
params: &[
|
||||
ParamDescriptor { id: "freq_hz", label: "Freq", min: 20.0, max: 20_000.0, default: 440.0, unit: "Hz" },
|
||||
ParamDescriptor { id: "waveform", label: "Wave", min: 0.0, max: 4.0, default: 1.0, unit: "" },
|
||||
],
|
||||
};
|
||||
|
||||
#[inline]
|
||||
fn next_sample(&mut self) -> f32 {
|
||||
let p = self.phase;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Voltage-controlled amplifier (VCA).
|
||||
|
||||
use crate::{AudioProcessor, CVProcessor};
|
||||
use crate::descriptor::{ComponentDescriptor, Direction, JackDescriptor, ParamDescriptor, SignalKind};
|
||||
|
||||
pub struct Vca {
|
||||
pub gain: f32, // 0.0–1.0
|
||||
@@ -10,6 +11,19 @@ impl Vca {
|
||||
pub fn new(gain: f32) -> Self {
|
||||
Self { gain }
|
||||
}
|
||||
|
||||
pub const DESCRIPTOR: ComponentDescriptor = ComponentDescriptor {
|
||||
kind: "vca",
|
||||
label: "VCA",
|
||||
jacks: &[
|
||||
JackDescriptor { id: "audio_in", label: "In", direction: Direction::Input, signal: SignalKind::Audio },
|
||||
JackDescriptor { id: "cv_in", label: "CV", direction: Direction::Input, signal: SignalKind::Cv },
|
||||
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
|
||||
],
|
||||
params: &[
|
||||
ParamDescriptor { id: "gain", label: "Gain", min: 0.0, max: 1.0, default: 1.0, unit: "" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
impl<const B: usize> AudioProcessor<B> for Vca {
|
||||
|
||||
Reference in New Issue
Block a user