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