This commit is contained in:
2026-04-20 00:24:09 +03:00
parent cdff703f7e
commit 1cc59d5408
5 changed files with 791 additions and 62 deletions

View File

@@ -1,4 +1,4 @@
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum Stage { pub enum Stage {
Idle, Idle,
Delay, Delay,
@@ -12,17 +12,21 @@ pub enum Stage {
pub struct Dahdsr { pub struct Dahdsr {
pub stage: Stage, pub stage: Stage,
pub level: f32, pub level: f32,
pub delay: f32, pub delay: f32,
pub attack: f32, pub attack: f32,
pub hold: f32, pub hold: f32,
pub decay: f32, pub decay: f32,
pub sustain: f32, pub sustain: f32,
pub release: f32, pub release: f32,
pub attack_curve: f32, pub attack_curve: f32,
pub decay_curve: f32, pub decay_curve: f32,
pub release_curve: f32, pub release_curve: f32,
pub sample_rate: f32,
sample_rate: f32,
stage_samples: u32, stage_samples: u32,
release_level: f32,
} }
impl Dahdsr { impl Dahdsr {
@@ -41,21 +45,113 @@ impl Dahdsr {
release_curve: 0.0, release_curve: 0.0,
sample_rate: sr, sample_rate: sr,
stage_samples: 0, stage_samples: 0,
release_level: 0.0,
} }
} }
pub fn note_on(&mut self) { pub fn note_on(&mut self) {
self.stage = Stage::Delay; self.stage = Stage::Delay;
self.stage_samples = 0; self.stage_samples = 0;
} }
pub fn note_off(&mut self) { pub fn note_off(&mut self) {
self.stage = Stage::Release; if self.stage != Stage::Idle {
self.stage_samples = 0; self.release_level = self.level;
self.stage = Stage::Release;
self.stage_samples = 0;
}
} }
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
self.stage != Stage::Idle self.stage != Stage::Idle
} }
#[inline]
fn shape(t: f32, curve: f32) -> f32 {
if curve.abs() < 1e-4 {
return t;
}
let exp = 1.0
+ if curve > 0.0 {
curve * 9.0
} else {
curve * 0.9
};
t.powf(exp)
}
#[inline] #[inline]
pub fn tick(&mut self) -> f32 { pub fn tick(&mut self) -> f32 {
0.0 let sr = self.sample_rate;
match self.stage {
Stage::Idle => {
self.level = 0.0;
}
Stage::Delay => {
self.level = 0.0;
let len = (self.delay * sr) as u32;
self.stage_samples += 1;
if len == 0 || self.stage_samples >= len {
self.stage = Stage::Attack;
self.stage_samples = 0;
}
}
Stage::Attack => {
let len = (self.attack * sr).max(1.0) as u32;
self.stage_samples += 1;
let t = (self.stage_samples as f32 / len as f32).min(1.0);
self.level = Self::shape(t, -self.attack_curve);
if self.stage_samples >= len {
self.level = 1.0;
self.stage = Stage::Hold;
self.stage_samples = 0;
}
}
Stage::Hold => {
self.level = 1.0;
let len = (self.hold * sr) as u32;
self.stage_samples += 1;
if len == 0 || self.stage_samples >= len {
self.stage = Stage::Decay;
self.stage_samples = 0;
}
}
Stage::Decay => {
let len = (self.decay * sr).max(1.0) as u32;
self.stage_samples += 1;
let t = (self.stage_samples as f32 / len as f32).min(1.0);
let shaped = Self::shape(1.0 - t, self.decay_curve);
self.level = self.sustain + (1.0 - self.sustain) * shaped;
if self.stage_samples >= len {
self.level = self.sustain;
self.stage = Stage::Sustain;
self.stage_samples = 0;
}
}
Stage::Sustain => {
self.level = self.sustain;
}
Stage::Release => {
let len = (self.release * sr).max(1.0) as u32;
self.stage_samples += 1;
let t = (self.stage_samples as f32 / len as f32).min(1.0);
let shaped = Self::shape(1.0 - t, self.release_curve);
self.level = self.release_level * shaped;
if self.stage_samples >= len {
self.level = 0.0;
self.stage = Stage::Idle;
self.stage_samples = 0;
}
}
}
self.level
} }
} }

