1use crate::graph::{NodeHandle, Patch, PatchError};
50use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
51use alloc::boxed::Box;
52use alloc::collections::VecDeque;
53use alloc::format;
54use alloc::sync::Arc;
55use alloc::vec;
56use alloc::vec::Vec;
57use core::sync::atomic::Ordering;
58use libm::Libm;
61use portable_atomic::AtomicU64;
62
63const FOLLOWER_TAU_S: f64 = 0.010;
70const COUNT_TAU_S: f64 = 0.010;
73const GRACE_S: f64 = 0.005;
77const RELEASE_THRESHOLD: f64 = 0.001;
79const MAX_RELEASE_S: f64 = 10.0;
87const TRIGGER_S: f64 = 0.001;
89
90#[inline]
95fn one_pole_coeff(tau_s: f64, sample_rate: f64) -> f64 {
96 if sample_rate <= 0.0 || tau_s <= 0.0 {
97 return 0.0;
98 }
99 Libm::<f64>::exp(-1.0 / (tau_s * sample_rate))
100}
101
102#[inline]
108fn balance_gains(pan: f64) -> (f64, f64) {
109 let p = pan.clamp(-1.0, 1.0);
110 if p <= 0.0 {
111 (1.0, 1.0 + p) } else {
113 (1.0 - p, 1.0) }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
119pub enum AllocationMode {
120 #[default]
122 RoundRobin,
123 QuietestSteal,
125 OldestSteal,
127 NoSteal,
129 HighestPriority,
134 LowestPriority,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum VoiceState {
144 Free,
146 Active,
148 Releasing,
150}
151
152#[derive(Debug, Clone)]
154pub struct Voice {
155 pub index: usize,
157 pub state: VoiceState,
159 pub note: Option<u8>,
161 pub velocity: f64,
163 pub voct: f64,
165 pub gate: f64,
167 pub trigger: f64,
169 pub age: u64,
171 pub release_samples: u64,
173 pub envelope_level: f64,
177}
178
179impl Voice {
180 pub fn new(index: usize) -> Self {
182 Self {
183 index,
184 state: VoiceState::Free,
185 note: None,
186 velocity: 0.0,
187 voct: 0.0,
188 gate: 0.0,
189 trigger: 0.0,
190 age: 0,
191 release_samples: 0,
192 envelope_level: 0.0,
193 }
194 }
195
196 pub fn note_on(&mut self, note: u8, velocity: f64) {
198 self.state = VoiceState::Active;
199 self.note = Some(note);
200 self.velocity = velocity;
201 self.voct = midi_note_to_voct(note);
202 self.gate = 1.0;
203 self.trigger = 1.0; self.age = 0;
205 self.release_samples = 0;
206 }
207
208 pub fn note_off(&mut self) {
210 if self.state == VoiceState::Active {
211 self.state = VoiceState::Releasing;
212 self.gate = 0.0;
213 self.release_samples = 0;
214 }
215 }
216
217 pub fn free(&mut self) {
219 self.state = VoiceState::Free;
220 self.note = None;
221 self.velocity = 0.0;
222 self.gate = 0.0;
223 self.trigger = 0.0;
224 self.release_samples = 0;
225 self.envelope_level = 0.0;
226 }
227
228 pub fn tick(&mut self) {
232 self.age = self.age.saturating_add(1);
233 self.trigger = 0.0;
234 if self.state == VoiceState::Releasing {
235 self.release_samples = self.release_samples.saturating_add(1);
236 }
237 }
238
239 pub fn is_free(&self) -> bool {
241 self.state == VoiceState::Free
242 }
243
244 pub fn is_playing_note(&self, note: u8) -> bool {
246 self.note == Some(note) && self.state != VoiceState::Free
247 }
248}
249
250#[inline]
253pub fn midi_note_to_voct(note: u8) -> f64 {
254 (note as f64 - 60.0) / 12.0
255}
256
257#[inline]
259pub fn voct_to_midi_note(voct: f64) -> u8 {
260 Libm::<f64>::round(voct * 12.0 + 60.0).clamp(0.0, 127.0) as u8
261}
262
263#[derive(Debug)]
265pub struct VoiceAllocator {
266 num_voices: usize,
268 mode: AllocationMode,
270 voices: Vec<Voice>,
272 lru_queue: VecDeque<usize>,
274 release_threshold: f64,
276 release_grace_samples: u64,
278 max_release_samples: u64,
282 last_stolen: Option<usize>,
284}
285
286impl VoiceAllocator {
287 pub fn new(num_voices: usize) -> Self {
289 let mut voices = Vec::with_capacity(num_voices);
290 for i in 0..num_voices {
291 voices.push(Voice::new(i));
292 }
293
294 let mut lru_queue = VecDeque::with_capacity(num_voices);
295 for i in 0..num_voices {
296 lru_queue.push_back(i);
297 }
298
299 Self {
300 num_voices,
301 mode: AllocationMode::RoundRobin,
302 voices,
303 lru_queue,
304 release_threshold: 0.0001,
308 release_grace_samples: 0,
309 max_release_samples: u64::MAX,
312 last_stolen: None,
313 }
314 }
315
316 pub fn set_mode(&mut self, mode: AllocationMode) {
318 self.mode = mode;
319 }
320
321 pub fn mode(&self) -> AllocationMode {
323 self.mode
324 }
325
326 pub fn set_release_criteria(&mut self, threshold: f64, grace_samples: u64) {
330 self.release_threshold = threshold;
331 self.release_grace_samples = grace_samples;
332 }
333
334 pub fn set_max_release_samples(&mut self, max_samples: u64) {
340 self.max_release_samples = max_samples;
341 }
342
343 pub fn max_release_samples(&self) -> u64 {
345 self.max_release_samples
346 }
347
348 pub fn num_voices(&self) -> usize {
350 self.num_voices
351 }
352
353 pub fn voice(&self, index: usize) -> Option<&Voice> {
355 self.voices.get(index)
356 }
357
358 pub fn voice_mut(&mut self, index: usize) -> Option<&mut Voice> {
360 self.voices.get_mut(index)
361 }
362
363 pub fn voices(&self) -> &[Voice] {
365 &self.voices
366 }
367
368 pub fn voices_mut(&mut self) -> &mut [Voice] {
370 &mut self.voices
371 }
372
373 pub fn active_count(&self) -> usize {
375 self.voices
376 .iter()
377 .filter(|v| v.state != VoiceState::Free)
378 .count()
379 }
380
381 pub fn last_stolen(&self) -> Option<usize> {
385 self.last_stolen
386 }
387
388 pub fn note_on(&mut self, note: u8, velocity: f64) -> Option<usize> {
393 self.last_stolen = None;
394
395 if let Some(idx) = self.voices.iter().position(|v| v.is_playing_note(note)) {
397 self.voices[idx].note_on(note, velocity);
398 self.update_lru(idx); return Some(idx);
400 }
401
402 if let Some(idx) = self.find_free_voice() {
404 self.voices[idx].note_on(note, velocity);
405 self.update_lru(idx);
406 return Some(idx);
407 }
408
409 if let Some(idx) = self.find_steal_voice(note) {
411 self.voices[idx].note_on(note, velocity);
412 self.update_lru(idx);
413 self.last_stolen = Some(idx);
414 return Some(idx);
415 }
416
417 None
419 }
420
421 pub fn note_off(&mut self, note: u8) -> Option<usize> {
424 for voice in &mut self.voices {
425 if voice.is_playing_note(note) {
426 voice.note_off();
427 return Some(voice.index);
428 }
429 }
430 None
431 }
432
433 pub fn all_notes_off(&mut self) {
435 for voice in &mut self.voices {
436 voice.note_off();
437 }
438 }
439
440 pub fn panic(&mut self) {
442 for voice in &mut self.voices {
443 voice.free();
444 }
445 }
446
447 pub fn tick(&mut self) {
450 for voice in &mut self.voices {
451 voice.tick();
452 }
453
454 let threshold = self.release_threshold;
455 let grace = self.release_grace_samples;
456 let max_release = self.max_release_samples;
457 for voice in &mut self.voices {
458 if voice.state != VoiceState::Releasing {
459 continue;
460 }
461 let quiet_done = voice.envelope_level < threshold && voice.release_samples >= grace;
463 let timed_out = voice.release_samples >= max_release;
466 if quiet_done || timed_out {
467 voice.free();
468 }
469 }
470 }
471
472 pub fn set_envelope_level(&mut self, voice_index: usize, level: f64) {
475 if let Some(voice) = self.voices.get_mut(voice_index) {
476 voice.envelope_level = level;
477 }
478 }
479
480 fn find_free_voice(&self) -> Option<usize> {
481 self.lru_queue
483 .iter()
484 .find(|&&idx| self.voices[idx].is_free())
485 .copied()
486 }
487
488 fn find_steal_voice(&self, note: u8) -> Option<usize> {
492 if self.mode == AllocationMode::NoSteal {
493 return None;
494 }
495 self.select_victim(note, VoiceState::Releasing)
496 .or_else(|| self.select_victim(note, VoiceState::Active))
497 }
498
499 fn select_victim(&self, note: u8, state: VoiceState) -> Option<usize> {
503 let candidates = || self.voices.iter().filter(|v| v.state == state);
504 match self.mode {
505 AllocationMode::NoSteal => None,
506 AllocationMode::RoundRobin | AllocationMode::OldestSteal => {
507 candidates().max_by_key(|v| v.age).map(|v| v.index)
508 }
509 AllocationMode::QuietestSteal => candidates()
510 .min_by(|a, b| {
511 a.envelope_level
512 .partial_cmp(&b.envelope_level)
513 .unwrap_or(core::cmp::Ordering::Equal)
514 })
515 .map(|v| v.index),
516 AllocationMode::HighestPriority => candidates()
517 .filter(|v| v.note.map(|n| n < note).unwrap_or(false))
518 .min_by_key(|v| v.note)
519 .map(|v| v.index),
520 AllocationMode::LowestPriority => candidates()
521 .filter(|v| v.note.map(|n| n > note).unwrap_or(false))
522 .max_by_key(|v| v.note)
523 .map(|v| v.index),
524 }
525 }
526
527 fn update_lru(&mut self, used_idx: usize) {
528 if let Some(pos) = self.lru_queue.iter().position(|&x| x == used_idx) {
530 self.lru_queue.remove(pos);
531 }
532 self.lru_queue.push_back(used_idx);
533 }
534}
535
536#[derive(Debug, Clone)]
538pub struct UnisonConfig {
539 pub voices: usize,
541 pub detune_cents: f64,
545 pub stereo_spread: f64,
547 pub phase_random: f64,
549}
550
551impl Default for UnisonConfig {
552 fn default() -> Self {
553 Self {
554 voices: 1,
555 detune_cents: 0.0,
556 stereo_spread: 0.0,
557 phase_random: 0.0,
558 }
559 }
560}
561
562impl UnisonConfig {
563 pub fn new(voices: usize, detune_cents: f64) -> Self {
566 Self {
567 voices: voices.max(1),
568 detune_cents,
569 stereo_spread: 0.5,
570 phase_random: 0.0,
571 }
572 }
573
574 pub fn detune_offset(&self, voice_index: usize) -> f64 {
581 if self.voices <= 1 {
582 return 0.0;
583 }
584
585 let normalized = voice_index as f64 / (self.voices - 1) as f64;
587 let centered = normalized * 2.0 - 1.0; centered * self.detune_cents / 2400.0
591 }
592
593 pub fn pan_position(&self, voice_index: usize) -> f64 {
596 if self.voices <= 1 {
597 return 0.0;
598 }
599
600 let normalized = voice_index as f64 / (self.voices - 1) as f64;
601 let centered = normalized * 2.0 - 1.0; centered * self.stereo_spread
603 }
604
605 pub fn voice_gain(&self) -> f64 {
608 1.0 / Libm::<f64>::sqrt(self.voices.max(1) as f64)
609 }
610}
611
612#[derive(Debug)]
621pub struct VoiceControl {
622 voct: AtomicU64,
623 gate: AtomicU64,
624 trigger: AtomicU64,
625 velocity: AtomicU64,
626}
627
628impl VoiceControl {
629 pub fn new() -> Self {
631 Self {
632 voct: AtomicU64::new(0f64.to_bits()),
633 gate: AtomicU64::new(0f64.to_bits()),
634 trigger: AtomicU64::new(0f64.to_bits()),
635 velocity: AtomicU64::new(1f64.to_bits()),
636 }
637 }
638
639 #[inline]
640 fn load(a: &AtomicU64) -> f64 {
641 f64::from_bits(a.load(Ordering::Relaxed))
642 }
643
644 #[inline]
645 fn store(a: &AtomicU64, v: f64) {
646 a.store(v.to_bits(), Ordering::Relaxed);
647 }
648
649 pub fn voct(&self) -> f64 {
651 Self::load(&self.voct)
652 }
653 pub fn gate(&self) -> f64 {
655 Self::load(&self.gate)
656 }
657 pub fn trigger(&self) -> f64 {
659 Self::load(&self.trigger)
660 }
661 pub fn velocity(&self) -> f64 {
663 Self::load(&self.velocity)
664 }
665
666 pub fn set_voct(&self, v: f64) {
668 Self::store(&self.voct, v);
669 }
670 pub fn set_gate(&self, v: f64) {
672 Self::store(&self.gate, v);
673 }
674 pub fn set_trigger(&self, v: f64) {
676 Self::store(&self.trigger, v);
677 }
678 pub fn set_velocity(&self, v: f64) {
680 Self::store(&self.velocity, v);
681 }
682
683 fn reset(&self) {
684 self.set_voct(0.0);
685 self.set_gate(0.0);
686 self.set_trigger(0.0);
687 self.set_velocity(1.0);
688 }
689}
690
691impl Default for VoiceControl {
692 fn default() -> Self {
693 Self::new()
694 }
695}
696
697pub struct VoiceInput {
712 control: Arc<VoiceControl>,
713 spec: PortSpec,
714 trigger_len: u32,
715 trigger_remaining: u32,
716 prev_trigger_req: f64,
717}
718
719impl VoiceInput {
720 pub fn new() -> Self {
722 Self::with_control(Arc::new(VoiceControl::new()), 48_000.0)
723 }
724
725 pub fn with_control(control: Arc<VoiceControl>, sample_rate: f64) -> Self {
728 Self {
729 control,
730 spec: PortSpec {
731 inputs: vec![],
732 outputs: vec![
733 PortDef::new(0, "voct", SignalKind::VoltPerOctave),
734 PortDef::new(1, "gate", SignalKind::Gate),
735 PortDef::new(2, "trigger", SignalKind::Trigger),
736 PortDef::new(3, "velocity", SignalKind::CvUnipolar),
737 ],
738 },
739 trigger_len: Self::trigger_len_for(sample_rate),
740 trigger_remaining: 0,
741 prev_trigger_req: 0.0,
742 }
743 }
744
745 #[inline]
746 fn trigger_len_for(sample_rate: f64) -> u32 {
747 (Libm::<f64>::round(TRIGGER_S * sample_rate)).max(1.0) as u32
748 }
749
750 pub fn control(&self) -> &Arc<VoiceControl> {
752 &self.control
753 }
754
755 pub fn set_from_voice(&mut self, voice: &Voice) {
757 self.control.set_voct(voice.voct);
758 self.control.set_gate(voice.gate);
759 self.control.set_trigger(voice.trigger);
760 self.control.set_velocity(voice.velocity);
761 }
762
763 pub fn set_voct(&mut self, voct: f64) {
765 self.control.set_voct(voct);
766 }
767
768 pub fn set_gate(&mut self, gate: f64) {
770 self.control.set_gate(gate);
771 }
772
773 pub fn set_trigger(&mut self, trigger: f64) {
775 self.control.set_trigger(trigger);
776 }
777
778 pub fn set_velocity(&mut self, velocity: f64) {
780 self.control.set_velocity(velocity);
781 }
782
783 pub fn voct(&self) -> f64 {
785 self.control.voct()
786 }
787 pub fn gate(&self) -> f64 {
789 self.control.gate()
790 }
791 pub fn trigger(&self) -> f64 {
793 self.control.trigger()
794 }
795 pub fn velocity(&self) -> f64 {
797 self.control.velocity()
798 }
799}
800
801impl Default for VoiceInput {
802 fn default() -> Self {
803 Self::new()
804 }
805}
806
807impl GraphModule for VoiceInput {
808 fn port_spec(&self) -> &PortSpec {
809 &self.spec
810 }
811
812 fn tick(&mut self, _inputs: &PortValues, outputs: &mut PortValues) {
813 outputs.set(0, self.control.voct());
814 outputs.set(1, if self.control.gate() > 0.5 { 5.0 } else { 0.0 });
815
816 let req = self.control.trigger();
818 if req > 0.5 && self.prev_trigger_req <= 0.5 {
819 self.trigger_remaining = self.trigger_len;
820 }
821 self.prev_trigger_req = req;
822 let trig_out = if self.trigger_remaining > 0 {
823 self.trigger_remaining -= 1;
824 5.0
825 } else {
826 0.0
827 };
828 outputs.set(2, trig_out);
829
830 outputs.set(3, self.control.velocity() * 10.0); }
832
833 fn reset(&mut self) {
834 self.control.reset();
835 self.trigger_remaining = 0;
836 self.prev_trigger_req = 0.0;
837 }
838
839 fn set_sample_rate(&mut self, sample_rate: f64) {
840 self.trigger_len = Self::trigger_len_for(sample_rate);
841 }
842
843 fn type_id(&self) -> &'static str {
844 "voice_input"
845 }
846}
847
848type VoiceBuilder = dyn Fn(&mut Patch, &NodeHandle) -> Result<(), PatchError>;
854
855struct SubVoice {
860 patch: Patch,
861 control: Arc<VoiceControl>,
862 controller: NodeHandle,
863}
864
865struct VoiceSlot {
868 subs: Vec<SubVoice>,
869 follower: f64,
871}
872
873pub struct PolyPatch {
888 allocator: VoiceAllocator,
890 voices: Vec<VoiceSlot>,
892 unison: UnisonConfig,
894 sample_rate: f64,
896 builder: Option<Box<VoiceBuilder>>,
898 smoothed_count: f64,
900 follower_coeff: f64,
902 count_coeff: f64,
903 grace_samples: u64,
904 output_left: f64,
906 output_right: f64,
907}
908
909impl PolyPatch {
910 pub fn new(num_voices: usize, sample_rate: f64) -> Self {
915 Self::build(num_voices, sample_rate, None).expect("empty voice build cannot fail")
917 }
918
919 pub fn with_voice_fn<F>(
926 num_voices: usize,
927 sample_rate: f64,
928 builder: F,
929 ) -> Result<Self, PatchError>
930 where
931 F: Fn(&mut Patch, &NodeHandle) -> Result<(), PatchError> + 'static,
932 {
933 Self::build(num_voices, sample_rate, Some(Box::new(builder)))
934 }
935
936 fn build(
937 num_voices: usize,
938 sample_rate: f64,
939 builder: Option<Box<VoiceBuilder>>,
940 ) -> Result<Self, PatchError> {
941 let mut poly = Self {
942 allocator: VoiceAllocator::new(num_voices),
943 voices: Vec::new(),
944 unison: UnisonConfig::default(),
945 sample_rate,
946 builder,
947 smoothed_count: 0.0,
948 follower_coeff: 0.0,
949 count_coeff: 0.0,
950 grace_samples: 0,
951 output_left: 0.0,
952 output_right: 0.0,
953 };
954 poly.recompute_coeffs();
955 poly.voices = poly.build_voices()?;
956 Ok(poly)
957 }
958
959 fn recompute_coeffs(&mut self) {
960 self.follower_coeff = one_pole_coeff(FOLLOWER_TAU_S, self.sample_rate);
961 self.count_coeff = one_pole_coeff(COUNT_TAU_S, self.sample_rate);
962 self.grace_samples = (GRACE_S * self.sample_rate).max(1.0) as u64;
963 self.allocator
964 .set_release_criteria(RELEASE_THRESHOLD, self.grace_samples);
965 self.allocator
966 .set_max_release_samples(Self::release_cap_samples(MAX_RELEASE_S, self.sample_rate));
967 }
968
969 fn release_cap_samples(seconds: f64, sample_rate: f64) -> u64 {
972 let samples = seconds * sample_rate;
973 if !samples.is_finite() || samples <= 0.0 {
974 u64::MAX
975 } else {
976 (samples as u64).max(1)
977 }
978 }
979
980 pub fn set_max_release_time(&mut self, seconds: f64) {
985 self.allocator
986 .set_max_release_samples(Self::release_cap_samples(seconds, self.sample_rate));
987 }
988
989 fn build_voices(&self) -> Result<Vec<VoiceSlot>, PatchError> {
993 let unison_voices = self.unison.voices.max(1);
994 let mut voices = Vec::with_capacity(self.allocator.num_voices());
995 for _ in 0..self.allocator.num_voices() {
996 let mut subs = Vec::with_capacity(unison_voices);
997 for _ in 0..unison_voices {
998 let control = Arc::new(VoiceControl::new());
999 let mut patch = Patch::new(self.sample_rate);
1000 let controller = patch.add(
1001 "voice_ctrl",
1002 VoiceInput::with_control(control.clone(), self.sample_rate),
1003 );
1004 if let Some(builder) = &self.builder {
1005 builder(&mut patch, &controller)?;
1006 }
1007 patch.compile()?;
1008 subs.push(SubVoice {
1009 patch,
1010 control,
1011 controller,
1012 });
1013 }
1014 voices.push(VoiceSlot {
1015 subs,
1016 follower: 0.0,
1017 });
1018 }
1019 Ok(voices)
1020 }
1021
1022 pub fn sample_rate(&self) -> f64 {
1024 self.sample_rate
1025 }
1026
1027 pub fn num_voices(&self) -> usize {
1029 self.allocator.num_voices()
1030 }
1031
1032 pub fn set_sample_rate(&mut self, sample_rate: f64) {
1039 self.sample_rate = sample_rate;
1040 self.recompute_coeffs();
1041 if let Ok(voices) = self.build_voices() {
1042 self.voices = voices;
1043 }
1044 self.smoothed_count = 0.0;
1045 }
1046
1047 pub fn voice_control(&self, index: usize) -> Option<&Arc<VoiceControl>> {
1049 self.voices
1050 .get(index)
1051 .and_then(|v| v.subs.first())
1052 .map(|s| &s.control)
1053 }
1054
1055 pub fn voice_controller(&self, index: usize) -> Option<&NodeHandle> {
1058 self.voices
1059 .get(index)
1060 .and_then(|v| v.subs.first())
1061 .map(|s| &s.controller)
1062 }
1063
1064 pub fn allocator(&self) -> &VoiceAllocator {
1066 &self.allocator
1067 }
1068
1069 pub fn allocator_mut(&mut self) -> &mut VoiceAllocator {
1071 &mut self.allocator
1072 }
1073
1074 pub fn set_unison(&mut self, config: UnisonConfig) {
1078 let count_changed = config.voices.max(1) != self.unison.voices.max(1);
1079 self.unison = config;
1080 if count_changed {
1081 if let Ok(voices) = self.build_voices() {
1082 self.voices = voices;
1083 }
1084 }
1085 }
1086
1087 pub fn unison(&self) -> &UnisonConfig {
1089 &self.unison
1090 }
1091
1092 pub fn voice_patch(&self, index: usize) -> Option<&Patch> {
1094 self.voices
1095 .get(index)
1096 .and_then(|v| v.subs.first())
1097 .map(|s| &s.patch)
1098 }
1099
1100 pub fn voice_patch_mut(&mut self, index: usize) -> Option<&mut Patch> {
1102 self.voices
1103 .get_mut(index)
1104 .and_then(|v| v.subs.first_mut())
1105 .map(|s| &mut s.patch)
1106 }
1107
1108 pub fn note_on(&mut self, note: u8, velocity: u8) {
1113 let velocity_f = velocity as f64 / 127.0;
1114 if let Some(idx) = self.allocator.note_on(note, velocity_f) {
1115 if self.allocator.last_stolen() == Some(idx) {
1116 if let Some(slot) = self.voices.get_mut(idx) {
1117 for sub in &mut slot.subs {
1118 sub.patch.reset();
1119 }
1120 slot.follower = 0.0;
1121 }
1122 }
1123 }
1124 }
1125
1126 pub fn note_off(&mut self, note: u8) {
1128 self.allocator.note_off(note);
1129 }
1130
1131 pub fn all_notes_off(&mut self) {
1133 self.allocator.all_notes_off();
1134 }
1135
1136 pub fn panic(&mut self) {
1138 self.allocator.panic();
1139 }
1140
1141 pub fn compile(&mut self) -> Result<(), PatchError> {
1143 for slot in &mut self.voices {
1144 for sub in &mut slot.subs {
1145 sub.patch.compile()?;
1146 }
1147 }
1148 Ok(())
1149 }
1150
1151 pub fn compensation_gain(&self) -> f64 {
1155 1.0 / Libm::<f64>::sqrt(self.smoothed_count.max(1.0))
1156 }
1157
1158 pub fn tick(&mut self) -> (f64, f64) {
1160 let unison = self.unison.clone();
1163 let unison_voices = unison.voices.max(1);
1164 let unison_gain = unison.voice_gain();
1165 let use_pan = unison_voices > 1 && unison.stereo_spread != 0.0;
1166 let follower_coeff = self.follower_coeff;
1167
1168 let inst_count = self.allocator.active_count() as f64;
1170 self.smoothed_count =
1171 self.count_coeff * self.smoothed_count + (1.0 - self.count_coeff) * inst_count;
1172
1173 let mut left = 0.0;
1174 let mut right = 0.0;
1175
1176 for i in 0..self.voices.len() {
1177 let (state, base_voct, gate, trigger, velocity) = {
1178 let v = &self.allocator.voices()[i];
1179 (v.state, v.voct, v.gate, v.trigger, v.velocity)
1180 };
1181
1182 if state == VoiceState::Free {
1183 self.voices[i].follower = 0.0;
1184 continue;
1185 }
1186
1187 let slot = &mut self.voices[i];
1188 let mut peak = 0.0;
1189 for (u, sub) in slot.subs.iter_mut().enumerate() {
1190 sub.control.set_voct(base_voct + unison.detune_offset(u));
1192 sub.control.set_gate(gate);
1193 sub.control.set_trigger(trigger);
1194 sub.control.set_velocity(velocity);
1195
1196 let (l, r) = sub.patch.tick();
1197
1198 let (lg, rg) = if use_pan {
1199 balance_gains(unison.pan_position(u))
1200 } else {
1201 (1.0, 1.0)
1202 };
1203 let sl = l * lg * unison_gain;
1204 let sr = r * rg * unison_gain;
1205 left += sl;
1206 right += sr;
1207
1208 let mag = Libm::<f64>::fabs(sl).max(Libm::<f64>::fabs(sr));
1209 if mag > peak {
1210 peak = mag;
1211 }
1212 }
1213
1214 slot.follower = follower_coeff * slot.follower + (1.0 - follower_coeff) * peak;
1216 let level = slot.follower;
1217 self.allocator.set_envelope_level(i, level);
1218 }
1219
1220 self.allocator.tick();
1223
1224 let g = 1.0 / Libm::<f64>::sqrt(self.smoothed_count.max(1.0));
1226 left *= g;
1227 right *= g;
1228
1229 self.output_left = left;
1230 self.output_right = right;
1231 (left, right)
1232 }
1233
1234 pub fn output(&self) -> (f64, f64) {
1236 (self.output_left, self.output_right)
1237 }
1238
1239 pub fn reset(&mut self) {
1241 for slot in &mut self.voices {
1242 slot.follower = 0.0;
1243 for sub in &mut slot.subs {
1244 sub.patch.reset();
1245 }
1246 }
1247 self.allocator.panic();
1248 self.smoothed_count = 0.0;
1249 self.output_left = 0.0;
1250 self.output_right = 0.0;
1251 }
1252}
1253
1254pub struct VoiceMixer {
1256 num_voices: usize,
1257 spec: PortSpec,
1258}
1259
1260impl VoiceMixer {
1261 pub fn new(num_voices: usize) -> Self {
1263 let mut inputs = Vec::with_capacity(num_voices * 2);
1264 for i in 0..num_voices {
1265 inputs.push(PortDef::new(
1266 i as u32 * 2,
1267 format!("in{}_l", i),
1268 SignalKind::Audio,
1269 ));
1270 inputs.push(PortDef::new(
1271 i as u32 * 2 + 1,
1272 format!("in{}_r", i),
1273 SignalKind::Audio,
1274 ));
1275 }
1276
1277 Self {
1278 num_voices,
1279 spec: PortSpec {
1280 inputs,
1281 outputs: vec![
1282 PortDef::new(100, "left", SignalKind::Audio),
1283 PortDef::new(101, "right", SignalKind::Audio),
1284 ],
1285 },
1286 }
1287 }
1288}
1289
1290impl GraphModule for VoiceMixer {
1291 fn port_spec(&self) -> &PortSpec {
1292 &self.spec
1293 }
1294
1295 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1296 let mut left = 0.0;
1297 let mut right = 0.0;
1298
1299 for i in 0..self.num_voices {
1300 left += inputs.get_or(i as u32 * 2, 0.0);
1301 right += inputs.get_or(i as u32 * 2 + 1, 0.0);
1302 }
1303
1304 outputs.set(100, left);
1305 outputs.set(101, right);
1306 }
1307
1308 fn reset(&mut self) {}
1309
1310 fn set_sample_rate(&mut self, _: f64) {}
1311
1312 fn type_id(&self) -> &'static str {
1313 "voice_mixer"
1314 }
1315}
1316
1317#[cfg(test)]
1318mod tests {
1319 use super::*;
1320 use crate::modules::{Adsr, StereoOutput, Vca, Vco};
1321
1322 struct DcSource {
1325 value: f64,
1326 spec: PortSpec,
1327 }
1328
1329 impl DcSource {
1330 fn new(value: f64) -> Self {
1331 Self {
1332 value,
1333 spec: PortSpec {
1334 inputs: vec![],
1335 outputs: vec![PortDef::new(0, "out", SignalKind::Audio)],
1336 },
1337 }
1338 }
1339 }
1340
1341 impl GraphModule for DcSource {
1342 fn port_spec(&self) -> &PortSpec {
1343 &self.spec
1344 }
1345 fn tick(&mut self, _inputs: &PortValues, outputs: &mut PortValues) {
1346 outputs.set(0, self.value);
1347 }
1348 fn reset(&mut self) {}
1349 fn set_sample_rate(&mut self, _: f64) {}
1350 fn type_id(&self) -> &'static str {
1351 "dc_source"
1352 }
1353 }
1354
1355 #[test]
1358 fn test_voice_allocation_basic() {
1359 let mut allocator = VoiceAllocator::new(4);
1360
1361 let voice1 = allocator.note_on(60, 0.8);
1362 assert_eq!(voice1, Some(0));
1363 assert_eq!(allocator.active_count(), 1);
1364
1365 let voice2 = allocator.note_on(64, 0.7);
1366 assert_eq!(voice2, Some(1));
1367 assert_eq!(allocator.active_count(), 2);
1368
1369 allocator.note_off(60);
1370 assert_eq!(allocator.active_count(), 2); allocator.tick();
1373 }
1374
1375 #[test]
1376 fn test_voice_allocation_retrigger() {
1377 let mut allocator = VoiceAllocator::new(4);
1378
1379 let voice1 = allocator.note_on(60, 0.8);
1380 assert_eq!(voice1, Some(0));
1381
1382 let voice2 = allocator.note_on(60, 0.9);
1383 assert_eq!(voice2, Some(0));
1384 assert_eq!(allocator.active_count(), 1);
1385 }
1386
1387 #[test]
1389 fn test_retrigger_updates_lru() {
1390 let mut allocator = VoiceAllocator::new(3);
1391
1392 allocator.note_on(60, 1.0); allocator.note_on(62, 1.0); allocator.note_on(64, 1.0); allocator.note_off(60);
1397 allocator.note_off(62);
1398 allocator.note_off(64);
1399 allocator.panic();
1401
1402 assert_eq!(allocator.note_on(60, 1.0), Some(0));
1405 assert_eq!(allocator.note_on(60, 1.0), Some(0)); allocator.voice_mut(0).unwrap().free();
1410 allocator.voice_mut(1).unwrap().free();
1411 allocator.voice_mut(2).unwrap().free();
1412 let next = allocator.note_on(67, 1.0).unwrap();
1415 assert_ne!(next, 0, "retrigger should have pushed voice 0 to LRU back");
1416 }
1417
1418 #[test]
1421 fn test_voice_stealing_oldest_active() {
1422 let mut allocator = VoiceAllocator::new(2);
1423 allocator.set_mode(AllocationMode::OldestSteal);
1424
1425 allocator.note_on(60, 0.8);
1426 allocator.tick();
1427 allocator.note_on(62, 0.7);
1428 allocator.tick();
1429
1430 let stolen = allocator.note_on(64, 0.6);
1432 assert_eq!(stolen, Some(0));
1433 assert_eq!(allocator.last_stolen(), Some(0));
1434 }
1435
1436 #[test]
1437 fn test_voice_stealing_prefers_releasing() {
1438 let mut allocator = VoiceAllocator::new(2);
1439 allocator.set_mode(AllocationMode::OldestSteal);
1440
1441 allocator.note_on(60, 0.8);
1443 allocator.tick();
1444 allocator.note_on(62, 0.7);
1445 allocator.tick();
1446 allocator.note_off(62); let stolen = allocator.note_on(64, 0.6);
1452 assert_eq!(stolen, Some(1));
1453 assert_eq!(allocator.last_stolen(), Some(1));
1454 }
1455
1456 #[test]
1457 fn test_quietest_steal_uses_real_levels() {
1458 let mut allocator = VoiceAllocator::new(3);
1459 allocator.set_mode(AllocationMode::QuietestSteal);
1460
1461 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);
1467 allocator.set_envelope_level(1, 0.1);
1468 allocator.set_envelope_level(2, 0.7);
1469
1470 let stolen = allocator.note_on(67, 1.0);
1471 assert_eq!(
1472 stolen,
1473 Some(1),
1474 "QuietestSteal should pick the lowest level"
1475 );
1476 }
1477
1478 #[test]
1479 fn test_no_steal_mode_drops_note() {
1480 let mut allocator = VoiceAllocator::new(2);
1481 allocator.set_mode(AllocationMode::NoSteal);
1482
1483 allocator.note_on(60, 0.8);
1484 allocator.note_on(62, 0.7);
1485
1486 let result = allocator.note_on(64, 0.6);
1488 assert_eq!(result, None);
1489 assert_eq!(allocator.last_stolen(), None);
1490 }
1491
1492 #[test]
1494 fn test_priority_mode_drop_when_no_victim() {
1495 let mut allocator = VoiceAllocator::new(2);
1496 allocator.set_mode(AllocationMode::HighestPriority);
1497
1498 allocator.note_on(80, 1.0);
1501 allocator.note_on(84, 1.0);
1502 assert_eq!(allocator.note_on(60, 1.0), None);
1503
1504 let stolen = allocator.note_on(90, 1.0);
1506 assert_eq!(stolen, Some(0));
1507 }
1508
1509 #[test]
1512 fn test_midi_note_to_voct() {
1513 assert!((midi_note_to_voct(60) - 0.0).abs() < 0.001);
1514 assert!((midi_note_to_voct(72) - 1.0).abs() < 0.001);
1515 assert!((midi_note_to_voct(48) - (-1.0)).abs() < 0.001);
1516 }
1517
1518 #[test]
1519 fn test_voct_to_midi_note() {
1520 assert_eq!(voct_to_midi_note(0.0), 60);
1521 assert_eq!(voct_to_midi_note(1.0), 72);
1522 assert_eq!(voct_to_midi_note(-1.0), 48);
1523 }
1524
1525 #[test]
1528 fn test_unison_detune_total_spread() {
1529 let config = UnisonConfig::new(3, 10.0);
1531
1532 let d0 = config.detune_offset(0);
1533 let d1 = config.detune_offset(1);
1534 let d2 = config.detune_offset(2);
1535
1536 assert!(d0 < 0.0);
1537 assert!((d1 - 0.0).abs() < 1e-9);
1538 assert!(d2 > 0.0);
1539 assert!((d0 + d2).abs() < 1e-9, "spread must be symmetric");
1540
1541 let d0_cents = d0 * 1200.0;
1544 let d2_cents = d2 * 1200.0;
1545 let span_cents = (d2 - d0) * 1200.0;
1546 assert!((d0_cents + 5.0).abs() < 1e-6, "low edge should be -5 cents");
1547 assert!(
1548 (d2_cents - 5.0).abs() < 1e-6,
1549 "high edge should be +5 cents"
1550 );
1551 assert!(
1552 (span_cents - 10.0).abs() < 1e-6,
1553 "total spread should equal detune_cents (got {span_cents})"
1554 );
1555 }
1556
1557 #[test]
1558 fn test_unison_pan() {
1559 let mut config = UnisonConfig::new(3, 10.0);
1560 config.stereo_spread = 1.0;
1561
1562 assert!((config.pan_position(0) - (-1.0)).abs() < 0.001);
1563 assert!((config.pan_position(1) - 0.0).abs() < 0.001);
1564 assert!((config.pan_position(2) - 1.0).abs() < 0.001);
1565 }
1566
1567 #[test]
1568 fn test_unison_config_voice_gain() {
1569 let config = UnisonConfig::new(4, 10.0);
1570 let gain = config.voice_gain();
1571 assert!((gain - 0.5).abs() < 1e-9); }
1573
1574 #[test]
1577 fn test_balance_gains_center_unity() {
1578 let (l, r) = balance_gains(0.0);
1579 assert!((l - 1.0).abs() < 1e-9 && (r - 1.0).abs() < 1e-9);
1580 }
1581
1582 #[test]
1583 fn test_balance_gains_partial() {
1584 let (l, r) = balance_gains(-0.5); assert!((l - 1.0).abs() < 1e-9 && (r - 0.5).abs() < 1e-9);
1586 let (l, r) = balance_gains(0.5); assert!((l - 0.5).abs() < 1e-9 && (r - 1.0).abs() < 1e-9);
1588 }
1589
1590 #[test]
1593 fn test_voice_input_module() {
1594 let mut input = VoiceInput::new();
1595 let mut outputs = PortValues::new();
1596
1597 input.set_voct(0.5);
1598 input.set_gate(1.0);
1599 input.set_velocity(0.8);
1600
1601 input.tick(&PortValues::new(), &mut outputs);
1602
1603 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); }
1607
1608 #[test]
1609 fn test_voice_input_trigger_pulse_in_samples() {
1610 let mut input = VoiceInput::with_control(Arc::new(VoiceControl::new()), 48_000.0);
1612 let mut outputs = PortValues::new();
1613
1614 input.set_trigger(1.0); input.tick(&PortValues::new(), &mut outputs);
1616 assert!(outputs.get_or(2, 0.0) > 2.5, "trigger high on first sample");
1617
1618 input.set_trigger(0.0);
1620 input.tick(&PortValues::new(), &mut outputs);
1621 assert!(
1622 outputs.get_or(2, 0.0) > 2.5,
1623 "trigger pulse should last several samples, not one"
1624 );
1625
1626 for _ in 0..64 {
1628 input.tick(&PortValues::new(), &mut outputs);
1629 }
1630 assert!(outputs.get_or(2, 0.0) < 2.5, "trigger pulse should end");
1631 }
1632
1633 #[test]
1634 fn test_voice_input_default() {
1635 let input = VoiceInput::default();
1636 assert!((input.voct() - 0.0).abs() < 0.001);
1637 }
1638
1639 #[test]
1640 fn test_voice_input_set_from_voice() {
1641 let mut voice = Voice::new(0);
1642 voice.note_on(72, 0.8);
1643
1644 let mut input = VoiceInput::new();
1645 input.set_from_voice(&voice);
1646
1647 assert!((input.voct() - 1.0).abs() < 0.001);
1648 assert!((input.velocity() - 0.8).abs() < 0.001);
1649 }
1650
1651 #[test]
1652 fn test_voice_input_reset_type_id() {
1653 let mut input = VoiceInput::new();
1654 input.set_voct(1.0);
1655 input.reset();
1656 assert!((input.voct() - 0.0).abs() < 0.001);
1657 assert_eq!(input.type_id(), "voice_input");
1658 input.set_sample_rate(48000.0);
1659 }
1660
1661 #[test]
1662 fn test_voice_input_set_trigger() {
1663 let mut input = VoiceInput::new();
1664 input.set_trigger(1.0);
1665 assert!((input.trigger() - 1.0).abs() < 0.001);
1666 }
1667
1668 #[test]
1671 fn test_voice_is_free() {
1672 let voice = Voice::new(0);
1673 assert!(voice.is_free());
1674
1675 let mut playing = Voice::new(1);
1676 playing.note_on(60, 1.0);
1677 assert!(!playing.is_free());
1678 }
1679
1680 #[test]
1681 fn test_voice_is_playing_note() {
1682 let mut voice = Voice::new(0);
1683 voice.note_on(60, 1.0);
1684 assert!(voice.is_playing_note(60));
1685 assert!(!voice.is_playing_note(61));
1686 }
1687
1688 #[test]
1689 fn test_voice_note_off_and_free() {
1690 let mut voice = Voice::new(0);
1691 voice.note_on(60, 1.0);
1692 voice.note_off();
1693 assert!(voice.state == VoiceState::Releasing);
1694
1695 voice.free();
1696 assert!(voice.is_free());
1697 }
1698
1699 #[test]
1700 fn test_voice_tick_clears_trigger_and_counts_release() {
1701 let mut voice = Voice::new(0);
1702 voice.note_on(60, 1.0);
1703 voice.tick();
1704 assert!(voice.trigger == 0.0);
1705 assert_eq!(voice.release_samples, 0); voice.note_off();
1708 voice.tick();
1709 assert_eq!(voice.release_samples, 1); }
1711
1712 #[test]
1715 fn test_voice_allocator_mode() {
1716 let mut allocator = VoiceAllocator::new(4);
1717 allocator.set_mode(AllocationMode::QuietestSteal);
1718 assert_eq!(allocator.mode(), AllocationMode::QuietestSteal);
1719 }
1720
1721 #[test]
1722 fn test_voice_allocator_num_voices() {
1723 let allocator = VoiceAllocator::new(8);
1724 assert_eq!(allocator.num_voices(), 8);
1725 }
1726
1727 #[test]
1728 fn test_voice_allocator_voice_access() {
1729 let mut allocator = VoiceAllocator::new(4);
1730 assert!(allocator.voice(0).is_some());
1731 assert!(allocator.voice_mut(0).is_some());
1732 }
1733
1734 #[test]
1735 fn test_voice_allocator_voices() {
1736 let allocator = VoiceAllocator::new(4);
1737 assert_eq!(allocator.voices().len(), 4);
1738 }
1739
1740 #[test]
1741 fn test_voice_allocator_voices_mut() {
1742 let mut allocator = VoiceAllocator::new(4);
1743 assert_eq!(allocator.voices_mut().len(), 4);
1744 }
1745
1746 #[test]
1747 fn test_voice_allocator_all_notes_off() {
1748 let mut allocator = VoiceAllocator::new(4);
1749 allocator.note_on(60, 1.0);
1750 allocator.note_on(64, 1.0);
1751 allocator.all_notes_off();
1752 assert!(allocator
1753 .voices()
1754 .iter()
1755 .all(|v| v.state == VoiceState::Releasing || v.state == VoiceState::Free));
1756 }
1757
1758 #[test]
1759 fn test_voice_allocator_tick() {
1760 let mut allocator = VoiceAllocator::new(4);
1761 allocator.note_on(60, 1.0);
1762 allocator.tick();
1763 }
1764
1765 #[test]
1766 fn test_voice_allocator_set_envelope_level() {
1767 let mut allocator = VoiceAllocator::new(4);
1768 if let Some(i) = allocator.note_on(60, 1.0) {
1769 allocator.set_envelope_level(i, 0.5);
1770 assert!((allocator.voice(i).unwrap().envelope_level - 0.5).abs() < 1e-9);
1771 }
1772 }
1773
1774 #[test]
1775 fn test_voice_allocator_release_grace_keeps_voice() {
1776 let mut allocator = VoiceAllocator::new(1);
1777 allocator.set_release_criteria(0.001, 100); allocator.note_on(60, 1.0);
1779 allocator.note_off(60);
1780
1781 allocator.set_envelope_level(0, 0.0);
1783 for _ in 0..50 {
1784 allocator.set_envelope_level(0, 0.0);
1785 allocator.tick();
1786 }
1787 assert_eq!(allocator.voice(0).unwrap().state, VoiceState::Releasing);
1788
1789 for _ in 0..100 {
1791 allocator.set_envelope_level(0, 0.0);
1792 allocator.tick();
1793 }
1794 assert_eq!(allocator.voice(0).unwrap().state, VoiceState::Free);
1795 }
1796
1797 #[test]
1800 fn test_poly_patch_basic() {
1801 let mut poly = PolyPatch::new(4, 44100.0);
1802 poly.note_on(60, 100);
1803 assert_eq!(poly.allocator().active_count(), 1);
1804 poly.note_on(64, 90);
1805 assert_eq!(poly.allocator().active_count(), 2);
1806 poly.note_off(60);
1807 }
1808
1809 #[test]
1810 fn test_poly_patch_panic() {
1811 let mut poly = PolyPatch::new(4, 44100.0);
1812 poly.note_on(60, 100);
1813 poly.note_on(64, 90);
1814 poly.note_on(67, 80);
1815 poly.panic();
1816 assert_eq!(poly.allocator().active_count(), 0);
1817 }
1818
1819 #[test]
1820 fn test_poly_patch_sample_rate() {
1821 let poly = PolyPatch::new(4, 48000.0);
1822 assert_eq!(poly.sample_rate(), 48000.0);
1823 }
1824
1825 #[test]
1826 fn test_poly_patch_set_sample_rate() {
1827 let mut poly = PolyPatch::new(4, 44100.0);
1828 poly.set_sample_rate(48000.0);
1829 assert_eq!(poly.sample_rate(), 48000.0);
1830 }
1831
1832 #[test]
1833 fn test_poly_patch_controller_access() {
1834 let poly = PolyPatch::new(4, 44100.0);
1835 assert!(poly.voice_control(0).is_some());
1836 assert!(poly.voice_controller(0).is_some());
1837 assert!(poly.voice_control(99).is_none());
1838 }
1839
1840 #[test]
1841 fn test_poly_patch_allocator_mut() {
1842 let mut poly = PolyPatch::new(4, 44100.0);
1843 poly.allocator_mut().set_mode(AllocationMode::OldestSteal);
1844 assert_eq!(poly.allocator().mode(), AllocationMode::OldestSteal);
1845 }
1846
1847 #[test]
1848 fn test_poly_patch_unison() {
1849 let mut poly = PolyPatch::new(4, 44100.0);
1850 poly.set_unison(UnisonConfig::new(2, 5.0));
1851 assert_eq!(poly.unison().voices, 2);
1852 }
1853
1854 #[test]
1855 fn test_poly_patch_voice_patch_access() {
1856 let mut poly = PolyPatch::new(4, 44100.0);
1857 assert_eq!(poly.num_voices(), 4);
1858 assert!(poly.voice_patch(0).is_some());
1859 assert!(poly.voice_patch_mut(0).is_some());
1860 assert!(poly.voice_patch(99).is_none());
1861 }
1862
1863 #[test]
1864 fn test_poly_patch_all_notes_off() {
1865 let mut poly = PolyPatch::new(4, 44100.0);
1866 poly.note_on(60, 100);
1867 poly.note_on(64, 100);
1868 poly.all_notes_off();
1869 }
1870
1871 #[test]
1872 fn test_poly_patch_compile_tick_output() {
1873 let mut poly = PolyPatch::new(2, 44100.0);
1874 poly.compile().unwrap();
1875 poly.note_on(60, 100);
1876 poly.tick();
1877 let (left, right) = poly.output();
1878 let _ = (left, right);
1879 }
1880
1881 #[test]
1882 fn test_poly_patch_reset() {
1883 let mut poly = PolyPatch::new(4, 44100.0);
1884 poly.note_on(60, 100);
1885 poly.reset();
1886 assert_eq!(poly.allocator().active_count(), 0);
1887 }
1888
1889 fn build_synth(num_voices: usize, sr: f64) -> PolyPatch {
1894 PolyPatch::with_voice_fn(num_voices, sr, |patch, ctrl| {
1895 let sr = patch.sample_rate();
1896 let vco = patch.add("vco", Vco::new(sr));
1897 let adsr = patch.add("adsr", Adsr::new(sr));
1898 let vca = patch.add("vca", Vca::new());
1899 let out = patch.add("out", StereoOutput::new());
1900 patch.connect(ctrl.out("voct"), vco.in_("voct"))?;
1901 patch.connect(ctrl.out("gate"), adsr.in_("gate"))?;
1902 patch.connect(vco.out("sin"), vca.in_("in"))?;
1903 patch.connect(adsr.out("env"), vca.in_("cv"))?;
1904 patch.connect(vca.out("out"), out.in_("left"))?;
1905 patch.set_output(out.id());
1906 Ok(())
1907 })
1908 .unwrap()
1909 }
1910
1911 fn measure_period_samples(poly: &mut PolyPatch, warmup: usize, window: usize) -> f64 {
1913 for _ in 0..warmup {
1914 poly.tick();
1915 }
1916 let mut prev = 0.0;
1917 let mut crossings = Vec::new();
1918 for n in 0..window {
1919 let (l, _r) = poly.tick();
1920 if prev <= 0.0 && l > 0.0 {
1921 crossings.push(n);
1922 }
1923 prev = l;
1924 }
1925 assert!(crossings.len() >= 2, "need at least two zero crossings");
1926 let span = (crossings[crossings.len() - 1] - crossings[0]) as f64;
1927 span / (crossings.len() - 1) as f64
1928 }
1929
1930 #[test]
1931 fn test_e2e_pitch_tracks_note() {
1932 let sr = 48_000.0;
1933 let mut poly = build_synth(1, sr);
1934
1935 poly.note_on(60, 100);
1937 let p_c4 = measure_period_samples(&mut poly, 4000, 8000);
1938 poly.note_off(60);
1939 for _ in 0..(sr as usize / 5) {
1940 poly.tick(); }
1942
1943 poly.note_on(72, 100);
1944 let p_c5 = measure_period_samples(&mut poly, 4000, 8000);
1945
1946 let expect_c4 = sr / 261.63;
1948 let expect_c5 = sr / 523.25;
1949 assert!(
1950 (p_c4 - expect_c4).abs() / expect_c4 < 0.05,
1951 "C4 period {p_c4} vs expected {expect_c4}"
1952 );
1953 assert!(
1954 (p_c5 - expect_c5).abs() / expect_c5 < 0.05,
1955 "C5 period {p_c5} vs expected {expect_c5}"
1956 );
1957 assert!(
1958 (p_c4 / p_c5 - 2.0).abs() < 0.1,
1959 "octave should halve the period (ratio {})",
1960 p_c4 / p_c5
1961 );
1962 }
1963
1964 #[test]
1965 fn test_e2e_gate_reaches_adsr_and_release_tail_completes() {
1966 let sr = 48_000.0;
1967 let mut poly = build_synth(1, sr);
1968
1969 poly.note_on(60, 127);
1970
1971 let mut sustain_peak = 0.0f64;
1973 for _ in 0..4800 {
1974 poly.tick();
1975 }
1976 for _ in 0..2000 {
1977 let (l, _r) = poly.tick();
1978 sustain_peak = sustain_peak.max(l.abs());
1979 }
1980 assert!(
1981 sustain_peak > 0.1,
1982 "gate should drive the ADSR/VCA (sustain peak {sustain_peak})"
1983 );
1984
1985 poly.note_off(60);
1987 poly.tick();
1988 assert_ne!(
1989 poly.allocator().voice(0).unwrap().state,
1990 VoiceState::Free,
1991 "voice freed one sample after note-off (truncated release)"
1992 );
1993
1994 let mut just_after = 0.0f64;
1997 for _ in 0..200 {
1998 let (l, _r) = poly.tick();
1999 just_after = just_after.max(l.abs());
2000 }
2001 assert!(
2002 just_after > 0.02,
2003 "release tail truncated (amp {just_after} right after note-off)"
2004 );
2005
2006 let mut freed = false;
2008 for _ in 0..(sr as usize / 2) {
2009 poly.tick();
2010 if poly.allocator().voice(0).unwrap().state == VoiceState::Free {
2011 freed = true;
2012 break;
2013 }
2014 }
2015 assert!(freed, "released voice should eventually free");
2016 }
2017
2018 #[test]
2021 fn test_e2e_sample_rate_propagates_to_voices() {
2022 let sr1 = 48_000.0;
2023 let mut poly = build_synth(1, sr1);
2024 poly.note_on(60, 100);
2025 let p1 = measure_period_samples(&mut poly, 4000, 8000);
2026
2027 poly.set_sample_rate(sr1 / 2.0);
2031 poly.note_on(60, 100);
2032 let p2 = measure_period_samples(&mut poly, 2000, 4000);
2033
2034 assert!(
2035 (p2 / p1 - 0.5).abs() < 0.1,
2036 "half sample rate should halve the period in samples (p1={p1}, p2={p2})"
2037 );
2038 }
2039
2040 fn build_dc_poly(num_voices: usize, value: f64, sr: f64) -> PolyPatch {
2043 PolyPatch::with_voice_fn(num_voices, sr, move |patch, _ctrl| {
2044 let dc = patch.add("dc", DcSource::new(value));
2045 let out = patch.add("out", StereoOutput::new());
2046 patch.connect(dc.out("out"), out.in_("left"))?;
2047 patch.set_output(out.id());
2048 Ok(())
2049 })
2050 .unwrap()
2051 }
2052
2053 #[test]
2054 fn test_single_voice_unity_gain() {
2055 let sr = 48_000.0;
2057 let mut poly = build_dc_poly(4, 1.0, sr);
2058 poly.note_on(60, 100);
2059 let mut out = (0.0, 0.0);
2060 for _ in 0..2000 {
2061 out = poly.tick();
2062 }
2063 assert!(
2064 (out.0 - 1.0).abs() < 0.01 && (out.1 - 1.0).abs() < 0.01,
2065 "single voice should pass at unity gain, got {out:?}"
2066 );
2067 }
2068
2069 #[test]
2070 fn test_eight_voices_bounded_output() {
2071 let sr = 48_000.0;
2072 let mut poly = build_dc_poly(8, 1.0, sr);
2073
2074 poly.note_on(60, 100);
2076 let mut single = 0.0;
2077 for _ in 0..2000 {
2078 single = poly.tick().0;
2079 }
2080 poly.panic();
2081 for _ in 0..2000 {
2082 poly.tick();
2083 }
2084
2085 for n in 0..8u8 {
2087 poly.note_on(60 + n, 100);
2088 }
2089 let mut eight = 0.0;
2090 for _ in 0..4000 {
2091 eight = poly.tick().0;
2092 }
2093
2094 assert!((single - 1.0).abs() < 0.01);
2095 assert!(
2096 eight < 8.0 * single - 0.5,
2097 "8 voices must be well below 8x single ({eight} vs {})",
2098 8.0 * single
2099 );
2100 assert!(
2102 (eight / single - 8.0f64.sqrt()).abs() < 0.3,
2103 "8-voice sum should follow 1/sqrt(N) (ratio {})",
2104 eight / single
2105 );
2106 }
2107
2108 #[test]
2109 fn test_gain_compensation_is_smooth() {
2110 let sr = 48_000.0;
2111 let mut poly = PolyPatch::new(8, sr);
2112
2113 poly.note_on(60, 100);
2114 for _ in 0..4000 {
2115 poly.tick();
2116 }
2117 let g_before = poly.compensation_gain();
2118 assert!(
2119 (g_before - 1.0).abs() < 0.01,
2120 "one voice => unity comp gain"
2121 );
2122
2123 poly.note_on(64, 100);
2125 poly.tick();
2126 let g_step = poly.compensation_gain();
2127 assert!(
2128 (g_before - g_step).abs() < 0.05,
2129 "comp gain stepped discontinuously: {g_before} -> {g_step}"
2130 );
2131
2132 for _ in 0..4000 {
2134 poly.tick();
2135 }
2136 let g_settled = poly.compensation_gain();
2137 assert!(
2138 (g_settled - 1.0 / 2.0f64.sqrt()).abs() < 0.02,
2139 "comp gain should settle to 1/sqrt(2), got {g_settled}"
2140 );
2141 }
2142
2143 #[test]
2146 fn test_steal_resets_voice_dsp() {
2147 let sr = 48_000.0;
2148 let mut poly = build_dc_poly(1, 1.0, sr);
2150 poly.allocator_mut().set_mode(AllocationMode::OldestSteal);
2151
2152 poly.note_on(60, 100);
2153 for _ in 0..500 {
2154 poly.tick();
2155 }
2156 poly.note_on(72, 100);
2158 assert_eq!(poly.allocator().last_stolen(), Some(0));
2159 let (l, _r) = poly.tick();
2162 assert!(l.abs() > 0.0, "stolen voice should keep producing audio");
2163 }
2164
2165 #[test]
2172 fn test_nondecaying_released_voice_force_frees() {
2173 let sr = 48_000.0;
2174 let mut poly = build_dc_poly(1, 1.0, sr);
2175 poly.allocator_mut().set_mode(AllocationMode::NoSteal);
2176
2177 poly.note_on(60, 100);
2180 for _ in 0..200 {
2181 poly.tick();
2182 }
2183 poly.note_off(60);
2184
2185 for _ in 0..2000 {
2188 poly.tick();
2189 }
2190 assert_eq!(
2191 poly.allocator().voices()[0].state,
2192 VoiceState::Releasing,
2193 "DC voice does not decay, so it is still releasing"
2194 );
2195 poly.note_on(72, 100);
2196 assert_ne!(
2197 poly.allocator().voices()[0].note,
2198 Some(72),
2199 "NoSteal cannot reuse a still-Releasing voice yet"
2200 );
2201
2202 poly.set_max_release_time(0.01); for _ in 0..1000 {
2205 poly.tick();
2206 }
2207 assert_eq!(
2208 poly.allocator().voices()[0].state,
2209 VoiceState::Free,
2210 "a non-decaying released voice must force-free after the release cap"
2211 );
2212
2213 poly.note_on(72, 100);
2215 assert_eq!(poly.allocator().voices()[0].note, Some(72));
2216 assert_eq!(poly.allocator().voices()[0].state, VoiceState::Active);
2217 }
2218
2219 #[test]
2222 fn test_release_cap_configuration() {
2223 let sr = 48_000.0;
2224 let mut poly = build_dc_poly(1, 1.0, sr);
2225 assert_eq!(
2227 poly.allocator().max_release_samples(),
2228 (MAX_RELEASE_S * sr) as u64
2229 );
2230 poly.set_max_release_time(0.0);
2231 assert_eq!(poly.allocator().max_release_samples(), u64::MAX);
2232 }
2233
2234 #[test]
2237 fn test_voice_mixer() {
2238 let mixer = VoiceMixer::new(4);
2239 let spec = mixer.port_spec();
2240 assert!(!spec.inputs.is_empty());
2241 assert!(!spec.outputs.is_empty());
2242 }
2243
2244 #[test]
2245 fn test_voice_mixer_tick() {
2246 let mut mixer = VoiceMixer::new(2);
2247 let mut inputs = PortValues::new();
2248 let mut outputs = PortValues::new();
2249
2250 inputs.set(0, 1.0);
2251 inputs.set(1, 2.0);
2252 inputs.set(2, 3.0);
2253 inputs.set(3, 4.0);
2254
2255 mixer.tick(&inputs, &mut outputs);
2256
2257 assert!(outputs.get(100).is_some());
2258 assert!(outputs.get(101).is_some());
2259 }
2260
2261 #[test]
2262 fn test_voice_mixer_reset_type_id() {
2263 let mut mixer = VoiceMixer::new(2);
2264 mixer.reset();
2265 mixer.set_sample_rate(48000.0);
2266 assert_eq!(mixer.type_id(), "voice_mixer");
2267 }
2268
2269 fn poly_stress(mode: AllocationMode) {
2275 let sr = 48_000.0;
2276 let mut poly = build_synth(16, sr);
2277 poly.allocator_mut().set_mode(mode);
2278 assert_eq!(poly.num_voices(), 16);
2279
2280 let mut state: u64 = 0x1234_5678_9abc_def0;
2282 let mut next = move || {
2283 state = state
2284 .wrapping_mul(6_364_136_223_846_793_005)
2285 .wrapping_add(1_442_695_040_888_963_407);
2286 (state >> 33) as u32
2287 };
2288
2289 let mut held: Vec<u8> = Vec::new();
2290 for t in 0..12_000 {
2291 if t % 30 == 0 {
2293 let r = next() % 100;
2294 if r < 55 {
2295 let note = 36 + (next() % 48) as u8;
2297 let vel = 1 + (next() % 127) as u8;
2298 poly.note_on(note, vel);
2299 if !held.contains(¬e) {
2300 held.push(note);
2301 }
2302 } else if r < 80 && !held.is_empty() {
2303 let idx = (next() as usize) % held.len();
2304 let note = held.remove(idx);
2305 poly.note_off(note);
2306 } else if !held.is_empty() {
2307 let idx = (next() as usize) % held.len();
2309 let note = held[idx];
2310 let vel = 1 + (next() % 127) as u8;
2311 poly.note_on(note, vel);
2312 }
2313 }
2314
2315 let (l, r) = poly.tick();
2316 assert!(
2317 l.is_finite() && r.is_finite(),
2318 "non-finite output at tick {t}"
2319 );
2320 assert!(
2321 l.abs() < 16.0 && r.abs() < 16.0,
2322 "polyphonic output exploded at tick {t}: ({l}, {r})"
2323 );
2324 assert!(
2325 poly.allocator().active_count() <= poly.num_voices(),
2326 "active_count {} exceeded voice count at tick {t}",
2327 poly.allocator().active_count()
2328 );
2329 }
2330
2331 poly.all_notes_off();
2333 for _ in 0..48_000 {
2334 let (l, r) = poly.tick();
2335 assert!(l.is_finite() && r.is_finite());
2336 }
2337 assert_eq!(
2338 poly.allocator().active_count(),
2339 0,
2340 "all voices should auto-free after a long release tail"
2341 );
2342 }
2343
2344 #[test]
2345 fn test_poly_stress_16_voices_oldest_steal() {
2346 poly_stress(AllocationMode::OldestSteal);
2347 }
2348
2349 #[test]
2350 fn test_poly_stress_16_voices_no_steal() {
2351 poly_stress(AllocationMode::NoSteal);
2352 }
2353}