//! Voice allocation and the polyphonic mixer. Owned by the audio thread. use crate::dsp::{eval_voice, Program, Voice}; #[derive(Clone, Copy)] pub enum Event { NoteOn { note: u8, vel: u8 }, NoteOff { note: u8 }, Cc { idx: u8, val: u8 }, } pub struct Engine { voices: Vec, regs: Vec, sr: f32, clock: u64, } impl Engine { pub fn new(poly: usize, sr: f32) -> Self { Engine { voices: (0..poly).map(|_| Voice::new()).collect(), regs: vec![0.0; 128], sr, clock: 0, } } pub fn active_voices(&self) -> usize { self.voices.iter().filter(|v| v.active).count() } pub fn apply(&mut self, ev: Event) { match ev { Event::NoteOn { note, vel } => { self.clock += 1; // Prefer a free voice, else steal the oldest. let idx = self .voices .iter() .position(|v| !v.active) .unwrap_or_else(|| { let mut best = 0; for i in 1..self.voices.len() { if self.voices[i].age < self.voices[best].age { best = i; } } best }); let v = &mut self.voices[idx]; v.active = true; v.note = note; v.freq = 440.0 * 2f32.powf((note as f32 - 69.0) / 12.0); v.gate = 1.0; v.vel = vel as f32 / 127.0; v.age = self.clock; // fresh phases/env so a stolen voice doesn't click from old state for x in v.state.iter_mut() { *x = 0.0; } v.env_level = 0.0; } Event::NoteOff { note } => { for v in self.voices.iter_mut() { if v.active && v.note == note { v.gate = 0.0; } } } Event::Cc { idx, val } => { self.regs[idx as usize] = val as f32 / 127.0; } } } #[inline] pub fn render(&mut self, prog: &Program) -> f32 { let mut mix = 0.0f32; for v in self.voices.iter_mut() { if !v.active { continue; } mix += eval_voice(v, prog, &self.regs, self.sr); // Reap voices whose gate is closed and envelope has decayed. if v.gate < 0.5 && v.env_level < 1e-4 { v.active = false; } } // Gentle headroom limiter. (mix * 0.3).tanh() } } #[cfg(test)] mod tests { use super::*; use crate::dsp::compile; use crate::grid::Grid; #[cfg(test)] impl Engine { fn first_freq(&self) -> Option { self.voices.iter().find(|v| v.active).map(|v| v.freq) } fn reg(&self, i: usize) -> f32 { self.regs[i] } } fn grid_from(src: &str) -> Grid { let mut g = Grid::new(); for (r, line) in src.lines().enumerate() { for (c, ch) in line.chars().enumerate() { g.set(c, r, ch); } } g } #[test] fn noteon_maps_a4_to_440() { let mut e = Engine::new(4, 48000.0); e.apply(Event::NoteOn { note: 69, vel: 100 }); assert!((e.first_freq().unwrap() - 440.0).abs() < 0.01); assert_eq!(e.active_voices(), 1); } #[test] fn noteoff_reaps_gated_voice() { let p = compile(&grid_from("A\n!")); // no envelope -> gate reaps immediately let mut e = Engine::new(4, 48000.0); e.apply(Event::NoteOn { note: 60, vel: 100 }); e.render(&p); assert_eq!(e.active_voices(), 1); e.apply(Event::NoteOff { note: 60 }); e.render(&p); assert_eq!(e.active_voices(), 0); } #[test] fn monophonic_steals_the_oldest_voice() { let mut e = Engine::new(1, 48000.0); e.apply(Event::NoteOn { note: 60, vel: 100 }); e.apply(Event::NoteOn { note: 72, vel: 100 }); assert_eq!(e.active_voices(), 1); assert!((e.first_freq().unwrap() - 440.0 * 2f32.powf((72.0 - 69.0) / 12.0)).abs() < 0.01); } #[test] fn cc_writes_register() { let mut e = Engine::new(2, 48000.0); e.apply(Event::Cc { idx: 0, val: 127 }); assert!((e.reg(0) - 1.0).abs() < 1e-6); } #[test] fn master_output_is_limited() { // many stacked saws would exceed 1.0 without the limiter let p = compile(&grid_from("A.A.A.A\n!.!.!.!")); let mut e = Engine::new(4, 48000.0); e.apply(Event::NoteOn { note: 60, vel: 127 }); assert!(e.render(&p).abs() <= 1.0); } }