sim_lib_music_transform/exact/
progression.rs1use std::collections::BTreeMap;
4
5use sim_lib_music_core::{ObjectId, Staff, StaffVoice, Time};
6
7use super::{
8 MusicTransform, MusicTransformChange, expand_staff, finish, parallel_staff, slice_staff,
9};
10use crate::TransformError;
11
12pub fn progression_multiply(
17 progression: &Staff,
18 factor: Time,
19) -> Result<MusicTransform<Staff>, TransformError> {
20 expand_staff(progression, factor)
21}
22
23pub fn progression_overlay(
28 progressions: &[Staff],
29) -> Result<MusicTransform<Staff>, TransformError> {
30 parallel_staff(progressions)
31}
32
33pub fn progression_slice(
35 progression: &Staff,
36 start: Time,
37 end: Time,
38) -> Result<MusicTransform<Staff>, TransformError> {
39 slice_staff(progression, start, end)
40}
41
42pub fn progression_repeat(
49 progression: &Staff,
50 occurrences: usize,
51) -> Result<MusicTransform<Staff>, TransformError> {
52 if occurrences == 0 {
53 return finish(Vec::new(), Vec::new());
54 }
55 let occurrence_count =
56 i64::try_from(occurrences).map_err(|_| TransformError::InvalidTransformOutput {
57 transform: "progression-repeat",
58 reason: "occurrence count exceeds exact time range",
59 })?;
60 let span = progression.duration();
61 let total_duration = span * Time::from_integer(occurrence_count);
62 let mut voices = BTreeMap::<ObjectId, StaffVoice>::new();
63 let mut changes = Vec::new();
64
65 for occurrence in 0..occurrences {
66 let occurrence_time =
67 i64::try_from(occurrence).expect("occurrence was bounded by occurrence count");
68 let offset = span * Time::from_integer(occurrence_time);
69 for voice in &progression.voices {
70 let output = voices
71 .entry(voice.id.clone())
72 .or_insert_with(|| StaffVoice {
73 id: voice.id.clone(),
74 name: voice.name.clone(),
75 duration: total_duration,
76 notes: Vec::new(),
77 });
78 if output.name != voice.name {
79 return Err(TransformError::InvalidTransformOutput {
80 transform: "progression-repeat",
81 reason: "matching voice ids have different names",
82 });
83 }
84 for source in &voice.notes {
85 let mut repeated = source.clone();
86 repeated.onset += offset;
87 if occurrence > 0 {
88 repeated.note_id =
89 repeated_id(&source.note_id, occurrence, "progression-repeat")?;
90 repeated.event_id =
91 repeated_id(&source.event_id, occurrence, "progression-repeat")?;
92 changes.push(MusicTransformChange::RepeatedIdentity {
93 source_note_id: source.note_id.clone(),
94 source_event_id: source.event_id.clone(),
95 repeated_note_id: repeated.note_id.clone(),
96 repeated_event_id: repeated.event_id.clone(),
97 occurrence,
98 });
99 }
100 if repeated.onset != source.onset {
101 changes.push(MusicTransformChange::Onset {
102 event_id: repeated.event_id.clone(),
103 before: source.onset,
104 after: repeated.onset,
105 });
106 }
107 output.notes.push(repeated);
108 }
109 }
110 }
111 finish(voices.into_values().collect(), changes)
112}
113
114fn repeated_id(
115 source: &ObjectId,
116 occurrence: usize,
117 transform: &'static str,
118) -> Result<ObjectId, TransformError> {
119 ObjectId::new(format!("{source}/repeat/{occurrence}")).map_err(|_| {
120 TransformError::InvalidTransformOutput {
121 transform,
122 reason: "could not derive a repeated object identity",
123 }
124 })
125}