View File

@@ -1,4 +1,7 @@
#[derive(Clone, Copy)] use crate::oscillator::WavetableBank;
use std::sync::Arc;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum LfoMode { pub enum LfoMode {
FreeRun, FreeRun,
BpmSync, BpmSync,
@@ -6,33 +9,82 @@ pub enum LfoMode {
Envelope, Envelope,
} }
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum LfoPhaseBehavior {
Global,
PerVoice,
}
pub struct Lfo { pub struct Lfo {
pub phase: f32, pub phase: f32,
pub initial_phase: f32,
pub rate: f32, pub rate: f32,
pub depth: f32, pub depth: f32,
pub mode: LfoMode, pub mode: LfoMode,
pub phase_behavior: LfoPhaseBehavior,
pub wave_pos: f32, pub wave_pos: f32,
pub sync: bool, finished: bool,
sample_rate: f32, sample_rate: f32,
bank: Arc<WavetableBank>,
} }
impl Lfo { impl Lfo {
pub fn new(sr: f32) -> Self { pub fn new(sr: f32, bank: Arc<WavetableBank>) -> Self {
Self { Self {
phase: 0.0, phase: 0.0,
initial_phase: 0.0,
rate: 1.0, rate: 1.0,
depth: 1.0, depth: 1.0,
mode: LfoMode::FreeRun, mode: LfoMode::FreeRun,
phase_behavior: LfoPhaseBehavior::PerVoice,
wave_pos: 0.0, wave_pos: 0.0,
sync: false, finished: false,
sample_rate: sr, sample_rate: sr,
bank,
} }
} }
pub fn retrigger(&mut self) { pub fn retrigger(&mut self) {
self.phase = 0.0; if self.phase_behavior == LfoPhaseBehavior::PerVoice {
self.phase = self.initial_phase;
self.finished = false;
}
} }
#[inline] #[inline]
pub fn tick(&mut self, _host_bpm: f32) -> f32 { pub fn tick(&mut self, host_bpm: f32) -> f32 {
0.0 if self.finished {
return self.bank.lookup(1.0, self.wave_pos) * self.depth;
}
let dt = self.phase_increment(host_bpm);
let sample = self.bank.lookup(self.phase, self.wave_pos) * self.depth;
self.phase += dt;
if self.phase >= 1.0 {
match self.mode {
LfoMode::OneShot => {
self.phase = 1.0;
self.finished = true;
}
_ => {
self.phase -= 1.0;
}
}
}
sample
}
#[inline]
fn phase_increment(&self, host_bpm: f32) -> f32 {
match self.mode {
LfoMode::FreeRun | LfoMode::OneShot => self.rate / self.sample_rate,
LfoMode::BpmSync => {
let beats_per_sec = host_bpm / 60.0;
beats_per_sec * self.rate / self.sample_rate
}
LfoMode::Envelope => 1.0 / (self.rate.max(1e-4) * self.sample_rate),
}
} }
} }

View File

@@ -5,10 +5,16 @@ pub mod mod_matrix;
pub mod oscillator; pub mod oscillator;
pub mod voice; pub mod voice;
use std::sync::Arc;
use crossbeam_channel::{Receiver, Sender, bounded}; use crossbeam_channel::{Receiver, Sender, bounded};
use envelope::Stage;
use mod_matrix::ModMatrix; use mod_matrix::ModMatrix;
use oscillator::WavetableBank;
use params::{ParamId, ParamStore}; use params::{ParamId, ParamStore};
use voice::Voice; use voice::{PlayMode, PortamentoMode, Voice};
pub use voice::{PlayMode as EnginePlayMode, PortamentoMode as EnginePortamentoMode};
pub const MAX_VOICES: usize = 16; pub const MAX_VOICES: usize = 16;
@@ -25,12 +31,19 @@ pub struct Engine {
round_robin: usize, round_robin: usize,
mod_matrix: ModMatrix, mod_matrix: ModMatrix,
pub metrics_tx: Sender<EngineMetrics>, pub metrics_tx: Sender<EngineMetrics>,
play_mode: PlayMode,
host_bpm: f32,
_bank: Arc<WavetableBank>,
} }
impl Engine { impl Engine {
pub fn new(params: ParamStore, sample_rate: f32) -> (Self, Receiver<EngineMetrics>) { pub fn new(params: ParamStore, sample_rate: f32) -> (Self, Receiver<EngineMetrics>) {
let (tx, rx) = bounded(4); let (tx, rx) = bounded(4);
let voices = Box::new(std::array::from_fn(|_| Voice::new(sample_rate))); let bank = Arc::new(WavetableBank::new());
let voices = Box::new(std::array::from_fn(|_| {
Voice::new(sample_rate, Arc::clone(&bank))
}));
( (
Self { Self {
params, params,
@@ -39,30 +52,77 @@ impl Engine {
round_robin: 0, round_robin: 0,
mod_matrix: ModMatrix::new(), mod_matrix: ModMatrix::new(),
metrics_tx: tx, metrics_tx: tx,
play_mode: PlayMode::Poly,
host_bpm: 120.0,
_bank: bank,
}, },
rx, rx,
) )
} }
pub fn new_simple(params: ParamStore, sample_rate: f32) -> Self { pub fn new_simple(params: ParamStore, sample_rate: f32) -> Self {
let (engine, _rx) = Self::new(params, sample_rate); Self::new(params, sample_rate).0
engine
} }
pub fn set_sample_rate(&mut self, rate: f32) { pub fn set_sample_rate(&mut self, rate: f32) {
self.sample_rate = rate; self.sample_rate = rate;
} }
pub fn note_on(&mut self, note: u8, vel: u8) { pub fn set_play_mode(&mut self, mode: PlayMode) {
let idx = self.round_robin % MAX_VOICES; self.play_mode = mode;
let v = &mut self.voices[idx]; }
v.active = true;
v.note = note; pub fn set_bpm(&mut self, bpm: f32) {
v.velocity = vel as f32 / 127.0; self.host_bpm = bpm.max(1.0);
for e in &mut v.envs { }
e.note_on();
fn allocate_voice(&mut self, note: u8) -> (usize, bool) {
match self.play_mode {
PlayMode::Mono => (0, false),
PlayMode::Legato => {
let legato = self.voices[0].active;
(0, legato)
}
PlayMode::Poly => {
if let Some(i) = self.voices.iter().position(|v| v.active && v.note == note) {
return (i, false);
}
if let Some(i) = self.voices.iter().position(|v| !v.active) {
self.round_robin = i + 1;
return (i, false);
}
if let Some(i) = self
.voices
.iter()
.position(|v| v.active && v.envs[0].stage == Stage::Release)
{
return (i, false);
}
let idx = self.round_robin % MAX_VOICES;
self.round_robin += 1;
(idx, false)
}
} }
self.round_robin += 1; }
pub fn note_on(&mut self, note: u8, vel: u8) {
let port_time = {
let t = self.params.get(ParamId::PortamentoTime);
t * t * 5.0
};
let port_mode = if self.params.get(ParamId::PortamentoMode) > 0.5 {
PortamentoMode::Exponential
} else {
PortamentoMode::Linear
};
let (idx, legato) = self.allocate_voice(note);
let v = &mut self.voices[idx];
v.portamento_time = port_time;
v.portamento_mode = port_mode;
v.trigger(note, vel as f32 / 127.0, legato);
} }
pub fn note_off(&mut self, note: u8) { pub fn note_off(&mut self, note: u8) {
@@ -71,27 +131,7 @@ impl Engine {
.iter_mut() .iter_mut()
.filter(|v| v.active && v.note == note) .filter(|v| v.active && v.note == note)
{ {
for e in &mut v.envs { v.release();
e.note_off();
}
}
}
/// No alloc, no lock. Must stay that way.
#[inline]
pub fn process(&mut self, out_l: &mut [f32], out_r: &mut [f32]) {
debug_assert_eq!(out_l.len(), out_r.len());
out_l.fill(0.0);
out_r.fill(0.0);
let vol = self.params.get(ParamId::MasterVolume);
for v in self.voices.iter_mut().filter(|v| v.active) {
let (vl, vr) = v.process();
for (o, s) in out_l.iter_mut().zip(std::iter::repeat(vl)) {
*o += s * vol;
}
for (o, s) in out_r.iter_mut().zip(std::iter::repeat(vr)) {
*o += s * vol;
}
} }
} }
@@ -102,4 +142,275 @@ impl Engine {
_ => {} _ => {}
} }
} }
#[inline]
fn sync_voice_params(&mut self) {
macro_rules! osc_params {
($g:expr, $p:expr, $st:expr, $fi:expr, $uc:expr, $ud:expr, $us:expr, $wp:expr) => {{
let gain = self.params.get($g);
let pan = self.params.get($p) * 2.0 - 1.0;
let semi = self.params.get($st) * 96.0 - 48.0;
let fine = self.params.get($fi) * 200.0 - 100.0;
let ucnt = ((self.params.get($uc) * 15.0) as u8).saturating_add(1);
let udet = self.params.get($ud) * 2.0;
let uspr = self.params.get($us);
let wp = self.params.get($wp);
(gain, pan, semi, fine, ucnt, udet, uspr, wp)
}};
}
let osc1 = osc_params!(
ParamId::Osc1Gain,
ParamId::Osc1Pan,
ParamId::Osc1Semitone,
ParamId::Osc1Fine,
ParamId::Osc1UnisonCount,
ParamId::Osc1UnisonDetune,
ParamId::Osc1UnisonSpread,
ParamId::Osc1WavePos
);
let osc2 = osc_params!(
ParamId::Osc2Gain,
ParamId::Osc2Pan,
ParamId::Osc2Semitone,
ParamId::Osc2Fine,
ParamId::Osc2UnisonCount,
ParamId::Osc2UnisonDetune,
ParamId::Osc2UnisonSpread,
ParamId::Osc2WavePos
);
let osc3 = osc_params!(
ParamId::Osc3Gain,
ParamId::Osc3Pan,
ParamId::Osc3Semitone,
ParamId::Osc3Fine,
ParamId::Osc3UnisonCount,
ParamId::Osc3UnisonDetune,
ParamId::Osc3UnisonSpread,
ParamId::Osc3WavePos
);
macro_rules! env_times {
($d:expr,$a:expr,$h:expr,$dc:expr,$s:expr,$r:expr,
$ac:expr,$dcc:expr,$rc:expr) => {{
let sq = |p: ParamId| {
let v = self.params.get(p);
v * v * 10.0
};
let cv = |p: ParamId| self.params.get(p) * 2.0 - 1.0;
(
sq($d),
sq($a),
sq($h),
sq($dc),
self.params.get($s),
sq($r),
cv($ac),
cv($dcc),
cv($rc),
)
}};
}
let e1 = env_times!(
ParamId::Env1Delay,
ParamId::Env1Attack,
ParamId::Env1Hold,
ParamId::Env1Decay,
ParamId::Env1Sustain,
ParamId::Env1Release,
ParamId::Env1AttackCurve,
ParamId::Env1DecayCurve,
ParamId::Env1ReleaseCurve
);
let e2 = env_times!(
ParamId::Env2Delay,
ParamId::Env2Attack,
ParamId::Env2Hold,
ParamId::Env2Decay,
ParamId::Env2Sustain,
ParamId::Env2Release,
ParamId::Env2AttackCurve,
ParamId::Env2DecayCurve,
ParamId::Env2ReleaseCurve
);
let e3 = env_times!(
ParamId::Env3Delay,
ParamId::Env3Attack,
ParamId::Env3Hold,
ParamId::Env3Decay,
ParamId::Env3Sustain,
ParamId::Env3Release,
ParamId::Env3AttackCurve,
ParamId::Env3DecayCurve,
ParamId::Env3ReleaseCurve
);
macro_rules! lfo_params {
($rt:expr, $ph:expr, $dp:expr, $wp:expr, $sy:expr) => {{
let rate = {
let v = self.params.get($rt);
0.01 * 10.0_f32.powf(v * 3.0)
};
let phase = self.params.get($ph);
let depth = self.params.get($dp);
let wp = self.params.get($wp);
let sync = self.params.get($sy) > 0.5;
(rate, phase, depth, wp, sync)
}};
}
let l1 = lfo_params!(
ParamId::Lfo1Rate,
ParamId::Lfo1Phase,
ParamId::Lfo1Depth,
ParamId::Lfo1WavePos,
ParamId::Lfo1Sync
);
let l2 = lfo_params!(
ParamId::Lfo2Rate,
ParamId::Lfo2Phase,
ParamId::Lfo2Depth,
ParamId::Lfo2WavePos,
ParamId::Lfo2Sync
);
let l3 = lfo_params!(
ParamId::Lfo3Rate,
ParamId::Lfo3Phase,
ParamId::Lfo3Depth,
ParamId::Lfo3WavePos,
ParamId::Lfo3Sync
);
let l4 = lfo_params!(
ParamId::Lfo4Rate,
ParamId::Lfo4Phase,
ParamId::Lfo4Depth,
ParamId::Lfo4WavePos,
ParamId::Lfo4Sync
);
for v in self.voices.iter_mut() {
{
let o = &mut v.oscs[0];
(
o.gain,
o.pan,
o.semitone,
o.fine,
o.unison_count,
o.unison_detune,
o.unison_spread,
o.wave_pos,
) = osc1;
}
{
let o = &mut v.oscs[1];
(
o.gain,
o.pan,
o.semitone,
o.fine,
o.unison_count,
o.unison_detune,
o.unison_spread,
o.wave_pos,
) = osc2;
}
{
let o = &mut v.oscs[2];
(
o.gain,
o.pan,
o.semitone,
o.fine,
o.unison_count,
o.unison_detune,
o.unison_spread,
o.wave_pos,
) = osc3;
}
{
let e = &mut v.envs[0];
(
e.delay,
e.attack,
e.hold,
e.decay,
e.sustain,
e.release,
e.attack_curve,
e.decay_curve,
e.release_curve,
) = e1;
}
{
let e = &mut v.envs[1];
(
e.delay,
e.attack,
e.hold,
e.decay,
e.sustain,
e.release,
e.attack_curve,
e.decay_curve,
e.release_curve,
) = e2;
}
{
let e = &mut v.envs[2];
(
e.delay,
e.attack,
e.hold,
e.decay,
e.sustain,
e.release,
e.attack_curve,
e.decay_curve,
e.release_curve,
) = e3;
}
macro_rules! apply_lfo {
($lfo:expr, $params:expr) => {
let (rate, _initial_phase, depth, wp, sync) = $params;
$lfo.rate = rate;
$lfo.depth = depth;
$lfo.wave_pos = wp;
if sync {
$lfo.mode = lfo::LfoMode::BpmSync;
} else if $lfo.mode == lfo::LfoMode::BpmSync {
$lfo.mode = lfo::LfoMode::FreeRun;
}
};
}
apply_lfo!(v.lfos[0], l1);
apply_lfo!(v.lfos[1], l2);
apply_lfo!(v.lfos[2], l3);
apply_lfo!(v.lfos[3], l4);
}
}
#[inline]
pub fn process(&mut self, out_l: &mut [f32], out_r: &mut [f32]) {
debug_assert_eq!(out_l.len(), out_r.len());
self.sync_voice_params();
out_l.fill(0.0);
out_r.fill(0.0);
let vol = self.params.get(ParamId::MasterVolume);
let bpm = self.host_bpm;
for v in self.voices.iter_mut().filter(|v| v.active) {
for (sl, sr) in out_l.iter_mut().zip(out_r.iter_mut()) {
let (vl, vr) = v.process(bpm);
*sl += vl * vol;
*sr += vr * vol;
}
}
}
} }

