Skip to main content

sim_lib_music_transform/
mutator.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::num::ParseIntError;
3
4use sim_lib_music_core::{Music, MusicObject, Time, TimedNote};
5use sim_lib_pitch_core::{Pitch, PitchClass};
6use sim_lib_pitch_scale::{Mode, Scale};
7use thiserror::Error;
8
9use crate::{
10    RetrogradeMode, canonical_roll, chord_tones_in, pitch_invert, retrograde_with_mode,
11    to_piano_roll, transpose,
12};
13
14/// Error raised while parsing or validating a pattern mutator.
15#[derive(Debug, Error, Clone, PartialEq, Eq)]
16pub enum PatternMutatorError {
17    /// The wire string was not a valid pattern mutator encoding.
18    #[error("invalid pattern mutator wire format")]
19    InvalidWire,
20    /// A numeric field could not be parsed.
21    #[error("invalid pattern mutator number")]
22    InvalidNumber,
23    /// A scale mode name was not recognized.
24    #[error("invalid pattern mutator mode: {0}")]
25    InvalidMode(String),
26    /// A pitch class value was out of range.
27    #[error("invalid pattern mutator pitch class: {0}")]
28    InvalidPitchClass(u8),
29}
30
31/// Set of source note indices held fixed (locked) during mutation.
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub struct PatternLockSet {
34    note_indices: BTreeSet<usize>,
35}
36
37impl PatternLockSet {
38    /// Builds a lock set from a collection of source note indices.
39    pub fn from_note_indices(indices: impl IntoIterator<Item = usize>) -> Self {
40        Self {
41            note_indices: indices.into_iter().collect(),
42        }
43    }
44
45    /// Returns whether the given source index is locked.
46    pub fn contains(&self, index: usize) -> bool {
47        self.note_indices.contains(&index)
48    }
49
50    /// Returns the set of locked source note indices.
51    pub fn note_indices(&self) -> &BTreeSet<usize> {
52        &self.note_indices
53    }
54}
55
56/// A single mutation operation applied to a pattern's notes.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum MutationOp {
59    /// Reverse note onsets within the pattern span.
60    Reverse,
61    /// Rotate notes across their distinct onset slots by `steps`.
62    Rotate {
63        /// Number of slots to rotate (signed).
64        steps: i32,
65    },
66    /// Transpose unlocked notes by `semitones`.
67    Transpose {
68        /// Semitone offset.
69        semitones: i32,
70    },
71    /// Invert unlocked notes about `axis`.
72    Invert {
73        /// Inversion axis pitch.
74        axis: Pitch,
75    },
76    /// Shuffle note onsets within each beat-sized bucket.
77    ShuffleWithinBeat {
78        /// Bucket width in beats.
79        beat: Time,
80    },
81    /// Randomly drop notes, keeping roughly `keep_percent` of them.
82    Thin {
83        /// Target percentage of notes to keep.
84        keep_percent: u8,
85    },
86    /// Duplicate notes transposed by `semitones` to thicken the texture.
87    Thicken {
88        /// Semitone offset of the added copies.
89        semitones: i32,
90    },
91    /// Remap velocities into the `[low, high]` range.
92    VelocityRemap {
93        /// Lower velocity bound.
94        low: u8,
95        /// Upper velocity bound.
96        high: u8,
97    },
98    /// Displace note onsets forward or backward by `offset`.
99    RhythmDisplace {
100        /// Displacement magnitude.
101        offset: Time,
102    },
103    /// Conform note pitches to the nearest tone of `scale`.
104    ScaleConform {
105        /// Scale notes are conformed to.
106        scale: Scale,
107    },
108}
109
110/// Configuration describing a sequence of pattern mutations and their controls.
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct PatternMutatorConfig {
113    /// Operations applied in order.
114    pub operations: Vec<MutationOp>,
115    /// Strength of each operation, from 0 to 100.
116    pub amount: u8,
117    /// Seed for the deterministic pseudo-random generator.
118    pub seed: u64,
119    /// Source notes held fixed across all operations.
120    pub locks: PatternLockSet,
121}
122
123impl PatternMutatorConfig {
124    /// Builds a config from operations with default amount, seed, and locks.
125    pub fn new(operations: Vec<MutationOp>) -> Self {
126        Self {
127            operations,
128            amount: 100,
129            seed: 0,
130            locks: PatternLockSet::default(),
131        }
132    }
133
134    /// Sets the mutation strength, clamped to at most 100.
135    pub fn with_amount(mut self, amount: u8) -> Self {
136        self.amount = amount.min(100);
137        self
138    }
139
140    /// Sets the random seed.
141    pub fn with_seed(mut self, seed: u64) -> Self {
142        self.seed = seed;
143        self
144    }
145
146    /// Sets the locked note set.
147    pub fn with_locks(mut self, locks: PatternLockSet) -> Self {
148        self.locks = locks;
149        self
150    }
151
152    /// Applies the configured mutations to the input and returns the result.
153    pub fn apply(&self, object: &dyn MusicObject) -> Music {
154        mutate_pattern(object, self)
155    }
156
157    /// Serializes this config to its `pattern-mutator|...` wire string.
158    pub fn to_wire(&self) -> String {
159        let locks = self
160            .locks
161            .note_indices()
162            .iter()
163            .map(usize::to_string)
164            .collect::<Vec<_>>()
165            .join(",");
166        let ops = self
167            .operations
168            .iter()
169            .map(op_wire)
170            .collect::<Vec<_>>()
171            .join(";");
172        format!(
173            "pattern-mutator|amount={}|seed={}|locks={}|ops={}",
174            self.amount, self.seed, locks, ops
175        )
176    }
177
178    /// Parses a config from its wire string, validating each field.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use sim_lib_music_transform::{MutationOp, PatternMutatorConfig};
184    ///
185    /// let config = PatternMutatorConfig::new(vec![MutationOp::Reverse]).with_amount(80);
186    /// let wire = config.to_wire();
187    /// assert_eq!(PatternMutatorConfig::from_wire(&wire), Ok(config));
188    /// ```
189    pub fn from_wire(value: &str) -> Result<Self, PatternMutatorError> {
190        let Some(rest) = value.strip_prefix("pattern-mutator|") else {
191            return Err(PatternMutatorError::InvalidWire);
192        };
193        let mut amount = 100;
194        let mut seed = 0;
195        let mut locks = PatternLockSet::default();
196        let mut operations = Vec::new();
197
198        for part in rest.split('|') {
199            let (key, value) = part
200                .split_once('=')
201                .ok_or(PatternMutatorError::InvalidWire)?;
202            match key {
203                "amount" => amount = parse_number::<u8>(value)?.min(100),
204                "seed" => seed = parse_number(value)?,
205                "locks" if value.is_empty() => locks = PatternLockSet::default(),
206                "locks" => {
207                    locks = PatternLockSet::from_note_indices(
208                        value
209                            .split(',')
210                            .map(parse_number)
211                            .collect::<Result<Vec<_>, _>>()?,
212                    )
213                }
214                "ops" if value.is_empty() => operations = Vec::new(),
215                "ops" => {
216                    operations = value
217                        .split(';')
218                        .map(parse_op)
219                        .collect::<Result<Vec<_>, _>>()?
220                }
221                _ => return Err(PatternMutatorError::InvalidWire),
222            }
223        }
224
225        Ok(Self {
226            operations,
227            amount,
228            seed,
229            locks,
230        })
231    }
232}
233
234/// Applies a [`PatternMutatorConfig`] to material and returns the mutated music.
235pub fn mutate_pattern(object: &dyn MusicObject, config: &PatternMutatorConfig) -> Music {
236    let original = to_piano_roll(object)
237        .items
238        .into_iter()
239        .enumerate()
240        .map(|(source_index, item)| PatternNote { source_index, item })
241        .collect::<Vec<_>>();
242    let mut notes = original.clone();
243    let mut rng = PatternRng::new(config.seed);
244    let mut next_source_index = original.len();
245
246    for op in &config.operations {
247        apply_op(
248            &mut notes,
249            op,
250            config.amount,
251            &config.locks,
252            &mut rng,
253            &mut next_source_index,
254        );
255        restore_locks(&mut notes, &original, &config.locks);
256    }
257
258    Music::PianoRoll(canonical_roll(
259        notes.into_iter().map(|note| note.item).collect(),
260    ))
261}
262
263#[derive(Clone, Debug, PartialEq, Eq)]
264struct PatternNote {
265    source_index: usize,
266    item: TimedNote,
267}
268
269fn apply_op(
270    notes: &mut Vec<PatternNote>,
271    op: &MutationOp,
272    amount: u8,
273    locks: &PatternLockSet,
274    rng: &mut PatternRng,
275    next_source_index: &mut usize,
276) {
277    if amount == 0 && !matches!(op, MutationOp::Thin { .. }) {
278        return;
279    }
280    match op {
281        MutationOp::Reverse => apply_reverse(notes, locks),
282        MutationOp::Rotate { steps } => apply_rotate(notes, scaled_i32(*steps, amount), locks),
283        MutationOp::Transpose { semitones } => {
284            let semitones = scaled_i32(*semitones, amount);
285            if semitones != 0 {
286                transform_unlocked(notes, locks, |item| {
287                    transform_single(item, |object| transpose(object, semitones))
288                });
289            }
290        }
291        MutationOp::Invert { axis } => {
292            transform_unlocked(notes, locks, |item| {
293                transform_single(item, |object| pitch_invert(object, *axis))
294            });
295        }
296        MutationOp::ShuffleWithinBeat { beat } => {
297            if *beat > Time::from_integer(0) {
298                apply_shuffle_within_beat(notes, *beat, amount, locks, rng);
299            }
300        }
301        MutationOp::Thin { keep_percent } => {
302            apply_thin(notes, effective_keep(*keep_percent, amount), locks, rng);
303        }
304        MutationOp::Thicken { semitones } => {
305            let semitones = scaled_i32(*semitones, amount);
306            if semitones != 0 {
307                apply_thicken(notes, semitones, amount, locks, rng, next_source_index);
308            }
309        }
310        MutationOp::VelocityRemap { low, high } => {
311            apply_velocity_remap(notes, *low, *high, amount, locks, rng);
312        }
313        MutationOp::RhythmDisplace { offset } => {
314            let offset = scaled_time(*offset, amount);
315            if offset != Time::from_integer(0) {
316                apply_rhythm_displace(notes, offset, locks, rng);
317            }
318        }
319        MutationOp::ScaleConform { scale } => {
320            transform_unlocked(notes, locks, |item| {
321                transform_single(item, |object| chord_tones_in(object, scale))
322            });
323        }
324    }
325}
326
327fn apply_reverse(notes: &mut [PatternNote], locks: &PatternLockSet) {
328    let span = pattern_span(notes);
329    let _algebra = retrograde_with_mode(&pattern_music(notes), RetrogradeMode::Cutout);
330    for note in notes {
331        if !locks.contains(note.source_index) {
332            note.item.onset = span - note.item.onset - note.item.note.duration;
333        }
334    }
335}
336
337fn apply_rotate(notes: &mut [PatternNote], steps: i32, locks: &PatternLockSet) {
338    if steps == 0 {
339        return;
340    }
341    let mut slots = notes.iter().map(|note| note.item.onset).collect::<Vec<_>>();
342    slots.sort();
343    slots.dedup();
344    if slots.len() < 2 {
345        return;
346    }
347    for note in notes {
348        if locks.contains(note.source_index) {
349            continue;
350        }
351        let Ok(index) = slots.binary_search(&note.item.onset) else {
352            continue;
353        };
354        let target = (index as i32 + steps).rem_euclid(slots.len() as i32) as usize;
355        note.item.onset = slots[target];
356    }
357}
358
359fn apply_shuffle_within_beat(
360    notes: &mut [PatternNote],
361    beat: Time,
362    amount: u8,
363    locks: &PatternLockSet,
364    rng: &mut PatternRng,
365) {
366    let mut groups: BTreeMap<i64, Vec<usize>> = BTreeMap::new();
367    for (index, note) in notes.iter().enumerate() {
368        if !locks.contains(note.source_index) {
369            groups
370                .entry(time_bucket(note.item.onset, beat))
371                .or_default()
372                .push(index);
373        }
374    }
375    for group in groups.values() {
376        if group.len() < 2 || !rng.chance(amount) {
377            continue;
378        }
379        let mut onsets = group
380            .iter()
381            .map(|index| notes[*index].item.onset)
382            .collect::<Vec<_>>();
383        rng.shuffle(&mut onsets);
384        for (index, onset) in group.iter().zip(onsets) {
385            notes[*index].item.onset = onset;
386        }
387    }
388}
389
390fn apply_thin(
391    notes: &mut Vec<PatternNote>,
392    keep_percent: u8,
393    locks: &PatternLockSet,
394    rng: &mut PatternRng,
395) {
396    notes.retain(|note| locks.contains(note.source_index) || rng.chance(keep_percent));
397}
398
399fn apply_thicken(
400    notes: &mut Vec<PatternNote>,
401    semitones: i32,
402    amount: u8,
403    locks: &PatternLockSet,
404    rng: &mut PatternRng,
405    next_source_index: &mut usize,
406) {
407    let mut extras = Vec::new();
408    for note in notes.iter() {
409        if locks.contains(note.source_index) || !rng.chance(amount) {
410            continue;
411        }
412        let item = transform_single(note.item.clone(), |object| transpose(object, semitones));
413        extras.push(PatternNote {
414            source_index: *next_source_index,
415            item,
416        });
417        *next_source_index += 1;
418    }
419    notes.extend(extras);
420}
421
422fn apply_velocity_remap(
423    notes: &mut [PatternNote],
424    low: u8,
425    high: u8,
426    amount: u8,
427    locks: &PatternLockSet,
428    rng: &mut PatternRng,
429) {
430    let (low, high) = if low <= high {
431        (low, high)
432    } else {
433        (high, low)
434    };
435    let width = usize::from(high - low) + 1;
436    for note in notes {
437        if locks.contains(note.source_index) {
438            continue;
439        }
440        let target = low + rng.range(width) as u8;
441        note.item.note.velocity = blend_u8(note.item.note.velocity, target, amount);
442    }
443}
444
445fn apply_rhythm_displace(
446    notes: &mut [PatternNote],
447    offset: Time,
448    locks: &PatternLockSet,
449    rng: &mut PatternRng,
450) {
451    for note in notes {
452        if locks.contains(note.source_index) {
453            continue;
454        }
455        let moved = if rng.next_bool() {
456            note.item.onset + offset
457        } else {
458            note.item.onset - offset
459        };
460        note.item.onset = moved.max(Time::from_integer(0));
461    }
462}
463
464fn transform_unlocked(
465    notes: &mut [PatternNote],
466    locks: &PatternLockSet,
467    mut transform: impl FnMut(TimedNote) -> TimedNote,
468) {
469    for note in notes {
470        if !locks.contains(note.source_index) {
471            note.item = transform(note.item.clone());
472        }
473    }
474}
475
476fn restore_locks(notes: &mut Vec<PatternNote>, original: &[PatternNote], locks: &PatternLockSet) {
477    for original_note in original {
478        if !locks.contains(original_note.source_index) {
479            continue;
480        }
481        match notes
482            .iter_mut()
483            .find(|note| note.source_index == original_note.source_index)
484        {
485            Some(note) => note.item = original_note.item.clone(),
486            None => notes.push(original_note.clone()),
487        }
488    }
489}
490
491fn transform_single(
492    item: TimedNote,
493    transform: impl FnOnce(&dyn MusicObject) -> Music,
494) -> TimedNote {
495    let music = Music::PianoRoll(canonical_roll(vec![item]));
496    let Music::PianoRoll(mut roll) = transform(&music) else {
497        unreachable!("music transform returns a piano roll")
498    };
499    roll.items.remove(0)
500}
501
502fn pattern_music(notes: &[PatternNote]) -> Music {
503    Music::PianoRoll(canonical_roll(
504        notes.iter().map(|note| note.item.clone()).collect(),
505    ))
506}
507
508fn pattern_span(notes: &[PatternNote]) -> Time {
509    notes
510        .iter()
511        .map(|note| note.item.onset + note.item.note.duration)
512        .max()
513        .unwrap_or_else(|| Time::from_integer(0))
514}
515
516fn scaled_i32(value: i32, amount: u8) -> i32 {
517    let scaled = value * i32::from(amount) / 100;
518    if scaled == 0 && value != 0 && amount > 0 {
519        value.signum()
520    } else {
521        scaled
522    }
523}
524
525fn scaled_time(value: Time, amount: u8) -> Time {
526    value * Time::new(i64::from(amount), 100)
527}
528
529fn effective_keep(keep_percent: u8, amount: u8) -> u8 {
530    let keep_percent = keep_percent.min(100);
531    let remove = 100 - keep_percent;
532    100 - ((u16::from(remove) * u16::from(amount) / 100) as u8)
533}
534
535fn blend_u8(source: u8, target: u8, amount: u8) -> u8 {
536    let source = u16::from(source);
537    let target = u16::from(target);
538    let amount = u16::from(amount);
539    ((source * (100 - amount) + target * amount) / 100) as u8
540}
541
542fn time_bucket(onset: Time, beat: Time) -> i64 {
543    let ratio = onset / beat;
544    (*ratio.numer()).div_euclid(*ratio.denom())
545}
546
547fn op_wire(op: &MutationOp) -> String {
548    match op {
549        MutationOp::Reverse => "reverse".to_owned(),
550        MutationOp::Rotate { steps } => format!("rotate:{steps}"),
551        MutationOp::Transpose { semitones } => format!("transpose:{semitones}"),
552        MutationOp::Invert { axis } => format!("invert:{}", axis.semitone()),
553        MutationOp::ShuffleWithinBeat { beat } => format!("shuffle:{}", time_wire(*beat)),
554        MutationOp::Thin { keep_percent } => format!("thin:{keep_percent}"),
555        MutationOp::Thicken { semitones } => format!("thicken:{semitones}"),
556        MutationOp::VelocityRemap { low, high } => format!("velocity:{low}:{high}"),
557        MutationOp::RhythmDisplace { offset } => format!("rhythm:{}", time_wire(*offset)),
558        MutationOp::ScaleConform { scale } => {
559            format!("scale:{}:{}", scale.tonic.0, scale.mode.name())
560        }
561    }
562}
563
564fn parse_op(value: &str) -> Result<MutationOp, PatternMutatorError> {
565    let mut parts = value.split(':');
566    match parts.next().ok_or(PatternMutatorError::InvalidWire)? {
567        "reverse" => Ok(MutationOp::Reverse),
568        "rotate" => Ok(MutationOp::Rotate {
569            steps: parse_required(parts.next())?,
570        }),
571        "transpose" => Ok(MutationOp::Transpose {
572            semitones: parse_required(parts.next())?,
573        }),
574        "invert" => Ok(MutationOp::Invert {
575            axis: Pitch::from_semitone(parse_required(parts.next())?),
576        }),
577        "shuffle" => Ok(MutationOp::ShuffleWithinBeat {
578            beat: parse_time(parts.next().ok_or(PatternMutatorError::InvalidWire)?)?,
579        }),
580        "thin" => Ok(MutationOp::Thin {
581            keep_percent: parse_required(parts.next())?,
582        }),
583        "thicken" => Ok(MutationOp::Thicken {
584            semitones: parse_required(parts.next())?,
585        }),
586        "velocity" => Ok(MutationOp::VelocityRemap {
587            low: parse_required(parts.next())?,
588            high: parse_required(parts.next())?,
589        }),
590        "rhythm" => Ok(MutationOp::RhythmDisplace {
591            offset: parse_time(parts.next().ok_or(PatternMutatorError::InvalidWire)?)?,
592        }),
593        "scale" => {
594            let tonic = parse_required(parts.next())?;
595            let mode = parse_mode(parts.next().ok_or(PatternMutatorError::InvalidWire)?)?;
596            let tonic = PitchClass::new(tonic)
597                .map_err(|_| PatternMutatorError::InvalidPitchClass(tonic))?;
598            Ok(MutationOp::ScaleConform {
599                scale: Scale::new(tonic, mode),
600            })
601        }
602        _ => Err(PatternMutatorError::InvalidWire),
603    }
604}
605
606fn parse_required<T: std::str::FromStr>(value: Option<&str>) -> Result<T, PatternMutatorError>
607where
608    PatternMutatorError: From<<T as std::str::FromStr>::Err>,
609{
610    value
611        .ok_or(PatternMutatorError::InvalidWire)?
612        .parse()
613        .map_err(PatternMutatorError::from)
614}
615
616fn parse_number<T: std::str::FromStr>(value: &str) -> Result<T, PatternMutatorError>
617where
618    PatternMutatorError: From<<T as std::str::FromStr>::Err>,
619{
620    value.parse().map_err(PatternMutatorError::from)
621}
622
623impl From<ParseIntError> for PatternMutatorError {
624    fn from(_: ParseIntError) -> Self {
625        Self::InvalidNumber
626    }
627}
628
629fn time_wire(time: Time) -> String {
630    format!("{}/{}", time.numer(), time.denom())
631}
632
633fn parse_time(value: &str) -> Result<Time, PatternMutatorError> {
634    let (numer, denom) = value
635        .split_once('/')
636        .ok_or(PatternMutatorError::InvalidWire)?;
637    Ok(Time::new(parse_number(numer)?, parse_number(denom)?))
638}
639
640fn parse_mode(value: &str) -> Result<Mode, PatternMutatorError> {
641    match value {
642        "major" => Ok(Mode::Major),
643        "minor-natural" => Ok(Mode::MinorNatural),
644        "minor-harmonic" => Ok(Mode::MinorHarmonic),
645        "minor-melodic" => Ok(Mode::MinorMelodic),
646        "dorian" => Ok(Mode::Dorian),
647        "phrygian" => Ok(Mode::Phrygian),
648        "lydian" => Ok(Mode::Lydian),
649        "mixolydian" => Ok(Mode::Mixolydian),
650        "aeolian" => Ok(Mode::Aeolian),
651        "locrian" => Ok(Mode::Locrian),
652        "whole-tone" => Ok(Mode::WholeTone),
653        "diminished" => Ok(Mode::Diminished),
654        "chromatic" => Ok(Mode::Chromatic),
655        _ => Err(PatternMutatorError::InvalidMode(value.to_owned())),
656    }
657}
658
659#[derive(Clone, Debug)]
660struct PatternRng {
661    state: u64,
662}
663
664impl PatternRng {
665    fn new(seed: u64) -> Self {
666        Self {
667            state: seed ^ 0x9e37_79b9_7f4a_7c15,
668        }
669    }
670
671    fn next_u64(&mut self) -> u64 {
672        self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
673        let mut value = self.state;
674        value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
675        value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
676        value ^ (value >> 31)
677    }
678
679    fn next_bool(&mut self) -> bool {
680        self.next_u64() & 1 == 1
681    }
682
683    fn range(&mut self, upper: usize) -> usize {
684        if upper == 0 {
685            0
686        } else {
687            (self.next_u64() as usize) % upper
688        }
689    }
690
691    fn chance(&mut self, percent: u8) -> bool {
692        percent >= 100 || (percent > 0 && self.range(100) < usize::from(percent))
693    }
694
695    fn shuffle<T>(&mut self, values: &mut [T]) {
696        for index in (1..values.len()).rev() {
697            let swap_with = self.range(index + 1);
698            values.swap(index, swap_with);
699        }
700    }
701}