update
This commit is contained in:
717
crates/engine/src/effects.rs
Normal file
717
crates/engine/src/effects.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
pub enum FilterKind {
|
pub enum FilterKind {
|
||||||
Ladder,
|
Ladder,
|
||||||
@@ -6,14 +8,200 @@ pub enum FilterKind {
|
|||||||
Formant,
|
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 struct Filter {
|
||||||
pub kind: FilterKind,
|
pub kind: FilterKind,
|
||||||
pub cutoff: f32,
|
pub cutoff: f32,
|
||||||
pub resonance: f32,
|
pub resonance: f32,
|
||||||
pub drive: f32,
|
pub drive: f32,
|
||||||
pub keytrack: f32,
|
pub keytrack: f32,
|
||||||
state: [f32; 4],
|
pub fm_amount: f32,
|
||||||
|
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
|
|
||||||
|
ladder: Ladder,
|
||||||
|
svf: Svf,
|
||||||
|
comb: Comb,
|
||||||
|
formant: Formant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Filter {
|
impl Filter {
|
||||||
@@ -24,15 +212,71 @@ impl Filter {
|
|||||||
resonance: 0.0,
|
resonance: 0.0,
|
||||||
drive: 0.0,
|
drive: 0.0,
|
||||||
keytrack: 0.0,
|
keytrack: 0.0,
|
||||||
state: [0.0; 4],
|
fm_amount: 0.0,
|
||||||
sample_rate: sr,
|
sample_rate: sr,
|
||||||
|
ladder: Ladder::new(),
|
||||||
|
svf: Svf::new(),
|
||||||
|
comb: Comb::new(),
|
||||||
|
formant: Formant::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.state = [0.0; 4];
|
self.ladder.reset();
|
||||||
|
self.svf.reset();
|
||||||
|
self.comb.reset();
|
||||||
|
self.formant.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn process(&mut self, x: f32, _note_freq: f32) -> f32 {
|
pub fn process(&mut self, x: f32, note_freq: f32, fm_in: f32) -> f32 {
|
||||||
x
|
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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,416 +1,9 @@
|
|||||||
pub mod envelope;
|
pub mod envelope;
|
||||||
|
pub mod effects;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
pub mod lfo;
|
pub mod lfo;
|
||||||
pub mod mod_matrix;
|
pub mod mod_matrix;
|
||||||
pub mod oscillator;
|
pub mod oscillator;
|
||||||
pub mod voice;
|
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
193
crates/engine/src/synth.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,14 @@ pub enum PortamentoMode {
|
|||||||
Exponential,
|
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 struct Voice {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub note: u8,
|
pub note: u8,
|
||||||
@@ -30,12 +38,15 @@ pub struct Voice {
|
|||||||
|
|
||||||
pub filter1: Filter,
|
pub filter1: Filter,
|
||||||
pub filter2: Filter,
|
pub filter2: Filter,
|
||||||
|
pub filter_serial: bool,
|
||||||
|
|
||||||
pub current_freq: f32,
|
pub current_freq: f32,
|
||||||
pub target_freq: f32,
|
pub target_freq: f32,
|
||||||
pub portamento_time: f32,
|
pub portamento_time: f32,
|
||||||
pub portamento_mode: PortamentoMode,
|
pub portamento_mode: PortamentoMode,
|
||||||
|
|
||||||
|
pub mod_sources: ModSources,
|
||||||
|
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +61,12 @@ impl Voice {
|
|||||||
lfos: std::array::from_fn(|_| Lfo::new(sr, Arc::clone(&bank))),
|
lfos: std::array::from_fn(|_| Lfo::new(sr, Arc::clone(&bank))),
|
||||||
filter1: Filter::new(sr),
|
filter1: Filter::new(sr),
|
||||||
filter2: Filter::new(sr),
|
filter2: Filter::new(sr),
|
||||||
|
filter_serial: true,
|
||||||
current_freq: 440.0,
|
current_freq: 440.0,
|
||||||
target_freq: 440.0,
|
target_freq: 440.0,
|
||||||
portamento_time: 0.0,
|
portamento_time: 0.0,
|
||||||
portamento_mode: PortamentoMode::Exponential,
|
portamento_mode: PortamentoMode::Exponential,
|
||||||
|
mod_sources: ModSources::default(),
|
||||||
sample_rate: sr,
|
sample_rate: sr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,11 +76,9 @@ impl Voice {
|
|||||||
self.velocity = velocity;
|
self.velocity = velocity;
|
||||||
self.active = true;
|
self.active = true;
|
||||||
self.target_freq = midi_to_freq(note as f32);
|
self.target_freq = midi_to_freq(note as f32);
|
||||||
|
|
||||||
if self.portamento_time == 0.0 {
|
if self.portamento_time == 0.0 {
|
||||||
self.current_freq = self.target_freq;
|
self.current_freq = self.target_freq;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !legato {
|
if !legato {
|
||||||
for o in &mut self.oscs {
|
for o in &mut self.oscs {
|
||||||
o.reset_phases();
|
o.reset_phases();
|
||||||
@@ -93,49 +104,80 @@ impl Voice {
|
|||||||
self.current_freq = self.target_freq;
|
self.current_freq = self.target_freq;
|
||||||
return self.current_freq;
|
return self.current_freq;
|
||||||
}
|
}
|
||||||
|
|
||||||
let alpha = 1.0 / (self.portamento_time * self.sample_rate).max(1.0);
|
let alpha = 1.0 / (self.portamento_time * self.sample_rate).max(1.0);
|
||||||
|
|
||||||
match self.portamento_mode {
|
match self.portamento_mode {
|
||||||
PortamentoMode::Linear => {
|
PortamentoMode::Linear => {
|
||||||
self.current_freq += (self.target_freq - self.current_freq) * alpha;
|
self.current_freq += (self.target_freq - self.current_freq) * alpha;
|
||||||
}
|
}
|
||||||
PortamentoMode::Exponential => {
|
PortamentoMode::Exponential => {
|
||||||
let ratio = self.target_freq / self.current_freq.max(1.0);
|
let ratio = self.target_freq / self.current_freq.max(1.0);
|
||||||
let blended_ratio = 1.0 + (ratio - 1.0) * alpha;
|
self.current_freq *= 1.0 + (ratio - 1.0) * alpha;
|
||||||
self.current_freq *= blended_ratio;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.current_freq
|
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]
|
#[inline]
|
||||||
pub fn process(&mut self, host_bpm: f32) -> (f32, f32) {
|
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 {
|
if self.envs[0].stage == Stage::Idle {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
return (0.0, 0.0);
|
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 freq = self.advance_portamento();
|
||||||
|
|
||||||
let mut l = 0.0f32;
|
let (mut l, mut r) = (0.0f32, 0.0f32);
|
||||||
let mut r = 0.0f32;
|
|
||||||
for osc in &mut self.oscs {
|
for osc in &mut self.oscs {
|
||||||
let (ol, or_) = osc.tick(freq, self.sample_rate, 0.0);
|
let (ol, or_) = osc.tick(freq, self.sample_rate, 0.0);
|
||||||
l += ol;
|
l += ol;
|
||||||
r += or_;
|
r += or_;
|
||||||
}
|
}
|
||||||
|
|
||||||
let l = self.filter1.process(l, freq);
|
let ms = self.mod_sources;
|
||||||
let r = self.filter2.process(r, freq);
|
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;
|
let amp = amp_env * self.velocity;
|
||||||
(l * amp, r * amp)
|
(l * amp, r * amp)
|
||||||
|
|||||||
@@ -88,17 +88,23 @@ pub enum ParamId {
|
|||||||
Lfo4WavePos,
|
Lfo4WavePos,
|
||||||
Lfo4Sync,
|
Lfo4Sync,
|
||||||
|
|
||||||
// Filter 1-2
|
// Filter 1
|
||||||
Filter1Cutoff,
|
Filter1Cutoff,
|
||||||
Filter1Resonance,
|
Filter1Resonance,
|
||||||
Filter1Drive,
|
Filter1Drive,
|
||||||
Filter1Keytrack,
|
Filter1Keytrack,
|
||||||
Filter1Type,
|
Filter1Type,
|
||||||
|
Filter1FmAmount,
|
||||||
|
|
||||||
|
// Filter 2
|
||||||
Filter2Cutoff,
|
Filter2Cutoff,
|
||||||
Filter2Resonance,
|
Filter2Resonance,
|
||||||
Filter2Drive,
|
Filter2Drive,
|
||||||
Filter2Keytrack,
|
Filter2Keytrack,
|
||||||
Filter2Type,
|
Filter2Type,
|
||||||
|
Filter2FmAmount,
|
||||||
|
|
||||||
|
FilterRouting,
|
||||||
|
|
||||||
// FX
|
// FX
|
||||||
DistDrive,
|
DistDrive,
|
||||||
@@ -149,11 +155,13 @@ impl ParamId {
|
|||||||
Self::MasterVolume => 1.0,
|
Self::MasterVolume => 1.0,
|
||||||
Self::Filter1Cutoff | Self::Filter2Cutoff => 1.0,
|
Self::Filter1Cutoff | Self::Filter2Cutoff => 1.0,
|
||||||
Self::Filter1Resonance | Self::Filter2Resonance => 0.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::Env1Sustain | Self::Env2Sustain | Self::Env3Sustain => 1.0,
|
||||||
Self::Env1Attack | Self::Env2Attack | Self::Env3Attack => 0.01,
|
Self::Env1Attack | Self::Env2Attack | Self::Env3Attack => 0.01,
|
||||||
Self::Env1Decay | Self::Env2Decay | Self::Env3Decay => 0.3,
|
Self::Env1Decay | Self::Env2Decay | Self::Env3Decay => 0.3,
|
||||||
Self::Env1Release | Self::Env2Release | Self::Env3Release => 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::ReverbMix => 0.15,
|
||||||
Self::ChorusMix => 0.0,
|
Self::ChorusMix => 0.0,
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
@@ -174,6 +182,13 @@ impl ParamId {
|
|||||||
Self::Filter1Resonance => "Res",
|
Self::Filter1Resonance => "Res",
|
||||||
Self::Filter1Drive => "Drive",
|
Self::Filter1Drive => "Drive",
|
||||||
Self::Filter1Keytrack => "Key",
|
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::Env1Attack => "A",
|
||||||
Self::Env1Decay => "D",
|
Self::Env1Decay => "D",
|
||||||
Self::Env1Sustain => "S",
|
Self::Env1Sustain => "S",
|
||||||
@@ -217,6 +232,7 @@ impl ParamStore {
|
|||||||
pub fn get(&self, id: ParamId) -> f32 {
|
pub fn get(&self, id: ParamId) -> f32 {
|
||||||
self.params[id as usize].load(Ordering::Relaxed)
|
self.params[id as usize].load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set(&self, id: ParamId, v: f32) {
|
pub fn set(&self, id: ParamId, v: f32) {
|
||||||
self.params[id as usize].store(v, Ordering::Relaxed);
|
self.params[id as usize].store(v, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -10,4 +10,9 @@ crate-type = ["cdylib"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
params = { workspace = true }
|
params = { workspace = true }
|
||||||
engine = { workspace = true }
|
engine = { workspace = true }
|
||||||
lv2 = { workspace = true }
|
lv2 = { workspace = true, features = [
|
||||||
|
"lv2-atom",
|
||||||
|
"lv2-units",
|
||||||
|
"lv2-urid",
|
||||||
|
"lv2-midi",
|
||||||
|
] }
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
||||||
|
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
|
||||||
|
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
|
||||||
|
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||||
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
||||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||||
|
|
||||||
@@ -6,16 +9,24 @@
|
|||||||
a lv2:Plugin, lv2:InstrumentPlugin ;
|
a lv2:Plugin, lv2:InstrumentPlugin ;
|
||||||
|
|
||||||
doap:name "Tenko" ;
|
doap:name "Tenko" ;
|
||||||
doap:license <https://spdx.org/licenses/MIT> ;
|
|
||||||
|
lv2:requiredFeature urid:map ;
|
||||||
|
|
||||||
lv2:port [
|
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 ;
|
a lv2:OutputPort, lv2:AudioPort ;
|
||||||
lv2:index 0 ;
|
lv2:index 1 ;
|
||||||
lv2:symbol "out_l" ;
|
lv2:symbol "out_l" ;
|
||||||
rdfs:label "Left Out"
|
rdfs:label "Left Out"
|
||||||
] , [
|
] , [
|
||||||
a lv2:OutputPort, lv2:AudioPort ;
|
a lv2:OutputPort, lv2:AudioPort ;
|
||||||
lv2:index 1 ;
|
lv2:index 2 ;
|
||||||
lv2:symbol "out_r" ;
|
lv2:symbol "out_r" ;
|
||||||
rdfs:label "Right Out"
|
rdfs:label "Right Out"
|
||||||
] .
|
] .
|
||||||
|
|||||||
@@ -1,35 +1,98 @@
|
|||||||
use engine::Engine;
|
use std::sync::Arc;
|
||||||
use lv2::prelude::*;
|
|
||||||
|
use engine::{oscillator::WavetableBank, synth::Synth};
|
||||||
|
use lv2::lv2_urid::LV2Map;
|
||||||
|
use lv2::{lv2_atom, prelude::*};
|
||||||
use params::ParamStore;
|
use params::ParamStore;
|
||||||
|
|
||||||
|
#[derive(URIDCollection)]
|
||||||
|
struct TenkoUrids {
|
||||||
|
atom: AtomURIDCollection,
|
||||||
|
midi: MidiURIDCollection,
|
||||||
|
units: UnitURIDCollection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FeatureCollection)]
|
||||||
|
struct InitFeatures<'a> {
|
||||||
|
map: LV2Map<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PortCollection)]
|
#[derive(PortCollection)]
|
||||||
struct Ports {
|
struct Ports {
|
||||||
|
midi_in: InputPort<AtomPort>,
|
||||||
out_l: OutputPort<Audio>,
|
out_l: OutputPort<Audio>,
|
||||||
out_r: OutputPort<Audio>,
|
out_r: OutputPort<Audio>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uri("https://git.yokai.digital/deadYokai/tenko")]
|
#[uri("https://git.yokai.digital/deadYokai/tenko")]
|
||||||
struct TenkoLv2 {
|
struct TenkoLv2 {
|
||||||
engine: Engine,
|
synth: Synth,
|
||||||
|
urids: TenkoUrids,
|
||||||
|
host_bpm: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for TenkoLv2 {
|
impl Plugin for TenkoLv2 {
|
||||||
type Ports = Ports;
|
type Ports = Ports;
|
||||||
type InitFeatures = ();
|
type InitFeatures = InitFeatures<'static>;
|
||||||
type AudioFeatures = ();
|
type AudioFeatures = ();
|
||||||
|
|
||||||
fn new(info: &PluginInfo, _features: &mut ()) -> Option<Self> {
|
fn new(info: &PluginInfo, features: &mut InitFeatures<'static>) -> Option<Self> {
|
||||||
|
let urids = TenkoUrids::from_map(&features.map)?;
|
||||||
let params = ParamStore::new();
|
let params = ParamStore::new();
|
||||||
let engine = Engine::new(params, info.sample_rate() as f32);
|
let bank = Arc::new(WavetableBank::new());
|
||||||
Some(Self { engine })
|
let synth = Synth::new(info.sample_rate() as f32, params, bank);
|
||||||
|
Some(Self {
|
||||||
|
synth,
|
||||||
|
urids,
|
||||||
|
host_bpm: 120.0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&mut self, ports: &mut Ports, _: &mut (), _: u32) {
|
fn run(&mut self, ports: &mut Ports, _features: &mut (), _count: u32) {
|
||||||
for s in ports.out_l.iter_mut() {
|
if let Some(seq) = ports
|
||||||
*s = 0.0;
|
.midi_in
|
||||||
|
.read(self.urids.atom.sequence, self.urids.units.beat)
|
||||||
|
{
|
||||||
|
for (_, atom) in seq {
|
||||||
|
if let Some(raw) = atom.read(self.urids.midi.raw, ()) {
|
||||||
|
self.dispatch_midi(raw);
|
||||||
|
} else if let Some((_header, reader)) = atom.read(self.urids.atom.object, ()) {
|
||||||
|
self.handle_time_position(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for s in ports.out_r.iter_mut() {
|
|
||||||
*s = 0.0;
|
for (l, r) in ports.out_l.iter_mut().zip(ports.out_r.iter_mut()) {
|
||||||
|
let (sl, sr) = self.synth.process(self.host_bpm);
|
||||||
|
*l = sl;
|
||||||
|
*r = sr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TenkoLv2 {
|
||||||
|
#[inline]
|
||||||
|
fn dispatch_midi(&mut self, raw: &[u8]) {
|
||||||
|
if raw.len() < 3 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let note = raw[1];
|
||||||
|
let vel = raw[2];
|
||||||
|
match raw[0] & 0xF0 {
|
||||||
|
0x90 if vel > 0 => self.synth.note_on(note, vel),
|
||||||
|
0x90 | 0x80 => self.synth.note_off(note),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn handle_time_position(&mut self, reader: lv2_atom::object::ObjectReader) {
|
||||||
|
for (_, atom) in reader {
|
||||||
|
if let Some(bpm) = atom.read(self.urids.atom.float, ())
|
||||||
|
&& bpm > 0.0
|
||||||
|
{
|
||||||
|
self.host_bpm = bpm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user