This commit is contained in:
2026-04-20 13:19:15 +03:00
parent 1cc59d5408
commit ad8910ea19
9 changed files with 1333 additions and 449 deletions

View File

@@ -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<f32>,
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<f32>,
buf_r: Vec<f32>,
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))
}
}

View File

@@ -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(),
}
}
}

View File

@@ -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<EngineMetrics>,
play_mode: PlayMode,
host_bpm: f32,
_bank: Arc<WavetableBank>,
}
impl Engine {
pub fn new(params: ParamStore, sample_rate: f32) -> (Self, Receiver<EngineMetrics>) {
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;
}
}
}
}

193
crates/engine/src/synth.rs Normal file
View File

@@ -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<WavetableBank>) -> 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,
}
}

View File

@@ -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)