View File

@@ -1,34 +1,192 @@
use std::sync::Arc;
pub const TABLE_SIZE: usize = 2048; pub const TABLE_SIZE: usize = 2048;
pub type Wavetable = Box<[f32; TABLE_SIZE]>; const TABLE_MASK: usize = TABLE_SIZE - 1;
pub const MAX_UNISON: usize = 16;
pub struct WavetableBank {
tables: Box<[[f32; TABLE_SIZE]; 4]>,
}
impl WavetableBank {
pub fn new() -> Self {
let mut t = Box::new([[0.0f32; TABLE_SIZE]; 4]);
for i in 0..TABLE_SIZE {
let ph = i as f32 / TABLE_SIZE as f32;
let th = 2.0 * std::f32::consts::PI * ph;
t[0][i] = th.sin();
t[1][i] = if ph < 0.5 {
4.0 * ph - 1.0
} else {
3.0 - 4.0 * ph
};
t[2][i] = 2.0 * ph - 1.0;
t[3][i] = if ph < 0.5 { 1.0 } else { -1.0 };
}
Self { tables: t }
}
#[inline]
pub fn lookup(&self, phase: f32, wave_pos: f32) -> f32 {
let pos = wave_pos.clamp(0.0, 1.0) * 3.0;
let lo = pos as usize;
let hi = (lo + 1).min(3);
let morph = pos - lo as f32;
let a = Self::lerp_table(&self.tables[lo], phase);
let b = Self::lerp_table(&self.tables[hi], phase);
a + (b - a) * morph
}
#[inline]
fn lerp_table(table: &[f32; TABLE_SIZE], phase: f32) -> f32 {
let fp = phase.rem_euclid(1.0) * TABLE_SIZE as f32;
let i0 = fp as usize & TABLE_MASK;
let i1 = (i0 + 1) & TABLE_MASK;
let fr = fp - fp.floor();
unsafe {
let a = *table.get_unchecked(i0);
let b = *table.get_unchecked(i1);
a + (b - a) * fr
}
}
}
impl Default for WavetableBank {
fn default() -> Self {
Self::new()
}
}
#[inline]
fn poly_blep(t: f32, dt: f32) -> f32 {
if dt <= 0.0 {
return 0.0;
}
if t < dt {
let t = t / dt;
2.0 * t - t * t - 1.0
} else if t > 1.0 - dt {
let t = (t - 1.0) / dt;
t * t + 2.0 * t + 1.0
} else {
0.0
}
}
#[inline]
pub fn saw_polyblep(phase: f32, dt: f32) -> f32 {
(2.0 * phase - 1.0) - poly_blep(phase, dt)
}
#[inline]
pub fn square_polyblep(phase: f32, dt: f32, duty: f32) -> f32 {
let raw = if phase < duty { 1.0 } else { -1.0 };
raw + poly_blep(phase, dt) - poly_blep((phase - duty).rem_euclid(1.0), dt)
}
#[inline]
pub fn midi_to_freq(note: f32) -> f32 {
440.0 * 2.0_f32.powf((note - 69.0) / 12.0)
}
#[derive(Clone, Copy, Default)]
struct UnisonVoice {
phase: f32,
}
pub struct WavetableOsc { pub struct WavetableOsc {
pub phase: f32,
pub gain: f32, pub gain: f32,
pub pan: f32, pub pan: f32,
pub semitone: f32,
pub fine: f32,
pub unison_count: u8, pub unison_count: u8,
pub unison_detune: f32, pub unison_detune: f32,
pub unison_spread: f32, pub unison_spread: f32,
pub wave_pos: f32, pub wave_pos: f32,
wavetable: Wavetable, pub use_polyblep: bool,
bank: Arc<WavetableBank>,
voices: [UnisonVoice; MAX_UNISON],
} }
impl WavetableOsc { impl WavetableOsc {
pub fn new() -> Self { pub fn new(bank: Arc<WavetableBank>) -> Self {
Self { Self {
phase: 0.0,
gain: 1.0, gain: 1.0,
pan: 0.0, pan: 0.0,
semitone: 0.0,
fine: 0.0,
unison_count: 1, unison_count: 1,
unison_detune: 0.1, unison_detune: 0.0,
unison_spread: 0.5, unison_spread: 0.5,
wave_pos: 0.0, wave_pos: 0.0,
wavetable: Box::new([0.0f32; TABLE_SIZE]), use_polyblep: false,
bank,
voices: [UnisonVoice::default(); MAX_UNISON],
}
}
pub fn reset_phases(&mut self) {
for v in &mut self.voices {
v.phase = 0.0;
} }
} }
#[inline] #[inline]
pub fn tick(&mut self, phase_inc: f32) -> (f32, f32) { pub fn tick(&mut self, base_freq: f32, sr: f32, mod_semitone: f32) -> (f32, f32) {
self.phase = (self.phase + phase_inc).fract(); if self.gain == 0.0 {
(0.0, 0.0) return (0.0, 0.0);
}
let n = (self.unison_count as usize).clamp(1, MAX_UNISON);
let osc_freq =
base_freq * 2.0_f32.powf((self.semitone + mod_semitone + self.fine / 100.0) / 12.0);
let mut l_sum = 0.0f32;
let mut r_sum = 0.0f32;
for i in 0..n {
let detune_st = if n == 1 {
0.0
} else {
let t = i as f32 / (n as f32 - 1.0); // 0..1
(t * 2.0 - 1.0) * self.unison_detune * 0.5
};
let freq = osc_freq * 2.0_f32.powf(detune_st / 12.0);
let dt = (freq / sr).clamp(1e-6, 0.5);
let v = &mut self.voices[i];
let sample = if self.use_polyblep && self.wave_pos >= 0.5 {
if self.wave_pos < 0.75 {
saw_polyblep(v.phase, dt)
} else {
square_polyblep(v.phase, dt, 0.5)
}
} else {
self.bank.lookup(v.phase, self.wave_pos)
};
v.phase = (v.phase + dt).rem_euclid(1.0);
let pan = if n == 1 {
self.pan
} else {
let t = i as f32 / (n as f32 - 1.0);
(self.pan + (t * 2.0 - 1.0) * self.unison_spread).clamp(-1.0, 1.0)
};
let p = (pan + 1.0) * 0.5;
let pl = (1.0 - p).sqrt();
let pr = p.sqrt();
l_sum += sample * pl;
r_sum += sample * pr;
}
let g = self.gain / (n as f32).sqrt();
(l_sum * g, r_sum * g)
} }
} }

