Skip to main content

rust_synth/math/
genetic.rs

1//! Simple evolutionary operators on track parameters.
2//!
3//! Each track's "genome" is the tuple of tonal params (freq, cutoff,
4//! resonance, reverb_mix, pulse_depth). Mutation perturbs them by a
5//! strength-scaled random; crossover mixes two tracks gene-by-gene and
6//! snaps the frequency back onto a golden-pentatonic lattice so the mix
7//! stays harmonically coherent.
8
9use fundsp::hacker::Shared;
10
11use super::harmony::{golden_pentatonic, rand_f32, rand_u32};
12
13/// Opaque view of a track's params — everything `mutate` and `crossover`
14/// touch. Keeps this module free of `audio::track` coupling.
15pub struct Genome<'a> {
16    pub freq: &'a Shared,
17    pub cutoff: &'a Shared,
18    pub resonance: &'a Shared,
19    pub reverb_mix: &'a Shared,
20    pub pulse_depth: &'a Shared,
21    pub pattern_hits: &'a Shared,
22    pub pattern_rotation: &'a Shared,
23}
24
25/// Mutate a single gene slot. `strength` in [0, 1]:
26///   0.0 → no-op,
27///   0.3 → gentle drift (default),
28///   1.0 → wild.
29///
30/// Freq snaps to the closest golden-pentatonic note around its current
31/// value so the voice never strays into clashing intervals.
32pub fn mutate(g: &Genome, seed: &mut u64, strength: f32) {
33    let s = strength.clamp(0.0, 1.0);
34
35    // Freq — pick a neighbour on the golden pentatonic scale.
36    let cur = g.freq.value();
37    let scale = golden_pentatonic(cur);
38    let idx = rand_u32(seed, scale.len() as u32) as usize;
39    g.freq.set_value(scale[idx]);
40
41    // Cutoff — multiplicative nudge, log-scaled (exp perturbation).
42    let cut_factor = (1.0 + s * 0.8 * rand_f32(seed)).clamp(0.25, 4.0);
43    g.cutoff
44        .set_value((g.cutoff.value() * cut_factor).clamp(40.0, 12000.0));
45
46    // Resonance — additive drift, clamped well below Moog self-oscillation
47    // (≈ 0.7). Smaller perturbation too, so auto-evolve can't spike it.
48    let res = (g.resonance.value() + s * 0.15 * rand_f32(seed)).clamp(0.0, 0.55);
49    g.resonance.set_value(res);
50
51    let rev = (g.reverb_mix.value() + s * 0.25 * rand_f32(seed)).clamp(0.0, 1.0);
52    g.reverb_mix.set_value(rev);
53
54    let pulse = (g.pulse_depth.value() + s * 0.2 * rand_f32(seed)).clamp(0.0, 1.0);
55    g.pulse_depth.set_value(pulse);
56
57    // Pattern drift — drum voices get rhythmic variety. Non-drum voices
58    // still have these Shared values; the preset just ignores them.
59    // Strength 1.0 → up to ±3 hits, ±4 rotation; scaled by s.
60    let hits = (g.pattern_hits.value() + s * 3.0 * rand_f32(seed)).clamp(1.0, 11.0);
61    g.pattern_hits.set_value(hits);
62    let rot = (g.pattern_rotation.value() + s * 4.0 * rand_f32(seed)).rem_euclid(16.0);
63    g.pattern_rotation.set_value(rot);
64}
65
66/// Uniform crossover — each gene comes from `a` or `b` with 50/50 chance.
67/// Result is written into `a`. Freq is snapped to pentatonic afterwards.
68pub fn crossover(a: &Genome, b: &Genome, seed: &mut u64) {
69    if rand_u32(seed, 2) == 0 {
70        a.freq.set_value(b.freq.value());
71    }
72    if rand_u32(seed, 2) == 0 {
73        a.cutoff.set_value(b.cutoff.value());
74    }
75    if rand_u32(seed, 2) == 0 {
76        a.resonance.set_value(b.resonance.value());
77    }
78    if rand_u32(seed, 2) == 0 {
79        a.reverb_mix.set_value(b.reverb_mix.value());
80    }
81    if rand_u32(seed, 2) == 0 {
82        a.pulse_depth.set_value(b.pulse_depth.value());
83    }
84    if rand_u32(seed, 2) == 0 {
85        a.pattern_hits.set_value(b.pattern_hits.value());
86    }
87    if rand_u32(seed, 2) == 0 {
88        a.pattern_rotation.set_value(b.pattern_rotation.value());
89    }
90    // Snap freq after crossover.
91    let cur = a.freq.value();
92    let scale = golden_pentatonic(cur);
93    let idx = rand_u32(seed, scale.len() as u32) as usize;
94    a.freq.set_value(scale[idx]);
95}