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