Skip to main content

acorde_core/model/
musical_diff.rs

1//! Arrangement comparison by written musical position, separate from the edit patch diff.
2
3use super::score::{Note, NoteAddr, Score};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::BTreeSet;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum StructurePolicy {
11    /// Require identical part, staff and measure structure before matching by index.
12    Strict,
13    /// Explicitly pair the common index prefix; report notes in extra voices as additions/removals.
14    IndexPrefix,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub struct VoiceAddr {
19    pub part: usize,
20    pub staff: usize,
21    pub voice: usize,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct VoiceCorrespondence {
26    pub reference: VoiceAddr,
27    pub current: VoiceAddr,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct NoteCorrespondence {
32    pub reference: NoteAddr,
33    pub current: NoteAddr,
34}
35
36/// Explicit policy for comparison. Supplied note addresses take priority over position matching.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(default)]
39pub struct MusicalComparisonOptions {
40    pub structure_policy: StructurePolicy,
41    pub voice_pairs: Vec<VoiceCorrespondence>,
42    pub note_pairs: Vec<NoteCorrespondence>,
43}
44
45impl Default for MusicalComparisonOptions {
46    fn default() -> Self {
47        Self {
48            structure_policy: StructurePolicy::Strict,
49            voice_pairs: Vec::new(),
50            note_pairs: Vec::new(),
51        }
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum MusicalChangeKind {
58    Added,
59    Removed,
60    Modified,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct MusicalChange {
65    pub kind: MusicalChangeKind,
66    pub reference: Option<NoteAddr>,
67    pub current: Option<NoteAddr>,
68    /// Stable field names. `onset_beats` is a derived rhythmic position.
69    pub changed_fields: Vec<String>,
70    /// `timing`, `pitch`, and/or `notation` for modified notes.
71    pub domains: Vec<String>,
72    pub reference_onset_beats: Option<f64>,
73    pub current_onset_beats: Option<f64>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct MusicalComparisonDiagnostic {
78    pub code: String,
79    pub reference_voice: Option<VoiceAddr>,
80    pub current_voice: Option<VoiceAddr>,
81    pub measure: Option<usize>,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct MusicalComparisonResult {
86    pub changes: Vec<MusicalChange>,
87    pub diagnostics: Vec<MusicalComparisonDiagnostic>,
88}
89
90struct Timed<'a> {
91    note: &'a Note,
92    index: usize,
93    onset: f64,
94}
95
96fn timed(voice: &[Note]) -> Vec<Timed<'_>> {
97    let mut onset = 0.0;
98    voice
99        .iter()
100        .enumerate()
101        .map(|(index, note)| {
102            let entry = Timed { note, index, onset };
103            onset += note.beats();
104            entry
105        })
106        .collect()
107}
108
109fn voice_at(score: &Score, address: VoiceAddr, measure: usize) -> Option<&[Note]> {
110    score
111        .parts
112        .get(address.part)?
113        .staves
114        .get(address.staff)?
115        .measures
116        .get(measure)?
117        .voices
118        .get(address.voice)
119        .map(Vec::as_slice)
120}
121
122fn voice_exists(score: &Score, address: VoiceAddr) -> bool {
123    address.voice < 4
124        && score
125            .parts
126            .get(address.part)
127            .and_then(|part| part.staves.get(address.staff))
128            .is_some()
129}
130
131fn all_voices(score: &Score) -> Vec<VoiceAddr> {
132    score
133        .parts
134        .iter()
135        .enumerate()
136        .flat_map(|(part, item)| {
137            item.staves.iter().enumerate().flat_map(move |(staff, _)| {
138                (0..4).map(move |voice| VoiceAddr { part, staff, voice })
139            })
140        })
141        .collect()
142}
143
144fn same_structure(reference: &Score, current: &Score) -> bool {
145    reference.parts.len() == current.parts.len()
146        && reference.parts.iter().zip(&current.parts).all(|(a, b)| {
147            a.staves.len() == b.staves.len()
148                && a.staves
149                    .iter()
150                    .zip(&b.staves)
151                    .all(|(x, y)| x.measures.len() == y.measures.len())
152        })
153}
154
155fn diagnostic(
156    code: &str,
157    reference_voice: Option<VoiceAddr>,
158    current_voice: Option<VoiceAddr>,
159    measure: Option<usize>,
160) -> MusicalComparisonDiagnostic {
161    MusicalComparisonDiagnostic {
162        code: code.into(),
163        reference_voice,
164        current_voice,
165        measure,
166    }
167}
168
169fn note_addr(voice: VoiceAddr, measure: usize, note: usize) -> NoteAddr {
170    NoteAddr {
171        part: voice.part,
172        staff: voice.staff,
173        measure,
174        voice: voice.voice,
175        note,
176    }
177}
178
179fn canonical(value: &mut Value) {
180    match value {
181        Value::Object(fields) => {
182            for name in ["id", "offset_x", "offset_y", "relative_x", "relative_y"] {
183                fields.remove(name);
184            }
185            for value in fields.values_mut() {
186                canonical(value);
187            }
188        }
189        Value::Array(values) => {
190            for value in values {
191                canonical(value);
192            }
193        }
194        _ => {}
195    }
196}
197
198fn note_value(note: &Note) -> Value {
199    let mut value = serde_json::to_value(note).unwrap_or(Value::Null);
200    canonical(&mut value);
201    value
202}
203
204fn cents(note: &Note) -> Vec<i32> {
205    let mut pitches: Vec<_> = note
206        .pitches
207        .iter()
208        .map(|pitch| pitch.to_midi_cents())
209        .collect();
210    pitches.sort_unstable();
211    pitches
212}
213
214fn same_sound_identity(a: &Note, b: &Note) -> bool {
215    a.is_rest == b.is_rest
216        && a.is_grace == b.is_grace
217        && a.is_cue == b.is_cue
218        && cents(a) == cents(b)
219}
220
221fn same_onset(a: &Timed<'_>, b: &Timed<'_>) -> bool {
222    (a.onset - b.onset).abs() <= 1e-8
223}
224
225fn match_unique(
226    reference: &[Timed<'_>],
227    current: &[Timed<'_>],
228    used_reference: &mut [bool],
229    used_current: &mut [bool],
230    pairs: &mut Vec<(usize, usize)>,
231    predicate: impl Fn(&Timed<'_>, &Timed<'_>) -> bool,
232) {
233    loop {
234        let mut progress = false;
235        for (ri, a) in reference.iter().enumerate() {
236            if used_reference[ri] {
237                continue;
238            }
239            let candidates: Vec<_> = current
240                .iter()
241                .enumerate()
242                .filter(|(ci, b)| !used_current[*ci] && predicate(a, b))
243                .map(|(ci, _)| ci)
244                .collect();
245            if candidates.len() != 1 {
246                continue;
247            }
248            let ci = candidates[0];
249            let reverse = reference
250                .iter()
251                .enumerate()
252                .filter(|(other, r)| !used_reference[*other] && predicate(r, &current[ci]))
253                .count();
254            if reverse != 1 {
255                continue;
256            }
257            used_reference[ri] = true;
258            used_current[ci] = true;
259            pairs.push((ri, ci));
260            progress = true;
261        }
262        if !progress {
263            break;
264        }
265    }
266}
267
268fn modified(
269    reference: &Timed<'_>,
270    current: &Timed<'_>,
271    ra: NoteAddr,
272    ca: NoteAddr,
273) -> Option<MusicalChange> {
274    let a = note_value(reference.note);
275    let b = note_value(current.note);
276    let mut fields = BTreeSet::new();
277    if !same_onset(reference, current) {
278        fields.insert("onset_beats".to_string());
279    }
280    if let (Some(x), Some(y)) = (a.as_object(), b.as_object()) {
281        for key in x.keys().chain(y.keys()) {
282            if x.get(key) != y.get(key) {
283                fields.insert(key.clone());
284            }
285        }
286    } else if a != b {
287        fields.insert("note".to_string());
288    }
289    if fields.is_empty() {
290        return None;
291    }
292    let mut domains = Vec::new();
293    if fields.contains("onset_beats")
294        || (reference.note.beats() - current.note.beats()).abs() > 1e-8
295    {
296        domains.push("timing".into());
297    }
298    if reference.note.is_rest != current.note.is_rest
299        || cents(reference.note) != cents(current.note)
300    {
301        domains.push("pitch".into());
302    }
303    if fields
304        .iter()
305        .any(|field| !matches!(field.as_str(), "onset_beats" | "pitches"))
306        || (fields.contains("pitches") && cents(reference.note) == cents(current.note))
307    {
308        domains.push("notation".into());
309    }
310    Some(MusicalChange {
311        kind: MusicalChangeKind::Modified,
312        reference: Some(ra),
313        current: Some(ca),
314        changed_fields: fields.into_iter().collect(),
315        domains,
316        reference_onset_beats: Some(reference.onset),
317        current_onset_beats: Some(current.onset),
318    })
319}
320
321fn unmatched_change(kind: MusicalChangeKind, address: NoteAddr, onset: f64) -> MusicalChange {
322    let (reference, current, reference_onset_beats, current_onset_beats) = match kind {
323        MusicalChangeKind::Added => (None, Some(address), None, Some(onset)),
324        MusicalChangeKind::Removed => (Some(address), None, Some(onset), None),
325        MusicalChangeKind::Modified => unreachable!(),
326    };
327    MusicalChange {
328        kind,
329        reference,
330        current,
331        changed_fields: Vec::new(),
332        domains: Vec::new(),
333        reference_onset_beats,
334        current_onset_beats,
335    }
336}
337
338fn whole_voice_changes(
339    score: &Score,
340    voice: VoiceAddr,
341    kind: MusicalChangeKind,
342    result: &mut MusicalComparisonResult,
343) {
344    let measures = &score.parts[voice.part].staves[voice.staff].measures;
345    for (measure, _) in measures.iter().enumerate() {
346        if let Some(notes) = voice_at(score, voice, measure) {
347            for entry in timed(notes) {
348                result.changes.push(unmatched_change(
349                    kind,
350                    note_addr(voice, measure, entry.index),
351                    entry.onset,
352                ));
353            }
354        }
355    }
356}
357
358/// Compare reference and current notation at each measure's written onset.
359///
360/// This operation never mutates either score and does not change `diff` or patch semantics.
361/// With default strict policy, structurally different scores return a diagnostic. Select
362/// `index_prefix` or supply voice pairs to compare differently structured arrangements.
363pub fn compare_musical_positions(
364    reference: &Score,
365    current: &Score,
366    options: &MusicalComparisonOptions,
367) -> MusicalComparisonResult {
368    let mut result = MusicalComparisonResult::default();
369    if options.voice_pairs.is_empty()
370        && options.structure_policy == StructurePolicy::Strict
371        && !same_structure(reference, current)
372    {
373        result
374            .diagnostics
375            .push(diagnostic("voice-structure-mismatch", None, None, None));
376        return result;
377    }
378    let reference_voices = all_voices(reference);
379    let current_voices = all_voices(current);
380    let pairs = if !options.voice_pairs.is_empty() {
381        options.voice_pairs.clone()
382    } else {
383        reference_voices
384            .iter()
385            .filter(|voice| current_voices.contains(voice))
386            .map(|voice| VoiceCorrespondence {
387                reference: *voice,
388                current: *voice,
389            })
390            .collect()
391    };
392    let mut used_reference = Vec::new();
393    let mut used_current = Vec::new();
394    for pair in &pairs {
395        if !voice_exists(reference, pair.reference)
396            || !voice_exists(current, pair.current)
397            || used_reference.contains(&pair.reference)
398            || used_current.contains(&pair.current)
399        {
400            result.diagnostics.push(diagnostic(
401                "invalid-voice-correspondence",
402                Some(pair.reference),
403                Some(pair.current),
404                None,
405            ));
406            return result;
407        }
408        used_reference.push(pair.reference);
409        used_current.push(pair.current);
410    }
411    let mut used_note_reference = Vec::new();
412    let mut used_note_current = Vec::new();
413    for mapping in &options.note_pairs {
414        if !pairs.iter().any(|pair| {
415            pair.reference.part == mapping.reference.part
416                && pair.reference.staff == mapping.reference.staff
417                && pair.reference.voice == mapping.reference.voice
418                && pair.current.part == mapping.current.part
419                && pair.current.staff == mapping.current.staff
420                && pair.current.voice == mapping.current.voice
421        }) || mapping.reference.measure != mapping.current.measure
422            || voice_at(
423                reference,
424                VoiceAddr {
425                    part: mapping.reference.part,
426                    staff: mapping.reference.staff,
427                    voice: mapping.reference.voice,
428                },
429                mapping.reference.measure,
430            )
431            .is_none_or(|notes| mapping.reference.note >= notes.len())
432            || voice_at(
433                current,
434                VoiceAddr {
435                    part: mapping.current.part,
436                    staff: mapping.current.staff,
437                    voice: mapping.current.voice,
438                },
439                mapping.current.measure,
440            )
441            .is_none_or(|notes| mapping.current.note >= notes.len())
442            || used_note_reference.contains(&mapping.reference)
443            || used_note_current.contains(&mapping.current)
444        {
445            result.diagnostics.push(diagnostic(
446                "invalid-note-correspondence",
447                None,
448                None,
449                Some(mapping.reference.measure),
450            ));
451            return result;
452        }
453        used_note_reference.push(mapping.reference.clone());
454        used_note_current.push(mapping.current.clone());
455    }
456    for pair in &pairs {
457        let reference_measures =
458            &reference.parts[pair.reference.part].staves[pair.reference.staff].measures;
459        let current_measures =
460            &current.parts[pair.current.part].staves[pair.current.staff].measures;
461        if reference_measures.len() != current_measures.len() {
462            result.diagnostics.push(diagnostic(
463                "measure-count-mismatch",
464                Some(pair.reference),
465                Some(pair.current),
466                None,
467            ));
468            continue;
469        }
470        for measure in 0..reference_measures.len() {
471            if reference_measures[measure].number != current_measures[measure].number {
472                result.diagnostics.push(diagnostic(
473                    "measure-number-mismatch",
474                    Some(pair.reference),
475                    Some(pair.current),
476                    Some(measure),
477                ));
478                continue;
479            }
480            let a = timed(voice_at(reference, pair.reference, measure).unwrap_or(&[]));
481            let b = timed(voice_at(current, pair.current, measure).unwrap_or(&[]));
482            let mut used_a = vec![false; a.len()];
483            let mut used_b = vec![false; b.len()];
484            let mut matched = Vec::new();
485            for identity in options.note_pairs.iter().filter(|identity| {
486                identity.reference.measure == measure
487                    && identity.reference.part == pair.reference.part
488                    && identity.reference.staff == pair.reference.staff
489                    && identity.reference.voice == pair.reference.voice
490                    && identity.current.part == pair.current.part
491                    && identity.current.staff == pair.current.staff
492                    && identity.current.voice == pair.current.voice
493            }) {
494                let ri = identity.reference.note;
495                let ci = identity.current.note;
496                if ri >= a.len() || ci >= b.len() || used_a[ri] || used_b[ci] {
497                    result.diagnostics.push(diagnostic(
498                        "invalid-note-correspondence",
499                        Some(pair.reference),
500                        Some(pair.current),
501                        Some(measure),
502                    ));
503                    continue;
504                }
505                used_a[ri] = true;
506                used_b[ci] = true;
507                matched.push((ri, ci));
508            }
509            match_unique(&a, &b, &mut used_a, &mut used_b, &mut matched, |x, y| {
510                same_onset(x, y) && note_value(x.note) == note_value(y.note)
511            });
512            match_unique(&a, &b, &mut used_a, &mut used_b, &mut matched, |x, y| {
513                same_onset(x, y) && same_sound_identity(x.note, y.note)
514            });
515            match_unique(&a, &b, &mut used_a, &mut used_b, &mut matched, |x, y| {
516                same_sound_identity(x.note, y.note)
517            });
518            match_unique(&a, &b, &mut used_a, &mut used_b, &mut matched, same_onset);
519            for (ri, ci) in matched {
520                if let Some(change) = modified(
521                    &a[ri],
522                    &b[ci],
523                    note_addr(pair.reference, measure, a[ri].index),
524                    note_addr(pair.current, measure, b[ci].index),
525                ) {
526                    result.changes.push(change);
527                }
528            }
529            let unmatched_a: Vec<_> = used_a
530                .iter()
531                .enumerate()
532                .filter_map(|(i, used)| (!used).then_some(i))
533                .collect();
534            let unmatched_b: Vec<_> = used_b
535                .iter()
536                .enumerate()
537                .filter_map(|(i, used)| (!used).then_some(i))
538                .collect();
539            if !unmatched_a.is_empty()
540                && !unmatched_b.is_empty()
541                && (unmatched_a.len() > 1 || unmatched_b.len() > 1)
542            {
543                result.diagnostics.push(diagnostic(
544                    "ambiguous-note-alignment",
545                    Some(pair.reference),
546                    Some(pair.current),
547                    Some(measure),
548                ));
549            }
550            for ri in unmatched_a {
551                result.changes.push(unmatched_change(
552                    MusicalChangeKind::Removed,
553                    note_addr(pair.reference, measure, a[ri].index),
554                    a[ri].onset,
555                ));
556            }
557            for ci in unmatched_b {
558                result.changes.push(unmatched_change(
559                    MusicalChangeKind::Added,
560                    note_addr(pair.current, measure, b[ci].index),
561                    b[ci].onset,
562                ));
563            }
564        }
565    }
566    for voice in reference_voices {
567        if !used_reference.contains(&voice) {
568            whole_voice_changes(reference, voice, MusicalChangeKind::Removed, &mut result);
569        }
570    }
571    for voice in current_voices {
572        if !used_current.contains(&voice) {
573            whole_voice_changes(current, voice, MusicalChangeKind::Added, &mut result);
574        }
575    }
576    result
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use crate::{Duration, Pitch, TupletBoundary, TupletInfo};
583
584    fn note(midi: u8, duration: Duration) -> Note {
585        Note::new(Pitch::from_midi(midi, false), duration)
586    }
587
588    fn score(notes: Vec<Note>) -> Score {
589        let mut score = Score::new("comparison", 120, 4, 4, 0, 1);
590        score.parts[0].staves[0].measures[0].voices[0] = notes;
591        score
592    }
593
594    #[test]
595    fn rearticulation_does_not_mark_later_notes() {
596        let reference = score(vec![
597            note(60, Duration::Quarter),
598            note(62, Duration::Quarter),
599            note(64, Duration::Quarter),
600            note(65, Duration::Quarter),
601        ]);
602        let current = score(vec![
603            note(60, Duration::Eighth),
604            note(60, Duration::Eighth),
605            note(62, Duration::Quarter),
606            note(64, Duration::Quarter),
607            note(65, Duration::Quarter),
608        ]);
609        let result = compare_musical_positions(&reference, &current, &Default::default());
610        assert!(result.diagnostics.is_empty());
611        let current_indices: Vec<_> = result
612            .changes
613            .iter()
614            .filter_map(|change| change.current.as_ref().map(|address| address.note))
615            .collect();
616        assert_eq!(current_indices, vec![0, 1]);
617        assert_eq!(result.changes[0].kind, MusicalChangeKind::Modified);
618        assert_eq!(result.changes[0].reference.as_ref().unwrap().note, 0);
619        assert_eq!(result.changes[1].kind, MusicalChangeKind::Added);
620    }
621
622    #[test]
623    fn swing_tuplet_marks_only_the_changed_first_beat() {
624        let reference = score(vec![
625            note(60, Duration::Eighth),
626            note(62, Duration::Eighth),
627            note(64, Duration::Quarter),
628            note(65, Duration::Quarter),
629            note(67, Duration::Quarter),
630        ]);
631        let mut current = reference.clone();
632        current.parts[0].staves[0].measures[0].voices[0][0].duration = Duration::Quarter;
633        for index in 0..2 {
634            current.parts[0].staves[0].measures[0].voices[0][index].tuplet = Some(TupletInfo {
635                actual_notes: 3,
636                normal_notes: 2,
637            });
638        }
639        let result = compare_musical_positions(&reference, &current, &Default::default());
640        let indices: Vec<_> = result
641            .changes
642            .iter()
643            .filter_map(|change| change.current.as_ref().map(|address| address.note))
644            .collect();
645        assert_eq!(indices, vec![0, 1]);
646        assert!(
647            result
648                .changes
649                .iter()
650                .all(|change| change.domains.contains(&"timing".into()))
651        );
652    }
653
654    #[test]
655    fn grace_insertion_and_notation_only_tuplet_boundary_are_distinct() {
656        let reference = score(vec![
657            note(60, Duration::Quarter),
658            note(62, Duration::Quarter),
659        ]);
660        let mut current = reference.clone();
661        let mut grace = note(67, Duration::Eighth);
662        grace.is_grace = true;
663        current.parts[0].staves[0].measures[0].voices[0].insert(0, grace);
664        let result = compare_musical_positions(&reference, &current, &Default::default());
665        assert_eq!(result.changes.len(), 1);
666        assert_eq!(result.changes[0].kind, MusicalChangeKind::Added);
667        assert_eq!(result.changes[0].current.as_ref().unwrap().note, 0);
668
669        let mut notation = reference.clone();
670        notation.parts[0].staves[0].measures[0].voices[0][0].tuplet_boundary =
671            Some(TupletBoundary::Start);
672        let result = compare_musical_positions(&reference, &notation, &Default::default());
673        assert_eq!(result.changes.len(), 1);
674        assert_eq!(result.changes[0].changed_fields, vec!["tuplet_boundary"]);
675        assert_eq!(result.changes[0].domains, vec!["notation"]);
676    }
677
678    #[test]
679    fn adjacent_authored_tuplets_do_not_move_later_notes() {
680        let mut reference = score(vec![
681            note(60, Duration::Eighth),
682            note(62, Duration::Eighth),
683            note(64, Duration::Eighth),
684            note(65, Duration::Eighth),
685            note(67, Duration::Quarter),
686        ]);
687        let voice = &mut reference.parts[0].staves[0].measures[0].voices[0];
688        for note in voice.iter_mut().take(4) {
689            note.tuplet = Some(TupletInfo {
690                actual_notes: 3,
691                normal_notes: 2,
692            });
693        }
694        voice[0].tuplet_boundary = Some(TupletBoundary::Start);
695        voice[1].tuplet_boundary = Some(TupletBoundary::Stop);
696        voice[2].tuplet_boundary = Some(TupletBoundary::Start);
697        voice[3].tuplet_boundary = Some(TupletBoundary::Stop);
698        let mut current = reference.clone();
699        current.parts[0].staves[0].measures[0].voices[0][1].tuplet_boundary = None;
700        let result = compare_musical_positions(&reference, &current, &Default::default());
701        assert_eq!(result.changes.len(), 1);
702        assert_eq!(result.changes[0].current.as_ref().unwrap().note, 1);
703        assert_eq!(result.changes[0].changed_fields, vec!["tuplet_boundary"]);
704    }
705
706    #[test]
707    fn strict_shape_rejects_added_part_but_explicit_index_prefix_reports_its_notes() {
708        let reference = score(vec![note(60, Duration::Whole)]);
709        let mut current = reference.clone();
710        let mut added = current.parts[0].clone();
711        added.staves[0].measures[0].voices[0] = vec![note(48, Duration::Whole)];
712        current.parts.push(added);
713        let strict = compare_musical_positions(&reference, &current, &Default::default());
714        assert!(strict.changes.is_empty());
715        assert_eq!(strict.diagnostics[0].code, "voice-structure-mismatch");
716        let options = MusicalComparisonOptions {
717            structure_policy: StructurePolicy::IndexPrefix,
718            ..Default::default()
719        };
720        let result = compare_musical_positions(&reference, &current, &options);
721        assert_eq!(result.changes.len(), 1);
722        assert_eq!(result.changes[0].kind, MusicalChangeKind::Added);
723        assert_eq!(result.changes[0].current.as_ref().unwrap().part, 1);
724    }
725
726    #[test]
727    fn rest_in_pickup_does_not_shift_the_next_short_measure() {
728        let mut reference = score(vec![note(60, Duration::Quarter)]);
729        reference.parts[0].staves[0].measures[0].actual_length = Some(crate::MeasureLength {
730            numerator: 1,
731            denominator: 4,
732        });
733        let mut second = reference.parts[0].staves[0].measures[0].clone();
734        second.number = 2;
735        second.actual_length = Some(crate::MeasureLength {
736            numerator: 2,
737            denominator: 4,
738        });
739        second.voices[0] = vec![note(62, Duration::Quarter), note(64, Duration::Quarter)];
740        reference.parts[0].staves[0].measures.push(second);
741        let mut current = reference.clone();
742        current.parts[0].staves[0].measures[0].voices[0] =
743            vec![Note::rest(Duration::Eighth), note(60, Duration::Eighth)];
744        let result = compare_musical_positions(&reference, &current, &Default::default());
745        assert!(result.changes.iter().all(|change| {
746            change
747                .current
748                .as_ref()
749                .is_none_or(|address| address.measure == 0)
750        }));
751        assert!(
752            result
753                .changes
754                .iter()
755                .any(|change| change.kind == MusicalChangeKind::Added)
756        );
757    }
758
759    #[test]
760    fn explicit_voice_and_note_correspondence_survive_inserted_part() {
761        let reference = score(vec![note(60, Duration::Quarter)]);
762        let mut current = reference.clone();
763        current.parts.insert(0, reference.parts[0].clone());
764        current.parts[0].staves[0].measures[0].voices[0] = vec![note(48, Duration::Quarter)];
765        let reference_voice = VoiceAddr {
766            part: 0,
767            staff: 0,
768            voice: 0,
769        };
770        let current_voice = VoiceAddr {
771            part: 1,
772            staff: 0,
773            voice: 0,
774        };
775        let options = MusicalComparisonOptions {
776            voice_pairs: vec![VoiceCorrespondence {
777                reference: reference_voice,
778                current: current_voice,
779            }],
780            note_pairs: vec![NoteCorrespondence {
781                reference: note_addr(reference_voice, 0, 0),
782                current: note_addr(current_voice, 0, 0),
783            }],
784            ..Default::default()
785        };
786        let result = compare_musical_positions(&reference, &current, &options);
787        assert!(result.diagnostics.is_empty());
788        assert_eq!(result.changes.len(), 1);
789        assert_eq!(result.changes[0].kind, MusicalChangeKind::Added);
790        assert_eq!(result.changes[0].current.as_ref().unwrap().part, 0);
791    }
792}