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