Skip to main content

sim_lib_music_transform/
mutator.rs

1use std::collections::BTreeSet;
2
3use sim_lib_music_core::{Music, MusicObject, Time, TimedNote};
4use sim_lib_pitch_core::Pitch;
5use sim_lib_pitch_scale::Scale;
6use thiserror::Error;
7
8use crate::{TransformError, canonical_roll, to_piano_roll};
9
10mod ops;
11mod rng;
12mod wire;
13
14use ops::{apply_op, restore_locks};
15use rng::PatternRng;
16use wire::{op_wire, parse_number, parse_op};
17
18/// Error raised while parsing or validating a pattern mutator.
19#[derive(Debug, Error, Clone, PartialEq, Eq)]
20pub enum PatternMutatorError {
21    /// The wire string was not a valid pattern mutator encoding.
22    #[error("invalid pattern mutator wire format")]
23    InvalidWire,
24    /// A numeric field could not be parsed.
25    #[error("invalid pattern mutator number")]
26    InvalidNumber,
27    /// A scale mode name was not recognized.
28    #[error("invalid pattern mutator mode: {0}")]
29    InvalidMode(String),
30    /// A pitch class value was out of range.
31    #[error("invalid pattern mutator pitch class: {0}")]
32    InvalidPitchClass(u8),
33}
34
35/// Set of source note indices held fixed (locked) during mutation.
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct PatternLockSet {
38    note_indices: BTreeSet<usize>,
39}
40
41impl PatternLockSet {
42    /// Builds a lock set from a collection of source note indices.
43    pub fn from_note_indices(indices: impl IntoIterator<Item = usize>) -> Self {
44        Self {
45            note_indices: indices.into_iter().collect(),
46        }
47    }
48
49    /// Returns whether the given source index is locked.
50    pub fn contains(&self, index: usize) -> bool {
51        self.note_indices.contains(&index)
52    }
53
54    /// Returns the set of locked source note indices.
55    pub fn note_indices(&self) -> &BTreeSet<usize> {
56        &self.note_indices
57    }
58}
59
60/// A single mutation operation applied to a pattern's notes.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub enum MutationOp {
63    /// Reverse note onsets within the pattern span.
64    Reverse,
65    /// Rotate notes across their distinct onset slots by `steps`.
66    Rotate {
67        /// Number of slots to rotate (signed).
68        steps: i32,
69    },
70    /// Transpose unlocked notes by `semitones`.
71    Transpose {
72        /// Semitone offset.
73        semitones: i32,
74    },
75    /// Invert unlocked notes about `axis`.
76    Invert {
77        /// Inversion axis pitch.
78        axis: Pitch,
79    },
80    /// Shuffle note onsets within each beat-sized bucket.
81    ShuffleWithinBeat {
82        /// Bucket width in beats.
83        beat: Time,
84    },
85    /// Randomly drop notes, keeping roughly `keep_percent` of them.
86    Thin {
87        /// Target percentage of notes to keep.
88        keep_percent: u8,
89    },
90    /// Duplicate notes transposed by `semitones` to thicken the texture.
91    Thicken {
92        /// Semitone offset of the added copies.
93        semitones: i32,
94    },
95    /// Remap velocities into the `[low, high]` range.
96    VelocityRemap {
97        /// Lower velocity bound.
98        low: u8,
99        /// Upper velocity bound.
100        high: u8,
101    },
102    /// Displace note onsets forward or backward by `offset`.
103    RhythmDisplace {
104        /// Displacement magnitude.
105        offset: Time,
106    },
107    /// Conform note pitches to the nearest tone of `scale`.
108    ScaleConform {
109        /// Scale notes are conformed to.
110        scale: Scale,
111    },
112}
113
114/// Configuration describing a sequence of pattern mutations and their controls.
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct PatternMutatorConfig {
117    /// Operations applied in order.
118    pub operations: Vec<MutationOp>,
119    /// Strength of each operation, from 0 to 100.
120    pub amount: u8,
121    /// Seed for the deterministic pseudo-random generator.
122    pub seed: u64,
123    /// Source notes held fixed across all operations.
124    pub locks: PatternLockSet,
125}
126
127impl PatternMutatorConfig {
128    /// Builds a config from operations with default amount, seed, and locks.
129    pub fn new(operations: Vec<MutationOp>) -> Self {
130        Self {
131            operations,
132            amount: 100,
133            seed: 0,
134            locks: PatternLockSet::default(),
135        }
136    }
137
138    /// Sets the mutation strength, clamped to at most 100.
139    pub fn with_amount(mut self, amount: u8) -> Self {
140        self.amount = amount.min(100);
141        self
142    }
143
144    /// Sets the random seed.
145    pub fn with_seed(mut self, seed: u64) -> Self {
146        self.seed = seed;
147        self
148    }
149
150    /// Sets the locked note set.
151    pub fn with_locks(mut self, locks: PatternLockSet) -> Self {
152        self.locks = locks;
153        self
154    }
155
156    /// Applies the configured mutations to the input and returns the result.
157    pub fn apply(&self, object: &dyn MusicObject) -> Result<Music, TransformError> {
158        mutate_pattern(object, self)
159    }
160
161    /// Serializes this config to its `pattern-mutator|...` wire string.
162    pub fn to_wire(&self) -> String {
163        let locks = self
164            .locks
165            .note_indices()
166            .iter()
167            .map(usize::to_string)
168            .collect::<Vec<_>>()
169            .join(",");
170        let ops = self
171            .operations
172            .iter()
173            .map(op_wire)
174            .collect::<Vec<_>>()
175            .join(";");
176        format!(
177            "pattern-mutator|amount={}|seed={}|locks={}|ops={}",
178            self.amount, self.seed, locks, ops
179        )
180    }
181
182    /// Parses a config from its wire string, validating each field.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use sim_lib_music_transform::{MutationOp, PatternMutatorConfig};
188    ///
189    /// let config = PatternMutatorConfig::new(vec![MutationOp::Reverse]).with_amount(80);
190    /// let wire = config.to_wire();
191    /// assert_eq!(PatternMutatorConfig::from_wire(&wire), Ok(config));
192    /// ```
193    pub fn from_wire(value: &str) -> Result<Self, PatternMutatorError> {
194        let Some(rest) = value.strip_prefix("pattern-mutator|") else {
195            return Err(PatternMutatorError::InvalidWire);
196        };
197        let mut amount = 100;
198        let mut seed = 0;
199        let mut locks = PatternLockSet::default();
200        let mut operations = Vec::new();
201
202        for part in rest.split('|') {
203            let (key, value) = part
204                .split_once('=')
205                .ok_or(PatternMutatorError::InvalidWire)?;
206            match key {
207                "amount" => amount = parse_number::<u8>(value)?.min(100),
208                "seed" => seed = parse_number(value)?,
209                "locks" if value.is_empty() => locks = PatternLockSet::default(),
210                "locks" => {
211                    locks = PatternLockSet::from_note_indices(
212                        value
213                            .split(',')
214                            .map(parse_number)
215                            .collect::<Result<Vec<_>, _>>()?,
216                    )
217                }
218                "ops" if value.is_empty() => operations = Vec::new(),
219                "ops" => {
220                    operations = value
221                        .split(';')
222                        .map(parse_op)
223                        .collect::<Result<Vec<_>, _>>()?
224                }
225                _ => return Err(PatternMutatorError::InvalidWire),
226            }
227        }
228
229        Ok(Self {
230            operations,
231            amount,
232            seed,
233            locks,
234        })
235    }
236}
237
238/// Applies a [`PatternMutatorConfig`] to material and returns the mutated music.
239pub fn mutate_pattern(
240    object: &dyn MusicObject,
241    config: &PatternMutatorConfig,
242) -> Result<Music, TransformError> {
243    let original = to_piano_roll(object)?
244        .items
245        .into_iter()
246        .enumerate()
247        .map(|(source_index, item)| PatternNote { source_index, item })
248        .collect::<Vec<_>>();
249    let mut notes = original.clone();
250    let mut rng = PatternRng::new(config.seed);
251    let mut next_source_index = original.len();
252
253    for op in &config.operations {
254        apply_op(
255            &mut notes,
256            op,
257            config.amount,
258            &config.locks,
259            &mut rng,
260            &mut next_source_index,
261        )?;
262        restore_locks(&mut notes, &original, &config.locks);
263    }
264
265    Ok(Music::PianoRoll(canonical_roll(
266        notes.into_iter().map(|note| note.item).collect(),
267    )?))
268}
269
270#[derive(Clone, Debug, PartialEq, Eq)]
271struct PatternNote {
272    source_index: usize,
273    item: TimedNote,
274}