update
This commit is contained in:
@@ -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) {
|
||||||
|
if self.stage != Stage::Idle {
|
||||||
|
self.release_level = self.level;
|
||||||
self.stage = Stage::Release;
|
self.stage = Stage::Release;
|
||||||
self.stage_samples = 0;
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
v.velocity = vel as f32 / 127.0;
|
|
||||||
for e in &mut v.envs {
|
|
||||||
e.note_on();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user