1use 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 Strict,
13 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#[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 pub changed_fields: Vec<String>,
70 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(¤t.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, ¤t[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
358pub 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 ¤t.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, ¤t, &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, ¤t, &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, ¤t, &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, ¬ation, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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, ¤t, &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}