94 lines
2.7 KiB
Rust
94 lines
2.7 KiB
Rust
//! 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()
|
||
|
|
}
|
||
|
|
}
|