View File

@@ -1,31 +1,143 @@
use crate::{envelope::Dahdsr, filter::Filter, lfo::Lfo, oscillator::WavetableOsc}; use crate::{
envelope::{Dahdsr, Stage},
filter::Filter,
lfo::Lfo,
oscillator::{WavetableBank, WavetableOsc, midi_to_freq},
};
use std::sync::Arc;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum PlayMode {
Poly,
Mono,
Legato,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum PortamentoMode {
Linear,
Exponential,
}
pub struct Voice { pub struct Voice {
pub active: bool, pub active: bool,
pub note: u8, pub note: u8,
pub velocity: f32, pub velocity: f32,
pub oscs: [WavetableOsc; 3], pub oscs: [WavetableOsc; 3],
pub envs: [Dahdsr; 3], pub envs: [Dahdsr; 3],
pub lfos: [Lfo; 4], pub lfos: [Lfo; 4],
pub filter1: Filter, pub filter1: Filter,
pub filter2: Filter, pub filter2: Filter,
pub current_freq: f32,
pub target_freq: f32,
pub portamento_time: f32,
pub portamento_mode: PortamentoMode,
sample_rate: f32,
} }
impl Voice { impl Voice {
pub fn new(sr: f32) -> Self { pub fn new(sr: f32, bank: Arc<WavetableBank>) -> Self {
Self { Self {
active: false, active: false,
note: 0, note: 0,
velocity: 0.0, velocity: 0.0,
oscs: std::array::from_fn(|_| WavetableOsc::new()), oscs: std::array::from_fn(|_| WavetableOsc::new(Arc::clone(&bank))),
envs: std::array::from_fn(|_| Dahdsr::new(sr)), envs: std::array::from_fn(|_| Dahdsr::new(sr)),
lfos: std::array::from_fn(|_| Lfo::new(sr)), lfos: std::array::from_fn(|_| Lfo::new(sr, Arc::clone(&bank))),
filter1: Filter::new(sr), filter1: Filter::new(sr),
filter2: Filter::new(sr), filter2: Filter::new(sr),
current_freq: 440.0,
target_freq: 440.0,
portamento_time: 0.0,
portamento_mode: PortamentoMode::Exponential,
sample_rate: sr,
} }
} }
pub fn trigger(&mut self, note: u8, velocity: f32, legato: bool) {
self.note = note;
self.velocity = velocity;
self.active = true;
self.target_freq = midi_to_freq(note as f32);
if self.portamento_time == 0.0 {
self.current_freq = self.target_freq;
}
if !legato {
for o in &mut self.oscs {
o.reset_phases();
}
for e in &mut self.envs {
e.note_on();
}
for l in &mut self.lfos {
l.retrigger();
}
}
}
pub fn release(&mut self) {
for e in &mut self.envs {
e.note_off();
}
}
#[inline] #[inline]
pub fn process(&mut self) -> (f32, f32) { fn advance_portamento(&mut self) -> f32 {
(0.0, 0.0) if self.portamento_time <= 0.0 || (self.current_freq - self.target_freq).abs() < 0.01 {
self.current_freq = self.target_freq;
return self.current_freq;
}
let alpha = 1.0 / (self.portamento_time * self.sample_rate).max(1.0);
match self.portamento_mode {
PortamentoMode::Linear => {
self.current_freq += (self.target_freq - self.current_freq) * alpha;
}
PortamentoMode::Exponential => {
let ratio = self.target_freq / self.current_freq.max(1.0);
let blended_ratio = 1.0 + (ratio - 1.0) * alpha;
self.current_freq *= blended_ratio;
}
}
self.current_freq
}
#[inline]
pub fn process(&mut self, host_bpm: f32) -> (f32, f32) {
let amp_env = self.envs[0].tick();
if self.envs[0].stage == Stage::Idle {
self.active = false;
return (0.0, 0.0);
}
let _env2 = self.envs[1].tick();
let _env3 = self.envs[2].tick();
let _lfo_out: [f32; 4] = std::array::from_fn(|i| self.lfos[i].tick(host_bpm));
let freq = self.advance_portamento();
let mut l = 0.0f32;
let mut r = 0.0f32;
for osc in &mut self.oscs {
let (ol, or_) = osc.tick(freq, self.sample_rate, 0.0);
l += ol;
r += or_;
}
let l = self.filter1.process(l, freq);
let r = self.filter2.process(r, freq);
let amp = amp_env * self.velocity;
(l * amp, r * amp)
} }
} }