diff --git a/crates/engine/src/mod_matrix.rs b/crates/engine/src/mod_matrix.rs index fd09bd5..fe6faf2 100644 --- a/crates/engine/src/mod_matrix.rs +++ b/crates/engine/src/mod_matrix.rs @@ -1,6 +1,8 @@ -use params::ParamId; +use params::{ParamId, ParamStore}; -#[derive(Clone, Copy)] +use crate::voice::ModSources; + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ModSource { Env(u8), Lfo(u8), @@ -11,7 +13,30 @@ pub enum ModSource { Macro(u8), } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] +pub struct ControlSources { + pub modwheel: f32, + pub aftertouch: f32, + pub macros: [f32; 8], +} + +impl ControlSources { + #[inline] + pub fn sync_macros(&mut self, p: &ParamStore) { + self.macros = [ + p.get(ParamId::Macro1), + p.get(ParamId::Macro2), + p.get(ParamId::Macro3), + p.get(ParamId::Macro4), + p.get(ParamId::Macro5), + p.get(ParamId::Macro6), + p.get(ParamId::Macro7), + p.get(ParamId::Macro8), + ]; + } +} + +#[derive(Clone, Copy, Debug)] pub struct ModSlot { pub active: bool, pub source: ModSource, @@ -32,15 +57,112 @@ impl Default for ModSlot { } } +#[derive(Clone, Copy)] +pub struct ModValues { + data: [f32; ParamId::COUNT], +} + +impl ModValues { + #[inline] + pub fn zero() -> Self { + Self { + data: [0.0; ParamId::COUNT], + } + } + + #[inline] + pub fn get(&self, id: ParamId) -> f32 { + // SAFETY: ParamId repr(usize), COUNT is the discriminant of the sentinel + unsafe { *self.data.get_unchecked(id as usize) } + } + + #[inline] + fn add(&mut self, id: ParamId, v: f32) { + unsafe { + *self.data.get_unchecked_mut(id as usize) += v; + } + } +} + pub struct ModMatrix { pub slots: [ModSlot; 64], } +impl Default for ModMatrix { + fn default() -> Self { + Self::new() + } +} + impl ModMatrix { pub fn new() -> Self { Self { slots: [ModSlot::default(); 64], } } - pub fn apply(&self, _params: ¶ms::ParamStore) {} + + #[inline] + pub fn compute(&self, ms: &ModSources, cs: &ControlSources) -> ModValues { + let mut out = ModValues::zero(); + + for slot in &self.slots { + if !slot.active { + continue; + } + let depth = slot.depth; + if depth == 0.0 { + continue; + } + + let src: f32 = match slot.source { + ModSource::Env(i) => ms.env.get(i as usize).copied().unwrap_or(0.0), + ModSource::Lfo(i) => ms.lfo.get(i as usize).copied().unwrap_or(0.0), + ModSource::Velocity => ms.vel, + ModSource::Note => ms.note, + ModSource::Modwheel => cs.modwheel, + ModSource::Aftertouch => cs.aftertouch, + ModSource::Macro(i) => cs.macros.get(i as usize).copied().unwrap_or(0.0), + }; + + out.add(slot.dest, src * depth); + } + + out + } + + pub fn free_slot(&self) -> Option { + self.slots.iter().position(|s| !s.active) + } + + pub fn connect( + &mut self, + source: ModSource, + dest: ParamId, + depth: f32, + per_voice: bool, + ) -> Option { + let idx = self.free_slot()?; + self.slots[idx] = ModSlot { + active: true, + source, + dest, + depth, + per_voice, + }; + Some(idx) + } + + pub fn disconnect(&mut self, idx: usize) { + if idx < 64 { + self.slots[idx] = ModSlot::default(); + } + } + + pub fn has_dest(&self, dest: ParamId) -> bool { + self.slots.iter().any(|s| s.active && s.dest == dest) + } + + pub fn active_slots(&self) -> impl Iterator { + self.slots.iter().enumerate().filter(|(_, s)| s.active) + } } diff --git a/crates/engine/src/synth.rs b/crates/engine/src/synth.rs index f36b9c0..549d606 100644 --- a/crates/engine/src/synth.rs +++ b/crates/engine/src/synth.rs @@ -5,6 +5,7 @@ use params::{ParamId, ParamStore}; use crate::{ effects::{DistType, EffectsChain}, filter::FilterKind, + mod_matrix::{ControlSources, ModMatrix}, oscillator::WavetableBank, voice::{PlayMode, PortamentoMode, Voice}, }; @@ -18,6 +19,8 @@ pub struct Synth { sample_rate: f32, round_robin: usize, play_mode: PlayMode, + pub mod_matrix: ModMatrix, + control_sources: ControlSources, } impl Synth { @@ -29,6 +32,8 @@ impl Synth { sample_rate: sr, round_robin: 0, play_mode: PlayMode::Poly, + mod_matrix: ModMatrix::new(), + control_sources: ControlSources::default(), } } @@ -59,15 +64,30 @@ impl Synth { } } + pub fn set_modwheel(&mut self, v: f32) { + self.control_sources.modwheel = v.clamp(0.0, 1.0); + } + + pub fn set_aftertouch(&mut self, v: f32) { + self.control_sources.aftertouch = v.clamp(0.0, 1.0); + } + + pub fn mod_matrix_mut(&mut self) -> &mut ModMatrix { + &mut self.mod_matrix + } + #[inline] pub fn process(&mut self, host_bpm: f32) -> (f32, f32) { self.sync_params(); + self.control_sources.sync_macros(&self.params); + let mut l = 0.0f32; let mut r = 0.0f32; + for v in &mut self.voices { if v.active { - let (vl, vr) = v.process(host_bpm); + let (vl, vr) = v.process(host_bpm, &self.mod_matrix, &self.control_sources); l += vl; r += vr; } @@ -105,6 +125,8 @@ impl Synth { PortamentoMode::Exponential }; + self.play_mode = play_mode_from(p.get(ParamId::Polyphony)); + for v in &mut self.voices { v.filter1.cutoff = f1_cutoff; v.filter1.resonance = f1_res; @@ -120,6 +142,21 @@ impl Synth { v.portamento_time = port_time; v.portamento_mode = port_mode; + + sync_env(&mut v.envs[0], p, 0); + sync_env(&mut v.envs[1], p, 1); + sync_env(&mut v.envs[2], p, 2); + + sync_lfo(&mut v.lfos[0], p, 0); + sync_lfo(&mut v.lfos[1], p, 1); + sync_lfo(&mut v.lfos[2], p, 2); + sync_lfo(&mut v.lfos[3], p, 3); + + sync_osc_base(&mut v.oscs[0], p, 0); + sync_osc_base(&mut v.oscs[1], p, 1); + sync_osc_base(&mut v.oscs[2], p, 2); + + v.filter_serial = (p.get(ParamId::FilterRouting) as u32) == 0; } let fx = &mut self.fx; @@ -144,7 +181,7 @@ impl Synth { fx.delay.mix = p.get(ParamId::DelayMix); let eq = &mut fx.eq; - let new_low = p.get(ParamId::EqLowGain) * 24.0 - 12.0; // ±12 dB + let new_low = p.get(ParamId::EqLowGain) * 24.0 - 12.0; let new_mid_f = p.get(ParamId::EqMidFreq) * 7900.0 + 100.0; let new_mid_g = p.get(ParamId::EqMidGain) * 24.0 - 12.0; let new_high = p.get(ParamId::EqHighGain) * 24.0 - 12.0; @@ -173,6 +210,69 @@ impl Synth { } } +const ENV_BASE: [ParamId; 3] = [ParamId::Env1Delay, ParamId::Env2Delay, ParamId::Env3Delay]; + +fn sync_env(env: &mut crate::envelope::Dahdsr, p: &ParamStore, idx: usize) { + let base = ENV_BASE[idx] as usize; + fn load(p: &ParamStore, base: usize, off: usize) -> f32 { + let id: ParamId = unsafe { std::mem::transmute(base + off) }; + p.get(id) + } + env.delay = load(p, base, 0) * 2.0; + env.attack = load(p, base, 1) * 10.0 + 0.001; + env.hold = load(p, base, 2) * 2.0; + env.decay = load(p, base, 3) * 10.0 + 0.001; + env.sustain = load(p, base, 4); + env.release = load(p, base, 5) * 10.0 + 0.001; + env.attack_curve = load(p, base, 6) * 2.0 - 1.0; + env.decay_curve = load(p, base, 7) * 2.0 - 1.0; + env.release_curve = load(p, base, 8) * 2.0 - 1.0; +} + +const LFO_BASE: [ParamId; 4] = [ + ParamId::Lfo1Rate, + ParamId::Lfo2Rate, + ParamId::Lfo3Rate, + ParamId::Lfo4Rate, +]; + +fn sync_lfo(lfo: &mut crate::lfo::Lfo, p: &ParamStore, idx: usize) { + let base = LFO_BASE[idx] as usize; + fn load(p: &ParamStore, base: usize, off: usize) -> f32 { + let id: ParamId = unsafe { std::mem::transmute(base + off) }; + p.get(id) + } + lfo.rate = load(p, base, 0) * 20.0 + 0.01; + lfo.initial_phase = load(p, base, 1); + lfo.depth = load(p, base, 2); + lfo.wave_pos = load(p, base, 3); + lfo.mode = if load(p, base, 4) > 0.5 { + crate::lfo::LfoMode::BpmSync + } else { + crate::lfo::LfoMode::FreeRun + }; +} + +const OSC_BASE: [ParamId; 3] = [ParamId::Osc1Gain, ParamId::Osc2Gain, ParamId::Osc3Gain]; + +fn sync_osc_base(osc: &mut crate::oscillator::WavetableOsc, p: &ParamStore, idx: usize) { + let base = OSC_BASE[idx] as usize; + fn load(p: &ParamStore, base: usize, off: usize) -> f32 { + let id: ParamId = unsafe { std::mem::transmute(base + off) }; + p.get(id) + } + osc.gain = load(p, base, 0); + osc.pan = load(p, base, 1) * 2.0 - 1.0; + osc.wave_pos = load(p, base, 7); + + osc.semitone = load(p, base, 2) * 96.0 - 48.0; + osc.fine = (load(p, base, 3) * 2.0 - 1.0) * 100.0; + + osc.unison_count = (load(p, base, 4) * 15.0 + 1.0) as u8; + osc.unison_detune = load(p, base, 5); + osc.unison_spread = load(p, base, 6); +} + fn filter_kind_from(v: u32) -> FilterKind { match v { 0 => FilterKind::Ladder, @@ -191,3 +291,13 @@ fn dist_type_from(v: u32) -> DistType { _ => DistType::SoftClip, } } + +fn play_mode_from(v: f32) -> PlayMode { + if v < 0.33 { + PlayMode::Poly + } else if v < 0.66 { + PlayMode::Mono + } else { + PlayMode::Legato + } +} diff --git a/crates/engine/src/voice.rs b/crates/engine/src/voice.rs index c11f884..e5fd409 100644 --- a/crates/engine/src/voice.rs +++ b/crates/engine/src/voice.rs @@ -1,7 +1,10 @@ +use params::ParamId; + use crate::{ envelope::{Dahdsr, Stage}, filter::Filter, lfo::Lfo, + mod_matrix::{ControlSources, ModMatrix, ModValues}, oscillator::{WavetableBank, WavetableOsc, midi_to_freq}, }; use std::sync::Arc; @@ -144,7 +147,17 @@ impl Voice { } #[inline] - pub fn process(&mut self, host_bpm: f32) -> (f32, f32) { + fn semitones_to_ratio(semis: f32) -> f32 { + (semis * (2.0_f32.ln() / 12.0)).exp() + } + + #[inline] + pub fn process( + &mut self, + host_bpm: f32, + matrix: &ModMatrix, + cs: &ControlSources, + ) -> (f32, f32) { let amp_env = self.tick_mod_sources(host_bpm); if self.envs[0].stage == Stage::Idle { @@ -152,16 +165,64 @@ impl Voice { return (0.0, 0.0); } - let freq = self.advance_portamento(); + let mv: ModValues = matrix.compute(&self.mod_sources, cs); + + let pitch_mod_semis = mv.get(ParamId::Osc1Semitone) + + mv.get(ParamId::Osc2Semitone) + + mv.get(ParamId::Osc3Semitone); + let pitch_ratio = Self::semitones_to_ratio(pitch_mod_semis * 24.0); + + let base_freq = self.advance_portamento(); + let freq = base_freq * pitch_ratio; let (mut l, mut r) = (0.0f32, 0.0f32); - for osc in &mut self.oscs { + + let wave_mods = [ + mv.get(ParamId::Osc1WavePos), + mv.get(ParamId::Osc2WavePos), + mv.get(ParamId::Osc3WavePos), + ]; + let gain_mods = [ + mv.get(ParamId::Osc1Gain), + mv.get(ParamId::Osc2Gain), + mv.get(ParamId::Osc3Gain), + ]; + let pan_mods = [ + mv.get(ParamId::Osc1Pan), + mv.get(ParamId::Osc2Pan), + mv.get(ParamId::Osc3Pan), + ]; + + for (i, osc) in self.oscs.iter_mut().enumerate() { + let eff_wave = (osc.wave_pos + wave_mods[i]).clamp(0.0, 1.0); + let eff_gain = (osc.gain + gain_mods[i]).max(0.0); + let eff_pan = (osc.pan + pan_mods[i]).clamp(-1.0, 1.0); + + let saved_wave = osc.wave_pos; + let saved_gain = osc.gain; + let saved_pan = osc.pan; + osc.wave_pos = eff_wave; + osc.gain = eff_gain; + osc.pan = eff_pan; + let (ol, or_) = osc.tick(freq, self.sample_rate, 0.0); l += ol; r += or_; - } + osc.wave_pos = saved_wave; + osc.gain = saved_gain; + osc.pan = saved_pan; + } let ms = self.mod_sources; + + let eff_c1 = (self.filter1.cutoff + mv.get(ParamId::Filter1Cutoff)).clamp(0.0, 1.0); + let eff_c2 = (self.filter2.cutoff + mv.get(ParamId::Filter2Cutoff)).clamp(0.0, 1.0); + + let saved_c1 = self.filter1.cutoff; + let saved_c2 = self.filter2.cutoff; + self.filter1.cutoff = eff_c1; + self.filter2.cutoff = eff_c2; + let fm1 = Self::filter_fm(&ms, self.filter1.fm_amount); let fm2 = Self::filter_fm(&ms, self.filter2.fm_amount); @@ -179,7 +240,12 @@ impl Voice { ((l1 + l2) * 0.5, (r1 + r2) * 0.5) }; - let amp = amp_env * self.velocity; + self.filter1.cutoff = saved_c1; + self.filter2.cutoff = saved_c2; + + let amp_mod = mv.get(ParamId::MasterVolume); + let amp = amp_env * self.velocity * (1.0 + amp_mod).max(0.0); + (l * amp, r * amp) } }