Skip to main content

sim_lib_music_transform/exact/
progression.rs

1//! Exact progression algebra over identity-bearing staffs.
2
3use 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
12/// Multiplies every exact onset, duration, and trailing span in a progression.
13///
14/// This is duration multiplication, not repetition: every object identity is
15/// retained exactly once.
16pub fn progression_multiply(
17    progression: &Staff,
18    factor: Time,
19) -> Result<MusicTransform<Staff>, TransformError> {
20    expand_staff(progression, factor)
21}
22
23/// Overlays exact progressions at the same origin.
24///
25/// The input identity sets must be disjoint except that equal voice ids with
26/// equal names may be merged. Reusing a note or event id fails closed.
27pub fn progression_overlay(
28    progressions: &[Staff],
29) -> Result<MusicTransform<Staff>, TransformError> {
30    parallel_staff(progressions)
31}
32
33/// Extracts an exact half-open progression window.
34pub 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
42/// Repeats a progression back to back while keeping global identity unique.
43///
44/// The first occurrence preserves every original id. Later occurrences derive
45/// note and event ids by appending `/repeat/<occurrence>` and report every
46/// derivation as [`MusicTransformChange::RepeatedIdentity`]. Voice ids remain
47/// stable because each repeated line is still the same logical voice.
48pub 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}