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#[derive(Debug, Error, Clone, PartialEq, Eq)]
20pub enum PatternMutatorError {
21 #[error("invalid pattern mutator wire format")]
23 InvalidWire,
24 #[error("invalid pattern mutator number")]
26 InvalidNumber,
27 #[error("invalid pattern mutator mode: {0}")]
29 InvalidMode(String),
30 #[error("invalid pattern mutator pitch class: {0}")]
32 InvalidPitchClass(u8),
33}
34
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct PatternLockSet {
38 note_indices: BTreeSet<usize>,
39}
40
41impl PatternLockSet {
42 pub fn from_note_indices(indices: impl IntoIterator<Item = usize>) -> Self {
44 Self {
45 note_indices: indices.into_iter().collect(),
46 }
47 }
48
49 pub fn contains(&self, index: usize) -> bool {
51 self.note_indices.contains(&index)
52 }
53
54 pub fn note_indices(&self) -> &BTreeSet<usize> {
56 &self.note_indices
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub enum MutationOp {
63 Reverse,
65 Rotate {
67 steps: i32,
69 },
70 Transpose {
72 semitones: i32,
74 },
75 Invert {
77 axis: Pitch,
79 },
80 ShuffleWithinBeat {
82 beat: Time,
84 },
85 Thin {
87 keep_percent: u8,
89 },
90 Thicken {
92 semitones: i32,
94 },
95 VelocityRemap {
97 low: u8,
99 high: u8,
101 },
102 RhythmDisplace {
104 offset: Time,
106 },
107 ScaleConform {
109 scale: Scale,
111 },
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct PatternMutatorConfig {
117 pub operations: Vec<MutationOp>,
119 pub amount: u8,
121 pub seed: u64,
123 pub locks: PatternLockSet,
125}
126
127impl PatternMutatorConfig {
128 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 pub fn with_amount(mut self, amount: u8) -> Self {
140 self.amount = amount.min(100);
141 self
142 }
143
144 pub fn with_seed(mut self, seed: u64) -> Self {
146 self.seed = seed;
147 self
148 }
149
150 pub fn with_locks(mut self, locks: PatternLockSet) -> Self {
152 self.locks = locks;
153 self
154 }
155
156 pub fn apply(&self, object: &dyn MusicObject) -> Result<Music, TransformError> {
158 mutate_pattern(object, self)
159 }
160
161 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 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
238pub 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}