//! Grid -> compiled signal program, and the per-sample voice evaluator. //! //! The grid is the editor surface only. On every edit it compiles to a flat, //! row-ordered list of nodes (topological by construction: signal flows //! downward, so a cell's input - the cell above - is always evaluated first). //! The audio thread never sees the grid, only the compiled `Program`. //! //! Glyph convention: UPPERCASE letters are operators, digits 0-9 are constant //! params, lowercase a-z read MIDI CC (a = CC 0). A neighbour to the left/right //! supplies a parameter; `M` instead reads the left cell as a second signal. use crate::grid::{Grid, H, SLOTS, W}; pub const MAX_STATE: usize = 4 * SLOTS; pub const TAU: f32 = std::f32::consts::TAU; const PI: f32 = std::f32::consts::PI; #[derive(Clone, Copy)] pub enum Op { Sine, Saw, Pulse, Tri, Noise, Low, High, Band, Env, Soft, Clip, Fold, Decimate, Gain, Ring, Mix, Out, } #[derive(Clone, Copy)] pub enum Param { Const(f32), Reg(usize), None, } impl Param { #[inline] fn get(self, regs: &[f32]) -> Option { match self { Param::Const(c) => Some(c), Param::Reg(i) => Some(regs[i]), Param::None => None, } } } #[derive(Clone, Copy)] pub struct Node { pub op: Op, pub out: usize, pub input: Option, pub lsig: Option, pub left: Param, pub right: Param, pub st: usize, } pub struct Program { pub nodes: Vec, pub has_env: bool, } /// (operator, persistent state slots per voice) fn op_of(c: char) -> Option<(Op, usize)> { Some(match c { 'O' => (Op::Sine, 1), 'A' => (Op::Saw, 1), 'U' => (Op::Pulse, 1), 'V' => (Op::Tri, 1), 'N' => (Op::Noise, 0), 'L' => (Op::Low, 2), 'H' => (Op::High, 2), 'B' => (Op::Band, 2), 'E' => (Op::Env, 1), 'S' => (Op::Soft, 0), 'C' => (Op::Clip, 0), 'F' => (Op::Fold, 0), 'D' => (Op::Decimate, 2), 'G' => (Op::Gain, 0), 'R' => (Op::Ring, 1), 'M' => (Op::Mix, 0), '!' => (Op::Out, 0), _ => return None, }) } fn param_of(c: char) -> Param { match c { '0'..='9' => Param::Const((c as u8 - b'0') as f32), 'a'..='z' => Param::Reg((c as u8 - b'a') as usize), _ => Param::None, } } fn occupied(c: char) -> bool { !matches!(c, ' ' | '.' | '\0') } pub fn compile(grid: &Grid) -> Program { let mut nodes = Vec::new(); let mut st = 0usize; let mut has_env = false; for row in 0..H { for col in 0..W { let Some((op, ssize)) = op_of(grid.get(col, row)) else { continue }; if matches!(op, Op::Env) { has_env = true; } let input = if row > 0 && occupied(grid.get(col, row - 1)) { Some((row - 1) * W + col) } else { None }; let lsig = if col > 0 { Some(row * W + col - 1) } else { None }; let left = if col > 0 { param_of(grid.get(col - 1, row)) } else { Param::None }; let right = if col + 1 < W { param_of(grid.get(col + 1, row)) } else { Param::None }; let this_st = st; st = (st + ssize).min(MAX_STATE - 4); nodes.push(Node { op, out: row * W + col, input, lsig, left, right, st: this_st }); } } Program { nodes, has_env } } pub struct Voice { pub active: bool, pub note: u8, pub freq: f32, pub gate: f32, pub vel: f32, pub env_level: f32, pub age: u64, pub rng: u32, pub state: Vec, } impl Voice { pub fn new() -> Self { Voice { active: false, note: 0, freq: 0.0, gate: 0.0, vel: 0.0, env_level: 0.0, age: 0, rng: 0x1234_5678, state: vec![0.0; MAX_STATE], } } } #[inline] fn cutoff_hz(v: f32) -> f32 { let n = if v > 1.0 { v / 9.0 } else { v }; 40.0 * 400f32.powf(n.clamp(0.0, 1.0)) } #[inline] fn phasor(ph: &mut f32, inc: f32) -> f32 { let p = *ph; *ph += inc; if *ph >= 1.0 { *ph -= 1.0; } p } /// TPT state-variable filter step. Returns (low, band, high). #[inline] fn svf(inp: f32, fc: f32, k: f32, sr: f32, ic1: &mut f32, ic2: &mut f32) -> (f32, f32, f32) { let g = (PI * fc / sr).tan(); let a1 = 1.0 / (1.0 + g * (g + k)); let a2 = g * a1; let a3 = g * a2; let v3 = inp - *ic2; let v1 = a1 * *ic1 + a2 * v3; let v2 = *ic2 + a2 * *ic1 + a3 * v3; *ic1 = 2.0 * v1 - *ic1; *ic2 = 2.0 * v2 - *ic2; (v2, v1, inp - k * v1 - v2) } #[inline] fn res_k(right: Option) -> f32 { // higher right glyph -> more resonance (lower k = higher Q) (1.5 - right.unwrap_or(3.0) * 0.15).clamp(0.15, 1.5) } #[inline] pub fn eval_voice(v: &mut Voice, p: &Program, regs: &[f32], sr: f32) -> f32 { let mut s = [0f32; SLOTS]; let mut out = 0.0f32; let mut env_max = 0.0f32; for n in &p.nodes { let inp = n.input.map(|i| s[i]).unwrap_or(0.0); let l = n.left.get(regs); let r = n.right.get(regs); let y = match n.op { Op::Sine => { let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0); (phasor(&mut v.state[n.st], v.freq * ratio / sr) * TAU).sin() } Op::Saw => { let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0); 2.0 * phasor(&mut v.state[n.st], v.freq * ratio / sr) - 1.0 } Op::Pulse => { let w = l.map(|x| (x / 9.0).clamp(0.05, 0.95)).unwrap_or(0.5); if phasor(&mut v.state[n.st], v.freq / sr) < w { 1.0 } else { -1.0 } } Op::Tri => 4.0 * (phasor(&mut v.state[n.st], v.freq / sr) - 0.5).abs() - 1.0, Op::Noise => { v.rng ^= v.rng << 13; v.rng ^= v.rng >> 17; v.rng ^= v.rng << 5; (v.rng as f32 / u32::MAX as f32) * 2.0 - 1.0 } Op::Low | Op::High | Op::Band => { let fc = cutoff_hz(l.unwrap_or(9.0)); let k = res_k(r); let (ic1, ic2) = { let (a, b) = v.state.split_at_mut(n.st + 1); (&mut a[n.st], &mut b[0]) }; let (lo, ba, hi) = svf(inp, fc, k, sr, ic1, ic2); match n.op { Op::High => hi, Op::Band => ba, _ => lo, } } Op::Env => { let atk = l.map(|x| 0.002 + x * 0.05).unwrap_or(0.01); let rel = r.map(|x| 0.01 + x * 0.15).unwrap_or(0.15); let level = &mut v.state[n.st]; let target = if v.gate > 0.5 { 1.0 } else { 0.0 }; let rate = if v.gate > 0.5 { atk } else { rel }; *level += (target - *level) * (1.0 / (rate * sr)).min(1.0); if *level > env_max { env_max = *level; } inp * *level * v.vel } Op::Soft => { let drive = l.filter(|&x| x > 0.0).unwrap_or(1.0); (inp * drive).tanh() } Op::Clip => { let t = l.map(|x| (x / 9.0).clamp(0.05, 1.0)).unwrap_or(0.5); inp.clamp(-t, t) } Op::Fold => { let amt = 1.0 + l.unwrap_or(1.0); (inp * amt * PI * 0.5).sin() } Op::Decimate => { let factor = 1.0 + l.unwrap_or(4.0) * 3.0; let held = &mut v.state[n.st]; let held_val = *held; let ph = &mut v.state[n.st + 1]; *ph += 1.0; if *ph >= factor { *ph = 0.0; v.state[n.st] = inp; inp } else { held_val } } Op::Gain => { let g = l.map(|x| if x > 1.0 { x / 4.5 } else { x * 2.0 }).unwrap_or(1.0); inp * g } Op::Ring => { let ratio = l.filter(|&x| x > 0.0).unwrap_or(1.0); let m = (phasor(&mut v.state[n.st], v.freq * ratio / sr) * TAU).sin(); inp * m } Op::Mix => inp + n.lsig.map(|i| s[i]).unwrap_or(0.0), Op::Out => { out += inp; inp } }; s[n.out] = y; } if !p.has_env { out *= v.gate * v.vel; env_max = v.gate; } v.env_level = env_max; out }