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