#![allow(dead_code)]
pub mod am;
pub mod direct;
pub mod fm;
pub mod granular;
pub mod orbital;
pub mod spectral;
pub mod vocal;
pub mod waveguide_mapper;
pub use am::AmMapping;
pub use direct::DirectMapping;
pub use fm::FmMapping;
pub use granular::GranularMapping;
pub use orbital::OrbitalResonance;
pub use spectral::SpectralMapping;
pub use vocal::VocalMapping;
pub use waveguide_mapper::WaveguideMapping;
use crate::config::SonificationConfig;
use crate::synth::OscShape;
#[derive(Clone)]
pub struct AudioParams {
pub freqs: [f32; 4],
pub amps: [f32; 4],
pub filter_cutoff: f32,
pub filter_q: f32,
pub pans: [f32; 4],
pub grain_spawn_rate: f32,
pub grain_base_freq: f32,
pub grain_freq_spread: f32,
pub partials: [f32; 32],
pub partials_base_freq: f32,
pub mode: SonifMode,
pub gain: f32,
pub transpose_semitones: f32,
pub chord_intervals: [f32; 3],
pub voice_levels: [f32; 4],
pub portamento_ms: f32,
pub chaos_level: f32,
pub master_volume: f32,
pub reverb_wet: f32,
pub delay_ms: f32,
pub delay_feedback: f32,
pub fm_carrier_freq: f32,
pub fm_mod_ratio: f32,
pub fm_mod_index: f32,
pub voice_shapes: [OscShape; 4],
pub bit_depth: f32,
pub rate_crush: f32,
pub ks_trigger: bool,
pub ks_freq: f32,
pub ks_volume: f32,
pub chorus_mix: f32,
pub chorus_rate: f32,
pub chorus_depth: f32,
pub waveshaper_drive: f32,
pub waveshaper_mix: f32,
pub adsr_attack_ms: f32,
pub adsr_decay_ms: f32,
pub adsr_sustain: f32,
pub adsr_release_ms: f32,
pub layer_level: f32,
pub layer_pan: f32,
pub layer_id: usize,
pub waveguide_tension: f32,
pub waveguide_damping: f32,
pub waveguide_excite: bool,
pub spectral_freeze_active: bool,
pub spectral_freeze_freqs: [f32; 16],
pub spectral_freeze_amps: [f32; 16],
pub eq_low_db: f32,
pub eq_mid_db: f32,
pub eq_high_db: f32,
pub eq_mid_freq: f32,
pub voice_detune_cents: f32,
pub sub_osc_level: f32,
pub grain_density: f32,
}
impl Default for AudioParams {
fn default() -> Self {
Self {
freqs: [0.0; 4],
amps: [0.0; 4],
filter_cutoff: 0.0,
filter_q: 0.0,
pans: [0.0; 4],
grain_spawn_rate: 0.0,
grain_base_freq: 0.0,
grain_freq_spread: 0.0,
partials: [0.0; 32],
partials_base_freq: 0.0,
mode: SonifMode::Direct,
gain: 0.0,
transpose_semitones: 0.0,
chord_intervals: [0.0; 3],
voice_levels: [1.0, 0.8, 0.6, 0.4],
portamento_ms: 80.0,
chaos_level: 0.0,
master_volume: 0.7,
reverb_wet: 0.4,
delay_ms: 300.0,
delay_feedback: 0.3,
fm_carrier_freq: 220.0,
fm_mod_ratio: 2.0,
fm_mod_index: 1.0,
voice_shapes: [OscShape::Sine; 4],
bit_depth: 16.0,
rate_crush: 0.0,
ks_trigger: false,
ks_freq: 220.0,
ks_volume: 0.5,
chorus_mix: 0.0,
chorus_rate: 0.5,
chorus_depth: 3.0,
waveshaper_drive: 1.0,
waveshaper_mix: 0.0,
adsr_attack_ms: 10.0,
adsr_decay_ms: 200.0,
adsr_sustain: 0.7,
adsr_release_ms: 400.0,
layer_level: 1.0,
layer_pan: 0.0,
layer_id: 0,
waveguide_tension: 0.5,
waveguide_damping: 0.98,
waveguide_excite: false,
spectral_freeze_active: false,
spectral_freeze_freqs: [0.0; 16],
spectral_freeze_amps: [0.0; 16],
eq_low_db: 0.0,
eq_mid_db: 0.0,
eq_high_db: 0.0,
eq_mid_freq: 1000.0,
voice_detune_cents: 0.0,
sub_osc_level: 0.0,
grain_density: 1.0,
}
}
}
pub fn chord_intervals_for(mode: &str) -> [f32; 3] {
match mode {
"major" => [4.0, 7.0, 0.0],
"minor" => [3.0, 7.0, 0.0],
"power" => [7.0, 12.0, 0.0],
"sus2" => [2.0, 7.0, 0.0],
"octave" => [12.0, 24.0, 0.0],
"dom7" => [4.0, 7.0, 10.0],
"min7" => [3.0, 7.0, 10.0],
"maj7" => [4.0, 7.0, 11.0],
"aug" => [4.0, 8.0, 0.0],
"dim" => [3.0, 6.0, 0.0],
"sus4" => [5.0, 7.0, 0.0],
_ => [0.0, 0.0, 0.0],
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum SonifMode {
#[default]
Direct,
Orbital,
Granular,
Spectral,
FM,
Vocal,
Waveguide,
AM,
Resonator,
}
impl std::fmt::Display for SonifMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Direct => write!(f, "Direct"),
Self::Orbital => write!(f, "Orbital"),
Self::Granular => write!(f, "Granular"),
Self::Spectral => write!(f, "Spectral"),
Self::FM => write!(f, "FM"),
Self::Vocal => write!(f, "Vocal"),
Self::Waveguide => write!(f, "Waveguide"),
Self::AM => write!(f, "AM"),
Self::Resonator => write!(f, "Resonator"),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum Scale {
#[default]
Pentatonic,
Chromatic,
JustIntonation,
Microtonal,
Edo19, Edo31, Edo24, WholeTone, Phrygian, Lydian, HarmonicSeries, Hirajoshi, Blues, Dorian, Mixolydian, HungarianMinor, Locrian, Octatonic, NaturalMinor, HarmonicMinor, }
fn scale_intervals(scale: Scale) -> &'static [f32] {
match scale {
Scale::Pentatonic => &[0.0, 2.0, 4.0, 7.0, 9.0],
Scale::Chromatic => &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0],
Scale::JustIntonation => &[0.0, 2.039, 3.863, 4.980, 7.020, 8.841, 10.884], Scale::Microtonal => &[
0.0, 0.75, 1.5, 2.25, 3.0, 3.75, 4.5, 5.25, 6.0, 6.75, 7.5, 8.25, 9.0,
],
Scale::WholeTone => &[0.0, 2.0, 4.0, 6.0, 8.0, 10.0],
Scale::Phrygian => &[0.0, 1.0, 3.0, 5.0, 7.0, 8.0, 10.0],
Scale::Lydian => &[0.0, 2.0, 4.0, 6.0, 7.0, 9.0, 11.0],
Scale::Hirajoshi => &[0.0, 2.0, 3.0, 7.0, 8.0],
Scale::Blues => &[0.0, 3.0, 5.0, 6.0, 7.0, 10.0],
Scale::Dorian => &[0.0, 2.0, 3.0, 5.0, 7.0, 9.0, 10.0],
Scale::Mixolydian => &[0.0, 2.0, 4.0, 5.0, 7.0, 9.0, 10.0],
Scale::HungarianMinor => &[0.0, 2.0, 3.0, 6.0, 7.0, 8.0, 11.0],
Scale::Locrian => &[0.0, 1.0, 3.0, 5.0, 6.0, 8.0, 10.0],
Scale::Octatonic => &[0.0, 2.0, 3.0, 5.0, 6.0, 8.0, 9.0, 11.0],
Scale::NaturalMinor => &[0.0, 2.0, 3.0, 5.0, 7.0, 8.0, 10.0],
Scale::HarmonicMinor => &[0.0, 2.0, 3.0, 5.0, 7.0, 8.0, 11.0],
Scale::Edo19 | Scale::Edo31 | Scale::Edo24 | Scale::HarmonicSeries => &[0.0],
}
}
fn scale_intervals_owned(scale: Scale) -> Vec<f32> {
match scale {
Scale::Edo19 => (0..19).map(|i| i as f32 * 12.0 / 19.0).collect(),
Scale::Edo31 => (0..31).map(|i| i as f32 * 12.0 / 31.0).collect(),
Scale::Edo24 => (0..24).map(|i| i as f32 * 0.5).collect(),
other => scale_intervals(other).to_vec(),
}
}
impl Scale {
pub fn intervals(self) -> Vec<f32> {
scale_intervals_owned(self)
}
}
const HARMONIC_SERIES: &[f32] = &[
110.0, 220.0, 330.0, 440.0, 550.0, 660.0, 770.0, 880.0, 990.0, 1100.0, 1320.0, 1540.0,
];
pub fn quantize_to_scale(t: f32, base_hz: f32, octave_range: f32, scale: Scale) -> f32 {
if scale == Scale::HarmonicSeries {
let t = t.clamp(0.0, 1.0);
let target_hz = base_hz * 2.0f32.powf(t * octave_range);
return HARMONIC_SERIES
.iter()
.copied()
.min_by(|a, b| {
(a - target_hz)
.abs()
.partial_cmp(&(b - target_hz).abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(110.0);
}
let intervals = scale_intervals_owned(scale);
let n = intervals.len() as f32;
let total_steps = octave_range * n;
let step_float = ((t.clamp(0.0, 1.0) * total_steps) as usize).min(total_steps as usize);
let octave = step_float / intervals.len();
let degree = step_float % intervals.len();
let semitones = octave as f32 * 12.0 + intervals[degree];
base_hz * 2.0f32.powf(semitones / 12.0)
}
pub trait Sonification: Send {
fn map(&mut self, state: &[f64], speed: f64, config: &SonificationConfig) -> AudioParams;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chord_intervals_major() {
let iv = chord_intervals_for("major");
assert!((iv[0] - 4.0).abs() < 1e-6, "major: expected 4 semitones, got {}", iv[0]);
assert!((iv[1] - 7.0).abs() < 1e-6, "major: expected 7 semitones, got {}", iv[1]);
}
#[test]
fn chord_intervals_dom7_three_voices() {
let iv = chord_intervals_for("dom7");
assert!((iv[0] - 4.0).abs() < 1e-6);
assert!((iv[1] - 7.0).abs() < 1e-6);
assert!((iv[2] - 10.0).abs() < 1e-6, "dom7 third voice: expected 10, got {}", iv[2]);
}
#[test]
fn chord_intervals_unknown_returns_zeros() {
let iv = chord_intervals_for("definitely_not_a_chord");
assert_eq!(iv, [0.0, 0.0, 0.0]);
}
#[test]
fn chord_intervals_all_named_modes_return_finite() {
for mode in &["major", "minor", "power", "sus2", "octave", "dom7", "min7", "maj7", "aug", "dim", "sus4"] {
let iv = chord_intervals_for(mode);
for (i, &v) in iv.iter().enumerate() {
assert!(v.is_finite(), "chord {} interval[{}] non-finite", mode, i);
}
}
}
#[test]
fn quantize_whole_tone_returns_finite_in_range() {
let base = 220.0f32;
let oct = 2.0f32;
for i in 0..=20 {
let t = i as f32 / 20.0;
let f = quantize_to_scale(t, base, oct, Scale::WholeTone);
assert!(f.is_finite() && f > 0.0, "WholeTone: non-positive at t={}: {}", t, f);
assert!(f >= base * 0.99, "WholeTone: below base at t={}: {}", t, f);
}
}
#[test]
fn quantize_phrygian_returns_finite() {
let base = 220.0f32;
for i in 0..=10 {
let t = i as f32 / 10.0;
let f = quantize_to_scale(t, base, 2.0, Scale::Phrygian);
assert!(f.is_finite() && f > 0.0, "Phrygian: invalid at t={}: {}", t, f);
}
}
#[test]
fn quantize_lydian_returns_finite() {
let base = 440.0f32;
for i in 0..=10 {
let t = i as f32 / 10.0;
let f = quantize_to_scale(t, base, 2.0, Scale::Lydian);
assert!(f.is_finite() && f > 0.0, "Lydian: invalid at t={}: {}", t, f);
}
}
#[test]
fn quantize_harmonic_series_snaps_to_harmonics() {
let known: &[f32] = &[110.0, 220.0, 330.0, 440.0, 550.0, 660.0, 770.0, 880.0, 990.0, 1100.0, 1320.0, 1540.0];
for i in 0..=10 {
let t = i as f32 / 10.0;
let f = quantize_to_scale(t, 110.0, 4.0, Scale::HarmonicSeries);
assert!(
known.iter().any(|&h| (f - h).abs() < 1.0),
"HarmonicSeries: {} not in known harmonics at t={}",
f,
t
);
}
}
#[test]
fn quantize_edo19_returns_monotone_frequencies() {
let base = 220.0f32;
let mut prev = 0.0f32;
for i in 0..=20 {
let t = i as f32 / 20.0;
let f = quantize_to_scale(t, base, 2.0, Scale::Edo19);
assert!(f >= prev, "EDO19 not monotone at t={}: {} < {}", t, f, prev);
prev = f;
}
}
#[test]
fn quantize_edo31_frequencies_in_range() {
let base = 220.0f32;
for i in 0..=20 {
let t = i as f32 / 20.0;
let f = quantize_to_scale(t, base, 2.0, Scale::Edo31);
assert!(f >= base * 0.99 && f.is_finite(), "EDO31 out of range at t={}: {}", t, f);
}
}
#[test]
fn quantize_just_intonation_returns_valid_frequencies() {
let base = 220.0f32;
for i in 0..=10 {
let t = i as f32 / 10.0;
let f = quantize_to_scale(t, base, 2.0, Scale::JustIntonation);
assert!(f >= base * 0.99 && f.is_finite(), "JustIntonation at t={}: {}", t, f);
}
}
#[test]
fn test_hirajoshi_scale_produces_finite_output() {
let base = 220.0_f32;
let oct = 2.0_f32;
for &t in &[0.0_f32, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
let f = quantize_to_scale(t, base, oct, Scale::Hirajoshi);
assert!(f.is_finite() && f > 0.0, "Hirajoshi at t={}: {}", t, f);
}
}
#[test]
fn test_hirajoshi_scale_interval_count() {
let intervals = &[0.0_f32, 2.0, 3.0, 7.0, 8.0];
assert_eq!(intervals.len(), 5, "Hirajoshi should have 5 notes per octave");
}
#[test]
fn test_blues_scale_produces_finite_output() {
let base = 220.0_f32;
let oct = 2.0_f32;
for &t in &[0.0_f32, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
let f = quantize_to_scale(t, base, oct, Scale::Blues);
assert!(f.is_finite() && f > 0.0, "Blues at t={}: {}", t, f);
}
}
#[test]
fn test_blues_scale_interval_count() {
let intervals = &[0.0_f32, 3.0, 5.0, 6.0, 7.0, 10.0];
assert_eq!(intervals.len(), 6, "Blues should have 6 notes per octave");
}
#[test]
fn test_hirajoshi_different_from_pentatonic() {
let base = 440.0_f32;
let oct = 2.0_f32;
let t = 0.3_f32;
let h = quantize_to_scale(t, base, oct, Scale::Hirajoshi);
let p = quantize_to_scale(t, base, oct, Scale::Pentatonic);
assert!(h.is_finite() && h > 0.0);
assert!(p.is_finite() && p > 0.0);
}
#[test]
fn test_dorian_scale_produces_finite_output() {
let base = 220.0_f32;
for &t in &[0.0_f32, 0.2, 0.5, 0.8, 1.0] {
let f = quantize_to_scale(t, base, 2.0, Scale::Dorian);
assert!(f.is_finite() && f > 0.0, "Dorian at t={}: {}", t, f);
}
}
#[test]
fn test_mixolydian_scale_produces_finite_output() {
let base = 220.0_f32;
for &t in &[0.0_f32, 0.2, 0.5, 0.8, 1.0] {
let f = quantize_to_scale(t, base, 2.0, Scale::Mixolydian);
assert!(f.is_finite() && f > 0.0, "Mixolydian at t={}: {}", t, f);
}
}
#[test]
fn test_hungarian_minor_scale_produces_finite_output() {
let base = 220.0_f32;
for &t in &[0.0_f32, 0.2, 0.5, 0.8, 1.0] {
let f = quantize_to_scale(t, base, 2.0, Scale::HungarianMinor);
assert!(f.is_finite() && f > 0.0, "HungarianMinor at t={}: {}", t, f);
}
}
#[test]
fn test_modal_scales_all_seven_notes() {
let dorian = &[0.0_f32, 2.0, 3.0, 5.0, 7.0, 9.0, 10.0];
let mixo = &[0.0_f32, 2.0, 4.0, 5.0, 7.0, 9.0, 10.0];
let hung = &[0.0_f32, 2.0, 3.0, 6.0, 7.0, 8.0, 11.0];
assert_eq!(dorian.len(), 7, "Dorian should have 7 notes");
assert_eq!(mixo.len(), 7, "Mixolydian should have 7 notes");
assert_eq!(hung.len(), 7, "HungarianMinor should have 7 notes");
}
#[test]
fn test_locrian_scale_produces_finite_output() {
let base = 220.0_f32;
for &t in &[0.0_f32, 0.2, 0.5, 0.8, 1.0] {
let f = quantize_to_scale(t, base, 2.0, Scale::Locrian);
assert!(f.is_finite() && f > 0.0, "Locrian at t={}: {}", t, f);
}
}
#[test]
fn test_locrian_scale_interval_count() {
let intervals = Scale::Locrian.intervals();
assert_eq!(intervals.len(), 7, "Locrian should have 7 notes per octave");
}
#[test]
fn test_locrian_scale_intervals_correct() {
let intervals = Scale::Locrian.intervals();
let expected = [0.0_f32, 1.0, 3.0, 5.0, 6.0, 8.0, 10.0];
assert_eq!(intervals.len(), expected.len());
for (i, (&got, &exp)) in intervals.iter().zip(expected.iter()).enumerate() {
assert!((got - exp).abs() < 1e-6, "Locrian interval[{}]: expected {}, got {}", i, exp, got);
}
}
#[test]
fn test_octatonic_scale_produces_finite_output() {
let base = 220.0_f32;
for &t in &[0.0_f32, 0.2, 0.5, 0.8, 1.0] {
let f = quantize_to_scale(t, base, 2.0, Scale::Octatonic);
assert!(f.is_finite() && f > 0.0, "Octatonic at t={}: {}", t, f);
}
}
#[test]
fn test_octatonic_scale_interval_count() {
let intervals = Scale::Octatonic.intervals();
assert_eq!(intervals.len(), 8, "Octatonic should have 8 notes per octave");
}
#[test]
fn test_octatonic_scale_intervals_correct() {
let intervals = Scale::Octatonic.intervals();
let expected = [0.0_f32, 2.0, 3.0, 5.0, 6.0, 8.0, 9.0, 11.0];
assert_eq!(intervals.len(), expected.len());
for (i, (&got, &exp)) in intervals.iter().zip(expected.iter()).enumerate() {
assert!((got - exp).abs() < 1e-6, "Octatonic interval[{}]: expected {}, got {}", i, exp, got);
}
}
#[test]
fn test_locrian_different_from_chromatic() {
let base = 220.0_f32;
let t = 0.3_f32;
let loc = quantize_to_scale(t, base, 2.0, Scale::Locrian);
let chrom = quantize_to_scale(t, base, 2.0, Scale::Chromatic);
assert!(loc.is_finite() && loc > 0.0);
assert!(chrom.is_finite() && chrom > 0.0);
let loc_intervals = Scale::Locrian.intervals();
let chrom_intervals = Scale::Chromatic.intervals();
assert_ne!(loc_intervals.len(), chrom_intervals.len(), "Locrian and Chromatic should have different note counts");
}
#[test]
fn test_octatonic_symmetric_interval_structure() {
let intervals = Scale::Octatonic.intervals();
assert_eq!(intervals.len(), 8);
let steps: Vec<f32> = intervals.windows(2).map(|w| w[1] - w[0]).collect();
for (i, &step) in steps.iter().enumerate() {
let expected = if i % 2 == 0 { 2.0_f32 } else { 1.0_f32 };
assert!((step - expected).abs() < 1e-6, "Octatonic step[{}]: expected {}, got {}", i, expected, step);
}
}
#[test]
fn test_natural_minor_scale_produces_finite_output() {
let base = 220.0_f32;
for &t in &[0.0_f32, 0.2, 0.5, 0.8, 1.0] {
let f = quantize_to_scale(t, base, 2.0, Scale::NaturalMinor);
assert!(f.is_finite() && f > 0.0, "NaturalMinor at t={}: {}", t, f);
}
}
#[test]
fn test_natural_minor_scale_interval_count() {
let intervals = Scale::NaturalMinor.intervals();
assert_eq!(intervals.len(), 7, "NaturalMinor should have 7 notes per octave");
}
#[test]
fn test_natural_minor_scale_intervals_correct() {
let intervals = Scale::NaturalMinor.intervals();
let expected = [0.0_f32, 2.0, 3.0, 5.0, 7.0, 8.0, 10.0];
assert_eq!(intervals.len(), expected.len());
for (i, (&got, &exp)) in intervals.iter().zip(expected.iter()).enumerate() {
assert!((got - exp).abs() < 1e-6, "NaturalMinor interval[{}]: expected {}, got {}", i, exp, got);
}
}
#[test]
fn test_harmonic_minor_scale_produces_finite_output() {
let base = 220.0_f32;
for &t in &[0.0_f32, 0.2, 0.5, 0.8, 1.0] {
let f = quantize_to_scale(t, base, 2.0, Scale::HarmonicMinor);
assert!(f.is_finite() && f > 0.0, "HarmonicMinor at t={}: {}", t, f);
}
}
#[test]
fn test_harmonic_minor_scale_interval_count() {
let intervals = Scale::HarmonicMinor.intervals();
assert_eq!(intervals.len(), 7, "HarmonicMinor should have 7 notes per octave");
}
#[test]
fn test_harmonic_minor_scale_intervals_correct() {
let intervals = Scale::HarmonicMinor.intervals();
let expected = [0.0_f32, 2.0, 3.0, 5.0, 7.0, 8.0, 11.0];
assert_eq!(intervals.len(), expected.len());
for (i, (&got, &exp)) in intervals.iter().zip(expected.iter()).enumerate() {
assert!((got - exp).abs() < 1e-6, "HarmonicMinor interval[{}]: expected {}, got {}", i, exp, got);
}
}
#[test]
fn test_harmonic_minor_differs_from_natural_minor() {
let nat = Scale::NaturalMinor.intervals();
let harm = Scale::HarmonicMinor.intervals();
assert_eq!(nat.len(), harm.len(), "Both should be 7-note scales");
assert!((nat[6] - harm[6]).abs() > 0.5, "7th degree should differ between natural and harmonic minor");
}
}