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    pub character: &'a Shared,
24}
25
26/// Mutate a single gene slot. `strength` in [0, 1]:
27///   0.0 → no-op,
28///   0.3 → gentle drift (default),
29///   1.0 → wild.
30///
31/// Freq snaps to the closest golden-pentatonic note around its current
32/// value so the voice never strays into clashing intervals.
33pub fn mutate(g: &Genome, seed: &mut u64, strength: f32) {
34    let s = strength.clamp(0.0, 1.0);
35
36    // Freq — pick a neighbour on the golden pentatonic scale.
37    let cur = g.freq.value();
38    let scale = golden_pentatonic(cur);
39    let idx = rand_u32(seed, scale.len() as u32) as usize;
40    g.freq.set_value(scale[idx]);
41
42    // Cutoff — multiplicative nudge, log-scaled (exp perturbation).
43    let cut_factor = (1.0 + s * 0.8 * rand_f32(seed)).clamp(0.25, 4.0);
44    g.cutoff
45        .set_value((g.cutoff.value() * cut_factor).clamp(40.0, 12000.0));
46
47    // Resonance — additive drift, clamped well below Moog self-oscillation
48    // (≈ 0.7). Smaller perturbation too, so auto-evolve can't spike it.
49    let res = (g.resonance.value() + s * 0.15 * rand_f32(seed)).clamp(0.0, 0.55);
50    g.resonance.set_value(res);
51
52    let rev = (g.reverb_mix.value() + s * 0.25 * rand_f32(seed)).clamp(0.0, 1.0);
53    g.reverb_mix.set_value(rev);
54
55    let pulse = (g.pulse_depth.value() + s * 0.2 * rand_f32(seed)).clamp(0.0, 1.0);
56    g.pulse_depth.set_value(pulse);
57
58    // Pattern drift — drum voices get rhythmic variety. Non-drum voices
59    // still have these Shared values; the preset just ignores them.
60    // Strength 1.0 → up to ±3 hits, ±4 rotation; scaled by s.
61    let hits = (g.pattern_hits.value() + s * 3.0 * rand_f32(seed)).clamp(1.0, 11.0);
62    g.pattern_hits.set_value(hits);
63    let rot = (g.pattern_rotation.value() + s * 4.0 * rand_f32(seed)).rem_euclid(16.0);
64    g.pattern_rotation.set_value(rot);
65
66    // Character — formula-shape drift. Wider strength because the
67    // audible effect per unit is smaller than cutoff / gain drift.
68    let ch = (g.character.value() + s * 0.35 * rand_f32(seed)).clamp(0.0, 1.0);
69    g.character.set_value(ch);
70}
71
72/// Uniform crossover — each gene comes from `a` or `b` with 50/50 chance.
73/// Result is written into `a`. Freq is snapped to pentatonic afterwards.
74pub fn crossover(a: &Genome, b: &Genome, seed: &mut u64) {
75    if rand_u32(seed, 2) == 0 {
76        a.freq.set_value(b.freq.value());
77    }
78    if rand_u32(seed, 2) == 0 {
79        a.cutoff.set_value(b.cutoff.value());
80    }
81    if rand_u32(seed, 2) == 0 {
82        a.resonance.set_value(b.resonance.value());
83    }
84    if rand_u32(seed, 2) == 0 {
85        a.reverb_mix.set_value(b.reverb_mix.value());
86    }
87    if rand_u32(seed, 2) == 0 {
88        a.pulse_depth.set_value(b.pulse_depth.value());
89    }
90    if rand_u32(seed, 2) == 0 {
91        a.pattern_hits.set_value(b.pattern_hits.value());
92    }
93    if rand_u32(seed, 2) == 0 {
94        a.pattern_rotation.set_value(b.pattern_rotation.value());
95    }
96    if rand_u32(seed, 2) == 0 {
97        a.character.set_value(b.character.value());
98    }
99    // Snap freq after crossover.
100    let cur = a.freq.value();
101    let scale = golden_pentatonic(cur);
102    let idx = rand_u32(seed, scale.len() as u32) as usize;
103    a.freq.set_value(scale[idx]);
104}