diff --git a/crates/engine/src/envelope.rs b/crates/engine/src/envelope.rs index fe265bf..9e55c62 100644 --- a/crates/engine/src/envelope.rs +++ b/crates/engine/src/envelope.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Debug)] pub enum Stage { Idle, Delay, @@ -12,17 +12,21 @@ pub enum Stage { pub struct Dahdsr { pub stage: Stage, pub level: f32, + pub delay: f32, pub attack: f32, pub hold: f32, pub decay: f32, pub sustain: f32, pub release: f32, + pub attack_curve: f32, pub decay_curve: f32, pub release_curve: f32, - pub sample_rate: f32, + + sample_rate: f32, stage_samples: u32, + release_level: f32, } impl Dahdsr { @@ -41,21 +45,113 @@ impl Dahdsr { release_curve: 0.0, sample_rate: sr, stage_samples: 0, + release_level: 0.0, } } + pub fn note_on(&mut self) { self.stage = Stage::Delay; self.stage_samples = 0; } + pub fn note_off(&mut self) { - self.stage = Stage::Release; - self.stage_samples = 0; + if self.stage != Stage::Idle { + self.release_level = self.level; + self.stage = Stage::Release; + self.stage_samples = 0; + } } + pub fn is_active(&self) -> bool { self.stage != Stage::Idle } + + #[inline] + fn shape(t: f32, curve: f32) -> f32 { + if curve.abs() < 1e-4 { + return t; + } + let exp = 1.0 + + if curve > 0.0 { + curve * 9.0 + } else { + curve * 0.9 + }; + t.powf(exp) + } + #[inline] pub fn tick(&mut self) -> f32 { - 0.0 + let sr = self.sample_rate; + + match self.stage { + Stage::Idle => { + self.level = 0.0; + } + + Stage::Delay => { + self.level = 0.0; + let len = (self.delay * sr) as u32; + self.stage_samples += 1; + if len == 0 || self.stage_samples >= len { + self.stage = Stage::Attack; + self.stage_samples = 0; + } + } + + Stage::Attack => { + let len = (self.attack * sr).max(1.0) as u32; + self.stage_samples += 1; + let t = (self.stage_samples as f32 / len as f32).min(1.0); + self.level = Self::shape(t, -self.attack_curve); + if self.stage_samples >= len { + self.level = 1.0; + self.stage = Stage::Hold; + self.stage_samples = 0; + } + } + + Stage::Hold => { + self.level = 1.0; + let len = (self.hold * sr) as u32; + self.stage_samples += 1; + if len == 0 || self.stage_samples >= len { + self.stage = Stage::Decay; + self.stage_samples = 0; + } + } + + Stage::Decay => { + let len = (self.decay * sr).max(1.0) as u32; + self.stage_samples += 1; + let t = (self.stage_samples as f32 / len as f32).min(1.0); + let shaped = Self::shape(1.0 - t, self.decay_curve); + self.level = self.sustain + (1.0 - self.sustain) * shaped; + if self.stage_samples >= len { + self.level = self.sustain; + self.stage = Stage::Sustain; + self.stage_samples = 0; + } + } + + Stage::Sustain => { + self.level = self.sustain; + } + + Stage::Release => { + let len = (self.release * sr).max(1.0) as u32; + self.stage_samples += 1; + let t = (self.stage_samples as f32 / len as f32).min(1.0); + let shaped = Self::shape(1.0 - t, self.release_curve); + self.level = self.release_level * shaped; + if self.stage_samples >= len { + self.level = 0.0; + self.stage = Stage::Idle; + self.stage_samples = 0; + } + } + } + + self.level } } diff --git a/crates/engine/src/lfo.rs b/crates/engine/src/lfo.rs index 95d1c44..cc17b76 100644 --- a/crates/engine/src/lfo.rs +++ b/crates/engine/src/lfo.rs @@ -1,4 +1,7 @@ -#[derive(Clone, Copy)] +use crate::oscillator::WavetableBank; +use std::sync::Arc; + +#[derive(Clone, Copy, PartialEq, Debug)] pub enum LfoMode { FreeRun, BpmSync, @@ -6,33 +9,82 @@ pub enum LfoMode { Envelope, } +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum LfoPhaseBehavior { + Global, + PerVoice, +} + pub struct Lfo { pub phase: f32, + pub initial_phase: f32, pub rate: f32, pub depth: f32, pub mode: LfoMode, + pub phase_behavior: LfoPhaseBehavior, pub wave_pos: f32, - pub sync: bool, + finished: bool, sample_rate: f32, + bank: Arc, } impl Lfo { - pub fn new(sr: f32) -> Self { + pub fn new(sr: f32, bank: Arc) -> Self { Self { phase: 0.0, + initial_phase: 0.0, rate: 1.0, depth: 1.0, mode: LfoMode::FreeRun, + phase_behavior: LfoPhaseBehavior::PerVoice, wave_pos: 0.0, - sync: false, + finished: false, sample_rate: sr, + bank, } } + pub fn retrigger(&mut self) { - self.phase = 0.0; + if self.phase_behavior == LfoPhaseBehavior::PerVoice { + self.phase = self.initial_phase; + self.finished = false; + } } + #[inline] - pub fn tick(&mut self, _host_bpm: f32) -> f32 { - 0.0 + pub fn tick(&mut self, host_bpm: f32) -> f32 { + if self.finished { + return self.bank.lookup(1.0, self.wave_pos) * self.depth; + } + + let dt = self.phase_increment(host_bpm); + let sample = self.bank.lookup(self.phase, self.wave_pos) * self.depth; + + self.phase += dt; + if self.phase >= 1.0 { + match self.mode { + LfoMode::OneShot => { + self.phase = 1.0; + self.finished = true; + } + _ => { + self.phase -= 1.0; + } + } + } + + sample + } + + #[inline] + fn phase_increment(&self, host_bpm: f32) -> f32 { + match self.mode { + LfoMode::FreeRun | LfoMode::OneShot => self.rate / self.sample_rate, + LfoMode::BpmSync => { + let beats_per_sec = host_bpm / 60.0; + beats_per_sec * self.rate / self.sample_rate + } + LfoMode::Envelope => 1.0 / (self.rate.max(1e-4) * self.sample_rate), + } } } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 2b64e5d..6ef11a5 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -5,10 +5,16 @@ pub mod mod_matrix; pub mod oscillator; pub mod voice; +use std::sync::Arc; + use crossbeam_channel::{Receiver, Sender, bounded}; +use envelope::Stage; use mod_matrix::ModMatrix; +use oscillator::WavetableBank; use params::{ParamId, ParamStore}; -use voice::Voice; +use voice::{PlayMode, PortamentoMode, Voice}; + +pub use voice::{PlayMode as EnginePlayMode, PortamentoMode as EnginePortamentoMode}; pub const MAX_VOICES: usize = 16; @@ -25,12 +31,19 @@ pub struct Engine { round_robin: usize, mod_matrix: ModMatrix, pub metrics_tx: Sender, + + play_mode: PlayMode, + host_bpm: f32, + _bank: Arc, } impl Engine { pub fn new(params: ParamStore, sample_rate: f32) -> (Self, Receiver) { let (tx, rx) = bounded(4); - let voices = Box::new(std::array::from_fn(|_| Voice::new(sample_rate))); + let bank = Arc::new(WavetableBank::new()); + let voices = Box::new(std::array::from_fn(|_| { + Voice::new(sample_rate, Arc::clone(&bank)) + })); ( Self { params, @@ -39,30 +52,77 @@ impl Engine { round_robin: 0, mod_matrix: ModMatrix::new(), metrics_tx: tx, + play_mode: PlayMode::Poly, + host_bpm: 120.0, + _bank: bank, }, rx, ) } pub fn new_simple(params: ParamStore, sample_rate: f32) -> Self { - let (engine, _rx) = Self::new(params, sample_rate); - engine + Self::new(params, sample_rate).0 } pub fn set_sample_rate(&mut self, rate: f32) { self.sample_rate = rate; } - pub fn note_on(&mut self, note: u8, vel: u8) { - let idx = self.round_robin % MAX_VOICES; - let v = &mut self.voices[idx]; - v.active = true; - v.note = note; - v.velocity = vel as f32 / 127.0; - for e in &mut v.envs { - e.note_on(); + pub fn set_play_mode(&mut self, mode: PlayMode) { + self.play_mode = mode; + } + + pub fn set_bpm(&mut self, bpm: f32) { + self.host_bpm = bpm.max(1.0); + } + + fn allocate_voice(&mut self, note: u8) -> (usize, bool) { + match self.play_mode { + PlayMode::Mono => (0, false), + + PlayMode::Legato => { + let legato = self.voices[0].active; + (0, legato) + } + + PlayMode::Poly => { + if let Some(i) = self.voices.iter().position(|v| v.active && v.note == note) { + return (i, false); + } + if let Some(i) = self.voices.iter().position(|v| !v.active) { + self.round_robin = i + 1; + return (i, false); + } + if let Some(i) = self + .voices + .iter() + .position(|v| v.active && v.envs[0].stage == Stage::Release) + { + return (i, false); + } + let idx = self.round_robin % MAX_VOICES; + self.round_robin += 1; + (idx, false) + } } - self.round_robin += 1; + } + + pub fn note_on(&mut self, note: u8, vel: u8) { + let port_time = { + let t = self.params.get(ParamId::PortamentoTime); + t * t * 5.0 + }; + let port_mode = if self.params.get(ParamId::PortamentoMode) > 0.5 { + PortamentoMode::Exponential + } else { + PortamentoMode::Linear + }; + + let (idx, legato) = self.allocate_voice(note); + let v = &mut self.voices[idx]; + v.portamento_time = port_time; + v.portamento_mode = port_mode; + v.trigger(note, vel as f32 / 127.0, legato); } pub fn note_off(&mut self, note: u8) { @@ -71,27 +131,7 @@ impl Engine { .iter_mut() .filter(|v| v.active && v.note == note) { - for e in &mut v.envs { - e.note_off(); - } - } - } - - /// No alloc, no lock. Must stay that way. - #[inline] - pub fn process(&mut self, out_l: &mut [f32], out_r: &mut [f32]) { - debug_assert_eq!(out_l.len(), out_r.len()); - out_l.fill(0.0); - out_r.fill(0.0); - let vol = self.params.get(ParamId::MasterVolume); - for v in self.voices.iter_mut().filter(|v| v.active) { - let (vl, vr) = v.process(); - for (o, s) in out_l.iter_mut().zip(std::iter::repeat(vl)) { - *o += s * vol; - } - for (o, s) in out_r.iter_mut().zip(std::iter::repeat(vr)) { - *o += s * vol; - } + v.release(); } } @@ -102,4 +142,275 @@ impl Engine { _ => {} } } + + #[inline] + fn sync_voice_params(&mut self) { + macro_rules! osc_params { + ($g:expr, $p:expr, $st:expr, $fi:expr, $uc:expr, $ud:expr, $us:expr, $wp:expr) => {{ + let gain = self.params.get($g); + let pan = self.params.get($p) * 2.0 - 1.0; + let semi = self.params.get($st) * 96.0 - 48.0; + let fine = self.params.get($fi) * 200.0 - 100.0; + let ucnt = ((self.params.get($uc) * 15.0) as u8).saturating_add(1); + let udet = self.params.get($ud) * 2.0; + let uspr = self.params.get($us); + let wp = self.params.get($wp); + (gain, pan, semi, fine, ucnt, udet, uspr, wp) + }}; + } + + let osc1 = osc_params!( + ParamId::Osc1Gain, + ParamId::Osc1Pan, + ParamId::Osc1Semitone, + ParamId::Osc1Fine, + ParamId::Osc1UnisonCount, + ParamId::Osc1UnisonDetune, + ParamId::Osc1UnisonSpread, + ParamId::Osc1WavePos + ); + let osc2 = osc_params!( + ParamId::Osc2Gain, + ParamId::Osc2Pan, + ParamId::Osc2Semitone, + ParamId::Osc2Fine, + ParamId::Osc2UnisonCount, + ParamId::Osc2UnisonDetune, + ParamId::Osc2UnisonSpread, + ParamId::Osc2WavePos + ); + let osc3 = osc_params!( + ParamId::Osc3Gain, + ParamId::Osc3Pan, + ParamId::Osc3Semitone, + ParamId::Osc3Fine, + ParamId::Osc3UnisonCount, + ParamId::Osc3UnisonDetune, + ParamId::Osc3UnisonSpread, + ParamId::Osc3WavePos + ); + + macro_rules! env_times { + ($d:expr,$a:expr,$h:expr,$dc:expr,$s:expr,$r:expr, + $ac:expr,$dcc:expr,$rc:expr) => {{ + let sq = |p: ParamId| { + let v = self.params.get(p); + v * v * 10.0 + }; + let cv = |p: ParamId| self.params.get(p) * 2.0 - 1.0; + ( + sq($d), + sq($a), + sq($h), + sq($dc), + self.params.get($s), + sq($r), + cv($ac), + cv($dcc), + cv($rc), + ) + }}; + } + + let e1 = env_times!( + ParamId::Env1Delay, + ParamId::Env1Attack, + ParamId::Env1Hold, + ParamId::Env1Decay, + ParamId::Env1Sustain, + ParamId::Env1Release, + ParamId::Env1AttackCurve, + ParamId::Env1DecayCurve, + ParamId::Env1ReleaseCurve + ); + let e2 = env_times!( + ParamId::Env2Delay, + ParamId::Env2Attack, + ParamId::Env2Hold, + ParamId::Env2Decay, + ParamId::Env2Sustain, + ParamId::Env2Release, + ParamId::Env2AttackCurve, + ParamId::Env2DecayCurve, + ParamId::Env2ReleaseCurve + ); + let e3 = env_times!( + ParamId::Env3Delay, + ParamId::Env3Attack, + ParamId::Env3Hold, + ParamId::Env3Decay, + ParamId::Env3Sustain, + ParamId::Env3Release, + ParamId::Env3AttackCurve, + ParamId::Env3DecayCurve, + ParamId::Env3ReleaseCurve + ); + + macro_rules! lfo_params { + ($rt:expr, $ph:expr, $dp:expr, $wp:expr, $sy:expr) => {{ + let rate = { + let v = self.params.get($rt); + 0.01 * 10.0_f32.powf(v * 3.0) + }; + let phase = self.params.get($ph); + let depth = self.params.get($dp); + let wp = self.params.get($wp); + let sync = self.params.get($sy) > 0.5; + (rate, phase, depth, wp, sync) + }}; + } + + let l1 = lfo_params!( + ParamId::Lfo1Rate, + ParamId::Lfo1Phase, + ParamId::Lfo1Depth, + ParamId::Lfo1WavePos, + ParamId::Lfo1Sync + ); + let l2 = lfo_params!( + ParamId::Lfo2Rate, + ParamId::Lfo2Phase, + ParamId::Lfo2Depth, + ParamId::Lfo2WavePos, + ParamId::Lfo2Sync + ); + let l3 = lfo_params!( + ParamId::Lfo3Rate, + ParamId::Lfo3Phase, + ParamId::Lfo3Depth, + ParamId::Lfo3WavePos, + ParamId::Lfo3Sync + ); + let l4 = lfo_params!( + ParamId::Lfo4Rate, + ParamId::Lfo4Phase, + ParamId::Lfo4Depth, + ParamId::Lfo4WavePos, + ParamId::Lfo4Sync + ); + + for v in self.voices.iter_mut() { + { + let o = &mut v.oscs[0]; + ( + o.gain, + o.pan, + o.semitone, + o.fine, + o.unison_count, + o.unison_detune, + o.unison_spread, + o.wave_pos, + ) = osc1; + } + { + let o = &mut v.oscs[1]; + ( + o.gain, + o.pan, + o.semitone, + o.fine, + o.unison_count, + o.unison_detune, + o.unison_spread, + o.wave_pos, + ) = osc2; + } + { + let o = &mut v.oscs[2]; + ( + o.gain, + o.pan, + o.semitone, + o.fine, + o.unison_count, + o.unison_detune, + o.unison_spread, + o.wave_pos, + ) = osc3; + } + + { + let e = &mut v.envs[0]; + ( + e.delay, + e.attack, + e.hold, + e.decay, + e.sustain, + e.release, + e.attack_curve, + e.decay_curve, + e.release_curve, + ) = e1; + } + { + let e = &mut v.envs[1]; + ( + e.delay, + e.attack, + e.hold, + e.decay, + e.sustain, + e.release, + e.attack_curve, + e.decay_curve, + e.release_curve, + ) = e2; + } + { + let e = &mut v.envs[2]; + ( + e.delay, + e.attack, + e.hold, + e.decay, + e.sustain, + e.release, + e.attack_curve, + e.decay_curve, + e.release_curve, + ) = e3; + } + + macro_rules! apply_lfo { + ($lfo:expr, $params:expr) => { + let (rate, _initial_phase, depth, wp, sync) = $params; + $lfo.rate = rate; + $lfo.depth = depth; + $lfo.wave_pos = wp; + if sync { + $lfo.mode = lfo::LfoMode::BpmSync; + } else if $lfo.mode == lfo::LfoMode::BpmSync { + $lfo.mode = lfo::LfoMode::FreeRun; + } + }; + } + apply_lfo!(v.lfos[0], l1); + apply_lfo!(v.lfos[1], l2); + apply_lfo!(v.lfos[2], l3); + apply_lfo!(v.lfos[3], l4); + } + } + + #[inline] + pub fn process(&mut self, out_l: &mut [f32], out_r: &mut [f32]) { + debug_assert_eq!(out_l.len(), out_r.len()); + + self.sync_voice_params(); + + out_l.fill(0.0); + out_r.fill(0.0); + + let vol = self.params.get(ParamId::MasterVolume); + let bpm = self.host_bpm; + + for v in self.voices.iter_mut().filter(|v| v.active) { + for (sl, sr) in out_l.iter_mut().zip(out_r.iter_mut()) { + let (vl, vr) = v.process(bpm); + *sl += vl * vol; + *sr += vr * vol; + } + } + } } diff --git a/crates/engine/src/oscillator.rs b/crates/engine/src/oscillator.rs index 7a26e9d..bcd1319 100644 --- a/crates/engine/src/oscillator.rs +++ b/crates/engine/src/oscillator.rs @@ -1,34 +1,192 @@ +use std::sync::Arc; + pub const TABLE_SIZE: usize = 2048; -pub type Wavetable = Box<[f32; TABLE_SIZE]>; +const TABLE_MASK: usize = TABLE_SIZE - 1; +pub const MAX_UNISON: usize = 16; + +pub struct WavetableBank { + tables: Box<[[f32; TABLE_SIZE]; 4]>, +} + +impl WavetableBank { + pub fn new() -> Self { + let mut t = Box::new([[0.0f32; TABLE_SIZE]; 4]); + for i in 0..TABLE_SIZE { + let ph = i as f32 / TABLE_SIZE as f32; + let th = 2.0 * std::f32::consts::PI * ph; + t[0][i] = th.sin(); + t[1][i] = if ph < 0.5 { + 4.0 * ph - 1.0 + } else { + 3.0 - 4.0 * ph + }; + t[2][i] = 2.0 * ph - 1.0; + t[3][i] = if ph < 0.5 { 1.0 } else { -1.0 }; + } + Self { tables: t } + } + + #[inline] + pub fn lookup(&self, phase: f32, wave_pos: f32) -> f32 { + let pos = wave_pos.clamp(0.0, 1.0) * 3.0; + let lo = pos as usize; + let hi = (lo + 1).min(3); + let morph = pos - lo as f32; + let a = Self::lerp_table(&self.tables[lo], phase); + let b = Self::lerp_table(&self.tables[hi], phase); + a + (b - a) * morph + } + + #[inline] + fn lerp_table(table: &[f32; TABLE_SIZE], phase: f32) -> f32 { + let fp = phase.rem_euclid(1.0) * TABLE_SIZE as f32; + let i0 = fp as usize & TABLE_MASK; + let i1 = (i0 + 1) & TABLE_MASK; + let fr = fp - fp.floor(); + unsafe { + let a = *table.get_unchecked(i0); + let b = *table.get_unchecked(i1); + a + (b - a) * fr + } + } +} + +impl Default for WavetableBank { + fn default() -> Self { + Self::new() + } +} + +#[inline] +fn poly_blep(t: f32, dt: f32) -> f32 { + if dt <= 0.0 { + return 0.0; + } + if t < dt { + let t = t / dt; + 2.0 * t - t * t - 1.0 + } else if t > 1.0 - dt { + let t = (t - 1.0) / dt; + t * t + 2.0 * t + 1.0 + } else { + 0.0 + } +} + +#[inline] +pub fn saw_polyblep(phase: f32, dt: f32) -> f32 { + (2.0 * phase - 1.0) - poly_blep(phase, dt) +} + +#[inline] +pub fn square_polyblep(phase: f32, dt: f32, duty: f32) -> f32 { + let raw = if phase < duty { 1.0 } else { -1.0 }; + raw + poly_blep(phase, dt) - poly_blep((phase - duty).rem_euclid(1.0), dt) +} + +#[inline] +pub fn midi_to_freq(note: f32) -> f32 { + 440.0 * 2.0_f32.powf((note - 69.0) / 12.0) +} + +#[derive(Clone, Copy, Default)] +struct UnisonVoice { + phase: f32, +} pub struct WavetableOsc { - pub phase: f32, pub gain: f32, pub pan: f32, + pub semitone: f32, + pub fine: f32, pub unison_count: u8, pub unison_detune: f32, pub unison_spread: f32, pub wave_pos: f32, - wavetable: Wavetable, + pub use_polyblep: bool, + + bank: Arc, + voices: [UnisonVoice; MAX_UNISON], } impl WavetableOsc { - pub fn new() -> Self { + pub fn new(bank: Arc) -> Self { Self { - phase: 0.0, gain: 1.0, pan: 0.0, + semitone: 0.0, + fine: 0.0, unison_count: 1, - unison_detune: 0.1, + unison_detune: 0.0, unison_spread: 0.5, wave_pos: 0.0, - wavetable: Box::new([0.0f32; TABLE_SIZE]), + use_polyblep: false, + bank, + voices: [UnisonVoice::default(); MAX_UNISON], + } + } + + pub fn reset_phases(&mut self) { + for v in &mut self.voices { + v.phase = 0.0; } } #[inline] - pub fn tick(&mut self, phase_inc: f32) -> (f32, f32) { - self.phase = (self.phase + phase_inc).fract(); - (0.0, 0.0) + pub fn tick(&mut self, base_freq: f32, sr: f32, mod_semitone: f32) -> (f32, f32) { + if self.gain == 0.0 { + return (0.0, 0.0); + } + + let n = (self.unison_count as usize).clamp(1, MAX_UNISON); + + let osc_freq = + base_freq * 2.0_f32.powf((self.semitone + mod_semitone + self.fine / 100.0) / 12.0); + + let mut l_sum = 0.0f32; + let mut r_sum = 0.0f32; + + for i in 0..n { + let detune_st = if n == 1 { + 0.0 + } else { + let t = i as f32 / (n as f32 - 1.0); // 0..1 + (t * 2.0 - 1.0) * self.unison_detune * 0.5 + }; + + let freq = osc_freq * 2.0_f32.powf(detune_st / 12.0); + let dt = (freq / sr).clamp(1e-6, 0.5); + + let v = &mut self.voices[i]; + + let sample = if self.use_polyblep && self.wave_pos >= 0.5 { + if self.wave_pos < 0.75 { + saw_polyblep(v.phase, dt) + } else { + square_polyblep(v.phase, dt, 0.5) + } + } else { + self.bank.lookup(v.phase, self.wave_pos) + }; + + v.phase = (v.phase + dt).rem_euclid(1.0); + + let pan = if n == 1 { + self.pan + } else { + let t = i as f32 / (n as f32 - 1.0); + (self.pan + (t * 2.0 - 1.0) * self.unison_spread).clamp(-1.0, 1.0) + }; + + let p = (pan + 1.0) * 0.5; + let pl = (1.0 - p).sqrt(); + let pr = p.sqrt(); + + l_sum += sample * pl; + r_sum += sample * pr; + } + + let g = self.gain / (n as f32).sqrt(); + (l_sum * g, r_sum * g) } } diff --git a/crates/engine/src/voice.rs b/crates/engine/src/voice.rs index 69860c4..4251f41 100644 --- a/crates/engine/src/voice.rs +++ b/crates/engine/src/voice.rs @@ -1,31 +1,143 @@ -use crate::{envelope::Dahdsr, filter::Filter, lfo::Lfo, oscillator::WavetableOsc}; +use crate::{ + envelope::{Dahdsr, Stage}, + filter::Filter, + lfo::Lfo, + oscillator::{WavetableBank, WavetableOsc, midi_to_freq}, +}; +use std::sync::Arc; + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum PlayMode { + Poly, + Mono, + Legato, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum PortamentoMode { + Linear, + Exponential, +} pub struct Voice { pub active: bool, pub note: u8, pub velocity: f32, + pub oscs: [WavetableOsc; 3], pub envs: [Dahdsr; 3], pub lfos: [Lfo; 4], + pub filter1: Filter, pub filter2: Filter, + + pub current_freq: f32, + pub target_freq: f32, + pub portamento_time: f32, + pub portamento_mode: PortamentoMode, + + sample_rate: f32, } impl Voice { - pub fn new(sr: f32) -> Self { + pub fn new(sr: f32, bank: Arc) -> Self { Self { active: false, note: 0, velocity: 0.0, - oscs: std::array::from_fn(|_| WavetableOsc::new()), + oscs: std::array::from_fn(|_| WavetableOsc::new(Arc::clone(&bank))), envs: std::array::from_fn(|_| Dahdsr::new(sr)), - lfos: std::array::from_fn(|_| Lfo::new(sr)), + lfos: std::array::from_fn(|_| Lfo::new(sr, Arc::clone(&bank))), filter1: Filter::new(sr), filter2: Filter::new(sr), + current_freq: 440.0, + target_freq: 440.0, + portamento_time: 0.0, + portamento_mode: PortamentoMode::Exponential, + sample_rate: sr, } } + + pub fn trigger(&mut self, note: u8, velocity: f32, legato: bool) { + self.note = note; + self.velocity = velocity; + self.active = true; + self.target_freq = midi_to_freq(note as f32); + + if self.portamento_time == 0.0 { + self.current_freq = self.target_freq; + } + + if !legato { + for o in &mut self.oscs { + o.reset_phases(); + } + for e in &mut self.envs { + e.note_on(); + } + for l in &mut self.lfos { + l.retrigger(); + } + } + } + + pub fn release(&mut self) { + for e in &mut self.envs { + e.note_off(); + } + } + #[inline] - pub fn process(&mut self) -> (f32, f32) { - (0.0, 0.0) + fn advance_portamento(&mut self) -> f32 { + if self.portamento_time <= 0.0 || (self.current_freq - self.target_freq).abs() < 0.01 { + self.current_freq = self.target_freq; + return self.current_freq; + } + + let alpha = 1.0 / (self.portamento_time * self.sample_rate).max(1.0); + + match self.portamento_mode { + PortamentoMode::Linear => { + self.current_freq += (self.target_freq - self.current_freq) * alpha; + } + PortamentoMode::Exponential => { + let ratio = self.target_freq / self.current_freq.max(1.0); + let blended_ratio = 1.0 + (ratio - 1.0) * alpha; + self.current_freq *= blended_ratio; + } + } + + self.current_freq + } + + #[inline] + pub fn process(&mut self, host_bpm: f32) -> (f32, f32) { + let amp_env = self.envs[0].tick(); + + if self.envs[0].stage == Stage::Idle { + self.active = false; + return (0.0, 0.0); + } + + let _env2 = self.envs[1].tick(); + let _env3 = self.envs[2].tick(); + + let _lfo_out: [f32; 4] = std::array::from_fn(|i| self.lfos[i].tick(host_bpm)); + + let freq = self.advance_portamento(); + + let mut l = 0.0f32; + let mut r = 0.0f32; + for osc in &mut self.oscs { + let (ol, or_) = osc.tick(freq, self.sample_rate, 0.0); + l += ol; + r += or_; + } + + let l = self.filter1.process(l, freq); + let r = self.filter2.process(r, freq); + + let amp = amp_env * self.velocity; + (l * amp, r * amp) } }