diff --git a/crates/engine/src/effects.rs b/crates/engine/src/effects.rs new file mode 100644 index 0000000..c9889ab --- /dev/null +++ b/crates/engine/src/effects.rs @@ -0,0 +1,717 @@ +use std::f32::consts::PI; + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum DistType { + SoftClip, + Wavefolder, + Bitcrush, +} + +#[derive(Clone)] +pub struct Distortion { + pub kind: DistType, + pub drive: f32, + pub mix: f32, +} + +impl Distortion { + pub fn new() -> Self { + Self { + kind: DistType::SoftClip, + drive: 0.0, + mix: 1.0, + } + } + + #[inline] + pub fn process(&self, x: f32) -> f32 { + if self.drive == 0.0 { + return x; + } + let gain = 1.0 + self.drive * 15.0; + let wet = match self.kind { + DistType::SoftClip => { + let d = x * gain; + if d.abs() < 1.0 { + d - d * d * d / 3.0 + } else { + d.signum() * 2.0 / 3.0 + } + } + DistType::Wavefolder => { + let mut v = x * gain; + for _ in 0..4 { + if v > 1.0 { + v = 2.0 - v; + } + if v < -1.0 { + v = -2.0 - v; + } + } + v + } + DistType::Bitcrush => { + let bits = (1 << (1 + (15.0 * (1.0 - self.drive)) as u32).max(1)) as f32; + (x * gain * bits).round() / bits + } + }; + x + (wet - x) * self.mix + } +} + +const CHORUS_BUF: usize = 8192; + +#[derive(Clone)] +pub struct Chorus { + pub rate: f32, + pub depth: f32, + pub mix: f32, + pub feedback: f32, + pub stereo_offset: f32, + + buf_l: Box<[f32; CHORUS_BUF]>, + buf_r: Box<[f32; CHORUS_BUF]>, + write: usize, + lfo_phase: f32, + sample_rate: f32, +} + +impl Chorus { + pub fn new(sr: f32) -> Self { + Self { + rate: 0.5, + depth: 0.5, + mix: 0.0, + feedback: 0.0, + stereo_offset: PI / 2.0, + buf_l: Box::new([0.0; CHORUS_BUF]), + buf_r: Box::new([0.0; CHORUS_BUF]), + write: 0, + lfo_phase: 0.0, + sample_rate: sr, + } + } + + pub fn reset(&mut self) { + *self.buf_l = [0.0; CHORUS_BUF]; + *self.buf_r = [0.0; CHORUS_BUF]; + self.write = 0; + self.lfo_phase = 0.0; + } + + #[inline] + pub fn process(&mut self, (xl, xr): (f32, f32)) -> (f32, f32) { + if self.mix == 0.0 { + return (xl, xr); + } + + let lfo_l = self.lfo_phase.sin(); + let lfo_r = (self.lfo_phase + self.stereo_offset).sin(); + self.lfo_phase += 2.0 * PI * self.rate / self.sample_rate; + if self.lfo_phase > 2.0 * PI { + self.lfo_phase -= 2.0 * PI; + } + + let base_ms = 7.0; + let depth_ms = self.depth * 15.0; + let delay_l = ((base_ms + lfo_l * depth_ms) * 0.001 * self.sample_rate) as usize; + let delay_r = ((base_ms + lfo_r * depth_ms) * 0.001 * self.sample_rate) as usize; + + let dl = delay_l.clamp(1, CHORUS_BUF - 1); + let dr = delay_r.clamp(1, CHORUS_BUF - 1); + + let read_l = (self.write + CHORUS_BUF - dl) % CHORUS_BUF; + let read_r = (self.write + CHORUS_BUF - dr) % CHORUS_BUF; + + let wet_l = self.buf_l[read_l]; + let wet_r = self.buf_r[read_r]; + + self.buf_l[self.write] = xl + wet_l * self.feedback; + self.buf_r[self.write] = xr + wet_r * self.feedback; + self.write = (self.write + 1) % CHORUS_BUF; + + (xl + (wet_l - xl) * self.mix, xr + (wet_r - xr) * self.mix) + } +} + +const PHASER_MAX_STAGES: usize = 8; + +#[derive(Clone)] +pub struct Phaser { + pub rate: f32, + pub depth: f32, + pub mix: f32, + pub poles: usize, + pub feedback: f32, + + ap_state: [f32; PHASER_MAX_STAGES], + lfo_phase: f32, + fb_sample: f32, + sample_rate: f32, +} + +impl Phaser { + pub fn new(sr: f32) -> Self { + Self { + rate: 0.3, + depth: 0.7, + mix: 0.0, + poles: 4, + feedback: 0.0, + ap_state: [0.0; PHASER_MAX_STAGES], + lfo_phase: 0.0, + fb_sample: 0.0, + sample_rate: sr, + } + } + + pub fn reset(&mut self) { + self.ap_state = [0.0; PHASER_MAX_STAGES]; + self.lfo_phase = 0.0; + self.fb_sample = 0.0; + } + + #[inline] + pub fn process(&mut self, x: f32) -> f32 { + if self.mix == 0.0 { + return x; + } + + let lfo = self.lfo_phase.sin(); + self.lfo_phase += 2.0 * PI * self.rate / self.sample_rate; + if self.lfo_phase > 2.0 * PI { + self.lfo_phase -= 2.0 * PI; + } + + let freq_hz = 200.0 * (40.0_f32).powf(0.5 + 0.5 * lfo * self.depth); + let a = (PI * freq_hz / self.sample_rate).tan(); + let coef = (a - 1.0) / (a + 1.0); + + let poles = self.poles.min(PHASER_MAX_STAGES); + let mut s = x + self.fb_sample * self.feedback; + + for i in 0..poles { + let y = coef * s + self.ap_state[i] - coef * self.ap_state[i]; // 1-pole AP + let yn = coef * (s - self.ap_state[i]) + self.ap_state[i]; + self.ap_state[i] = s; + s = yn; + let _ = y; + } + + self.fb_sample = s; + x + (s - x) * self.mix + } +} + +const fn dl(ms: f32, sr: f32) -> usize { + (ms * 0.001 * sr) as usize +} + +const PREDELAY_MAX: usize = 4096; +const INPUT_DIFF1_A: usize = 142; +const INPUT_DIFF1_B: usize = 107; +const INPUT_DIFF2_A: usize = 379; +const INPUT_DIFF2_B: usize = 277; +const TANK_DL: [usize; 8] = [2048, 4453, 2560, 4217, 908, 3720, 2656, 3163]; + +const REVERB_TAPS: &[(usize, usize)] = &[ + (1, 266), + (1, 2974), + (1, 2673), + (2, 187), + (3, 1996), + (3, 353), + (3, 3627), + (0, 1990), + (0, 1228), + (0, 335), + (2, 1913), + (2, 2111), +]; + +#[derive(Clone)] +struct Ring { + buf: Vec, + pos: usize, + len: usize, +} + +impl Ring { + fn new(len: usize) -> Self { + Self { + buf: vec![0.0; len], + pos: 0, + len, + } + } + #[inline] + fn read(&self, offset: usize) -> f32 { + self.buf[(self.pos + self.len - offset % self.len) % self.len] + } + #[inline] + fn write(&mut self, v: f32) { + self.buf[self.pos] = v; + self.pos = (self.pos + 1) % self.len; + } + #[inline] + fn allpass(&mut self, x: f32, delay: usize, c: f32) -> f32 { + let d = self.read(delay); + let y = -c * x + d + c * x * c; + let buf = self.read(delay); + let out = -c * x + buf; + self.write(x + c * buf); + out + } +} + +#[derive(Clone)] +pub struct Reverb { + pub size: f32, + pub damping: f32, + pub mix: f32, + + predelay: Ring, + idiff: [Ring; 4], + tank: [Ring; 8], + damp_state: [f32; 2], + lfo_phase: f32, + sample_rate: f32, +} + +impl Reverb { + pub fn new(sr: f32) -> Self { + let scale = sr / 44100.0; + let sz = |n: usize| ((n as f32 * scale) as usize).max(1); + let tank_lens: [usize; 8] = std::array::from_fn(|i| sz(TANK_DL[i])); + + #[cfg(debug_assertions)] + for &(buf, tap) in REVERB_TAPS { + debug_assert!( + tap < tank_lens[buf], + "Reverb tap out of range: tank[{buf}] tap={tap} but len={} at sr={sr}. \ + Increase TANK_DL[{buf}] or scale tap with SR.", + tank_lens[buf], + ); + } + + Self { + size: 0.7, + damping: 0.3, + mix: 0.0, + predelay: Ring::new(PREDELAY_MAX), + idiff: [ + Ring::new(sz(INPUT_DIFF1_A)), + Ring::new(sz(INPUT_DIFF1_B)), + Ring::new(sz(INPUT_DIFF2_A)), + Ring::new(sz(INPUT_DIFF2_B)), + ], + tank: std::array::from_fn(|i| Ring::new(tank_lens[i])), + damp_state: [0.0; 2], + lfo_phase: 0.0, + sample_rate: sr, + } + } + + pub fn reset(&mut self) { + for r in &mut self.idiff { + r.buf.fill(0.0); + r.pos = 0; + } + for r in &mut self.tank { + r.buf.fill(0.0); + r.pos = 0; + } + self.predelay.buf.fill(0.0); + self.predelay.pos = 0; + self.damp_state = [0.0; 2]; + self.lfo_phase = 0.0; + } + + #[inline] + pub fn process(&mut self, (xl, xr): (f32, f32)) -> (f32, f32) { + if self.mix == 0.0 { + return (xl, xr); + } + + let decay = 0.5 + self.size * 0.49; + let damp = self.damping; + + let pd_len = (self.size * PREDELAY_MAX as f32 * 0.5) as usize; + let pd_len = pd_len.clamp(1, PREDELAY_MAX - 1); + let x_in = (xl + xr) * 0.5; + let pd_out = self.predelay.read(pd_len); + self.predelay.write(x_in); + + let coeffs = [0.75, 0.75, 0.625, 0.625_f32]; + let delays = [ + self.idiff[0].len - 1, + self.idiff[1].len - 1, + self.idiff[2].len - 1, + self.idiff[3].len - 1, + ]; + let mut s = pd_out; + for i in 0..4 { + s = self.idiff[i].allpass(s, delays[i], coeffs[i]); + } + + let lfo = self.lfo_phase.sin() * 8.0; + self.lfo_phase += 2.0 * PI * 0.3 / self.sample_rate; + if self.lfo_phase > 2.0 * PI { + self.lfo_phase -= 2.0 * PI; + } + + let t0 = self.tank[0].len - 1; + let t1 = self.tank[1].len - 1; + let t2 = self.tank[2].len - 1; + let t3 = self.tank[3].len - 1; + + let tank_in_l = s + self.tank[3].read(t3) * decay; + self.damp_state[0] = self.damp_state[0] + damp * (tank_in_l - self.damp_state[0]); + let ap_out_l = self.tank[0].allpass(self.damp_state[0], t0, -0.7); + self.tank[1].write(ap_out_l); + let dl_out_l = self.tank[1].read(t1); + + let tank_in_r = s + dl_out_l * decay; + self.damp_state[1] = self.damp_state[1] + damp * (tank_in_r - self.damp_state[1]); + let ap_out_r = self.tank[2].allpass(self.damp_state[1], t2, -0.7); + self.tank[3].write(ap_out_r); + let dl_out_r = self.tank[3].read(t3); + + let wet_l = self.tank[1].read(266) + self.tank[1].read(2974) - self.tank[2].read(1913) + + self.tank[3].read(1996) + - self.tank[0].read(1990) + - self.tank[2].read(187); + let wet_r = self.tank[3].read(353) + self.tank[3].read(3627) - self.tank[0].read(1228) + + self.tank[1].read(2673) + - self.tank[2].read(2111) + - self.tank[0].read(335); + + let _ = (lfo, dl_out_l, dl_out_r); + let wl = wet_l * 0.6; + let wr = wet_r * 0.6; + (xl + (wl - xl) * self.mix, xr + (wr - xr) * self.mix) + } +} + +const DELAY_MAX_SECS: f32 = 2.0; + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum DelaySync { + Free, + TempoSync, +} + +#[derive(Clone)] +pub struct Delay { + pub time: f32, + pub feedback: f32, + pub mix: f32, + pub sync: DelaySync, + pub ping_pong: bool, + pub filter_hz: f32, + + buf_l: Vec, + buf_r: Vec, + write: usize, + lp_l: f32, + lp_r: f32, + sample_rate: f32, +} + +impl Delay { + pub fn new(sr: f32) -> Self { + let sz = (DELAY_MAX_SECS * sr) as usize + 1; + Self { + time: 0.5, + feedback: 0.5, + mix: 0.0, + sync: DelaySync::Free, + ping_pong: false, + filter_hz: 8000.0, + buf_l: vec![0.0; sz], + buf_r: vec![0.0; sz], + write: 0, + lp_l: 0.0, + lp_r: 0.0, + sample_rate: sr, + } + } + + pub fn reset(&mut self) { + self.buf_l.fill(0.0); + self.buf_r.fill(0.0); + self.write = 0; + self.lp_l = 0.0; + self.lp_r = 0.0; + } + + #[inline] + pub fn process(&mut self, (xl, xr): (f32, f32), host_bpm: f32) -> (f32, f32) { + if self.mix == 0.0 { + return (xl, xr); + } + + let delay_secs = match self.sync { + DelaySync::Free => self.time * DELAY_MAX_SECS, + DelaySync::TempoSync => { + let fractions = [0.125_f32, 0.25, 0.375, 0.5, 0.75, 1.0]; + let idx = (self.time * (fractions.len() - 1) as f32) as usize; + let beats = fractions[idx.min(fractions.len() - 1)]; + beats * 60.0 / host_bpm.max(20.0) + } + }; + + let delay_samps = (delay_secs * self.sample_rate) as usize; + let sz = self.buf_l.len(); + let delay_samps = delay_samps.clamp(1, sz - 1); + let read = (self.write + sz - delay_samps) % sz; + + let rd_l = self.buf_l[read]; + let rd_r = self.buf_r[read]; + + let lp_c = (2.0 * PI * self.filter_hz / self.sample_rate).min(0.99); + self.lp_l += lp_c * (rd_l - self.lp_l); + self.lp_r += lp_c * (rd_r - self.lp_r); + + if self.ping_pong { + self.buf_l[self.write] = xl + self.lp_r * self.feedback; + self.buf_r[self.write] = self.lp_l * self.feedback; + } else { + self.buf_l[self.write] = xl + self.lp_l * self.feedback; + self.buf_r[self.write] = xr + self.lp_r * self.feedback; + } + self.write = (self.write + 1) % sz; + + (xl + (rd_l - xl) * self.mix, xr + (rd_r - xr) * self.mix) + } +} + +#[derive(Clone, Copy)] +struct BiquadCoeffs { + b0: f32, + b1: f32, + b2: f32, + a1: f32, + a2: f32, +} + +#[derive(Clone, Copy)] +struct BiquadState { + x1: f32, + x2: f32, + y1: f32, + y2: f32, +} + +impl BiquadState { + fn new() -> Self { + Self { + x1: 0.0, + x2: 0.0, + y1: 0.0, + y2: 0.0, + } + } + + #[inline] + fn process(&mut self, x: f32, c: &BiquadCoeffs) -> f32 { + let y = c.b0 * x + c.b1 * self.x1 + c.b2 * self.x2 - c.a1 * self.y1 - c.a2 * self.y2; + self.x2 = self.x1; + self.x1 = x; + self.y2 = self.y1; + self.y1 = y; + y + } +} + +fn low_shelf(freq: f32, gain_db: f32, sr: f32) -> BiquadCoeffs { + let a = 10.0_f32.powf(gain_db / 40.0); + let w0 = 2.0 * PI * freq / sr; + let cos_w = w0.cos(); + let sin_w = w0.sin(); + let s = sin_w / 2.0 * ((a + 1.0 / a) * (1.0 / 0.707 - 1.0) + 2.0).sqrt(); + let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w + 2.0 * a.sqrt() * s); + let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w); + let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w - 2.0 * a.sqrt() * s); + let a0 = (a + 1.0) + (a - 1.0) * cos_w + 2.0 * a.sqrt() * s; + let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w); + let a2 = (a + 1.0) + (a - 1.0) * cos_w - 2.0 * a.sqrt() * s; + BiquadCoeffs { + b0: b0 / a0, + b1: b1 / a0, + b2: b2 / a0, + a1: a1 / a0, + a2: a2 / a0, + } +} + +fn high_shelf(freq: f32, gain_db: f32, sr: f32) -> BiquadCoeffs { + let a = 10.0_f32.powf(gain_db / 40.0); + let w0 = 2.0 * PI * freq / sr; + let cos_w = w0.cos(); + let sin_w = w0.sin(); + let s = sin_w / 2.0 * ((a + 1.0 / a) * (1.0 / 0.707 - 1.0) + 2.0).sqrt(); + let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w + 2.0 * a.sqrt() * s); + let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w); + let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w - 2.0 * a.sqrt() * s); + let a0 = (a + 1.0) - (a - 1.0) * cos_w + 2.0 * a.sqrt() * s; + let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w); + let a2 = (a + 1.0) - (a - 1.0) * cos_w - 2.0 * a.sqrt() * s; + BiquadCoeffs { + b0: b0 / a0, + b1: b1 / a0, + b2: b2 / a0, + a1: a1 / a0, + a2: a2 / a0, + } +} + +fn peaking_eq(freq: f32, gain_db: f32, q: f32, sr: f32) -> BiquadCoeffs { + let a = 10.0_f32.powf(gain_db / 40.0); + let w0 = 2.0 * PI * freq / sr; + let alpha = w0.sin() / (2.0 * q.max(0.01)); + let b0 = 1.0 + alpha * a; + let b1 = -2.0 * w0.cos(); + let b2 = 1.0 - alpha * a; + let a0 = 1.0 + alpha / a; + let a1 = -2.0 * w0.cos(); + let a2 = 1.0 - alpha / a; + BiquadCoeffs { + b0: b0 / a0, + b1: b1 / a0, + b2: b2 / a0, + a1: a1 / a0, + a2: a2 / a0, + } +} + +#[derive(Clone)] +pub struct Eq { + pub low_gain_db: f32, + pub mid_freq_hz: f32, + pub mid_gain_db: f32, + pub mid_q: f32, + pub high_gain_db: f32, + + ls_l: BiquadState, + ls_r: BiquadState, + mid_l: BiquadState, + mid_r: BiquadState, + hs_l: BiquadState, + hs_r: BiquadState, + + ls_c: BiquadCoeffs, + mid_c: BiquadCoeffs, + hs_c: BiquadCoeffs, + + dirty: bool, + sample_rate: f32, +} + +impl Eq { + pub fn new(sr: f32) -> Self { + let flat = BiquadCoeffs { + b0: 1.0, + b1: 0.0, + b2: 0.0, + a1: 0.0, + a2: 0.0, + }; + Self { + low_gain_db: 0.0, + mid_freq_hz: 1000.0, + mid_gain_db: 0.0, + mid_q: 1.0, + high_gain_db: 0.0, + ls_l: BiquadState::new(), + ls_r: BiquadState::new(), + mid_l: BiquadState::new(), + mid_r: BiquadState::new(), + hs_l: BiquadState::new(), + hs_r: BiquadState::new(), + ls_c: flat, + mid_c: flat, + hs_c: flat, + dirty: true, + sample_rate: sr, + } + } + + pub fn update_coeffs(&mut self) { + self.ls_c = low_shelf(80.0, self.low_gain_db, self.sample_rate); + self.mid_c = peaking_eq( + self.mid_freq_hz.clamp(100.0, 16000.0), + self.mid_gain_db, + self.mid_q, + self.sample_rate, + ); + self.hs_c = high_shelf(12000.0, self.high_gain_db, self.sample_rate); + self.dirty = false; + } + + #[inline] + pub fn process(&mut self, (xl, xr): (f32, f32)) -> (f32, f32) { + if self.dirty { + self.update_coeffs(); + } + + let l = self.ls_l.process(xl, &self.ls_c); + let r = self.ls_r.process(xr, &self.ls_c); + let l = self.mid_l.process(l, &self.mid_c); + let r = self.mid_r.process(r, &self.mid_c); + let l = self.hs_l.process(l, &self.hs_c); + let r = self.hs_r.process(r, &self.hs_c); + (l, r) + } + + pub fn mark_dirty(&mut self) { + self.dirty = true; + } +} + +pub struct EffectsChain { + pub dist: Distortion, + pub chorus: Chorus, + pub phaser: Phaser, + pub reverb: Reverb, + pub delay: Delay, + pub eq: Eq, +} + +impl EffectsChain { + pub fn new(sr: f32) -> Self { + Self { + dist: Distortion::new(), + chorus: Chorus::new(sr), + phaser: Phaser::new(sr), + reverb: Reverb::new(sr), + delay: Delay::new(sr), + eq: Eq::new(sr), + } + } + + pub fn reset(&mut self) { + self.chorus.reset(); + self.phaser.reset(); + self.reverb.reset(); + self.delay.reset(); + } + + #[inline] + pub fn process(&mut self, (xl, xr): (f32, f32), host_bpm: f32) -> (f32, f32) { + let xl = self.dist.process(xl); + let xr = self.dist.process(xr); + + let (xl, xr) = self.chorus.process((xl, xr)); + + let mono = (xl + xr) * 0.5; + let ph = self.phaser.process(mono); + let (xl, xr) = (xl + (ph - xl) * 0.5, xr + (ph - xr) * 0.5); + + let (xl, xr) = self.reverb.process((xl, xr)); + + let (xl, xr) = self.delay.process((xl, xr), host_bpm); + + self.eq.process((xl, xr)) + } +} diff --git a/crates/engine/src/filter.rs b/crates/engine/src/filter.rs index 117e184..2c2eceb 100644 --- a/crates/engine/src/filter.rs +++ b/crates/engine/src/filter.rs @@ -1,3 +1,5 @@ +use std::f32::consts::PI; + #[derive(Clone, Copy, PartialEq, Debug)] pub enum FilterKind { Ladder, @@ -6,14 +8,200 @@ pub enum FilterKind { Formant, } +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum FilterRouting { + Serial, + Parallel, +} + +#[derive(Clone)] +struct Ladder { + s: [f32; 4], + comp: f32, +} + +impl Ladder { + fn new() -> Self { + Self { + s: [0.0; 4], + comp: 0.5, + } + } + fn reset(&mut self) { + self.s = [0.0; 4]; + } + + #[inline] + fn process(&mut self, x: f32, cutoff_norm: f32, res: f32, drive: f32) -> f32 { + let g = (PI * cutoff_norm.clamp(0.001, 0.499)).tan(); + let G = g / (1.0 + g); + let k = 4.0 * res.clamp(0.0, 1.0); + + let xd = (x * (1.0 + drive * 3.0)).tanh(); + + let u = xd - k * self.s[3]; + let lp0 = self.s[0] + G * (self.tanh_cheap(u) - self.s[0]); + let lp1 = self.s[1] + G * (self.tanh_cheap(lp0) - self.s[1]); + let lp2 = self.s[2] + G * (self.tanh_cheap(lp1) - self.s[2]); + let lp3 = self.s[3] + G * (self.tanh_cheap(lp2) - self.s[3]); + + self.s[0] = lp0; + self.s[1] = lp1; + self.s[2] = lp2; + self.s[3] = lp3; + + lp3 * (1.0 + self.comp * k) + } + + #[inline(always)] + fn tanh_cheap(&self, x: f32) -> f32 { + let x2 = x * x; + x * (27.0 + x2) / (27.0 + 9.0 * x2) + } +} + +#[derive(Clone)] +struct Svf { + ic1eq: f32, + ic2eq: f32, +} + +impl Svf { + fn new() -> Self { + Self { + ic1eq: 0.0, + ic2eq: 0.0, + } + } + fn reset(&mut self) { + self.ic1eq = 0.0; + self.ic2eq = 0.0; + } + + #[inline] + fn process(&mut self, x: f32, cutoff_norm: f32, res: f32, drive: f32) -> f32 { + let g = (PI * cutoff_norm.clamp(0.001, 0.499)).tan(); + let k = 2.0 - 2.0 * res.clamp(0.0, 1.0); + + let xd = if drive > 0.0 { + (x * (1.0 + drive * 2.0)).tanh() + } else { + x + }; + + let v1 = (xd - self.ic2eq - k * self.ic1eq) / (1.0 + g * (g + k)); + let v2 = self.ic2eq + g * (xd - self.ic2eq - k * self.ic1eq) / (1.0 + g * (g + k)); + + let g1 = g * v1; + let hp = xd - k * self.ic1eq - self.ic2eq; + let bp = g * hp / (1.0 + g * (g + k)) + self.ic1eq; + let lp = g * bp + self.ic2eq; + + self.ic1eq = 2.0 * bp - self.ic1eq; + self.ic2eq = 2.0 * lp - self.ic2eq; + + let _ = (v1, v2, g1, hp); + lp + } +} + +const COMB_MAX: usize = 4096; + +#[derive(Clone)] +struct Comb { + buf: Box<[f32; COMB_MAX]>, + pos: usize, +} + +impl Comb { + fn new() -> Self { + Self { + buf: Box::new([0.0; COMB_MAX]), + pos: 0, + } + } + fn reset(&mut self) { + *self.buf = [0.0; COMB_MAX]; + self.pos = 0; + } + + #[inline] + fn process(&mut self, x: f32, cutoff_norm: f32, res: f32) -> f32 { + let delay = ((1.0 - cutoff_norm.clamp(0.01, 0.99)) * (COMB_MAX - 1) as f32) as usize; + let delay = delay.max(1); + let read_idx = (self.pos + COMB_MAX - delay) % COMB_MAX; + let y = self.buf[read_idx]; + self.buf[self.pos] = x + y * res.clamp(0.0, 0.98); + self.pos = (self.pos + 1) % COMB_MAX; + y + } +} + +#[derive(Clone)] +struct Formant { + svf1: Svf, + svf2: Svf, +} + +static VOWELS: [(f32, f32, f32, f32); 5] = [ + (800.0, 80.0, 1200.0, 120.0), + (400.0, 40.0, 2200.0, 220.0), + (350.0, 35.0, 2800.0, 280.0), + (450.0, 45.0, 750.0, 75.0), + (325.0, 32.0, 700.0, 70.0), +]; + +impl Formant { + fn new() -> Self { + Self { + svf1: Svf::new(), + svf2: Svf::new(), + } + } + fn reset(&mut self) { + self.svf1.reset(); + self.svf2.reset(); + } + + #[inline] + fn process(&mut self, x: f32, cutoff: f32, sr: f32) -> f32 { + let t = (cutoff * 4.0).clamp(0.0, 3.9999); + let i = t as usize; + let frac = t - i as f32; + let (f1a, b1a, f2a, b2a) = VOWELS[i]; + let (f1b, b1b, f2b, b2b) = VOWELS[(i + 1).min(4)]; + let f1 = f1a + (f1b - f1a) * frac; + let b1 = b1a + (b1b - b1a) * frac; + let f2 = f2a + (f2b - f2a) * frac; + let _b2 = b2a + (b2b - b2a) * frac; + + let c1 = (f1 / sr).clamp(0.001, 0.499); + let r1 = 1.0 - (PI * b1 / sr).min(0.99); + let c2 = (f2 / sr).clamp(0.001, 0.499); + let r2 = 1.0 - (PI * _b2 / sr).min(0.99); + + let y1 = self.svf1.process(x, c1, r1, 0.0); + let y2 = self.svf2.process(x, c2, r2, 0.0); + (y1 + y2) * 0.5 + } +} + +// ── Public Filter ───────────────────────────────────────────────────────────── + pub struct Filter { pub kind: FilterKind, pub cutoff: f32, pub resonance: f32, pub drive: f32, pub keytrack: f32, - state: [f32; 4], + pub fm_amount: f32, + sample_rate: f32, + + ladder: Ladder, + svf: Svf, + comb: Comb, + formant: Formant, } impl Filter { @@ -24,15 +212,71 @@ impl Filter { resonance: 0.0, drive: 0.0, keytrack: 0.0, - state: [0.0; 4], + fm_amount: 0.0, sample_rate: sr, + ladder: Ladder::new(), + svf: Svf::new(), + comb: Comb::new(), + formant: Formant::new(), } } + pub fn reset(&mut self) { - self.state = [0.0; 4]; + self.ladder.reset(); + self.svf.reset(); + self.comb.reset(); + self.formant.reset(); } + #[inline] - pub fn process(&mut self, x: f32, _note_freq: f32) -> f32 { - x + pub fn process(&mut self, x: f32, note_freq: f32, fm_in: f32) -> f32 { + let base_norm = self.cutoff_to_norm(self.cutoff); + + let kt_shift = if self.keytrack != 0.0 { + let semis = 12.0 * (note_freq / 440.0).log2(); + semis * self.keytrack * 0.003 + } else { + 0.0 + }; + + let fm_shift = fm_in * self.fm_amount * 0.2; + let cutoff_norm = (base_norm + kt_shift + fm_shift).clamp(0.001, 0.499); + + match self.kind { + FilterKind::Ladder => self + .ladder + .process(x, cutoff_norm, self.resonance, self.drive), + FilterKind::Svf => self.svf.process(x, cutoff_norm, self.resonance, self.drive), + FilterKind::Comb => self.comb.process(x, cutoff_norm, self.resonance), + FilterKind::Formant => self.formant.process(x, self.cutoff, self.sample_rate), + } + } + + #[inline] + pub fn process_simple(&mut self, x: f32, note_freq: f32) -> f32 { + self.process(x, note_freq, 0.0) + } + + fn cutoff_to_norm(&self, v: f32) -> f32 { + let hz = 20.0 * (1000.0_f32).powf(v.clamp(0.0, 1.0)); + (hz / self.sample_rate).clamp(0.001, 0.499) + } +} + +impl Clone for Filter { + fn clone(&self) -> Self { + Self { + kind: self.kind, + cutoff: self.cutoff, + resonance: self.resonance, + drive: self.drive, + keytrack: self.keytrack, + fm_amount: self.fm_amount, + sample_rate: self.sample_rate, + ladder: self.ladder.clone(), + svf: self.svf.clone(), + comb: self.comb.clone(), + formant: self.formant.clone(), + } } } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 6ef11a5..c35b404 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1,416 +1,9 @@ pub mod envelope; +pub mod effects; pub mod filter; pub mod lfo; pub mod mod_matrix; pub mod oscillator; pub mod voice; +pub mod synth; -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::{PlayMode, PortamentoMode, Voice}; - -pub use voice::{PlayMode as EnginePlayMode, PortamentoMode as EnginePortamentoMode}; - -pub const MAX_VOICES: usize = 16; - -pub struct EngineMetrics { - pub voice_count: u8, - pub cpu_load: f32, - pub waveform: [f32; 256], -} - -pub struct Engine { - params: ParamStore, - sample_rate: f32, - voices: Box<[Voice; MAX_VOICES]>, - 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 bank = Arc::new(WavetableBank::new()); - let voices = Box::new(std::array::from_fn(|_| { - Voice::new(sample_rate, Arc::clone(&bank)) - })); - ( - Self { - params, - sample_rate, - voices, - 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 { - Self::new(params, sample_rate).0 - } - - pub fn set_sample_rate(&mut self, rate: f32) { - self.sample_rate = rate; - } - - 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) - } - } - } - - 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) { - for v in self - .voices - .iter_mut() - .filter(|v| v.active && v.note == note) - { - v.release(); - } - } - - pub fn midi_event(&mut self, data: [u8; 3]) { - match data[0] & 0xF0 { - 0x90 if data[2] > 0 => self.note_on(data[1], data[2]), - 0x80 | 0x90 => self.note_off(data[1]), - _ => {} - } - } - - #[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/synth.rs b/crates/engine/src/synth.rs new file mode 100644 index 0000000..f36b9c0 --- /dev/null +++ b/crates/engine/src/synth.rs @@ -0,0 +1,193 @@ +use std::sync::Arc; + +use params::{ParamId, ParamStore}; + +use crate::{ + effects::{DistType, EffectsChain}, + filter::FilterKind, + oscillator::WavetableBank, + voice::{PlayMode, PortamentoMode, Voice}, +}; + +const MAX_VOICES: usize = 16; + +pub struct Synth { + voices: [Voice; MAX_VOICES], + fx: EffectsChain, + params: ParamStore, + sample_rate: f32, + round_robin: usize, + play_mode: PlayMode, +} + +impl Synth { + pub fn new(sr: f32, params: ParamStore, bank: Arc) -> Self { + Self { + voices: std::array::from_fn(|_| Voice::new(sr, Arc::clone(&bank))), + fx: EffectsChain::new(sr), + params, + sample_rate: sr, + round_robin: 0, + play_mode: PlayMode::Poly, + } + } + + pub fn note_on(&mut self, note: u8, vel: u8) { + let velocity = vel as f32 / 127.0; + match self.play_mode { + PlayMode::Poly => { + let idx = self.find_free_voice(); + self.voices[idx].trigger(note, velocity, false); + self.round_robin = (idx + 1) % MAX_VOICES; + } + PlayMode::Mono | PlayMode::Legato => { + let legato = + self.play_mode == PlayMode::Legato && self.voices.iter().any(|v| v.active); + self.voices[0].trigger(note, velocity, legato); + for v in &mut self.voices[1..] { + v.active = false; + } + } + } + } + + pub fn note_off(&mut self, note: u8) { + for v in &mut self.voices { + if v.active && v.note == note { + v.release(); + } + } + } + + #[inline] + pub fn process(&mut self, host_bpm: f32) -> (f32, f32) { + self.sync_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); + l += vl; + r += vr; + } + } + + let scale = 1.0 / (MAX_VOICES as f32).sqrt(); + l *= scale; + r *= scale; + + let (l, r) = self.fx.process((l, r), host_bpm); + + let vol = self.params.get(ParamId::MasterVolume); + (l * vol, r * vol) + } + + fn sync_params(&mut self) { + let p = &self.params; + + let f1_cutoff = p.get(ParamId::Filter1Cutoff); + let f1_res = p.get(ParamId::Filter1Resonance); + let f1_drive = p.get(ParamId::Filter1Drive); + let f1_keytrack = p.get(ParamId::Filter1Keytrack); + let f1_type = filter_kind_from(p.get(ParamId::Filter1Type) as u32); + + let f2_cutoff = p.get(ParamId::Filter2Cutoff); + let f2_res = p.get(ParamId::Filter2Resonance); + let f2_drive = p.get(ParamId::Filter2Drive); + let f2_keytrack = p.get(ParamId::Filter2Keytrack); + let f2_type = filter_kind_from(p.get(ParamId::Filter2Type) as u32); + + let port_time = p.get(ParamId::PortamentoTime); + let port_mode = if p.get(ParamId::PortamentoMode) > 0.5 { + PortamentoMode::Linear + } else { + PortamentoMode::Exponential + }; + + for v in &mut self.voices { + v.filter1.cutoff = f1_cutoff; + v.filter1.resonance = f1_res; + v.filter1.drive = f1_drive; + v.filter1.keytrack = f1_keytrack; + v.filter1.kind = f1_type; + + v.filter2.cutoff = f2_cutoff; + v.filter2.resonance = f2_res; + v.filter2.drive = f2_drive; + v.filter2.keytrack = f2_keytrack; + v.filter2.kind = f2_type; + + v.portamento_time = port_time; + v.portamento_mode = port_mode; + } + + let fx = &mut self.fx; + fx.dist.drive = p.get(ParamId::DistDrive); + fx.dist.kind = dist_type_from(p.get(ParamId::DistType) as u32); + fx.dist.mix = if fx.dist.drive > 0.0 { 1.0 } else { 0.0 }; + + fx.chorus.rate = p.get(ParamId::ChorusRate) * 8.0 + 0.1; + fx.chorus.depth = p.get(ParamId::ChorusDepth); + fx.chorus.mix = p.get(ParamId::ChorusMix); + + fx.phaser.rate = p.get(ParamId::PhaserRate) * 4.0 + 0.05; + fx.phaser.depth = p.get(ParamId::PhaserDepth); + fx.phaser.mix = p.get(ParamId::PhaserMix); + + fx.reverb.size = p.get(ParamId::ReverbSize); + fx.reverb.damping = p.get(ParamId::ReverbDamping); + fx.reverb.mix = p.get(ParamId::ReverbMix); + + fx.delay.time = p.get(ParamId::DelayTime); + fx.delay.feedback = p.get(ParamId::DelayFeedback); + 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_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; + + if (eq.low_gain_db - new_low).abs() > 1e-4 + || (eq.mid_freq_hz - new_mid_f).abs() > 0.5 + || (eq.mid_gain_db - new_mid_g).abs() > 1e-4 + || (eq.high_gain_db - new_high).abs() > 1e-4 + { + eq.low_gain_db = new_low; + eq.mid_freq_hz = new_mid_f; + eq.mid_gain_db = new_mid_g; + eq.high_gain_db = new_high; + eq.mark_dirty(); + } + } + + fn find_free_voice(&self) -> usize { + for i in 0..MAX_VOICES { + let idx = (self.round_robin + i) % MAX_VOICES; + if !self.voices[idx].active { + return idx; + } + } + self.round_robin % MAX_VOICES + } +} + +fn filter_kind_from(v: u32) -> FilterKind { + match v { + 0 => FilterKind::Ladder, + 1 => FilterKind::Svf, + 2 => FilterKind::Comb, + 3 => FilterKind::Formant, + _ => FilterKind::Svf, + } +} + +fn dist_type_from(v: u32) -> DistType { + match v { + 0 => DistType::SoftClip, + 1 => DistType::Wavefolder, + 2 => DistType::Bitcrush, + _ => DistType::SoftClip, + } +} diff --git a/crates/engine/src/voice.rs b/crates/engine/src/voice.rs index 4251f41..c11f884 100644 --- a/crates/engine/src/voice.rs +++ b/crates/engine/src/voice.rs @@ -19,6 +19,14 @@ pub enum PortamentoMode { Exponential, } +#[derive(Clone, Copy, Default)] +pub struct ModSources { + pub env: [f32; 3], + pub lfo: [f32; 4], + pub vel: f32, + pub note: f32, +} + pub struct Voice { pub active: bool, pub note: u8, @@ -30,12 +38,15 @@ pub struct Voice { pub filter1: Filter, pub filter2: Filter, + pub filter_serial: bool, pub current_freq: f32, pub target_freq: f32, pub portamento_time: f32, pub portamento_mode: PortamentoMode, + pub mod_sources: ModSources, + sample_rate: f32, } @@ -50,10 +61,12 @@ impl Voice { lfos: std::array::from_fn(|_| Lfo::new(sr, Arc::clone(&bank))), filter1: Filter::new(sr), filter2: Filter::new(sr), + filter_serial: true, current_freq: 440.0, target_freq: 440.0, portamento_time: 0.0, portamento_mode: PortamentoMode::Exponential, + mod_sources: ModSources::default(), sample_rate: sr, } } @@ -63,11 +76,9 @@ impl Voice { 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(); @@ -93,49 +104,80 @@ impl Voice { 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 *= 1.0 + (ratio - 1.0) * alpha; } } - self.current_freq } + #[inline] + fn tick_mod_sources(&mut self, host_bpm: f32) -> f32 { + let env0 = self.envs[0].tick(); + let env1 = self.envs[1].tick(); + let env2 = self.envs[2].tick(); + + self.mod_sources = ModSources { + env: [env0, env1, env2], + lfo: std::array::from_fn(|i| self.lfos[i].tick(host_bpm)), + vel: self.velocity, + note: self.note as f32 / 127.0, + }; + + env0 + } + + #[inline] + fn filter_fm(ms: &ModSources, fm_amount: f32) -> f32 { + if fm_amount == 0.0 { + return 0.0; + } + let env_contrib = (ms.env[1] - 0.5) * 2.0; + let lfo_contrib = ms.lfo[0]; + (env_contrib * 0.5 + lfo_contrib * 0.5) * fm_amount + } + #[inline] pub fn process(&mut self, host_bpm: f32) -> (f32, f32) { - let amp_env = self.envs[0].tick(); + let amp_env = self.tick_mod_sources(host_bpm); 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; + let (mut l, mut r) = (0.0f32, 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 ms = self.mod_sources; + let fm1 = Self::filter_fm(&ms, self.filter1.fm_amount); + let fm2 = Self::filter_fm(&ms, self.filter2.fm_amount); + + let (l, r) = if self.filter_serial { + let l = self.filter1.process(l, freq, fm1); + let r = self.filter1.process(r, freq, fm1); + let l = self.filter2.process(l, freq, fm2); + let r = self.filter2.process(r, freq, fm2); + (l, r) + } else { + let l1 = self.filter1.process(l, freq, fm1); + let r1 = self.filter1.process(r, freq, fm1); + let l2 = self.filter2.process(l, freq, fm2); + let r2 = self.filter2.process(r, freq, fm2); + ((l1 + l2) * 0.5, (r1 + r2) * 0.5) + }; let amp = amp_env * self.velocity; (l * amp, r * amp) diff --git a/crates/params/src/lib.rs b/crates/params/src/lib.rs index def9f5e..181a5ce 100644 --- a/crates/params/src/lib.rs +++ b/crates/params/src/lib.rs @@ -88,17 +88,23 @@ pub enum ParamId { Lfo4WavePos, Lfo4Sync, - // Filter 1-2 + // Filter 1 Filter1Cutoff, Filter1Resonance, Filter1Drive, Filter1Keytrack, Filter1Type, + Filter1FmAmount, + + // Filter 2 Filter2Cutoff, Filter2Resonance, Filter2Drive, Filter2Keytrack, Filter2Type, + Filter2FmAmount, + + FilterRouting, // FX DistDrive, @@ -149,11 +155,13 @@ impl ParamId { Self::MasterVolume => 1.0, Self::Filter1Cutoff | Self::Filter2Cutoff => 1.0, Self::Filter1Resonance | Self::Filter2Resonance => 0.0, + Self::Filter1FmAmount | Self::Filter2FmAmount => 0.0, + Self::FilterRouting => 0.0, Self::Env1Sustain | Self::Env2Sustain | Self::Env3Sustain => 1.0, Self::Env1Attack | Self::Env2Attack | Self::Env3Attack => 0.01, Self::Env1Decay | Self::Env2Decay | Self::Env3Decay => 0.3, Self::Env1Release | Self::Env2Release | Self::Env3Release => 0.3, - Self::Polyphony => 1.0, // normalized: 1.0 = 16 voices + Self::Polyphony => 1.0, Self::ReverbMix => 0.15, Self::ChorusMix => 0.0, _ => 0.0, @@ -174,6 +182,13 @@ impl ParamId { Self::Filter1Resonance => "Res", Self::Filter1Drive => "Drive", Self::Filter1Keytrack => "Key", + Self::Filter1FmAmount => "FM", + Self::Filter2Cutoff => "Cutoff", + Self::Filter2Resonance => "Res", + Self::Filter2Drive => "Drive", + Self::Filter2Keytrack => "Key", + Self::Filter2FmAmount => "FM", + Self::FilterRouting => "Route", Self::Env1Attack => "A", Self::Env1Decay => "D", Self::Env1Sustain => "S", @@ -217,6 +232,7 @@ impl ParamStore { pub fn get(&self, id: ParamId) -> f32 { self.params[id as usize].load(Ordering::Relaxed) } + #[inline] pub fn set(&self, id: ParamId, v: f32) { self.params[id as usize].store(v, Ordering::Relaxed); diff --git a/crates/plugin-lv2/Cargo.toml b/crates/plugin-lv2/Cargo.toml index df99511..c973b20 100644 --- a/crates/plugin-lv2/Cargo.toml +++ b/crates/plugin-lv2/Cargo.toml @@ -10,4 +10,9 @@ crate-type = ["cdylib"] [dependencies] params = { workspace = true } engine = { workspace = true } -lv2 = { workspace = true } +lv2 = { workspace = true, features = [ + "lv2-atom", + "lv2-units", + "lv2-urid", + "lv2-midi", +] } diff --git a/crates/plugin-lv2/lv2/tenko.ttl b/crates/plugin-lv2/lv2/tenko.ttl index a279c34..23d8c9e 100644 --- a/crates/plugin-lv2/lv2/tenko.ttl +++ b/crates/plugin-lv2/lv2/tenko.ttl @@ -1,4 +1,7 @@ @prefix lv2: . +@prefix atom: . +@prefix midi: . +@prefix urid: . @prefix doap: . @prefix rdfs: . @@ -6,16 +9,24 @@ a lv2:Plugin, lv2:InstrumentPlugin ; doap:name "Tenko" ; - doap:license ; + + lv2:requiredFeature urid:map ; lv2:port [ + a lv2:InputPort, atom:AtomPort ; + lv2:index 0 ; + lv2:symbol "midi_in" ; + rdfs:label "MIDI In" ; + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent + ] , [ a lv2:OutputPort, lv2:AudioPort ; - lv2:index 0 ; + lv2:index 1 ; lv2:symbol "out_l" ; rdfs:label "Left Out" ] , [ a lv2:OutputPort, lv2:AudioPort ; - lv2:index 1 ; + lv2:index 2 ; lv2:symbol "out_r" ; rdfs:label "Right Out" ] . diff --git a/crates/plugin-lv2/src/lib.rs b/crates/plugin-lv2/src/lib.rs index 348fe1d..e65b12c 100644 --- a/crates/plugin-lv2/src/lib.rs +++ b/crates/plugin-lv2/src/lib.rs @@ -1,35 +1,98 @@ -use engine::Engine; -use lv2::prelude::*; +use std::sync::Arc; + +use engine::{oscillator::WavetableBank, synth::Synth}; +use lv2::lv2_urid::LV2Map; +use lv2::{lv2_atom, prelude::*}; use params::ParamStore; +#[derive(URIDCollection)] +struct TenkoUrids { + atom: AtomURIDCollection, + midi: MidiURIDCollection, + units: UnitURIDCollection, +} + +#[derive(FeatureCollection)] +struct InitFeatures<'a> { + map: LV2Map<'a>, +} + #[derive(PortCollection)] struct Ports { + midi_in: InputPort, out_l: OutputPort