Files
galdr/src/engine.rs
T

94 lines
2.7 KiB
Rust
Raw Normal View History

//! 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<Voice>,
regs: Vec<f32>,
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()
}
}