mod matrix
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
use params::ParamId;
|
||||
use params::{ParamId, ParamStore};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
use crate::voice::ModSources;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ModSource {
|
||||
Env(u8),
|
||||
Lfo(u8),
|
||||
@@ -11,7 +13,30 @@ pub enum ModSource {
|
||||
Macro(u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct ControlSources {
|
||||
pub modwheel: f32,
|
||||
pub aftertouch: f32,
|
||||
pub macros: [f32; 8],
|
||||
}
|
||||
|
||||
impl ControlSources {
|
||||
#[inline]
|
||||
pub fn sync_macros(&mut self, p: &ParamStore) {
|
||||
self.macros = [
|
||||
p.get(ParamId::Macro1),
|
||||
p.get(ParamId::Macro2),
|
||||
p.get(ParamId::Macro3),
|
||||
p.get(ParamId::Macro4),
|
||||
p.get(ParamId::Macro5),
|
||||
p.get(ParamId::Macro6),
|
||||
p.get(ParamId::Macro7),
|
||||
p.get(ParamId::Macro8),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ModSlot {
|
||||
pub active: bool,
|
||||
pub source: ModSource,
|
||||
@@ -32,15 +57,112 @@ impl Default for ModSlot {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ModValues {
|
||||
data: [f32; ParamId::COUNT],
|
||||
}
|
||||
|
||||
impl ModValues {
|
||||
#[inline]
|
||||
pub fn zero() -> Self {
|
||||
Self {
|
||||
data: [0.0; ParamId::COUNT],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get(&self, id: ParamId) -> f32 {
|
||||
// SAFETY: ParamId repr(usize), COUNT is the discriminant of the sentinel
|
||||
unsafe { *self.data.get_unchecked(id as usize) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn add(&mut self, id: ParamId, v: f32) {
|
||||
unsafe {
|
||||
*self.data.get_unchecked_mut(id as usize) += v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModMatrix {
|
||||
pub slots: [ModSlot; 64],
|
||||
}
|
||||
|
||||
impl Default for ModMatrix {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModMatrix {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
slots: [ModSlot::default(); 64],
|
||||
}
|
||||
}
|
||||
pub fn apply(&self, _params: ¶ms::ParamStore) {}
|
||||
|
||||
#[inline]
|
||||
pub fn compute(&self, ms: &ModSources, cs: &ControlSources) -> ModValues {
|
||||
let mut out = ModValues::zero();
|
||||
|
||||
for slot in &self.slots {
|
||||
if !slot.active {
|
||||
continue;
|
||||
}
|
||||
let depth = slot.depth;
|
||||
if depth == 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let src: f32 = match slot.source {
|
||||
ModSource::Env(i) => ms.env.get(i as usize).copied().unwrap_or(0.0),
|
||||
ModSource::Lfo(i) => ms.lfo.get(i as usize).copied().unwrap_or(0.0),
|
||||
ModSource::Velocity => ms.vel,
|
||||
ModSource::Note => ms.note,
|
||||
ModSource::Modwheel => cs.modwheel,
|
||||
ModSource::Aftertouch => cs.aftertouch,
|
||||
ModSource::Macro(i) => cs.macros.get(i as usize).copied().unwrap_or(0.0),
|
||||
};
|
||||
|
||||
out.add(slot.dest, src * depth);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn free_slot(&self) -> Option<usize> {
|
||||
self.slots.iter().position(|s| !s.active)
|
||||
}
|
||||
|
||||
pub fn connect(
|
||||
&mut self,
|
||||
source: ModSource,
|
||||
dest: ParamId,
|
||||
depth: f32,
|
||||
per_voice: bool,
|
||||
) -> Option<usize> {
|
||||
let idx = self.free_slot()?;
|
||||
self.slots[idx] = ModSlot {
|
||||
active: true,
|
||||
source,
|
||||
dest,
|
||||
depth,
|
||||
per_voice,
|
||||
};
|
||||
Some(idx)
|
||||
}
|
||||
|
||||
pub fn disconnect(&mut self, idx: usize) {
|
||||
if idx < 64 {
|
||||
self.slots[idx] = ModSlot::default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_dest(&self, dest: ParamId) -> bool {
|
||||
self.slots.iter().any(|s| s.active && s.dest == dest)
|
||||
}
|
||||
|
||||
pub fn active_slots(&self) -> impl Iterator<Item = (usize, &ModSlot)> {
|
||||
self.slots.iter().enumerate().filter(|(_, s)| s.active)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use params::{ParamId, ParamStore};
|
||||
use crate::{
|
||||
effects::{DistType, EffectsChain},
|
||||
filter::FilterKind,
|
||||
mod_matrix::{ControlSources, ModMatrix},
|
||||
oscillator::WavetableBank,
|
||||
voice::{PlayMode, PortamentoMode, Voice},
|
||||
};
|
||||
@@ -18,6 +19,8 @@ pub struct Synth {
|
||||
sample_rate: f32,
|
||||
round_robin: usize,
|
||||
play_mode: PlayMode,
|
||||
pub mod_matrix: ModMatrix,
|
||||
control_sources: ControlSources,
|
||||
}
|
||||
|
||||
impl Synth {
|
||||
@@ -29,6 +32,8 @@ impl Synth {
|
||||
sample_rate: sr,
|
||||
round_robin: 0,
|
||||
play_mode: PlayMode::Poly,
|
||||
mod_matrix: ModMatrix::new(),
|
||||
control_sources: ControlSources::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +64,30 @@ impl Synth {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_modwheel(&mut self, v: f32) {
|
||||
self.control_sources.modwheel = v.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
pub fn set_aftertouch(&mut self, v: f32) {
|
||||
self.control_sources.aftertouch = v.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
pub fn mod_matrix_mut(&mut self) -> &mut ModMatrix {
|
||||
&mut self.mod_matrix
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn process(&mut self, host_bpm: f32) -> (f32, f32) {
|
||||
self.sync_params();
|
||||
|
||||
self.control_sources.sync_macros(&self.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);
|
||||
let (vl, vr) = v.process(host_bpm, &self.mod_matrix, &self.control_sources);
|
||||
l += vl;
|
||||
r += vr;
|
||||
}
|
||||
@@ -105,6 +125,8 @@ impl Synth {
|
||||
PortamentoMode::Exponential
|
||||
};
|
||||
|
||||
self.play_mode = play_mode_from(p.get(ParamId::Polyphony));
|
||||
|
||||
for v in &mut self.voices {
|
||||
v.filter1.cutoff = f1_cutoff;
|
||||
v.filter1.resonance = f1_res;
|
||||
@@ -120,6 +142,21 @@ impl Synth {
|
||||
|
||||
v.portamento_time = port_time;
|
||||
v.portamento_mode = port_mode;
|
||||
|
||||
sync_env(&mut v.envs[0], p, 0);
|
||||
sync_env(&mut v.envs[1], p, 1);
|
||||
sync_env(&mut v.envs[2], p, 2);
|
||||
|
||||
sync_lfo(&mut v.lfos[0], p, 0);
|
||||
sync_lfo(&mut v.lfos[1], p, 1);
|
||||
sync_lfo(&mut v.lfos[2], p, 2);
|
||||
sync_lfo(&mut v.lfos[3], p, 3);
|
||||
|
||||
sync_osc_base(&mut v.oscs[0], p, 0);
|
||||
sync_osc_base(&mut v.oscs[1], p, 1);
|
||||
sync_osc_base(&mut v.oscs[2], p, 2);
|
||||
|
||||
v.filter_serial = (p.get(ParamId::FilterRouting) as u32) == 0;
|
||||
}
|
||||
|
||||
let fx = &mut self.fx;
|
||||
@@ -144,7 +181,7 @@ impl Synth {
|
||||
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_low = p.get(ParamId::EqLowGain) * 24.0 - 12.0;
|
||||
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;
|
||||
@@ -173,6 +210,69 @@ impl Synth {
|
||||
}
|
||||
}
|
||||
|
||||
const ENV_BASE: [ParamId; 3] = [ParamId::Env1Delay, ParamId::Env2Delay, ParamId::Env3Delay];
|
||||
|
||||
fn sync_env(env: &mut crate::envelope::Dahdsr, p: &ParamStore, idx: usize) {
|
||||
let base = ENV_BASE[idx] as usize;
|
||||
fn load(p: &ParamStore, base: usize, off: usize) -> f32 {
|
||||
let id: ParamId = unsafe { std::mem::transmute(base + off) };
|
||||
p.get(id)
|
||||
}
|
||||
env.delay = load(p, base, 0) * 2.0;
|
||||
env.attack = load(p, base, 1) * 10.0 + 0.001;
|
||||
env.hold = load(p, base, 2) * 2.0;
|
||||
env.decay = load(p, base, 3) * 10.0 + 0.001;
|
||||
env.sustain = load(p, base, 4);
|
||||
env.release = load(p, base, 5) * 10.0 + 0.001;
|
||||
env.attack_curve = load(p, base, 6) * 2.0 - 1.0;
|
||||
env.decay_curve = load(p, base, 7) * 2.0 - 1.0;
|
||||
env.release_curve = load(p, base, 8) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
const LFO_BASE: [ParamId; 4] = [
|
||||
ParamId::Lfo1Rate,
|
||||
ParamId::Lfo2Rate,
|
||||
ParamId::Lfo3Rate,
|
||||
ParamId::Lfo4Rate,
|
||||
];
|
||||
|
||||
fn sync_lfo(lfo: &mut crate::lfo::Lfo, p: &ParamStore, idx: usize) {
|
||||
let base = LFO_BASE[idx] as usize;
|
||||
fn load(p: &ParamStore, base: usize, off: usize) -> f32 {
|
||||
let id: ParamId = unsafe { std::mem::transmute(base + off) };
|
||||
p.get(id)
|
||||
}
|
||||
lfo.rate = load(p, base, 0) * 20.0 + 0.01;
|
||||
lfo.initial_phase = load(p, base, 1);
|
||||
lfo.depth = load(p, base, 2);
|
||||
lfo.wave_pos = load(p, base, 3);
|
||||
lfo.mode = if load(p, base, 4) > 0.5 {
|
||||
crate::lfo::LfoMode::BpmSync
|
||||
} else {
|
||||
crate::lfo::LfoMode::FreeRun
|
||||
};
|
||||
}
|
||||
|
||||
const OSC_BASE: [ParamId; 3] = [ParamId::Osc1Gain, ParamId::Osc2Gain, ParamId::Osc3Gain];
|
||||
|
||||
fn sync_osc_base(osc: &mut crate::oscillator::WavetableOsc, p: &ParamStore, idx: usize) {
|
||||
let base = OSC_BASE[idx] as usize;
|
||||
fn load(p: &ParamStore, base: usize, off: usize) -> f32 {
|
||||
let id: ParamId = unsafe { std::mem::transmute(base + off) };
|
||||
p.get(id)
|
||||
}
|
||||
osc.gain = load(p, base, 0);
|
||||
osc.pan = load(p, base, 1) * 2.0 - 1.0;
|
||||
osc.wave_pos = load(p, base, 7);
|
||||
|
||||
osc.semitone = load(p, base, 2) * 96.0 - 48.0;
|
||||
osc.fine = (load(p, base, 3) * 2.0 - 1.0) * 100.0;
|
||||
|
||||
osc.unison_count = (load(p, base, 4) * 15.0 + 1.0) as u8;
|
||||
osc.unison_detune = load(p, base, 5);
|
||||
osc.unison_spread = load(p, base, 6);
|
||||
}
|
||||
|
||||
fn filter_kind_from(v: u32) -> FilterKind {
|
||||
match v {
|
||||
0 => FilterKind::Ladder,
|
||||
@@ -191,3 +291,13 @@ fn dist_type_from(v: u32) -> DistType {
|
||||
_ => DistType::SoftClip,
|
||||
}
|
||||
}
|
||||
|
||||
fn play_mode_from(v: f32) -> PlayMode {
|
||||
if v < 0.33 {
|
||||
PlayMode::Poly
|
||||
} else if v < 0.66 {
|
||||
PlayMode::Mono
|
||||
} else {
|
||||
PlayMode::Legato
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use params::ParamId;
|
||||
|
||||
use crate::{
|
||||
envelope::{Dahdsr, Stage},
|
||||
filter::Filter,
|
||||
lfo::Lfo,
|
||||
mod_matrix::{ControlSources, ModMatrix, ModValues},
|
||||
oscillator::{WavetableBank, WavetableOsc, midi_to_freq},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -144,7 +147,17 @@ impl Voice {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn process(&mut self, host_bpm: f32) -> (f32, f32) {
|
||||
fn semitones_to_ratio(semis: f32) -> f32 {
|
||||
(semis * (2.0_f32.ln() / 12.0)).exp()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn process(
|
||||
&mut self,
|
||||
host_bpm: f32,
|
||||
matrix: &ModMatrix,
|
||||
cs: &ControlSources,
|
||||
) -> (f32, f32) {
|
||||
let amp_env = self.tick_mod_sources(host_bpm);
|
||||
|
||||
if self.envs[0].stage == Stage::Idle {
|
||||
@@ -152,16 +165,64 @@ impl Voice {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
|
||||
let freq = self.advance_portamento();
|
||||
let mv: ModValues = matrix.compute(&self.mod_sources, cs);
|
||||
|
||||
let pitch_mod_semis = mv.get(ParamId::Osc1Semitone)
|
||||
+ mv.get(ParamId::Osc2Semitone)
|
||||
+ mv.get(ParamId::Osc3Semitone);
|
||||
let pitch_ratio = Self::semitones_to_ratio(pitch_mod_semis * 24.0);
|
||||
|
||||
let base_freq = self.advance_portamento();
|
||||
let freq = base_freq * pitch_ratio;
|
||||
|
||||
let (mut l, mut r) = (0.0f32, 0.0f32);
|
||||
for osc in &mut self.oscs {
|
||||
|
||||
let wave_mods = [
|
||||
mv.get(ParamId::Osc1WavePos),
|
||||
mv.get(ParamId::Osc2WavePos),
|
||||
mv.get(ParamId::Osc3WavePos),
|
||||
];
|
||||
let gain_mods = [
|
||||
mv.get(ParamId::Osc1Gain),
|
||||
mv.get(ParamId::Osc2Gain),
|
||||
mv.get(ParamId::Osc3Gain),
|
||||
];
|
||||
let pan_mods = [
|
||||
mv.get(ParamId::Osc1Pan),
|
||||
mv.get(ParamId::Osc2Pan),
|
||||
mv.get(ParamId::Osc3Pan),
|
||||
];
|
||||
|
||||
for (i, osc) in self.oscs.iter_mut().enumerate() {
|
||||
let eff_wave = (osc.wave_pos + wave_mods[i]).clamp(0.0, 1.0);
|
||||
let eff_gain = (osc.gain + gain_mods[i]).max(0.0);
|
||||
let eff_pan = (osc.pan + pan_mods[i]).clamp(-1.0, 1.0);
|
||||
|
||||
let saved_wave = osc.wave_pos;
|
||||
let saved_gain = osc.gain;
|
||||
let saved_pan = osc.pan;
|
||||
osc.wave_pos = eff_wave;
|
||||
osc.gain = eff_gain;
|
||||
osc.pan = eff_pan;
|
||||
|
||||
let (ol, or_) = osc.tick(freq, self.sample_rate, 0.0);
|
||||
l += ol;
|
||||
r += or_;
|
||||
}
|
||||
|
||||
osc.wave_pos = saved_wave;
|
||||
osc.gain = saved_gain;
|
||||
osc.pan = saved_pan;
|
||||
}
|
||||
let ms = self.mod_sources;
|
||||
|
||||
let eff_c1 = (self.filter1.cutoff + mv.get(ParamId::Filter1Cutoff)).clamp(0.0, 1.0);
|
||||
let eff_c2 = (self.filter2.cutoff + mv.get(ParamId::Filter2Cutoff)).clamp(0.0, 1.0);
|
||||
|
||||
let saved_c1 = self.filter1.cutoff;
|
||||
let saved_c2 = self.filter2.cutoff;
|
||||
self.filter1.cutoff = eff_c1;
|
||||
self.filter2.cutoff = eff_c2;
|
||||
|
||||
let fm1 = Self::filter_fm(&ms, self.filter1.fm_amount);
|
||||
let fm2 = Self::filter_fm(&ms, self.filter2.fm_amount);
|
||||
|
||||
@@ -179,7 +240,12 @@ impl Voice {
|
||||
((l1 + l2) * 0.5, (r1 + r2) * 0.5)
|
||||
};
|
||||
|
||||
let amp = amp_env * self.velocity;
|
||||
self.filter1.cutoff = saved_c1;
|
||||
self.filter2.cutoff = saved_c2;
|
||||
|
||||
let amp_mod = mv.get(ParamId::MasterVolume);
|
||||
let amp = amp_env * self.velocity * (1.0 + amp_mod).max(0.0);
|
||||
|
||||
(l * amp, r * amp)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user