Skip to main content

auracle_grammar/
diff.rs

1//! Structural/parameter diff between two patch trees, in trace-address terms.
2//!
3//! Used to make evolution legible: "what did this MH step / generation
4//! actually do" rendered as knob moves, module swaps, and added or removed
5//! subtrees.
6
7use fugue_evo::genome::trace_genome::{ChoiceValue, TraceGenome};
8use serde::{Deserialize, Serialize};
9
10use crate::edit::split_addr;
11use crate::term::PatchTree;
12
13/// One changed choice site.
14#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15pub struct DiffEntry {
16    /// Full trace address (`key#site`).
17    pub addr: String,
18    /// Display value before (`None` if the site was added).
19    pub before: Option<String>,
20    /// Display value after (`None` if the site was removed).
21    pub after: Option<String>,
22}
23
24fn display_value(site: &str, v: &ChoiceValue) -> String {
25    match v {
26        ChoiceValue::F64(x) => format!("{x:.2}"),
27        ChoiceValue::Bool(b) => if *b { "source" } else { "processor" }.into(),
28        ChoiceValue::Usize(i) => {
29            let name = |names: &[&str]| names.get(*i).map(|s| s.to_string());
30            match site {
31                "wave" => name(&["sin", "tri", "saw", "sqr"]),
32                "color" => name(&["white", "pink"]),
33                "fkind" => name(&["svf lp", "svf bp", "svf hp", "ladder"]),
34                // These three are the grammar's categoricals, in the index
35                // order `crate::genome` persists. They fell out of date once
36                // already, which shows up as an evolution diff reporting the
37                // raw index — "op: 3 → 11" instead of "delay → tremolo" —
38                // exactly where the point of the view is legibility.
39                "src" => name(&["vco", "supersaw", "noise", "wavetable", "pluck", "formant"]),
40                "op" => name(&[
41                    "mix",
42                    "filter",
43                    "wavefolder",
44                    "delay",
45                    "chorus",
46                    "reverb",
47                    "distortion",
48                    "bitcrush",
49                    "phaser",
50                    "ring mod",
51                    "flanger",
52                    "tremolo",
53                    "vibrato",
54                    "eq",
55                    "granular",
56                ]),
57                "mod" => name(&["no mod", "lfo", "mod env", "s&h rand", "follower"]),
58                "table" => name(&[
59                    "sine",
60                    "tri",
61                    "saw",
62                    "square",
63                    "pulse 25",
64                    "pulse 12",
65                    "formant a",
66                    "formant o",
67                ]),
68                "dmode" => name(&["soft", "hard", "tube"]),
69                "oct" => Some(format!("{:+}", *i as i8 - 2)),
70                _ => None,
71            }
72            .unwrap_or_else(|| i.to_string())
73        }
74        other => format!("{other:?}"),
75    }
76}
77
78/// Diff two trees by their canonical trace encodings.
79///
80/// Entries are sorted by address; a structural move shows up as a cluster of
81/// removed/added sites under the rewritten keys.
82pub fn tree_diff(before: &PatchTree, after: &PatchTree) -> Vec<DiffEntry> {
83    let ta = before.to_trace();
84    let tb = after.to_trace();
85    let mut out = Vec::new();
86    for (addr, ca) in &ta.choices {
87        let site = split_addr(addr).1;
88        match tb.choices.get(addr) {
89            Some(cb) if cb.value == ca.value => {}
90            Some(cb) => out.push(DiffEntry {
91                addr: addr.to_string(),
92                before: Some(display_value(site, &ca.value)),
93                after: Some(display_value(site, &cb.value)),
94            }),
95            None => out.push(DiffEntry {
96                addr: addr.to_string(),
97                before: Some(display_value(site, &ca.value)),
98                after: None,
99            }),
100        }
101    }
102    for (addr, cb) in &tb.choices {
103        if !ta.choices.contains_key(addr) {
104            let site = split_addr(addr).1;
105            out.push(DiffEntry {
106                addr: addr.to_string(),
107                before: None,
108                after: Some(display_value(site, &cb.value)),
109            });
110        }
111    }
112    out.sort_by(|x, y| x.addr.cmp(&y.addr));
113    out
114}