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#[derive(Debug, Error, Clone, PartialEq, Eq)]
16pub enum PatternMutatorError {
17 #[error("invalid pattern mutator wire format")]
19 InvalidWire,
20 #[error("invalid pattern mutator number")]
22 InvalidNumber,
23 #[error("invalid pattern mutator mode: {0}")]
25 InvalidMode(String),
26 #[error("invalid pattern mutator pitch class: {0}")]
28 InvalidPitchClass(u8),
29}
30
31#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub struct PatternLockSet {
34 note_indices: BTreeSet<usize>,
35}
36
37impl PatternLockSet {
38 pub fn from_note_indices(indices: impl IntoIterator<Item = usize>) -> Self {
40 Self {
41 note_indices: indices.into_iter().collect(),
42 }
43 }
44
45 pub fn contains(&self, index: usize) -> bool {
47 self.note_indices.contains(&index)
48 }
49
50 pub fn note_indices(&self) -> &BTreeSet<usize> {
52 &self.note_indices
53 }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum MutationOp {
59 Reverse,
61 Rotate {
63 steps: i32,
65 },
66 Transpose {
68 semitones: i32,
70 },
71 Invert {
73 axis: Pitch,
75 },
76 ShuffleWithinBeat {
78 beat: Time,
80 },
81 Thin {
83 keep_percent: u8,
85 },
86 Thicken {
88 semitones: i32,
90 },
91 VelocityRemap {
93 low: u8,
95 high: u8,
97 },
98 RhythmDisplace {
100 offset: Time,
102 },
103 ScaleConform {
105 scale: Scale,
107 },
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct PatternMutatorConfig {
113 pub operations: Vec<MutationOp>,
115 pub amount: u8,
117 pub seed: u64,
119 pub locks: PatternLockSet,
121}
122
123impl PatternMutatorConfig {
124 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 pub fn with_amount(mut self, amount: u8) -> Self {
136 self.amount = amount.min(100);
137 self
138 }
139
140 pub fn with_seed(mut self, seed: u64) -> Self {
142 self.seed = seed;
143 self
144 }
145
146 pub fn with_locks(mut self, locks: PatternLockSet) -> Self {
148 self.locks = locks;
149 self
150 }
151
152 pub fn apply(&self, object: &dyn MusicObject) -> Music {
154 mutate_pattern(object, self)
155 }
156
157 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 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
234pub 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(¬e.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}