#![allow(dead_code)]
use crate::config::*;
use crate::patches::load_preset;
#[derive(Debug, Clone, PartialEq)]
pub enum TransitionType {
Morph,
Snap,
Fade,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum MorphCurve {
#[default]
Linear,
Smooth,
Exponential,
}
impl MorphCurve {
pub fn apply(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
MorphCurve::Linear => t,
MorphCurve::Smooth => t * t * (3.0 - 2.0 * t),
MorphCurve::Exponential => {
let k = 10.0_f32;
let sig = |x: f32| 1.0_f32 / (1.0 + (-k * (x - 0.5)).exp());
let lo = sig(0.0_f32);
let hi = sig(1.0_f32);
(sig(t) - lo) / (hi - lo)
}
}
}
pub fn from_str(s: &str) -> Self {
match s {
"smooth" => MorphCurve::Smooth,
"exponential" => MorphCurve::Exponential,
_ => MorphCurve::Linear,
}
}
pub fn as_str(&self) -> &'static str {
match self {
MorphCurve::Linear => "linear",
MorphCurve::Smooth => "smooth",
MorphCurve::Exponential => "exponential",
}
}
}
impl Default for TransitionType {
fn default() -> Self {
Self::Morph
}
}
#[derive(Clone)]
pub struct Scene {
pub name: String,
pub config: Config,
pub hold_secs: f32,
pub morph_secs: f32,
pub active: bool,
pub transition_prob: f32,
pub transition_type: TransitionType,
pub morph_curve: MorphCurve,
}
impl Scene {
pub fn empty(n: usize) -> Self {
Self {
name: format!("Scene {}", n + 1),
config: Config::default(),
hold_secs: 30.0,
morph_secs: 8.0,
active: false,
transition_prob: 1.0,
transition_type: TransitionType::default(),
morph_curve: MorphCurve::default(),
}
}
}
pub fn lerp_config(a: &Config, b: &Config, t: f32) -> Config {
let t = t.clamp(0.0, 1.0);
let lf64 = |a: f64, b: f64| -> f64 { a + (b - a) * t as f64 };
let lf32 = |a: f32, b: f32| -> f32 { a + (b - a) * t };
let ls = |a: &str, b: &str| -> String {
if t < 0.5 {
a.to_string()
} else {
b.to_string()
}
};
Config {
system: SystemConfig {
name: ls(&a.system.name, &b.system.name),
dt: lf64(a.system.dt, b.system.dt),
speed: lf64(a.system.speed, b.system.speed),
},
sonification: SonificationConfig {
mode: ls(&a.sonification.mode, &b.sonification.mode),
scale: ls(&a.sonification.scale, &b.sonification.scale),
base_frequency: lf64(a.sonification.base_frequency, b.sonification.base_frequency),
octave_range: lf64(a.sonification.octave_range, b.sonification.octave_range),
chord_mode: ls(&a.sonification.chord_mode, &b.sonification.chord_mode),
transpose_semitones: lf32(
a.sonification.transpose_semitones,
b.sonification.transpose_semitones,
),
portamento_ms: lf32(a.sonification.portamento_ms, b.sonification.portamento_ms),
voice_levels: std::array::from_fn(|i| {
lf32(
a.sonification.voice_levels[i],
b.sonification.voice_levels[i],
)
}),
voice_shapes: if t < 0.5 {
a.sonification.voice_shapes.clone()
} else {
b.sonification.voice_shapes.clone()
},
},
audio: AudioConfig {
sample_rate: a.audio.sample_rate,
buffer_size: a.audio.buffer_size,
reverb_wet: lf32(a.audio.reverb_wet, b.audio.reverb_wet),
delay_ms: lf32(a.audio.delay_ms, b.audio.delay_ms),
delay_feedback: lf32(a.audio.delay_feedback, b.audio.delay_feedback),
master_volume: lf32(a.audio.master_volume, b.audio.master_volume).max(0.45),
bit_depth: lf32(a.audio.bit_depth, b.audio.bit_depth),
rate_crush: lf32(a.audio.rate_crush, b.audio.rate_crush),
chorus_mix: lf32(a.audio.chorus_mix, b.audio.chorus_mix),
chorus_rate: lf32(a.audio.chorus_rate, b.audio.chorus_rate),
chorus_depth: lf32(a.audio.chorus_depth, b.audio.chorus_depth),
waveshaper_drive: lf32(a.audio.waveshaper_drive, b.audio.waveshaper_drive),
waveshaper_mix: lf32(a.audio.waveshaper_mix, b.audio.waveshaper_mix),
..a.audio.clone()
},
lorenz: LorenzConfig {
sigma: lf64(a.lorenz.sigma, b.lorenz.sigma),
rho: lf64(a.lorenz.rho, b.lorenz.rho),
beta: lf64(a.lorenz.beta, b.lorenz.beta),
},
rossler: RosslerConfig {
a: lf64(a.rossler.a, b.rossler.a),
b: lf64(a.rossler.b, b.rossler.b),
c: lf64(a.rossler.c, b.rossler.c),
},
double_pendulum: DoublePendulumConfig {
m1: lf64(a.double_pendulum.m1, b.double_pendulum.m1),
m2: lf64(a.double_pendulum.m2, b.double_pendulum.m2),
l1: lf64(a.double_pendulum.l1, b.double_pendulum.l1),
l2: lf64(a.double_pendulum.l2, b.double_pendulum.l2),
},
geodesic_torus: GeodesicTorusConfig {
big_r: lf64(a.geodesic_torus.big_r, b.geodesic_torus.big_r),
r: lf64(a.geodesic_torus.r, b.geodesic_torus.r),
},
kuramoto: KuramotoConfig {
n_oscillators: a.kuramoto.n_oscillators,
coupling: lf64(a.kuramoto.coupling, b.kuramoto.coupling),
},
duffing: DuffingConfig {
delta: lf64(a.duffing.delta, b.duffing.delta),
alpha: lf64(a.duffing.alpha, b.duffing.alpha),
beta: lf64(a.duffing.beta, b.duffing.beta),
gamma: lf64(a.duffing.gamma, b.duffing.gamma),
omega: lf64(a.duffing.omega, b.duffing.omega),
},
van_der_pol: VanDerPolConfig {
mu: lf64(a.van_der_pol.mu, b.van_der_pol.mu),
},
halvorsen: HalvorsenConfig {
a: lf64(a.halvorsen.a, b.halvorsen.a),
},
aizawa: AizawaConfig {
a: lf64(a.aizawa.a, b.aizawa.a),
b: lf64(a.aizawa.b, b.aizawa.b),
c: lf64(a.aizawa.c, b.aizawa.c),
d: lf64(a.aizawa.d, b.aizawa.d),
e: lf64(a.aizawa.e, b.aizawa.e),
f: lf64(a.aizawa.f, b.aizawa.f),
},
chua: ChuaConfig {
alpha: lf64(a.chua.alpha, b.chua.alpha),
beta: lf64(a.chua.beta, b.chua.beta),
m0: lf64(a.chua.m0, b.chua.m0),
m1: lf64(a.chua.m1, b.chua.m1),
},
hindmarsh_rose: HindmarshRoseConfig {
current_i: lf64(a.hindmarsh_rose.current_i, b.hindmarsh_rose.current_i),
r: lf64(a.hindmarsh_rose.r, b.hindmarsh_rose.r),
},
coupled_map_lattice: CmlConfig {
r: lf64(a.coupled_map_lattice.r, b.coupled_map_lattice.r),
eps: lf64(a.coupled_map_lattice.eps, b.coupled_map_lattice.eps),
},
mackey_glass: MackeyGlassConfig {
beta: lf64(a.mackey_glass.beta, b.mackey_glass.beta),
gamma: lf64(a.mackey_glass.gamma, b.mackey_glass.gamma),
tau: lf64(a.mackey_glass.tau, b.mackey_glass.tau),
n: lf64(a.mackey_glass.n, b.mackey_glass.n),
},
nose_hoover: NoseHooverConfig {
a: lf64(a.nose_hoover.a, b.nose_hoover.a),
},
henon_map: HenonMapConfig {
a: lf64(a.henon_map.a, b.henon_map.a),
b: lf64(a.henon_map.b, b.henon_map.b),
},
lorenz96: Lorenz96Config {
f: lf64(a.lorenz96.f, b.lorenz96.f),
},
thomas: ThomasConfig {
b: lf64(a.thomas.b, b.thomas.b),
},
dadras: DadrasConfig {
a: lf64(a.dadras.a, b.dadras.a),
b: lf64(a.dadras.b, b.dadras.b),
c: lf64(a.dadras.c, b.dadras.c),
d: lf64(a.dadras.d, b.dadras.d),
e: lf64(a.dadras.e, b.dadras.e),
},
rucklidge: RucklidgeConfig {
kappa: lf64(a.rucklidge.kappa, b.rucklidge.kappa),
lambda: lf64(a.rucklidge.lambda, b.rucklidge.lambda),
},
chen: ChenConfig {
a: lf64(a.chen.a, b.chen.a),
b: lf64(a.chen.b, b.chen.b),
c: lf64(a.chen.c, b.chen.c),
},
burke_shaw: BurkeShawConfig {
sigma: lf64(a.burke_shaw.sigma, b.burke_shaw.sigma),
rho: lf64(a.burke_shaw.rho, b.burke_shaw.rho),
},
lorenz84: crate::config::Lorenz84Config {
a: lf64(a.lorenz84.a, b.lorenz84.a),
b: lf64(a.lorenz84.b, b.lorenz84.b),
f: lf64(a.lorenz84.f, b.lorenz84.f),
g: lf64(a.lorenz84.g, b.lorenz84.g),
},
rabinovich_fabrikant: crate::config::RabinovichFabrikantConfig {
alpha: lf64(a.rabinovich_fabrikant.alpha, b.rabinovich_fabrikant.alpha),
gamma: lf64(a.rabinovich_fabrikant.gamma, b.rabinovich_fabrikant.gamma),
},
rikitake: crate::config::RikitakeConfig {
mu: lf64(a.rikitake.mu, b.rikitake.mu),
a: lf64(a.rikitake.a, b.rikitake.a),
},
logistic_map: crate::config::LogisticMapConfig {
r: lf64(a.logistic_map.r, b.logistic_map.r),
},
standard_map: crate::config::StandardMapConfig {
k: lf64(a.standard_map.k, b.standard_map.k),
},
stochastic_lorenz: crate::config::StochasticLorenzConfig {
sigma: lf64(a.stochastic_lorenz.sigma, b.stochastic_lorenz.sigma),
rho: lf64(a.stochastic_lorenz.rho, b.stochastic_lorenz.rho),
beta: lf64(a.stochastic_lorenz.beta, b.stochastic_lorenz.beta),
noise_strength: lf64(a.stochastic_lorenz.noise_strength, b.stochastic_lorenz.noise_strength),
},
delayed_map: crate::config::DelayedMapConfig {
r: lf64(a.delayed_map.r, b.delayed_map.r),
tau: if t < 0.5 { a.delayed_map.tau } else { b.delayed_map.tau },
},
oregonator: crate::config::OregonatorConfig {
f: lf64(a.oregonator.f, b.oregonator.f),
},
mathieu: crate::config::MathieuConfig {
a: lf64(a.mathieu.a, b.mathieu.a),
q: lf64(a.mathieu.q, b.mathieu.q),
},
kuramoto_driven: crate::config::KuramotoDrivenConfig {
coupling: lf64(a.kuramoto_driven.coupling, b.kuramoto_driven.coupling),
drive_amp: lf64(a.kuramoto_driven.drive_amp, b.kuramoto_driven.drive_amp),
drive_freq: lf64(a.kuramoto_driven.drive_freq, b.kuramoto_driven.drive_freq),
},
fractional_lorenz: crate::config::FractionalLorenzConfig {
alpha: lf64(a.fractional_lorenz.alpha, b.fractional_lorenz.alpha),
sigma: lf64(a.fractional_lorenz.sigma, b.fractional_lorenz.sigma),
rho: lf64(a.fractional_lorenz.rho, b.fractional_lorenz.rho),
beta: lf64(a.fractional_lorenz.beta, b.fractional_lorenz.beta),
},
bouali: crate::config::BoualiConfig {
a: lf64(a.bouali.a, b.bouali.a),
s: lf64(a.bouali.s, b.bouali.s),
},
newton_leipnik: crate::config::NewtonLeipnikConfig {
a: lf64(a.newton_leipnik.a, b.newton_leipnik.a),
b: lf64(a.newton_leipnik.b, b.newton_leipnik.b),
},
shimizu_morioka: crate::config::ShimizuMoriokaConfig {
a: lf64(a.shimizu_morioka.a, b.shimizu_morioka.a),
b: lf64(a.shimizu_morioka.b, b.shimizu_morioka.b),
},
genesio_tesi: crate::config::GenesioTesiConfig {
a: lf64(a.genesio_tesi.a, b.genesio_tesi.a),
b: lf64(a.genesio_tesi.b, b.genesio_tesi.b),
c: lf64(a.genesio_tesi.c, b.genesio_tesi.c),
},
liu: crate::config::LiuConfig {
a: lf64(a.liu.a, b.liu.a),
b: lf64(a.liu.b, b.liu.b),
c: lf64(a.liu.c, b.liu.c),
e: lf64(a.liu.e, b.liu.e),
k: lf64(a.liu.k, b.liu.k),
m: lf64(a.liu.m, b.liu.m),
},
windmi: crate::config::WindmiConfig {
a: lf64(a.windmi.a, b.windmi.a),
b: lf64(a.windmi.b, b.windmi.b),
},
finance: crate::config::FinanceConfig {
a: lf64(a.finance.a, b.finance.a),
b: lf64(a.finance.b, b.finance.b),
c: lf64(a.finance.c, b.finance.c),
},
hyperchaos: crate::config::HyperchaosConfig {
a: lf64(a.hyperchaos.a, b.hyperchaos.a),
b: lf64(a.hyperchaos.b, b.hyperchaos.b),
c: lf64(a.hyperchaos.c, b.hyperchaos.c),
d: lf64(a.hyperchaos.d, b.hyperchaos.d),
},
tinkerbell: crate::config::TinkerbellConfig {
a: lf64(a.tinkerbell.a, b.tinkerbell.a),
b: lf64(a.tinkerbell.b, b.tinkerbell.b),
c: lf64(a.tinkerbell.c, b.tinkerbell.c),
d: lf64(a.tinkerbell.d, b.tinkerbell.d),
},
viz: a.viz.clone(), ..a.clone()
}
}
pub fn total_duration(scenes: &[Scene]) -> f32 {
let active: Vec<usize> = (0..scenes.len()).filter(|&i| scenes[i].active).collect();
active
.iter()
.enumerate()
.map(|(ord, &idx)| {
let s = &scenes[idx];
let morph = if ord > 0 && s.transition_type != TransitionType::Snap {
s.morph_secs
} else {
0.0
};
morph + s.hold_secs
})
.sum()
}
pub fn scene_at(scenes: &[Scene], elapsed: f32) -> Option<(usize, bool, f32)> {
let active: Vec<usize> = (0..scenes.len()).filter(|&i| scenes[i].active).collect();
if active.is_empty() {
return None;
}
let mut t = elapsed;
for (ord, &idx) in active.iter().enumerate() {
let scene = &scenes[idx];
if ord > 0 && scene.transition_type != TransitionType::Snap {
if t < scene.morph_secs {
return Some((idx, true, t / scene.morph_secs.max(0.001)));
}
t -= scene.morph_secs;
}
if t < scene.hold_secs {
return Some((idx, false, t / scene.hold_secs.max(0.001)));
}
t -= scene.hold_secs;
}
None }
fn lcg(seed: &mut u64) -> f64 {
#[allow(clippy::unreadable_literal)]
{
*seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
}
(*seed >> 33) as f64 / (1u64 << 31) as f64
}
fn make_scene(
name: &str,
preset: &str,
hold: f32,
morph: f32,
tweaks: impl FnOnce(&mut Config),
) -> Scene {
let mut cfg = load_preset(preset);
tweaks(&mut cfg);
Scene {
name: name.to_string(),
config: cfg,
hold_secs: hold,
morph_secs: morph,
active: true,
transition_prob: 1.0,
transition_type: TransitionType::default(),
morph_curve: MorphCurve::default(),
}
}
pub fn demo_arrangement() -> Vec<Scene> {
let mut scenes: Vec<Scene> = vec![
make_scene("Opening", "Midnight Approach", 25.0, 0.0, |_| {}),
make_scene("The Emergence", "The Phase Transition", 20.0, 30.0, |c| {
c.kuramoto.coupling = 1.0;
}),
make_scene("Turbulence", "Frozen Machinery", 15.0, 25.0, |_| {}),
make_scene("The Turn", "Glass Harp", 20.0, 28.0, |_| {}),
make_scene("Dissolution", "Collapsing Cathedral", 25.0, 30.0, |_| {}),
make_scene("Return", "Midnight Approach", 20.0, 25.0, |c| {
c.audio.reverb_wet = (c.audio.reverb_wet + 0.08).min(0.95);
}),
];
while scenes.len() < 8 {
scenes.push(Scene::empty(scenes.len()));
}
scenes
}
pub fn generate_song(mood: &str, seed: u64) -> Vec<Scene> {
#[allow(clippy::unreadable_literal)]
let mut rng = seed ^ 0xDEAD_BEEF_CAFE_BABE;
let rf = |rng: &mut u64| -> f32 { lcg(rng) as f32 }; let ri = |rng: &mut u64, n: usize| -> usize {
(lcg(rng) * n as f64) as usize % n
};
let rrange = |rng: &mut u64, lo: f32, hi: f32| -> f32 {
lo + rf(rng) * (hi - lo)
};
let pick =
|rng: &mut u64, choices: &[&str]| -> String { choices[ri(rng, choices.len())].to_string() };
let ambient_pool: &[&str] = &[
"Midnight Approach",
"The Irrational Winding",
"Breathing Galaxy",
"Memory of Water",
"Monk's Bell",
"Deep Hypnosis",
"Aurora Borealis",
"The Synchronization",
"Last Light",
"Cathedral Organ",
"Collapsing Cathedral",
"The Butterfly's Aria",
"Siren Call",
"Throat of the Storm",
"Double Convection",
"Mirror Attractor",
"Half-Speed Spiral",
"Substorm",
];
let rhythmic_pool: &[&str] = &[
"Frozen Machinery",
"The Phase Transition",
"Clockwork Insect",
"Planetary Clockwork",
"Industrial Heartbeat",
"Bone Structure",
"Dissociation",
"Neon Labyrinth",
"Tungsten Filament",
"The Double Scroll",
"Solar Wind",
"Electric Kelp",
"Möbius Lead",
"Polarity Reversal",
"Velocity Band",
];
let experimental_pool: &[&str] = &[
"Neon Labyrinth",
"Dissociation",
"Tungsten Filament",
"The Double Scroll",
"Bone Structure",
"Clockwork Insect",
"Ancient Algorithm",
"Seismic Event",
"Frozen Machinery",
"The Phase Transition",
"Electric Kelp",
"Solar Wind",
"Collapsing Cathedral",
"Cyclic Tangle",
"Anti-Lorenz",
"Mirror Attractor",
"Xz Knot",
"Equilibrium Fugue",
"Half-Speed Spiral",
"Jerk Circuit",
"Invisible Hand",
"Hyperchaos Engine",
];
let melodic_pool: &[&str] = &[
"Glass Harp",
"Electric Kelp",
"The Butterfly's Aria",
"Solar Wind",
"Möbius Lead",
"Breathing Galaxy",
"Siren Call",
"Aurora Borealis",
"The Synchronization",
"Equilibrium Fugue",
"Half-Speed Spiral",
"Memory of Water",
];
let pool = match mood {
"rhythmic" => rhythmic_pool,
"experimental" => experimental_pool,
"melodic" => melodic_pool,
_ => ambient_pool,
};
let modes_ambient: &[&str] = &[
"direct",
"direct",
"orbital",
"spectral",
"granular",
"vocal",
"waveguide",
];
let modes_rhythmic: &[&str] = &[
"direct", "direct", "granular", "fm", "orbital", "granular", "spectral",
];
let modes_experimental: &[&str] = &[
"spectral",
"fm",
"granular",
"orbital",
"vocal",
"waveguide",
"fm",
];
let modes_melodic: &[&str] = &[
"direct", "direct", "orbital", "vocal", "waveguide", "fm", "direct",
];
let mode_pool = match mood {
"rhythmic" => modes_rhythmic,
"experimental" => modes_experimental,
"melodic" => modes_melodic,
_ => modes_ambient,
};
let scales_ambient: &[&str] = &[
"pentatonic",
"pentatonic",
"just_intonation",
"chromatic",
"pentatonic",
];
let scales_rhythmic: &[&str] = &[
"pentatonic",
"chromatic",
"chromatic",
"just_intonation",
"pentatonic",
];
let scales_experimental: &[&str] = &[
"microtonal",
"chromatic",
"just_intonation",
"pentatonic",
"microtonal",
];
let scales_melodic: &[&str] = &[
"major",
"pentatonic",
"pentatonic",
"just_intonation",
"major",
];
let scale_pool = match mood {
"rhythmic" => scales_rhythmic,
"experimental" => scales_experimental,
"melodic" => scales_melodic,
_ => scales_ambient,
};
let voice_shapes: &[&str] = &[
"sine", "sine", "sine", "triangle", "saw", "triangle", "triangle",
];
let chord_modes: &[&str] = &["none", "none", "none", "power", "octave", "power"];
let root_freqs: &[f64] = &[
82.4, 110.0, 130.8, 164.8, 196.0, 220.0, 261.6, 293.7, 329.6, 392.0, 440.0, 523.3,
];
let transpose_opts: &[f32] = &[-12.0, -7.0, -5.0, 0.0, 0.0, 0.0, 5.0, 7.0, 12.0];
let anchor_root_freq = root_freqs[ri(&mut rng, root_freqs.len())];
let anchor_scale = pick(&mut rng, scale_pool);
let anchor_speed_mult = rrange(&mut rng, 0.6, 2.0);
let reverb_budget_total = rrange(&mut rng, 2.0, 3.5);
let mut reverb_spent = 0.0f32;
let (hold_lo, hold_hi, morph_lo, morph_hi) = match mood {
"rhythmic" => (10.0f32, 20.0, 18.0f32, 32.0),
"experimental" => (8.0f32, 18.0, 22.0f32, 42.0),
"melodic" => (14.0f32, 28.0, 24.0f32, 44.0),
_ => (12.0f32, 26.0, 22.0f32, 40.0),
};
let n_scenes = 6;
let mut chosen_presets: Vec<String> = Vec::with_capacity(n_scenes);
for _ in 0..n_scenes {
let mut attempts = 0;
loop {
let candidate = pick(&mut rng, pool);
let last = chosen_presets.last().map(|s| s.as_str()).unwrap_or("");
if candidate != last || attempts > 8 {
chosen_presets.push(candidate);
break;
}
attempts += 1;
}
}
let names_ambient: &[&str] = &[
"Drift", "Emerge", "Float", "Breathe", "Dissolve", "Return", "Expand", "Recede",
];
let names_rhythmic: &[&str] = &[
"Pulse", "Build", "Lock", "Scatter", "Drive", "Release", "Surge", "Drop",
];
let names_experimental: &[&str] = &[
"Fracture", "Warp", "Corrupt", "Scatter", "Void", "Strange", "Collapse", "Mutate",
];
let names_melodic: &[&str] = &[
"Theme", "Variation", "Bridge", "Return", "Coda", "Interlude", "Cadence", "Resolution",
];
let name_pool = match mood {
"rhythmic" => names_rhythmic,
"experimental" => names_experimental,
"melodic" => names_melodic,
_ => names_ambient,
};
let mut scenes: Vec<Scene> = Vec::with_capacity(n_scenes);
for (i, preset) in chosen_presets.iter().enumerate() {
let hold = rrange(&mut rng, hold_lo, hold_hi);
let morph = if i == 0 {
0.0
} else {
rrange(&mut rng, morph_lo, morph_hi)
};
let mode = pick(&mut rng, mode_pool);
let scale = if rf(&mut rng) < 0.65 {
anchor_scale.clone()
} else {
pick(&mut rng, scale_pool)
};
let chord = pick(&mut rng, chord_modes);
let remaining_scenes = (n_scenes - i).max(1) as f32;
let reverb_max =
((reverb_budget_total - reverb_spent) / remaining_scenes).clamp(0.15, 0.80);
let reverb = rrange(&mut rng, 0.15, reverb_max);
reverb_spent += reverb;
let chorus = rrange(&mut rng, 0.0, 0.65);
let delay_fb = rrange(&mut rng, 0.0, 0.65);
let delay_ms = rrange(&mut rng, 60.0, 800.0);
let porta = rrange(&mut rng, 20.0, 800.0);
let waveshaper_drive = rrange(&mut rng, 1.0, 6.0);
let waveshaper_mix = if rf(&mut rng) > 0.5 {
rrange(&mut rng, 0.0, 0.5)
} else {
0.0
};
let base_freq = if rf(&mut rng) < 0.70 {
anchor_root_freq
} else {
let octave_shift = if rf(&mut rng) < 0.5 { 0.5 } else { 2.0 };
(anchor_root_freq * octave_shift).clamp(82.0, 880.0)
};
let transpose = transpose_opts[ri(&mut rng, transpose_opts.len())];
let octave_range = rrange(&mut rng, 0.8, 3.5) as f64;
let speed_scatter = rrange(&mut rng, 0.6, 1.4);
let speed_mult = anchor_speed_mult * speed_scatter;
let vs0 = pick(&mut rng, voice_shapes);
let vs1 = pick(&mut rng, voice_shapes);
let vs2 = pick(&mut rng, voice_shapes);
let vs3 = pick(&mut rng, voice_shapes);
let vl0 = rrange(&mut rng, 0.3, 1.0);
let vl1 = rrange(&mut rng, 0.0, 1.0);
let vl2 = rrange(&mut rng, 0.0, 0.9);
let vl3 = rrange(&mut rng, 0.0, 0.7);
let lorenz_sigma = rrange(&mut rng, 7.0, 14.0) as f64;
let lorenz_rho = rrange(&mut rng, 20.0, 40.0) as f64;
let lorenz_beta = rrange(&mut rng, 1.5, 4.0) as f64;
let rossler_a = rrange(&mut rng, 0.05, 0.35) as f64;
let rossler_c = rrange(&mut rng, 3.0, 13.0) as f64;
let kuramoto_k = rrange(&mut rng, 0.3, 4.5) as f64;
let duffing_om = rrange(&mut rng, 0.6, 1.4) as f64;
let halvorsen_a = rrange(&mut rng, 1.2, 1.9) as f64;
let thomas_b = rrange(&mut rng, 0.15, 0.30) as f64;
let chen_a = rrange(&mut rng, 32.0, 50.0) as f64;
let chen_c = rrange(&mut rng, 20.0, 35.0) as f64;
let rikitake_mu = rrange(&mut rng, 0.5, 2.0) as f64;
let rikitake_a = rrange(&mut rng, 3.0, 8.0) as f64;
let rucklidge_lambda = rrange(&mut rng, 5.0, 9.0) as f64;
let shimizu_a = rrange(&mut rng, 0.5, 1.2) as f64;
let shimizu_b = rrange(&mut rng, 0.3, 0.8) as f64;
let genesio_a = rrange(&mut rng, 0.8, 1.8) as f64;
let genesio_b = rrange(&mut rng, 2.0, 4.0) as f64;
let genesio_c = rrange(&mut rng, 4.0, 8.0) as f64;
let liu_b = rrange(&mut rng, 1.5, 3.5) as f64;
let liu_k = rrange(&mut rng, 2.0, 6.0) as f64;
let liu_m = rrange(&mut rng, 2.0, 6.0) as f64;
let windmi_a = rrange(&mut rng, 0.6, 1.2) as f64;
let windmi_b = rrange(&mut rng, 1.8, 3.2) as f64;
let finance_a = rrange(&mut rng, 2.0, 5.0) as f64;
let finance_b = rrange(&mut rng, 0.05, 0.3) as f64;
let finance_c = rrange(&mut rng, 0.5, 2.0) as f64;
let name_idx = (i + ri(&mut rng, 3)) % name_pool.len();
let name = name_pool[name_idx];
scenes.push(make_scene(name, preset, hold, morph, move |c| {
c.sonification.mode = mode;
c.sonification.scale = scale;
c.sonification.chord_mode = chord;
c.sonification.base_frequency = base_freq;
c.sonification.octave_range = octave_range;
c.sonification.transpose_semitones = transpose;
c.sonification.portamento_ms = porta;
c.sonification.voice_shapes = [vs0, vs1, vs2, vs3];
c.sonification.voice_levels = [vl0, vl1, vl2, vl3];
c.system.speed = (c.system.speed * speed_mult as f64).clamp(0.5, 6.0);
c.audio.reverb_wet = reverb.min(0.82);
c.audio.chorus_mix = chorus;
c.audio.delay_feedback = delay_fb;
c.audio.delay_ms = delay_ms;
c.audio.waveshaper_drive = waveshaper_drive;
c.audio.waveshaper_mix = waveshaper_mix;
c.lorenz.sigma = lorenz_sigma;
c.lorenz.rho = lorenz_rho;
c.lorenz.beta = lorenz_beta;
c.rossler.a = rossler_a;
c.rossler.c = rossler_c;
c.kuramoto.coupling = kuramoto_k;
c.duffing.omega = duffing_om;
c.halvorsen.a = halvorsen_a;
c.thomas.b = thomas_b;
c.chen.a = chen_a;
c.chen.c = chen_c;
c.rikitake.mu = rikitake_mu;
c.rikitake.a = rikitake_a;
c.rucklidge.lambda = rucklidge_lambda;
c.shimizu_morioka.a = shimizu_a;
c.shimizu_morioka.b = shimizu_b;
c.genesio_tesi.a = genesio_a;
c.genesio_tesi.b = genesio_b;
c.genesio_tesi.c = genesio_c;
c.liu.b = liu_b;
c.liu.k = liu_k;
c.liu.m = liu_m;
c.windmi.a = windmi_a;
c.windmi.b = windmi_b;
c.finance.a = finance_a;
c.finance.b = finance_b;
c.finance.c = finance_c;
c.audio.master_volume = c.audio.master_volume.max(0.62);
}));
}
if mood == "rhythmic" || mood == "experimental" {
for scene in scenes.iter_mut() {
if rf(&mut rng) > 0.55 {
scene.config.audio.bit_depth = rrange(&mut rng, 5.0, 15.0);
scene.config.audio.rate_crush = rrange(&mut rng, 0.0, 0.45);
}
}
}
while scenes.len() < 8 {
scenes.push(Scene::empty(scenes.len()));
}
scenes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn make_scene(hold: f32, morph: f32, active: bool) -> Scene {
let mut s = Scene::empty(0);
s.hold_secs = hold;
s.morph_secs = morph;
s.active = active;
s
}
#[test]
fn lerp_config_t0_equals_a() {
let mut a = Config::default();
a.lorenz.sigma = 5.0;
let b = Config::default();
let out = lerp_config(&a, &b, 0.0);
assert!((out.lorenz.sigma - 5.0).abs() < 1e-9);
}
#[test]
fn lerp_config_t1_equals_b() {
let a = Config::default();
let mut b = Config::default();
b.lorenz.sigma = 20.0;
let out = lerp_config(&a, &b, 1.0);
assert!((out.lorenz.sigma - 20.0).abs() < 1e-9);
}
#[test]
fn lerp_config_midpoint_is_average() {
let mut a = Config::default();
let mut b = Config::default();
a.lorenz.sigma = 4.0;
b.lorenz.sigma = 8.0;
let out = lerp_config(&a, &b, 0.5);
assert!((out.lorenz.sigma - 6.0).abs() < 1e-9, "expected 6.0, got {}", out.lorenz.sigma);
}
#[test]
fn lerp_config_t_clamped_above_1() {
let mut a = Config::default();
let mut b = Config::default();
a.lorenz.rho = 10.0;
b.lorenz.rho = 30.0;
let out = lerp_config(&a, &b, 1.5);
assert!((out.lorenz.rho - 30.0).abs() < 1e-9);
}
#[test]
fn lerp_config_t_clamped_below_0() {
let mut a = Config::default();
let mut b = Config::default();
a.lorenz.beta = 1.0;
b.lorenz.beta = 3.0;
let out = lerp_config(&a, &b, -0.5);
assert!((out.lorenz.beta - 1.0).abs() < 1e-9);
}
#[test]
fn lerp_config_result_passes_validate() {
let a = Config::default();
let b = Config::default();
let mut out = lerp_config(&a, &b, 0.5);
out.validate(); }
#[test]
fn total_duration_single_active_scene() {
let scenes = vec![make_scene(10.0, 5.0, true)];
assert!((total_duration(&scenes) - 10.0).abs() < 1e-5);
}
#[test]
fn total_duration_two_active_scenes() {
let scenes = vec![
make_scene(10.0, 0.0, true),
make_scene(8.0, 4.0, true),
];
assert!((total_duration(&scenes) - 22.0).abs() < 1e-5);
}
#[test]
fn total_duration_inactive_scenes_ignored() {
let scenes = vec![
make_scene(10.0, 0.0, true),
make_scene(5.0, 3.0, false),
make_scene(8.0, 4.0, true),
];
assert!((total_duration(&scenes) - 22.0).abs() < 1e-5);
}
#[test]
fn total_duration_all_inactive_is_zero() {
let scenes = vec![
make_scene(10.0, 5.0, false),
make_scene(8.0, 4.0, false),
];
assert!((total_duration(&scenes) - 0.0).abs() < 1e-5);
}
#[test]
fn scene_at_during_first_scene_hold() {
let scenes = vec![
make_scene(10.0, 0.0, true),
make_scene(8.0, 4.0, true),
];
let result = scene_at(&scenes, 5.0);
assert!(result.is_some());
let (idx, morphing, _t) = result.unwrap();
assert_eq!(idx, 0, "should be in first scene");
assert!(!morphing, "should not be morphing");
}
#[test]
fn scene_at_during_morph_phase() {
let scenes = vec![
make_scene(10.0, 0.0, true),
make_scene(8.0, 4.0, true),
];
let result = scene_at(&scenes, 12.0);
assert!(result.is_some());
let (idx, morphing, t) = result.unwrap();
assert_eq!(idx, 1, "should be targeting second scene");
assert!(morphing, "should be morphing");
assert!(t > 0.0 && t < 1.0, "morph t should be in (0,1), got {}", t);
}
#[test]
fn scene_at_beyond_end_returns_none() {
let scenes = vec![make_scene(5.0, 0.0, true)];
let result = scene_at(&scenes, 100.0);
assert!(result.is_none(), "should return None after arrangement ends");
}
#[test]
fn scene_at_no_active_scenes_returns_none() {
let scenes = vec![make_scene(10.0, 5.0, false)];
let result = scene_at(&scenes, 5.0);
assert!(result.is_none());
}
#[test]
fn generate_song_returns_eight_scenes() {
for mood in &["ambient", "rhythmic", "experimental", "melodic", "unknown_mood"] {
let scenes = generate_song(mood, 42);
assert_eq!(scenes.len(), 8, "mood={} returned {} scenes", mood, scenes.len());
}
}
#[test]
fn generate_song_all_configs_pass_validate() {
let scenes = generate_song("ambient", 42);
for (i, scene) in scenes.iter().enumerate() {
let mut c = scene.config.clone();
c.validate(); if scene.active {
assert!(
c.audio.master_volume >= 0.0 && c.audio.master_volume <= 1.0,
"Scene {} master_volume out of range: {}", i, c.audio.master_volume
);
}
}
}
#[test]
fn generate_song_deterministic_same_seed() {
let s1 = generate_song("ambient", 99);
let s2 = generate_song("ambient", 99);
assert_eq!(s1.len(), s2.len());
for (a, b) in s1.iter().zip(s2.iter()) {
assert_eq!(a.name, b.name, "Same seed produced different scene names");
}
}
#[test]
fn generate_song_different_seeds_differ() {
let s1 = generate_song("ambient", 1);
let s2 = generate_song("ambient", 2);
let any_diff = s1.iter().zip(s2.iter()).any(|(a, b)| a.name != b.name);
assert!(any_diff, "Different seeds produced identical arrangements");
}
#[test]
fn generate_song_all_scenes_have_positive_durations() {
let scenes = generate_song("rhythmic", 7);
for (i, scene) in scenes.iter().filter(|s| s.active).enumerate() {
assert!(scene.hold_secs > 0.0, "Active scene {} hold_secs = {}", i, scene.hold_secs);
}
}
#[test]
fn demo_arrangement_returns_scenes() {
let scenes = demo_arrangement();
assert!(!scenes.is_empty(), "demo_arrangement returned no scenes");
}
#[test]
fn demo_arrangement_has_at_least_one_active_scene() {
let scenes = demo_arrangement();
let active_count = scenes.iter().filter(|s| s.active).count();
assert!(active_count > 0, "demo_arrangement has no active scenes");
}
}