mod matrix

This commit is contained in:
2026-04-20 18:54:49 +03:00
parent 67b37dedc6
commit 17f3be1676
3 changed files with 309 additions and 11 deletions

View File

@@ -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 { pub enum ModSource {
Env(u8), Env(u8),
Lfo(u8), Lfo(u8),
@@ -11,7 +13,30 @@ pub enum ModSource {
Macro(u8), 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 struct ModSlot {
pub active: bool, pub active: bool,
pub source: ModSource, 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 struct ModMatrix {
pub slots: [ModSlot; 64], pub slots: [ModSlot; 64],
} }
impl Default for ModMatrix {
fn default() -> Self {
Self::new()
}
}
impl ModMatrix { impl ModMatrix {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
slots: [ModSlot::default(); 64], slots: [ModSlot::default(); 64],
} }
} }
pub fn apply(&self, _params: &params::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)
}
} }

View File

@@ -5,6 +5,7 @@ use params::{ParamId, ParamStore};
use crate::{ use crate::{
effects::{DistType, EffectsChain}, effects::{DistType, EffectsChain},
filter::FilterKind, filter::FilterKind,
mod_matrix::{ControlSources, ModMatrix},
oscillator::WavetableBank, oscillator::WavetableBank,
voice::{PlayMode, PortamentoMode, Voice}, voice::{PlayMode, PortamentoMode, Voice},
}; };
@@ -18,6 +19,8 @@ pub struct Synth {
sample_rate: f32, sample_rate: f32,
round_robin: usize, round_robin: usize,
play_mode: PlayMode, play_mode: PlayMode,
pub mod_matrix: ModMatrix,
control_sources: ControlSources,
} }
impl Synth { impl Synth {
@@ -29,6 +32,8 @@ impl Synth {
sample_rate: sr, sample_rate: sr,
round_robin: 0, round_robin: 0,
play_mode: PlayMode::Poly, 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] #[inline]
pub fn process(&mut self, host_bpm: f32) -> (f32, f32) { pub fn process(&mut self, host_bpm: f32) -> (f32, f32) {
self.sync_params(); self.sync_params();
self.control_sources.sync_macros(&self.params);
let mut l = 0.0f32; let mut l = 0.0f32;
let mut r = 0.0f32; let mut r = 0.0f32;
for v in &mut self.voices { for v in &mut self.voices {
if v.active { 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; l += vl;
r += vr; r += vr;
} }
@@ -105,6 +125,8 @@ impl Synth {
PortamentoMode::Exponential PortamentoMode::Exponential
}; };
self.play_mode = play_mode_from(p.get(ParamId::Polyphony));
for v in &mut self.voices { for v in &mut self.voices {
v.filter1.cutoff = f1_cutoff; v.filter1.cutoff = f1_cutoff;
v.filter1.resonance = f1_res; v.filter1.resonance = f1_res;
@@ -120,6 +142,21 @@ impl Synth {
v.portamento_time = port_time; v.portamento_time = port_time;
v.portamento_mode = port_mode; 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; let fx = &mut self.fx;
@@ -144,7 +181,7 @@ impl Synth {
fx.delay.mix = p.get(ParamId::DelayMix); fx.delay.mix = p.get(ParamId::DelayMix);
let eq = &mut fx.eq; 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_f = p.get(ParamId::EqMidFreq) * 7900.0 + 100.0;
let new_mid_g = p.get(ParamId::EqMidGain) * 24.0 - 12.0; let new_mid_g = p.get(ParamId::EqMidGain) * 24.0 - 12.0;
let new_high = p.get(ParamId::EqHighGain) * 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 { fn filter_kind_from(v: u32) -> FilterKind {
match v { match v {
0 => FilterKind::Ladder, 0 => FilterKind::Ladder,
@@ -191,3 +291,13 @@ fn dist_type_from(v: u32) -> DistType {
_ => DistType::SoftClip, _ => 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
}
}

View File

@@ -1,7 +1,10 @@
use params::ParamId;
use crate::{ use crate::{
envelope::{Dahdsr, Stage}, envelope::{Dahdsr, Stage},
filter::Filter, filter::Filter,
lfo::Lfo, lfo::Lfo,
mod_matrix::{ControlSources, ModMatrix, ModValues},
oscillator::{WavetableBank, WavetableOsc, midi_to_freq}, oscillator::{WavetableBank, WavetableOsc, midi_to_freq},
}; };
use std::sync::Arc; use std::sync::Arc;
@@ -144,7 +147,17 @@ impl Voice {
} }
#[inline] #[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); let amp_env = self.tick_mod_sources(host_bpm);
if self.envs[0].stage == Stage::Idle { if self.envs[0].stage == Stage::Idle {
@@ -152,16 +165,64 @@ impl Voice {
return (0.0, 0.0); 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); 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); let (ol, or_) = osc.tick(freq, self.sample_rate, 0.0);
l += ol; l += ol;
r += or_; r += or_;
}
osc.wave_pos = saved_wave;
osc.gain = saved_gain;
osc.pan = saved_pan;
}
let ms = self.mod_sources; 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 fm1 = Self::filter_fm(&ms, self.filter1.fm_amount);
let fm2 = Self::filter_fm(&ms, self.filter2.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) ((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) (l * amp, r * amp)
} }
} }