use crate::audio::chorus::Chorus;
use crate::audio::delay::Delay;
use crate::audio::drums::DrumMachine;
use crate::audio::fx::Reverb;
use crate::audio::voice::Voice;
use crate::params::{AudioEvent, ChannelNo, MidiNote, SynthParams};
const POLYPHONY: usize = 4;
const POLYPHONY_F32: f32 = 4.0;
pub const NUM_CHANNELS: usize = 4;
const MASTER_GAIN: f32 = 0.5;
#[derive(Clone, Copy, Default)]
struct VoiceSlot {
note: Option<MidiNote>,
age: u64,
}
struct AudioChannel {
params: SynthParams,
voices: [Voice; POLYPHONY],
slots: [VoiceSlot; POLYPHONY],
age_counter: u64,
#[cfg(feature = "arp")]
arp: crate::audio::arp::Arpeggiator,
}
impl AudioChannel {
fn new() -> Self {
Self {
params: SynthParams::default(),
voices: std::array::from_fn(|_| Voice::new()),
slots: std::array::from_fn(|_| VoiceSlot::default()),
age_counter: 0,
#[cfg(feature = "arp")]
arp: crate::audio::arp::Arpeggiator::new(),
}
}
fn is_voice_idle(&self, idx: usize) -> bool {
let voice = &self.voices[idx];
!voice.active && !voice.env.is_active() && self.slots[idx].note.is_none()
}
fn allocate_voice_index(&self, midi: MidiNote) -> usize {
if let Some(idx) = self.slots.iter().position(|s| s.note == Some(midi)) {
return idx;
}
if let Some(idx) = (0..POLYPHONY).find(|&idx| self.is_voice_idle(idx)) {
return idx;
}
self.slots
.iter()
.enumerate()
.min_by_key(|(_, s)| s.age)
.map_or(0, |(idx, _)| idx)
}
fn note_on(&mut self, midi: MidiNote, sample_rate: f32) {
#[cfg(feature = "arp")]
if self.params.arp.enabled {
self.arp.add_note(&mut self.params.arp, midi);
return;
}
self.voice_note_on(midi, sample_rate);
}
fn note_off(&mut self, midi: MidiNote) {
#[cfg(feature = "arp")]
if self.params.arp.enabled {
self.arp.remove_note(&mut self.params.arp, midi);
if self.params.arp.count == 0
&& let Some(stuck) = self.arp.sounding.take()
{
self.voice_note_off(stuck);
}
return;
}
self.voice_note_off(midi);
}
fn voice_note_on(&mut self, midi: MidiNote, sample_rate: f32) {
let idx = self.allocate_voice_index(midi);
self.age_counter = self.age_counter.saturating_add(1);
self.slots[idx].note = Some(midi);
self.slots[idx].age = self.age_counter;
self.voices[idx].note_on(midi, &self.params, sample_rate);
}
fn voice_note_off(&mut self, midi: MidiNote) {
if let Some(idx) = self.slots.iter().position(|s| s.note == Some(midi)) {
self.voices[idx].note_off();
self.slots[idx].note = None;
}
}
fn panic(&mut self) {
#[cfg(feature = "arp")]
self.arp.panic(&mut self.params.arp);
for voice in &mut self.voices {
voice.panic();
}
for slot in &mut self.slots {
*slot = VoiceSlot::default();
}
self.age_counter = 0;
}
fn process(&mut self, sample_rate: f32) -> f32 {
#[cfg(feature = "arp")]
if self.params.arp.enabled && self.params.arp.count > 0 {
let events = self.arp.tick(sample_rate, &self.params.arp);
if let Some(note) = events.off {
self.voice_note_off(note);
}
if let Some(note) = events.on {
self.voice_note_on(note, sample_rate);
}
}
self.voices
.iter_mut()
.map(|v| v.process(&self.params, sample_rate))
.sum::<f32>()
/ POLYPHONY_F32
}
}
pub struct SynthProcessor<const N: usize> {
channels: [AudioChannel; N],
reverb: Reverb,
chorus: Chorus,
delay: Delay,
drums: DrumMachine,
sample_rate: f32,
}
impl<const N: usize> SynthProcessor<N> {
#[must_use]
pub fn new(sample_rate: f32) -> Self {
const {
assert!(
N >= 1,
"SynthProcessor requires at least 1 synthesis channel (N >= 1)"
);
};
let channels: [AudioChannel; N] = std::array::from_fn(|_| AudioChannel::new());
let mut reverb = Reverb::new();
reverb.set_params(
channels[0].params.fx.reverb_size,
channels[0].params.fx.reverb_damping,
);
Self {
channels,
reverb,
chorus: Chorus::new(sample_rate),
delay: Delay::new(sample_rate),
drums: DrumMachine::new(sample_rate),
sample_rate,
}
}
fn apply_reverb_params(&mut self) {
let fx = &self.channels[0].params.fx;
self.reverb.set_params(fx.reverb_size, fx.reverb_damping);
}
fn note_on(&mut self, ch: ChannelNo, midi: MidiNote) {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel.note_on(midi, self.sample_rate);
}
}
fn note_off(&mut self, ch: ChannelNo, midi: MidiNote) {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel.note_off(midi);
}
}
fn panic(&mut self) {
for channel in &mut self.channels {
channel.panic();
}
self.drums.panic();
}
fn apply_events(&mut self, events: &[AudioEvent]) {
for event in events {
match event {
AudioEvent::NoteOn(midi) => self.note_on(ChannelNo::DEFAULT, *midi),
AudioEvent::NoteOff(midi) => self.note_off(ChannelNo::DEFAULT, *midi),
AudioEvent::Panic => self.panic(),
AudioEvent::LoadPatch(p) => {
self.channels[0].params = (**p).clone();
self.apply_reverb_params();
}
AudioEvent::Drum(hit) => self.drums.trigger(*hit),
AudioEvent::NoteOnChannel(ch, midi) => self.note_on(*ch, *midi),
AudioEvent::NoteOffChannel(ch, midi) => self.note_off(*ch, *midi),
AudioEvent::LoadPatchChannel(ch, p) => {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel.params = (**p).clone();
}
if *ch == ChannelNo::DEFAULT {
self.apply_reverb_params();
}
}
#[cfg(feature = "arp")]
AudioEvent::ArpSetNotes(ch, notes, count) => {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel
.arp
.set_notes(&mut channel.params.arp, ¬es[..(*count as usize).min(4)]);
}
}
#[cfg(feature = "arp")]
AudioEvent::ArpEnabled(ch, enabled) => {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel.params.arp.enabled = *enabled;
}
}
}
}
}
pub fn process_block(&mut self, events: &[AudioEvent], buf: &mut [f32], hw_channels: usize) {
assert!(hw_channels > 0, "hw_channels must be > 0");
assert_eq!(
buf.len() % hw_channels,
0,
"buf.len() must be a multiple of hw_channels"
);
self.apply_events(events);
let sample_rate = self.sample_rate;
let reverb_mix = self.channels[0].params.fx.reverb_mix;
let chorus_params = self.channels[0].params.chorus.clone();
let delay_params = self.channels[0].params.delay.clone();
for frame in buf.chunks_mut(hw_channels) {
let mix: f32 = self
.channels
.iter_mut()
.map(|ch| ch.process(sample_rate))
.sum::<f32>()
+ self.drums.process(sample_rate);
let mix = self.chorus.process(mix, &chorus_params);
let mix = self.delay.process(mix, &delay_params);
let sample = self.reverb.process(mix, reverb_mix) * MASTER_GAIN;
let sample = if sample.is_finite() {
sample.clamp(-1.0, 1.0)
} else {
core::hint::cold_path();
0.0
};
for ch in frame.iter_mut() {
*ch = sample;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::params::DrumHit;
const SR: f32 = 44100.0;
const NOTE: MidiNote = MidiNote::MIDDLE_C;
fn make() -> SynthProcessor<NUM_CHANNELS> {
SynthProcessor::new(SR)
}
#[test]
fn renders_silence_with_no_events() {
let mut proc = make();
let mut buf = vec![1.0_f32; 256];
proc.process_block(&[], &mut buf, 2);
assert!(buf.iter().all(|s| s.is_finite()), "non-finite samples");
assert!(
buf.iter().all(|s| *s == 0.0),
"expected silence with no notes active"
);
}
#[test]
fn note_on_produces_non_zero_output() {
let mut proc = make();
let mut buf = vec![0.0_f32; 4096];
proc.process_block(&[AudioEvent::NoteOn(NOTE)], &mut buf, 1);
assert!(
buf.iter().any(|s| s.abs() > 1e-6),
"note_on produced no audible signal"
);
}
#[test]
fn note_off_after_note_on_does_not_panic() {
let mut proc = make();
let mut buf = vec![0.0_f32; 256];
proc.process_block(&[AudioEvent::NoteOn(NOTE)], &mut buf, 1);
proc.process_block(&[AudioEvent::NoteOff(NOTE)], &mut buf, 1);
}
#[test]
fn panic_event_clears_voice_state() {
let mut proc = make();
let mut buf = vec![0.0_f32; 8192];
let n2 = MidiNote::A4;
proc.process_block(
&[AudioEvent::NoteOn(NOTE), AudioEvent::NoteOn(n2)],
&mut buf,
1,
);
proc.process_block(&[AudioEvent::Panic], &mut buf, 1);
let mut after = vec![0.0_f32; 8192];
proc.process_block(&[], &mut after, 1);
assert!(
after.iter().all(|s| s.is_finite()),
"non-finite after panic"
);
}
#[test]
fn channel_specific_note_on() {
let mut proc = make();
let mut buf = vec![0.0_f32; 4096];
let ch = ChannelNo::from(1u8);
proc.process_block(&[AudioEvent::NoteOnChannel(ch, NOTE)], &mut buf, 1);
assert!(
buf.iter().any(|s| s.abs() > 1e-6),
"channel-specific note_on produced no signal"
);
}
#[test]
fn channel_specific_note_off_does_not_affect_other_channel() {
let mut proc = make();
let mut buf = vec![0.0_f32; 4096];
proc.process_block(&[AudioEvent::NoteOn(NOTE)], &mut buf, 1);
let ch1 = ChannelNo::from(1u8);
proc.process_block(&[AudioEvent::NoteOffChannel(ch1, NOTE)], &mut buf, 1);
assert!(
buf.iter().any(|s| s.abs() > 1e-6),
"wrong channel note_off silenced the wrong channel"
);
}
#[test]
fn load_patch_channel_0_does_not_panic() {
let mut proc = make();
let mut buf = vec![0.0_f32; 256];
let patch = Box::new(SynthParams::default());
proc.process_block(&[AudioEvent::LoadPatch(patch)], &mut buf, 1);
}
#[test]
fn load_patch_channel_n_does_not_panic() {
let mut proc = make();
let mut buf = vec![0.0_f32; 256];
let ch = ChannelNo::from(2u8);
let patch = Box::new(SynthParams::default());
proc.process_block(&[AudioEvent::LoadPatchChannel(ch, patch)], &mut buf, 1);
}
#[test]
fn drum_event_produces_signal() {
let mut proc = make();
let mut buf = vec![0.0_f32; 4096];
proc.process_block(&[AudioEvent::Drum(DrumHit::Kick)], &mut buf, 1);
assert!(
buf.iter().any(|s| s.abs() > 1e-6),
"drum kick produced no signal"
);
}
#[test]
fn stereo_frames_have_equal_left_right() {
let mut proc = make();
let mut buf = vec![0.0_f32; 512];
proc.process_block(&[AudioEvent::NoteOn(NOTE)], &mut buf, 2);
#[allow(clippy::float_cmp)]
for frame in buf.chunks(2) {
assert_eq!(frame[0], frame[1], "stereo channels differ within a frame");
}
}
#[test]
#[should_panic(expected = "hw_channels must be > 0")]
fn zero_hw_channels_panics() {
let mut proc = make();
let mut buf = vec![0.0_f32; 4];
proc.process_block(&[], &mut buf, 0);
}
#[test]
#[should_panic(expected = "buf.len() must be a multiple of hw_channels")]
fn misaligned_buf_panics() {
let mut proc = make();
let mut buf = vec![0.0_f32; 3];
proc.process_block(&[], &mut buf, 2);
}
#[cfg(feature = "arp")]
#[test]
fn arp_set_notes_and_enable_produces_signal() {
let mut proc = make();
let mut buf = vec![0.0_f32; 4096];
proc.process_block(
&[AudioEvent::ArpEnabled(ChannelNo::DEFAULT, true)],
&mut buf,
1,
);
assert!(
buf.iter().all(|s| s.abs() < 1e-6),
"arp with count=0 should be silent"
);
let notes = [MidiNote::MIDDLE_C; 4];
let mut buf2 = vec![0.0_f32; 8192];
proc.process_block(
&[AudioEvent::ArpSetNotes(ChannelNo::DEFAULT, notes, 1)],
&mut buf2,
1,
);
assert!(
buf2.iter().any(|s| s.abs() > 1e-6),
"arp with 1 note loaded should produce signal"
);
}
#[cfg(feature = "arp")]
#[test]
fn arp_disabled_after_enable_allows_direct_notes() {
let mut proc = make();
let notes = [MidiNote::MIDDLE_C; 4];
let setup = vec![
AudioEvent::ArpEnabled(ChannelNo::DEFAULT, true),
AudioEvent::ArpSetNotes(ChannelNo::DEFAULT, notes, 1),
];
let mut buf = vec![0.0_f32; 8192];
proc.process_block(&setup, &mut buf, 1);
assert!(
buf.iter().any(|s| s.abs() > 1e-6),
"arp-driven audio should appear without a direct NoteOn"
);
let mut silence_buf = vec![0.0_f32; 256];
proc.process_block(
&[
AudioEvent::ArpEnabled(ChannelNo::DEFAULT, false),
AudioEvent::Panic,
],
&mut silence_buf,
1,
);
let mut buf2 = vec![0.0_f32; 4096];
proc.process_block(&[AudioEvent::NoteOn(MidiNote::MIDDLE_C)], &mut buf2, 1);
assert!(
buf2.iter().any(|s| s.abs() > 1e-6),
"direct NoteOn after arp disable should produce signal"
);
}
#[cfg(feature = "arp")]
#[test]
fn arp_panic_leaves_processor_in_finite_state() {
let mut proc = make();
let notes = [MidiNote::MIDDLE_C; 4];
let setup = vec![
AudioEvent::ArpSetNotes(ChannelNo::DEFAULT, notes, 1),
AudioEvent::ArpEnabled(ChannelNo::DEFAULT, true),
];
let mut buf = vec![0.0_f32; 4096];
proc.process_block(&setup, &mut buf, 1);
proc.process_block(&[AudioEvent::Panic], &mut buf, 1);
let mut after = vec![0.0_f32; 4096];
proc.process_block(&[], &mut after, 1);
assert!(
after.iter().all(|s| s.is_finite()),
"non-finite output after arp panic"
);
}
}