use serde::{Deserialize, Serialize};
pub const NUM_SOURCES: usize = 8;
pub const NUM_DESTINATIONS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ModSource {
Lfo1,
Lfo2,
AmpEnvelope,
FilterEnvelope,
Velocity,
ModWheel,
Aftertouch,
PitchBend,
}
impl ModSource {
#[inline]
fn index(self) -> usize {
match self {
Self::Lfo1 => 0,
Self::Lfo2 => 1,
Self::AmpEnvelope => 2,
Self::FilterEnvelope => 3,
Self::Velocity => 4,
Self::ModWheel => 5,
Self::Aftertouch => 6,
Self::PitchBend => 7,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ModDestination {
Pitch,
FilterCutoff,
FilterResonance,
Amplitude,
Pan,
PulseWidth,
FmIndex,
LfoRate,
}
impl ModDestination {
#[inline]
fn index(self) -> usize {
match self {
Self::Pitch => 0,
Self::FilterCutoff => 1,
Self::FilterResonance => 2,
Self::Amplitude => 3,
Self::Pan => 4,
Self::PulseWidth => 5,
Self::FmIndex => 6,
Self::LfoRate => 7,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ModRouting {
pub source: ModSource,
pub destination: ModDestination,
pub depth: f32,
pub enabled: bool,
}
impl ModRouting {
#[must_use]
pub fn new(source: ModSource, destination: ModDestination, depth: f32) -> Self {
Self {
source,
destination,
depth: depth.clamp(-1.0, 1.0),
enabled: true,
}
}
}
pub const MAX_ROUTINGS: usize = 16;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModMatrix {
routings: Vec<ModRouting>,
#[serde(skip)]
source_values: [f32; NUM_SOURCES],
#[serde(skip)]
destination_values: [f32; NUM_DESTINATIONS],
}
impl ModMatrix {
#[must_use]
pub fn new() -> Self {
Self {
routings: Vec::new(),
source_values: [0.0; 8],
destination_values: [0.0; 8],
}
}
pub fn add_routing(&mut self, routing: ModRouting) -> bool {
if self.routings.len() >= MAX_ROUTINGS {
return false;
}
self.routings.push(routing);
true
}
pub fn remove_routing(&mut self, index: usize) {
if index < self.routings.len() {
self.routings.remove(index);
}
}
pub fn clear(&mut self) {
self.routings.clear();
}
pub fn set_source(&mut self, source: ModSource, value: f32) {
self.source_values[source.index()] = value;
}
pub fn compute(&mut self) {
self.destination_values = [0.0; 8];
for routing in &self.routings {
if !routing.enabled {
continue;
}
let src_val = self.source_values[routing.source.index()];
let mod_amount = src_val * routing.depth;
self.destination_values[routing.destination.index()] += mod_amount;
}
}
#[inline]
#[must_use]
pub fn get_destination(&self, dest: ModDestination) -> f32 {
self.destination_values[dest.index()]
}
#[must_use]
pub fn num_routings(&self) -> usize {
self.routings.len()
}
#[must_use]
pub fn routings(&self) -> &[ModRouting] {
&self.routings
}
pub fn routing_mut(&mut self, index: usize) -> Option<&mut ModRouting> {
self.routings.get_mut(index)
}
}
impl Default for ModMatrix {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_routing() {
let mut m = ModMatrix::new();
m.add_routing(ModRouting::new(ModSource::Lfo1, ModDestination::Pitch, 0.5));
m.set_source(ModSource::Lfo1, 0.8);
m.compute();
let val = m.get_destination(ModDestination::Pitch);
assert!((val - 0.4).abs() < 0.001, "0.8 * 0.5 = 0.4, got {val}");
}
#[test]
fn test_multiple_sources_same_dest() {
let mut m = ModMatrix::new();
m.add_routing(ModRouting::new(
ModSource::Lfo1,
ModDestination::FilterCutoff,
0.5,
));
m.add_routing(ModRouting::new(
ModSource::FilterEnvelope,
ModDestination::FilterCutoff,
1.0,
));
m.set_source(ModSource::Lfo1, 0.6);
m.set_source(ModSource::FilterEnvelope, 0.4);
m.compute();
let val = m.get_destination(ModDestination::FilterCutoff);
assert!((val - 0.7).abs() < 0.001, "expected 0.7, got {val}");
}
#[test]
fn test_disabled_routing() {
let mut m = ModMatrix::new();
let mut r = ModRouting::new(ModSource::Lfo1, ModDestination::Pitch, 1.0);
r.enabled = false;
m.add_routing(r);
m.set_source(ModSource::Lfo1, 1.0);
m.compute();
assert!(
m.get_destination(ModDestination::Pitch).abs() < f32::EPSILON,
"disabled routing should not contribute"
);
}
#[test]
fn test_max_routings() {
let mut m = ModMatrix::new();
for _ in 0..MAX_ROUTINGS {
assert!(m.add_routing(ModRouting::new(ModSource::Lfo1, ModDestination::Pitch, 0.1)));
}
assert!(!m.add_routing(ModRouting::new(ModSource::Lfo1, ModDestination::Pitch, 0.1)));
}
#[test]
fn test_serde_roundtrip() {
let mut m = ModMatrix::new();
m.add_routing(ModRouting::new(
ModSource::Velocity,
ModDestination::Amplitude,
0.8,
));
let json = serde_json::to_string(&m).unwrap();
let back: ModMatrix = serde_json::from_str(&json).unwrap();
assert_eq!(m.num_routings(), back.num_routings());
}
}