Skip to main content

acorde_core/model/
repeat.rs

1use super::notation::Barline;
2use super::score::Score;
3
4/// Returns the ordered list of physical measure indices to play back,
5/// expanding repeat sections, volta brackets, and navigation marks.
6///
7/// Handles:
8/// - `RepeatStart` / `RepeatEnd` / `RepeatBoth` barlines
9/// - First/second-ending volta brackets
10/// - `DaCapo`, `DaCapoAlFine`, `DaCapoAlCoda`
11/// - `DalSegno`, `DalSegnoAlFine`, `DalSegnoAlCoda`
12/// - `Fine`, `ToCoda`, `Coda` markers
13///
14/// Limitations: single-level repeats; during a D.C./D.S. return pass, all
15/// barline repeats and voltas are ignored (safe linear fallback).
16pub fn measure_sequence(score: &Score) -> Vec<usize> {
17    let measures = match score
18        .parts
19        .first()
20        .and_then(|p| p.staves.first())
21        .map(|s| &s.measures)
22    {
23        Some(m) => m,
24        None => return vec![],
25    };
26
27    let n = measures.len();
28
29    // Pre-scan: locate Fine, Segno, and Coda markers.
30    let mut segno_idx: Option<usize> = None;
31    let mut coda_idx: Option<usize> = None;
32    for (j, m) in measures.iter().enumerate() {
33        match m.navigation.as_deref() {
34            Some("Segno") => segno_idx = Some(j),
35            Some("Coda") => coda_idx = Some(j),
36            _ => {}
37        }
38    }
39
40    let mut seq = Vec::with_capacity(n + 4);
41    let mut i = 0usize;
42    let mut repeat_start = 0usize;
43    let mut volta_pass: u8 = 1;
44    // Navigation-pass state (D.C./D.S. return).
45    let mut in_nav_pass = false;
46    let mut nav_fine = false; // stop at Fine
47    let mut nav_coda = false; // jump to Coda at ToCoda
48
49    while i < n {
50        let m = &measures[i];
51
52        // Volta / barline-repeat handling is suspended during a navigation pass.
53        if !in_nav_pass {
54            // Skip volta blocks that don't belong to the current pass.
55            if let Some(volta) = &m.volta
56                && volta.number != volta_pass
57                && (volta.kind == "begin" || volta.kind == "begin_end")
58            {
59                if volta.kind == "begin_end" {
60                    i += 1;
61                    continue;
62                } else {
63                    i += 1;
64                    while i < n {
65                        if let Some(v) = &measures[i].volta
66                            && v.kind == "end"
67                        {
68                            i += 1;
69                            break;
70                        }
71                        i += 1;
72                    }
73                    continue;
74                }
75            }
76        }
77
78        seq.push(i);
79
80        // Navigation-pass checks (Fine / ToCoda).
81        if in_nav_pass {
82            if nav_fine && matches!(m.navigation.as_deref(), Some("Fine")) {
83                break;
84            }
85            if nav_coda
86                && matches!(m.navigation.as_deref(), Some("ToCoda"))
87                && let Some(ci) = coda_idx
88            {
89                i = ci;
90                in_nav_pass = false;
91                continue;
92            }
93        } else {
94            // Check for D.C./D.S. marks.
95            let jump: Option<(usize, bool, bool)> = match m.navigation.as_deref() {
96                Some("DaCapo") => Some((0, false, false)),
97                Some("DaCapoAlFine") => Some((0, true, false)),
98                Some("DaCapoAlCoda") => Some((0, false, true)),
99                Some("DalSegno") => segno_idx.map(|s| (s, false, false)),
100                Some("DalSegnoAlFine") => segno_idx.map(|s| (s, true, false)),
101                Some("DalSegnoAlCoda") => segno_idx.map(|s| (s, false, true)),
102                _ => None,
103            };
104            if let Some((target, fine, coda)) = jump {
105                in_nav_pass = true;
106                nav_fine = fine;
107                nav_coda = coda;
108                i = target;
109                continue;
110            }
111
112            // Barline-repeat handling (only outside navigation pass).
113            if matches!(m.barline_left, Barline::RepeatStart | Barline::RepeatBoth) {
114                repeat_start = i;
115            }
116
117            match m.barline_right {
118                Barline::RepeatEnd | Barline::RepeatBoth => {
119                    if volta_pass == 1 {
120                        volta_pass = 2;
121                        i = repeat_start;
122                    } else {
123                        volta_pass = 1;
124                        if matches!(m.barline_right, Barline::RepeatBoth) {
125                            repeat_start = i + 1;
126                        }
127                        i += 1;
128                    }
129                    continue;
130                }
131                _ => {}
132            }
133        }
134
135        i += 1;
136    }
137
138    seq
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::model::notation::Barline;
145    use crate::model::score::{Measure, Score, VoltaBracket};
146
147    fn score_with_measures(measures: Vec<Measure>) -> Score {
148        let mut score = Score::new("T", 120, 4, 4, 0, 0);
149        score.parts[0].staves[0].measures = measures;
150        score
151    }
152
153    fn plain(n: u32) -> Measure {
154        let mut m = Measure::empty(4, 4);
155        m.number = n;
156        m
157    }
158
159    fn with_nav(n: u32, nav: &str) -> Measure {
160        let mut m = plain(n);
161        m.navigation = Some(nav.to_string());
162        m
163    }
164
165    #[test]
166    fn no_repeat_is_linear() {
167        let score = score_with_measures(vec![plain(1), plain(2), plain(3), plain(4)]);
168        assert_eq!(measure_sequence(&score), vec![0, 1, 2, 3]);
169    }
170
171    #[test]
172    fn simple_repeat_doubles_section() {
173        let mut m0 = plain(1);
174        m0.barline_left = Barline::RepeatStart;
175        let m1 = plain(2);
176        let mut m2 = plain(3);
177        m2.barline_right = Barline::RepeatEnd;
178        let m3 = plain(4);
179
180        let score = score_with_measures(vec![m0, m1, m2, m3]);
181        assert_eq!(measure_sequence(&score), vec![0, 1, 2, 0, 1, 2, 3]);
182    }
183
184    #[test]
185    fn volta_first_pass_plays_ending_1() {
186        let mut m0 = plain(1);
187        m0.barline_left = Barline::RepeatStart;
188        let m1 = plain(2);
189        let mut m2 = plain(3);
190        m2.volta = Some(VoltaBracket {
191            number: 1,
192            kind: "begin_end".into(),
193        });
194        m2.barline_right = Barline::RepeatEnd;
195        let mut m3 = plain(4);
196        m3.volta = Some(VoltaBracket {
197            number: 2,
198            kind: "begin_end".into(),
199        });
200
201        let score = score_with_measures(vec![m0, m1, m2, m3]);
202        assert_eq!(measure_sequence(&score), vec![0, 1, 2, 0, 1, 3]);
203    }
204
205    #[test]
206    fn volta_second_pass_skips_ending_1() {
207        let mut m0 = plain(1);
208        m0.barline_left = Barline::RepeatStart;
209        let m1 = plain(2);
210        let mut m2 = plain(3);
211        m2.volta = Some(VoltaBracket {
212            number: 1,
213            kind: "begin_end".into(),
214        });
215        m2.barline_right = Barline::RepeatEnd;
216        let mut m3 = plain(4);
217        m3.volta = Some(VoltaBracket {
218            number: 2,
219            kind: "begin_end".into(),
220        });
221        let m4 = plain(5);
222
223        let score = score_with_measures(vec![m0, m1, m2, m3, m4]);
224        let seq = measure_sequence(&score);
225        assert_eq!(seq, vec![0, 1, 2, 0, 1, 3, 4]);
226        let second_pass_third = seq[5];
227        assert_eq!(second_pass_third, 3);
228    }
229
230    // ── Navigation marks ─────────────────────────────────────────────────────
231
232    #[test]
233    fn da_capo_jumps_to_start() {
234        // [A][B][C D.C.] → A B C A B C ...
235        // The second D.C. encounter is ignored (in_nav_pass=true) so we play A B C straight.
236        let score = score_with_measures(vec![plain(1), plain(2), with_nav(3, "DaCapo")]);
237        // Pass: 0,1,2 → D.C. → in_nav_pass, jump to 0 → 0,1,2 → i=3 → done
238        assert_eq!(measure_sequence(&score), vec![0, 1, 2, 0, 1, 2]);
239    }
240
241    #[test]
242    fn da_capo_al_fine_stops_at_fine() {
243        // [A Fine][B][C D.C.alFine] → A B C A (stop at Fine=0)
244        let mut m0 = plain(1);
245        m0.navigation = Some("Fine".into());
246        let score = score_with_measures(vec![m0, plain(2), with_nav(3, "DaCapoAlFine")]);
247        // Pass: 0,1,2 → D.C.alFine → jump to 0 → push 0 → Fine found → break
248        assert_eq!(measure_sequence(&score), vec![0, 1, 2, 0]);
249    }
250
251    #[test]
252    fn dal_segno_al_coda_jumps_to_coda() {
253        // [A][B Segno][C][D ToCoda][E D.S.alCoda][F Coda][G]
254        // Pass: 0,1,2,3,4 → D.S.alCoda → jump to segno(1) → in_nav_pass
255        // → push 1,2,3 → ToCoda at 3 → jump to coda(5) → push 5,6
256        let mut m1 = plain(2);
257        m1.navigation = Some("Segno".into());
258        let mut m3 = plain(4);
259        m3.navigation = Some("ToCoda".into());
260        let mut m5 = plain(6);
261        m5.navigation = Some("Coda".into());
262        let score = score_with_measures(vec![
263            plain(1),
264            m1,
265            plain(3),
266            m3,
267            with_nav(5, "DalSegnoAlCoda"),
268            m5,
269            plain(7),
270        ]);
271        assert_eq!(measure_sequence(&score), vec![0, 1, 2, 3, 4, 1, 2, 3, 5, 6]);
272    }
273}