1use super::{
2 duration::Duration,
3 notation::{
4 Articulation, Barline, BeamState, ChordDefinition, ChordSymbol, Clef, CrossStaff, Dynamic,
5 FiguredBassFigure, GuitarTechnique, HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind,
6 StyledText, TabPosition, TablatureConfig, TimeSignature, TupletInfo, VerseLyric,
7 },
8 pitch::Pitch,
9};
10use crate::Error;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ScoreMetadata {
16 pub title: String,
17 pub composer: String,
18 pub lyricist: String,
19 pub copyright: String,
20 pub work_number: String,
21 pub movement_title: String,
22}
23
24impl Default for ScoreMetadata {
25 fn default() -> Self {
26 Self {
27 title: "Untitled Score".to_string(),
28 composer: String::new(),
29 lyricist: String::new(),
30 copyright: String::new(),
31 work_number: String::new(),
32 movement_title: String::new(),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScoreSettings {
39 pub tempo_bpm: u16,
40 pub time_signature: TimeSignature,
41 pub key_signature: KeySignature,
42}
43
44impl Default for ScoreSettings {
45 fn default() -> Self {
46 Self {
47 tempo_bpm: 120,
48 time_signature: TimeSignature::default(),
49 key_signature: KeySignature::default(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum PartGroupSymbol {
57 Bracket, Brace, Line, }
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PartGroup {
65 pub first_part: usize,
67 pub last_part: usize,
69 pub symbol: PartGroupSymbol,
70 #[serde(default)]
72 pub barlines_connect: bool,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct StaffGroup {
82 pub first_staff: usize,
84 pub last_staff: usize,
86 pub symbol: PartGroupSymbol,
87 #[serde(default)]
89 pub barlines_connect: bool,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Score {
94 pub id: String,
95 #[serde(default)]
97 pub schema_version: u32,
98 pub metadata: ScoreMetadata,
99 pub settings: ScoreSettings,
100 pub parts: Vec<Part>,
101 #[serde(default)]
102 pub part_groups: Vec<PartGroup>,
103 #[serde(default)]
105 pub texts: Vec<StyledText>,
106 #[serde(default)]
108 pub style_overrides: Vec<ViewStyleOverride>,
109 #[serde(default)]
111 pub object_style_overrides: Vec<ObjectStyleOverride>,
112 #[serde(default)]
114 pub chord_definitions: Vec<ChordDefinition>,
115 #[serde(default)]
117 pub spanners: Vec<NotationSpanner>,
118 #[serde(default)]
120 pub views: Vec<ScoreView>,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub struct ViewStaffRef {
126 pub part: usize,
127 pub staff: usize,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
132#[serde(rename_all = "snake_case")]
133pub enum ViewTranspositionPolicy {
134 #[default]
135 Written,
136 Concert,
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
141pub struct ScoreViewLayoutOverrides {
142 #[serde(default)]
143 pub measures_per_row: Option<usize>,
144 #[serde(default)]
145 pub hidden_staves: Vec<ViewStaffRef>,
146 #[serde(default)]
147 pub system_breaks: Vec<usize>,
148 #[serde(default)]
149 pub page_breaks: Vec<usize>,
150 #[serde(default)]
152 pub style_overrides: Vec<(String, String)>,
153 #[serde(default)]
155 pub typed_style_overrides: Vec<ViewStyleOverride>,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum ViewStyleProperty {
162 StaffSpace,
163 TextScale,
164 AnnotationGap,
165 SystemGap,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
171pub struct ViewStyleOverride {
172 pub property: ViewStyleProperty,
173 pub value: f32,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case", tag = "kind")]
179pub enum ObjectStyleTarget {
180 ScoreText {
181 text_index: usize,
182 },
183 MeasureText {
184 part: usize,
185 staff: usize,
186 measure: usize,
187 text_index: usize,
188 },
189 Note {
190 address: NoteAddr,
191 },
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct StyleImportProvenance {
197 pub format: String,
198 pub source_location: String,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct ObjectStyleOverride {
204 pub target: ObjectStyleTarget,
205 pub property: ViewStyleProperty,
206 pub value: f32,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub provenance: Option<StyleImportProvenance>,
209}
210
211impl ScoreViewLayoutOverrides {
212 pub fn style_value(&self, property: ViewStyleProperty) -> Option<f32> {
214 self.typed_style_overrides
215 .iter()
216 .rev()
217 .find(|override_| override_.property == property)
218 .map(|override_| override_.value)
219 }
220
221 pub fn resolved_style(&self) -> ViewStyle {
223 let mut style = ViewStyle::default();
224 apply_style_overrides(&mut style, &self.typed_style_overrides);
225 style
226 }
227}
228
229fn apply_style_overrides(style: &mut ViewStyle, overrides: &[ViewStyleOverride]) {
230 for override_ in overrides {
231 match override_.property {
232 ViewStyleProperty::StaffSpace => style.staff_space = override_.value,
233 ViewStyleProperty::TextScale => style.text_scale = override_.value,
234 ViewStyleProperty::AnnotationGap => style.annotation_gap = override_.value,
235 ViewStyleProperty::SystemGap => style.system_gap = override_.value,
236 }
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
245pub struct ViewStyle {
246 pub staff_space: f32,
247 pub text_scale: f32,
248 pub annotation_gap: f32,
249 pub system_gap: f32,
250}
251
252impl Default for ViewStyle {
253 fn default() -> Self {
254 Self {
255 staff_space: 1.0,
256 text_scale: 1.0,
257 annotation_gap: 1.0,
258 system_gap: 2.0,
259 }
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265pub struct ViewStaffKindOverride {
266 pub staff: ViewStaffRef,
267 pub kind: StaffKind,
268}
269
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272pub struct ScoreView {
273 pub id: String,
274 pub name: String,
275 #[serde(default)]
276 pub parts: Vec<usize>,
277 #[serde(default)]
278 pub transposition_policy: ViewTranspositionPolicy,
279 #[serde(default)]
281 pub staff_kind_overrides: Vec<ViewStaffKindOverride>,
282 #[serde(default)]
283 pub layout: ScoreViewLayoutOverrides,
284}
285
286impl ScoreView {
287 pub fn linked_part(id: impl Into<String>, name: impl Into<String>, part: usize) -> Self {
289 Self {
290 id: id.into(),
291 name: name.into(),
292 parts: vec![part],
293 transposition_policy: ViewTranspositionPolicy::Written,
294 staff_kind_overrides: Vec::new(),
295 layout: ScoreViewLayoutOverrides::default(),
296 }
297 }
298
299 pub fn linked_tablature_staff(
301 id: impl Into<String>,
302 name: impl Into<String>,
303 part: usize,
304 staff: usize,
305 ) -> Self {
306 let mut view = Self::linked_part(id, name, part);
307 view.staff_kind_overrides.push(ViewStaffKindOverride {
308 staff: ViewStaffRef { part, staff },
309 kind: StaffKind::Tablature,
310 });
311 view
312 }
313
314 pub fn linked_standard_staff(
316 id: impl Into<String>,
317 name: impl Into<String>,
318 part: usize,
319 staff: usize,
320 ) -> Self {
321 let mut view = Self::linked_part(id, name, part);
322 view.staff_kind_overrides.push(ViewStaffKindOverride {
323 staff: ViewStaffRef { part, staff },
324 kind: StaffKind::Standard,
325 });
326 view
327 }
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
332pub enum NotationSpannerKind {
333 Slur,
334 Glissando,
335 TrillLine,
336 Pedal,
337 Ottava,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct NotationSpanner {
343 pub id: String,
344 pub kind: NotationSpannerKind,
345 pub start: NoteAddr,
346 pub end: NoteAddr,
347 #[serde(default)]
348 pub number: Option<u16>,
349 #[serde(default)]
350 pub line_type: Option<String>,
351 #[serde(default)]
352 pub text: Option<String>,
353 #[serde(default)]
354 pub placement: Option<String>,
355 #[serde(default)]
356 pub ottava_size: Option<u8>,
357 #[serde(default)]
359 pub ottava_type: Option<String>,
360}
361
362impl Default for Score {
363 fn default() -> Self {
364 let mut part = Part::new("Piano", "Pno.");
365 part.staves.push(Staff::new(Clef::Treble));
366 for _ in 0..4 {
367 part.staves[0].measures.push(Measure::empty(4, 4));
368 }
369 Self {
370 id: Uuid::new_v4().to_string(),
371 schema_version: 1,
372 metadata: ScoreMetadata::default(),
373 settings: ScoreSettings::default(),
374 parts: vec![part],
375 part_groups: Vec::new(),
376 texts: Vec::new(),
377 style_overrides: Vec::new(),
378 object_style_overrides: Vec::new(),
379 chord_definitions: Vec::new(),
380 spanners: Vec::new(),
381 views: Vec::new(),
382 }
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
388pub enum ScoreTemplate {
389 Solo,
391 Piano,
393 StringQuartet,
395 StringOrchestra,
397 BrassQuintet,
399}
400
401impl Score {
402 pub fn resolved_view_style(&self, layout: &ScoreViewLayoutOverrides) -> ViewStyle {
404 let mut style = ViewStyle::default();
405 apply_style_overrides(&mut style, &self.style_overrides);
406 apply_style_overrides(&mut style, &layout.typed_style_overrides);
407 style
408 }
409
410 pub fn new(
411 title: &str,
412 tempo_bpm: u16,
413 numerator: u8,
414 denominator: u8,
415 fifths: i8,
416 measure_count: u32,
417 ) -> Self {
418 let mut score = Score::default();
419 score.metadata.title = title.to_string();
420 score.settings.tempo_bpm = tempo_bpm;
421 score.settings.time_signature = TimeSignature {
422 numerator,
423 denominator,
424 };
425 score.settings.key_signature = KeySignature {
426 fifths,
427 mode: "major".to_string(),
428 };
429
430 score.parts[0].staves[0].measures.clear();
431 for i in 0..measure_count {
432 let mut m = Measure::empty(numerator, denominator);
433 m.number = i + 1;
434 score.parts[0].staves[0].measures.push(m);
435 }
436 score
437 }
438
439 pub fn template(kind: ScoreTemplate) -> Self {
444 fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
445 (0..count)
446 .map(|i| {
447 let mut m = Measure::empty(num, den);
448 m.number = i + 1;
449 m
450 })
451 .collect()
452 }
453 fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
454 let mut p = Part::new(name, short);
455 p.midi_program = program;
456 let mut s = Staff::new(clef);
457 s.measures = measures(4, 4, 4);
458 p.staves.push(s);
459 p
460 }
461
462 let mut score = Score {
463 id: uuid::Uuid::new_v4().to_string(),
464 schema_version: 1,
465 metadata: ScoreMetadata::default(),
466 settings: ScoreSettings::default(),
467 parts: Vec::new(),
468 part_groups: Vec::new(),
469 texts: Vec::new(),
470 style_overrides: Vec::new(),
471 object_style_overrides: Vec::new(),
472 chord_definitions: Vec::new(),
473 spanners: Vec::new(),
474 views: Vec::new(),
475 };
476
477 match kind {
478 ScoreTemplate::Solo => {
479 score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
480 }
481 ScoreTemplate::Piano => {
482 let mut p = Part::new("Piano", "Pno.");
483 p.midi_program = 0;
484 let mut treble = Staff::new(Clef::Treble);
485 treble.measures = measures(4, 4, 4);
486 let mut bass = Staff::new(Clef::Bass);
487 bass.measures = measures(4, 4, 4);
488 p.staves.push(treble);
489 p.staves.push(bass);
490 score.parts.push(p);
491 }
492 ScoreTemplate::StringQuartet => {
493 score
494 .parts
495 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
496 score
497 .parts
498 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
499 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
500 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
501 }
502 ScoreTemplate::StringOrchestra => {
503 score
504 .parts
505 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
506 score
507 .parts
508 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
509 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
510 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
511 score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
512 }
513 ScoreTemplate::BrassQuintet => {
514 score
515 .parts
516 .push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
517 score
518 .parts
519 .push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
520 score
521 .parts
522 .push(part("French Horn", "Hn.", Clef::Treble, 60));
523 score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
524 score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
525 }
526 }
527 score
528 }
529
530 pub fn measure_count(&self) -> usize {
531 self.parts
532 .first()
533 .and_then(|p| p.staves.first())
534 .map(|s| s.measures.len())
535 .unwrap_or(0)
536 }
537
538 pub fn section_range(
543 &self,
544 measure_index: usize,
545 ) -> Result<std::ops::RangeInclusive<usize>, Error> {
546 let measures = self
547 .parts
548 .first()
549 .and_then(|part| part.staves.first())
550 .map(|staff| &staff.measures)
551 .ok_or(Error::MeasureNotFound(measure_index))?;
552 if measure_index >= measures.len() {
553 return Err(Error::MeasureNotFound(measure_index));
554 }
555 let start = (0..=measure_index)
556 .rev()
557 .find(|&index| measures[index].section_break)
558 .unwrap_or(0);
559 let end = ((measure_index + 1)..measures.len())
560 .find(|&index| measures[index].section_break)
561 .map(|index| index - 1)
562 .unwrap_or(measures.len() - 1);
563 Ok(start..=end)
564 }
565
566 pub fn statistics(&self) -> ScoreStats {
568 let measure_count = self.measure_count();
569 let part_count = self.parts.len();
570
571 let seq = measure_sequence(self);
573 let total_beats: f64 = self
574 .parts
575 .first()
576 .and_then(|p| p.staves.first())
577 .map(|s| {
578 seq.iter()
579 .filter_map(|&idx| s.measures.get(idx))
580 .flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
581 .map(|n| n.beats())
582 .sum()
583 })
584 .unwrap_or(0.0);
585
586 let mut note_count = 0usize;
587 let mut rest_count = 0usize;
588 for part in &self.parts {
589 for staff in &part.staves {
590 for measure in &staff.measures {
591 for voice in &measure.voices {
592 for note in voice {
593 if note.is_rest {
594 rest_count += 1;
595 } else {
596 note_count += 1;
597 }
598 }
599 }
600 }
601 }
602 }
603
604 let bpm = self.settings.tempo_bpm as f64;
605 let estimated_duration_secs = if bpm > 0.0 {
606 total_beats / bpm * 60.0
607 } else {
608 0.0
609 };
610
611 ScoreStats {
612 measure_count,
613 note_count,
614 rest_count,
615 part_count,
616 estimated_duration_secs,
617 }
618 }
619
620 pub fn resolve_view(&self, view_id: &str) -> Result<Score, Error> {
626 let view = self
627 .views
628 .iter()
629 .find(|view| view.id == view_id)
630 .ok_or_else(|| {
631 Error::InvalidCommand(format!("score view '{view_id}' does not exist"))
632 })?;
633 let mut source_to_target = vec![None; self.parts.len()];
634 let mut parts = Vec::with_capacity(view.parts.len());
635 for &source_index in &view.parts {
636 let source = self
637 .parts
638 .get(source_index)
639 .ok_or(Error::PartNotFound(source_index))?;
640 if source_to_target[source_index].is_some() {
641 return Err(Error::InvalidCommand(format!(
642 "score view '{}' selects part {} more than once",
643 view.id, source_index
644 )));
645 }
646 source_to_target[source_index] = Some(parts.len());
647 parts.push(source.clone());
648 }
649 for override_ in &view.staff_kind_overrides {
650 let target_part = source_to_target
651 .get(override_.staff.part)
652 .copied()
653 .flatten()
654 .ok_or_else(|| {
655 Error::InvalidCommand(
656 "view overrides a staff outside its selected parts".into(),
657 )
658 })?;
659 let target_staff = parts[target_part]
660 .staves
661 .get_mut(override_.staff.staff)
662 .ok_or_else(|| {
663 Error::InvalidCommand(format!(
664 "view overrides staff {} outside part {}",
665 override_.staff.staff, override_.staff.part
666 ))
667 })?;
668 if override_.kind == StaffKind::Tablature && target_staff.tablature.is_none() {
669 return Err(Error::InvalidCommand(
670 "tablature view requires a tablature configuration on its source staff".into(),
671 ));
672 }
673 target_staff.presentation.kind = override_.kind;
674 }
675 let spanners = self
676 .spanners
677 .iter()
678 .filter_map(|spanner| {
679 let start = source_to_target
680 .get(spanner.start.part)
681 .copied()
682 .flatten()?;
683 let end = source_to_target.get(spanner.end.part).copied().flatten()?;
684 let mut projected = spanner.clone();
685 projected.start.part = start;
686 projected.end.part = end;
687 Some(projected)
688 })
689 .collect();
690 let part_groups = self
691 .part_groups
692 .iter()
693 .filter_map(|group| {
694 let first = source_to_target.get(group.first_part).copied().flatten()?;
695 let last = source_to_target.get(group.last_part).copied().flatten()?;
696 Some(PartGroup {
697 first_part: first,
698 last_part: last,
699 symbol: group.symbol.clone(),
700 barlines_connect: group.barlines_connect,
701 })
702 })
703 .collect();
704 let mut projected_view = view.clone();
705 projected_view.parts = (0..parts.len()).collect();
706 for reference in &mut projected_view.layout.hidden_staves {
707 reference.part = source_to_target
708 .get(reference.part)
709 .copied()
710 .flatten()
711 .ok_or_else(|| {
712 Error::InvalidCommand("view hides a part outside its selection".into())
713 })?;
714 }
715 for override_ in &mut projected_view.staff_kind_overrides {
716 override_.staff.part = source_to_target
717 .get(override_.staff.part)
718 .copied()
719 .flatten()
720 .ok_or_else(|| {
721 Error::InvalidCommand("view overrides a staff outside its selection".into())
722 })?;
723 }
724 Ok(Score {
725 id: Uuid::new_v4().to_string(),
726 schema_version: self.schema_version,
727 metadata: self.metadata.clone(),
728 settings: self.settings.clone(),
729 parts,
730 part_groups,
731 texts: self.texts.clone(),
732 style_overrides: self.style_overrides.clone(),
733 object_style_overrides: self.object_style_overrides.clone(),
734 chord_definitions: self.chord_definitions.clone(),
735 spanners,
736 views: vec![projected_view],
737 })
738 }
739
740 pub fn extract_part(&self, part_index: usize) -> Option<Score> {
743 let part = self.parts.get(part_index)?.clone();
744 let spanners = self
745 .spanners
746 .iter()
747 .filter(|spanner| spanner.start.part == part_index && spanner.end.part == part_index)
748 .cloned()
749 .map(|mut spanner| {
750 spanner.start.part = 0;
751 spanner.end.part = 0;
752 spanner
753 })
754 .collect();
755 Some(Score {
756 id: Uuid::new_v4().to_string(),
757 schema_version: 1,
758 metadata: self.metadata.clone(),
759 settings: self.settings.clone(),
760 parts: vec![part],
761 part_groups: Vec::new(),
762 texts: self.texts.clone(),
763 style_overrides: self.style_overrides.clone(),
764 object_style_overrides: self.object_style_overrides.clone(),
765 chord_definitions: self.chord_definitions.clone(),
766 spanners,
767 views: Vec::new(),
768 })
769 }
770
771 pub fn extract_part_checked(&self, part_index: usize) -> Result<Score, Error> {
773 if !super::validate::validate(self).is_valid() {
774 return Err(Error::InvalidScore);
775 }
776 let extracted = self
777 .extract_part(part_index)
778 .ok_or(Error::PartNotFound(part_index))?;
779 if !super::validate::validate(&extracted).is_valid() {
780 return Err(Error::InvalidScore);
781 }
782 Ok(extracted)
783 }
784
785 pub fn merge(&self, other: &Score) -> Score {
789 let self_count = self.measure_count();
790 let other_count = other.measure_count();
791 let max_count = self_count.max(other_count);
792 let ts = self.settings.time_signature.clone();
793
794 let pad = |mut part: Part, from: usize| -> Part {
795 for staff in &mut part.staves {
796 for i in from..max_count {
797 let mut m = Measure::empty(ts.numerator, ts.denominator);
798 m.number = i as u32 + 1;
799 staff.measures.push(m);
800 }
801 }
802 part
803 };
804
805 let mut parts: Vec<Part> = self
806 .parts
807 .iter()
808 .cloned()
809 .map(|p| pad(p, self_count))
810 .collect();
811 for p in &other.parts {
812 parts.push(pad(p.clone(), other_count));
813 }
814 let self_part_count = self.parts.len();
815 let mut spanners = self.spanners.clone();
816 spanners.extend(other.spanners.iter().cloned().map(|mut spanner| {
817 spanner.start.part += self_part_count;
818 spanner.end.part += self_part_count;
819 spanner
820 }));
821
822 Score {
823 id: Uuid::new_v4().to_string(),
824 schema_version: 1,
825 metadata: self.metadata.clone(),
826 settings: self.settings.clone(),
827 parts,
828 part_groups: Vec::new(),
829 texts: self.texts.clone(),
830 style_overrides: self.style_overrides.clone(),
831 object_style_overrides: self.object_style_overrides.clone(),
832 chord_definitions: self.chord_definitions.clone(),
833 spanners,
834 views: Vec::new(),
835 }
836 }
837
838 pub fn merge_checked(&self, other: &Score) -> Result<Score, Error> {
840 if !super::validate::validate(self).is_valid()
841 || !super::validate::validate(other).is_valid()
842 {
843 return Err(Error::InvalidScore);
844 }
845 let merged = self.merge(other);
846 if !super::validate::validate(&merged).is_valid() {
847 return Err(Error::InvalidScore);
848 }
849 Ok(merged)
850 }
851}
852
853#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
855pub struct ScoreStats {
856 pub measure_count: usize,
857 pub note_count: usize,
859 pub rest_count: usize,
860 pub part_count: usize,
861 pub estimated_duration_secs: f64,
863}
864
865use super::pitch::Step;
868use super::repeat::measure_sequence;
869
870pub fn assign_tablature_positions(score: &mut Score) -> usize {
882 const MAX_FRET: i16 = 24;
883 let mut assigned = 0;
884
885 for part in &mut score.parts {
886 for staff in &mut part.staves {
887 if staff.tablature.is_none() {
888 continue;
889 }
890 let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
891 .map(|measure_index| staff.tablature_at(measure_index))
892 .collect();
893
894 for (measure_index, measure) in staff.measures.iter_mut().enumerate() {
895 let Some(tab) = configurations[measure_index].as_ref() else {
896 continue;
897 };
898 for voice in &mut measure.voices {
899 for note in voice.iter_mut() {
900 if note.is_rest
901 || note.tab_position.is_some()
902 || !note.tab_positions.is_empty()
903 || note.pitches.is_empty()
904 {
905 continue;
906 }
907 let pitches: Vec<i16> = note.pitches.iter().map(Pitch::to_midi).collect();
908 let Some(positions) = best_tablature_assignment(
909 &pitches,
910 &tab.tuning_midi,
911 tab.lines as usize,
912 i16::from(tab.capo),
913 MAX_FRET,
914 ) else {
915 continue;
916 };
917
918 note.tab_position = positions.first().cloned();
919 note.tab_positions = positions;
920 note.string_number = note.tab_position.as_ref().map(|p| p.string);
921 assigned += 1;
922 }
923 }
924 }
925 }
926 }
927
928 assigned
929}
930
931pub fn optimize_tablature_positions(score: &mut Score) -> usize {
938 const MAX_FRET: i16 = 24;
939 let mut assigned = 0;
940
941 for part in &mut score.parts {
942 for staff in &mut part.staves {
943 if staff.tablature.is_none() {
944 continue;
945 }
946 let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
947 .map(|measure_index| staff.tablature_at(measure_index))
948 .collect();
949 for voice in 0..4 {
950 let mut locations = Vec::new();
952 for (measure_index, measure) in staff.measures.iter().enumerate() {
953 for (note_index, note) in measure.voices[voice].iter().enumerate() {
954 if !note.is_rest && !note.pitches.is_empty() {
955 locations.push((measure_index, note_index, note.clone()));
956 }
957 }
958 }
959 let candidates: Vec<Vec<Vec<TabPosition>>> = locations
960 .iter()
961 .map(|(measure_index, _, note)| {
962 if let Some(positions) = if !note.tab_positions.is_empty() {
963 Some(note.tab_positions.clone())
964 } else {
965 note.tab_position.clone().map(|position| vec![position])
966 } {
967 vec![positions]
968 } else {
969 let Some(tab) = configurations[*measure_index].as_ref() else {
970 return Vec::new();
971 };
972 tablature_assignments(
973 ¬e.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
974 &tab.tuning_midi,
975 tab.lines as usize,
976 i16::from(tab.capo),
977 MAX_FRET,
978 )
979 }
980 })
981 .collect();
982 if candidates.iter().any(Vec::is_empty) {
983 continue;
984 }
985
986 let mut costs: Vec<Vec<(u32, Option<usize>)>> = candidates
987 .iter()
988 .map(|events| vec![(u32::MAX, None); events.len()])
989 .collect();
990 for (candidate_index, candidate) in candidates[0].iter().enumerate() {
991 costs[0][candidate_index] = (tablature_load(candidate), None);
992 }
993 for event_index in 1..candidates.len() {
994 for (candidate_index, candidate) in candidates[event_index].iter().enumerate() {
995 let load = tablature_load(candidate);
996 for (previous_index, previous) in
997 candidates[event_index - 1].iter().enumerate()
998 {
999 let previous_cost = costs[event_index - 1][previous_index].0;
1000 let cost = previous_cost
1001 .saturating_add(load)
1002 .saturating_add(tablature_movement(previous, candidate));
1003 if cost < costs[event_index][candidate_index].0 {
1004 costs[event_index][candidate_index] = (cost, Some(previous_index));
1005 }
1006 }
1007 }
1008 }
1009 let mut selected = vec![0; candidates.len()];
1010 if let Some((last, _)) = costs.last().and_then(|row| {
1011 row.iter()
1012 .enumerate()
1013 .min_by_key(|(index, (cost, _))| (*cost, *index))
1014 }) {
1015 selected[candidates.len() - 1] = last;
1016 for event_index in (1..candidates.len()).rev() {
1017 selected[event_index - 1] =
1018 costs[event_index][selected[event_index]].1.unwrap_or(0);
1019 }
1020 }
1021
1022 for (((measure_index, note_index, original), event_candidates), selected_index) in
1023 locations.into_iter().zip(candidates).zip(selected)
1024 {
1025 if original.tab_position.is_none() && original.tab_positions.is_empty() {
1026 let note = &mut staff.measures[measure_index].voices[voice][note_index];
1027 let positions = event_candidates[selected_index].clone();
1028 note.tab_position = positions.first().cloned();
1029 note.tab_positions = positions;
1030 note.string_number = note.tab_position.as_ref().map(|p| p.string);
1031 assigned += 1;
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 assigned
1039}
1040
1041fn tablature_load(positions: &[TabPosition]) -> u32 {
1042 let sum: u32 = positions
1043 .iter()
1044 .map(|position| u32::from(position.fret))
1045 .sum();
1046 let min = positions
1047 .iter()
1048 .map(|position| position.fret)
1049 .min()
1050 .unwrap_or(0);
1051 let max = positions
1052 .iter()
1053 .map(|position| position.fret)
1054 .max()
1055 .unwrap_or(0);
1056 let span = max - min;
1057 let stretch_penalty = span.saturating_sub(4) as u32 * 12;
1060 sum + u32::from(span) * 2 + stretch_penalty
1061}
1062
1063fn tablature_movement(previous: &[TabPosition], current: &[TabPosition]) -> u32 {
1064 previous
1065 .iter()
1066 .zip(current)
1067 .map(|(a, b)| u32::from(a.fret.abs_diff(b.fret)) + u32::from(a.string.abs_diff(b.string)))
1068 .sum()
1069}
1070
1071fn tablature_assignments(
1072 pitches: &[i16],
1073 tuning: &[i16],
1074 lines: usize,
1075 capo: i16,
1076 max_fret: i16,
1077) -> Vec<Vec<TabPosition>> {
1078 #[allow(clippy::too_many_arguments)]
1079 fn visit(
1080 pitches: &[i16],
1081 tuning: &[i16],
1082 lines: usize,
1083 capo: i16,
1084 max_fret: i16,
1085 index: usize,
1086 used: &mut [bool],
1087 current: &mut Vec<TabPosition>,
1088 output: &mut Vec<Vec<TabPosition>>,
1089 ) {
1090 if index == pitches.len() {
1091 output.push(current.clone());
1092 return;
1093 }
1094 for (string, open) in tuning.iter().enumerate().take(lines) {
1095 if used[string] {
1096 continue;
1097 }
1098 let fret = pitches[index] - *open - capo;
1099 if !(0..=max_fret).contains(&fret) {
1100 continue;
1101 }
1102 used[string] = true;
1103 current.push(TabPosition {
1104 string: (string + 1) as u8,
1105 fret: fret as u8,
1106 });
1107 visit(
1108 pitches,
1109 tuning,
1110 lines,
1111 capo,
1112 max_fret,
1113 index + 1,
1114 used,
1115 current,
1116 output,
1117 );
1118 current.pop();
1119 used[string] = false;
1120 }
1121 }
1122
1123 if pitches.is_empty() || pitches.len() > lines {
1124 return Vec::new();
1125 }
1126 let mut output = Vec::new();
1127 visit(
1128 pitches,
1129 tuning,
1130 lines,
1131 capo,
1132 max_fret,
1133 0,
1134 &mut vec![false; lines],
1135 &mut Vec::new(),
1136 &mut output,
1137 );
1138 output
1139}
1140
1141fn best_tablature_assignment(
1142 pitches: &[i16],
1143 tuning: &[i16],
1144 lines: usize,
1145 capo: i16,
1146 max_fret: i16,
1147) -> Option<Vec<TabPosition>> {
1148 type Assignment = (i16, i16, i16, Vec<u8>, Vec<TabPosition>);
1149
1150 #[allow(clippy::too_many_arguments)]
1151 fn search(
1152 pitches: &[i16],
1153 tuning: &[i16],
1154 lines: usize,
1155 capo: i16,
1156 max_fret: i16,
1157 index: usize,
1158 used: &mut [bool],
1159 current: &mut Vec<TabPosition>,
1160 best: &mut Option<Assignment>,
1161 ) {
1162 if index == pitches.len() {
1163 let sum: i16 = current.iter().map(|p| i16::from(p.fret)).sum();
1164 let min = current.iter().map(|p| p.fret).min().unwrap_or(0);
1165 let max = current.iter().map(|p| p.fret).max().unwrap_or(0);
1166 let strings: Vec<u8> = current.iter().map(|p| p.string).collect();
1167 let candidate = (
1168 sum,
1169 i16::from(max) - i16::from(min),
1170 i16::from(max),
1171 strings,
1172 current.clone(),
1173 );
1174 if best.as_ref().is_none_or(|existing| {
1175 (candidate.0, candidate.1, candidate.2, &candidate.3)
1176 < (existing.0, existing.1, existing.2, &existing.3)
1177 }) {
1178 *best = Some(candidate);
1179 }
1180 return;
1181 }
1182
1183 for (string, open) in tuning.iter().enumerate().take(lines) {
1184 if used[string] {
1185 continue;
1186 }
1187 let fret = pitches[index] - *open - capo;
1188 if !(0..=max_fret).contains(&fret) {
1189 continue;
1190 }
1191 used[string] = true;
1192 current.push(TabPosition {
1193 string: (string + 1) as u8,
1194 fret: fret as u8,
1195 });
1196 search(
1197 pitches,
1198 tuning,
1199 lines,
1200 capo,
1201 max_fret,
1202 index + 1,
1203 used,
1204 current,
1205 best,
1206 );
1207 current.pop();
1208 used[string] = false;
1209 }
1210 }
1211
1212 if pitches.len() > lines {
1213 return None;
1214 }
1215 let mut used = vec![false; lines];
1216 let mut current = Vec::with_capacity(pitches.len());
1217 let mut best = None;
1218 search(
1219 pitches,
1220 tuning,
1221 lines,
1222 capo,
1223 max_fret,
1224 0,
1225 &mut used,
1226 &mut current,
1227 &mut best,
1228 );
1229 best.map(|(_, _, _, _, positions)| positions)
1230}
1231
1232pub fn transpose(score: &Score, semitones: i8) -> Score {
1236 if semitones == 0 {
1237 return score.clone();
1238 }
1239 let mut out = score.clone();
1240 out.settings.key_signature.fifths = transpose_fifths(
1241 score.settings.key_signature.fifths,
1242 &score.settings.key_signature.mode,
1243 semitones,
1244 );
1245 for part in &mut out.parts {
1246 for staff in &mut part.staves {
1247 for measure in &mut staff.measures {
1248 if let Some(ref mut ks) = measure.key_sig {
1249 ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
1250 }
1251 for voice in &mut measure.voices {
1252 for note in voice.iter_mut() {
1253 for pitch in note.pitches.iter_mut() {
1254 *pitch = transpose_pitch(pitch, semitones);
1255 }
1256 }
1257 }
1258 }
1259 }
1260 }
1261 out
1262}
1263
1264pub fn transpose_checked(score: &Score, semitones: i8) -> Result<Score, Error> {
1266 if !super::validate::validate(score).is_valid() {
1267 return Err(Error::InvalidScore);
1268 }
1269 let transposed = transpose(score, semitones);
1270 if !super::validate::validate(&transposed).is_valid() {
1271 return Err(Error::InvalidScore);
1272 }
1273 Ok(transposed)
1274}
1275
1276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1278#[serde(rename_all = "snake_case")]
1279pub enum RegionalTranspositionTarget {
1280 Written,
1282 Concert,
1284}
1285
1286pub fn transpose_staff_region_checked(
1289 score: &Score,
1290 part_index: usize,
1291 staff_index: usize,
1292 start_measure: usize,
1293 end_measure: usize,
1294 semitones: i8,
1295 target: RegionalTranspositionTarget,
1296) -> Result<Score, Error> {
1297 if !super::validate::validate(score).is_valid() {
1298 return Err(Error::InvalidScore);
1299 }
1300 let source_staff = score
1301 .parts
1302 .get(part_index)
1303 .ok_or(Error::PartNotFound(part_index))?
1304 .staves
1305 .get(staff_index)
1306 .ok_or(Error::StaffNotFound(staff_index))?;
1307 if start_measure >= end_measure || end_measure > source_staff.measures.len() {
1308 return Err(Error::InvalidCommand(format!(
1309 "invalid measure range {start_measure}..{end_measure}"
1310 )));
1311 }
1312 if target == RegionalTranspositionTarget::Concert
1313 && (start_measure != 0 || end_measure != source_staff.measures.len())
1314 {
1315 return Err(Error::InvalidCommand(
1316 "concert-pitch staff transposition requires the full staff range".into(),
1317 ));
1318 }
1319 let mut transformed = score.clone();
1320 let staff = &mut transformed.parts[part_index].staves[staff_index];
1321 match target {
1322 RegionalTranspositionTarget::Written => {
1323 for measure in &mut staff.measures[start_measure..end_measure] {
1324 if let Some(key) = &mut measure.key_sig {
1325 key.fifths = transpose_fifths(key.fifths, &key.mode, semitones);
1326 }
1327 for voice in &mut measure.voices {
1328 for note in voice {
1329 for pitch in &mut note.pitches {
1330 *pitch = transpose_pitch(pitch, semitones);
1331 }
1332 }
1333 }
1334 }
1335 }
1336 RegionalTranspositionTarget::Concert => {
1337 staff.transpose_semitones = staff.transpose_semitones.saturating_add(semitones);
1338 }
1339 }
1340 if !super::validate::validate(&transformed).is_valid() {
1341 return Err(Error::InvalidScore);
1342 }
1343 Ok(transformed)
1344}
1345
1346fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
1347 let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
1348 let pc = new_midi % 12;
1349 let oct = (new_midi / 12) as i8 - 1;
1350 let (step, alter): (Step, i8) = if semitones >= 0 {
1351 match pc {
1352 0 => (Step::C, 0),
1353 1 => (Step::C, 1),
1354 2 => (Step::D, 0),
1355 3 => (Step::D, 1),
1356 4 => (Step::E, 0),
1357 5 => (Step::F, 0),
1358 6 => (Step::F, 1),
1359 7 => (Step::G, 0),
1360 8 => (Step::G, 1),
1361 9 => (Step::A, 0),
1362 10 => (Step::A, 1),
1363 11 => (Step::B, 0),
1364 _ => (Step::C, 0),
1365 }
1366 } else {
1367 match pc {
1368 0 => (Step::C, 0),
1369 1 => (Step::D, -1),
1370 2 => (Step::D, 0),
1371 3 => (Step::E, -1),
1372 4 => (Step::E, 0),
1373 5 => (Step::F, 0),
1374 6 => (Step::G, -1),
1375 7 => (Step::G, 0),
1376 8 => (Step::A, -1),
1377 9 => (Step::A, 0),
1378 10 => (Step::B, -1),
1379 11 => (Step::B, 0),
1380 _ => (Step::C, 0),
1381 }
1382 };
1383 Pitch::with_microtone(step, oct, alter, pitch.microtone_cents)
1384}
1385
1386fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
1392 let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
1393 let tonic_pc = if mode == "minor" {
1394 ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
1395 } else {
1396 tonic_major_pc
1397 };
1398 let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
1399 let major_tonic = if mode == "minor" {
1400 ((new_tonic as i32 + 3).rem_euclid(12)) as u8
1401 } else {
1402 new_tonic
1403 };
1404 let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
1405 if raw > 6 { raw - 12 } else { raw }
1406}
1407
1408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1410pub struct InstrumentRange {
1411 pub lowest: u8,
1412 pub highest: u8,
1413}
1414
1415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1422pub struct InstrumentDefinition {
1423 pub id: String,
1425 #[serde(default)]
1426 pub name: String,
1427 #[serde(default)]
1428 pub short_name: String,
1429 #[serde(default)]
1430 pub family: Option<String>,
1431 #[serde(default)]
1432 pub transpose_semitones: i8,
1433 #[serde(default)]
1434 pub written_range: Option<InstrumentRange>,
1435 #[serde(default)]
1436 pub sounding_range: Option<InstrumentRange>,
1437 #[serde(default)]
1438 pub default_clefs: Vec<Clef>,
1439 #[serde(default = "default_instrument_staff_count")]
1440 pub staff_count: u8,
1441 #[serde(default)]
1442 pub staff_kind: StaffKind,
1443 #[serde(default)]
1444 pub midi_channel: u8,
1445 #[serde(default)]
1446 pub midi_program: u8,
1447 #[serde(default)]
1448 pub percussion_map_id: Option<String>,
1449}
1450
1451const fn default_instrument_staff_count() -> u8 {
1452 1
1453}
1454
1455impl InstrumentDefinition {
1456 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
1458 Self {
1459 id: id.into(),
1460 name: name.into(),
1461 short_name: String::new(),
1462 family: None,
1463 transpose_semitones: 0,
1464 written_range: None,
1465 sounding_range: None,
1466 default_clefs: Vec::new(),
1467 staff_count: 1,
1468 staff_kind: StaffKind::Standard,
1469 midi_channel: 0,
1470 midi_program: 0,
1471 percussion_map_id: None,
1472 }
1473 }
1474}
1475
1476#[derive(Debug, Clone, Serialize, Deserialize)]
1477pub struct Part {
1478 pub id: String,
1479 pub name: String,
1480 pub short_name: String,
1481 pub staves: Vec<Staff>,
1482 #[serde(default)]
1484 pub midi_channel: u8,
1485 #[serde(default)]
1487 pub midi_program: u8,
1488 #[serde(default)]
1490 pub midi_pitch_bends: Vec<MidiPitchBend>,
1491 #[serde(default)]
1493 pub midi_control_changes: Vec<MidiControlChange>,
1494 #[serde(default)]
1496 pub midi_program_changes: Vec<MidiProgramChange>,
1497 #[serde(default)]
1499 pub midi_aftertouch: Vec<MidiAftertouch>,
1500 #[serde(default)]
1502 pub percussion_instruments: Vec<PercussionInstrument>,
1503 #[serde(default)]
1505 pub staff_groups: Vec<StaffGroup>,
1506 #[serde(default)]
1508 pub instrument: Option<InstrumentDefinition>,
1509}
1510
1511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1512pub struct MidiPitchBend {
1513 pub tick: u64,
1514 pub channel: u8,
1515 pub value: i16,
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1520pub struct MidiControlChange {
1521 pub tick: u64,
1522 pub channel: u8,
1523 pub controller: u8,
1525 pub value: u8,
1527}
1528
1529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1530pub struct MidiProgramChange {
1531 pub tick: u64,
1532 pub channel: u8,
1533 pub program: u8,
1535}
1536
1537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1538pub struct MidiAftertouch {
1539 pub tick: u64,
1540 pub channel: u8,
1541 pub key: Option<u8>,
1543 pub value: u8,
1545}
1546
1547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1553pub struct PercussionInstrument {
1554 pub id: String,
1556 #[serde(default)]
1557 pub name: Option<String>,
1558 #[serde(default)]
1560 pub midi_unpitched: Option<u8>,
1561 #[serde(default)]
1563 pub staff_position: Option<i8>,
1564 #[serde(default)]
1566 pub notehead: Option<NoteHead>,
1567 #[serde(default)]
1569 pub preferred_voice: Option<u8>,
1570 #[serde(default)]
1572 pub techniques: Vec<String>,
1573}
1574
1575impl Part {
1576 pub fn new(name: &str, short_name: &str) -> Self {
1577 Self {
1578 id: Uuid::new_v4().to_string(),
1579 name: name.to_string(),
1580 short_name: short_name.to_string(),
1581 staves: Vec::new(),
1582 midi_channel: 0,
1583 midi_program: 0,
1584 midi_pitch_bends: Vec::new(),
1585 midi_control_changes: Vec::new(),
1586 midi_program_changes: Vec::new(),
1587 midi_aftertouch: Vec::new(),
1588 percussion_instruments: Vec::new(),
1589 staff_groups: Vec::new(),
1590 instrument: None,
1591 }
1592 }
1593
1594 pub fn percussion_instrument_for_note(&self, note: &Note) -> Option<&PercussionInstrument> {
1602 if !note.is_unpitched {
1603 return None;
1604 }
1605 if let Some(instrument_id) = note.instrument_id.as_deref() {
1606 return self
1607 .percussion_instruments
1608 .iter()
1609 .find(|instrument| instrument.id == instrument_id);
1610 }
1611 let midi_key = u8::try_from(note.pitches.first()?.to_midi()).ok()?;
1612 self.percussion_instruments
1613 .iter()
1614 .find(|instrument| instrument.midi_unpitched == Some(midi_key))
1615 }
1616}
1617
1618#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1620#[serde(rename_all = "snake_case")]
1621pub enum StaffKind {
1622 #[default]
1623 Standard,
1624 Tablature,
1625 Percussion,
1626}
1627
1628#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1630#[serde(rename_all = "snake_case")]
1631pub enum StaffNoteheadScheme {
1632 #[default]
1633 Standard,
1634 PitchNames,
1635 ShapeNotes,
1636}
1637
1638#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1640#[serde(rename_all = "snake_case")]
1641pub enum TablatureRhythmDisplay {
1642 #[default]
1644 FretOnly,
1645 Stems,
1647}
1648
1649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1655pub struct StaffPresentation {
1656 #[serde(default)]
1657 pub kind: StaffKind,
1658 #[serde(default = "default_staff_line_count")]
1659 pub lines: u8,
1660 #[serde(default = "default_staff_line_distance")]
1661 pub line_distance: f32,
1662 #[serde(default)]
1663 pub small: bool,
1664 #[serde(default)]
1665 pub cutaway: bool,
1666 #[serde(default = "default_staff_visible")]
1667 pub visible: bool,
1668 #[serde(default)]
1669 pub notehead_scheme: StaffNoteheadScheme,
1670 #[serde(default)]
1672 pub tablature_rhythm_display: TablatureRhythmDisplay,
1673 #[serde(default)]
1675 pub tablature_fret_mark_style: TablatureFretMarkStyle,
1676}
1677
1678const fn default_staff_line_count() -> u8 {
1679 5
1680}
1681
1682const fn default_staff_line_distance() -> f32 {
1683 1.0
1684}
1685
1686const fn default_staff_visible() -> bool {
1687 true
1688}
1689
1690impl Default for StaffPresentation {
1691 fn default() -> Self {
1692 Self {
1693 kind: StaffKind::Standard,
1694 lines: default_staff_line_count(),
1695 line_distance: default_staff_line_distance(),
1696 small: false,
1697 cutaway: false,
1698 visible: true,
1699 notehead_scheme: StaffNoteheadScheme::Standard,
1700 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
1701 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
1702 }
1703 }
1704}
1705
1706#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1708#[serde(rename_all = "snake_case")]
1709pub enum TablatureFretMarkStyle {
1710 #[default]
1711 Arabic,
1712 RomanUpper,
1713 RomanLower,
1714}
1715
1716impl StaffPresentation {
1717 fn for_clef(clef: &Clef) -> Self {
1718 let mut presentation = Self::default();
1719 if matches!(clef, Clef::Percussion) {
1720 presentation.kind = StaffKind::Percussion;
1721 }
1722 presentation
1723 }
1724}
1725
1726#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub struct Staff {
1728 pub clef: Clef,
1729 pub measures: Vec<Measure>,
1730 #[serde(default)]
1733 pub transpose_semitones: i8,
1734 #[serde(default)]
1735 pub tablature: Option<TablatureConfig>,
1736 #[serde(default)]
1737 pub presentation: StaffPresentation,
1738}
1739
1740impl Staff {
1741 pub fn new(clef: Clef) -> Self {
1742 Self {
1743 presentation: StaffPresentation::for_clef(&clef),
1744 clef,
1745 measures: Vec::new(),
1746 transpose_semitones: 0,
1747 tablature: None,
1748 }
1749 }
1750
1751 pub fn tablature_at(&self, measure_index: usize) -> Option<TablatureConfig> {
1757 let mut active = self.tablature.clone();
1758 for measure in self.measures.iter().take(measure_index.saturating_add(1)) {
1759 if let Some(change) = &measure.tablature_change {
1760 active = Some(change.clone());
1761 }
1762 }
1763 active
1764 }
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1768pub struct VoltaBracket {
1769 pub number: u8,
1771 pub kind: String,
1773}
1774
1775#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1777#[serde(rename_all = "kebab-case")]
1778pub enum HarpPedalPosition {
1779 Flat,
1780 #[default]
1781 Natural,
1782 Sharp,
1783}
1784
1785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1790pub struct HarpPedalDiagram {
1791 #[serde(default)]
1792 pub positions: [HarpPedalPosition; 7],
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1794 pub placement: Option<String>,
1795}
1796
1797impl Default for HarpPedalDiagram {
1798 fn default() -> Self {
1799 Self {
1800 positions: [HarpPedalPosition::Natural; 7],
1801 placement: None,
1802 }
1803 }
1804}
1805
1806#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1810pub struct MeasureLength {
1811 pub numerator: u32,
1812 pub denominator: u32,
1813}
1814
1815impl MeasureLength {
1816 pub const MAX_BEATS: f64 = 256.0;
1818
1819 pub fn beats(&self) -> Option<f64> {
1821 if self.numerator == 0 || self.denominator == 0 {
1822 return None;
1823 }
1824 let beats = 4.0 * f64::from(self.numerator) / f64::from(self.denominator);
1825 (beats.is_finite() && beats <= Self::MAX_BEATS).then_some(beats)
1826 }
1827
1828 pub fn from_beats(beats: f64) -> Option<Self> {
1831 const DIVISIONS: u32 = 3 * 5 * 7 * 64;
1832 if !beats.is_finite() || beats <= 0.0 || beats > Self::MAX_BEATS {
1833 return None;
1834 }
1835 let ticks = (beats * f64::from(DIVISIONS)).round();
1836 (ticks >= 1.0).then(|| Self::from_ticks(ticks as u32, DIVISIONS))?
1837 }
1838
1839 pub fn from_ticks(ticks: u32, divisions: u32) -> Option<Self> {
1841 let numerator = u64::from(ticks);
1842 let denominator = u64::from(divisions).checked_mul(4)?;
1843 if numerator == 0 || denominator == 0 {
1844 return None;
1845 }
1846 let gcd = {
1847 let (mut a, mut b) = (numerator, denominator);
1848 while b != 0 {
1849 (a, b) = (b, a % b);
1850 }
1851 a
1852 };
1853 let length = Self {
1854 numerator: u32::try_from(numerator / gcd).ok()?,
1855 denominator: u32::try_from(denominator / gcd).ok()?,
1856 };
1857 length.beats().map(|_| length)
1858 }
1859}
1860
1861#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1865pub struct MidMeasureClef {
1866 pub offset: MeasureLength,
1867 pub clef: Clef,
1868}
1869
1870#[derive(Debug, Clone, Serialize, Deserialize)]
1871pub struct Measure {
1872 pub number: u32,
1873 pub time_sig: Option<TimeSignature>,
1874 pub key_sig: Option<KeySignature>,
1875 pub clef: Option<Clef>,
1876 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1879 pub mid_clefs: Vec<MidMeasureClef>,
1880 pub tempo: Option<u16>,
1881 #[serde(default)]
1884 pub tempo_ramp_to: Option<u16>,
1885 #[serde(default)]
1887 pub instrument_change: Option<InstrumentDefinition>,
1888 #[serde(default)]
1890 pub tablature_change: Option<TablatureConfig>,
1891 pub barline_left: Barline,
1892 pub barline_right: Barline,
1893 #[serde(default)]
1894 pub volta: Option<VoltaBracket>,
1895 #[serde(default)]
1896 pub tempo_text: Option<String>,
1897 #[serde(default)]
1898 pub rehearsal: Option<String>,
1899 #[serde(default)]
1902 pub navigation: Option<String>,
1903 #[serde(default)]
1905 pub expression_text: Option<String>,
1906 #[serde(default)]
1907 pub texts: Vec<StyledText>,
1908 #[serde(default)]
1910 pub figured_bass: Vec<FiguredBassFigure>,
1911 #[serde(default)]
1913 pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
1914 #[serde(default)]
1916 pub multi_rest_count: Option<u8>,
1917 #[serde(default)]
1919 pub system_break: bool,
1920 #[serde(default)]
1922 pub page_break: bool,
1923 #[serde(default)]
1926 pub section_break: bool,
1927 pub voices: [Vec<Note>; 4],
1929 #[serde(default)]
1935 pub source_voice_numbers: [Option<u32>; 4],
1936 #[serde(default, skip_serializing_if = "Option::is_none")]
1939 pub actual_length: Option<MeasureLength>,
1940 #[serde(default, skip_serializing_if = "Option::is_none")]
1944 pub measure_repeat: Option<u8>,
1945}
1946
1947impl Measure {
1948 pub fn clef_at(&self, start_clef: &Clef, beats: f64) -> Clef {
1951 self.mid_clefs
1952 .iter()
1953 .take_while(|change| {
1954 change
1955 .offset
1956 .beats()
1957 .is_some_and(|offset| offset <= beats + 1e-9)
1958 })
1959 .last()
1960 .map_or_else(|| start_clef.clone(), |change| change.clef.clone())
1961 }
1962
1963 pub fn empty(numerator: u8, denominator: u8) -> Self {
1964 let total_beats = TimeSignature {
1965 numerator,
1966 denominator,
1967 }
1968 .total_beats();
1969 let mut voice0: Vec<Note> = Vec::new();
1970 let mut remaining = total_beats;
1971 while remaining > 1e-9 {
1972 let dur = Duration::whole_filling_beats(remaining);
1973 remaining -= dur.beats(0);
1974 voice0.push(Note::rest(dur));
1975 }
1976 Self {
1977 number: 0,
1978 time_sig: None,
1979 key_sig: None,
1980 clef: None,
1981 mid_clefs: Vec::new(),
1982 tempo: None,
1983 tempo_ramp_to: None,
1984 instrument_change: None,
1985 tablature_change: None,
1986 barline_left: Barline::Normal,
1987 barline_right: Barline::Normal,
1988 volta: None,
1989 tempo_text: None,
1990 rehearsal: None,
1991 navigation: None,
1992 expression_text: None,
1993 texts: Vec::new(),
1994 figured_bass: Vec::new(),
1995 harp_pedal_diagrams: Vec::new(),
1996 multi_rest_count: None,
1997 system_break: false,
1998 page_break: false,
1999 section_break: false,
2000 voices: [voice0, vec![], vec![], vec![]],
2001 source_voice_numbers: [None; 4],
2002 actual_length: None,
2003 measure_repeat: None,
2004 }
2005 }
2006
2007 pub fn repeat_content_from(&mut self, source: &Measure) {
2010 for (target, voice) in self.voices.iter_mut().zip(&source.voices) {
2011 *target = voice.iter().map(Note::repeated_copy).collect();
2012 }
2013 self.source_voice_numbers = source.source_voice_numbers;
2014 }
2015
2016 pub fn same_sounding_content(&self, other: &Measure) -> bool {
2019 self.voices.iter().zip(&other.voices).all(|(left, right)| {
2020 left.len() == right.len()
2021 && left.iter().zip(right).all(|(a, b)| {
2022 a.is_rest == b.is_rest
2023 && a.pitches == b.pitches
2024 && a.duration == b.duration
2025 && a.dot_count == b.dot_count
2026 && a.tuplet == b.tuplet
2027 && a.is_grace == b.is_grace
2028 })
2029 })
2030 }
2031
2032 pub fn duration_beats(&self, time_signature: &TimeSignature) -> f64 {
2035 self.actual_length
2036 .and_then(|length| length.beats())
2037 .unwrap_or_else(|| {
2038 self.time_sig
2039 .as_ref()
2040 .unwrap_or(time_signature)
2041 .total_beats()
2042 })
2043 }
2044
2045 pub fn renumber(&mut self, n: u32) {
2046 self.number = n;
2047 }
2048}
2049
2050#[derive(Debug, Clone, Serialize, Deserialize)]
2051pub struct Note {
2052 pub id: String,
2053 pub is_rest: bool,
2054 #[serde(default)]
2056 pub is_unpitched: bool,
2057 #[serde(default)]
2059 pub instrument_id: Option<String>,
2060 #[serde(default)]
2064 pub offset_x: Option<f64>,
2065 #[serde(default)]
2067 pub offset_y: Option<f64>,
2068 #[serde(default)]
2070 pub relative_x: Option<f64>,
2071 #[serde(default)]
2073 pub relative_y: Option<f64>,
2074 pub pitches: Vec<Pitch>,
2076 #[serde(default)]
2077 pub tab_position: Option<super::notation::TabPosition>,
2078 #[serde(default)]
2081 pub tab_positions: Vec<super::notation::TabPosition>,
2082 pub duration: Duration,
2083 pub dot_count: u8,
2084 pub tie_start: bool,
2085 pub tie_end: bool,
2086 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2090 pub pitch_tie_starts: Vec<bool>,
2091 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2093 pub pitch_tie_ends: Vec<bool>,
2094 pub beam: BeamState,
2095 pub articulations: Vec<Articulation>,
2096 pub dynamic: Option<Dynamic>,
2097 pub stem_up: Option<bool>,
2098 #[serde(default)]
2099 pub hairpin_start: Option<HairpinKind>,
2100 #[serde(default)]
2101 pub hairpin_end: bool,
2102 #[serde(default)]
2103 pub tuplet: Option<TupletInfo>,
2104 #[serde(default)]
2105 pub chord_symbol: Option<ChordSymbol>,
2106 #[serde(default)]
2107 pub is_grace: bool,
2108 #[serde(default)]
2110 pub grace_slash: bool,
2111 #[serde(default)]
2112 pub ottava_start: Option<OttavaKind>,
2113 #[serde(default)]
2114 pub ottava_end: bool,
2115 #[serde(default)]
2116 pub lyric: Option<Lyric>,
2117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2119 pub additional_lyrics: Vec<VerseLyric>,
2120 #[serde(default)]
2121 pub pedal_start: bool,
2122 #[serde(default)]
2123 pub pedal_end: bool,
2124 #[serde(default)]
2125 pub slur_start: bool,
2126 #[serde(default)]
2127 pub slur_end: bool,
2128 #[serde(default)]
2130 pub arpeggiate: Option<bool>,
2131 #[serde(default)]
2133 pub technique_text: Option<String>,
2134 #[serde(default)]
2135 pub glissando_start: bool,
2136 #[serde(default)]
2137 pub glissando_end: bool,
2138 #[serde(default)]
2139 pub cross_staff: Option<CrossStaff>,
2140 #[serde(default)]
2142 pub fingering: Option<u8>,
2143 #[serde(default)]
2146 pub fingerings: Vec<u8>,
2147 #[serde(default)]
2149 pub string_number: Option<u8>,
2150 #[serde(default)]
2151 pub note_head: NoteHead,
2152 #[serde(default)]
2154 pub is_cue: bool,
2155 #[serde(default)]
2157 pub trill_line_start: bool,
2158 #[serde(default)]
2160 pub trill_line_end: bool,
2161 #[serde(default)]
2163 pub guitar_technique: Option<GuitarTechnique>,
2164 #[serde(default)]
2166 pub guitar_bend_alter_cents: Option<i16>,
2167 #[serde(default)]
2169 pub guitar_bend_curve: Vec<GuitarBendPoint>,
2170}
2171
2172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2174pub struct GuitarBendPoint {
2175 pub position_per_mille: u16,
2176 pub alter_cents: i16,
2177}
2178
2179impl Note {
2180 pub fn select_fingering(
2182 &self,
2183 policy: super::notation::FingeringSelectionPolicy,
2184 ) -> Option<u8> {
2185 let candidates = if self.fingerings.is_empty() {
2186 self.fingering.into_iter().collect::<Vec<_>>()
2187 } else {
2188 self.fingerings.clone()
2189 };
2190 match policy {
2191 super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
2192 super::notation::FingeringSelectionPolicy::LowestNumber => {
2193 candidates.iter().copied().min()
2194 }
2195 super::notation::FingeringSelectionPolicy::HighestNumber => {
2196 candidates.iter().copied().max()
2197 }
2198 }
2199 }
2200
2201 pub fn new(pitch: Pitch, duration: Duration) -> Self {
2202 Self {
2203 id: Uuid::new_v4().to_string(),
2204 is_rest: false,
2205 is_unpitched: false,
2206 instrument_id: None,
2207 offset_x: None,
2208 offset_y: None,
2209 relative_x: None,
2210 relative_y: None,
2211 pitches: vec![pitch],
2212 tab_position: None,
2213 tab_positions: Vec::new(),
2214 duration,
2215 dot_count: 0,
2216 tie_start: false,
2217 tie_end: false,
2218 pitch_tie_starts: Vec::new(),
2219 pitch_tie_ends: Vec::new(),
2220 beam: BeamState::None,
2221 articulations: Vec::new(),
2222 dynamic: None,
2223 stem_up: None,
2224 hairpin_start: None,
2225 hairpin_end: false,
2226 tuplet: None,
2227 chord_symbol: None,
2228 is_grace: false,
2229 grace_slash: false,
2230 ottava_start: None,
2231 ottava_end: false,
2232 lyric: None,
2233 additional_lyrics: Vec::new(),
2234 pedal_start: false,
2235 pedal_end: false,
2236 slur_start: false,
2237 slur_end: false,
2238 arpeggiate: None,
2239 technique_text: None,
2240 glissando_start: false,
2241 glissando_end: false,
2242 cross_staff: None,
2243 fingering: None,
2244 fingerings: Vec::new(),
2245 string_number: None,
2246 note_head: NoteHead::Normal,
2247 is_cue: false,
2248 trill_line_start: false,
2249 trill_line_end: false,
2250 guitar_technique: None,
2251 guitar_bend_alter_cents: None,
2252 guitar_bend_curve: Vec::new(),
2253 }
2254 }
2255
2256 pub fn rest(duration: Duration) -> Self {
2257 Self {
2258 id: Uuid::new_v4().to_string(),
2259 is_rest: true,
2260 is_unpitched: false,
2261 instrument_id: None,
2262 offset_x: None,
2263 offset_y: None,
2264 relative_x: None,
2265 relative_y: None,
2266 pitches: Vec::new(),
2267 tab_position: None,
2268 tab_positions: Vec::new(),
2269 duration,
2270 dot_count: 0,
2271 tie_start: false,
2272 tie_end: false,
2273 pitch_tie_starts: Vec::new(),
2274 pitch_tie_ends: Vec::new(),
2275 beam: BeamState::None,
2276 articulations: Vec::new(),
2277 dynamic: None,
2278 stem_up: None,
2279 hairpin_start: None,
2280 hairpin_end: false,
2281 tuplet: None,
2282 chord_symbol: None,
2283 is_grace: false,
2284 grace_slash: false,
2285 ottava_start: None,
2286 ottava_end: false,
2287 lyric: None,
2288 additional_lyrics: Vec::new(),
2289 pedal_start: false,
2290 pedal_end: false,
2291 slur_start: false,
2292 slur_end: false,
2293 arpeggiate: None,
2294 technique_text: None,
2295 glissando_start: false,
2296 glissando_end: false,
2297 cross_staff: None,
2298 fingering: None,
2299 fingerings: Vec::new(),
2300 string_number: None,
2301 note_head: NoteHead::Normal,
2302 is_cue: false,
2303 trill_line_start: false,
2304 trill_line_end: false,
2305 guitar_technique: None,
2306 guitar_bend_alter_cents: None,
2307 guitar_bend_curve: Vec::new(),
2308 }
2309 }
2310
2311 pub fn repeated_copy(&self) -> Note {
2314 let mut copy = self.clone();
2315 copy.id = Uuid::new_v4().to_string();
2316 copy.tie_start = false;
2317 copy.tie_end = false;
2318 copy.slur_start = false;
2319 copy.slur_end = false;
2320 copy.hairpin_start = None;
2321 copy.hairpin_end = false;
2322 copy.pedal_start = false;
2323 copy.pedal_end = false;
2324 copy.ottava_start = None;
2325 copy.ottava_end = false;
2326 copy.glissando_start = false;
2327 copy.glissando_end = false;
2328 copy.trill_line_start = false;
2329 copy.trill_line_end = false;
2330 copy.lyric = None;
2331 copy.additional_lyrics.clear();
2332 copy.chord_symbol = None;
2333 copy.dynamic = None;
2334 copy
2335 }
2336
2337 pub fn pitch_tie_start(&self, index: usize) -> bool {
2340 per_pitch_flag(
2341 &self.pitch_tie_starts,
2342 self.pitches.len(),
2343 index,
2344 self.tie_start,
2345 )
2346 }
2347
2348 pub fn pitch_tie_end(&self, index: usize) -> bool {
2350 per_pitch_flag(
2351 &self.pitch_tie_ends,
2352 self.pitches.len(),
2353 index,
2354 self.tie_end,
2355 )
2356 }
2357
2358 pub fn set_pitch_ties(&mut self, starts: &[bool], ends: &[bool]) {
2363 let count = self.pitches.len();
2364 if starts.len() == count {
2365 self.tie_start = starts.iter().any(|tied| *tied);
2366 self.pitch_tie_starts = if starts.iter().all(|tied| *tied == self.tie_start) {
2367 Vec::new()
2368 } else {
2369 starts.to_vec()
2370 };
2371 }
2372 if ends.len() == count {
2373 self.tie_end = ends.iter().any(|tied| *tied);
2374 self.pitch_tie_ends = if ends.iter().all(|tied| *tied == self.tie_end) {
2375 Vec::new()
2376 } else {
2377 ends.to_vec()
2378 };
2379 }
2380 }
2381
2382 pub fn pitch_tie_starts_or_uniform(&self) -> Vec<bool> {
2384 (0..self.pitches.len())
2385 .map(|index| self.pitch_tie_start(index))
2386 .collect()
2387 }
2388
2389 pub fn pitch_tie_ends_or_uniform(&self) -> Vec<bool> {
2391 (0..self.pitches.len())
2392 .map(|index| self.pitch_tie_end(index))
2393 .collect()
2394 }
2395
2396 pub fn is_plain_whole_rest(&self) -> bool {
2400 self.is_rest
2401 && matches!(self.duration, Duration::Whole)
2402 && self.dot_count == 0
2403 && self.tuplet.is_none()
2404 && !self.is_grace
2405 && !self.is_cue
2406 }
2407
2408 pub fn beats(&self) -> f64 {
2409 if self.is_grace || self.is_cue {
2410 return 0.0;
2411 }
2412 let base = self.duration.beats(self.dot_count);
2413 if let Some(ref t) = self.tuplet {
2414 base * (t.normal_notes as f64) / (t.actual_notes as f64)
2415 } else {
2416 base
2417 }
2418 }
2419}
2420
2421impl Duration {
2422 pub fn whole_filling_beats(beats: f64) -> Duration {
2424 if beats >= 4.0 {
2425 Duration::Whole
2426 } else if beats >= 2.0 {
2427 Duration::Half
2428 } else if beats >= 1.0 {
2429 Duration::Quarter
2430 } else if beats >= 0.5 {
2431 Duration::Eighth
2432 } else if beats >= 0.25 {
2433 Duration::Sixteenth
2434 } else if beats >= 0.125 {
2435 Duration::ThirtySecond
2436 } else {
2437 Duration::SixtyFourth
2438 }
2439 }
2440}
2441
2442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2446pub struct NoteAddr {
2447 pub part: usize,
2448 pub staff: usize,
2449 pub measure: usize,
2450 pub voice: usize,
2451 pub note: usize,
2452}
2453
2454#[derive(Debug, Clone, Serialize, Deserialize)]
2458pub enum ScoreChange {
2459 ScoreViewsChanged {
2460 old: Vec<ScoreView>,
2461 new: Vec<ScoreView>,
2462 },
2463 ScoreTextChanged {
2464 old: Vec<StyledText>,
2465 new: Vec<StyledText>,
2466 },
2467 MeasureTextChanged {
2468 part: usize,
2469 staff: usize,
2470 measure: usize,
2471 old: Vec<StyledText>,
2472 new: Vec<StyledText>,
2473 },
2474 FiguredBassChanged {
2475 part: usize,
2476 staff: usize,
2477 measure: usize,
2478 old: Vec<FiguredBassFigure>,
2479 new: Vec<FiguredBassFigure>,
2480 },
2481 HarpPedalDiagramsChanged {
2482 part: usize,
2483 staff: usize,
2484 measure: usize,
2485 old: Vec<HarpPedalDiagram>,
2486 new: Vec<HarpPedalDiagram>,
2487 },
2488 ChordDefinitionsChanged {
2489 old: Vec<ChordDefinition>,
2490 new: Vec<ChordDefinition>,
2491 },
2492 PartNamesChanged {
2493 part: usize,
2494 old_name: String,
2495 new_name: String,
2496 old_short_name: String,
2497 new_short_name: String,
2498 },
2499 PartMidiChanged {
2500 part: usize,
2501 old_channel: u8,
2502 new_channel: u8,
2503 old_program: u8,
2504 new_program: u8,
2505 },
2506 InstrumentDefinitionChanged {
2507 part: usize,
2508 old: Option<InstrumentDefinition>,
2509 new: Option<InstrumentDefinition>,
2510 },
2511 PartMidiAutomationChanged {
2512 part: usize,
2513 old_pitch_bends: Vec<MidiPitchBend>,
2514 new_pitch_bends: Vec<MidiPitchBend>,
2515 old_control_changes: Vec<MidiControlChange>,
2516 new_control_changes: Vec<MidiControlChange>,
2517 old_program_changes: Vec<MidiProgramChange>,
2518 new_program_changes: Vec<MidiProgramChange>,
2519 old_aftertouch: Vec<MidiAftertouch>,
2520 new_aftertouch: Vec<MidiAftertouch>,
2521 },
2522 StaffConfigurationChanged {
2523 part: usize,
2524 staff: usize,
2525 old_clef: Clef,
2526 new_clef: Clef,
2527 old_transpose_semitones: i8,
2528 new_transpose_semitones: i8,
2529 },
2530 StaffPresentationChanged {
2531 part: usize,
2532 staff: usize,
2533 old: StaffPresentation,
2534 new: StaffPresentation,
2535 },
2536 MeasurePresentationChanged {
2537 part: usize,
2538 staff: usize,
2539 measure: usize,
2540 old_number: u32,
2541 new_number: u32,
2542 old_clef: Option<Clef>,
2543 new_clef: Option<Clef>,
2544 old_tempo_text: Option<String>,
2545 new_tempo_text: Option<String>,
2546 old_navigation: Option<String>,
2547 new_navigation: Option<String>,
2548 old_expression_text: Option<String>,
2549 new_expression_text: Option<String>,
2550 old_multi_rest_count: Option<u8>,
2551 new_multi_rest_count: Option<u8>,
2552 old_system_break: bool,
2553 new_system_break: bool,
2554 old_page_break: bool,
2555 new_page_break: bool,
2556 old_section_break: bool,
2557 new_section_break: bool,
2558 },
2559 TablatureConfigChanged {
2560 part: usize,
2561 staff: usize,
2562 old: Option<TablatureConfig>,
2563 new: Option<TablatureConfig>,
2564 },
2565 TablatureChangeChanged {
2566 part: usize,
2567 staff: usize,
2568 measure: usize,
2569 old: Option<TablatureConfig>,
2570 new: Option<TablatureConfig>,
2571 },
2572 MidMeasureClefsChanged {
2573 part: usize,
2574 staff: usize,
2575 measure: usize,
2576 old: Vec<MidMeasureClef>,
2577 new: Vec<MidMeasureClef>,
2578 },
2579 UnrepresentedFieldChanged {
2584 path: String,
2585 },
2586 MetadataChanged {
2587 field: String,
2588 old: String,
2589 new: String,
2590 },
2591 TempoChanged {
2592 old: u16,
2593 new: u16,
2594 },
2595 KeySignatureChanged {
2596 old: KeySignature,
2597 new: KeySignature,
2598 },
2599 PartAdded {
2600 part_index: usize,
2601 },
2602 PartRemoved {
2603 part_index: usize,
2604 name: String,
2605 },
2606 NoteAdded {
2607 part: usize,
2608 staff: usize,
2609 measure: usize,
2610 voice: usize,
2611 note_index: usize,
2612 },
2613 NoteRemoved {
2614 part: usize,
2615 staff: usize,
2616 measure: usize,
2617 voice: usize,
2618 note: Box<Note>,
2619 },
2620 NoteModified {
2621 part: usize,
2622 staff: usize,
2623 measure: usize,
2624 voice: usize,
2625 note_index: usize,
2626 old: Box<Note>,
2627 new: Box<Note>,
2628 },
2629 TimeSigChanged {
2630 part: usize,
2631 staff: usize,
2632 measure: usize,
2633 old: Option<TimeSignature>,
2634 new: Option<TimeSignature>,
2635 },
2636 MeasureTempoChanged {
2637 part: usize,
2638 staff: usize,
2639 measure: usize,
2640 old: Option<u16>,
2641 new: Option<u16>,
2642 },
2643 MeasureTempoRampChanged {
2644 part: usize,
2645 staff: usize,
2646 measure: usize,
2647 old: Option<u16>,
2648 new: Option<u16>,
2649 },
2650 BarlineChanged {
2651 part: usize,
2652 staff: usize,
2653 measure: usize,
2654 },
2655 RehearsalMarkChanged {
2656 part: usize,
2657 staff: usize,
2658 measure: usize,
2659 old: Option<String>,
2660 new: Option<String>,
2661 },
2662 VoltaChanged {
2663 part: usize,
2664 staff: usize,
2665 measure: usize,
2666 },
2667}
2668
2669pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
2675 let mut changes: Vec<ScoreChange> = Vec::new();
2676
2677 if a.views != b.views {
2678 changes.push(ScoreChange::ScoreViewsChanged {
2679 old: a.views.clone(),
2680 new: b.views.clone(),
2681 });
2682 }
2683
2684 if a.texts != b.texts {
2685 changes.push(ScoreChange::ScoreTextChanged {
2686 old: a.texts.clone(),
2687 new: b.texts.clone(),
2688 });
2689 }
2690 if a.chord_definitions != b.chord_definitions {
2691 changes.push(ScoreChange::ChordDefinitionsChanged {
2692 old: a.chord_definitions.clone(),
2693 new: b.chord_definitions.clone(),
2694 });
2695 }
2696 if let Some(path) = first_unrepresented_field_change(a, b) {
2697 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
2698 }
2699
2700 macro_rules! meta {
2701 ($field:ident, $name:literal) => {
2702 if a.metadata.$field != b.metadata.$field {
2703 changes.push(ScoreChange::MetadataChanged {
2704 field: $name.to_string(),
2705 old: a.metadata.$field.clone(),
2706 new: b.metadata.$field.clone(),
2707 });
2708 }
2709 };
2710 }
2711 meta!(title, "title");
2712 meta!(composer, "composer");
2713 meta!(lyricist, "lyricist");
2714 meta!(copyright, "copyright");
2715 meta!(work_number, "work_number");
2716 meta!(movement_title, "movement_title");
2717
2718 if a.settings.tempo_bpm != b.settings.tempo_bpm {
2719 changes.push(ScoreChange::TempoChanged {
2720 old: a.settings.tempo_bpm,
2721 new: b.settings.tempo_bpm,
2722 });
2723 }
2724 if a.settings.key_signature != b.settings.key_signature {
2725 changes.push(ScoreChange::KeySignatureChanged {
2726 old: a.settings.key_signature.clone(),
2727 new: b.settings.key_signature.clone(),
2728 });
2729 }
2730
2731 let a_len = a.parts.len();
2732 let b_len = b.parts.len();
2733 for i in b_len..a_len {
2734 changes.push(ScoreChange::PartRemoved {
2735 part_index: i,
2736 name: a.parts[i].name.clone(),
2737 });
2738 }
2739 for i in a_len..b_len {
2740 changes.push(ScoreChange::PartAdded { part_index: i });
2741 }
2742
2743 for pi in 0..a_len.min(b_len) {
2744 let ap = &a.parts[pi];
2745 let bp = &b.parts[pi];
2746 if ap.name != bp.name || ap.short_name != bp.short_name {
2747 changes.push(ScoreChange::PartNamesChanged {
2748 part: pi,
2749 old_name: ap.name.clone(),
2750 new_name: bp.name.clone(),
2751 old_short_name: ap.short_name.clone(),
2752 new_short_name: bp.short_name.clone(),
2753 });
2754 }
2755 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
2756 changes.push(ScoreChange::PartMidiChanged {
2757 part: pi,
2758 old_channel: ap.midi_channel,
2759 new_channel: bp.midi_channel,
2760 old_program: ap.midi_program,
2761 new_program: bp.midi_program,
2762 });
2763 }
2764 if ap.instrument != bp.instrument {
2765 changes.push(ScoreChange::InstrumentDefinitionChanged {
2766 part: pi,
2767 old: ap.instrument.clone(),
2768 new: bp.instrument.clone(),
2769 });
2770 }
2771 if ap.midi_pitch_bends != bp.midi_pitch_bends
2772 || ap.midi_control_changes != bp.midi_control_changes
2773 || ap.midi_program_changes != bp.midi_program_changes
2774 || ap.midi_aftertouch != bp.midi_aftertouch
2775 {
2776 changes.push(ScoreChange::PartMidiAutomationChanged {
2777 part: pi,
2778 old_pitch_bends: ap.midi_pitch_bends.clone(),
2779 new_pitch_bends: bp.midi_pitch_bends.clone(),
2780 old_control_changes: ap.midi_control_changes.clone(),
2781 new_control_changes: bp.midi_control_changes.clone(),
2782 old_program_changes: ap.midi_program_changes.clone(),
2783 new_program_changes: bp.midi_program_changes.clone(),
2784 old_aftertouch: ap.midi_aftertouch.clone(),
2785 new_aftertouch: bp.midi_aftertouch.clone(),
2786 });
2787 }
2788 for si in 0..ap.staves.len().min(bp.staves.len()) {
2789 let a_staff = &ap.staves[si];
2790 let b_staff = &bp.staves[si];
2791 if a_staff.clef != b_staff.clef
2792 || a_staff.transpose_semitones != b_staff.transpose_semitones
2793 {
2794 changes.push(ScoreChange::StaffConfigurationChanged {
2795 part: pi,
2796 staff: si,
2797 old_clef: a_staff.clef.clone(),
2798 new_clef: b_staff.clef.clone(),
2799 old_transpose_semitones: a_staff.transpose_semitones,
2800 new_transpose_semitones: b_staff.transpose_semitones,
2801 });
2802 }
2803 if a_staff.tablature != b_staff.tablature {
2804 changes.push(ScoreChange::TablatureConfigChanged {
2805 part: pi,
2806 staff: si,
2807 old: a_staff.tablature.clone(),
2808 new: b_staff.tablature.clone(),
2809 });
2810 }
2811 if a_staff.presentation != b_staff.presentation {
2812 changes.push(ScoreChange::StaffPresentationChanged {
2813 part: pi,
2814 staff: si,
2815 old: a_staff.presentation.clone(),
2816 new: b_staff.presentation.clone(),
2817 });
2818 }
2819 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
2820 let am = &a_staff.measures[mi];
2821 let bm = &b_staff.measures[mi];
2822 if am.tablature_change != bm.tablature_change {
2823 changes.push(ScoreChange::TablatureChangeChanged {
2824 part: pi,
2825 staff: si,
2826 measure: mi,
2827 old: am.tablature_change.clone(),
2828 new: bm.tablature_change.clone(),
2829 });
2830 }
2831 if am.mid_clefs != bm.mid_clefs {
2832 changes.push(ScoreChange::MidMeasureClefsChanged {
2833 part: pi,
2834 staff: si,
2835 measure: mi,
2836 old: am.mid_clefs.clone(),
2837 new: bm.mid_clefs.clone(),
2838 });
2839 }
2840 if am.number != bm.number
2841 || am.clef != bm.clef
2842 || am.tempo_text != bm.tempo_text
2843 || am.navigation != bm.navigation
2844 || am.expression_text != bm.expression_text
2845 || am.multi_rest_count != bm.multi_rest_count
2846 || am.system_break != bm.system_break
2847 || am.page_break != bm.page_break
2848 || am.section_break != bm.section_break
2849 {
2850 changes.push(ScoreChange::MeasurePresentationChanged {
2851 part: pi,
2852 staff: si,
2853 measure: mi,
2854 old_number: am.number,
2855 new_number: bm.number,
2856 old_clef: am.clef.clone(),
2857 new_clef: bm.clef.clone(),
2858 old_tempo_text: am.tempo_text.clone(),
2859 new_tempo_text: bm.tempo_text.clone(),
2860 old_navigation: am.navigation.clone(),
2861 new_navigation: bm.navigation.clone(),
2862 old_expression_text: am.expression_text.clone(),
2863 new_expression_text: bm.expression_text.clone(),
2864 old_multi_rest_count: am.multi_rest_count,
2865 new_multi_rest_count: bm.multi_rest_count,
2866 old_system_break: am.system_break,
2867 new_system_break: bm.system_break,
2868 old_page_break: am.page_break,
2869 new_page_break: bm.page_break,
2870 old_section_break: am.section_break,
2871 new_section_break: bm.section_break,
2872 });
2873 }
2874 for vi in 0..4usize {
2875 let av = &am.voices[vi];
2876 let bv = &bm.voices[vi];
2877 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
2878 if !note_content_eq(a_note, b_note) {
2879 changes.push(ScoreChange::NoteModified {
2880 part: pi,
2881 staff: si,
2882 measure: mi,
2883 voice: vi,
2884 note_index: ni,
2885 old: Box::new(a_note.clone()),
2886 new: Box::new(b_note.clone()),
2887 });
2888 }
2889 }
2890 for note in av.iter().skip(bv.len()) {
2891 changes.push(ScoreChange::NoteRemoved {
2892 part: pi,
2893 staff: si,
2894 measure: mi,
2895 voice: vi,
2896 note: Box::new(note.clone()),
2897 });
2898 }
2899 for ni in av.len()..bv.len() {
2900 changes.push(ScoreChange::NoteAdded {
2901 part: pi,
2902 staff: si,
2903 measure: mi,
2904 voice: vi,
2905 note_index: ni,
2906 });
2907 }
2908 }
2909 if am.time_sig != bm.time_sig {
2910 changes.push(ScoreChange::TimeSigChanged {
2911 part: pi,
2912 staff: si,
2913 measure: mi,
2914 old: am.time_sig.clone(),
2915 new: bm.time_sig.clone(),
2916 });
2917 }
2918 if am.tempo != bm.tempo {
2919 changes.push(ScoreChange::MeasureTempoChanged {
2920 part: pi,
2921 staff: si,
2922 measure: mi,
2923 old: am.tempo,
2924 new: bm.tempo,
2925 });
2926 }
2927 if am.tempo_ramp_to != bm.tempo_ramp_to {
2928 changes.push(ScoreChange::MeasureTempoRampChanged {
2929 part: pi,
2930 staff: si,
2931 measure: mi,
2932 old: am.tempo_ramp_to,
2933 new: bm.tempo_ramp_to,
2934 });
2935 }
2936 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
2937 changes.push(ScoreChange::BarlineChanged {
2938 part: pi,
2939 staff: si,
2940 measure: mi,
2941 });
2942 }
2943 if am.rehearsal != bm.rehearsal {
2944 changes.push(ScoreChange::RehearsalMarkChanged {
2945 part: pi,
2946 staff: si,
2947 measure: mi,
2948 old: am.rehearsal.clone(),
2949 new: bm.rehearsal.clone(),
2950 });
2951 }
2952 if am.volta != bm.volta {
2953 changes.push(ScoreChange::VoltaChanged {
2954 part: pi,
2955 staff: si,
2956 measure: mi,
2957 });
2958 }
2959 if am.texts != bm.texts {
2960 changes.push(ScoreChange::MeasureTextChanged {
2961 part: pi,
2962 staff: si,
2963 measure: mi,
2964 old: am.texts.clone(),
2965 new: bm.texts.clone(),
2966 });
2967 }
2968 if am.figured_bass != bm.figured_bass {
2969 changes.push(ScoreChange::FiguredBassChanged {
2970 part: pi,
2971 staff: si,
2972 measure: mi,
2973 old: am.figured_bass.clone(),
2974 new: bm.figured_bass.clone(),
2975 });
2976 }
2977 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
2978 changes.push(ScoreChange::HarpPedalDiagramsChanged {
2979 part: pi,
2980 staff: si,
2981 measure: mi,
2982 old: am.harp_pedal_diagrams.clone(),
2983 new: bm.harp_pedal_diagrams.clone(),
2984 });
2985 }
2986 }
2987 }
2988 }
2989
2990 changes
2991}
2992
2993fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
2994 if a.settings.time_signature != b.settings.time_signature {
2995 return Some("settings.time_signature".to_string());
2996 }
2997 if a.part_groups.len() != b.part_groups.len()
2998 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
2999 x.first_part != y.first_part
3000 || x.last_part != y.last_part
3001 || x.symbol != y.symbol
3002 || x.barlines_connect != y.barlines_connect
3003 })
3004 {
3005 return Some("part_groups".to_string());
3006 }
3007 a.parts
3008 .iter()
3009 .zip(&b.parts)
3010 .enumerate()
3011 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
3012}
3013
3014fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
3015 let prefix = format!("parts[{part_index}]");
3016 if a.percussion_instruments != b.percussion_instruments {
3017 return Some(format!("{prefix}.percussion_instruments"));
3018 }
3019 if a.staff_groups != b.staff_groups {
3020 return Some(format!("{prefix}.staff_groups"));
3021 }
3022 if a.staves.len() != b.staves.len() {
3023 return Some(format!("{prefix}.staves"));
3024 }
3025 a.staves
3026 .iter()
3027 .zip(&b.staves)
3028 .enumerate()
3029 .find_map(|(staff_index, (a, b))| {
3030 first_unrepresented_staff_change(&prefix, staff_index, a, b)
3031 })
3032}
3033
3034fn first_unrepresented_staff_change(
3035 part_prefix: &str,
3036 staff_index: usize,
3037 a: &Staff,
3038 b: &Staff,
3039) -> Option<String> {
3040 let prefix = format!("{part_prefix}.staves[{staff_index}]");
3041 if a.measures.len() != b.measures.len() {
3042 return Some(format!("{prefix}.measures"));
3043 }
3044 a.measures
3045 .iter()
3046 .zip(&b.measures)
3047 .enumerate()
3048 .find_map(|(measure_index, (a, b))| {
3049 first_unrepresented_measure_change(&prefix, measure_index, a, b)
3050 })
3051}
3052
3053fn first_unrepresented_measure_change(
3054 _staff_prefix: &str,
3055 _measure_index: usize,
3056 _a: &Measure,
3057 _b: &Measure,
3058) -> Option<String> {
3059 None
3060}
3061
3062#[derive(Debug, Clone, Serialize, Deserialize)]
3069pub enum ScorePatch {
3070 SetScoreViews {
3071 value: Vec<ScoreView>,
3072 },
3073 SetScoreTexts {
3074 value: Vec<StyledText>,
3075 },
3076 SetMeasureTexts {
3077 part: usize,
3078 staff: usize,
3079 measure: usize,
3080 value: Vec<StyledText>,
3081 },
3082 SetFiguredBass {
3083 part: usize,
3084 staff: usize,
3085 measure: usize,
3086 value: Vec<FiguredBassFigure>,
3087 },
3088 SetHarpPedalDiagrams {
3089 part: usize,
3090 staff: usize,
3091 measure: usize,
3092 value: Vec<HarpPedalDiagram>,
3093 },
3094 SetChordDefinitions {
3095 value: Vec<ChordDefinition>,
3096 },
3097 SetPartNames {
3098 part: usize,
3099 name: String,
3100 short_name: String,
3101 },
3102 SetPartMidi {
3103 part: usize,
3104 channel: u8,
3105 program: u8,
3106 },
3107 SetInstrumentDefinition {
3108 part: usize,
3109 value: Option<InstrumentDefinition>,
3110 },
3111 SetPartMidiAutomation {
3112 part: usize,
3113 pitch_bends: Vec<MidiPitchBend>,
3114 control_changes: Vec<MidiControlChange>,
3115 program_changes: Vec<MidiProgramChange>,
3116 aftertouch: Vec<MidiAftertouch>,
3117 },
3118 SetStaffConfiguration {
3119 part: usize,
3120 staff: usize,
3121 clef: Clef,
3122 transpose_semitones: i8,
3123 },
3124 SetStaffPresentation {
3125 part: usize,
3126 staff: usize,
3127 value: StaffPresentation,
3128 },
3129 SetMeasurePresentation {
3130 part: usize,
3131 staff: usize,
3132 measure: usize,
3133 number: u32,
3134 clef: Option<Clef>,
3135 tempo_text: Option<String>,
3136 navigation: Option<String>,
3137 expression_text: Option<String>,
3138 multi_rest_count: Option<u8>,
3139 system_break: bool,
3140 page_break: bool,
3141 #[serde(default)]
3142 section_break: bool,
3143 },
3144 SetTablatureConfig {
3145 part: usize,
3146 staff: usize,
3147 value: Option<TablatureConfig>,
3148 },
3149 SetMeasureTablatureChange {
3150 part: usize,
3151 staff: usize,
3152 measure: usize,
3153 value: Option<TablatureConfig>,
3154 },
3155 SetMeasureMidClefs {
3156 part: usize,
3157 staff: usize,
3158 measure: usize,
3159 value: Vec<MidMeasureClef>,
3160 },
3161 SetMetadata {
3162 field: String,
3163 value: String,
3164 },
3165 SetTempo {
3166 value: u16,
3167 },
3168 SetKeySignature {
3169 part: usize,
3170 staff: usize,
3171 measure: usize,
3172 value: Option<KeySignature>,
3173 },
3174 SetTimeSignature {
3175 part: usize,
3176 staff: usize,
3177 measure: usize,
3178 value: Option<TimeSignature>,
3179 },
3180 SetBarlines {
3181 part: usize,
3182 staff: usize,
3183 measure: usize,
3184 left: Barline,
3185 right: Barline,
3186 },
3187 SetRehearsal {
3188 part: usize,
3189 staff: usize,
3190 measure: usize,
3191 value: Option<String>,
3192 },
3193 SetVolta {
3194 part: usize,
3195 staff: usize,
3196 measure: usize,
3197 value: Option<VoltaBracket>,
3198 },
3199 AddNote {
3201 part: usize,
3202 staff: usize,
3203 measure: usize,
3204 voice: usize,
3205 #[serde(default = "legacy_append_index")]
3207 note_index: usize,
3208 note: Box<Note>,
3209 },
3210 RemoveNote {
3211 part: usize,
3212 staff: usize,
3213 measure: usize,
3214 voice: usize,
3215 note_index: usize,
3216 },
3217 ReplaceNote {
3219 part: usize,
3220 staff: usize,
3221 measure: usize,
3222 voice: usize,
3223 note_index: usize,
3224 note: Box<Note>,
3225 },
3226 SetMeasureTempo {
3227 part: usize,
3228 staff: usize,
3229 measure: usize,
3230 value: Option<u16>,
3231 },
3232 SetMeasureTempoRamp {
3233 part: usize,
3234 staff: usize,
3235 measure: usize,
3236 value: Option<u16>,
3237 },
3238 ReplaceScore {
3241 score: Box<Score>,
3242 },
3243}
3244
3245fn legacy_append_index() -> usize {
3246 usize::MAX
3247}
3248
3249fn patch_requires_replace(a: &Score, b: &Score) -> bool {
3253 if a.settings.time_signature != b.settings.time_signature
3254 || a.settings.key_signature != b.settings.key_signature
3255 || a.parts.len() != b.parts.len()
3256 || a.part_groups.len() != b.part_groups.len()
3257 {
3258 return true;
3259 }
3260 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3261 x.first_part != y.first_part
3262 || x.last_part != y.last_part
3263 || x.symbol != y.symbol
3264 || x.barlines_connect != y.barlines_connect
3265 }) {
3266 return true;
3267 }
3268 for (ap, bp) in a.parts.iter().zip(&b.parts) {
3269 if ap.percussion_instruments != bp.percussion_instruments
3270 || ap.staff_groups != bp.staff_groups
3271 || ap.staves.len() != bp.staves.len()
3272 {
3273 return true;
3274 }
3275 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
3276 if as_.measures.len() != bs.measures.len() {
3277 return true;
3278 }
3279 }
3280 }
3281 false
3282}
3283
3284pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
3289 let mut patches: Vec<ScorePatch> = Vec::new();
3290
3291 if patch_requires_replace(a, b) {
3292 return vec![ScorePatch::ReplaceScore {
3293 score: Box::new(b.clone()),
3294 }];
3295 }
3296
3297 if a.views != b.views {
3298 patches.push(ScorePatch::SetScoreViews {
3299 value: b.views.clone(),
3300 });
3301 }
3302
3303 if a.texts != b.texts {
3304 patches.push(ScorePatch::SetScoreTexts {
3305 value: b.texts.clone(),
3306 });
3307 }
3308 if a.chord_definitions != b.chord_definitions {
3309 patches.push(ScorePatch::SetChordDefinitions {
3310 value: b.chord_definitions.clone(),
3311 });
3312 }
3313
3314 macro_rules! meta {
3315 ($field:ident, $name:literal) => {
3316 if a.metadata.$field != b.metadata.$field {
3317 patches.push(ScorePatch::SetMetadata {
3318 field: $name.to_string(),
3319 value: b.metadata.$field.clone(),
3320 });
3321 }
3322 };
3323 }
3324 meta!(title, "title");
3325 meta!(composer, "composer");
3326 meta!(lyricist, "lyricist");
3327 meta!(copyright, "copyright");
3328 meta!(work_number, "work_number");
3329 meta!(movement_title, "movement_title");
3330
3331 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3332 patches.push(ScorePatch::SetTempo {
3333 value: b.settings.tempo_bpm,
3334 });
3335 }
3336
3337 for pi in 0..a.parts.len().min(b.parts.len()) {
3338 let ap = &a.parts[pi];
3339 let bp = &b.parts[pi];
3340 if ap.name != bp.name || ap.short_name != bp.short_name {
3341 patches.push(ScorePatch::SetPartNames {
3342 part: pi,
3343 name: bp.name.clone(),
3344 short_name: bp.short_name.clone(),
3345 });
3346 }
3347 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3348 patches.push(ScorePatch::SetPartMidi {
3349 part: pi,
3350 channel: bp.midi_channel,
3351 program: bp.midi_program,
3352 });
3353 }
3354 if ap.instrument != bp.instrument {
3355 patches.push(ScorePatch::SetInstrumentDefinition {
3356 part: pi,
3357 value: bp.instrument.clone(),
3358 });
3359 }
3360 if ap.midi_pitch_bends != bp.midi_pitch_bends
3361 || ap.midi_control_changes != bp.midi_control_changes
3362 || ap.midi_program_changes != bp.midi_program_changes
3363 || ap.midi_aftertouch != bp.midi_aftertouch
3364 {
3365 patches.push(ScorePatch::SetPartMidiAutomation {
3366 part: pi,
3367 pitch_bends: bp.midi_pitch_bends.clone(),
3368 control_changes: bp.midi_control_changes.clone(),
3369 program_changes: bp.midi_program_changes.clone(),
3370 aftertouch: bp.midi_aftertouch.clone(),
3371 });
3372 }
3373 for si in 0..ap.staves.len().min(bp.staves.len()) {
3374 let a_staff = &ap.staves[si];
3375 let b_staff = &bp.staves[si];
3376 if a_staff.clef != b_staff.clef
3377 || a_staff.transpose_semitones != b_staff.transpose_semitones
3378 {
3379 patches.push(ScorePatch::SetStaffConfiguration {
3380 part: pi,
3381 staff: si,
3382 clef: b_staff.clef.clone(),
3383 transpose_semitones: b_staff.transpose_semitones,
3384 });
3385 }
3386 if a_staff.tablature != b_staff.tablature {
3387 patches.push(ScorePatch::SetTablatureConfig {
3388 part: pi,
3389 staff: si,
3390 value: b_staff.tablature.clone(),
3391 });
3392 }
3393 if a_staff.presentation != b_staff.presentation {
3394 patches.push(ScorePatch::SetStaffPresentation {
3395 part: pi,
3396 staff: si,
3397 value: b_staff.presentation.clone(),
3398 });
3399 }
3400 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3401 let am = &a_staff.measures[mi];
3402 let bm = &b_staff.measures[mi];
3403
3404 if am.tablature_change != bm.tablature_change {
3405 patches.push(ScorePatch::SetMeasureTablatureChange {
3406 part: pi,
3407 staff: si,
3408 measure: mi,
3409 value: bm.tablature_change.clone(),
3410 });
3411 }
3412
3413 if am.mid_clefs != bm.mid_clefs {
3414 patches.push(ScorePatch::SetMeasureMidClefs {
3415 part: pi,
3416 staff: si,
3417 measure: mi,
3418 value: bm.mid_clefs.clone(),
3419 });
3420 }
3421
3422 if am.number != bm.number
3423 || am.clef != bm.clef
3424 || am.tempo_text != bm.tempo_text
3425 || am.navigation != bm.navigation
3426 || am.expression_text != bm.expression_text
3427 || am.multi_rest_count != bm.multi_rest_count
3428 || am.system_break != bm.system_break
3429 || am.page_break != bm.page_break
3430 || am.section_break != bm.section_break
3431 {
3432 patches.push(ScorePatch::SetMeasurePresentation {
3433 part: pi,
3434 staff: si,
3435 measure: mi,
3436 number: bm.number,
3437 clef: bm.clef.clone(),
3438 tempo_text: bm.tempo_text.clone(),
3439 navigation: bm.navigation.clone(),
3440 expression_text: bm.expression_text.clone(),
3441 multi_rest_count: bm.multi_rest_count,
3442 system_break: bm.system_break,
3443 page_break: bm.page_break,
3444 section_break: bm.section_break,
3445 });
3446 }
3447 if am.key_sig != bm.key_sig {
3448 patches.push(ScorePatch::SetKeySignature {
3449 part: pi,
3450 staff: si,
3451 measure: mi,
3452 value: bm.key_sig.clone(),
3453 });
3454 }
3455 if am.time_sig != bm.time_sig {
3456 patches.push(ScorePatch::SetTimeSignature {
3457 part: pi,
3458 staff: si,
3459 measure: mi,
3460 value: bm.time_sig.clone(),
3461 });
3462 }
3463 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3464 patches.push(ScorePatch::SetBarlines {
3465 part: pi,
3466 staff: si,
3467 measure: mi,
3468 left: bm.barline_left.clone(),
3469 right: bm.barline_right.clone(),
3470 });
3471 }
3472 if am.rehearsal != bm.rehearsal {
3473 patches.push(ScorePatch::SetRehearsal {
3474 part: pi,
3475 staff: si,
3476 measure: mi,
3477 value: bm.rehearsal.clone(),
3478 });
3479 }
3480 if am.volta != bm.volta {
3481 patches.push(ScorePatch::SetVolta {
3482 part: pi,
3483 staff: si,
3484 measure: mi,
3485 value: bm.volta.clone(),
3486 });
3487 }
3488 if am.tempo != bm.tempo {
3489 patches.push(ScorePatch::SetMeasureTempo {
3490 part: pi,
3491 staff: si,
3492 measure: mi,
3493 value: bm.tempo,
3494 });
3495 }
3496 if am.tempo_ramp_to != bm.tempo_ramp_to {
3497 patches.push(ScorePatch::SetMeasureTempoRamp {
3498 part: pi,
3499 staff: si,
3500 measure: mi,
3501 value: bm.tempo_ramp_to,
3502 });
3503 }
3504 if am.texts != bm.texts {
3505 patches.push(ScorePatch::SetMeasureTexts {
3506 part: pi,
3507 staff: si,
3508 measure: mi,
3509 value: bm.texts.clone(),
3510 });
3511 }
3512 if am.figured_bass != bm.figured_bass {
3513 patches.push(ScorePatch::SetFiguredBass {
3514 part: pi,
3515 staff: si,
3516 measure: mi,
3517 value: bm.figured_bass.clone(),
3518 });
3519 }
3520 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3521 patches.push(ScorePatch::SetHarpPedalDiagrams {
3522 part: pi,
3523 staff: si,
3524 measure: mi,
3525 value: bm.harp_pedal_diagrams.clone(),
3526 });
3527 }
3528
3529 for vi in 0..4usize {
3530 let av = &am.voices[vi];
3531 let bv = &bm.voices[vi];
3532 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3533 if !note_content_eq(a_note, b_note) {
3534 patches.push(ScorePatch::ReplaceNote {
3535 part: pi,
3536 staff: si,
3537 measure: mi,
3538 voice: vi,
3539 note_index: ni,
3540 note: Box::new(b_note.clone()),
3541 });
3542 }
3543 }
3544 for ni in (bv.len()..av.len()).rev() {
3546 patches.push(ScorePatch::RemoveNote {
3547 part: pi,
3548 staff: si,
3549 measure: mi,
3550 voice: vi,
3551 note_index: ni,
3552 });
3553 }
3554 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
3556 patches.push(ScorePatch::AddNote {
3557 part: pi,
3558 staff: si,
3559 measure: mi,
3560 voice: vi,
3561 note_index: av.len() + offset,
3562 note: Box::new(note.clone()),
3563 });
3564 }
3565 }
3566 }
3567 }
3568 }
3569
3570 patches
3571}
3572
3573pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
3578 let mut s = score.clone();
3579 for patch in patches {
3580 match patch {
3581 ScorePatch::ReplaceScore { score } => {
3582 s = (**score).clone();
3583 }
3584 ScorePatch::SetScoreViews { value } => {
3585 s.views = value.clone();
3586 }
3587 ScorePatch::SetScoreTexts { value } => {
3588 s.texts = value.clone();
3589 }
3590 ScorePatch::SetMeasureTexts {
3591 part,
3592 staff,
3593 measure,
3594 value,
3595 } => {
3596 s.parts
3597 .get_mut(*part)
3598 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3599 .staves
3600 .get_mut(*staff)
3601 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3602 .measures
3603 .get_mut(*measure)
3604 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3605 .texts = value.clone();
3606 }
3607 ScorePatch::SetFiguredBass {
3608 part,
3609 staff,
3610 measure,
3611 value,
3612 } => {
3613 s.parts
3614 .get_mut(*part)
3615 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3616 .staves
3617 .get_mut(*staff)
3618 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3619 .measures
3620 .get_mut(*measure)
3621 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3622 .figured_bass = value.clone();
3623 }
3624 ScorePatch::SetHarpPedalDiagrams {
3625 part,
3626 staff,
3627 measure,
3628 value,
3629 } => {
3630 s.parts
3631 .get_mut(*part)
3632 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3633 .staves
3634 .get_mut(*staff)
3635 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3636 .measures
3637 .get_mut(*measure)
3638 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3639 .harp_pedal_diagrams = value.clone();
3640 }
3641 ScorePatch::SetChordDefinitions { value } => {
3642 s.chord_definitions = value.clone();
3643 }
3644 ScorePatch::SetPartNames {
3645 part,
3646 name,
3647 short_name,
3648 } => {
3649 let target = s
3650 .parts
3651 .get_mut(*part)
3652 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3653 target.name = name.clone();
3654 target.short_name = short_name.clone();
3655 }
3656 ScorePatch::SetPartMidi {
3657 part,
3658 channel,
3659 program,
3660 } => {
3661 let target = s
3662 .parts
3663 .get_mut(*part)
3664 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3665 target.midi_channel = *channel;
3666 target.midi_program = *program;
3667 }
3668 ScorePatch::SetInstrumentDefinition { part, value } => {
3669 s.parts
3670 .get_mut(*part)
3671 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3672 .instrument = value.clone();
3673 }
3674 ScorePatch::SetPartMidiAutomation {
3675 part,
3676 pitch_bends,
3677 control_changes,
3678 program_changes,
3679 aftertouch,
3680 } => {
3681 let target = s
3682 .parts
3683 .get_mut(*part)
3684 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3685 target.midi_pitch_bends = pitch_bends.clone();
3686 target.midi_control_changes = control_changes.clone();
3687 target.midi_program_changes = program_changes.clone();
3688 target.midi_aftertouch = aftertouch.clone();
3689 }
3690 ScorePatch::SetStaffConfiguration {
3691 part,
3692 staff,
3693 clef,
3694 transpose_semitones,
3695 } => {
3696 let target = s
3697 .parts
3698 .get_mut(*part)
3699 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3700 .staves
3701 .get_mut(*staff)
3702 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3703 target.clef = clef.clone();
3704 target.transpose_semitones = *transpose_semitones;
3705 }
3706 ScorePatch::SetStaffPresentation { part, staff, value } => {
3707 let target = s
3708 .parts
3709 .get_mut(*part)
3710 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3711 .staves
3712 .get_mut(*staff)
3713 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3714 target.presentation = value.clone();
3715 }
3716 ScorePatch::SetMeasurePresentation {
3717 part,
3718 staff,
3719 measure,
3720 number,
3721 clef,
3722 tempo_text,
3723 navigation,
3724 expression_text,
3725 multi_rest_count,
3726 system_break,
3727 page_break,
3728 section_break,
3729 } => {
3730 let target = s
3731 .parts
3732 .get_mut(*part)
3733 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3734 .staves
3735 .get_mut(*staff)
3736 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3737 .measures
3738 .get_mut(*measure)
3739 .ok_or_else(|| {
3740 Error::InvalidPatch(format!("measure {measure} out of range"))
3741 })?;
3742 target.number = *number;
3743 target.clef = clef.clone();
3744 target.tempo_text = tempo_text.clone();
3745 target.navigation = navigation.clone();
3746 target.expression_text = expression_text.clone();
3747 target.multi_rest_count = *multi_rest_count;
3748 target.system_break = *system_break;
3749 target.page_break = *page_break;
3750 target.section_break = *section_break;
3751 }
3752 ScorePatch::SetTablatureConfig { part, staff, value } => {
3753 s.parts
3754 .get_mut(*part)
3755 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3756 .staves
3757 .get_mut(*staff)
3758 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3759 .tablature = value.clone();
3760 }
3761 ScorePatch::SetMeasureTablatureChange {
3762 part,
3763 staff,
3764 measure,
3765 value,
3766 } => {
3767 s.parts
3768 .get_mut(*part)
3769 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3770 .staves
3771 .get_mut(*staff)
3772 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3773 .measures
3774 .get_mut(*measure)
3775 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3776 .tablature_change = value.clone();
3777 }
3778 ScorePatch::SetMeasureMidClefs {
3779 part,
3780 staff,
3781 measure,
3782 value,
3783 } => {
3784 s.parts
3785 .get_mut(*part)
3786 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3787 .staves
3788 .get_mut(*staff)
3789 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3790 .measures
3791 .get_mut(*measure)
3792 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3793 .mid_clefs = value.clone();
3794 }
3795 ScorePatch::SetMetadata { field, value } => match field.as_str() {
3796 "title" => s.metadata.title = value.clone(),
3797 "composer" => s.metadata.composer = value.clone(),
3798 "lyricist" => s.metadata.lyricist = value.clone(),
3799 "copyright" => s.metadata.copyright = value.clone(),
3800 "work_number" => s.metadata.work_number = value.clone(),
3801 "movement_title" => s.metadata.movement_title = value.clone(),
3802 other => {
3803 return Err(Error::InvalidPatch(format!(
3804 "unknown metadata field: {other}"
3805 )));
3806 }
3807 },
3808 ScorePatch::SetTempo { value } => {
3809 s.settings.tempo_bpm = *value;
3810 }
3811 ScorePatch::SetKeySignature {
3812 part,
3813 staff,
3814 measure,
3815 value,
3816 } => {
3817 s.parts
3818 .get_mut(*part)
3819 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3820 .staves
3821 .get_mut(*staff)
3822 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3823 .measures
3824 .get_mut(*measure)
3825 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3826 .key_sig = value.clone();
3827 }
3828 ScorePatch::SetTimeSignature {
3829 part,
3830 staff,
3831 measure,
3832 value,
3833 } => {
3834 s.parts
3835 .get_mut(*part)
3836 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3837 .staves
3838 .get_mut(*staff)
3839 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3840 .measures
3841 .get_mut(*measure)
3842 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3843 .time_sig = value.clone();
3844 }
3845 ScorePatch::SetBarlines {
3846 part,
3847 staff,
3848 measure,
3849 left,
3850 right,
3851 } => {
3852 let m = s
3853 .parts
3854 .get_mut(*part)
3855 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3856 .staves
3857 .get_mut(*staff)
3858 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3859 .measures
3860 .get_mut(*measure)
3861 .ok_or_else(|| {
3862 Error::InvalidPatch(format!("measure {measure} out of range"))
3863 })?;
3864 m.barline_left = left.clone();
3865 m.barline_right = right.clone();
3866 }
3867 ScorePatch::SetRehearsal {
3868 part,
3869 staff,
3870 measure,
3871 value,
3872 } => {
3873 s.parts
3874 .get_mut(*part)
3875 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3876 .staves
3877 .get_mut(*staff)
3878 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3879 .measures
3880 .get_mut(*measure)
3881 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3882 .rehearsal = value.clone();
3883 }
3884 ScorePatch::SetVolta {
3885 part,
3886 staff,
3887 measure,
3888 value,
3889 } => {
3890 s.parts
3891 .get_mut(*part)
3892 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3893 .staves
3894 .get_mut(*staff)
3895 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3896 .measures
3897 .get_mut(*measure)
3898 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3899 .volta = value.clone();
3900 }
3901 ScorePatch::AddNote {
3902 part,
3903 staff,
3904 measure,
3905 voice,
3906 note_index,
3907 note,
3908 } => {
3909 let v = s
3910 .parts
3911 .get_mut(*part)
3912 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3913 .staves
3914 .get_mut(*staff)
3915 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3916 .measures
3917 .get_mut(*measure)
3918 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3919 .voices
3920 .get_mut(*voice)
3921 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3922 let insert_at = if *note_index == usize::MAX {
3923 v.len()
3924 } else {
3925 *note_index
3926 };
3927 if insert_at > v.len() {
3928 return Err(Error::InvalidPatch(format!(
3929 "note_index {note_index} out of range"
3930 )));
3931 }
3932 v.insert(insert_at, *note.clone());
3933 }
3934 ScorePatch::RemoveNote {
3935 part,
3936 staff,
3937 measure,
3938 voice,
3939 note_index,
3940 } => {
3941 let v = s
3942 .parts
3943 .get_mut(*part)
3944 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3945 .staves
3946 .get_mut(*staff)
3947 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3948 .measures
3949 .get_mut(*measure)
3950 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3951 .voices
3952 .get_mut(*voice)
3953 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3954 if *note_index >= v.len() {
3955 return Err(Error::InvalidPatch(format!(
3956 "note_index {note_index} out of range"
3957 )));
3958 }
3959 v.remove(*note_index);
3960 }
3961 ScorePatch::ReplaceNote {
3962 part,
3963 staff,
3964 measure,
3965 voice,
3966 note_index,
3967 note,
3968 } => {
3969 let v = s
3970 .parts
3971 .get_mut(*part)
3972 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3973 .staves
3974 .get_mut(*staff)
3975 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3976 .measures
3977 .get_mut(*measure)
3978 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3979 .voices
3980 .get_mut(*voice)
3981 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3982 if *note_index >= v.len() {
3983 return Err(Error::InvalidPatch(format!(
3984 "note_index {note_index} out of range"
3985 )));
3986 }
3987 v[*note_index] = *note.clone();
3988 }
3989 ScorePatch::SetMeasureTempo {
3990 part,
3991 staff,
3992 measure,
3993 value,
3994 } => {
3995 s.parts
3996 .get_mut(*part)
3997 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3998 .staves
3999 .get_mut(*staff)
4000 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4001 .measures
4002 .get_mut(*measure)
4003 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4004 .tempo = *value;
4005 }
4006 ScorePatch::SetMeasureTempoRamp {
4007 part,
4008 staff,
4009 measure,
4010 value,
4011 } => {
4012 s.parts
4013 .get_mut(*part)
4014 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4015 .staves
4016 .get_mut(*staff)
4017 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4018 .measures
4019 .get_mut(*measure)
4020 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4021 .tempo_ramp_to = *value;
4022 }
4023 }
4024 }
4025 if !super::validate::validate(&s).is_valid() {
4026 return Err(Error::InvalidScore);
4027 }
4028 Ok(s)
4029}
4030
4031pub fn voice_duration_beats(voice: &[Note], measure_beats: f64) -> f64 {
4035 match voice {
4036 [only] if only.is_plain_whole_rest() => measure_beats,
4037 _ => voice.iter().map(Note::beats).sum(),
4038 }
4039}
4040
4041pub fn respell_score(score: &mut Score, prefer_flat: bool) {
4045 for part in &mut score.parts {
4046 for staff in &mut part.staves {
4047 for measure in &mut staff.measures {
4048 for voice in &mut measure.voices {
4049 for note in voice.iter_mut() {
4050 for pitch in &mut note.pitches {
4051 *pitch = pitch.respell(prefer_flat);
4052 }
4053 }
4054 }
4055 }
4056 }
4057 }
4058}
4059
4060#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4062#[serde(rename_all = "snake_case")]
4063pub enum RespellPolicy {
4064 Flat,
4066 Sharp,
4068 Key,
4070}
4071
4072pub fn respell_staff_region(
4083 score: &mut Score,
4084 part_index: usize,
4085 staff_index: usize,
4086 start_measure: usize,
4087 end_measure: usize,
4088 policy: RespellPolicy,
4089) -> Result<(usize, usize), Error> {
4090 let score_key_flat = score.settings.key_signature.fifths < 0;
4091 let part = score
4092 .parts
4093 .get_mut(part_index)
4094 .ok_or(Error::PartNotFound(part_index))?;
4095 let staff_count = part.staves.len();
4096 if staff_index >= staff_count {
4097 return Err(Error::StaffNotFound(staff_index));
4098 }
4099 let measure_count = part.staves[staff_index].measures.len();
4100 if start_measure >= end_measure || end_measure > measure_count {
4101 return Err(Error::InvalidCommand(format!(
4102 "invalid measure range {start_measure}..{end_measure}"
4103 )));
4104 }
4105 let mut prefer_flat = Vec::with_capacity(end_measure);
4106 let mut running_flat = score_key_flat;
4107 for index in 0..end_measure {
4108 let key = part.staves[staff_index].measures[index]
4109 .key_sig
4110 .as_ref()
4111 .or_else(|| {
4112 part.staves[0]
4113 .measures
4114 .get(index)
4115 .and_then(|measure| measure.key_sig.as_ref())
4116 });
4117 if let Some(key) = key {
4118 running_flat = key.fifths < 0;
4119 }
4120 prefer_flat.push(match policy {
4121 RespellPolicy::Flat => true,
4122 RespellPolicy::Sharp => false,
4123 RespellPolicy::Key => running_flat,
4124 });
4125 }
4126
4127 let measures = &mut part.staves[staff_index].measures;
4128 for (index, measure) in measures
4129 .iter_mut()
4130 .enumerate()
4131 .take(end_measure)
4132 .skip(start_measure)
4133 {
4134 for voice in &mut measure.voices {
4135 for note in voice
4136 .iter_mut()
4137 .filter(|note| !note.is_rest && !note.is_unpitched)
4138 {
4139 for pitch in &mut note.pitches {
4140 *pitch = pitch.respell(prefer_flat[index]);
4141 }
4142 }
4143 }
4144 }
4145
4146 let mut changed = (start_measure, end_measure - 1);
4147 for voice in 0..4 {
4148 for measure in start_measure..end_measure {
4149 for note in 0..measures[measure].voices[voice].len() {
4150 if measures[measure].voices[voice][note].tie_start {
4151 let last = propagate_tied_spelling(measures, voice, (measure, note), true);
4152 changed.1 = changed.1.max(last);
4153 }
4154 }
4155 }
4156 if !measures[start_measure].voices[voice].is_empty() {
4157 let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
4158 changed.0 = changed.0.min(first);
4159 }
4160 }
4161 Ok(changed)
4162}
4163
4164fn propagate_tied_spelling(
4167 measures: &mut [Measure],
4168 voice: usize,
4169 from: (usize, usize),
4170 forward: bool,
4171) -> usize {
4172 let mut current = from;
4173 loop {
4174 let note = &measures[current.0].voices[voice][current.1];
4175 if !(if forward {
4176 note.tie_start
4177 } else {
4178 note.tie_end
4179 }) {
4180 return current.0;
4181 }
4182 let neighbor = if forward {
4183 if current.1 + 1 < measures[current.0].voices[voice].len() {
4184 Some((current.0, current.1 + 1))
4185 } else {
4186 measures
4187 .get(current.0 + 1)
4188 .filter(|measure| !measure.voices[voice].is_empty())
4189 .map(|_| (current.0 + 1, 0))
4190 }
4191 } else if current.1 > 0 {
4192 Some((current.0, current.1 - 1))
4193 } else {
4194 current.0.checked_sub(1).and_then(|previous| {
4195 measures[previous].voices[voice]
4196 .len()
4197 .checked_sub(1)
4198 .map(|last| (previous, last))
4199 })
4200 };
4201 let Some(next) = neighbor else {
4202 return current.0;
4203 };
4204 let next_note = &measures[next.0].voices[voice][next.1];
4205 if !(if forward {
4206 next_note.tie_end
4207 } else {
4208 next_note.tie_start
4209 }) {
4210 return current.0;
4211 }
4212 let source = measures[current.0].voices[voice][current.1].pitches.clone();
4213 for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
4214 if let Some(spelled) = source.iter().find(|candidate| {
4215 candidate.to_midi() == pitch.to_midi()
4216 && candidate.microtone_cents == pitch.microtone_cents
4217 }) {
4218 *pitch = spelled.clone();
4219 }
4220 }
4221 current = next;
4222 }
4223}
4224
4225pub fn respell_score_to_key(score: &mut Score) {
4229 let prefer_flat = score.settings.key_signature.fifths < 0;
4230 respell_score(score, prefer_flat);
4231}
4232
4233pub fn score_duration_secs(score: &Score) -> f64 {
4238 if score.settings.tempo_bpm == 0 {
4239 return 0.0;
4240 }
4241 let seq = measure_sequence(score);
4242 let mut total_secs = 0.0f64;
4243 let mut current_bpm = score.settings.tempo_bpm as f64;
4244 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4245 for &idx in &seq {
4246 if let Some(m) = staff.measures.get(idx) {
4247 if let Some(b) = m.tempo {
4248 current_bpm = b as f64;
4249 }
4250 if current_bpm == 0.0 {
4251 continue;
4252 }
4253 let beats = voice_duration_beats(
4254 &m.voices[0],
4255 m.duration_beats(&score.settings.time_signature),
4256 );
4257 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4258 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4259 current_bpm = f64::from(target);
4260 }
4261 }
4262 }
4263 }
4264 total_secs
4265}
4266
4267pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
4272 if score.settings.tempo_bpm == 0 {
4273 return 0.0;
4274 }
4275 let seq: Vec<usize> = measure_sequence(score)
4276 .into_iter()
4277 .filter(|&idx| idx >= region.0 && idx <= region.1)
4278 .collect();
4279 let mut total_secs = 0.0f64;
4280 let mut current_bpm = score.settings.tempo_bpm as f64;
4281 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4282 for &idx in &seq {
4283 if let Some(m) = staff.measures.get(idx) {
4284 if let Some(b) = m.tempo {
4285 current_bpm = b as f64;
4286 }
4287 if current_bpm == 0.0 {
4288 continue;
4289 }
4290 let beats = voice_duration_beats(
4291 &m.voices[0],
4292 m.duration_beats(&score.settings.time_signature),
4293 );
4294 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4295 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4296 current_bpm = f64::from(target);
4297 }
4298 }
4299 }
4300 }
4301 total_secs
4302}
4303
4304fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
4305 let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
4306 return beats / start_bpm * 60.0;
4307 };
4308 let end_bpm = f64::from(target_bpm);
4309 let delta = end_bpm - start_bpm;
4310 if delta.abs() < f64::EPSILON {
4311 return beats / start_bpm * 60.0;
4312 }
4313 60.0 * beats / delta * (end_bpm / start_bpm).ln()
4314}
4315
4316pub fn measure_beats_remaining(
4321 score: &Score,
4322 part_index: usize,
4323 staff_index: usize,
4324 measure_index: usize,
4325 voice_index: usize,
4326) -> Result<f64, Error> {
4327 let part = score
4328 .parts
4329 .get(part_index)
4330 .ok_or(Error::PartNotFound(part_index))?;
4331 let staff = part
4332 .staves
4333 .get(staff_index)
4334 .ok_or(Error::StaffNotFound(staff_index))?;
4335 let measure = staff
4336 .measures
4337 .get(measure_index)
4338 .ok_or(Error::MeasureNotFound(measure_index))?;
4339 let voice = measure
4340 .voices
4341 .get(voice_index)
4342 .ok_or(Error::VoiceOutOfRange(voice_index))?;
4343 let capacity = measure.duration_beats(&score.settings.time_signature);
4344 Ok((capacity - voice_duration_beats(voice, capacity)).max(0.0))
4345}
4346
4347pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
4353 if pitches.is_empty() {
4354 return true;
4355 }
4356 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
4357 avg < clef.middle_line_midi() as f64
4358}
4359
4360fn beam_beat_size(ts: &TimeSignature) -> f64 {
4361 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
4362 3.0 * 4.0 / ts.denominator as f64
4363 } else {
4364 4.0 / ts.denominator as f64
4365 }
4366}
4367
4368pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
4373 let beat_size = beam_beat_size(time_sig);
4374 let n = notes.len();
4375 let mut result = vec![BeamState::None; n];
4376
4377 let is_beamable = |note: &Note| -> bool {
4378 !note.is_rest
4379 && matches!(
4380 note.duration,
4381 Duration::Eighth
4382 | Duration::Sixteenth
4383 | Duration::ThirtySecond
4384 | Duration::SixtyFourth
4385 )
4386 };
4387
4388 let mut starts = Vec::with_capacity(n);
4390 let mut pos = 0.0f64;
4391 for note in notes {
4392 starts.push(pos);
4393 pos += note.beats();
4394 }
4395
4396 let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
4398
4399 let mut i = 0;
4400 while i < n {
4401 if !is_beamable(¬es[i]) {
4402 i += 1;
4403 continue;
4404 }
4405 let g = group_id(i);
4406 let mut j = i;
4408 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
4409 j += 1;
4410 }
4411 let run = j - i;
4412 if run == 1 {
4413 result[i] = BeamState::None;
4414 } else {
4415 result[i] = BeamState::Begin;
4416 result[i + 1..j - 1].fill(BeamState::Continue);
4417 result[j - 1] = BeamState::End;
4418 }
4419 i = j;
4420 }
4421 result
4422}
4423
4424fn note_content_eq(a: &Note, b: &Note) -> bool {
4425 a.is_rest == b.is_rest
4426 && a.is_unpitched == b.is_unpitched
4427 && a.instrument_id == b.instrument_id
4428 && a.offset_x == b.offset_x
4429 && a.offset_y == b.offset_y
4430 && a.relative_x == b.relative_x
4431 && a.relative_y == b.relative_y
4432 && a.pitches == b.pitches
4433 && a.duration == b.duration
4434 && a.dot_count == b.dot_count
4435 && a.tie_start == b.tie_start
4436 && a.tie_end == b.tie_end
4437 && a.beam == b.beam
4438 && a.articulations == b.articulations
4439 && a.dynamic == b.dynamic
4440 && a.stem_up == b.stem_up
4441 && a.hairpin_start == b.hairpin_start
4442 && a.hairpin_end == b.hairpin_end
4443 && a.tuplet == b.tuplet
4444 && a.chord_symbol == b.chord_symbol
4445 && a.is_grace == b.is_grace
4446 && a.grace_slash == b.grace_slash
4447 && a.ottava_start == b.ottava_start
4448 && a.ottava_end == b.ottava_end
4449 && a.lyric == b.lyric
4450 && a.additional_lyrics == b.additional_lyrics
4451 && a.pedal_start == b.pedal_start
4452 && a.pedal_end == b.pedal_end
4453 && a.slur_start == b.slur_start
4454 && a.slur_end == b.slur_end
4455 && a.arpeggiate == b.arpeggiate
4456 && a.tab_position == b.tab_position
4457 && a.tab_positions == b.tab_positions
4458 && a.guitar_technique == b.guitar_technique
4459 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
4460 && a.guitar_bend_curve == b.guitar_bend_curve
4461}
4462
4463fn per_pitch_flag(flags: &[bool], pitch_count: usize, index: usize, chord: bool) -> bool {
4464 if flags.len() == pitch_count {
4465 flags.get(index).copied().unwrap_or(chord)
4466 } else {
4467 chord
4468 }
4469}
4470
4471#[cfg(test)]
4472mod tests {
4473 use super::*;
4474 use crate::model::{
4475 notation::{FingeringSelectionPolicy, TextStyle},
4476 pitch::Step,
4477 };
4478
4479 #[test]
4480 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
4481 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4482 note.fingerings = vec![3, 1, 4];
4483 note.fingering = Some(3);
4484 assert_eq!(
4485 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
4486 Some(3)
4487 );
4488 assert_eq!(
4489 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
4490 Some(1)
4491 );
4492 assert_eq!(
4493 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
4494 Some(4)
4495 );
4496 assert_eq!(note.fingerings, vec![3, 1, 4]);
4497 assert_eq!(note.fingering, Some(3));
4498 }
4499
4500 #[test]
4501 fn default_score_has_one_part_four_measures() {
4502 let score = Score::default();
4503 assert_eq!(score.parts.len(), 1);
4504 assert_eq!(score.parts[0].staves.len(), 1);
4505 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
4506 }
4507
4508 #[test]
4509 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
4510 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4511 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4512 lines: 6,
4513 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4514 capo: 2,
4515 });
4516 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
4517 Pitch::with_alter(Step::F, 4, 1),
4518 Duration::Quarter,
4519 ));
4520 score.parts[0].staves[0].measures[0].voices[0]
4521 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
4522 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
4523 Some(TabPosition { string: 6, fret: 7 });
4524
4525 assert_eq!(assign_tablature_positions(&mut score), 1);
4526 let notes = &score.parts[0].staves[0].measures[0].voices[0];
4527 assert_eq!(
4528 notes[1].tab_position,
4529 Some(TabPosition { string: 1, fret: 0 })
4530 );
4531 assert_eq!(notes[1].string_number, Some(1));
4532 assert_eq!(
4533 notes[2].tab_position,
4534 Some(TabPosition { string: 6, fret: 7 })
4535 );
4536 }
4537
4538 #[test]
4539 fn assign_tablature_positions_uses_measure_local_capo_change() {
4540 let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
4541 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4542 lines: 6,
4543 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4544 capo: 0,
4545 });
4546 score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
4547 lines: 6,
4548 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4549 capo: 2,
4550 });
4551 score.parts[0].staves[0].measures[1].voices[0]
4552 .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
4553
4554 assert_eq!(assign_tablature_positions(&mut score), 1);
4555 assert_eq!(
4556 score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
4557 Some(TabPosition { string: 5, fret: 3 })
4558 );
4559 }
4560
4561 #[test]
4562 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
4563 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4564 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4565 lines: 6,
4566 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4567 capo: 0,
4568 });
4569 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4570 chord.pitches.push(Pitch::new(Step::G, 4));
4571 score.parts[0].staves[0].measures[0].voices[0].push(chord);
4572
4573 assert_eq!(assign_tablature_positions(&mut score), 1);
4574 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
4575 assert_eq!(
4576 positions,
4577 &vec![
4578 TabPosition { string: 2, fret: 5 },
4579 TabPosition { string: 1, fret: 3 },
4580 ]
4581 );
4582 }
4583
4584 #[test]
4585 fn new_score_measure_count() {
4586 let score = Score::new("Test", 120, 4, 4, 0, 8);
4587 assert_eq!(score.measure_count(), 8);
4588 }
4589
4590 #[test]
4591 fn note_beats_quarter() {
4592 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4593 assert!((note.beats() - 1.0).abs() < 1e-9);
4594 }
4595
4596 #[test]
4597 fn note_beats_dotted_quarter() {
4598 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4599 note.dot_count = 1;
4600 assert!((note.beats() - 1.5).abs() < 1e-9);
4601 }
4602
4603 #[test]
4604 fn grace_note_beats_zero() {
4605 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
4606 note.is_grace = true;
4607 assert_eq!(note.beats(), 0.0);
4608 }
4609
4610 #[test]
4611 fn measure_empty_4_4_fills_four_beats() {
4612 let m = Measure::empty(4, 4);
4613 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4614 assert!((total - 4.0).abs() < 1e-9);
4615 }
4616
4617 #[test]
4618 fn measure_empty_3_4_fills_three_beats() {
4619 let m = Measure::empty(3, 4);
4620 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4621 assert!((total - 3.0).abs() < 1e-9);
4622 }
4623
4624 #[test]
4625 fn whole_filling_beats() {
4626 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
4627 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
4628 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
4629 }
4630
4631 #[test]
4634 fn statistics_default_score_all_rests() {
4635 let score = Score::default();
4636 let s = score.statistics();
4637 assert_eq!(s.part_count, 1);
4638 assert_eq!(s.measure_count, 4);
4639 assert_eq!(s.note_count, 0);
4640 assert!(s.rest_count > 0);
4641 }
4642
4643 #[test]
4644 fn statistics_duration_estimate() {
4645 let score = Score::new("T", 120, 4, 4, 0, 1);
4647 let s = score.statistics();
4648 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
4649 }
4650
4651 #[test]
4652 fn score_duration_secs_matches_statistics() {
4653 use super::score_duration_secs;
4654 let score = Score::new("T", 120, 4, 4, 0, 4);
4655 let secs = score_duration_secs(&score);
4656 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
4658 }
4659
4660 #[test]
4661 fn score_duration_secs_integrates_measure_tempo_ramp() {
4662 use super::score_duration_secs;
4663 let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
4664 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4665 let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
4666 assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
4667 }
4668
4669 #[test]
4670 fn score_duration_secs_zero_bpm_returns_zero() {
4671 use super::score_duration_secs;
4672 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4673 score.settings.tempo_bpm = 0;
4674 assert_eq!(score_duration_secs(&score), 0.0);
4675 }
4676
4677 #[test]
4678 fn score_duration_secs_per_measure_tempo() {
4679 use super::score_duration_secs;
4680 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4682 score.parts[0].staves[0].measures[1].tempo = Some(60);
4683 let secs = score_duration_secs(&score);
4684 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
4685 }
4686
4687 #[test]
4690 fn extract_part_returns_single_part_score() {
4691 let mut score = Score::default();
4692 let mut p2 = Part::new("Violin", "Vln.");
4693 p2.staves.push(Staff::new(Clef::Treble));
4694 score.parts.push(p2);
4695 let ex = score.extract_part(0).unwrap();
4696 assert_eq!(ex.parts.len(), 1);
4697 assert_ne!(ex.id, score.id);
4698 assert_eq!(ex.metadata.title, score.metadata.title);
4699 }
4700
4701 #[test]
4702 fn extract_part_out_of_range_is_none() {
4703 let score = Score::default();
4704 assert!(score.extract_part(99).is_none());
4705 }
4706
4707 #[test]
4708 fn extract_and_merge_remap_typed_spanner_part_addresses() {
4709 let mut left = Score::template(ScoreTemplate::StringQuartet);
4710 let address = NoteAddr {
4711 part: 1,
4712 staff: 0,
4713 measure: 0,
4714 voice: 0,
4715 note: 0,
4716 };
4717 left.spanners.push(NotationSpanner {
4718 id: "left-span".to_string(),
4719 kind: NotationSpannerKind::Slur,
4720 start: address.clone(),
4721 end: address,
4722 number: Some(1),
4723 line_type: None,
4724 text: None,
4725 placement: None,
4726 ottava_size: None,
4727 ottava_type: None,
4728 });
4729 let extracted = left.extract_part_checked(1).expect("valid extracted part");
4730 assert_eq!(extracted.spanners[0].start.part, 0);
4731 assert_eq!(extracted.spanners[0].end.part, 0);
4732
4733 let mut right = Score::new("R", 120, 4, 4, 0, 1);
4734 let right_address = NoteAddr {
4735 part: 0,
4736 staff: 0,
4737 measure: 0,
4738 voice: 0,
4739 note: 0,
4740 };
4741 right.spanners.push(NotationSpanner {
4742 id: "right-span".to_string(),
4743 kind: NotationSpannerKind::Pedal,
4744 start: right_address.clone(),
4745 end: right_address,
4746 number: Some(1),
4747 line_type: None,
4748 text: None,
4749 placement: None,
4750 ottava_size: None,
4751 ottava_type: None,
4752 });
4753 let merged = left.merge_checked(&right).expect("valid merged score");
4754 let right_span = merged
4755 .spanners
4756 .iter()
4757 .find(|spanner| spanner.id == "right-span")
4758 .expect("merged right span");
4759 assert_eq!(right_span.start.part, left.parts.len());
4760 assert_eq!(right_span.end.part, left.parts.len());
4761 }
4762
4763 #[test]
4766 fn transpose_zero_is_clone() {
4767 let score = Score::new("T", 120, 4, 4, 0, 1);
4768 let t = transpose(&score, 0);
4769 assert_eq!(t.settings.key_signature.fifths, 0);
4770 }
4771
4772 #[test]
4773 fn transpose_staff_region_rewrites_only_selected_written_measures() {
4774 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4775 for measure in &mut score.parts[0].staves[0].measures {
4776 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4777 }
4778 let transformed = transpose_staff_region_checked(
4779 &score,
4780 0,
4781 0,
4782 1,
4783 2,
4784 2,
4785 RegionalTranspositionTarget::Written,
4786 )
4787 .expect("region should transpose");
4788 assert_eq!(
4789 transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
4790 Pitch::new(Step::C, 4)
4791 );
4792 assert_eq!(
4793 transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4794 Pitch::new(Step::D, 4)
4795 );
4796 assert_eq!(
4797 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4798 Pitch::new(Step::C, 4)
4799 );
4800 }
4801
4802 #[test]
4803 fn regional_concert_transposition_requires_full_staff() {
4804 let score = Score::new("T", 120, 4, 4, 0, 2);
4805 assert!(
4806 transpose_staff_region_checked(
4807 &score,
4808 0,
4809 0,
4810 1,
4811 2,
4812 -2,
4813 RegionalTranspositionTarget::Concert,
4814 )
4815 .is_err()
4816 );
4817 }
4818
4819 #[test]
4820 fn transpose_c_major_up_2_to_d_major() {
4821 let score = Score::new("T", 120, 4, 4, 0, 1);
4822 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
4823 }
4824
4825 #[test]
4826 fn transpose_d_major_up_5_to_g_major() {
4827 let score = Score::new("T", 120, 4, 4, 2, 1);
4828 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
4829 }
4830
4831 #[test]
4832 fn transpose_c4_up_1_to_csharp4() {
4833 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
4834 assert_eq!(p.to_midi(), 61);
4835 assert_eq!(p.step, Step::C);
4836 assert_eq!(p.alter, 1);
4837 }
4838
4839 #[test]
4840 fn transpose_c4_down_1_to_b3() {
4841 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
4842 assert_eq!(p.to_midi(), 59);
4843 assert_eq!(p.step, Step::B);
4844 assert_eq!(p.alter, 0);
4845 }
4846
4847 #[test]
4848 fn transpose_up_octave_keeps_step() {
4849 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
4850 assert_eq!(p.to_midi(), 81);
4851 assert_eq!(p.step, Step::A);
4852 assert_eq!(p.octave, 5);
4853 }
4854
4855 #[test]
4856 fn statistics_with_repeat_doubles_duration() {
4857 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4859 score.parts[0].staves[0].measures[0].barline_left =
4860 crate::model::notation::Barline::RepeatStart;
4861 score.parts[0].staves[0].measures[1].barline_right =
4862 crate::model::notation::Barline::RepeatEnd;
4863 let s = score.statistics();
4864 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
4866 }
4867
4868 #[test]
4869 fn transpose_octave_boundary_b4_to_c5() {
4870 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
4872 assert_eq!(p.to_midi(), 72);
4873 assert_eq!(p.step, Step::C);
4874 assert_eq!(p.octave, 5);
4875 }
4876
4877 #[test]
4878 fn transpose_clamp_at_midi_127() {
4879 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
4881 assert_eq!(p.to_midi(), 127);
4882 }
4883
4884 #[test]
4887 fn merge_combines_parts() {
4888 let mut a = Score::new("A", 120, 4, 4, 0, 2);
4889 let b = Score::new("B", 120, 4, 4, 0, 2);
4890 let mut p2 = Part::new("Violin", "Vln.");
4892 p2.staves.push(Staff::new(Clef::Treble));
4893 for i in 0..2usize {
4894 let mut m = Measure::empty(4, 4);
4895 m.number = i as u32 + 1;
4896 p2.staves[0].measures.push(m);
4897 }
4898 a.parts.push(p2);
4899 let merged = a.merge(&b);
4900 assert_eq!(merged.parts.len(), 3);
4902 }
4903
4904 #[test]
4905 fn merge_pads_shorter_score() {
4906 let a = Score::new("A", 120, 4, 4, 0, 4);
4907 let b = Score::new("B", 120, 4, 4, 0, 2);
4908 let merged = a.merge(&b);
4909 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
4911 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
4912 }
4913
4914 #[test]
4915 fn merge_uses_self_metadata() {
4916 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
4917 a.metadata.composer = "Composer A".to_string();
4918 let b = Score::new("Title B", 120, 4, 4, 0, 2);
4919 let merged = a.merge(&b);
4920 assert_eq!(merged.metadata.title, "Title A");
4921 assert_eq!(merged.metadata.composer, "Composer A");
4922 }
4923
4924 #[test]
4925 fn merge_new_id_differs_from_both() {
4926 let a = Score::new("A", 120, 4, 4, 0, 2);
4927 let b = Score::new("B", 120, 4, 4, 0, 2);
4928 let merged = a.merge(&b);
4929 assert_ne!(merged.id, a.id);
4930 assert_ne!(merged.id, b.id);
4931 }
4932
4933 #[test]
4936 fn staff_default_transpose_is_zero() {
4937 let s = Staff::new(Clef::Treble);
4938 assert_eq!(s.transpose_semitones, 0);
4939 }
4940
4941 #[test]
4942 fn staff_presentation_defaults_and_tracks_percussion_clef() {
4943 let standard = Staff::new(Clef::Treble);
4944 assert_eq!(standard.presentation, StaffPresentation::default());
4945
4946 let percussion = Staff::new(Clef::Percussion);
4947 assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
4948 assert_eq!(percussion.presentation.lines, 5);
4949 assert!(percussion.presentation.visible);
4950 }
4951
4952 #[test]
4955 fn score_default_has_schema_version_1() {
4956 let score = Score::default();
4957 assert_eq!(score.schema_version, 1);
4958 }
4959
4960 #[test]
4961 fn score_new_has_schema_version_1() {
4962 let score = Score::new("T", 120, 4, 4, 0, 4);
4963 assert_eq!(score.schema_version, 1);
4964 }
4965
4966 #[test]
4967 fn score_without_schema_version_deserializes_to_zero() {
4968 let json = r#"{"id":"abc","metadata":{"title":"T","composer":"","lyricist":"","copyright":"","work_number":"","movement_title":""},"settings":{"tempo_bpm":120,"time_signature":{"numerator":4,"denominator":4},"key_signature":{"fifths":0,"mode":"major"}},"parts":[]}"#;
4969 let score: Score = serde_json::from_str(json).unwrap();
4970 assert_eq!(score.schema_version, 0);
4971 }
4972
4973 #[test]
4974 fn legacy_score_json_defaults_typed_spanners() {
4975 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4976 let mut value = serde_json::to_value(score).expect("score serializes");
4977 value
4978 .as_object_mut()
4979 .expect("score is an object")
4980 .remove("spanners");
4981 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4982 assert!(restored.spanners.is_empty());
4983 }
4984
4985 #[test]
4986 fn legacy_staff_json_defaults_presentation() {
4987 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4988 let mut value = serde_json::to_value(score).expect("score serializes");
4989 value["parts"][0]["staves"][0]
4990 .as_object_mut()
4991 .expect("staff is an object")
4992 .remove("presentation");
4993
4994 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4995 assert_eq!(
4996 restored.parts[0].staves[0].presentation,
4997 StaffPresentation::default()
4998 );
4999 }
5000
5001 #[test]
5002 fn legacy_part_json_defaults_instrument_definition() {
5003 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5004 let mut value = serde_json::to_value(score).expect("score serializes");
5005 value["parts"][0]
5006 .as_object_mut()
5007 .expect("part is an object")
5008 .remove("instrument");
5009
5010 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5011 assert!(restored.parts[0].instrument.is_none());
5012 }
5013
5014 #[test]
5015 fn resolve_view_projects_linked_part_without_mutating_source_score() {
5016 let mut score = Score::template(ScoreTemplate::StringQuartet);
5017 let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
5018 view.layout.measures_per_row = Some(2);
5019 score.views.push(view);
5020
5021 let projected = score.resolve_view("violin-2").expect("view resolves");
5022 assert_eq!(projected.parts.len(), 1);
5023 assert_eq!(projected.parts[0].name, "Violin II");
5024 assert_eq!(projected.views.len(), 1);
5025 assert_eq!(projected.views[0].parts, vec![0]);
5026 assert_eq!(score.parts.len(), 4);
5027 assert_eq!(score.views[0].parts, vec![1]);
5028 }
5029
5030 #[test]
5031 fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
5032 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5033 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5034 lines: 6,
5035 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5036 capo: 0,
5037 });
5038 score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
5039 score.views.push(ScoreView::linked_tablature_staff(
5040 "guitar-tab",
5041 "Guitar Tab",
5042 0,
5043 0,
5044 ));
5045
5046 let projected = score.resolve_view("guitar-tab").expect("view resolves");
5047 assert_eq!(
5048 projected.parts[0].staves[0].presentation.kind,
5049 StaffKind::Tablature
5050 );
5051 assert_eq!(
5052 projected.views[0].staff_kind_overrides[0].staff,
5053 ViewStaffRef { part: 0, staff: 0 }
5054 );
5055 assert_eq!(
5056 score.parts[0].staves[0].presentation.kind,
5057 StaffKind::Standard
5058 );
5059 }
5060
5061 #[test]
5062 fn legacy_score_view_json_defaults_staff_kind_overrides() {
5063 let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
5064 score.views.push(ScoreView::linked_part("part", "Part", 0));
5065 let mut value = serde_json::to_value(score).expect("score serializes");
5066 value["views"][0]
5067 .as_object_mut()
5068 .expect("view is an object")
5069 .remove("staff_kind_overrides");
5070
5071 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5072 assert!(restored.views[0].staff_kind_overrides.is_empty());
5073 }
5074
5075 #[test]
5076 fn typed_view_style_overrides_are_ordered_and_json_compatible() {
5077 let mut view = ScoreView::linked_part("part", "Part", 0);
5078 view.layout.typed_style_overrides = vec![
5079 ViewStyleOverride {
5080 property: ViewStyleProperty::TextScale,
5081 value: 0.9,
5082 },
5083 ViewStyleOverride {
5084 property: ViewStyleProperty::TextScale,
5085 value: 1.1,
5086 },
5087 ];
5088 assert_eq!(
5089 view.layout.style_value(ViewStyleProperty::TextScale),
5090 Some(1.1)
5091 );
5092 let restored: ScoreView =
5093 serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
5094 assert_eq!(
5095 restored.layout.typed_style_overrides,
5096 view.layout.typed_style_overrides
5097 );
5098 assert_eq!(view.layout.resolved_style().text_scale, 1.1);
5099 assert_eq!(view.layout.resolved_style().system_gap, 2.0);
5100 }
5101
5102 #[test]
5103 fn score_style_defaults_are_inherited_then_overridden_by_view() {
5104 let mut score = Score::new("Style", 120, 4, 4, 0, 1);
5105 score.style_overrides = vec![
5106 ViewStyleOverride {
5107 property: ViewStyleProperty::StaffSpace,
5108 value: 1.2,
5109 },
5110 ViewStyleOverride {
5111 property: ViewStyleProperty::TextScale,
5112 value: 0.9,
5113 },
5114 ];
5115 let mut view = ScoreView::linked_part("part", "Part", 0);
5116 view.layout.typed_style_overrides.push(ViewStyleOverride {
5117 property: ViewStyleProperty::TextScale,
5118 value: 1.1,
5119 });
5120 let style = score.resolved_view_style(&view.layout);
5121 assert_eq!(style.staff_space, 1.2);
5122 assert_eq!(style.text_scale, 1.1);
5123 let restored: Score =
5124 serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
5125 assert_eq!(restored.style_overrides, score.style_overrides);
5126 }
5127
5128 #[test]
5129 fn legacy_measure_json_defaults_source_voice_numbers() {
5130 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5131 let mut value = serde_json::to_value(&score).expect("score serializes");
5132 value["parts"][0]["staves"][0]["measures"][0]
5133 .as_object_mut()
5134 .expect("measure is an object")
5135 .remove("source_voice_numbers");
5136
5137 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5138 assert_eq!(
5139 restored.parts[0].staves[0].measures[0].source_voice_numbers,
5140 [None; 4]
5141 );
5142 }
5143
5144 #[test]
5145 fn legacy_measure_json_defaults_tempo_ramp() {
5146 let measure: Measure = serde_json::from_str(
5147 r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
5148 )
5149 .expect("legacy measure deserializes");
5150 assert_eq!(measure.tempo_ramp_to, None);
5151 }
5152
5153 #[test]
5154 fn note_without_new_percussion_fields_uses_serde_defaults() {
5155 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5156 let mut value = serde_json::to_value(note).unwrap();
5157 let object = value.as_object_mut().unwrap();
5158 object.remove("is_unpitched");
5159 object.remove("instrument_id");
5160 let restored: Note = serde_json::from_value(value).unwrap();
5161 assert!(!restored.is_unpitched);
5162 assert_eq!(restored.instrument_id, None);
5163 }
5164
5165 #[test]
5166 fn percussion_instrument_resolution_prefers_id_then_display_key() {
5167 let mut part = Part::new("Drums", "Dr.");
5168 part.percussion_instruments = vec![
5169 PercussionInstrument {
5170 id: "snare".to_string(),
5171 name: Some("Acoustic Snare".to_string()),
5172 midi_unpitched: Some(38),
5173 staff_position: None,
5174 notehead: None,
5175 preferred_voice: None,
5176 techniques: Vec::new(),
5177 },
5178 PercussionInstrument {
5179 id: "rim".to_string(),
5180 name: Some("Side Stick".to_string()),
5181 midi_unpitched: Some(37),
5182 staff_position: None,
5183 notehead: None,
5184 preferred_voice: None,
5185 techniques: Vec::new(),
5186 },
5187 ];
5188 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
5189 note.is_unpitched = true;
5190 assert_eq!(
5191 part.percussion_instrument_for_note(¬e)
5192 .map(|instrument| instrument.id.as_str()),
5193 Some("snare")
5194 );
5195 note.instrument_id = Some("rim".to_string());
5196 assert_eq!(
5197 part.percussion_instrument_for_note(¬e)
5198 .map(|instrument| instrument.id.as_str()),
5199 Some("rim")
5200 );
5201 note.instrument_id = Some("missing".to_string());
5202 assert!(part.percussion_instrument_for_note(¬e).is_none());
5203 note.instrument_id = None;
5204 note.is_unpitched = false;
5205 assert!(part.percussion_instrument_for_note(¬e).is_none());
5206 }
5207
5208 #[test]
5209 fn percussion_kit_extensions_are_json_backward_compatible() {
5210 let mut instrument = PercussionInstrument {
5211 id: "snare".to_string(),
5212 name: Some("Acoustic Snare".to_string()),
5213 midi_unpitched: Some(38),
5214 staff_position: Some(0),
5215 notehead: Some(NoteHead::Cross),
5216 preferred_voice: Some(1),
5217 techniques: vec!["rim-shot".to_string()],
5218 };
5219 let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
5220 let object = value
5221 .as_object_mut()
5222 .expect("percussion instrument is an object");
5223 object.remove("staff_position");
5224 object.remove("notehead");
5225 object.remove("preferred_voice");
5226 object.remove("techniques");
5227 instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
5228 assert_eq!(instrument.staff_position, None);
5229 assert_eq!(instrument.notehead, None);
5230 assert_eq!(instrument.preferred_voice, None);
5231 assert!(instrument.techniques.is_empty());
5232 }
5233
5234 #[test]
5237 fn score_template_solo_has_one_part_treble() {
5238 let score = Score::template(ScoreTemplate::Solo);
5239 assert_eq!(score.parts.len(), 1);
5240 assert_eq!(score.parts[0].staves.len(), 1);
5241 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5242 assert_eq!(score.parts[0].midi_program, 0);
5243 }
5244
5245 #[test]
5246 fn score_template_piano_has_two_staves() {
5247 let score = Score::template(ScoreTemplate::Piano);
5248 assert_eq!(score.parts.len(), 1);
5249 assert_eq!(score.parts[0].staves.len(), 2);
5250 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5251 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
5252 }
5253
5254 #[test]
5255 fn score_template_string_quartet_has_four_parts() {
5256 let score = Score::template(ScoreTemplate::StringQuartet);
5257 assert_eq!(score.parts.len(), 4);
5258 assert_eq!(score.parts[2].staves[0].clef, Clef::Alto); assert_eq!(score.parts[3].staves[0].clef, Clef::Bass); assert_eq!(score.parts[0].midi_program, 40);
5261 assert_eq!(score.parts[3].midi_program, 42);
5262 }
5263
5264 #[test]
5265 fn score_template_string_orchestra_has_five_parts() {
5266 let score = Score::template(ScoreTemplate::StringOrchestra);
5267 assert_eq!(score.parts.len(), 5);
5268 assert_eq!(score.parts[4].midi_program, 43); }
5270
5271 #[test]
5272 fn score_template_brass_quintet_has_five_parts() {
5273 let score = Score::template(ScoreTemplate::BrassQuintet);
5274 assert_eq!(score.parts.len(), 5);
5275 assert_eq!(score.parts[2].midi_program, 60); }
5277
5278 #[test]
5279 fn score_template_default_measures_are_four() {
5280 let score = Score::template(ScoreTemplate::StringQuartet);
5281 for part in &score.parts {
5282 for staff in &part.staves {
5283 assert_eq!(staff.measures.len(), 4);
5284 }
5285 }
5286 }
5287
5288 #[test]
5291 fn measure_empty_has_no_breaks() {
5292 let m = Measure::empty(4, 4);
5293 assert!(!m.system_break);
5294 assert!(!m.page_break);
5295 assert!(!m.section_break);
5296 }
5297
5298 #[test]
5299 fn section_range_uses_semantic_boundaries_not_layout_breaks() {
5300 let mut score = Score::new("sections", 120, 4, 4, 0, 6);
5301 score.parts[0].staves[0].measures[0].section_break = true;
5302 score.parts[0].staves[0].measures[2].section_break = true;
5303 score.parts[0].staves[0].measures[4].section_break = true;
5304 score.parts[0].staves[0].measures[5].section_break = true;
5305 score.parts[0].staves[0].measures[1].system_break = true;
5306 score.parts[0].staves[0].measures[3].page_break = true;
5307 assert_eq!(score.section_range(0).unwrap(), 0..=1);
5308 assert_eq!(score.section_range(3).unwrap(), 2..=3);
5309 assert_eq!(score.section_range(4).unwrap(), 4..=4);
5310 assert_eq!(score.section_range(5).unwrap(), 5..=5);
5311 assert!(score.section_range(6).is_err());
5312 }
5313
5314 #[test]
5315 fn system_break_survives_json_roundtrip() {
5316 let mut m = Measure::empty(4, 4);
5317 m.system_break = true;
5318 let json = serde_json::to_string(&m).unwrap();
5319 let m2: Measure = serde_json::from_str(&json).unwrap();
5320 assert!(m2.system_break);
5321 assert!(!m2.page_break);
5322 }
5323
5324 #[test]
5327 fn diff_identical_scores_is_empty() {
5328 let s = Score::new("T", 120, 4, 4, 0, 2);
5329 assert!(diff(&s, &s).is_empty());
5330 }
5331
5332 #[test]
5333 fn score_patch_covers_measure_semantics_and_note_insert_index() {
5334 let mut a = Score::new("T", 120, 4, 4, 0, 1);
5335 a.parts[0].staves[0].measures[0].voices[0].clear();
5336 let mut b = a.clone();
5337 let measure = &mut b.parts[0].staves[0].measures[0];
5338 measure.key_sig = Some(KeySignature {
5339 fifths: -2,
5340 mode: "major".to_string(),
5341 });
5342 measure.time_sig = Some(TimeSignature {
5343 numerator: 3,
5344 denominator: 4,
5345 });
5346 measure.barline_left = Barline::RepeatStart;
5347 measure.barline_right = Barline::RepeatEnd;
5348 measure.rehearsal = Some("A".to_string());
5349 measure.volta = Some(VoltaBracket {
5350 number: 1,
5351 kind: "begin_end".to_string(),
5352 });
5353 measure.texts.push(StyledText {
5354 style: TextStyle::RehearsalMark,
5355 text: "A".to_string(),
5356 placement: None,
5357 offset_x: None,
5358 offset_y: None,
5359 relative_x: None,
5360 relative_y: None,
5361 });
5362 measure.figured_bass.push(FiguredBassFigure {
5363 number: "6".to_string(),
5364 alter: None,
5365 prefix: None,
5366 suffix: None,
5367 extender: false,
5368 });
5369 measure.harp_pedal_diagrams.push(HarpPedalDiagram {
5370 positions: [
5371 HarpPedalPosition::Flat,
5372 HarpPedalPosition::Natural,
5373 HarpPedalPosition::Sharp,
5374 HarpPedalPosition::Natural,
5375 HarpPedalPosition::Flat,
5376 HarpPedalPosition::Sharp,
5377 HarpPedalPosition::Natural,
5378 ],
5379 placement: Some("above".to_string()),
5380 });
5381 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
5382 let expected = b.parts[0].staves[0].measures[0].clone();
5383
5384 let patches = score_patch(&a, &b);
5385 assert!(
5386 patches
5387 .iter()
5388 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
5389 );
5390 assert!(
5391 patches
5392 .iter()
5393 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
5394 );
5395 assert!(
5396 patches
5397 .iter()
5398 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
5399 );
5400 assert!(
5401 patches
5402 .iter()
5403 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
5404 );
5405 assert!(
5406 patches
5407 .iter()
5408 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
5409 );
5410 assert!(
5411 patches
5412 .iter()
5413 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
5414 );
5415 assert!(
5416 patches
5417 .iter()
5418 .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
5419 );
5420 let result = apply_patch(&a, &patches).expect("patch application failed");
5421 let result_measure = &result.parts[0].staves[0].measures[0];
5422 assert_eq!(result_measure.key_sig, expected.key_sig);
5423 assert_eq!(result_measure.time_sig, expected.time_sig);
5424 assert_eq!(result_measure.barline_left, expected.barline_left);
5425 assert_eq!(result_measure.barline_right, expected.barline_right);
5426 assert_eq!(result_measure.rehearsal, expected.rehearsal);
5427 assert_eq!(result_measure.volta, expected.volta);
5428 assert_eq!(result_measure.texts, expected.texts);
5429 assert_eq!(result_measure.figured_bass, expected.figured_bass);
5430 assert_eq!(
5431 result_measure.harp_pedal_diagrams,
5432 expected.harp_pedal_diagrams
5433 );
5434 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
5435 }
5436
5437 #[test]
5438 fn diff_and_patch_preserve_score_level_texts() {
5439 let a = Score::new("T", 120, 4, 4, 0, 1);
5440 let mut b = a.clone();
5441 b.texts.push(StyledText {
5442 style: TextStyle::Expression,
5443 text: "Prelude".to_string(),
5444 placement: Some("above".to_string()),
5445 offset_x: Some(12.0),
5446 offset_y: Some(-8.0),
5447 relative_x: None,
5448 relative_y: None,
5449 });
5450
5451 let changes = diff(&a, &b);
5452 assert!(changes.iter().any(|change| matches!(
5453 change,
5454 ScoreChange::ScoreTextChanged { old, new }
5455 if old.is_empty() && new == &b.texts
5456 )));
5457
5458 let patches = score_patch(&a, &b);
5459 assert!(patches.iter().any(|patch| matches!(
5460 patch,
5461 ScorePatch::SetScoreTexts { value } if value == &b.texts
5462 )));
5463 let result = apply_patch(&a, &patches).expect("score text patch failed");
5464 assert_eq!(result.texts, b.texts);
5465 }
5466
5467 #[test]
5468 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
5469 let a = Score::new("T", 120, 4, 4, 0, 1);
5470 let mut b = a.clone();
5471 let measure = &mut b.parts[0].staves[0].measures[0];
5472 measure.number = 8;
5473 measure.clef = Some(Clef::Bass);
5474 measure.tempo_text = Some("Allegro".to_string());
5475 measure.navigation = Some("D.S.".to_string());
5476 measure.expression_text = Some("espressivo".to_string());
5477 measure.multi_rest_count = Some(3);
5478 measure.system_break = true;
5479 measure.page_break = true;
5480
5481 let changes = diff(&a, &b);
5482 assert!(changes.iter().any(|change| matches!(
5483 change,
5484 ScoreChange::MeasurePresentationChanged {
5485 part: 0,
5486 staff: 0,
5487 measure: 0,
5488 old_number: 1,
5489 new_number: 8,
5490 old_clef: None,
5491 new_clef: Some(Clef::Bass),
5492 new_tempo_text: Some(text),
5493 new_navigation: Some(navigation),
5494 new_expression_text: Some(expression),
5495 new_multi_rest_count: Some(3),
5496 old_system_break: false,
5497 new_system_break: true,
5498 old_page_break: false,
5499 new_page_break: true,
5500 ..
5501 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5502 )));
5503 assert!(!changes.iter().any(|change| matches!(
5504 change,
5505 ScoreChange::UnrepresentedFieldChanged { path }
5506 if path == "parts[0].staves[0].measures[0].number"
5507 )));
5508
5509 let patches = score_patch(&a, &b);
5510 assert!(patches.iter().any(|patch| matches!(
5511 patch,
5512 ScorePatch::SetMeasurePresentation {
5513 part: 0,
5514 staff: 0,
5515 measure: 0,
5516 number: 8,
5517 clef: Some(Clef::Bass),
5518 tempo_text: Some(text),
5519 navigation: Some(navigation),
5520 expression_text: Some(expression),
5521 multi_rest_count: Some(3),
5522 system_break: true,
5523 page_break: true,
5524 section_break: false,
5525 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5526 )));
5527 assert!(
5528 !patches
5529 .iter()
5530 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5531 );
5532 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
5533 let decoded: Vec<ScorePatch> =
5534 serde_json::from_str(&json).expect("measure presentation patch should decode");
5535 let mut legacy_json: serde_json::Value =
5536 serde_json::from_str(&json).expect("patch JSON value");
5537 for patch in legacy_json.as_array_mut().expect("patch JSON array") {
5538 patch
5539 .as_object_mut()
5540 .expect("patch JSON object")
5541 .remove("section_break");
5542 }
5543 let legacy: Vec<ScorePatch> =
5544 serde_json::from_value(legacy_json).expect("legacy patch should decode");
5545 assert!(matches!(
5546 legacy.first(),
5547 Some(ScorePatch::SetMeasurePresentation {
5548 section_break: false,
5549 ..
5550 })
5551 ));
5552 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
5553 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
5554 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
5555 assert_eq!(
5556 serde_json::to_value(&result).expect("patched score JSON"),
5557 serde_json::to_value(&b).expect("expected score JSON")
5558 );
5559 }
5560
5561 #[test]
5562 fn chord_definition_changes_use_typed_diff_and_local_patch() {
5563 let a = Score::new("T", 120, 4, 4, 0, 1);
5564 let mut b = a.clone();
5565 b.chord_definitions.push(ChordDefinition {
5566 id: Some("c-major".to_string()),
5567 label: Some("C".to_string()),
5568 kind: Some("major".to_string()),
5569 fret_position: Some(0),
5570 tab_strings: Some("x32010".to_string()),
5571 tab_courses: None,
5572 members: Vec::new(),
5573 barres: Vec::new(),
5574 });
5575
5576 let changes = diff(&a, &b);
5577 assert!(changes.iter().any(|change| matches!(
5578 change,
5579 ScoreChange::ChordDefinitionsChanged { old, new }
5580 if old.is_empty() && new == &b.chord_definitions
5581 )));
5582 assert!(!changes.iter().any(|change| matches!(
5583 change,
5584 ScoreChange::UnrepresentedFieldChanged { path }
5585 if path == "chord_definitions"
5586 )));
5587
5588 let patches = score_patch(&a, &b);
5589 assert!(patches.iter().any(|patch| matches!(
5590 patch,
5591 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
5592 )));
5593 assert!(
5594 !patches
5595 .iter()
5596 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5597 );
5598 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
5599 let decoded: Vec<ScorePatch> =
5600 serde_json::from_str(&json).expect("chord definition patch should decode");
5601 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
5602 assert_eq!(result.chord_definitions, b.chord_definitions);
5603 }
5604
5605 #[test]
5606 fn part_name_changes_use_typed_diff_and_local_patch() {
5607 let a = Score::new("T", 120, 4, 4, 0, 1);
5608 let mut b = a.clone();
5609 b.parts[0].name = "Violin".to_string();
5610 b.parts[0].short_name = "Vln.".to_string();
5611 b.parts[0].midi_channel = 4;
5612 b.parts[0].midi_program = 40;
5613 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
5614 tick: 120,
5615 channel: 4,
5616 value: 2048,
5617 });
5618 b.parts[0].midi_control_changes.push(MidiControlChange {
5619 tick: 240,
5620 channel: 4,
5621 controller: 64,
5622 value: 127,
5623 });
5624 b.parts[0].midi_program_changes.push(MidiProgramChange {
5625 tick: 0,
5626 channel: 4,
5627 program: 40,
5628 });
5629 b.parts[0].midi_aftertouch.push(MidiAftertouch {
5630 tick: 360,
5631 channel: 4,
5632 key: Some(64),
5633 value: 80,
5634 });
5635 b.parts[0].staves[0].clef = Clef::Bass;
5636 b.parts[0].staves[0].transpose_semitones = -2;
5637
5638 let changes = diff(&a, &b);
5639 assert!(changes.iter().any(|change| matches!(
5640 change,
5641 ScoreChange::PartNamesChanged {
5642 part: 0,
5643 old_name,
5644 new_name,
5645 old_short_name,
5646 new_short_name,
5647 } if old_name == "Piano"
5648 && new_name == "Violin"
5649 && old_short_name == "Pno."
5650 && new_short_name == "Vln."
5651 )));
5652 assert!(changes.iter().any(|change| matches!(
5653 change,
5654 ScoreChange::PartMidiChanged {
5655 part: 0,
5656 old_channel: 0,
5657 new_channel: 4,
5658 old_program: 0,
5659 new_program: 40,
5660 }
5661 )));
5662 assert!(changes.iter().any(|change| matches!(
5663 change,
5664 ScoreChange::PartMidiAutomationChanged {
5665 part: 0,
5666 new_pitch_bends,
5667 new_control_changes,
5668 new_program_changes,
5669 new_aftertouch,
5670 ..
5671 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
5672 && new_control_changes == &b.parts[0].midi_control_changes
5673 && new_program_changes == &b.parts[0].midi_program_changes
5674 && new_aftertouch == &b.parts[0].midi_aftertouch
5675 )));
5676 assert!(changes.iter().any(|change| matches!(
5677 change,
5678 ScoreChange::StaffConfigurationChanged {
5679 part: 0,
5680 staff: 0,
5681 old_clef: Clef::Treble,
5682 new_clef: Clef::Bass,
5683 old_transpose_semitones: 0,
5684 new_transpose_semitones: -2,
5685 }
5686 )));
5687 assert!(!changes.iter().any(|change| matches!(
5688 change,
5689 ScoreChange::UnrepresentedFieldChanged { path }
5690 if path == "parts[0].name"
5691 )));
5692
5693 let patches = score_patch(&a, &b);
5694 assert!(patches.iter().any(|patch| matches!(
5695 patch,
5696 ScorePatch::SetPartNames {
5697 part: 0,
5698 name,
5699 short_name,
5700 } if name == "Violin" && short_name == "Vln."
5701 )));
5702 assert!(
5703 !patches
5704 .iter()
5705 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5706 );
5707 assert!(patches.iter().any(|patch| matches!(
5708 patch,
5709 ScorePatch::SetPartMidi {
5710 part: 0,
5711 channel: 4,
5712 program: 40,
5713 }
5714 )));
5715 assert!(patches.iter().any(|patch| matches!(
5716 patch,
5717 ScorePatch::SetPartMidiAutomation {
5718 part: 0,
5719 pitch_bends,
5720 control_changes,
5721 program_changes,
5722 aftertouch,
5723 } if pitch_bends == &b.parts[0].midi_pitch_bends
5724 && control_changes == &b.parts[0].midi_control_changes
5725 && program_changes == &b.parts[0].midi_program_changes
5726 && aftertouch == &b.parts[0].midi_aftertouch
5727 )));
5728 assert!(patches.iter().any(|patch| matches!(
5729 patch,
5730 ScorePatch::SetStaffConfiguration {
5731 part: 0,
5732 staff: 0,
5733 clef: Clef::Bass,
5734 transpose_semitones: -2,
5735 }
5736 )));
5737 let result = apply_patch(&a, &patches).expect("part name patch failed");
5738 assert_eq!(result.parts[0].name, b.parts[0].name);
5739 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
5740 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
5741 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
5742 assert_eq!(
5743 result.parts[0].midi_pitch_bends,
5744 b.parts[0].midi_pitch_bends
5745 );
5746 assert_eq!(
5747 result.parts[0].midi_control_changes,
5748 b.parts[0].midi_control_changes
5749 );
5750 assert_eq!(
5751 result.parts[0].midi_program_changes,
5752 b.parts[0].midi_program_changes
5753 );
5754 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
5755 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
5756 assert_eq!(
5757 result.parts[0].staves[0].transpose_semitones,
5758 b.parts[0].staves[0].transpose_semitones
5759 );
5760 }
5761
5762 #[test]
5763 fn diff_reports_measure_text_and_figured_bass_changes() {
5764 let a = Score::new("T", 120, 4, 4, 0, 1);
5765 let mut b = a.clone();
5766 let measure = &mut b.parts[0].staves[0].measures[0];
5767 measure.texts.push(StyledText {
5768 style: TextStyle::Lyrics,
5769 text: "la".to_string(),
5770 placement: None,
5771 offset_x: None,
5772 offset_y: None,
5773 relative_x: None,
5774 relative_y: None,
5775 });
5776 measure.figured_bass.push(FiguredBassFigure {
5777 number: "6".to_string(),
5778 alter: None,
5779 prefix: None,
5780 suffix: None,
5781 extender: false,
5782 });
5783
5784 let changes = diff(&a, &b);
5785 assert!(changes.iter().any(|change| matches!(
5786 change,
5787 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
5788 if old.is_empty() && new.len() == 1
5789 )));
5790 assert!(changes.iter().any(|change| matches!(
5791 change,
5792 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
5793 if old.is_empty() && new.len() == 1
5794 )));
5795 }
5796
5797 #[test]
5798 fn diff_reports_tablature_changes_and_patches_them_locally() {
5799 let a = Score::new("T", 120, 4, 4, 0, 1);
5800 let mut b = a.clone();
5801 b.parts[0].staves[0].tablature = Some(TablatureConfig {
5802 lines: 6,
5803 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5804 capo: 2,
5805 });
5806
5807 let changes = diff(&a, &b);
5808 assert!(!changes.iter().any(|change| matches!(
5809 change,
5810 ScoreChange::UnrepresentedFieldChanged { path }
5811 if path == "parts[0].staves[0].tablature"
5812 )));
5813 assert!(changes.iter().any(|change| matches!(
5814 change,
5815 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
5816 if config.lines == 6 && config.capo == 2
5817 )));
5818 let patches = score_patch(&a, &b);
5819 assert!(patches.iter().any(|patch| matches!(
5820 patch,
5821 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
5822 if config.lines == 6 && config.capo == 2
5823 )));
5824 assert_eq!(
5825 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
5826 b.parts[0].staves[0].tablature
5827 );
5828 }
5829
5830 #[test]
5831 fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
5832 let mut a = Score::new("T", 120, 4, 4, 0, 2);
5833 a.parts[0].staves[0].tablature = Some(TablatureConfig {
5834 lines: 6,
5835 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5836 capo: 0,
5837 });
5838 let mut b = a.clone();
5839 b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
5840 lines: 6,
5841 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5842 capo: 3,
5843 });
5844
5845 let changes = diff(&a, &b);
5846 assert!(changes.iter().any(|change| matches!(
5847 change,
5848 ScoreChange::TablatureChangeChanged {
5849 part: 0,
5850 staff: 0,
5851 measure: 1,
5852 old: None,
5853 new: Some(config),
5854 } if config.capo == 3
5855 )));
5856 let patches = score_patch(&a, &b);
5857 assert!(patches.iter().any(|patch| matches!(
5858 patch,
5859 ScorePatch::SetMeasureTablatureChange {
5860 part: 0,
5861 staff: 0,
5862 measure: 1,
5863 value: Some(config),
5864 } if config.capo == 3
5865 )));
5866 assert_eq!(
5867 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
5868 b.parts[0].staves[0].measures[1].tablature_change
5869 );
5870 }
5871
5872 #[test]
5873 fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
5874 let a = Score::new("Ramp", 120, 4, 4, 0, 2);
5875 let mut b = a.clone();
5876 b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
5877
5878 assert!(diff(&a, &b).iter().any(|change| matches!(
5879 change,
5880 ScoreChange::MeasureTempoRampChanged {
5881 part: 0,
5882 staff: 0,
5883 measure: 1,
5884 old: None,
5885 new: Some(72),
5886 }
5887 )));
5888 let patches = score_patch(&a, &b);
5889 assert!(patches.iter().any(|patch| matches!(
5890 patch,
5891 ScorePatch::SetMeasureTempoRamp {
5892 part: 0,
5893 staff: 0,
5894 measure: 1,
5895 value: Some(72),
5896 }
5897 )));
5898 assert_eq!(
5899 apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
5900 .tempo_ramp_to,
5901 b.parts[0].staves[0].measures[1].tempo_ramp_to
5902 );
5903 }
5904
5905 #[test]
5906 fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
5907 let a = Score::new("T", 120, 4, 4, 0, 1);
5908 let mut b = a.clone();
5909 b.parts[0].staves[0].presentation = StaffPresentation {
5910 kind: StaffKind::Percussion,
5911 lines: 1,
5912 line_distance: 1.5,
5913 small: true,
5914 cutaway: true,
5915 visible: false,
5916 notehead_scheme: StaffNoteheadScheme::PitchNames,
5917 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
5918 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
5919 };
5920
5921 let changes = diff(&a, &b);
5922 assert!(changes.iter().any(|change| matches!(
5923 change,
5924 ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
5925 if new == &b.parts[0].staves[0].presentation
5926 )));
5927 let patches = score_patch(&a, &b);
5928 assert!(patches.iter().any(|patch| matches!(
5929 patch,
5930 ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
5931 if value == &b.parts[0].staves[0].presentation
5932 )));
5933 assert!(
5934 !patches
5935 .iter()
5936 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5937 );
5938 assert_eq!(
5939 apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
5940 b.parts[0].staves[0].presentation
5941 );
5942 }
5943
5944 #[test]
5945 fn score_patch_uses_local_presentation_patch_for_display_fields() {
5946 let a = Score::new("T", 120, 4, 4, 0, 1);
5947 let mut b = a.clone();
5948 b.parts[0].name = "Piano".to_string();
5949 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
5950 let patches = score_patch(&a, &b);
5951 assert!(patches.iter().any(|patch| matches!(
5952 patch,
5953 ScorePatch::SetMeasurePresentation {
5954 part: 0,
5955 staff: 0,
5956 measure: 0,
5957 expression_text: Some(text),
5958 ..
5959 } if text == "dolce"
5960 )));
5961 assert!(
5962 !patches
5963 .iter()
5964 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5965 );
5966 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
5967 assert_eq!(result.parts[0].name, "Piano");
5968 assert_eq!(
5969 result.parts[0].staves[0].measures[0].expression_text,
5970 Some("dolce".to_string())
5971 );
5972 }
5973
5974 #[test]
5975 fn diff_detects_tempo_change() {
5976 let a = Score::new("T", 120, 4, 4, 0, 1);
5977 let mut b = a.clone();
5978 b.settings.tempo_bpm = 90;
5979 let changes = diff(&a, &b);
5980 assert_eq!(changes.len(), 1);
5981 assert!(matches!(
5982 changes[0],
5983 ScoreChange::TempoChanged { old: 120, new: 90 }
5984 ));
5985 }
5986
5987 #[test]
5988 fn diff_detects_title_change() {
5989 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
5990 let mut b = a.clone();
5991 b.metadata.title = "New Title".to_string();
5992 let changes = diff(&a, &b);
5993 assert!(
5994 changes.iter().any(
5995 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
5996 )
5997 );
5998 }
5999
6000 #[test]
6001 fn diff_detects_note_modification() {
6002 let mut a = Score::new("T", 120, 4, 4, 0, 1);
6003 a.parts[0].staves[0].measures[0].voices[0] =
6004 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6005 let mut b = a.clone();
6006 b.parts[0].staves[0].measures[0].voices[0][0] =
6007 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
6008 let changes = diff(&a, &b);
6009 assert!(
6010 changes
6011 .iter()
6012 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
6013 );
6014 }
6015
6016 #[test]
6017 fn diff_detects_part_added() {
6018 let a = Score::new("T", 120, 4, 4, 0, 1);
6019 let mut b = a.clone();
6020 let mut p = Part::new("Violin", "Vln.");
6021 p.staves.push(Staff::new(Clef::Treble));
6022 b.parts.push(p);
6023 let changes = diff(&a, &b);
6024 assert!(
6025 changes
6026 .iter()
6027 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
6028 );
6029 }
6030
6031 #[test]
6032 fn diff_detects_measure_tempo_change() {
6033 let a = Score::new("T", 120, 4, 4, 0, 2);
6034 let mut b = a.clone();
6035 b.parts[0].staves[0].measures[1].tempo = Some(60);
6036 let changes = diff(&a, &b);
6037 assert!(changes.iter().any(|c| matches!(
6038 c,
6039 ScoreChange::MeasureTempoChanged {
6040 measure: 1,
6041 old: None,
6042 new: Some(60),
6043 ..
6044 }
6045 )));
6046 }
6047
6048 #[test]
6049 fn diff_detects_barline_change() {
6050 use crate::model::notation::Barline;
6051 let a = Score::new("T", 120, 4, 4, 0, 2);
6052 let mut b = a.clone();
6053 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
6054 let changes = diff(&a, &b);
6055 assert!(
6056 changes
6057 .iter()
6058 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
6059 );
6060 }
6061
6062 #[test]
6063 fn diff_detects_rehearsal_change() {
6064 let a = Score::new("T", 120, 4, 4, 0, 2);
6065 let mut b = a.clone();
6066 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
6067 let changes = diff(&a, &b);
6068 assert!(
6069 changes
6070 .iter()
6071 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
6072 );
6073 }
6074
6075 #[test]
6076 fn diff_detects_volta_change() {
6077 use super::VoltaBracket;
6078 let a = Score::new("T", 120, 4, 4, 0, 2);
6079 let mut b = a.clone();
6080 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
6081 number: 1,
6082 kind: "begin_end".into(),
6083 });
6084 let changes = diff(&a, &b);
6085 assert!(
6086 changes
6087 .iter()
6088 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
6089 );
6090 }
6091
6092 #[test]
6093 fn diff_detects_key_signature_change() {
6094 let a = Score::new("T", 120, 4, 4, 0, 1);
6095 let mut b = a.clone();
6096 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
6098 assert!(
6099 changes
6100 .iter()
6101 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
6102 );
6103 }
6104
6105 #[test]
6106 fn measure_key_signature_uses_typed_local_patch() {
6107 let a = Score::new("T", 120, 4, 4, 0, 1);
6108 let mut b = a.clone();
6109 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
6110 fifths: 2,
6111 mode: "major".to_string(),
6112 });
6113 let changes = diff(&a, &b);
6114 assert!(!changes.iter().any(|change| matches!(
6115 change,
6116 ScoreChange::UnrepresentedFieldChanged { path }
6117 if path == "parts[0].staves[0].measures[0].key_sig"
6118 )));
6119 let patches = score_patch(&a, &b);
6120 assert!(patches.iter().any(|patch| matches!(
6121 patch,
6122 ScorePatch::SetKeySignature {
6123 part: 0,
6124 staff: 0,
6125 measure: 0,
6126 value: Some(KeySignature { fifths: 2, .. }),
6127 }
6128 )));
6129 assert!(
6130 !patches
6131 .iter()
6132 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6133 );
6134 }
6135
6136 #[test]
6137 fn diff_same_key_signature_no_change() {
6138 let a = Score::new("T", 120, 4, 4, 2, 1);
6139 let changes = diff(&a, &a);
6140 assert!(changes.is_empty());
6141 }
6142
6143 #[test]
6144 fn score_duration_secs_region_partial() {
6145 use super::score_duration_secs_region;
6146 let score = Score::new("T", 120, 4, 4, 0, 4);
6148 let secs = score_duration_secs_region(&score, (1, 2));
6149 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
6150 }
6151
6152 #[test]
6153 fn score_duration_secs_region_single_measure() {
6154 use super::score_duration_secs_region;
6155 let score = Score::new("T", 120, 4, 4, 0, 4);
6157 let secs = score_duration_secs_region(&score, (0, 0));
6158 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
6159 }
6160
6161 #[test]
6164 fn measure_beats_remaining_empty_voice_returns_full() {
6165 use super::measure_beats_remaining;
6166 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6167 score.parts[0].staves[0].measures[0].voices[0].clear();
6168 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6169 assert!(
6170 (rem - 4.0).abs() < 1e-9,
6171 "expected 4.0 remaining, got {rem}"
6172 );
6173 }
6174
6175 #[test]
6176 fn measure_beats_remaining_half_full_returns_half() {
6177 use super::measure_beats_remaining;
6178 use crate::model::pitch::Step;
6179 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6180 score.parts[0].staves[0].measures[0].voices[0] = vec![
6181 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6182 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
6183 ];
6184 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6185 assert!(
6186 (rem - 2.0).abs() < 1e-9,
6187 "expected 2.0 remaining, got {rem}"
6188 );
6189 }
6190
6191 #[test]
6192 fn measure_beats_remaining_full_voice_returns_zero() {
6193 use super::measure_beats_remaining;
6194 use crate::model::pitch::Step;
6195 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6196 score.parts[0].staves[0].measures[0].voices[0] =
6197 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
6198 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6199 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
6200 }
6201
6202 #[test]
6203 fn measure_beats_remaining_tuplet_accounting() {
6204 use super::measure_beats_remaining;
6205 use crate::model::notation::TupletInfo;
6206 use crate::model::pitch::Step;
6207 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6209 let tuplet = TupletInfo {
6210 actual_notes: 3,
6211 normal_notes: 2,
6212 };
6213 let mk = |step| {
6214 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
6215 n.tuplet = Some(tuplet.clone());
6216 n
6217 };
6218 score.parts[0].staves[0].measures[0].voices[0] =
6219 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
6220 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6221 assert!(
6222 (rem - 2.0).abs() < 1e-9,
6223 "expected 2.0 remaining (triplets used 2.0), got {rem}"
6224 );
6225 }
6226
6227 #[test]
6228 fn measure_beats_remaining_out_of_range_returns_err() {
6229 use super::measure_beats_remaining;
6230 let score = Score::new("T", 120, 4, 4, 0, 1);
6231 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
6232 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
6233 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
6234 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
6235 }
6236
6237 #[test]
6238 fn note_content_eq_ignores_id() {
6239 use crate::model::pitch::Step;
6240 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
6241 let mut b = a.clone();
6242 b.id = "different-id".to_string();
6243 assert!(note_content_eq(&a, &b));
6244 b.pitches[0] = Pitch::new(Step::D, 4);
6246 assert!(!note_content_eq(&a, &b));
6247 let mut c = a.clone();
6249 a.stem_up = Some(true);
6250 c.stem_up = Some(false);
6251 assert!(!note_content_eq(&a, &c));
6252 }
6253
6254 #[test]
6255 fn suggested_stem_up_below_middle() {
6256 use crate::model::notation::Clef;
6257 let pitches = vec![Pitch::new(Step::C, 4)];
6259 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6260 }
6261
6262 #[test]
6263 fn suggested_stem_up_above_middle() {
6264 use crate::model::notation::Clef;
6265 let pitches = vec![Pitch::new(Step::G, 5)];
6267 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6268 }
6269
6270 #[test]
6271 fn suggested_stem_up_at_middle_line() {
6272 use crate::model::notation::Clef;
6273 let pitches = vec![Pitch::new(Step::B, 4)];
6275 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6276 }
6277
6278 #[test]
6279 fn suggested_stem_up_chord() {
6280 use crate::model::notation::Clef;
6281 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
6283 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6284 }
6285
6286 #[test]
6287 fn suggested_stem_up_bass_clef() {
6288 use crate::model::notation::Clef;
6289 let pitches = vec![Pitch::new(Step::D, 3)];
6291 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
6292 let pitches2 = vec![Pitch::new(Step::C, 3)];
6294 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
6295 }
6296
6297 #[test]
6298 fn suggested_stem_up_empty_pitches() {
6299 use crate::model::notation::Clef;
6300 assert!(suggested_stem_up(&[], &Clef::Treble));
6301 }
6302
6303 fn eighth(pitch: Pitch) -> Note {
6304 Note::new(pitch, Duration::Eighth)
6305 }
6306 fn quarter(pitch: Pitch) -> Note {
6307 Note::new(pitch, Duration::Quarter)
6308 }
6309 fn rest_eighth() -> Note {
6310 Note::rest(Duration::Eighth)
6311 }
6312
6313 #[test]
6314 fn compute_beams_4_4_four_eighths() {
6315 use crate::model::notation::{Clef, TimeSignature};
6316 let _ = Clef::Treble; let ts = TimeSignature {
6318 numerator: 4,
6319 denominator: 4,
6320 };
6321 let c4 = Pitch::new(Step::C, 4);
6322 let notes = vec![
6323 eighth(c4.clone()),
6324 eighth(c4.clone()),
6325 eighth(c4.clone()),
6326 eighth(c4.clone()),
6327 ];
6328 let beams = compute_beams(¬es, &ts);
6329 assert_eq!(beams[0], BeamState::Begin);
6331 assert_eq!(beams[1], BeamState::End);
6332 assert_eq!(beams[2], BeamState::Begin);
6333 assert_eq!(beams[3], BeamState::End);
6334 }
6335
6336 #[test]
6337 fn compute_beams_4_4_all_eighth_one_group() {
6338 use crate::model::notation::TimeSignature;
6339 let ts = TimeSignature {
6340 numerator: 4,
6341 denominator: 4,
6342 };
6343 let c4 = Pitch::new(Step::C, 4);
6344 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
6346 let beams = compute_beams(¬es, &ts);
6347 assert_eq!(beams[0], BeamState::Begin);
6348 assert_eq!(beams[1], BeamState::End);
6349 }
6350
6351 #[test]
6352 fn compute_beams_quarter_not_beamed() {
6353 use crate::model::notation::TimeSignature;
6354 let ts = TimeSignature {
6355 numerator: 4,
6356 denominator: 4,
6357 };
6358 let c4 = Pitch::new(Step::C, 4);
6359 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
6360 let beams = compute_beams(¬es, &ts);
6361 assert_eq!(beams[0], BeamState::None);
6362 assert_eq!(beams[1], BeamState::None);
6363 }
6364
6365 #[test]
6366 fn compute_beams_rest_breaks_beam() {
6367 use crate::model::notation::TimeSignature;
6368 let ts = TimeSignature {
6369 numerator: 4,
6370 denominator: 4,
6371 };
6372 let c4 = Pitch::new(Step::C, 4);
6373 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
6374 let beams = compute_beams(¬es, &ts);
6375 assert_eq!(beams[0], BeamState::None);
6377 assert_eq!(beams[1], BeamState::None);
6378 assert_eq!(beams[2], BeamState::None);
6379 }
6380
6381 #[test]
6382 fn compute_beams_6_8_compound() {
6383 use crate::model::notation::TimeSignature;
6384 let ts = TimeSignature {
6385 numerator: 6,
6386 denominator: 8,
6387 };
6388 let c4 = Pitch::new(Step::C, 4);
6389 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
6391 let beams = compute_beams(¬es, &ts);
6392 assert_eq!(beams[0], BeamState::Begin);
6393 assert_eq!(beams[1], BeamState::Continue);
6394 assert_eq!(beams[2], BeamState::End);
6395 assert_eq!(beams[3], BeamState::Begin);
6396 assert_eq!(beams[4], BeamState::Continue);
6397 assert_eq!(beams[5], BeamState::End);
6398 }
6399
6400 #[test]
6401 fn compute_beams_single_eighth() {
6402 use crate::model::notation::TimeSignature;
6403 let ts = TimeSignature {
6404 numerator: 4,
6405 denominator: 4,
6406 };
6407 let c4 = Pitch::new(Step::C, 4);
6408 let notes = vec![eighth(c4.clone())];
6409 let beams = compute_beams(¬es, &ts);
6410 assert_eq!(beams[0], BeamState::None);
6411 }
6412}
6413
6414#[cfg(test)]
6415mod mid_clef_tests {
6416 use super::*;
6417 use crate::{Command, JoinMeasuresCmd, ScoreEngine, SetMidMeasureClefsCmd, SplitMeasureCmd};
6418
6419 fn quarter_notes() -> Vec<Note> {
6420 [Step::C, Step::D, Step::E, Step::F]
6421 .into_iter()
6422 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter))
6423 .collect()
6424 }
6425
6426 fn change(beats: f64, clef: Clef) -> MidMeasureClef {
6427 MidMeasureClef {
6428 offset: MeasureLength::from_beats(beats).unwrap(),
6429 clef,
6430 }
6431 }
6432
6433 #[test]
6434 fn measure_length_from_beats_is_exact_for_tuplet_subdivisions() {
6435 let third = MeasureLength::from_beats(1.0 / 3.0).unwrap();
6436 assert_eq!((third.numerator, third.denominator), (1, 12));
6437 let two = MeasureLength::from_beats(2.0).unwrap();
6438 assert_eq!((two.numerator, two.denominator), (1, 2));
6439 assert!(MeasureLength::from_beats(0.0).is_none());
6440 }
6441
6442 #[test]
6443 fn clef_at_follows_mid_bar_changes_and_json_omits_an_empty_list() {
6444 let mut measure = Measure::empty(4, 4);
6445 let json = serde_json::to_string(&measure).unwrap();
6446 assert!(!json.contains("mid_clefs"));
6447 measure.mid_clefs = vec![change(2.0, Clef::Bass)];
6448 assert_eq!(measure.clef_at(&Clef::Treble, 1.0), Clef::Treble);
6449 assert_eq!(measure.clef_at(&Clef::Treble, 2.0), Clef::Bass);
6450 let restored: Measure =
6451 serde_json::from_str(&serde_json::to_string(&measure).unwrap()).unwrap();
6452 assert_eq!(restored.mid_clefs, measure.mid_clefs);
6453 }
6454
6455 #[test]
6456 fn validation_rejects_mid_bar_clefs_outside_the_bar_or_out_of_order() {
6457 let mut score = Score::new("clefs", 120, 4, 4, 0, 1);
6458 score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6459 score.parts[0].staves[0].measures[0].mid_clefs =
6460 vec![change(2.0, Clef::Bass), change(1.0, Clef::Treble)];
6461 let errors = crate::validate(&score).errors;
6462 assert!(errors.iter().any(|error| matches!(
6463 error,
6464 crate::ValidationError::InvalidMidMeasureClef { index: 1, .. }
6465 )));
6466 score.parts[0].staves[0].measures[0].mid_clefs = vec![change(4.0, Clef::Bass)];
6467 assert!(!crate::validate(&score).errors.is_empty());
6468 }
6469
6470 #[test]
6471 fn command_split_join_diff_and_patch_keep_mid_bar_clefs() {
6472 let mut engine = ScoreEngine::new();
6473 engine.score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6474 engine
6475 .apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6476 part_index: 0,
6477 staff_index: 0,
6478 measure_index: 0,
6479 clefs: vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)],
6480 }))
6481 .unwrap();
6482 let with_clefs = engine.score.clone();
6483 let plain = {
6484 let mut score = with_clefs.clone();
6485 score.parts[0].staves[0].measures[0].mid_clefs.clear();
6486 score
6487 };
6488 assert!(diff(&plain, &with_clefs).iter().any(|change| matches!(
6489 change,
6490 ScoreChange::MidMeasureClefsChanged { measure: 0, .. }
6491 )));
6492 let patched = apply_patch(&plain, &score_patch(&plain, &with_clefs)).unwrap();
6493 assert_eq!(
6494 patched.parts[0].staves[0].measures[0].mid_clefs,
6495 with_clefs.parts[0].staves[0].measures[0].mid_clefs
6496 );
6497
6498 engine
6499 .apply(Command::SplitMeasure(SplitMeasureCmd {
6500 measure_index: 0,
6501 split_at_beats: 1.0,
6502 }))
6503 .unwrap();
6504 let measures = &engine.score.parts[0].staves[0].measures;
6505 assert!(measures[0].mid_clefs.is_empty());
6506 assert_eq!(measures[1].clef, Some(Clef::Bass));
6507 assert_eq!(measures[1].mid_clefs, vec![change(2.0, Clef::Alto)]);
6508
6509 engine
6510 .apply(Command::JoinMeasures(JoinMeasuresCmd { measure_index: 0 }))
6511 .unwrap();
6512 let measure = &engine.score.parts[0].staves[0].measures[0];
6513 assert_eq!(
6514 measure.mid_clefs,
6515 vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)]
6516 );
6517
6518 let invalid = engine.apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6519 part_index: 0,
6520 staff_index: 0,
6521 measure_index: 0,
6522 clefs: vec![change(0.0 + 5.0, Clef::Bass)],
6523 }));
6524 assert!(invalid.is_err());
6525 }
6526}