1use std::collections::BTreeMap;
2
3use num_rational::Ratio;
4use thiserror::Error;
5
6use sim_lib_music_core::{
7 Articulation, AtomRef, Channel, ConversionError, Melody, MelodyItem, Music, MusicError,
8 MusicObject, Note, PianoRoll, Rest, Time, TimedNote,
9};
10use sim_lib_pitch_core::{Pitch, PitchClass};
11use sim_lib_pitch_scale::{Key, Mode, PitchScaleError, Scale};
12
13#[derive(Debug, Error, Clone, PartialEq, Eq)]
15pub enum TransformError {
16 #[error("transform factor must be positive")]
18 InvalidFactor,
19 #[error(transparent)]
21 InvalidMusic(#[from] MusicError),
22 #[error("{transform} transform returned invalid output: {reason}")]
24 InvalidTransformOutput {
25 transform: &'static str,
27 reason: &'static str,
29 },
30 #[error(transparent)]
32 InvalidStaff(#[from] ConversionError),
33 #[error(transparent)]
35 Assignment(#[from] sim_lib_discrete_graph::GraphError),
36}
37
38#[derive(Copy, Clone, Debug, PartialEq, Eq)]
40pub enum RetrogradeMode {
41 Cutout,
43 PinnedNoteOn,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum FunctionMap {
50 Major,
52 MinorNatural,
54 MinorHarmonic,
56 MinorMelodicAsc,
58 Dorian,
60 Phrygian,
62 Lydian,
64 Mixolydian,
66 Locrian,
68 Custom(Scale),
70}
71
72impl FunctionMap {
73 pub fn name(&self) -> &'static str {
75 match self {
76 Self::Major => "Major",
77 Self::MinorNatural => "MinorNatural",
78 Self::MinorHarmonic => "MinorHarmonic",
79 Self::MinorMelodicAsc => "MinorMelodicAsc",
80 Self::Dorian => "Dorian",
81 Self::Phrygian => "Phrygian",
82 Self::Lydian => "Lydian",
83 Self::Mixolydian => "Mixolydian",
84 Self::Locrian => "Locrian",
85 Self::Custom(_) => "Custom",
86 }
87 }
88
89 pub fn scale_for_key(&self, key: &Key) -> Scale {
91 match self {
92 Self::Major => Scale::new(key.tonic, Mode::Major),
93 Self::MinorNatural => Scale::new(key.tonic, Mode::MinorNatural),
94 Self::MinorHarmonic => Scale::new(key.tonic, Mode::MinorHarmonic),
95 Self::MinorMelodicAsc => Scale::new(key.tonic, Mode::MinorMelodic),
96 Self::Dorian => Scale::new(key.tonic, Mode::Dorian),
97 Self::Phrygian => Scale::new(key.tonic, Mode::Phrygian),
98 Self::Lydian => Scale::new(key.tonic, Mode::Lydian),
99 Self::Mixolydian => Scale::new(key.tonic, Mode::Mixolydian),
100 Self::Locrian => Scale::new(key.tonic, Mode::Locrian),
101 Self::Custom(scale) => *scale,
102 }
103 }
104
105 pub fn degree_to_pitch(
107 &self,
108 degree: usize,
109 key: &Key,
110 octave: i16,
111 ) -> Result<Pitch, PitchScaleError> {
112 Ok(Pitch {
113 class: self.scale_for_key(key).pitch_at_degree(degree)?,
114 octave,
115 })
116 }
117}
118
119#[derive(Clone, Debug, Default)]
121pub struct FunctionMapRegistry {
122 maps: BTreeMap<String, FunctionMap>,
123}
124
125impl FunctionMapRegistry {
126 pub fn new_with_builtins() -> Self {
128 let mut registry = Self::default();
129 for map in [
130 FunctionMap::Major,
131 FunctionMap::MinorNatural,
132 FunctionMap::MinorHarmonic,
133 FunctionMap::MinorMelodicAsc,
134 FunctionMap::Dorian,
135 FunctionMap::Phrygian,
136 FunctionMap::Lydian,
137 FunctionMap::Mixolydian,
138 FunctionMap::Locrian,
139 ] {
140 registry.register(map);
141 }
142 registry
143 }
144
145 pub fn register(&mut self, map: FunctionMap) {
147 self.maps.insert(map.name().to_owned(), map);
148 }
149
150 pub fn get(&self, name: &str) -> Option<&FunctionMap> {
152 self.maps.get(name)
153 }
154
155 pub fn names(&self) -> Vec<&str> {
157 self.maps.keys().map(String::as_str).collect()
158 }
159}
160
161pub fn augment(object: &dyn MusicObject, factor: Time) -> Result<Music, TransformError> {
163 scale_time(object, factor)
164}
165
166pub fn diminish(object: &dyn MusicObject, factor: Time) -> Result<Music, TransformError> {
168 if factor <= Time::from_integer(0) {
169 return Err(TransformError::InvalidFactor);
170 }
171 scale_time(object, factor.recip())
172}
173
174pub fn retrograde(object: &dyn MusicObject) -> Result<Music, TransformError> {
176 retrograde_with_mode(object, RetrogradeMode::Cutout)
177}
178
179pub fn retrograde_with_mode(
181 object: &dyn MusicObject,
182 mode: RetrogradeMode,
183) -> Result<Music, TransformError> {
184 let roll = to_piano_roll(object)?;
185 let total = object.duration();
186 let items = match mode {
187 RetrogradeMode::Cutout => roll
188 .items
189 .into_iter()
190 .map(|item| TimedNote {
191 onset: total - item.onset - item.note.duration,
192 note: item.note,
193 })
194 .collect(),
195 RetrogradeMode::PinnedNoteOn => {
196 let mut onsets: Vec<Time> = roll.items.iter().map(|item| item.onset).collect();
197 onsets.sort();
198 let notes: Vec<Note> = roll.items.into_iter().rev().map(|item| item.note).collect();
199 onsets
200 .into_iter()
201 .zip(notes)
202 .map(|(onset, note)| TimedNote { onset, note })
203 .collect()
204 }
205 };
206 Ok(Music::PianoRoll(canonical_roll(items)?))
207}
208
209pub fn time_invert(object: &dyn MusicObject) -> Result<Music, TransformError> {
211 let roll = to_piano_roll(object)?;
212 if roll.items.is_empty() {
213 return Ok(Music::PianoRoll(roll));
214 }
215 let total = object.duration();
216 let mut items: Vec<TimedNote> = roll
217 .items
218 .into_iter()
219 .map(|item| TimedNote {
220 onset: total - item.onset,
221 note: item.note,
222 })
223 .collect();
224 let min_onset = items
225 .iter()
226 .map(|item| item.onset)
227 .min()
228 .unwrap_or_else(|| Time::from_integer(0));
229 for item in &mut items {
230 item.onset -= min_onset;
231 }
232 Ok(Music::PianoRoll(canonical_roll(items)?))
233}
234
235pub fn loop_n(object: &dyn MusicObject, n: usize) -> Result<Music, TransformError> {
237 let roll = to_piano_roll(object)?;
238 let span = object.duration();
239 let items = (0..n)
240 .flat_map(|index| {
241 let offset = span * Time::from_integer(index as i64);
242 roll.items.iter().cloned().map(move |mut item| {
243 item.onset += offset;
244 item
245 })
246 })
247 .collect();
248 Ok(Music::PianoRoll(canonical_roll(items)?))
249}
250
251pub fn slice(object: &dyn MusicObject, start: Time, end: Time) -> Result<Music, TransformError> {
253 let roll = to_piano_roll(object)?;
254 let items = roll
255 .items
256 .into_iter()
257 .filter_map(|item| {
258 let item_start = item.onset;
259 let item_end = item.onset + item.note.duration;
260 let clipped_start = item_start.max(start);
261 let clipped_end = item_end.min(end);
262 (clipped_start < clipped_end).then(|| TimedNote {
263 onset: clipped_start - start,
264 note: Note {
265 duration: clipped_end - clipped_start,
266 ..item.note
267 },
268 })
269 })
270 .collect();
271 Ok(Music::PianoRoll(canonical_roll(items)?))
272}
273
274pub fn transpose(object: &dyn MusicObject, semitones: i32) -> Result<Music, TransformError> {
276 map_notes(object, |note| Note {
277 pitch: note.pitch.transpose(semitones),
278 ..note
279 })
280}
281
282pub fn transpose_diatonic(
286 object: &dyn MusicObject,
287 scale: &Scale,
288 steps: i32,
289) -> Result<Music, TransformError> {
290 map_notes(object, |note| Note {
291 pitch: scale
292 .transpose_diatonic(note.pitch, steps)
293 .unwrap_or(note.pitch),
294 ..note
295 })
296}
297
298pub fn pitch_invert(object: &dyn MusicObject, axis: Pitch) -> Result<Music, TransformError> {
300 map_notes(object, |note| Note {
301 pitch: note.pitch.invert(axis),
302 ..note
303 })
304}
305
306pub fn retrograde_invert(object: &dyn MusicObject, axis: Pitch) -> Result<Music, TransformError> {
308 let inverted = pitch_invert(object, axis)?;
309 retrograde(&inverted)
310}
311
312pub fn shift_octave(object: &dyn MusicObject, octaves: i16) -> Result<Music, TransformError> {
314 map_notes(object, |note| Note {
315 pitch: note.pitch.transpose(i32::from(octaves) * 12),
316 ..note
317 })
318}
319
320pub fn chord_tones_in(object: &dyn MusicObject, scale: &Scale) -> Result<Music, TransformError> {
322 map_notes(object, |note| Note {
323 pitch: nearest_pitch_in_scale(note.pitch, scale),
324 ..note
325 })
326}
327
328pub fn map_to_function(
332 object: &dyn MusicObject,
333 key: &Key,
334 fmap: &FunctionMap,
335) -> Result<Music, TransformError> {
336 let source_scale = Scale::new(key.tonic, key.mode);
337 map_notes(object, |note| {
338 match source_scale.degree_of(note.pitch.class) {
339 Some(degree) => Note {
340 pitch: fmap
341 .degree_to_pitch(degree, key, note.pitch.octave)
342 .unwrap_or(note.pitch),
343 ..note
344 },
345 None => note,
346 }
347 })
348}
349
350fn scale_time(object: &dyn MusicObject, factor: Time) -> Result<Music, TransformError> {
351 if factor <= Time::from_integer(0) {
352 return Err(TransformError::InvalidFactor);
353 }
354 map_roll(object, |mut item| {
355 item.onset *= factor;
356 item.note.duration *= factor;
357 item
358 })
359}
360
361pub(crate) fn map_notes(
362 object: &dyn MusicObject,
363 f: impl Fn(Note) -> Note,
364) -> Result<Music, TransformError> {
365 map_roll(object, |mut item| {
366 item.note = f(item.note);
367 item
368 })
369}
370
371pub(crate) fn map_roll(
372 object: &dyn MusicObject,
373 f: impl Fn(TimedNote) -> TimedNote,
374) -> Result<Music, TransformError> {
375 let roll = to_piano_roll(object)?;
376 let items = roll.items.into_iter().map(f).collect();
377 Ok(Music::PianoRoll(canonical_roll(items)?))
378}
379
380pub(crate) fn to_piano_roll(object: &dyn MusicObject) -> Result<PianoRoll, TransformError> {
381 let mut atoms = Vec::new();
382 object.voices(Time::from_integer(0), &mut atoms);
383 let items = atoms
384 .into_iter()
385 .filter_map(|atom| match atom.atom {
386 AtomRef::Note(note) => Some(TimedNote {
387 onset: atom.onset,
388 note,
389 }),
390 AtomRef::Rest(_) | AtomRef::Phantom(_) => None,
391 })
392 .collect();
393 canonical_roll(items)
394}
395
396pub(crate) fn canonical_roll(items: Vec<TimedNote>) -> Result<PianoRoll, TransformError> {
397 Ok(PianoRoll::new(items)?)
398}
399
400pub(crate) fn nearest_pitch_in_scale(pitch: Pitch, scale: &Scale) -> Pitch {
401 if scale.degree_of(pitch.class).is_some() {
402 return pitch;
403 }
404 let candidates = scale
405 .pitch_classes()
406 .into_iter()
407 .flat_map(|class| {
408 [
409 Pitch {
410 class,
411 octave: pitch.octave - 1,
412 },
413 Pitch {
414 class,
415 octave: pitch.octave,
416 },
417 Pitch {
418 class,
419 octave: pitch.octave + 1,
420 },
421 ]
422 })
423 .collect::<Vec<_>>();
424 candidates
425 .into_iter()
426 .min_by_key(|candidate| {
427 (
428 (candidate.semitone() - pitch.semitone()).abs(),
429 candidate.semitone(),
430 )
431 })
432 .unwrap_or(pitch)
433}
434
435pub fn simple_melody(items: &[(u8, Time)]) -> Melody {
440 Melody::new(
441 items
442 .iter()
443 .map(|(midi, duration)| {
444 MelodyItem::Note(
445 Note::new(
446 *duration,
447 Pitch::from_midi(*midi),
448 100,
449 Channel::new(0).expect("channel"),
450 Articulation::Normal,
451 )
452 .expect("note"),
453 )
454 })
455 .collect(),
456 )
457 .expect("melody")
458}
459
460pub fn silence(duration: Time) -> Rest {
462 Rest::new(duration).expect("rest")
463}
464
465pub fn quarter() -> Time {
467 Ratio::new(1, 4)
468}
469
470pub fn pitch_class_name(class: PitchClass) -> &'static str {
472 class.canonical_name()
473}