use crate::graph::{NodeHandle, Patch, PatchError};
use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
use alloc::boxed::Box;
use alloc::collections::VecDeque;
use alloc::format;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::sync::atomic::Ordering;
use libm::Libm;
use portable_atomic::AtomicU64;
const FOLLOWER_TAU_S: f64 = 0.010;
const COUNT_TAU_S: f64 = 0.010;
const GRACE_S: f64 = 0.005;
const RELEASE_THRESHOLD: f64 = 0.001;
const MAX_RELEASE_S: f64 = 10.0;
const TRIGGER_S: f64 = 0.001;
#[inline]
fn one_pole_coeff(tau_s: f64, sample_rate: f64) -> f64 {
if sample_rate <= 0.0 || tau_s <= 0.0 {
return 0.0;
}
Libm::<f64>::exp(-1.0 / (tau_s * sample_rate))
}
#[inline]
fn balance_gains(pan: f64) -> (f64, f64) {
let p = pan.clamp(-1.0, 1.0);
if p <= 0.0 {
(1.0, 1.0 + p) } else {
(1.0 - p, 1.0) }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AllocationMode {
#[default]
RoundRobin,
QuietestSteal,
OldestSteal,
NoSteal,
HighestPriority,
LowestPriority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VoiceState {
Free,
Active,
Releasing,
}
#[derive(Debug, Clone)]
pub struct Voice {
pub index: usize,
pub state: VoiceState,
pub note: Option<u8>,
pub velocity: f64,
pub voct: f64,
pub gate: f64,
pub trigger: f64,
pub age: u64,
pub release_samples: u64,
pub envelope_level: f64,
}
impl Voice {
pub fn new(index: usize) -> Self {
Self {
index,
state: VoiceState::Free,
note: None,
velocity: 0.0,
voct: 0.0,
gate: 0.0,
trigger: 0.0,
age: 0,
release_samples: 0,
envelope_level: 0.0,
}
}
pub fn note_on(&mut self, note: u8, velocity: f64) {
self.state = VoiceState::Active;
self.note = Some(note);
self.velocity = velocity;
self.voct = midi_note_to_voct(note);
self.gate = 1.0;
self.trigger = 1.0; self.age = 0;
self.release_samples = 0;
}
pub fn note_off(&mut self) {
if self.state == VoiceState::Active {
self.state = VoiceState::Releasing;
self.gate = 0.0;
self.release_samples = 0;
}
}
pub fn free(&mut self) {
self.state = VoiceState::Free;
self.note = None;
self.velocity = 0.0;
self.gate = 0.0;
self.trigger = 0.0;
self.release_samples = 0;
self.envelope_level = 0.0;
}
pub fn tick(&mut self) {
self.age = self.age.saturating_add(1);
self.trigger = 0.0;
if self.state == VoiceState::Releasing {
self.release_samples = self.release_samples.saturating_add(1);
}
}
pub fn is_free(&self) -> bool {
self.state == VoiceState::Free
}
pub fn is_playing_note(&self, note: u8) -> bool {
self.note == Some(note) && self.state != VoiceState::Free
}
}
#[inline]
pub fn midi_note_to_voct(note: u8) -> f64 {
(note as f64 - 60.0) / 12.0
}
#[inline]
pub fn voct_to_midi_note(voct: f64) -> u8 {
Libm::<f64>::round(voct * 12.0 + 60.0).clamp(0.0, 127.0) as u8
}
#[derive(Debug)]
pub struct VoiceAllocator {
num_voices: usize,
mode: AllocationMode,
voices: Vec<Voice>,
lru_queue: VecDeque<usize>,
release_threshold: f64,
release_grace_samples: u64,
max_release_samples: u64,
last_stolen: Option<usize>,
}
impl VoiceAllocator {
pub fn new(num_voices: usize) -> Self {
let mut voices = Vec::with_capacity(num_voices);
for i in 0..num_voices {
voices.push(Voice::new(i));
}
let mut lru_queue = VecDeque::with_capacity(num_voices);
for i in 0..num_voices {
lru_queue.push_back(i);
}
Self {
num_voices,
mode: AllocationMode::RoundRobin,
voices,
lru_queue,
release_threshold: 0.0001,
release_grace_samples: 0,
max_release_samples: u64::MAX,
last_stolen: None,
}
}
pub fn set_mode(&mut self, mode: AllocationMode) {
self.mode = mode;
}
pub fn mode(&self) -> AllocationMode {
self.mode
}
pub fn set_release_criteria(&mut self, threshold: f64, grace_samples: u64) {
self.release_threshold = threshold;
self.release_grace_samples = grace_samples;
}
pub fn set_max_release_samples(&mut self, max_samples: u64) {
self.max_release_samples = max_samples;
}
pub fn max_release_samples(&self) -> u64 {
self.max_release_samples
}
pub fn num_voices(&self) -> usize {
self.num_voices
}
pub fn voice(&self, index: usize) -> Option<&Voice> {
self.voices.get(index)
}
pub fn voice_mut(&mut self, index: usize) -> Option<&mut Voice> {
self.voices.get_mut(index)
}
pub fn voices(&self) -> &[Voice] {
&self.voices
}
pub fn voices_mut(&mut self) -> &mut [Voice] {
&mut self.voices
}
pub fn active_count(&self) -> usize {
self.voices
.iter()
.filter(|v| v.state != VoiceState::Free)
.count()
}
pub fn last_stolen(&self) -> Option<usize> {
self.last_stolen
}
pub fn note_on(&mut self, note: u8, velocity: f64) -> Option<usize> {
self.last_stolen = None;
if let Some(idx) = self.voices.iter().position(|v| v.is_playing_note(note)) {
self.voices[idx].note_on(note, velocity);
self.update_lru(idx); return Some(idx);
}
if let Some(idx) = self.find_free_voice() {
self.voices[idx].note_on(note, velocity);
self.update_lru(idx);
return Some(idx);
}
if let Some(idx) = self.find_steal_voice(note) {
self.voices[idx].note_on(note, velocity);
self.update_lru(idx);
self.last_stolen = Some(idx);
return Some(idx);
}
None
}
pub fn note_off(&mut self, note: u8) -> Option<usize> {
for voice in &mut self.voices {
if voice.is_playing_note(note) {
voice.note_off();
return Some(voice.index);
}
}
None
}
pub fn all_notes_off(&mut self) {
for voice in &mut self.voices {
voice.note_off();
}
}
pub fn panic(&mut self) {
for voice in &mut self.voices {
voice.free();
}
}
pub fn tick(&mut self) {
for voice in &mut self.voices {
voice.tick();
}
let threshold = self.release_threshold;
let grace = self.release_grace_samples;
let max_release = self.max_release_samples;
for voice in &mut self.voices {
if voice.state != VoiceState::Releasing {
continue;
}
let quiet_done = voice.envelope_level < threshold && voice.release_samples >= grace;
let timed_out = voice.release_samples >= max_release;
if quiet_done || timed_out {
voice.free();
}
}
}
pub fn set_envelope_level(&mut self, voice_index: usize, level: f64) {
if let Some(voice) = self.voices.get_mut(voice_index) {
voice.envelope_level = level;
}
}
fn find_free_voice(&self) -> Option<usize> {
self.lru_queue
.iter()
.find(|&&idx| self.voices[idx].is_free())
.copied()
}
fn find_steal_voice(&self, note: u8) -> Option<usize> {
if self.mode == AllocationMode::NoSteal {
return None;
}
self.select_victim(note, VoiceState::Releasing)
.or_else(|| self.select_victim(note, VoiceState::Active))
}
fn select_victim(&self, note: u8, state: VoiceState) -> Option<usize> {
let candidates = || self.voices.iter().filter(|v| v.state == state);
match self.mode {
AllocationMode::NoSteal => None,
AllocationMode::RoundRobin | AllocationMode::OldestSteal => {
candidates().max_by_key(|v| v.age).map(|v| v.index)
}
AllocationMode::QuietestSteal => candidates()
.min_by(|a, b| {
a.envelope_level
.partial_cmp(&b.envelope_level)
.unwrap_or(core::cmp::Ordering::Equal)
})
.map(|v| v.index),
AllocationMode::HighestPriority => candidates()
.filter(|v| v.note.map(|n| n < note).unwrap_or(false))
.min_by_key(|v| v.note)
.map(|v| v.index),
AllocationMode::LowestPriority => candidates()
.filter(|v| v.note.map(|n| n > note).unwrap_or(false))
.max_by_key(|v| v.note)
.map(|v| v.index),
}
}
fn update_lru(&mut self, used_idx: usize) {
if let Some(pos) = self.lru_queue.iter().position(|&x| x == used_idx) {
self.lru_queue.remove(pos);
}
self.lru_queue.push_back(used_idx);
}
}
#[derive(Debug, Clone)]
pub struct UnisonConfig {
pub voices: usize,
pub detune_cents: f64,
pub stereo_spread: f64,
pub phase_random: f64,
}
impl Default for UnisonConfig {
fn default() -> Self {
Self {
voices: 1,
detune_cents: 0.0,
stereo_spread: 0.0,
phase_random: 0.0,
}
}
}
impl UnisonConfig {
pub fn new(voices: usize, detune_cents: f64) -> Self {
Self {
voices: voices.max(1),
detune_cents,
stereo_spread: 0.5,
phase_random: 0.0,
}
}
pub fn detune_offset(&self, voice_index: usize) -> f64 {
if self.voices <= 1 {
return 0.0;
}
let normalized = voice_index as f64 / (self.voices - 1) as f64;
let centered = normalized * 2.0 - 1.0;
centered * self.detune_cents / 2400.0
}
pub fn pan_position(&self, voice_index: usize) -> f64 {
if self.voices <= 1 {
return 0.0;
}
let normalized = voice_index as f64 / (self.voices - 1) as f64;
let centered = normalized * 2.0 - 1.0; centered * self.stereo_spread
}
pub fn voice_gain(&self) -> f64 {
1.0 / Libm::<f64>::sqrt(self.voices.max(1) as f64)
}
}
#[derive(Debug)]
pub struct VoiceControl {
voct: AtomicU64,
gate: AtomicU64,
trigger: AtomicU64,
velocity: AtomicU64,
}
impl VoiceControl {
pub fn new() -> Self {
Self {
voct: AtomicU64::new(0f64.to_bits()),
gate: AtomicU64::new(0f64.to_bits()),
trigger: AtomicU64::new(0f64.to_bits()),
velocity: AtomicU64::new(1f64.to_bits()),
}
}
#[inline]
fn load(a: &AtomicU64) -> f64 {
f64::from_bits(a.load(Ordering::Relaxed))
}
#[inline]
fn store(a: &AtomicU64, v: f64) {
a.store(v.to_bits(), Ordering::Relaxed);
}
pub fn voct(&self) -> f64 {
Self::load(&self.voct)
}
pub fn gate(&self) -> f64 {
Self::load(&self.gate)
}
pub fn trigger(&self) -> f64 {
Self::load(&self.trigger)
}
pub fn velocity(&self) -> f64 {
Self::load(&self.velocity)
}
pub fn set_voct(&self, v: f64) {
Self::store(&self.voct, v);
}
pub fn set_gate(&self, v: f64) {
Self::store(&self.gate, v);
}
pub fn set_trigger(&self, v: f64) {
Self::store(&self.trigger, v);
}
pub fn set_velocity(&self, v: f64) {
Self::store(&self.velocity, v);
}
fn reset(&self) {
self.set_voct(0.0);
self.set_gate(0.0);
self.set_trigger(0.0);
self.set_velocity(1.0);
}
}
impl Default for VoiceControl {
fn default() -> Self {
Self::new()
}
}
pub struct VoiceInput {
control: Arc<VoiceControl>,
spec: PortSpec,
trigger_len: u32,
trigger_remaining: u32,
prev_trigger_req: f64,
}
impl VoiceInput {
pub fn new() -> Self {
Self::with_control(Arc::new(VoiceControl::new()), 48_000.0)
}
pub fn with_control(control: Arc<VoiceControl>, sample_rate: f64) -> Self {
Self {
control,
spec: PortSpec {
inputs: vec![],
outputs: vec![
PortDef::new(0, "voct", SignalKind::VoltPerOctave),
PortDef::new(1, "gate", SignalKind::Gate),
PortDef::new(2, "trigger", SignalKind::Trigger),
PortDef::new(3, "velocity", SignalKind::CvUnipolar),
],
},
trigger_len: Self::trigger_len_for(sample_rate),
trigger_remaining: 0,
prev_trigger_req: 0.0,
}
}
#[inline]
fn trigger_len_for(sample_rate: f64) -> u32 {
(Libm::<f64>::round(TRIGGER_S * sample_rate)).max(1.0) as u32
}
pub fn control(&self) -> &Arc<VoiceControl> {
&self.control
}
pub fn set_from_voice(&mut self, voice: &Voice) {
self.control.set_voct(voice.voct);
self.control.set_gate(voice.gate);
self.control.set_trigger(voice.trigger);
self.control.set_velocity(voice.velocity);
}
pub fn set_voct(&mut self, voct: f64) {
self.control.set_voct(voct);
}
pub fn set_gate(&mut self, gate: f64) {
self.control.set_gate(gate);
}
pub fn set_trigger(&mut self, trigger: f64) {
self.control.set_trigger(trigger);
}
pub fn set_velocity(&mut self, velocity: f64) {
self.control.set_velocity(velocity);
}
pub fn voct(&self) -> f64 {
self.control.voct()
}
pub fn gate(&self) -> f64 {
self.control.gate()
}
pub fn trigger(&self) -> f64 {
self.control.trigger()
}
pub fn velocity(&self) -> f64 {
self.control.velocity()
}
}
impl Default for VoiceInput {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for VoiceInput {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _inputs: &PortValues, outputs: &mut PortValues) {
outputs.set(0, self.control.voct());
outputs.set(1, if self.control.gate() > 0.5 { 5.0 } else { 0.0 });
let req = self.control.trigger();
if req > 0.5 && self.prev_trigger_req <= 0.5 {
self.trigger_remaining = self.trigger_len;
}
self.prev_trigger_req = req;
let trig_out = if self.trigger_remaining > 0 {
self.trigger_remaining -= 1;
5.0
} else {
0.0
};
outputs.set(2, trig_out);
outputs.set(3, self.control.velocity() * 10.0); }
fn reset(&mut self) {
self.control.reset();
self.trigger_remaining = 0;
self.prev_trigger_req = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.trigger_len = Self::trigger_len_for(sample_rate);
}
fn type_id(&self) -> &'static str {
"voice_input"
}
}
type VoiceBuilder = dyn Fn(&mut Patch, &NodeHandle) -> Result<(), PatchError>;
struct SubVoice {
patch: Patch,
control: Arc<VoiceControl>,
controller: NodeHandle,
}
struct VoiceSlot {
subs: Vec<SubVoice>,
follower: f64,
}
pub struct PolyPatch {
allocator: VoiceAllocator,
voices: Vec<VoiceSlot>,
unison: UnisonConfig,
sample_rate: f64,
builder: Option<Box<VoiceBuilder>>,
smoothed_count: f64,
follower_coeff: f64,
count_coeff: f64,
grace_samples: u64,
output_left: f64,
output_right: f64,
}
impl PolyPatch {
pub fn new(num_voices: usize, sample_rate: f64) -> Self {
Self::build(num_voices, sample_rate, None).expect("empty voice build cannot fail")
}
pub fn with_voice_fn<F>(
num_voices: usize,
sample_rate: f64,
builder: F,
) -> Result<Self, PatchError>
where
F: Fn(&mut Patch, &NodeHandle) -> Result<(), PatchError> + 'static,
{
Self::build(num_voices, sample_rate, Some(Box::new(builder)))
}
fn build(
num_voices: usize,
sample_rate: f64,
builder: Option<Box<VoiceBuilder>>,
) -> Result<Self, PatchError> {
let mut poly = Self {
allocator: VoiceAllocator::new(num_voices),
voices: Vec::new(),
unison: UnisonConfig::default(),
sample_rate,
builder,
smoothed_count: 0.0,
follower_coeff: 0.0,
count_coeff: 0.0,
grace_samples: 0,
output_left: 0.0,
output_right: 0.0,
};
poly.recompute_coeffs();
poly.voices = poly.build_voices()?;
Ok(poly)
}
fn recompute_coeffs(&mut self) {
self.follower_coeff = one_pole_coeff(FOLLOWER_TAU_S, self.sample_rate);
self.count_coeff = one_pole_coeff(COUNT_TAU_S, self.sample_rate);
self.grace_samples = (GRACE_S * self.sample_rate).max(1.0) as u64;
self.allocator
.set_release_criteria(RELEASE_THRESHOLD, self.grace_samples);
self.allocator
.set_max_release_samples(Self::release_cap_samples(MAX_RELEASE_S, self.sample_rate));
}
fn release_cap_samples(seconds: f64, sample_rate: f64) -> u64 {
let samples = seconds * sample_rate;
if !samples.is_finite() || samples <= 0.0 {
u64::MAX
} else {
(samples as u64).max(1)
}
}
pub fn set_max_release_time(&mut self, seconds: f64) {
self.allocator
.set_max_release_samples(Self::release_cap_samples(seconds, self.sample_rate));
}
fn build_voices(&self) -> Result<Vec<VoiceSlot>, PatchError> {
let unison_voices = self.unison.voices.max(1);
let mut voices = Vec::with_capacity(self.allocator.num_voices());
for _ in 0..self.allocator.num_voices() {
let mut subs = Vec::with_capacity(unison_voices);
for _ in 0..unison_voices {
let control = Arc::new(VoiceControl::new());
let mut patch = Patch::new(self.sample_rate);
let controller = patch.add(
"voice_ctrl",
VoiceInput::with_control(control.clone(), self.sample_rate),
);
if let Some(builder) = &self.builder {
builder(&mut patch, &controller)?;
}
patch.compile()?;
subs.push(SubVoice {
patch,
control,
controller,
});
}
voices.push(VoiceSlot {
subs,
follower: 0.0,
});
}
Ok(voices)
}
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
pub fn num_voices(&self) -> usize {
self.allocator.num_voices()
}
pub fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
self.recompute_coeffs();
if let Ok(voices) = self.build_voices() {
self.voices = voices;
}
self.smoothed_count = 0.0;
}
pub fn voice_control(&self, index: usize) -> Option<&Arc<VoiceControl>> {
self.voices
.get(index)
.and_then(|v| v.subs.first())
.map(|s| &s.control)
}
pub fn voice_controller(&self, index: usize) -> Option<&NodeHandle> {
self.voices
.get(index)
.and_then(|v| v.subs.first())
.map(|s| &s.controller)
}
pub fn allocator(&self) -> &VoiceAllocator {
&self.allocator
}
pub fn allocator_mut(&mut self) -> &mut VoiceAllocator {
&mut self.allocator
}
pub fn set_unison(&mut self, config: UnisonConfig) {
let count_changed = config.voices.max(1) != self.unison.voices.max(1);
self.unison = config;
if count_changed {
if let Ok(voices) = self.build_voices() {
self.voices = voices;
}
}
}
pub fn unison(&self) -> &UnisonConfig {
&self.unison
}
pub fn voice_patch(&self, index: usize) -> Option<&Patch> {
self.voices
.get(index)
.and_then(|v| v.subs.first())
.map(|s| &s.patch)
}
pub fn voice_patch_mut(&mut self, index: usize) -> Option<&mut Patch> {
self.voices
.get_mut(index)
.and_then(|v| v.subs.first_mut())
.map(|s| &mut s.patch)
}
pub fn note_on(&mut self, note: u8, velocity: u8) {
let velocity_f = velocity as f64 / 127.0;
if let Some(idx) = self.allocator.note_on(note, velocity_f) {
if self.allocator.last_stolen() == Some(idx) {
if let Some(slot) = self.voices.get_mut(idx) {
for sub in &mut slot.subs {
sub.patch.reset();
}
slot.follower = 0.0;
}
}
}
}
pub fn note_off(&mut self, note: u8) {
self.allocator.note_off(note);
}
pub fn all_notes_off(&mut self) {
self.allocator.all_notes_off();
}
pub fn panic(&mut self) {
self.allocator.panic();
}
pub fn compile(&mut self) -> Result<(), PatchError> {
for slot in &mut self.voices {
for sub in &mut slot.subs {
sub.patch.compile()?;
}
}
Ok(())
}
pub fn compensation_gain(&self) -> f64 {
1.0 / Libm::<f64>::sqrt(self.smoothed_count.max(1.0))
}
pub fn tick(&mut self) -> (f64, f64) {
let unison = self.unison.clone();
let unison_voices = unison.voices.max(1);
let unison_gain = unison.voice_gain();
let use_pan = unison_voices > 1 && unison.stereo_spread != 0.0;
let follower_coeff = self.follower_coeff;
let inst_count = self.allocator.active_count() as f64;
self.smoothed_count =
self.count_coeff * self.smoothed_count + (1.0 - self.count_coeff) * inst_count;
let mut left = 0.0;
let mut right = 0.0;
for i in 0..self.voices.len() {
let (state, base_voct, gate, trigger, velocity) = {
let v = &self.allocator.voices()[i];
(v.state, v.voct, v.gate, v.trigger, v.velocity)
};
if state == VoiceState::Free {
self.voices[i].follower = 0.0;
continue;
}
let slot = &mut self.voices[i];
let mut peak = 0.0;
for (u, sub) in slot.subs.iter_mut().enumerate() {
sub.control.set_voct(base_voct + unison.detune_offset(u));
sub.control.set_gate(gate);
sub.control.set_trigger(trigger);
sub.control.set_velocity(velocity);
let (l, r) = sub.patch.tick();
let (lg, rg) = if use_pan {
balance_gains(unison.pan_position(u))
} else {
(1.0, 1.0)
};
let sl = l * lg * unison_gain;
let sr = r * rg * unison_gain;
left += sl;
right += sr;
let mag = Libm::<f64>::fabs(sl).max(Libm::<f64>::fabs(sr));
if mag > peak {
peak = mag;
}
}
slot.follower = follower_coeff * slot.follower + (1.0 - follower_coeff) * peak;
let level = slot.follower;
self.allocator.set_envelope_level(i, level);
}
self.allocator.tick();
let g = 1.0 / Libm::<f64>::sqrt(self.smoothed_count.max(1.0));
left *= g;
right *= g;
self.output_left = left;
self.output_right = right;
(left, right)
}
pub fn output(&self) -> (f64, f64) {
(self.output_left, self.output_right)
}
pub fn reset(&mut self) {
for slot in &mut self.voices {
slot.follower = 0.0;
for sub in &mut slot.subs {
sub.patch.reset();
}
}
self.allocator.panic();
self.smoothed_count = 0.0;
self.output_left = 0.0;
self.output_right = 0.0;
}
}
pub struct VoiceMixer {
num_voices: usize,
spec: PortSpec,
}
impl VoiceMixer {
pub fn new(num_voices: usize) -> Self {
let mut inputs = Vec::with_capacity(num_voices * 2);
for i in 0..num_voices {
inputs.push(PortDef::new(
i as u32 * 2,
format!("in{}_l", i),
SignalKind::Audio,
));
inputs.push(PortDef::new(
i as u32 * 2 + 1,
format!("in{}_r", i),
SignalKind::Audio,
));
}
Self {
num_voices,
spec: PortSpec {
inputs,
outputs: vec![
PortDef::new(100, "left", SignalKind::Audio),
PortDef::new(101, "right", SignalKind::Audio),
],
},
}
}
}
impl GraphModule for VoiceMixer {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let mut left = 0.0;
let mut right = 0.0;
for i in 0..self.num_voices {
left += inputs.get_or(i as u32 * 2, 0.0);
right += inputs.get_or(i as u32 * 2 + 1, 0.0);
}
outputs.set(100, left);
outputs.set(101, right);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"voice_mixer"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modules::{Adsr, StereoOutput, Vca, Vco};
struct DcSource {
value: f64,
spec: PortSpec,
}
impl DcSource {
fn new(value: f64) -> Self {
Self {
value,
spec: PortSpec {
inputs: vec![],
outputs: vec![PortDef::new(0, "out", SignalKind::Audio)],
},
}
}
}
impl GraphModule for DcSource {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _inputs: &PortValues, outputs: &mut PortValues) {
outputs.set(0, self.value);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"dc_source"
}
}
#[test]
fn test_voice_allocation_basic() {
let mut allocator = VoiceAllocator::new(4);
let voice1 = allocator.note_on(60, 0.8);
assert_eq!(voice1, Some(0));
assert_eq!(allocator.active_count(), 1);
let voice2 = allocator.note_on(64, 0.7);
assert_eq!(voice2, Some(1));
assert_eq!(allocator.active_count(), 2);
allocator.note_off(60);
assert_eq!(allocator.active_count(), 2);
allocator.tick();
}
#[test]
fn test_voice_allocation_retrigger() {
let mut allocator = VoiceAllocator::new(4);
let voice1 = allocator.note_on(60, 0.8);
assert_eq!(voice1, Some(0));
let voice2 = allocator.note_on(60, 0.9);
assert_eq!(voice2, Some(0));
assert_eq!(allocator.active_count(), 1);
}
#[test]
fn test_retrigger_updates_lru() {
let mut allocator = VoiceAllocator::new(3);
allocator.note_on(60, 1.0); allocator.note_on(62, 1.0); allocator.note_on(64, 1.0); allocator.note_off(60);
allocator.note_off(62);
allocator.note_off(64);
allocator.panic();
assert_eq!(allocator.note_on(60, 1.0), Some(0));
assert_eq!(allocator.note_on(60, 1.0), Some(0));
allocator.voice_mut(0).unwrap().free();
allocator.voice_mut(1).unwrap().free();
allocator.voice_mut(2).unwrap().free();
let next = allocator.note_on(67, 1.0).unwrap();
assert_ne!(next, 0, "retrigger should have pushed voice 0 to LRU back");
}
#[test]
fn test_voice_stealing_oldest_active() {
let mut allocator = VoiceAllocator::new(2);
allocator.set_mode(AllocationMode::OldestSteal);
allocator.note_on(60, 0.8);
allocator.tick();
allocator.note_on(62, 0.7);
allocator.tick();
let stolen = allocator.note_on(64, 0.6);
assert_eq!(stolen, Some(0));
assert_eq!(allocator.last_stolen(), Some(0));
}
#[test]
fn test_voice_stealing_prefers_releasing() {
let mut allocator = VoiceAllocator::new(2);
allocator.set_mode(AllocationMode::OldestSteal);
allocator.note_on(60, 0.8);
allocator.tick();
allocator.note_on(62, 0.7);
allocator.tick();
allocator.note_off(62);
let stolen = allocator.note_on(64, 0.6);
assert_eq!(stolen, Some(1));
assert_eq!(allocator.last_stolen(), Some(1));
}
#[test]
fn test_quietest_steal_uses_real_levels() {
let mut allocator = VoiceAllocator::new(3);
allocator.set_mode(AllocationMode::QuietestSteal);
allocator.note_on(60, 1.0); allocator.note_on(62, 1.0); allocator.note_on(64, 1.0);
allocator.set_envelope_level(0, 0.9);
allocator.set_envelope_level(1, 0.1);
allocator.set_envelope_level(2, 0.7);
let stolen = allocator.note_on(67, 1.0);
assert_eq!(
stolen,
Some(1),
"QuietestSteal should pick the lowest level"
);
}
#[test]
fn test_no_steal_mode_drops_note() {
let mut allocator = VoiceAllocator::new(2);
allocator.set_mode(AllocationMode::NoSteal);
allocator.note_on(60, 0.8);
allocator.note_on(62, 0.7);
let result = allocator.note_on(64, 0.6);
assert_eq!(result, None);
assert_eq!(allocator.last_stolen(), None);
}
#[test]
fn test_priority_mode_drop_when_no_victim() {
let mut allocator = VoiceAllocator::new(2);
allocator.set_mode(AllocationMode::HighestPriority);
allocator.note_on(80, 1.0);
allocator.note_on(84, 1.0);
assert_eq!(allocator.note_on(60, 1.0), None);
let stolen = allocator.note_on(90, 1.0);
assert_eq!(stolen, Some(0));
}
#[test]
fn test_midi_note_to_voct() {
assert!((midi_note_to_voct(60) - 0.0).abs() < 0.001);
assert!((midi_note_to_voct(72) - 1.0).abs() < 0.001);
assert!((midi_note_to_voct(48) - (-1.0)).abs() < 0.001);
}
#[test]
fn test_voct_to_midi_note() {
assert_eq!(voct_to_midi_note(0.0), 60);
assert_eq!(voct_to_midi_note(1.0), 72);
assert_eq!(voct_to_midi_note(-1.0), 48);
}
#[test]
fn test_unison_detune_total_spread() {
let config = UnisonConfig::new(3, 10.0);
let d0 = config.detune_offset(0);
let d1 = config.detune_offset(1);
let d2 = config.detune_offset(2);
assert!(d0 < 0.0);
assert!((d1 - 0.0).abs() < 1e-9);
assert!(d2 > 0.0);
assert!((d0 + d2).abs() < 1e-9, "spread must be symmetric");
let d0_cents = d0 * 1200.0;
let d2_cents = d2 * 1200.0;
let span_cents = (d2 - d0) * 1200.0;
assert!((d0_cents + 5.0).abs() < 1e-6, "low edge should be -5 cents");
assert!(
(d2_cents - 5.0).abs() < 1e-6,
"high edge should be +5 cents"
);
assert!(
(span_cents - 10.0).abs() < 1e-6,
"total spread should equal detune_cents (got {span_cents})"
);
}
#[test]
fn test_unison_pan() {
let mut config = UnisonConfig::new(3, 10.0);
config.stereo_spread = 1.0;
assert!((config.pan_position(0) - (-1.0)).abs() < 0.001);
assert!((config.pan_position(1) - 0.0).abs() < 0.001);
assert!((config.pan_position(2) - 1.0).abs() < 0.001);
}
#[test]
fn test_unison_config_voice_gain() {
let config = UnisonConfig::new(4, 10.0);
let gain = config.voice_gain();
assert!((gain - 0.5).abs() < 1e-9); }
#[test]
fn test_balance_gains_center_unity() {
let (l, r) = balance_gains(0.0);
assert!((l - 1.0).abs() < 1e-9 && (r - 1.0).abs() < 1e-9);
}
#[test]
fn test_balance_gains_partial() {
let (l, r) = balance_gains(-0.5); assert!((l - 1.0).abs() < 1e-9 && (r - 0.5).abs() < 1e-9);
let (l, r) = balance_gains(0.5); assert!((l - 0.5).abs() < 1e-9 && (r - 1.0).abs() < 1e-9);
}
#[test]
fn test_voice_input_module() {
let mut input = VoiceInput::new();
let mut outputs = PortValues::new();
input.set_voct(0.5);
input.set_gate(1.0);
input.set_velocity(0.8);
input.tick(&PortValues::new(), &mut outputs);
assert!((outputs.get_or(0, 0.0) - 0.5).abs() < 0.001); assert!((outputs.get_or(1, 0.0) - 5.0).abs() < 0.001); assert!((outputs.get_or(3, 0.0) - 8.0).abs() < 0.001); }
#[test]
fn test_voice_input_trigger_pulse_in_samples() {
let mut input = VoiceInput::with_control(Arc::new(VoiceControl::new()), 48_000.0);
let mut outputs = PortValues::new();
input.set_trigger(1.0); input.tick(&PortValues::new(), &mut outputs);
assert!(outputs.get_or(2, 0.0) > 2.5, "trigger high on first sample");
input.set_trigger(0.0);
input.tick(&PortValues::new(), &mut outputs);
assert!(
outputs.get_or(2, 0.0) > 2.5,
"trigger pulse should last several samples, not one"
);
for _ in 0..64 {
input.tick(&PortValues::new(), &mut outputs);
}
assert!(outputs.get_or(2, 0.0) < 2.5, "trigger pulse should end");
}
#[test]
fn test_voice_input_default() {
let input = VoiceInput::default();
assert!((input.voct() - 0.0).abs() < 0.001);
}
#[test]
fn test_voice_input_set_from_voice() {
let mut voice = Voice::new(0);
voice.note_on(72, 0.8);
let mut input = VoiceInput::new();
input.set_from_voice(&voice);
assert!((input.voct() - 1.0).abs() < 0.001);
assert!((input.velocity() - 0.8).abs() < 0.001);
}
#[test]
fn test_voice_input_reset_type_id() {
let mut input = VoiceInput::new();
input.set_voct(1.0);
input.reset();
assert!((input.voct() - 0.0).abs() < 0.001);
assert_eq!(input.type_id(), "voice_input");
input.set_sample_rate(48000.0);
}
#[test]
fn test_voice_input_set_trigger() {
let mut input = VoiceInput::new();
input.set_trigger(1.0);
assert!((input.trigger() - 1.0).abs() < 0.001);
}
#[test]
fn test_voice_is_free() {
let voice = Voice::new(0);
assert!(voice.is_free());
let mut playing = Voice::new(1);
playing.note_on(60, 1.0);
assert!(!playing.is_free());
}
#[test]
fn test_voice_is_playing_note() {
let mut voice = Voice::new(0);
voice.note_on(60, 1.0);
assert!(voice.is_playing_note(60));
assert!(!voice.is_playing_note(61));
}
#[test]
fn test_voice_note_off_and_free() {
let mut voice = Voice::new(0);
voice.note_on(60, 1.0);
voice.note_off();
assert!(voice.state == VoiceState::Releasing);
voice.free();
assert!(voice.is_free());
}
#[test]
fn test_voice_tick_clears_trigger_and_counts_release() {
let mut voice = Voice::new(0);
voice.note_on(60, 1.0);
voice.tick();
assert!(voice.trigger == 0.0);
assert_eq!(voice.release_samples, 0);
voice.note_off();
voice.tick();
assert_eq!(voice.release_samples, 1); }
#[test]
fn test_voice_allocator_mode() {
let mut allocator = VoiceAllocator::new(4);
allocator.set_mode(AllocationMode::QuietestSteal);
assert_eq!(allocator.mode(), AllocationMode::QuietestSteal);
}
#[test]
fn test_voice_allocator_num_voices() {
let allocator = VoiceAllocator::new(8);
assert_eq!(allocator.num_voices(), 8);
}
#[test]
fn test_voice_allocator_voice_access() {
let mut allocator = VoiceAllocator::new(4);
assert!(allocator.voice(0).is_some());
assert!(allocator.voice_mut(0).is_some());
}
#[test]
fn test_voice_allocator_voices() {
let allocator = VoiceAllocator::new(4);
assert_eq!(allocator.voices().len(), 4);
}
#[test]
fn test_voice_allocator_voices_mut() {
let mut allocator = VoiceAllocator::new(4);
assert_eq!(allocator.voices_mut().len(), 4);
}
#[test]
fn test_voice_allocator_all_notes_off() {
let mut allocator = VoiceAllocator::new(4);
allocator.note_on(60, 1.0);
allocator.note_on(64, 1.0);
allocator.all_notes_off();
assert!(allocator
.voices()
.iter()
.all(|v| v.state == VoiceState::Releasing || v.state == VoiceState::Free));
}
#[test]
fn test_voice_allocator_tick() {
let mut allocator = VoiceAllocator::new(4);
allocator.note_on(60, 1.0);
allocator.tick();
}
#[test]
fn test_voice_allocator_set_envelope_level() {
let mut allocator = VoiceAllocator::new(4);
if let Some(i) = allocator.note_on(60, 1.0) {
allocator.set_envelope_level(i, 0.5);
assert!((allocator.voice(i).unwrap().envelope_level - 0.5).abs() < 1e-9);
}
}
#[test]
fn test_voice_allocator_release_grace_keeps_voice() {
let mut allocator = VoiceAllocator::new(1);
allocator.set_release_criteria(0.001, 100); allocator.note_on(60, 1.0);
allocator.note_off(60);
allocator.set_envelope_level(0, 0.0);
for _ in 0..50 {
allocator.set_envelope_level(0, 0.0);
allocator.tick();
}
assert_eq!(allocator.voice(0).unwrap().state, VoiceState::Releasing);
for _ in 0..100 {
allocator.set_envelope_level(0, 0.0);
allocator.tick();
}
assert_eq!(allocator.voice(0).unwrap().state, VoiceState::Free);
}
#[test]
fn test_poly_patch_basic() {
let mut poly = PolyPatch::new(4, 44100.0);
poly.note_on(60, 100);
assert_eq!(poly.allocator().active_count(), 1);
poly.note_on(64, 90);
assert_eq!(poly.allocator().active_count(), 2);
poly.note_off(60);
}
#[test]
fn test_poly_patch_panic() {
let mut poly = PolyPatch::new(4, 44100.0);
poly.note_on(60, 100);
poly.note_on(64, 90);
poly.note_on(67, 80);
poly.panic();
assert_eq!(poly.allocator().active_count(), 0);
}
#[test]
fn test_poly_patch_sample_rate() {
let poly = PolyPatch::new(4, 48000.0);
assert_eq!(poly.sample_rate(), 48000.0);
}
#[test]
fn test_poly_patch_set_sample_rate() {
let mut poly = PolyPatch::new(4, 44100.0);
poly.set_sample_rate(48000.0);
assert_eq!(poly.sample_rate(), 48000.0);
}
#[test]
fn test_poly_patch_controller_access() {
let poly = PolyPatch::new(4, 44100.0);
assert!(poly.voice_control(0).is_some());
assert!(poly.voice_controller(0).is_some());
assert!(poly.voice_control(99).is_none());
}
#[test]
fn test_poly_patch_allocator_mut() {
let mut poly = PolyPatch::new(4, 44100.0);
poly.allocator_mut().set_mode(AllocationMode::OldestSteal);
assert_eq!(poly.allocator().mode(), AllocationMode::OldestSteal);
}
#[test]
fn test_poly_patch_unison() {
let mut poly = PolyPatch::new(4, 44100.0);
poly.set_unison(UnisonConfig::new(2, 5.0));
assert_eq!(poly.unison().voices, 2);
}
#[test]
fn test_poly_patch_voice_patch_access() {
let mut poly = PolyPatch::new(4, 44100.0);
assert_eq!(poly.num_voices(), 4);
assert!(poly.voice_patch(0).is_some());
assert!(poly.voice_patch_mut(0).is_some());
assert!(poly.voice_patch(99).is_none());
}
#[test]
fn test_poly_patch_all_notes_off() {
let mut poly = PolyPatch::new(4, 44100.0);
poly.note_on(60, 100);
poly.note_on(64, 100);
poly.all_notes_off();
}
#[test]
fn test_poly_patch_compile_tick_output() {
let mut poly = PolyPatch::new(2, 44100.0);
poly.compile().unwrap();
poly.note_on(60, 100);
poly.tick();
let (left, right) = poly.output();
let _ = (left, right);
}
#[test]
fn test_poly_patch_reset() {
let mut poly = PolyPatch::new(4, 44100.0);
poly.note_on(60, 100);
poly.reset();
assert_eq!(poly.allocator().active_count(), 0);
}
fn build_synth(num_voices: usize, sr: f64) -> PolyPatch {
PolyPatch::with_voice_fn(num_voices, sr, |patch, ctrl| {
let sr = patch.sample_rate();
let vco = patch.add("vco", Vco::new(sr));
let adsr = patch.add("adsr", Adsr::new(sr));
let vca = patch.add("vca", Vca::new());
let out = patch.add("out", StereoOutput::new());
patch.connect(ctrl.out("voct"), vco.in_("voct"))?;
patch.connect(ctrl.out("gate"), adsr.in_("gate"))?;
patch.connect(vco.out("sin"), vca.in_("in"))?;
patch.connect(adsr.out("env"), vca.in_("cv"))?;
patch.connect(vca.out("out"), out.in_("left"))?;
patch.set_output(out.id());
Ok(())
})
.unwrap()
}
fn measure_period_samples(poly: &mut PolyPatch, warmup: usize, window: usize) -> f64 {
for _ in 0..warmup {
poly.tick();
}
let mut prev = 0.0;
let mut crossings = Vec::new();
for n in 0..window {
let (l, _r) = poly.tick();
if prev <= 0.0 && l > 0.0 {
crossings.push(n);
}
prev = l;
}
assert!(crossings.len() >= 2, "need at least two zero crossings");
let span = (crossings[crossings.len() - 1] - crossings[0]) as f64;
span / (crossings.len() - 1) as f64
}
#[test]
fn test_e2e_pitch_tracks_note() {
let sr = 48_000.0;
let mut poly = build_synth(1, sr);
poly.note_on(60, 100);
let p_c4 = measure_period_samples(&mut poly, 4000, 8000);
poly.note_off(60);
for _ in 0..(sr as usize / 5) {
poly.tick(); }
poly.note_on(72, 100);
let p_c5 = measure_period_samples(&mut poly, 4000, 8000);
let expect_c4 = sr / 261.63;
let expect_c5 = sr / 523.25;
assert!(
(p_c4 - expect_c4).abs() / expect_c4 < 0.05,
"C4 period {p_c4} vs expected {expect_c4}"
);
assert!(
(p_c5 - expect_c5).abs() / expect_c5 < 0.05,
"C5 period {p_c5} vs expected {expect_c5}"
);
assert!(
(p_c4 / p_c5 - 2.0).abs() < 0.1,
"octave should halve the period (ratio {})",
p_c4 / p_c5
);
}
#[test]
fn test_e2e_gate_reaches_adsr_and_release_tail_completes() {
let sr = 48_000.0;
let mut poly = build_synth(1, sr);
poly.note_on(60, 127);
let mut sustain_peak = 0.0f64;
for _ in 0..4800 {
poly.tick();
}
for _ in 0..2000 {
let (l, _r) = poly.tick();
sustain_peak = sustain_peak.max(l.abs());
}
assert!(
sustain_peak > 0.1,
"gate should drive the ADSR/VCA (sustain peak {sustain_peak})"
);
poly.note_off(60);
poly.tick();
assert_ne!(
poly.allocator().voice(0).unwrap().state,
VoiceState::Free,
"voice freed one sample after note-off (truncated release)"
);
let mut just_after = 0.0f64;
for _ in 0..200 {
let (l, _r) = poly.tick();
just_after = just_after.max(l.abs());
}
assert!(
just_after > 0.02,
"release tail truncated (amp {just_after} right after note-off)"
);
let mut freed = false;
for _ in 0..(sr as usize / 2) {
poly.tick();
if poly.allocator().voice(0).unwrap().state == VoiceState::Free {
freed = true;
break;
}
}
assert!(freed, "released voice should eventually free");
}
#[test]
fn test_e2e_sample_rate_propagates_to_voices() {
let sr1 = 48_000.0;
let mut poly = build_synth(1, sr1);
poly.note_on(60, 100);
let p1 = measure_period_samples(&mut poly, 4000, 8000);
poly.set_sample_rate(sr1 / 2.0);
poly.note_on(60, 100);
let p2 = measure_period_samples(&mut poly, 2000, 4000);
assert!(
(p2 / p1 - 0.5).abs() < 0.1,
"half sample rate should halve the period in samples (p1={p1}, p2={p2})"
);
}
fn build_dc_poly(num_voices: usize, value: f64, sr: f64) -> PolyPatch {
PolyPatch::with_voice_fn(num_voices, sr, move |patch, _ctrl| {
let dc = patch.add("dc", DcSource::new(value));
let out = patch.add("out", StereoOutput::new());
patch.connect(dc.out("out"), out.in_("left"))?;
patch.set_output(out.id());
Ok(())
})
.unwrap()
}
#[test]
fn test_single_voice_unity_gain() {
let sr = 48_000.0;
let mut poly = build_dc_poly(4, 1.0, sr);
poly.note_on(60, 100);
let mut out = (0.0, 0.0);
for _ in 0..2000 {
out = poly.tick();
}
assert!(
(out.0 - 1.0).abs() < 0.01 && (out.1 - 1.0).abs() < 0.01,
"single voice should pass at unity gain, got {out:?}"
);
}
#[test]
fn test_eight_voices_bounded_output() {
let sr = 48_000.0;
let mut poly = build_dc_poly(8, 1.0, sr);
poly.note_on(60, 100);
let mut single = 0.0;
for _ in 0..2000 {
single = poly.tick().0;
}
poly.panic();
for _ in 0..2000 {
poly.tick();
}
for n in 0..8u8 {
poly.note_on(60 + n, 100);
}
let mut eight = 0.0;
for _ in 0..4000 {
eight = poly.tick().0;
}
assert!((single - 1.0).abs() < 0.01);
assert!(
eight < 8.0 * single - 0.5,
"8 voices must be well below 8x single ({eight} vs {})",
8.0 * single
);
assert!(
(eight / single - 8.0f64.sqrt()).abs() < 0.3,
"8-voice sum should follow 1/sqrt(N) (ratio {})",
eight / single
);
}
#[test]
fn test_gain_compensation_is_smooth() {
let sr = 48_000.0;
let mut poly = PolyPatch::new(8, sr);
poly.note_on(60, 100);
for _ in 0..4000 {
poly.tick();
}
let g_before = poly.compensation_gain();
assert!(
(g_before - 1.0).abs() < 0.01,
"one voice => unity comp gain"
);
poly.note_on(64, 100);
poly.tick();
let g_step = poly.compensation_gain();
assert!(
(g_before - g_step).abs() < 0.05,
"comp gain stepped discontinuously: {g_before} -> {g_step}"
);
for _ in 0..4000 {
poly.tick();
}
let g_settled = poly.compensation_gain();
assert!(
(g_settled - 1.0 / 2.0f64.sqrt()).abs() < 0.02,
"comp gain should settle to 1/sqrt(2), got {g_settled}"
);
}
#[test]
fn test_steal_resets_voice_dsp() {
let sr = 48_000.0;
let mut poly = build_dc_poly(1, 1.0, sr);
poly.allocator_mut().set_mode(AllocationMode::OldestSteal);
poly.note_on(60, 100);
for _ in 0..500 {
poly.tick();
}
poly.note_on(72, 100);
assert_eq!(poly.allocator().last_stolen(), Some(0));
let (l, _r) = poly.tick();
assert!(l.abs() > 0.0, "stolen voice should keep producing audio");
}
#[test]
fn test_nondecaying_released_voice_force_frees() {
let sr = 48_000.0;
let mut poly = build_dc_poly(1, 1.0, sr);
poly.allocator_mut().set_mode(AllocationMode::NoSteal);
poly.note_on(60, 100);
for _ in 0..200 {
poly.tick();
}
poly.note_off(60);
for _ in 0..2000 {
poly.tick();
}
assert_eq!(
poly.allocator().voices()[0].state,
VoiceState::Releasing,
"DC voice does not decay, so it is still releasing"
);
poly.note_on(72, 100);
assert_ne!(
poly.allocator().voices()[0].note,
Some(72),
"NoSteal cannot reuse a still-Releasing voice yet"
);
poly.set_max_release_time(0.01); for _ in 0..1000 {
poly.tick();
}
assert_eq!(
poly.allocator().voices()[0].state,
VoiceState::Free,
"a non-decaying released voice must force-free after the release cap"
);
poly.note_on(72, 100);
assert_eq!(poly.allocator().voices()[0].note, Some(72));
assert_eq!(poly.allocator().voices()[0].state, VoiceState::Active);
}
#[test]
fn test_release_cap_configuration() {
let sr = 48_000.0;
let mut poly = build_dc_poly(1, 1.0, sr);
assert_eq!(
poly.allocator().max_release_samples(),
(MAX_RELEASE_S * sr) as u64
);
poly.set_max_release_time(0.0);
assert_eq!(poly.allocator().max_release_samples(), u64::MAX);
}
#[test]
fn test_voice_mixer() {
let mixer = VoiceMixer::new(4);
let spec = mixer.port_spec();
assert!(!spec.inputs.is_empty());
assert!(!spec.outputs.is_empty());
}
#[test]
fn test_voice_mixer_tick() {
let mut mixer = VoiceMixer::new(2);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(1, 2.0);
inputs.set(2, 3.0);
inputs.set(3, 4.0);
mixer.tick(&inputs, &mut outputs);
assert!(outputs.get(100).is_some());
assert!(outputs.get(101).is_some());
}
#[test]
fn test_voice_mixer_reset_type_id() {
let mut mixer = VoiceMixer::new(2);
mixer.reset();
mixer.set_sample_rate(48000.0);
assert_eq!(mixer.type_id(), "voice_mixer");
}
fn poly_stress(mode: AllocationMode) {
let sr = 48_000.0;
let mut poly = build_synth(16, sr);
poly.allocator_mut().set_mode(mode);
assert_eq!(poly.num_voices(), 16);
let mut state: u64 = 0x1234_5678_9abc_def0;
let mut next = move || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) as u32
};
let mut held: Vec<u8> = Vec::new();
for t in 0..12_000 {
if t % 30 == 0 {
let r = next() % 100;
if r < 55 {
let note = 36 + (next() % 48) as u8;
let vel = 1 + (next() % 127) as u8;
poly.note_on(note, vel);
if !held.contains(¬e) {
held.push(note);
}
} else if r < 80 && !held.is_empty() {
let idx = (next() as usize) % held.len();
let note = held.remove(idx);
poly.note_off(note);
} else if !held.is_empty() {
let idx = (next() as usize) % held.len();
let note = held[idx];
let vel = 1 + (next() % 127) as u8;
poly.note_on(note, vel);
}
}
let (l, r) = poly.tick();
assert!(
l.is_finite() && r.is_finite(),
"non-finite output at tick {t}"
);
assert!(
l.abs() < 16.0 && r.abs() < 16.0,
"polyphonic output exploded at tick {t}: ({l}, {r})"
);
assert!(
poly.allocator().active_count() <= poly.num_voices(),
"active_count {} exceeded voice count at tick {t}",
poly.allocator().active_count()
);
}
poly.all_notes_off();
for _ in 0..48_000 {
let (l, r) = poly.tick();
assert!(l.is_finite() && r.is_finite());
}
assert_eq!(
poly.allocator().active_count(),
0,
"all voices should auto-free after a long release tail"
);
}
#[test]
fn test_poly_stress_16_voices_oldest_steal() {
poly_stress(AllocationMode::OldestSteal);
}
#[test]
fn test_poly_stress_16_voices_no_steal() {
poly_stress(AllocationMode::NoSteal);
}
}