//! Melody variants: a loop's part (its phrase: the cycles it walks through //! from the first it plays until they repeat, 32 bars at most, frozen as //! notes) rewritten into variants a person auditions, keeps, applies or //! merges bar by bar. The procedural ones are here and seeded (the same //! seed, the same variant): variations, more syncopated, simpler, darker, //! an answer phrase, re-harmonised, busier, humanised. An AI's variants //! come back in the same form ([`parse_notes`]). Applying is one edit: a //! loop with code plays the phrase from its cycle on (and its own code //! before it, and after it when the phrase does not repeat); a pattern or //! a list becomes a `notes:` list. use crate::model::ScoreDoc; use crate::source; use crate::theory; use makepad_score_player::presets::DRUM_SLOTS; /// One note of a part: from `beat` (in the cycle) for `dur` beats. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Note { pub beat: f64, pub note: f32, pub dur: f32, pub vel: f32, /// A drum hit (`note` is its kit slot). pub hit: bool, } /// A loop's part: its notes over `beats` (its phrase: `beats / cycle` /// cycles of the loop from cycle number `first`; `repeats`: the phrase /// comes round again for good, else the loop's own code follows it). #[derive(Clone, Debug, PartialEq)] pub struct Part { pub beats: f64, pub beats_per_bar: f64, pub notes: Vec, pub cycle: f64, pub first: u32, pub repeats: bool, } impl Part { pub fn bars(&self) -> usize { (self.beats / self.beats_per_bar.max(1.0)).ceil().max(1.0) as usize } /// The notes as a score writes them: `[[beat note dur vel] …]`. pub fn text(&self) -> String { let items: Vec = self.notes.iter().map(|n| { let note = if n.hit { format!("@{}", DRUM_SLOTS.get(n.note as usize).copied().unwrap_or("kick")) } else { format!("@{}", theory::note_name(n.note)) }; format!("[{} {note} {} {}]", source::fmt_num(n.beat, 3), source::fmt_num(n.dur as f64, 3), source::fmt_num(n.vel as f64, 2)) }).collect(); format!("[{}]", items.join(" ")) } } /// What a loop plays in its first cycle, frozen (generated loops included). pub fn part_of(src: &str, node: &str) -> Result { let doc = ScoreDoc::load(src).map_err(|d| d.first().map_or("the score does not load".to_string(), |d| d.message.clone()))?; if doc.loop_def(node).is_none() { return Err(format!("`{node}` is not a loop")); } // Its phrase: the cycles it walks through before they repeat (32 bars // at most), so a melody written bar by bar is varied whole. let phrase = crate::render::phrase_notes(src, node, 32.0 * doc.beats_per_bar()).ok_or_else(|| format!("`{node}` plays nothing in the score"))?; let (len, notes, durs) = (phrase.beats, phrase.notes, phrase.durs); let notes = notes.iter().enumerate().map(|(i, (beat, note, vel, hit))| Note { beat: *beat, note: *note, dur: durs.get(i).copied().filter(|d| *d > 0.0).unwrap_or(0.25), vel: *vel, hit: *hit, }).collect(); Ok(Part { beats: len.max(1.0), beats_per_bar: doc.beats_per_bar(), notes, cycle: phrase.cycle.max(0.25), first: phrase.first, repeats: phrase.repeats }) } /// The `fn(i) { … }` of a loop's code: its parameter and its body. fn code_of(src: &str, node: &str) -> Option<(String, String)> { let outline = source::outline(src); let ctor = outline.node(node)?; let text = ctor.field("code")?.value.text(src).trim().to_string(); let open = text.find('(')?; let close = open + text[open..].find(')')?; let param = text[open + 1..close].trim().to_string(); let body_open = close + text[close..].find('{')?; let body_close = text.rfind('}')?; (body_close > body_open).then(|| (if param.is_empty() { "i".to_string() } else { param }, text[body_open + 1..body_close].to_string())) } /// `src` with loop `node` playing `part` (as a `notes:` list; its code, /// pattern and note go), checked to load. pub fn apply(src: &str, node: &str, part: &Part) -> Result { if let Some((param, body)) = code_of(src, node) { return apply_code(src, node, part, ¶m, &body); } let mut next = src.to_string(); for key in ["code", "hits", "note", "step"] { if let Ok(splice) = source::remove_field(&next, node, key) { next = source::apply(&next, &splice); } } for (key, value) in [("notes", part.text()), ("beats", source::fmt_num(part.beats, 3))] { let splice = source::set_field(&next, node, key, &value)?; next = source::apply(&next, &splice); } ScoreDoc::load(&next).map_err(|d| d.iter().map(|d| d.message.clone()).collect::>().join("; "))?; Ok(next) } /// A loop with code made to play `part`: its cycles from `first` on take /// the phrase's notes, cycle by cycle; before it (and after it, when the /// phrase does not repeat) the loop's own code plays as it did. fn apply_code(src: &str, node: &str, part: &Part, param: &str, body: &str) -> Result { let cycles = ((part.beats / part.cycle).round() as usize).max(1); let slices: Vec = (0..cycles).map(|k| { let (t0, t1) = (k as f64 * part.cycle, (k + 1) as f64 * part.cycle); let notes = part.notes.iter().filter(|n| n.beat >= t0 - 1e-9 && n.beat < t1 - 1e-9).map(|n| Note { beat: n.beat - t0, ..*n }).collect(); Part { notes, ..part.clone() }.text() }).collect(); // Its own names (a body may use `phrase`, `k` or `n` for its own). let within = if part.repeats { String::new() } else { format!(" && vary_k < {cycles}") }; let cycle = source::fmt_num(part.cycle, 3); let own = if body.trim().is_empty() { format!("sleep({cycle} - beat())") } else { body.trim().to_string() }; let code = format!( "fn({param}) {{\n let vary_k = {param} - {first}\n if vary_k >= 0{within} {{\n let vary_cycles = [{phrase}]\n for vary_n in vary_cycles[vary_k % {cycles}] {{\n if vary_n[0] > beat() {{ sleep(vary_n[0] - beat()) }}\n play(vary_n[1], {{dur: vary_n[2] amp: vary_n[3]}})\n }}\n sleep({cycle} - beat())\n }} else {{\n {own}\n }}\n }}", first = part.first, phrase = slices.join("\n "), ); let mut next = src.to_string(); for key in ["hits", "note", "step", "notes"] { if let Ok(splice) = source::remove_field(&next, node, key) { next = source::apply(&next, &splice); } } for (key, value) in [("code", code), ("beats", cycle.clone())] { let splice = source::set_field(&next, node, key, &value)?; next = source::apply(&next, &splice); } ScoreDoc::load(&next).map_err(|d| d.iter().map(|d| d.message.clone()).collect::>().join("; "))?; if let Some(error) = crate::render::loop_error(&next, node) { return Err(format!("`{node}` would not play: {error}")); } Ok(next) } /// Bar by bar from the parts chosen (`choice[bar]`: an index into /// `parts`); a bar past the choices is the first part's. pub fn merge(parts: &[&Part], choice: &[usize]) -> Part { let base = parts[0]; let bar = base.beats_per_bar; let mut notes = Vec::new(); for b in 0..base.bars() { let from = parts.get(choice.get(b).copied().unwrap_or(0)).copied().unwrap_or(base); let (t0, t1) = (b as f64 * bar, (b + 1) as f64 * bar); notes.extend(from.notes.iter().filter(|n| n.beat >= t0 - 1e-9 && n.beat < t1 - 1e-9).copied()); } Part { notes, ..base.clone() } } /// The procedural variants, by name. pub const KINDS: [(&str, &str); 8] = [ ("variations", "4 variations"), ("syncopated", "More syncopated"), ("simpler", "Simpler"), ("darker", "Darker"), ("answer", "Answer phrase"), ("reharmonise", "Re-harmonise"), ("busier", "Busier"), ("humanise", "Humanise"), ]; /// A small deterministic generator (SplitMix64). struct Rng(u64); impl Rng { fn new(seed: u64) -> Rng { Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ 0x2545_F491_4F6C_DD1D) } fn next(&mut self) -> f64 { self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); let mut z = self.0; z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); ((z ^ (z >> 31)) >> 11) as f64 / (1u64 << 53) as f64 } } /// The part's key: the root and scale (of major and natural minor) that /// hold most of its notes, its lowest best-fitting note the root. pub fn key_of(part: &Part) -> (f32, &'static [i32]) { let pcs: Vec = part.notes.iter().filter(|n| !n.hit).map(|n| (n.note.round() as i32).rem_euclid(12)).collect(); let mut best = (0i32, theory::scale_kind("minor").unwrap(), -1i32); for root in 0..12 { for kind in ["minor", "major"] { let scale = theory::scale_kind(kind).unwrap(); let fit = pcs.iter().filter(|pc| scale.contains(&((*pc - root).rem_euclid(12)))).count() as i32 * 4 + pcs.iter().filter(|pc| **pc == root).count() as i32; if fit > best.2 { best = (root, scale, fit); } } } let low = part.notes.iter().filter(|n| !n.hit).map(|n| n.note).fold(f32::MAX, f32::min); let low = if low == f32::MAX { 48.0 } else { low }; let root = low - ((low.round() as i32 - best.0).rem_euclid(12)) as f32; (root, best.1) } /// `note` moved by `steps` degrees of the scale on `root` (snapped first). fn degree(note: f32, steps: i32, root: f32, scale: &[i32]) -> f32 { let rel = (note - root).round() as i32; let octave = rel.div_euclid(12); let pc = rel.rem_euclid(12); let mut idx = scale.iter().position(|s| *s >= pc).unwrap_or(scale.len() - 1) as i32; if scale[idx as usize] != pc && idx > 0 && pc - scale[idx as usize - 1] < scale[idx as usize] - pc { idx -= 1; } let total = octave * scale.len() as i32 + idx + steps; let (o, i) = (total.div_euclid(scale.len() as i32), total.rem_euclid(scale.len() as i32)); root + (o * 12 + scale[i as usize]) as f32 } /// The grid the part is written on (the smallest step between onsets). fn grid(part: &Part) -> f64 { let mut beats: Vec = part.notes.iter().map(|n| n.beat).collect(); beats.sort_by(f64::total_cmp); beats.windows(2).map(|w| w[1] - w[0]).filter(|d| *d > 1e-3).fold(0.5f64, f64::min).max(0.125) } fn within(n: &Note, region: Option<(f64, f64)>) -> bool { region.is_none_or(|(a, b)| n.beat >= a - 1e-9 && n.beat < b - 1e-9) } /// Variants of `part` of a kind (one, or `n` for "variations"), seeded; /// only the notes in `region` (beats) change. Each with its label. pub fn vary(part: &Part, kind: &str, n: usize, seed: u64, region: Option<(f64, f64)>) -> Result, String> { let (root, scale) = key_of(part); let g = grid(part); let one = |label: &str, f: &dyn Fn(&mut Rng, &Part) -> Vec, s: u64| -> (String, Part) { let mut rng = Rng::new(seed.wrapping_add(s)); let mut notes = f(&mut rng, part); notes.sort_by(|a, b| a.beat.total_cmp(&b.beat)); notes.retain(|n| n.beat >= 0.0 && n.beat < part.beats - 1e-9); (label.to_string(), Part { notes, ..part.clone() }) }; // Each transform leaves the notes outside the region as they are. let keep_out = |p: &Part| -> Vec { p.notes.iter().filter(|n| !within(n, region)).copied().collect() }; let inside = |p: &Part| -> Vec { p.notes.iter().filter(|n| within(n, region)).copied().collect() }; let variation = |rng: &mut Rng, p: &Part| -> Vec { let mut out = keep_out(p); for mut n in inside(p) { if !n.hit { let r = rng.next(); if r < 0.3 { n.note = degree(n.note, if rng.next() < 0.5 { 1 } else { -1 }, root, scale); } else if r < 0.4 { n.note = degree(n.note, if rng.next() < 0.5 { 2 } else { -2 }, root, scale); } else if r < 0.45 { n.note += if n.note < root + 12.0 { 12.0 } else { -12.0 }; } } if rng.next() < 0.15 { // A rest instead (never the first note of a bar). if (n.beat / p.beats_per_bar).fract() > 1e-6 { continue; } } n.vel = (n.vel * (0.85 + 0.3 * rng.next() as f32)).clamp(0.1, 1.0); out.push(n); } out }; Ok(match kind { "variations" => (0..n.max(1)).map(|k| { let (_, p) = one("", &variation, k as u64 * 7919); (format!("Variation {}", k + 1), p) }).collect(), "syncopated" => vec![one("More syncopated", &|rng, p| { let mut out = keep_out(p); let onsets: Vec = p.notes.iter().map(|n| n.beat).collect(); for mut n in inside(p) { let on_beat = (n.beat / (2.0 * g)).fract() < 1e-6; // Pushed only into a step that is free (a dense line stays). let free = !onsets.iter().any(|b| (b - (n.beat - g)).abs() < 1e-6); if on_beat && free && (n.beat / p.beats_per_bar).fract() > 1e-6 && rng.next() < 0.6 { // Pushed: a grid step early, held over the beat. n.beat -= g; n.dur += g as f32; } out.push(n); } out }, 1)], "simpler" => vec![one("Simpler", &|rng, p| { let mut out = keep_out(p); let mut kept: Vec = Vec::new(); for n in inside(p) { let strong = (n.beat).fract() < 1e-6; if strong || rng.next() < 0.35 { kept.push(n); } } // Each note held to the next. for i in 0..kept.len() { if !kept[i].hit { let next = kept.get(i + 1).map_or(p.beats, |m| m.beat); kept[i].dur = ((next - kept[i].beat) * 0.9).max(kept[i].dur as f64) as f32; } } out.extend(kept); out }, 2)], "darker" => vec![one("Darker", &|_rng, p| { let minor = theory::scale_kind("phrygian").unwrap(); let mut out = keep_out(p); for mut n in inside(p) { if !n.hit { // Into the darker mode on the root, the top down an octave. n.note = degree(n.note, 0, root, minor); if n.note > root + 12.0 { n.note -= 12.0; } } n.vel *= 0.85; out.push(n); } out }, 3)], "answer" => vec![one("Answer phrase", &|_rng, p| { // The phrase's contour turned over around its first note, the // last note brought home to the root. let mut out = keep_out(p); let notes = inside(p); let first = notes.iter().find(|n| !n.hit).map_or(root, |n| n.note); let last = notes.iter().rposition(|n| !n.hit); for (i, mut n) in notes.into_iter().enumerate() { if !n.hit { let steps = ((first - n.note) / 2.0).round() as i32; n.note = degree(first, steps, root, scale); if Some(i) == last { n.note = root + 12.0 * ((n.note - root) / 12.0).round(); } } out.push(n); } out }, 4)], "reharmonise" => vec![one("Re-harmonised", &|_rng, p| { // The relative key's mode (minor ↔ major on the same notes moved). let other: &[i32] = if scale == theory::scale_kind("minor").unwrap() { theory::scale_kind("dorian").unwrap() } else { theory::scale_kind("mixolydian").unwrap() }; let mut out = keep_out(p); let moved = inside(p).iter().any(|n| !n.hit && degree(n.note, 0, root, other) != n.note); for mut n in inside(p) { if !n.hit { n.note = if moved { degree(n.note, 0, root, other) } else { // The mode changes nothing here: each bar over another // chord of the key instead (i, iv, vi, v), its shape kept. let bar = (n.beat / p.beats_per_bar).floor() as usize; degree(n.note, [0, 3, 5, 4][bar % 4], root, scale) }; } out.push(n); } out }, 5)], "busier" => vec![one("Busier", &|rng, p| { let mut out = keep_out(p); let notes = inside(p); for (i, n) in notes.iter().enumerate() { out.push(*n); let next = notes.get(i + 1); let gap = next.map_or(p.beats, |m| m.beat) - n.beat; if gap >= 2.0 * g && rng.next() < 0.7 { let mut pass = *n; pass.beat = n.beat + gap / 2.0; pass.dur = (gap / 2.0 * 0.8) as f32; pass.vel = n.vel * 0.7; if !n.hit { let target = next.filter(|m| !m.hit).map_or(n.note, |m| m.note); pass.note = degree(n.note, if target >= n.note { 1 } else { -1 }, root, scale); } out.push(pass); } } out }, 6)], "humanise" => vec![one("Humanised", &|rng, p| { let mut out = keep_out(p); for mut n in inside(p) { n.beat = (n.beat + (rng.next() - 0.5) * 0.04).max(0.0); n.vel = (n.vel * (0.8 + 0.35 * rng.next() as f32)).clamp(0.1, 1.0); out.push(n); } out }, 7)], other => return Err(format!("no variant `{other}` (there are {})", KINDS.iter().map(|k| k.0).collect::>().join(", "))), }) } /// A part written as `[[beat note dur vel] …]` (an AI's answer), read; /// the cycle's length and bar from `like`. pub fn parse_notes(text: &str, like: &Part) -> Result { // The last list (an answer that corrects itself ends with it). let open = text.rfind("[[").ok_or("no `[[beat note dur vel] …]` list")?; let close = open + text[open..].find("]]").ok_or("the list is not closed")? + 2; let body = &text[open + 1..close - 1]; let mut notes = Vec::new(); for item in body.split(']') { let item = item.trim().trim_start_matches(['[', ',', ' ']); if item.is_empty() { continue; } let words: Vec<&str> = item.split([' ', ',']).filter(|w| !w.is_empty()).collect(); if words.len() < 2 { continue; } let beat = words[0].parse::().map_err(|_| format!("`{item}`: a beat is a number"))?; let name = words[1].trim_start_matches('@'); let (note, hit) = match DRUM_SLOTS.iter().position(|s| *s == name) { Some(slot) => (slot as f32, true), None => (theory::note_number(name).or_else(|| name.parse::().ok()).ok_or_else(|| format!("`{name}` is not a note"))?, false), }; let dur = words.get(2).and_then(|w| w.parse::().ok()).unwrap_or(0.25); let vel = words.get(3).and_then(|w| w.parse::().ok()).unwrap_or(0.8); if beat >= 0.0 && beat < like.beats { notes.push(Note { beat, note, dur, vel, hit }); } } if notes.is_empty() { return Err("the answer has no notes".into()); } notes.sort_by(|a, b| a.beat.total_cmp(&b.beat)); Ok(Part { notes, ..like.clone() }) }