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::{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};
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PatternMutatorError {
#[error("invalid pattern mutator wire format")]
InvalidWire,
#[error("invalid pattern mutator number")]
InvalidNumber,
#[error("invalid pattern mutator mode: {0}")]
InvalidMode(String),
#[error("invalid pattern mutator pitch class: {0}")]
InvalidPitchClass(u8),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PatternLockSet {
note_indices: BTreeSet<usize>,
}
impl PatternLockSet {
pub fn from_note_indices(indices: impl IntoIterator<Item = usize>) -> Self {
Self {
note_indices: indices.into_iter().collect(),
}
}
pub fn contains(&self, index: usize) -> bool {
self.note_indices.contains(&index)
}
pub fn note_indices(&self) -> &BTreeSet<usize> {
&self.note_indices
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MutationOp {
Reverse,
Rotate {
steps: i32,
},
Transpose {
semitones: i32,
},
Invert {
axis: Pitch,
},
ShuffleWithinBeat {
beat: Time,
},
Thin {
keep_percent: u8,
},
Thicken {
semitones: i32,
},
VelocityRemap {
low: u8,
high: u8,
},
RhythmDisplace {
offset: Time,
},
ScaleConform {
scale: Scale,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PatternMutatorConfig {
pub operations: Vec<MutationOp>,
pub amount: u8,
pub seed: u64,
pub locks: PatternLockSet,
}
impl PatternMutatorConfig {
pub fn new(operations: Vec<MutationOp>) -> Self {
Self {
operations,
amount: 100,
seed: 0,
locks: PatternLockSet::default(),
}
}
pub fn with_amount(mut self, amount: u8) -> Self {
self.amount = amount.min(100);
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_locks(mut self, locks: PatternLockSet) -> Self {
self.locks = locks;
self
}
pub fn apply(&self, object: &dyn MusicObject) -> Music {
mutate_pattern(object, self)
}
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
)
}
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,
})
}
}
pub fn mutate_pattern(object: &dyn MusicObject, config: &PatternMutatorConfig) -> Music {
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);
}
Music::PianoRoll(canonical_roll(
notes.into_iter().map(|note| note.item).collect(),
))
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct PatternNote {
source_index: usize,
item: TimedNote,
}