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_ticks(ticks: u32, divisions: u32) -> Option<Self> {
1830 let numerator = u64::from(ticks);
1831 let denominator = u64::from(divisions).checked_mul(4)?;
1832 if numerator == 0 || denominator == 0 {
1833 return None;
1834 }
1835 let gcd = {
1836 let (mut a, mut b) = (numerator, denominator);
1837 while b != 0 {
1838 (a, b) = (b, a % b);
1839 }
1840 a
1841 };
1842 let length = Self {
1843 numerator: u32::try_from(numerator / gcd).ok()?,
1844 denominator: u32::try_from(denominator / gcd).ok()?,
1845 };
1846 length.beats().map(|_| length)
1847 }
1848}
1849
1850#[derive(Debug, Clone, Serialize, Deserialize)]
1851pub struct Measure {
1852 pub number: u32,
1853 pub time_sig: Option<TimeSignature>,
1854 pub key_sig: Option<KeySignature>,
1855 pub clef: Option<Clef>,
1856 pub tempo: Option<u16>,
1857 #[serde(default)]
1860 pub tempo_ramp_to: Option<u16>,
1861 #[serde(default)]
1863 pub instrument_change: Option<InstrumentDefinition>,
1864 #[serde(default)]
1866 pub tablature_change: Option<TablatureConfig>,
1867 pub barline_left: Barline,
1868 pub barline_right: Barline,
1869 #[serde(default)]
1870 pub volta: Option<VoltaBracket>,
1871 #[serde(default)]
1872 pub tempo_text: Option<String>,
1873 #[serde(default)]
1874 pub rehearsal: Option<String>,
1875 #[serde(default)]
1878 pub navigation: Option<String>,
1879 #[serde(default)]
1881 pub expression_text: Option<String>,
1882 #[serde(default)]
1883 pub texts: Vec<StyledText>,
1884 #[serde(default)]
1886 pub figured_bass: Vec<FiguredBassFigure>,
1887 #[serde(default)]
1889 pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
1890 #[serde(default)]
1892 pub multi_rest_count: Option<u8>,
1893 #[serde(default)]
1895 pub system_break: bool,
1896 #[serde(default)]
1898 pub page_break: bool,
1899 #[serde(default)]
1902 pub section_break: bool,
1903 pub voices: [Vec<Note>; 4],
1905 #[serde(default)]
1911 pub source_voice_numbers: [Option<u32>; 4],
1912 #[serde(default, skip_serializing_if = "Option::is_none")]
1915 pub actual_length: Option<MeasureLength>,
1916 #[serde(default, skip_serializing_if = "Option::is_none")]
1920 pub measure_repeat: Option<u8>,
1921}
1922
1923impl Measure {
1924 pub fn empty(numerator: u8, denominator: u8) -> Self {
1925 let total_beats = TimeSignature {
1926 numerator,
1927 denominator,
1928 }
1929 .total_beats();
1930 let mut voice0: Vec<Note> = Vec::new();
1931 let mut remaining = total_beats;
1932 while remaining > 1e-9 {
1933 let dur = Duration::whole_filling_beats(remaining);
1934 remaining -= dur.beats(0);
1935 voice0.push(Note::rest(dur));
1936 }
1937 Self {
1938 number: 0,
1939 time_sig: None,
1940 key_sig: None,
1941 clef: None,
1942 tempo: None,
1943 tempo_ramp_to: None,
1944 instrument_change: None,
1945 tablature_change: None,
1946 barline_left: Barline::Normal,
1947 barline_right: Barline::Normal,
1948 volta: None,
1949 tempo_text: None,
1950 rehearsal: None,
1951 navigation: None,
1952 expression_text: None,
1953 texts: Vec::new(),
1954 figured_bass: Vec::new(),
1955 harp_pedal_diagrams: Vec::new(),
1956 multi_rest_count: None,
1957 system_break: false,
1958 page_break: false,
1959 section_break: false,
1960 voices: [voice0, vec![], vec![], vec![]],
1961 source_voice_numbers: [None; 4],
1962 actual_length: None,
1963 measure_repeat: None,
1964 }
1965 }
1966
1967 pub fn repeat_content_from(&mut self, source: &Measure) {
1970 for (target, voice) in self.voices.iter_mut().zip(&source.voices) {
1971 *target = voice.iter().map(Note::repeated_copy).collect();
1972 }
1973 self.source_voice_numbers = source.source_voice_numbers;
1974 }
1975
1976 pub fn same_sounding_content(&self, other: &Measure) -> bool {
1979 self.voices.iter().zip(&other.voices).all(|(left, right)| {
1980 left.len() == right.len()
1981 && left.iter().zip(right).all(|(a, b)| {
1982 a.is_rest == b.is_rest
1983 && a.pitches == b.pitches
1984 && a.duration == b.duration
1985 && a.dot_count == b.dot_count
1986 && a.tuplet == b.tuplet
1987 && a.is_grace == b.is_grace
1988 })
1989 })
1990 }
1991
1992 pub fn duration_beats(&self, time_signature: &TimeSignature) -> f64 {
1995 self.actual_length
1996 .and_then(|length| length.beats())
1997 .unwrap_or_else(|| {
1998 self.time_sig
1999 .as_ref()
2000 .unwrap_or(time_signature)
2001 .total_beats()
2002 })
2003 }
2004
2005 pub fn renumber(&mut self, n: u32) {
2006 self.number = n;
2007 }
2008}
2009
2010#[derive(Debug, Clone, Serialize, Deserialize)]
2011pub struct Note {
2012 pub id: String,
2013 pub is_rest: bool,
2014 #[serde(default)]
2016 pub is_unpitched: bool,
2017 #[serde(default)]
2019 pub instrument_id: Option<String>,
2020 #[serde(default)]
2024 pub offset_x: Option<f64>,
2025 #[serde(default)]
2027 pub offset_y: Option<f64>,
2028 #[serde(default)]
2030 pub relative_x: Option<f64>,
2031 #[serde(default)]
2033 pub relative_y: Option<f64>,
2034 pub pitches: Vec<Pitch>,
2036 #[serde(default)]
2037 pub tab_position: Option<super::notation::TabPosition>,
2038 #[serde(default)]
2041 pub tab_positions: Vec<super::notation::TabPosition>,
2042 pub duration: Duration,
2043 pub dot_count: u8,
2044 pub tie_start: bool,
2045 pub tie_end: bool,
2046 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2050 pub pitch_tie_starts: Vec<bool>,
2051 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2053 pub pitch_tie_ends: Vec<bool>,
2054 pub beam: BeamState,
2055 pub articulations: Vec<Articulation>,
2056 pub dynamic: Option<Dynamic>,
2057 pub stem_up: Option<bool>,
2058 #[serde(default)]
2059 pub hairpin_start: Option<HairpinKind>,
2060 #[serde(default)]
2061 pub hairpin_end: bool,
2062 #[serde(default)]
2063 pub tuplet: Option<TupletInfo>,
2064 #[serde(default)]
2065 pub chord_symbol: Option<ChordSymbol>,
2066 #[serde(default)]
2067 pub is_grace: bool,
2068 #[serde(default)]
2070 pub grace_slash: bool,
2071 #[serde(default)]
2072 pub ottava_start: Option<OttavaKind>,
2073 #[serde(default)]
2074 pub ottava_end: bool,
2075 #[serde(default)]
2076 pub lyric: Option<Lyric>,
2077 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2079 pub additional_lyrics: Vec<VerseLyric>,
2080 #[serde(default)]
2081 pub pedal_start: bool,
2082 #[serde(default)]
2083 pub pedal_end: bool,
2084 #[serde(default)]
2085 pub slur_start: bool,
2086 #[serde(default)]
2087 pub slur_end: bool,
2088 #[serde(default)]
2090 pub arpeggiate: Option<bool>,
2091 #[serde(default)]
2093 pub technique_text: Option<String>,
2094 #[serde(default)]
2095 pub glissando_start: bool,
2096 #[serde(default)]
2097 pub glissando_end: bool,
2098 #[serde(default)]
2099 pub cross_staff: Option<CrossStaff>,
2100 #[serde(default)]
2102 pub fingering: Option<u8>,
2103 #[serde(default)]
2106 pub fingerings: Vec<u8>,
2107 #[serde(default)]
2109 pub string_number: Option<u8>,
2110 #[serde(default)]
2111 pub note_head: NoteHead,
2112 #[serde(default)]
2114 pub is_cue: bool,
2115 #[serde(default)]
2117 pub trill_line_start: bool,
2118 #[serde(default)]
2120 pub trill_line_end: bool,
2121 #[serde(default)]
2123 pub guitar_technique: Option<GuitarTechnique>,
2124 #[serde(default)]
2126 pub guitar_bend_alter_cents: Option<i16>,
2127 #[serde(default)]
2129 pub guitar_bend_curve: Vec<GuitarBendPoint>,
2130}
2131
2132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2134pub struct GuitarBendPoint {
2135 pub position_per_mille: u16,
2136 pub alter_cents: i16,
2137}
2138
2139impl Note {
2140 pub fn select_fingering(
2142 &self,
2143 policy: super::notation::FingeringSelectionPolicy,
2144 ) -> Option<u8> {
2145 let candidates = if self.fingerings.is_empty() {
2146 self.fingering.into_iter().collect::<Vec<_>>()
2147 } else {
2148 self.fingerings.clone()
2149 };
2150 match policy {
2151 super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
2152 super::notation::FingeringSelectionPolicy::LowestNumber => {
2153 candidates.iter().copied().min()
2154 }
2155 super::notation::FingeringSelectionPolicy::HighestNumber => {
2156 candidates.iter().copied().max()
2157 }
2158 }
2159 }
2160
2161 pub fn new(pitch: Pitch, duration: Duration) -> Self {
2162 Self {
2163 id: Uuid::new_v4().to_string(),
2164 is_rest: false,
2165 is_unpitched: false,
2166 instrument_id: None,
2167 offset_x: None,
2168 offset_y: None,
2169 relative_x: None,
2170 relative_y: None,
2171 pitches: vec![pitch],
2172 tab_position: None,
2173 tab_positions: Vec::new(),
2174 duration,
2175 dot_count: 0,
2176 tie_start: false,
2177 tie_end: false,
2178 pitch_tie_starts: Vec::new(),
2179 pitch_tie_ends: Vec::new(),
2180 beam: BeamState::None,
2181 articulations: Vec::new(),
2182 dynamic: None,
2183 stem_up: None,
2184 hairpin_start: None,
2185 hairpin_end: false,
2186 tuplet: None,
2187 chord_symbol: None,
2188 is_grace: false,
2189 grace_slash: false,
2190 ottava_start: None,
2191 ottava_end: false,
2192 lyric: None,
2193 additional_lyrics: Vec::new(),
2194 pedal_start: false,
2195 pedal_end: false,
2196 slur_start: false,
2197 slur_end: false,
2198 arpeggiate: None,
2199 technique_text: None,
2200 glissando_start: false,
2201 glissando_end: false,
2202 cross_staff: None,
2203 fingering: None,
2204 fingerings: Vec::new(),
2205 string_number: None,
2206 note_head: NoteHead::Normal,
2207 is_cue: false,
2208 trill_line_start: false,
2209 trill_line_end: false,
2210 guitar_technique: None,
2211 guitar_bend_alter_cents: None,
2212 guitar_bend_curve: Vec::new(),
2213 }
2214 }
2215
2216 pub fn rest(duration: Duration) -> Self {
2217 Self {
2218 id: Uuid::new_v4().to_string(),
2219 is_rest: true,
2220 is_unpitched: false,
2221 instrument_id: None,
2222 offset_x: None,
2223 offset_y: None,
2224 relative_x: None,
2225 relative_y: None,
2226 pitches: Vec::new(),
2227 tab_position: None,
2228 tab_positions: Vec::new(),
2229 duration,
2230 dot_count: 0,
2231 tie_start: false,
2232 tie_end: false,
2233 pitch_tie_starts: Vec::new(),
2234 pitch_tie_ends: Vec::new(),
2235 beam: BeamState::None,
2236 articulations: Vec::new(),
2237 dynamic: None,
2238 stem_up: None,
2239 hairpin_start: None,
2240 hairpin_end: false,
2241 tuplet: None,
2242 chord_symbol: None,
2243 is_grace: false,
2244 grace_slash: false,
2245 ottava_start: None,
2246 ottava_end: false,
2247 lyric: None,
2248 additional_lyrics: Vec::new(),
2249 pedal_start: false,
2250 pedal_end: false,
2251 slur_start: false,
2252 slur_end: false,
2253 arpeggiate: None,
2254 technique_text: None,
2255 glissando_start: false,
2256 glissando_end: false,
2257 cross_staff: None,
2258 fingering: None,
2259 fingerings: Vec::new(),
2260 string_number: None,
2261 note_head: NoteHead::Normal,
2262 is_cue: false,
2263 trill_line_start: false,
2264 trill_line_end: false,
2265 guitar_technique: None,
2266 guitar_bend_alter_cents: None,
2267 guitar_bend_curve: Vec::new(),
2268 }
2269 }
2270
2271 pub fn repeated_copy(&self) -> Note {
2274 let mut copy = self.clone();
2275 copy.id = Uuid::new_v4().to_string();
2276 copy.tie_start = false;
2277 copy.tie_end = false;
2278 copy.slur_start = false;
2279 copy.slur_end = false;
2280 copy.hairpin_start = None;
2281 copy.hairpin_end = false;
2282 copy.pedal_start = false;
2283 copy.pedal_end = false;
2284 copy.ottava_start = None;
2285 copy.ottava_end = false;
2286 copy.glissando_start = false;
2287 copy.glissando_end = false;
2288 copy.trill_line_start = false;
2289 copy.trill_line_end = false;
2290 copy.lyric = None;
2291 copy.additional_lyrics.clear();
2292 copy.chord_symbol = None;
2293 copy.dynamic = None;
2294 copy
2295 }
2296
2297 pub fn pitch_tie_start(&self, index: usize) -> bool {
2300 per_pitch_flag(
2301 &self.pitch_tie_starts,
2302 self.pitches.len(),
2303 index,
2304 self.tie_start,
2305 )
2306 }
2307
2308 pub fn pitch_tie_end(&self, index: usize) -> bool {
2310 per_pitch_flag(
2311 &self.pitch_tie_ends,
2312 self.pitches.len(),
2313 index,
2314 self.tie_end,
2315 )
2316 }
2317
2318 pub fn set_pitch_ties(&mut self, starts: &[bool], ends: &[bool]) {
2323 let count = self.pitches.len();
2324 if starts.len() == count {
2325 self.tie_start = starts.iter().any(|tied| *tied);
2326 self.pitch_tie_starts = if starts.iter().all(|tied| *tied == self.tie_start) {
2327 Vec::new()
2328 } else {
2329 starts.to_vec()
2330 };
2331 }
2332 if ends.len() == count {
2333 self.tie_end = ends.iter().any(|tied| *tied);
2334 self.pitch_tie_ends = if ends.iter().all(|tied| *tied == self.tie_end) {
2335 Vec::new()
2336 } else {
2337 ends.to_vec()
2338 };
2339 }
2340 }
2341
2342 pub fn pitch_tie_starts_or_uniform(&self) -> Vec<bool> {
2344 (0..self.pitches.len())
2345 .map(|index| self.pitch_tie_start(index))
2346 .collect()
2347 }
2348
2349 pub fn pitch_tie_ends_or_uniform(&self) -> Vec<bool> {
2351 (0..self.pitches.len())
2352 .map(|index| self.pitch_tie_end(index))
2353 .collect()
2354 }
2355
2356 pub fn is_plain_whole_rest(&self) -> bool {
2360 self.is_rest
2361 && matches!(self.duration, Duration::Whole)
2362 && self.dot_count == 0
2363 && self.tuplet.is_none()
2364 && !self.is_grace
2365 && !self.is_cue
2366 }
2367
2368 pub fn beats(&self) -> f64 {
2369 if self.is_grace || self.is_cue {
2370 return 0.0;
2371 }
2372 let base = self.duration.beats(self.dot_count);
2373 if let Some(ref t) = self.tuplet {
2374 base * (t.normal_notes as f64) / (t.actual_notes as f64)
2375 } else {
2376 base
2377 }
2378 }
2379}
2380
2381impl Duration {
2382 pub fn whole_filling_beats(beats: f64) -> Duration {
2384 if beats >= 4.0 {
2385 Duration::Whole
2386 } else if beats >= 2.0 {
2387 Duration::Half
2388 } else if beats >= 1.0 {
2389 Duration::Quarter
2390 } else if beats >= 0.5 {
2391 Duration::Eighth
2392 } else if beats >= 0.25 {
2393 Duration::Sixteenth
2394 } else if beats >= 0.125 {
2395 Duration::ThirtySecond
2396 } else {
2397 Duration::SixtyFourth
2398 }
2399 }
2400}
2401
2402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2406pub struct NoteAddr {
2407 pub part: usize,
2408 pub staff: usize,
2409 pub measure: usize,
2410 pub voice: usize,
2411 pub note: usize,
2412}
2413
2414#[derive(Debug, Clone, Serialize, Deserialize)]
2418pub enum ScoreChange {
2419 ScoreViewsChanged {
2420 old: Vec<ScoreView>,
2421 new: Vec<ScoreView>,
2422 },
2423 ScoreTextChanged {
2424 old: Vec<StyledText>,
2425 new: Vec<StyledText>,
2426 },
2427 MeasureTextChanged {
2428 part: usize,
2429 staff: usize,
2430 measure: usize,
2431 old: Vec<StyledText>,
2432 new: Vec<StyledText>,
2433 },
2434 FiguredBassChanged {
2435 part: usize,
2436 staff: usize,
2437 measure: usize,
2438 old: Vec<FiguredBassFigure>,
2439 new: Vec<FiguredBassFigure>,
2440 },
2441 HarpPedalDiagramsChanged {
2442 part: usize,
2443 staff: usize,
2444 measure: usize,
2445 old: Vec<HarpPedalDiagram>,
2446 new: Vec<HarpPedalDiagram>,
2447 },
2448 ChordDefinitionsChanged {
2449 old: Vec<ChordDefinition>,
2450 new: Vec<ChordDefinition>,
2451 },
2452 PartNamesChanged {
2453 part: usize,
2454 old_name: String,
2455 new_name: String,
2456 old_short_name: String,
2457 new_short_name: String,
2458 },
2459 PartMidiChanged {
2460 part: usize,
2461 old_channel: u8,
2462 new_channel: u8,
2463 old_program: u8,
2464 new_program: u8,
2465 },
2466 InstrumentDefinitionChanged {
2467 part: usize,
2468 old: Option<InstrumentDefinition>,
2469 new: Option<InstrumentDefinition>,
2470 },
2471 PartMidiAutomationChanged {
2472 part: usize,
2473 old_pitch_bends: Vec<MidiPitchBend>,
2474 new_pitch_bends: Vec<MidiPitchBend>,
2475 old_control_changes: Vec<MidiControlChange>,
2476 new_control_changes: Vec<MidiControlChange>,
2477 old_program_changes: Vec<MidiProgramChange>,
2478 new_program_changes: Vec<MidiProgramChange>,
2479 old_aftertouch: Vec<MidiAftertouch>,
2480 new_aftertouch: Vec<MidiAftertouch>,
2481 },
2482 StaffConfigurationChanged {
2483 part: usize,
2484 staff: usize,
2485 old_clef: Clef,
2486 new_clef: Clef,
2487 old_transpose_semitones: i8,
2488 new_transpose_semitones: i8,
2489 },
2490 StaffPresentationChanged {
2491 part: usize,
2492 staff: usize,
2493 old: StaffPresentation,
2494 new: StaffPresentation,
2495 },
2496 MeasurePresentationChanged {
2497 part: usize,
2498 staff: usize,
2499 measure: usize,
2500 old_number: u32,
2501 new_number: u32,
2502 old_clef: Option<Clef>,
2503 new_clef: Option<Clef>,
2504 old_tempo_text: Option<String>,
2505 new_tempo_text: Option<String>,
2506 old_navigation: Option<String>,
2507 new_navigation: Option<String>,
2508 old_expression_text: Option<String>,
2509 new_expression_text: Option<String>,
2510 old_multi_rest_count: Option<u8>,
2511 new_multi_rest_count: Option<u8>,
2512 old_system_break: bool,
2513 new_system_break: bool,
2514 old_page_break: bool,
2515 new_page_break: bool,
2516 old_section_break: bool,
2517 new_section_break: bool,
2518 },
2519 TablatureConfigChanged {
2520 part: usize,
2521 staff: usize,
2522 old: Option<TablatureConfig>,
2523 new: Option<TablatureConfig>,
2524 },
2525 TablatureChangeChanged {
2526 part: usize,
2527 staff: usize,
2528 measure: usize,
2529 old: Option<TablatureConfig>,
2530 new: Option<TablatureConfig>,
2531 },
2532 UnrepresentedFieldChanged {
2537 path: String,
2538 },
2539 MetadataChanged {
2540 field: String,
2541 old: String,
2542 new: String,
2543 },
2544 TempoChanged {
2545 old: u16,
2546 new: u16,
2547 },
2548 KeySignatureChanged {
2549 old: KeySignature,
2550 new: KeySignature,
2551 },
2552 PartAdded {
2553 part_index: usize,
2554 },
2555 PartRemoved {
2556 part_index: usize,
2557 name: String,
2558 },
2559 NoteAdded {
2560 part: usize,
2561 staff: usize,
2562 measure: usize,
2563 voice: usize,
2564 note_index: usize,
2565 },
2566 NoteRemoved {
2567 part: usize,
2568 staff: usize,
2569 measure: usize,
2570 voice: usize,
2571 note: Box<Note>,
2572 },
2573 NoteModified {
2574 part: usize,
2575 staff: usize,
2576 measure: usize,
2577 voice: usize,
2578 note_index: usize,
2579 old: Box<Note>,
2580 new: Box<Note>,
2581 },
2582 TimeSigChanged {
2583 part: usize,
2584 staff: usize,
2585 measure: usize,
2586 old: Option<TimeSignature>,
2587 new: Option<TimeSignature>,
2588 },
2589 MeasureTempoChanged {
2590 part: usize,
2591 staff: usize,
2592 measure: usize,
2593 old: Option<u16>,
2594 new: Option<u16>,
2595 },
2596 MeasureTempoRampChanged {
2597 part: usize,
2598 staff: usize,
2599 measure: usize,
2600 old: Option<u16>,
2601 new: Option<u16>,
2602 },
2603 BarlineChanged {
2604 part: usize,
2605 staff: usize,
2606 measure: usize,
2607 },
2608 RehearsalMarkChanged {
2609 part: usize,
2610 staff: usize,
2611 measure: usize,
2612 old: Option<String>,
2613 new: Option<String>,
2614 },
2615 VoltaChanged {
2616 part: usize,
2617 staff: usize,
2618 measure: usize,
2619 },
2620}
2621
2622pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
2628 let mut changes: Vec<ScoreChange> = Vec::new();
2629
2630 if a.views != b.views {
2631 changes.push(ScoreChange::ScoreViewsChanged {
2632 old: a.views.clone(),
2633 new: b.views.clone(),
2634 });
2635 }
2636
2637 if a.texts != b.texts {
2638 changes.push(ScoreChange::ScoreTextChanged {
2639 old: a.texts.clone(),
2640 new: b.texts.clone(),
2641 });
2642 }
2643 if a.chord_definitions != b.chord_definitions {
2644 changes.push(ScoreChange::ChordDefinitionsChanged {
2645 old: a.chord_definitions.clone(),
2646 new: b.chord_definitions.clone(),
2647 });
2648 }
2649 if let Some(path) = first_unrepresented_field_change(a, b) {
2650 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
2651 }
2652
2653 macro_rules! meta {
2654 ($field:ident, $name:literal) => {
2655 if a.metadata.$field != b.metadata.$field {
2656 changes.push(ScoreChange::MetadataChanged {
2657 field: $name.to_string(),
2658 old: a.metadata.$field.clone(),
2659 new: b.metadata.$field.clone(),
2660 });
2661 }
2662 };
2663 }
2664 meta!(title, "title");
2665 meta!(composer, "composer");
2666 meta!(lyricist, "lyricist");
2667 meta!(copyright, "copyright");
2668 meta!(work_number, "work_number");
2669 meta!(movement_title, "movement_title");
2670
2671 if a.settings.tempo_bpm != b.settings.tempo_bpm {
2672 changes.push(ScoreChange::TempoChanged {
2673 old: a.settings.tempo_bpm,
2674 new: b.settings.tempo_bpm,
2675 });
2676 }
2677 if a.settings.key_signature != b.settings.key_signature {
2678 changes.push(ScoreChange::KeySignatureChanged {
2679 old: a.settings.key_signature.clone(),
2680 new: b.settings.key_signature.clone(),
2681 });
2682 }
2683
2684 let a_len = a.parts.len();
2685 let b_len = b.parts.len();
2686 for i in b_len..a_len {
2687 changes.push(ScoreChange::PartRemoved {
2688 part_index: i,
2689 name: a.parts[i].name.clone(),
2690 });
2691 }
2692 for i in a_len..b_len {
2693 changes.push(ScoreChange::PartAdded { part_index: i });
2694 }
2695
2696 for pi in 0..a_len.min(b_len) {
2697 let ap = &a.parts[pi];
2698 let bp = &b.parts[pi];
2699 if ap.name != bp.name || ap.short_name != bp.short_name {
2700 changes.push(ScoreChange::PartNamesChanged {
2701 part: pi,
2702 old_name: ap.name.clone(),
2703 new_name: bp.name.clone(),
2704 old_short_name: ap.short_name.clone(),
2705 new_short_name: bp.short_name.clone(),
2706 });
2707 }
2708 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
2709 changes.push(ScoreChange::PartMidiChanged {
2710 part: pi,
2711 old_channel: ap.midi_channel,
2712 new_channel: bp.midi_channel,
2713 old_program: ap.midi_program,
2714 new_program: bp.midi_program,
2715 });
2716 }
2717 if ap.instrument != bp.instrument {
2718 changes.push(ScoreChange::InstrumentDefinitionChanged {
2719 part: pi,
2720 old: ap.instrument.clone(),
2721 new: bp.instrument.clone(),
2722 });
2723 }
2724 if ap.midi_pitch_bends != bp.midi_pitch_bends
2725 || ap.midi_control_changes != bp.midi_control_changes
2726 || ap.midi_program_changes != bp.midi_program_changes
2727 || ap.midi_aftertouch != bp.midi_aftertouch
2728 {
2729 changes.push(ScoreChange::PartMidiAutomationChanged {
2730 part: pi,
2731 old_pitch_bends: ap.midi_pitch_bends.clone(),
2732 new_pitch_bends: bp.midi_pitch_bends.clone(),
2733 old_control_changes: ap.midi_control_changes.clone(),
2734 new_control_changes: bp.midi_control_changes.clone(),
2735 old_program_changes: ap.midi_program_changes.clone(),
2736 new_program_changes: bp.midi_program_changes.clone(),
2737 old_aftertouch: ap.midi_aftertouch.clone(),
2738 new_aftertouch: bp.midi_aftertouch.clone(),
2739 });
2740 }
2741 for si in 0..ap.staves.len().min(bp.staves.len()) {
2742 let a_staff = &ap.staves[si];
2743 let b_staff = &bp.staves[si];
2744 if a_staff.clef != b_staff.clef
2745 || a_staff.transpose_semitones != b_staff.transpose_semitones
2746 {
2747 changes.push(ScoreChange::StaffConfigurationChanged {
2748 part: pi,
2749 staff: si,
2750 old_clef: a_staff.clef.clone(),
2751 new_clef: b_staff.clef.clone(),
2752 old_transpose_semitones: a_staff.transpose_semitones,
2753 new_transpose_semitones: b_staff.transpose_semitones,
2754 });
2755 }
2756 if a_staff.tablature != b_staff.tablature {
2757 changes.push(ScoreChange::TablatureConfigChanged {
2758 part: pi,
2759 staff: si,
2760 old: a_staff.tablature.clone(),
2761 new: b_staff.tablature.clone(),
2762 });
2763 }
2764 if a_staff.presentation != b_staff.presentation {
2765 changes.push(ScoreChange::StaffPresentationChanged {
2766 part: pi,
2767 staff: si,
2768 old: a_staff.presentation.clone(),
2769 new: b_staff.presentation.clone(),
2770 });
2771 }
2772 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
2773 let am = &a_staff.measures[mi];
2774 let bm = &b_staff.measures[mi];
2775 if am.tablature_change != bm.tablature_change {
2776 changes.push(ScoreChange::TablatureChangeChanged {
2777 part: pi,
2778 staff: si,
2779 measure: mi,
2780 old: am.tablature_change.clone(),
2781 new: bm.tablature_change.clone(),
2782 });
2783 }
2784 if am.number != bm.number
2785 || am.clef != bm.clef
2786 || am.tempo_text != bm.tempo_text
2787 || am.navigation != bm.navigation
2788 || am.expression_text != bm.expression_text
2789 || am.multi_rest_count != bm.multi_rest_count
2790 || am.system_break != bm.system_break
2791 || am.page_break != bm.page_break
2792 || am.section_break != bm.section_break
2793 {
2794 changes.push(ScoreChange::MeasurePresentationChanged {
2795 part: pi,
2796 staff: si,
2797 measure: mi,
2798 old_number: am.number,
2799 new_number: bm.number,
2800 old_clef: am.clef.clone(),
2801 new_clef: bm.clef.clone(),
2802 old_tempo_text: am.tempo_text.clone(),
2803 new_tempo_text: bm.tempo_text.clone(),
2804 old_navigation: am.navigation.clone(),
2805 new_navigation: bm.navigation.clone(),
2806 old_expression_text: am.expression_text.clone(),
2807 new_expression_text: bm.expression_text.clone(),
2808 old_multi_rest_count: am.multi_rest_count,
2809 new_multi_rest_count: bm.multi_rest_count,
2810 old_system_break: am.system_break,
2811 new_system_break: bm.system_break,
2812 old_page_break: am.page_break,
2813 new_page_break: bm.page_break,
2814 old_section_break: am.section_break,
2815 new_section_break: bm.section_break,
2816 });
2817 }
2818 for vi in 0..4usize {
2819 let av = &am.voices[vi];
2820 let bv = &bm.voices[vi];
2821 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
2822 if !note_content_eq(a_note, b_note) {
2823 changes.push(ScoreChange::NoteModified {
2824 part: pi,
2825 staff: si,
2826 measure: mi,
2827 voice: vi,
2828 note_index: ni,
2829 old: Box::new(a_note.clone()),
2830 new: Box::new(b_note.clone()),
2831 });
2832 }
2833 }
2834 for note in av.iter().skip(bv.len()) {
2835 changes.push(ScoreChange::NoteRemoved {
2836 part: pi,
2837 staff: si,
2838 measure: mi,
2839 voice: vi,
2840 note: Box::new(note.clone()),
2841 });
2842 }
2843 for ni in av.len()..bv.len() {
2844 changes.push(ScoreChange::NoteAdded {
2845 part: pi,
2846 staff: si,
2847 measure: mi,
2848 voice: vi,
2849 note_index: ni,
2850 });
2851 }
2852 }
2853 if am.time_sig != bm.time_sig {
2854 changes.push(ScoreChange::TimeSigChanged {
2855 part: pi,
2856 staff: si,
2857 measure: mi,
2858 old: am.time_sig.clone(),
2859 new: bm.time_sig.clone(),
2860 });
2861 }
2862 if am.tempo != bm.tempo {
2863 changes.push(ScoreChange::MeasureTempoChanged {
2864 part: pi,
2865 staff: si,
2866 measure: mi,
2867 old: am.tempo,
2868 new: bm.tempo,
2869 });
2870 }
2871 if am.tempo_ramp_to != bm.tempo_ramp_to {
2872 changes.push(ScoreChange::MeasureTempoRampChanged {
2873 part: pi,
2874 staff: si,
2875 measure: mi,
2876 old: am.tempo_ramp_to,
2877 new: bm.tempo_ramp_to,
2878 });
2879 }
2880 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
2881 changes.push(ScoreChange::BarlineChanged {
2882 part: pi,
2883 staff: si,
2884 measure: mi,
2885 });
2886 }
2887 if am.rehearsal != bm.rehearsal {
2888 changes.push(ScoreChange::RehearsalMarkChanged {
2889 part: pi,
2890 staff: si,
2891 measure: mi,
2892 old: am.rehearsal.clone(),
2893 new: bm.rehearsal.clone(),
2894 });
2895 }
2896 if am.volta != bm.volta {
2897 changes.push(ScoreChange::VoltaChanged {
2898 part: pi,
2899 staff: si,
2900 measure: mi,
2901 });
2902 }
2903 if am.texts != bm.texts {
2904 changes.push(ScoreChange::MeasureTextChanged {
2905 part: pi,
2906 staff: si,
2907 measure: mi,
2908 old: am.texts.clone(),
2909 new: bm.texts.clone(),
2910 });
2911 }
2912 if am.figured_bass != bm.figured_bass {
2913 changes.push(ScoreChange::FiguredBassChanged {
2914 part: pi,
2915 staff: si,
2916 measure: mi,
2917 old: am.figured_bass.clone(),
2918 new: bm.figured_bass.clone(),
2919 });
2920 }
2921 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
2922 changes.push(ScoreChange::HarpPedalDiagramsChanged {
2923 part: pi,
2924 staff: si,
2925 measure: mi,
2926 old: am.harp_pedal_diagrams.clone(),
2927 new: bm.harp_pedal_diagrams.clone(),
2928 });
2929 }
2930 }
2931 }
2932 }
2933
2934 changes
2935}
2936
2937fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
2938 if a.settings.time_signature != b.settings.time_signature {
2939 return Some("settings.time_signature".to_string());
2940 }
2941 if a.part_groups.len() != b.part_groups.len()
2942 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
2943 x.first_part != y.first_part
2944 || x.last_part != y.last_part
2945 || x.symbol != y.symbol
2946 || x.barlines_connect != y.barlines_connect
2947 })
2948 {
2949 return Some("part_groups".to_string());
2950 }
2951 a.parts
2952 .iter()
2953 .zip(&b.parts)
2954 .enumerate()
2955 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
2956}
2957
2958fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
2959 let prefix = format!("parts[{part_index}]");
2960 if a.percussion_instruments != b.percussion_instruments {
2961 return Some(format!("{prefix}.percussion_instruments"));
2962 }
2963 if a.staff_groups != b.staff_groups {
2964 return Some(format!("{prefix}.staff_groups"));
2965 }
2966 if a.staves.len() != b.staves.len() {
2967 return Some(format!("{prefix}.staves"));
2968 }
2969 a.staves
2970 .iter()
2971 .zip(&b.staves)
2972 .enumerate()
2973 .find_map(|(staff_index, (a, b))| {
2974 first_unrepresented_staff_change(&prefix, staff_index, a, b)
2975 })
2976}
2977
2978fn first_unrepresented_staff_change(
2979 part_prefix: &str,
2980 staff_index: usize,
2981 a: &Staff,
2982 b: &Staff,
2983) -> Option<String> {
2984 let prefix = format!("{part_prefix}.staves[{staff_index}]");
2985 if a.measures.len() != b.measures.len() {
2986 return Some(format!("{prefix}.measures"));
2987 }
2988 a.measures
2989 .iter()
2990 .zip(&b.measures)
2991 .enumerate()
2992 .find_map(|(measure_index, (a, b))| {
2993 first_unrepresented_measure_change(&prefix, measure_index, a, b)
2994 })
2995}
2996
2997fn first_unrepresented_measure_change(
2998 _staff_prefix: &str,
2999 _measure_index: usize,
3000 _a: &Measure,
3001 _b: &Measure,
3002) -> Option<String> {
3003 None
3004}
3005
3006#[derive(Debug, Clone, Serialize, Deserialize)]
3013pub enum ScorePatch {
3014 SetScoreViews {
3015 value: Vec<ScoreView>,
3016 },
3017 SetScoreTexts {
3018 value: Vec<StyledText>,
3019 },
3020 SetMeasureTexts {
3021 part: usize,
3022 staff: usize,
3023 measure: usize,
3024 value: Vec<StyledText>,
3025 },
3026 SetFiguredBass {
3027 part: usize,
3028 staff: usize,
3029 measure: usize,
3030 value: Vec<FiguredBassFigure>,
3031 },
3032 SetHarpPedalDiagrams {
3033 part: usize,
3034 staff: usize,
3035 measure: usize,
3036 value: Vec<HarpPedalDiagram>,
3037 },
3038 SetChordDefinitions {
3039 value: Vec<ChordDefinition>,
3040 },
3041 SetPartNames {
3042 part: usize,
3043 name: String,
3044 short_name: String,
3045 },
3046 SetPartMidi {
3047 part: usize,
3048 channel: u8,
3049 program: u8,
3050 },
3051 SetInstrumentDefinition {
3052 part: usize,
3053 value: Option<InstrumentDefinition>,
3054 },
3055 SetPartMidiAutomation {
3056 part: usize,
3057 pitch_bends: Vec<MidiPitchBend>,
3058 control_changes: Vec<MidiControlChange>,
3059 program_changes: Vec<MidiProgramChange>,
3060 aftertouch: Vec<MidiAftertouch>,
3061 },
3062 SetStaffConfiguration {
3063 part: usize,
3064 staff: usize,
3065 clef: Clef,
3066 transpose_semitones: i8,
3067 },
3068 SetStaffPresentation {
3069 part: usize,
3070 staff: usize,
3071 value: StaffPresentation,
3072 },
3073 SetMeasurePresentation {
3074 part: usize,
3075 staff: usize,
3076 measure: usize,
3077 number: u32,
3078 clef: Option<Clef>,
3079 tempo_text: Option<String>,
3080 navigation: Option<String>,
3081 expression_text: Option<String>,
3082 multi_rest_count: Option<u8>,
3083 system_break: bool,
3084 page_break: bool,
3085 #[serde(default)]
3086 section_break: bool,
3087 },
3088 SetTablatureConfig {
3089 part: usize,
3090 staff: usize,
3091 value: Option<TablatureConfig>,
3092 },
3093 SetMeasureTablatureChange {
3094 part: usize,
3095 staff: usize,
3096 measure: usize,
3097 value: Option<TablatureConfig>,
3098 },
3099 SetMetadata {
3100 field: String,
3101 value: String,
3102 },
3103 SetTempo {
3104 value: u16,
3105 },
3106 SetKeySignature {
3107 part: usize,
3108 staff: usize,
3109 measure: usize,
3110 value: Option<KeySignature>,
3111 },
3112 SetTimeSignature {
3113 part: usize,
3114 staff: usize,
3115 measure: usize,
3116 value: Option<TimeSignature>,
3117 },
3118 SetBarlines {
3119 part: usize,
3120 staff: usize,
3121 measure: usize,
3122 left: Barline,
3123 right: Barline,
3124 },
3125 SetRehearsal {
3126 part: usize,
3127 staff: usize,
3128 measure: usize,
3129 value: Option<String>,
3130 },
3131 SetVolta {
3132 part: usize,
3133 staff: usize,
3134 measure: usize,
3135 value: Option<VoltaBracket>,
3136 },
3137 AddNote {
3139 part: usize,
3140 staff: usize,
3141 measure: usize,
3142 voice: usize,
3143 #[serde(default = "legacy_append_index")]
3145 note_index: usize,
3146 note: Box<Note>,
3147 },
3148 RemoveNote {
3149 part: usize,
3150 staff: usize,
3151 measure: usize,
3152 voice: usize,
3153 note_index: usize,
3154 },
3155 ReplaceNote {
3157 part: usize,
3158 staff: usize,
3159 measure: usize,
3160 voice: usize,
3161 note_index: usize,
3162 note: Box<Note>,
3163 },
3164 SetMeasureTempo {
3165 part: usize,
3166 staff: usize,
3167 measure: usize,
3168 value: Option<u16>,
3169 },
3170 SetMeasureTempoRamp {
3171 part: usize,
3172 staff: usize,
3173 measure: usize,
3174 value: Option<u16>,
3175 },
3176 ReplaceScore {
3179 score: Box<Score>,
3180 },
3181}
3182
3183fn legacy_append_index() -> usize {
3184 usize::MAX
3185}
3186
3187fn patch_requires_replace(a: &Score, b: &Score) -> bool {
3191 if a.settings.time_signature != b.settings.time_signature
3192 || a.settings.key_signature != b.settings.key_signature
3193 || a.parts.len() != b.parts.len()
3194 || a.part_groups.len() != b.part_groups.len()
3195 {
3196 return true;
3197 }
3198 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3199 x.first_part != y.first_part
3200 || x.last_part != y.last_part
3201 || x.symbol != y.symbol
3202 || x.barlines_connect != y.barlines_connect
3203 }) {
3204 return true;
3205 }
3206 for (ap, bp) in a.parts.iter().zip(&b.parts) {
3207 if ap.percussion_instruments != bp.percussion_instruments
3208 || ap.staff_groups != bp.staff_groups
3209 || ap.staves.len() != bp.staves.len()
3210 {
3211 return true;
3212 }
3213 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
3214 if as_.measures.len() != bs.measures.len() {
3215 return true;
3216 }
3217 }
3218 }
3219 false
3220}
3221
3222pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
3227 let mut patches: Vec<ScorePatch> = Vec::new();
3228
3229 if patch_requires_replace(a, b) {
3230 return vec![ScorePatch::ReplaceScore {
3231 score: Box::new(b.clone()),
3232 }];
3233 }
3234
3235 if a.views != b.views {
3236 patches.push(ScorePatch::SetScoreViews {
3237 value: b.views.clone(),
3238 });
3239 }
3240
3241 if a.texts != b.texts {
3242 patches.push(ScorePatch::SetScoreTexts {
3243 value: b.texts.clone(),
3244 });
3245 }
3246 if a.chord_definitions != b.chord_definitions {
3247 patches.push(ScorePatch::SetChordDefinitions {
3248 value: b.chord_definitions.clone(),
3249 });
3250 }
3251
3252 macro_rules! meta {
3253 ($field:ident, $name:literal) => {
3254 if a.metadata.$field != b.metadata.$field {
3255 patches.push(ScorePatch::SetMetadata {
3256 field: $name.to_string(),
3257 value: b.metadata.$field.clone(),
3258 });
3259 }
3260 };
3261 }
3262 meta!(title, "title");
3263 meta!(composer, "composer");
3264 meta!(lyricist, "lyricist");
3265 meta!(copyright, "copyright");
3266 meta!(work_number, "work_number");
3267 meta!(movement_title, "movement_title");
3268
3269 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3270 patches.push(ScorePatch::SetTempo {
3271 value: b.settings.tempo_bpm,
3272 });
3273 }
3274
3275 for pi in 0..a.parts.len().min(b.parts.len()) {
3276 let ap = &a.parts[pi];
3277 let bp = &b.parts[pi];
3278 if ap.name != bp.name || ap.short_name != bp.short_name {
3279 patches.push(ScorePatch::SetPartNames {
3280 part: pi,
3281 name: bp.name.clone(),
3282 short_name: bp.short_name.clone(),
3283 });
3284 }
3285 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3286 patches.push(ScorePatch::SetPartMidi {
3287 part: pi,
3288 channel: bp.midi_channel,
3289 program: bp.midi_program,
3290 });
3291 }
3292 if ap.instrument != bp.instrument {
3293 patches.push(ScorePatch::SetInstrumentDefinition {
3294 part: pi,
3295 value: bp.instrument.clone(),
3296 });
3297 }
3298 if ap.midi_pitch_bends != bp.midi_pitch_bends
3299 || ap.midi_control_changes != bp.midi_control_changes
3300 || ap.midi_program_changes != bp.midi_program_changes
3301 || ap.midi_aftertouch != bp.midi_aftertouch
3302 {
3303 patches.push(ScorePatch::SetPartMidiAutomation {
3304 part: pi,
3305 pitch_bends: bp.midi_pitch_bends.clone(),
3306 control_changes: bp.midi_control_changes.clone(),
3307 program_changes: bp.midi_program_changes.clone(),
3308 aftertouch: bp.midi_aftertouch.clone(),
3309 });
3310 }
3311 for si in 0..ap.staves.len().min(bp.staves.len()) {
3312 let a_staff = &ap.staves[si];
3313 let b_staff = &bp.staves[si];
3314 if a_staff.clef != b_staff.clef
3315 || a_staff.transpose_semitones != b_staff.transpose_semitones
3316 {
3317 patches.push(ScorePatch::SetStaffConfiguration {
3318 part: pi,
3319 staff: si,
3320 clef: b_staff.clef.clone(),
3321 transpose_semitones: b_staff.transpose_semitones,
3322 });
3323 }
3324 if a_staff.tablature != b_staff.tablature {
3325 patches.push(ScorePatch::SetTablatureConfig {
3326 part: pi,
3327 staff: si,
3328 value: b_staff.tablature.clone(),
3329 });
3330 }
3331 if a_staff.presentation != b_staff.presentation {
3332 patches.push(ScorePatch::SetStaffPresentation {
3333 part: pi,
3334 staff: si,
3335 value: b_staff.presentation.clone(),
3336 });
3337 }
3338 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3339 let am = &a_staff.measures[mi];
3340 let bm = &b_staff.measures[mi];
3341
3342 if am.tablature_change != bm.tablature_change {
3343 patches.push(ScorePatch::SetMeasureTablatureChange {
3344 part: pi,
3345 staff: si,
3346 measure: mi,
3347 value: bm.tablature_change.clone(),
3348 });
3349 }
3350
3351 if am.number != bm.number
3352 || am.clef != bm.clef
3353 || am.tempo_text != bm.tempo_text
3354 || am.navigation != bm.navigation
3355 || am.expression_text != bm.expression_text
3356 || am.multi_rest_count != bm.multi_rest_count
3357 || am.system_break != bm.system_break
3358 || am.page_break != bm.page_break
3359 || am.section_break != bm.section_break
3360 {
3361 patches.push(ScorePatch::SetMeasurePresentation {
3362 part: pi,
3363 staff: si,
3364 measure: mi,
3365 number: bm.number,
3366 clef: bm.clef.clone(),
3367 tempo_text: bm.tempo_text.clone(),
3368 navigation: bm.navigation.clone(),
3369 expression_text: bm.expression_text.clone(),
3370 multi_rest_count: bm.multi_rest_count,
3371 system_break: bm.system_break,
3372 page_break: bm.page_break,
3373 section_break: bm.section_break,
3374 });
3375 }
3376 if am.key_sig != bm.key_sig {
3377 patches.push(ScorePatch::SetKeySignature {
3378 part: pi,
3379 staff: si,
3380 measure: mi,
3381 value: bm.key_sig.clone(),
3382 });
3383 }
3384 if am.time_sig != bm.time_sig {
3385 patches.push(ScorePatch::SetTimeSignature {
3386 part: pi,
3387 staff: si,
3388 measure: mi,
3389 value: bm.time_sig.clone(),
3390 });
3391 }
3392 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3393 patches.push(ScorePatch::SetBarlines {
3394 part: pi,
3395 staff: si,
3396 measure: mi,
3397 left: bm.barline_left.clone(),
3398 right: bm.barline_right.clone(),
3399 });
3400 }
3401 if am.rehearsal != bm.rehearsal {
3402 patches.push(ScorePatch::SetRehearsal {
3403 part: pi,
3404 staff: si,
3405 measure: mi,
3406 value: bm.rehearsal.clone(),
3407 });
3408 }
3409 if am.volta != bm.volta {
3410 patches.push(ScorePatch::SetVolta {
3411 part: pi,
3412 staff: si,
3413 measure: mi,
3414 value: bm.volta.clone(),
3415 });
3416 }
3417 if am.tempo != bm.tempo {
3418 patches.push(ScorePatch::SetMeasureTempo {
3419 part: pi,
3420 staff: si,
3421 measure: mi,
3422 value: bm.tempo,
3423 });
3424 }
3425 if am.tempo_ramp_to != bm.tempo_ramp_to {
3426 patches.push(ScorePatch::SetMeasureTempoRamp {
3427 part: pi,
3428 staff: si,
3429 measure: mi,
3430 value: bm.tempo_ramp_to,
3431 });
3432 }
3433 if am.texts != bm.texts {
3434 patches.push(ScorePatch::SetMeasureTexts {
3435 part: pi,
3436 staff: si,
3437 measure: mi,
3438 value: bm.texts.clone(),
3439 });
3440 }
3441 if am.figured_bass != bm.figured_bass {
3442 patches.push(ScorePatch::SetFiguredBass {
3443 part: pi,
3444 staff: si,
3445 measure: mi,
3446 value: bm.figured_bass.clone(),
3447 });
3448 }
3449 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3450 patches.push(ScorePatch::SetHarpPedalDiagrams {
3451 part: pi,
3452 staff: si,
3453 measure: mi,
3454 value: bm.harp_pedal_diagrams.clone(),
3455 });
3456 }
3457
3458 for vi in 0..4usize {
3459 let av = &am.voices[vi];
3460 let bv = &bm.voices[vi];
3461 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3462 if !note_content_eq(a_note, b_note) {
3463 patches.push(ScorePatch::ReplaceNote {
3464 part: pi,
3465 staff: si,
3466 measure: mi,
3467 voice: vi,
3468 note_index: ni,
3469 note: Box::new(b_note.clone()),
3470 });
3471 }
3472 }
3473 for ni in (bv.len()..av.len()).rev() {
3475 patches.push(ScorePatch::RemoveNote {
3476 part: pi,
3477 staff: si,
3478 measure: mi,
3479 voice: vi,
3480 note_index: ni,
3481 });
3482 }
3483 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
3485 patches.push(ScorePatch::AddNote {
3486 part: pi,
3487 staff: si,
3488 measure: mi,
3489 voice: vi,
3490 note_index: av.len() + offset,
3491 note: Box::new(note.clone()),
3492 });
3493 }
3494 }
3495 }
3496 }
3497 }
3498
3499 patches
3500}
3501
3502pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
3507 let mut s = score.clone();
3508 for patch in patches {
3509 match patch {
3510 ScorePatch::ReplaceScore { score } => {
3511 s = (**score).clone();
3512 }
3513 ScorePatch::SetScoreViews { value } => {
3514 s.views = value.clone();
3515 }
3516 ScorePatch::SetScoreTexts { value } => {
3517 s.texts = value.clone();
3518 }
3519 ScorePatch::SetMeasureTexts {
3520 part,
3521 staff,
3522 measure,
3523 value,
3524 } => {
3525 s.parts
3526 .get_mut(*part)
3527 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3528 .staves
3529 .get_mut(*staff)
3530 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3531 .measures
3532 .get_mut(*measure)
3533 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3534 .texts = value.clone();
3535 }
3536 ScorePatch::SetFiguredBass {
3537 part,
3538 staff,
3539 measure,
3540 value,
3541 } => {
3542 s.parts
3543 .get_mut(*part)
3544 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3545 .staves
3546 .get_mut(*staff)
3547 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3548 .measures
3549 .get_mut(*measure)
3550 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3551 .figured_bass = value.clone();
3552 }
3553 ScorePatch::SetHarpPedalDiagrams {
3554 part,
3555 staff,
3556 measure,
3557 value,
3558 } => {
3559 s.parts
3560 .get_mut(*part)
3561 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3562 .staves
3563 .get_mut(*staff)
3564 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3565 .measures
3566 .get_mut(*measure)
3567 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3568 .harp_pedal_diagrams = value.clone();
3569 }
3570 ScorePatch::SetChordDefinitions { value } => {
3571 s.chord_definitions = value.clone();
3572 }
3573 ScorePatch::SetPartNames {
3574 part,
3575 name,
3576 short_name,
3577 } => {
3578 let target = s
3579 .parts
3580 .get_mut(*part)
3581 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3582 target.name = name.clone();
3583 target.short_name = short_name.clone();
3584 }
3585 ScorePatch::SetPartMidi {
3586 part,
3587 channel,
3588 program,
3589 } => {
3590 let target = s
3591 .parts
3592 .get_mut(*part)
3593 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3594 target.midi_channel = *channel;
3595 target.midi_program = *program;
3596 }
3597 ScorePatch::SetInstrumentDefinition { part, value } => {
3598 s.parts
3599 .get_mut(*part)
3600 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3601 .instrument = value.clone();
3602 }
3603 ScorePatch::SetPartMidiAutomation {
3604 part,
3605 pitch_bends,
3606 control_changes,
3607 program_changes,
3608 aftertouch,
3609 } => {
3610 let target = s
3611 .parts
3612 .get_mut(*part)
3613 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3614 target.midi_pitch_bends = pitch_bends.clone();
3615 target.midi_control_changes = control_changes.clone();
3616 target.midi_program_changes = program_changes.clone();
3617 target.midi_aftertouch = aftertouch.clone();
3618 }
3619 ScorePatch::SetStaffConfiguration {
3620 part,
3621 staff,
3622 clef,
3623 transpose_semitones,
3624 } => {
3625 let target = s
3626 .parts
3627 .get_mut(*part)
3628 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3629 .staves
3630 .get_mut(*staff)
3631 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3632 target.clef = clef.clone();
3633 target.transpose_semitones = *transpose_semitones;
3634 }
3635 ScorePatch::SetStaffPresentation { part, staff, value } => {
3636 let target = s
3637 .parts
3638 .get_mut(*part)
3639 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3640 .staves
3641 .get_mut(*staff)
3642 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3643 target.presentation = value.clone();
3644 }
3645 ScorePatch::SetMeasurePresentation {
3646 part,
3647 staff,
3648 measure,
3649 number,
3650 clef,
3651 tempo_text,
3652 navigation,
3653 expression_text,
3654 multi_rest_count,
3655 system_break,
3656 page_break,
3657 section_break,
3658 } => {
3659 let target = s
3660 .parts
3661 .get_mut(*part)
3662 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3663 .staves
3664 .get_mut(*staff)
3665 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3666 .measures
3667 .get_mut(*measure)
3668 .ok_or_else(|| {
3669 Error::InvalidPatch(format!("measure {measure} out of range"))
3670 })?;
3671 target.number = *number;
3672 target.clef = clef.clone();
3673 target.tempo_text = tempo_text.clone();
3674 target.navigation = navigation.clone();
3675 target.expression_text = expression_text.clone();
3676 target.multi_rest_count = *multi_rest_count;
3677 target.system_break = *system_break;
3678 target.page_break = *page_break;
3679 target.section_break = *section_break;
3680 }
3681 ScorePatch::SetTablatureConfig { part, staff, value } => {
3682 s.parts
3683 .get_mut(*part)
3684 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3685 .staves
3686 .get_mut(*staff)
3687 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3688 .tablature = value.clone();
3689 }
3690 ScorePatch::SetMeasureTablatureChange {
3691 part,
3692 staff,
3693 measure,
3694 value,
3695 } => {
3696 s.parts
3697 .get_mut(*part)
3698 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3699 .staves
3700 .get_mut(*staff)
3701 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3702 .measures
3703 .get_mut(*measure)
3704 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3705 .tablature_change = value.clone();
3706 }
3707 ScorePatch::SetMetadata { field, value } => match field.as_str() {
3708 "title" => s.metadata.title = value.clone(),
3709 "composer" => s.metadata.composer = value.clone(),
3710 "lyricist" => s.metadata.lyricist = value.clone(),
3711 "copyright" => s.metadata.copyright = value.clone(),
3712 "work_number" => s.metadata.work_number = value.clone(),
3713 "movement_title" => s.metadata.movement_title = value.clone(),
3714 other => {
3715 return Err(Error::InvalidPatch(format!(
3716 "unknown metadata field: {other}"
3717 )));
3718 }
3719 },
3720 ScorePatch::SetTempo { value } => {
3721 s.settings.tempo_bpm = *value;
3722 }
3723 ScorePatch::SetKeySignature {
3724 part,
3725 staff,
3726 measure,
3727 value,
3728 } => {
3729 s.parts
3730 .get_mut(*part)
3731 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3732 .staves
3733 .get_mut(*staff)
3734 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3735 .measures
3736 .get_mut(*measure)
3737 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3738 .key_sig = value.clone();
3739 }
3740 ScorePatch::SetTimeSignature {
3741 part,
3742 staff,
3743 measure,
3744 value,
3745 } => {
3746 s.parts
3747 .get_mut(*part)
3748 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3749 .staves
3750 .get_mut(*staff)
3751 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3752 .measures
3753 .get_mut(*measure)
3754 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3755 .time_sig = value.clone();
3756 }
3757 ScorePatch::SetBarlines {
3758 part,
3759 staff,
3760 measure,
3761 left,
3762 right,
3763 } => {
3764 let m = s
3765 .parts
3766 .get_mut(*part)
3767 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3768 .staves
3769 .get_mut(*staff)
3770 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3771 .measures
3772 .get_mut(*measure)
3773 .ok_or_else(|| {
3774 Error::InvalidPatch(format!("measure {measure} out of range"))
3775 })?;
3776 m.barline_left = left.clone();
3777 m.barline_right = right.clone();
3778 }
3779 ScorePatch::SetRehearsal {
3780 part,
3781 staff,
3782 measure,
3783 value,
3784 } => {
3785 s.parts
3786 .get_mut(*part)
3787 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3788 .staves
3789 .get_mut(*staff)
3790 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3791 .measures
3792 .get_mut(*measure)
3793 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3794 .rehearsal = value.clone();
3795 }
3796 ScorePatch::SetVolta {
3797 part,
3798 staff,
3799 measure,
3800 value,
3801 } => {
3802 s.parts
3803 .get_mut(*part)
3804 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3805 .staves
3806 .get_mut(*staff)
3807 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3808 .measures
3809 .get_mut(*measure)
3810 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3811 .volta = value.clone();
3812 }
3813 ScorePatch::AddNote {
3814 part,
3815 staff,
3816 measure,
3817 voice,
3818 note_index,
3819 note,
3820 } => {
3821 let v = s
3822 .parts
3823 .get_mut(*part)
3824 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3825 .staves
3826 .get_mut(*staff)
3827 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3828 .measures
3829 .get_mut(*measure)
3830 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3831 .voices
3832 .get_mut(*voice)
3833 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3834 let insert_at = if *note_index == usize::MAX {
3835 v.len()
3836 } else {
3837 *note_index
3838 };
3839 if insert_at > v.len() {
3840 return Err(Error::InvalidPatch(format!(
3841 "note_index {note_index} out of range"
3842 )));
3843 }
3844 v.insert(insert_at, *note.clone());
3845 }
3846 ScorePatch::RemoveNote {
3847 part,
3848 staff,
3849 measure,
3850 voice,
3851 note_index,
3852 } => {
3853 let v = s
3854 .parts
3855 .get_mut(*part)
3856 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3857 .staves
3858 .get_mut(*staff)
3859 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3860 .measures
3861 .get_mut(*measure)
3862 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3863 .voices
3864 .get_mut(*voice)
3865 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3866 if *note_index >= v.len() {
3867 return Err(Error::InvalidPatch(format!(
3868 "note_index {note_index} out of range"
3869 )));
3870 }
3871 v.remove(*note_index);
3872 }
3873 ScorePatch::ReplaceNote {
3874 part,
3875 staff,
3876 measure,
3877 voice,
3878 note_index,
3879 note,
3880 } => {
3881 let v = s
3882 .parts
3883 .get_mut(*part)
3884 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3885 .staves
3886 .get_mut(*staff)
3887 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3888 .measures
3889 .get_mut(*measure)
3890 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3891 .voices
3892 .get_mut(*voice)
3893 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3894 if *note_index >= v.len() {
3895 return Err(Error::InvalidPatch(format!(
3896 "note_index {note_index} out of range"
3897 )));
3898 }
3899 v[*note_index] = *note.clone();
3900 }
3901 ScorePatch::SetMeasureTempo {
3902 part,
3903 staff,
3904 measure,
3905 value,
3906 } => {
3907 s.parts
3908 .get_mut(*part)
3909 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3910 .staves
3911 .get_mut(*staff)
3912 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3913 .measures
3914 .get_mut(*measure)
3915 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3916 .tempo = *value;
3917 }
3918 ScorePatch::SetMeasureTempoRamp {
3919 part,
3920 staff,
3921 measure,
3922 value,
3923 } => {
3924 s.parts
3925 .get_mut(*part)
3926 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3927 .staves
3928 .get_mut(*staff)
3929 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3930 .measures
3931 .get_mut(*measure)
3932 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3933 .tempo_ramp_to = *value;
3934 }
3935 }
3936 }
3937 if !super::validate::validate(&s).is_valid() {
3938 return Err(Error::InvalidScore);
3939 }
3940 Ok(s)
3941}
3942
3943pub fn voice_duration_beats(voice: &[Note], measure_beats: f64) -> f64 {
3947 match voice {
3948 [only] if only.is_plain_whole_rest() => measure_beats,
3949 _ => voice.iter().map(Note::beats).sum(),
3950 }
3951}
3952
3953pub fn respell_score(score: &mut Score, prefer_flat: bool) {
3957 for part in &mut score.parts {
3958 for staff in &mut part.staves {
3959 for measure in &mut staff.measures {
3960 for voice in &mut measure.voices {
3961 for note in voice.iter_mut() {
3962 for pitch in &mut note.pitches {
3963 *pitch = pitch.respell(prefer_flat);
3964 }
3965 }
3966 }
3967 }
3968 }
3969 }
3970}
3971
3972#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3974#[serde(rename_all = "snake_case")]
3975pub enum RespellPolicy {
3976 Flat,
3978 Sharp,
3980 Key,
3982}
3983
3984pub fn respell_staff_region(
3995 score: &mut Score,
3996 part_index: usize,
3997 staff_index: usize,
3998 start_measure: usize,
3999 end_measure: usize,
4000 policy: RespellPolicy,
4001) -> Result<(usize, usize), Error> {
4002 let score_key_flat = score.settings.key_signature.fifths < 0;
4003 let part = score
4004 .parts
4005 .get_mut(part_index)
4006 .ok_or(Error::PartNotFound(part_index))?;
4007 let staff_count = part.staves.len();
4008 if staff_index >= staff_count {
4009 return Err(Error::StaffNotFound(staff_index));
4010 }
4011 let measure_count = part.staves[staff_index].measures.len();
4012 if start_measure >= end_measure || end_measure > measure_count {
4013 return Err(Error::InvalidCommand(format!(
4014 "invalid measure range {start_measure}..{end_measure}"
4015 )));
4016 }
4017 let mut prefer_flat = Vec::with_capacity(end_measure);
4018 let mut running_flat = score_key_flat;
4019 for index in 0..end_measure {
4020 let key = part.staves[staff_index].measures[index]
4021 .key_sig
4022 .as_ref()
4023 .or_else(|| {
4024 part.staves[0]
4025 .measures
4026 .get(index)
4027 .and_then(|measure| measure.key_sig.as_ref())
4028 });
4029 if let Some(key) = key {
4030 running_flat = key.fifths < 0;
4031 }
4032 prefer_flat.push(match policy {
4033 RespellPolicy::Flat => true,
4034 RespellPolicy::Sharp => false,
4035 RespellPolicy::Key => running_flat,
4036 });
4037 }
4038
4039 let measures = &mut part.staves[staff_index].measures;
4040 for (index, measure) in measures
4041 .iter_mut()
4042 .enumerate()
4043 .take(end_measure)
4044 .skip(start_measure)
4045 {
4046 for voice in &mut measure.voices {
4047 for note in voice
4048 .iter_mut()
4049 .filter(|note| !note.is_rest && !note.is_unpitched)
4050 {
4051 for pitch in &mut note.pitches {
4052 *pitch = pitch.respell(prefer_flat[index]);
4053 }
4054 }
4055 }
4056 }
4057
4058 let mut changed = (start_measure, end_measure - 1);
4059 for voice in 0..4 {
4060 for measure in start_measure..end_measure {
4061 for note in 0..measures[measure].voices[voice].len() {
4062 if measures[measure].voices[voice][note].tie_start {
4063 let last = propagate_tied_spelling(measures, voice, (measure, note), true);
4064 changed.1 = changed.1.max(last);
4065 }
4066 }
4067 }
4068 if !measures[start_measure].voices[voice].is_empty() {
4069 let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
4070 changed.0 = changed.0.min(first);
4071 }
4072 }
4073 Ok(changed)
4074}
4075
4076fn propagate_tied_spelling(
4079 measures: &mut [Measure],
4080 voice: usize,
4081 from: (usize, usize),
4082 forward: bool,
4083) -> usize {
4084 let mut current = from;
4085 loop {
4086 let note = &measures[current.0].voices[voice][current.1];
4087 if !(if forward {
4088 note.tie_start
4089 } else {
4090 note.tie_end
4091 }) {
4092 return current.0;
4093 }
4094 let neighbor = if forward {
4095 if current.1 + 1 < measures[current.0].voices[voice].len() {
4096 Some((current.0, current.1 + 1))
4097 } else {
4098 measures
4099 .get(current.0 + 1)
4100 .filter(|measure| !measure.voices[voice].is_empty())
4101 .map(|_| (current.0 + 1, 0))
4102 }
4103 } else if current.1 > 0 {
4104 Some((current.0, current.1 - 1))
4105 } else {
4106 current.0.checked_sub(1).and_then(|previous| {
4107 measures[previous].voices[voice]
4108 .len()
4109 .checked_sub(1)
4110 .map(|last| (previous, last))
4111 })
4112 };
4113 let Some(next) = neighbor else {
4114 return current.0;
4115 };
4116 let next_note = &measures[next.0].voices[voice][next.1];
4117 if !(if forward {
4118 next_note.tie_end
4119 } else {
4120 next_note.tie_start
4121 }) {
4122 return current.0;
4123 }
4124 let source = measures[current.0].voices[voice][current.1].pitches.clone();
4125 for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
4126 if let Some(spelled) = source.iter().find(|candidate| {
4127 candidate.to_midi() == pitch.to_midi()
4128 && candidate.microtone_cents == pitch.microtone_cents
4129 }) {
4130 *pitch = spelled.clone();
4131 }
4132 }
4133 current = next;
4134 }
4135}
4136
4137pub fn respell_score_to_key(score: &mut Score) {
4141 let prefer_flat = score.settings.key_signature.fifths < 0;
4142 respell_score(score, prefer_flat);
4143}
4144
4145pub fn score_duration_secs(score: &Score) -> f64 {
4150 if score.settings.tempo_bpm == 0 {
4151 return 0.0;
4152 }
4153 let seq = measure_sequence(score);
4154 let mut total_secs = 0.0f64;
4155 let mut current_bpm = score.settings.tempo_bpm as f64;
4156 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4157 for &idx in &seq {
4158 if let Some(m) = staff.measures.get(idx) {
4159 if let Some(b) = m.tempo {
4160 current_bpm = b as f64;
4161 }
4162 if current_bpm == 0.0 {
4163 continue;
4164 }
4165 let beats = voice_duration_beats(
4166 &m.voices[0],
4167 m.duration_beats(&score.settings.time_signature),
4168 );
4169 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4170 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4171 current_bpm = f64::from(target);
4172 }
4173 }
4174 }
4175 }
4176 total_secs
4177}
4178
4179pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
4184 if score.settings.tempo_bpm == 0 {
4185 return 0.0;
4186 }
4187 let seq: Vec<usize> = measure_sequence(score)
4188 .into_iter()
4189 .filter(|&idx| idx >= region.0 && idx <= region.1)
4190 .collect();
4191 let mut total_secs = 0.0f64;
4192 let mut current_bpm = score.settings.tempo_bpm as f64;
4193 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4194 for &idx in &seq {
4195 if let Some(m) = staff.measures.get(idx) {
4196 if let Some(b) = m.tempo {
4197 current_bpm = b as f64;
4198 }
4199 if current_bpm == 0.0 {
4200 continue;
4201 }
4202 let beats = voice_duration_beats(
4203 &m.voices[0],
4204 m.duration_beats(&score.settings.time_signature),
4205 );
4206 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4207 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4208 current_bpm = f64::from(target);
4209 }
4210 }
4211 }
4212 }
4213 total_secs
4214}
4215
4216fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
4217 let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
4218 return beats / start_bpm * 60.0;
4219 };
4220 let end_bpm = f64::from(target_bpm);
4221 let delta = end_bpm - start_bpm;
4222 if delta.abs() < f64::EPSILON {
4223 return beats / start_bpm * 60.0;
4224 }
4225 60.0 * beats / delta * (end_bpm / start_bpm).ln()
4226}
4227
4228pub fn measure_beats_remaining(
4233 score: &Score,
4234 part_index: usize,
4235 staff_index: usize,
4236 measure_index: usize,
4237 voice_index: usize,
4238) -> Result<f64, Error> {
4239 let part = score
4240 .parts
4241 .get(part_index)
4242 .ok_or(Error::PartNotFound(part_index))?;
4243 let staff = part
4244 .staves
4245 .get(staff_index)
4246 .ok_or(Error::StaffNotFound(staff_index))?;
4247 let measure = staff
4248 .measures
4249 .get(measure_index)
4250 .ok_or(Error::MeasureNotFound(measure_index))?;
4251 let voice = measure
4252 .voices
4253 .get(voice_index)
4254 .ok_or(Error::VoiceOutOfRange(voice_index))?;
4255 let capacity = measure.duration_beats(&score.settings.time_signature);
4256 Ok((capacity - voice_duration_beats(voice, capacity)).max(0.0))
4257}
4258
4259pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
4265 if pitches.is_empty() {
4266 return true;
4267 }
4268 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
4269 avg < clef.middle_line_midi() as f64
4270}
4271
4272fn beam_beat_size(ts: &TimeSignature) -> f64 {
4273 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
4274 3.0 * 4.0 / ts.denominator as f64
4275 } else {
4276 4.0 / ts.denominator as f64
4277 }
4278}
4279
4280pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
4285 let beat_size = beam_beat_size(time_sig);
4286 let n = notes.len();
4287 let mut result = vec![BeamState::None; n];
4288
4289 let is_beamable = |note: &Note| -> bool {
4290 !note.is_rest
4291 && matches!(
4292 note.duration,
4293 Duration::Eighth
4294 | Duration::Sixteenth
4295 | Duration::ThirtySecond
4296 | Duration::SixtyFourth
4297 )
4298 };
4299
4300 let mut starts = Vec::with_capacity(n);
4302 let mut pos = 0.0f64;
4303 for note in notes {
4304 starts.push(pos);
4305 pos += note.beats();
4306 }
4307
4308 let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
4310
4311 let mut i = 0;
4312 while i < n {
4313 if !is_beamable(¬es[i]) {
4314 i += 1;
4315 continue;
4316 }
4317 let g = group_id(i);
4318 let mut j = i;
4320 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
4321 j += 1;
4322 }
4323 let run = j - i;
4324 if run == 1 {
4325 result[i] = BeamState::None;
4326 } else {
4327 result[i] = BeamState::Begin;
4328 result[i + 1..j - 1].fill(BeamState::Continue);
4329 result[j - 1] = BeamState::End;
4330 }
4331 i = j;
4332 }
4333 result
4334}
4335
4336fn note_content_eq(a: &Note, b: &Note) -> bool {
4337 a.is_rest == b.is_rest
4338 && a.is_unpitched == b.is_unpitched
4339 && a.instrument_id == b.instrument_id
4340 && a.offset_x == b.offset_x
4341 && a.offset_y == b.offset_y
4342 && a.relative_x == b.relative_x
4343 && a.relative_y == b.relative_y
4344 && a.pitches == b.pitches
4345 && a.duration == b.duration
4346 && a.dot_count == b.dot_count
4347 && a.tie_start == b.tie_start
4348 && a.tie_end == b.tie_end
4349 && a.beam == b.beam
4350 && a.articulations == b.articulations
4351 && a.dynamic == b.dynamic
4352 && a.stem_up == b.stem_up
4353 && a.hairpin_start == b.hairpin_start
4354 && a.hairpin_end == b.hairpin_end
4355 && a.tuplet == b.tuplet
4356 && a.chord_symbol == b.chord_symbol
4357 && a.is_grace == b.is_grace
4358 && a.grace_slash == b.grace_slash
4359 && a.ottava_start == b.ottava_start
4360 && a.ottava_end == b.ottava_end
4361 && a.lyric == b.lyric
4362 && a.additional_lyrics == b.additional_lyrics
4363 && a.pedal_start == b.pedal_start
4364 && a.pedal_end == b.pedal_end
4365 && a.slur_start == b.slur_start
4366 && a.slur_end == b.slur_end
4367 && a.arpeggiate == b.arpeggiate
4368 && a.tab_position == b.tab_position
4369 && a.tab_positions == b.tab_positions
4370 && a.guitar_technique == b.guitar_technique
4371 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
4372 && a.guitar_bend_curve == b.guitar_bend_curve
4373}
4374
4375fn per_pitch_flag(flags: &[bool], pitch_count: usize, index: usize, chord: bool) -> bool {
4376 if flags.len() == pitch_count {
4377 flags.get(index).copied().unwrap_or(chord)
4378 } else {
4379 chord
4380 }
4381}
4382
4383#[cfg(test)]
4384mod tests {
4385 use super::*;
4386 use crate::model::{
4387 notation::{FingeringSelectionPolicy, TextStyle},
4388 pitch::Step,
4389 };
4390
4391 #[test]
4392 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
4393 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4394 note.fingerings = vec![3, 1, 4];
4395 note.fingering = Some(3);
4396 assert_eq!(
4397 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
4398 Some(3)
4399 );
4400 assert_eq!(
4401 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
4402 Some(1)
4403 );
4404 assert_eq!(
4405 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
4406 Some(4)
4407 );
4408 assert_eq!(note.fingerings, vec![3, 1, 4]);
4409 assert_eq!(note.fingering, Some(3));
4410 }
4411
4412 #[test]
4413 fn default_score_has_one_part_four_measures() {
4414 let score = Score::default();
4415 assert_eq!(score.parts.len(), 1);
4416 assert_eq!(score.parts[0].staves.len(), 1);
4417 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
4418 }
4419
4420 #[test]
4421 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
4422 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4423 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4424 lines: 6,
4425 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4426 capo: 2,
4427 });
4428 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
4429 Pitch::with_alter(Step::F, 4, 1),
4430 Duration::Quarter,
4431 ));
4432 score.parts[0].staves[0].measures[0].voices[0]
4433 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
4434 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
4435 Some(TabPosition { string: 6, fret: 7 });
4436
4437 assert_eq!(assign_tablature_positions(&mut score), 1);
4438 let notes = &score.parts[0].staves[0].measures[0].voices[0];
4439 assert_eq!(
4440 notes[1].tab_position,
4441 Some(TabPosition { string: 1, fret: 0 })
4442 );
4443 assert_eq!(notes[1].string_number, Some(1));
4444 assert_eq!(
4445 notes[2].tab_position,
4446 Some(TabPosition { string: 6, fret: 7 })
4447 );
4448 }
4449
4450 #[test]
4451 fn assign_tablature_positions_uses_measure_local_capo_change() {
4452 let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
4453 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4454 lines: 6,
4455 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4456 capo: 0,
4457 });
4458 score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
4459 lines: 6,
4460 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4461 capo: 2,
4462 });
4463 score.parts[0].staves[0].measures[1].voices[0]
4464 .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
4465
4466 assert_eq!(assign_tablature_positions(&mut score), 1);
4467 assert_eq!(
4468 score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
4469 Some(TabPosition { string: 5, fret: 3 })
4470 );
4471 }
4472
4473 #[test]
4474 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
4475 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4476 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4477 lines: 6,
4478 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4479 capo: 0,
4480 });
4481 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4482 chord.pitches.push(Pitch::new(Step::G, 4));
4483 score.parts[0].staves[0].measures[0].voices[0].push(chord);
4484
4485 assert_eq!(assign_tablature_positions(&mut score), 1);
4486 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
4487 assert_eq!(
4488 positions,
4489 &vec![
4490 TabPosition { string: 2, fret: 5 },
4491 TabPosition { string: 1, fret: 3 },
4492 ]
4493 );
4494 }
4495
4496 #[test]
4497 fn new_score_measure_count() {
4498 let score = Score::new("Test", 120, 4, 4, 0, 8);
4499 assert_eq!(score.measure_count(), 8);
4500 }
4501
4502 #[test]
4503 fn note_beats_quarter() {
4504 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4505 assert!((note.beats() - 1.0).abs() < 1e-9);
4506 }
4507
4508 #[test]
4509 fn note_beats_dotted_quarter() {
4510 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4511 note.dot_count = 1;
4512 assert!((note.beats() - 1.5).abs() < 1e-9);
4513 }
4514
4515 #[test]
4516 fn grace_note_beats_zero() {
4517 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
4518 note.is_grace = true;
4519 assert_eq!(note.beats(), 0.0);
4520 }
4521
4522 #[test]
4523 fn measure_empty_4_4_fills_four_beats() {
4524 let m = Measure::empty(4, 4);
4525 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4526 assert!((total - 4.0).abs() < 1e-9);
4527 }
4528
4529 #[test]
4530 fn measure_empty_3_4_fills_three_beats() {
4531 let m = Measure::empty(3, 4);
4532 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4533 assert!((total - 3.0).abs() < 1e-9);
4534 }
4535
4536 #[test]
4537 fn whole_filling_beats() {
4538 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
4539 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
4540 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
4541 }
4542
4543 #[test]
4546 fn statistics_default_score_all_rests() {
4547 let score = Score::default();
4548 let s = score.statistics();
4549 assert_eq!(s.part_count, 1);
4550 assert_eq!(s.measure_count, 4);
4551 assert_eq!(s.note_count, 0);
4552 assert!(s.rest_count > 0);
4553 }
4554
4555 #[test]
4556 fn statistics_duration_estimate() {
4557 let score = Score::new("T", 120, 4, 4, 0, 1);
4559 let s = score.statistics();
4560 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
4561 }
4562
4563 #[test]
4564 fn score_duration_secs_matches_statistics() {
4565 use super::score_duration_secs;
4566 let score = Score::new("T", 120, 4, 4, 0, 4);
4567 let secs = score_duration_secs(&score);
4568 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
4570 }
4571
4572 #[test]
4573 fn score_duration_secs_integrates_measure_tempo_ramp() {
4574 use super::score_duration_secs;
4575 let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
4576 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4577 let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
4578 assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
4579 }
4580
4581 #[test]
4582 fn score_duration_secs_zero_bpm_returns_zero() {
4583 use super::score_duration_secs;
4584 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4585 score.settings.tempo_bpm = 0;
4586 assert_eq!(score_duration_secs(&score), 0.0);
4587 }
4588
4589 #[test]
4590 fn score_duration_secs_per_measure_tempo() {
4591 use super::score_duration_secs;
4592 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4594 score.parts[0].staves[0].measures[1].tempo = Some(60);
4595 let secs = score_duration_secs(&score);
4596 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
4597 }
4598
4599 #[test]
4602 fn extract_part_returns_single_part_score() {
4603 let mut score = Score::default();
4604 let mut p2 = Part::new("Violin", "Vln.");
4605 p2.staves.push(Staff::new(Clef::Treble));
4606 score.parts.push(p2);
4607 let ex = score.extract_part(0).unwrap();
4608 assert_eq!(ex.parts.len(), 1);
4609 assert_ne!(ex.id, score.id);
4610 assert_eq!(ex.metadata.title, score.metadata.title);
4611 }
4612
4613 #[test]
4614 fn extract_part_out_of_range_is_none() {
4615 let score = Score::default();
4616 assert!(score.extract_part(99).is_none());
4617 }
4618
4619 #[test]
4620 fn extract_and_merge_remap_typed_spanner_part_addresses() {
4621 let mut left = Score::template(ScoreTemplate::StringQuartet);
4622 let address = NoteAddr {
4623 part: 1,
4624 staff: 0,
4625 measure: 0,
4626 voice: 0,
4627 note: 0,
4628 };
4629 left.spanners.push(NotationSpanner {
4630 id: "left-span".to_string(),
4631 kind: NotationSpannerKind::Slur,
4632 start: address.clone(),
4633 end: address,
4634 number: Some(1),
4635 line_type: None,
4636 text: None,
4637 placement: None,
4638 ottava_size: None,
4639 ottava_type: None,
4640 });
4641 let extracted = left.extract_part_checked(1).expect("valid extracted part");
4642 assert_eq!(extracted.spanners[0].start.part, 0);
4643 assert_eq!(extracted.spanners[0].end.part, 0);
4644
4645 let mut right = Score::new("R", 120, 4, 4, 0, 1);
4646 let right_address = NoteAddr {
4647 part: 0,
4648 staff: 0,
4649 measure: 0,
4650 voice: 0,
4651 note: 0,
4652 };
4653 right.spanners.push(NotationSpanner {
4654 id: "right-span".to_string(),
4655 kind: NotationSpannerKind::Pedal,
4656 start: right_address.clone(),
4657 end: right_address,
4658 number: Some(1),
4659 line_type: None,
4660 text: None,
4661 placement: None,
4662 ottava_size: None,
4663 ottava_type: None,
4664 });
4665 let merged = left.merge_checked(&right).expect("valid merged score");
4666 let right_span = merged
4667 .spanners
4668 .iter()
4669 .find(|spanner| spanner.id == "right-span")
4670 .expect("merged right span");
4671 assert_eq!(right_span.start.part, left.parts.len());
4672 assert_eq!(right_span.end.part, left.parts.len());
4673 }
4674
4675 #[test]
4678 fn transpose_zero_is_clone() {
4679 let score = Score::new("T", 120, 4, 4, 0, 1);
4680 let t = transpose(&score, 0);
4681 assert_eq!(t.settings.key_signature.fifths, 0);
4682 }
4683
4684 #[test]
4685 fn transpose_staff_region_rewrites_only_selected_written_measures() {
4686 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4687 for measure in &mut score.parts[0].staves[0].measures {
4688 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4689 }
4690 let transformed = transpose_staff_region_checked(
4691 &score,
4692 0,
4693 0,
4694 1,
4695 2,
4696 2,
4697 RegionalTranspositionTarget::Written,
4698 )
4699 .expect("region should transpose");
4700 assert_eq!(
4701 transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
4702 Pitch::new(Step::C, 4)
4703 );
4704 assert_eq!(
4705 transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4706 Pitch::new(Step::D, 4)
4707 );
4708 assert_eq!(
4709 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4710 Pitch::new(Step::C, 4)
4711 );
4712 }
4713
4714 #[test]
4715 fn regional_concert_transposition_requires_full_staff() {
4716 let score = Score::new("T", 120, 4, 4, 0, 2);
4717 assert!(
4718 transpose_staff_region_checked(
4719 &score,
4720 0,
4721 0,
4722 1,
4723 2,
4724 -2,
4725 RegionalTranspositionTarget::Concert,
4726 )
4727 .is_err()
4728 );
4729 }
4730
4731 #[test]
4732 fn transpose_c_major_up_2_to_d_major() {
4733 let score = Score::new("T", 120, 4, 4, 0, 1);
4734 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
4735 }
4736
4737 #[test]
4738 fn transpose_d_major_up_5_to_g_major() {
4739 let score = Score::new("T", 120, 4, 4, 2, 1);
4740 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
4741 }
4742
4743 #[test]
4744 fn transpose_c4_up_1_to_csharp4() {
4745 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
4746 assert_eq!(p.to_midi(), 61);
4747 assert_eq!(p.step, Step::C);
4748 assert_eq!(p.alter, 1);
4749 }
4750
4751 #[test]
4752 fn transpose_c4_down_1_to_b3() {
4753 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
4754 assert_eq!(p.to_midi(), 59);
4755 assert_eq!(p.step, Step::B);
4756 assert_eq!(p.alter, 0);
4757 }
4758
4759 #[test]
4760 fn transpose_up_octave_keeps_step() {
4761 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
4762 assert_eq!(p.to_midi(), 81);
4763 assert_eq!(p.step, Step::A);
4764 assert_eq!(p.octave, 5);
4765 }
4766
4767 #[test]
4768 fn statistics_with_repeat_doubles_duration() {
4769 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4771 score.parts[0].staves[0].measures[0].barline_left =
4772 crate::model::notation::Barline::RepeatStart;
4773 score.parts[0].staves[0].measures[1].barline_right =
4774 crate::model::notation::Barline::RepeatEnd;
4775 let s = score.statistics();
4776 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
4778 }
4779
4780 #[test]
4781 fn transpose_octave_boundary_b4_to_c5() {
4782 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
4784 assert_eq!(p.to_midi(), 72);
4785 assert_eq!(p.step, Step::C);
4786 assert_eq!(p.octave, 5);
4787 }
4788
4789 #[test]
4790 fn transpose_clamp_at_midi_127() {
4791 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
4793 assert_eq!(p.to_midi(), 127);
4794 }
4795
4796 #[test]
4799 fn merge_combines_parts() {
4800 let mut a = Score::new("A", 120, 4, 4, 0, 2);
4801 let b = Score::new("B", 120, 4, 4, 0, 2);
4802 let mut p2 = Part::new("Violin", "Vln.");
4804 p2.staves.push(Staff::new(Clef::Treble));
4805 for i in 0..2usize {
4806 let mut m = Measure::empty(4, 4);
4807 m.number = i as u32 + 1;
4808 p2.staves[0].measures.push(m);
4809 }
4810 a.parts.push(p2);
4811 let merged = a.merge(&b);
4812 assert_eq!(merged.parts.len(), 3);
4814 }
4815
4816 #[test]
4817 fn merge_pads_shorter_score() {
4818 let a = Score::new("A", 120, 4, 4, 0, 4);
4819 let b = Score::new("B", 120, 4, 4, 0, 2);
4820 let merged = a.merge(&b);
4821 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
4823 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
4824 }
4825
4826 #[test]
4827 fn merge_uses_self_metadata() {
4828 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
4829 a.metadata.composer = "Composer A".to_string();
4830 let b = Score::new("Title B", 120, 4, 4, 0, 2);
4831 let merged = a.merge(&b);
4832 assert_eq!(merged.metadata.title, "Title A");
4833 assert_eq!(merged.metadata.composer, "Composer A");
4834 }
4835
4836 #[test]
4837 fn merge_new_id_differs_from_both() {
4838 let a = Score::new("A", 120, 4, 4, 0, 2);
4839 let b = Score::new("B", 120, 4, 4, 0, 2);
4840 let merged = a.merge(&b);
4841 assert_ne!(merged.id, a.id);
4842 assert_ne!(merged.id, b.id);
4843 }
4844
4845 #[test]
4848 fn staff_default_transpose_is_zero() {
4849 let s = Staff::new(Clef::Treble);
4850 assert_eq!(s.transpose_semitones, 0);
4851 }
4852
4853 #[test]
4854 fn staff_presentation_defaults_and_tracks_percussion_clef() {
4855 let standard = Staff::new(Clef::Treble);
4856 assert_eq!(standard.presentation, StaffPresentation::default());
4857
4858 let percussion = Staff::new(Clef::Percussion);
4859 assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
4860 assert_eq!(percussion.presentation.lines, 5);
4861 assert!(percussion.presentation.visible);
4862 }
4863
4864 #[test]
4867 fn score_default_has_schema_version_1() {
4868 let score = Score::default();
4869 assert_eq!(score.schema_version, 1);
4870 }
4871
4872 #[test]
4873 fn score_new_has_schema_version_1() {
4874 let score = Score::new("T", 120, 4, 4, 0, 4);
4875 assert_eq!(score.schema_version, 1);
4876 }
4877
4878 #[test]
4879 fn score_without_schema_version_deserializes_to_zero() {
4880 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":[]}"#;
4881 let score: Score = serde_json::from_str(json).unwrap();
4882 assert_eq!(score.schema_version, 0);
4883 }
4884
4885 #[test]
4886 fn legacy_score_json_defaults_typed_spanners() {
4887 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4888 let mut value = serde_json::to_value(score).expect("score serializes");
4889 value
4890 .as_object_mut()
4891 .expect("score is an object")
4892 .remove("spanners");
4893 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4894 assert!(restored.spanners.is_empty());
4895 }
4896
4897 #[test]
4898 fn legacy_staff_json_defaults_presentation() {
4899 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4900 let mut value = serde_json::to_value(score).expect("score serializes");
4901 value["parts"][0]["staves"][0]
4902 .as_object_mut()
4903 .expect("staff is an object")
4904 .remove("presentation");
4905
4906 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4907 assert_eq!(
4908 restored.parts[0].staves[0].presentation,
4909 StaffPresentation::default()
4910 );
4911 }
4912
4913 #[test]
4914 fn legacy_part_json_defaults_instrument_definition() {
4915 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4916 let mut value = serde_json::to_value(score).expect("score serializes");
4917 value["parts"][0]
4918 .as_object_mut()
4919 .expect("part is an object")
4920 .remove("instrument");
4921
4922 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4923 assert!(restored.parts[0].instrument.is_none());
4924 }
4925
4926 #[test]
4927 fn resolve_view_projects_linked_part_without_mutating_source_score() {
4928 let mut score = Score::template(ScoreTemplate::StringQuartet);
4929 let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
4930 view.layout.measures_per_row = Some(2);
4931 score.views.push(view);
4932
4933 let projected = score.resolve_view("violin-2").expect("view resolves");
4934 assert_eq!(projected.parts.len(), 1);
4935 assert_eq!(projected.parts[0].name, "Violin II");
4936 assert_eq!(projected.views.len(), 1);
4937 assert_eq!(projected.views[0].parts, vec![0]);
4938 assert_eq!(score.parts.len(), 4);
4939 assert_eq!(score.views[0].parts, vec![1]);
4940 }
4941
4942 #[test]
4943 fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
4944 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4945 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4946 lines: 6,
4947 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4948 capo: 0,
4949 });
4950 score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
4951 score.views.push(ScoreView::linked_tablature_staff(
4952 "guitar-tab",
4953 "Guitar Tab",
4954 0,
4955 0,
4956 ));
4957
4958 let projected = score.resolve_view("guitar-tab").expect("view resolves");
4959 assert_eq!(
4960 projected.parts[0].staves[0].presentation.kind,
4961 StaffKind::Tablature
4962 );
4963 assert_eq!(
4964 projected.views[0].staff_kind_overrides[0].staff,
4965 ViewStaffRef { part: 0, staff: 0 }
4966 );
4967 assert_eq!(
4968 score.parts[0].staves[0].presentation.kind,
4969 StaffKind::Standard
4970 );
4971 }
4972
4973 #[test]
4974 fn legacy_score_view_json_defaults_staff_kind_overrides() {
4975 let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
4976 score.views.push(ScoreView::linked_part("part", "Part", 0));
4977 let mut value = serde_json::to_value(score).expect("score serializes");
4978 value["views"][0]
4979 .as_object_mut()
4980 .expect("view is an object")
4981 .remove("staff_kind_overrides");
4982
4983 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4984 assert!(restored.views[0].staff_kind_overrides.is_empty());
4985 }
4986
4987 #[test]
4988 fn typed_view_style_overrides_are_ordered_and_json_compatible() {
4989 let mut view = ScoreView::linked_part("part", "Part", 0);
4990 view.layout.typed_style_overrides = vec![
4991 ViewStyleOverride {
4992 property: ViewStyleProperty::TextScale,
4993 value: 0.9,
4994 },
4995 ViewStyleOverride {
4996 property: ViewStyleProperty::TextScale,
4997 value: 1.1,
4998 },
4999 ];
5000 assert_eq!(
5001 view.layout.style_value(ViewStyleProperty::TextScale),
5002 Some(1.1)
5003 );
5004 let restored: ScoreView =
5005 serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
5006 assert_eq!(
5007 restored.layout.typed_style_overrides,
5008 view.layout.typed_style_overrides
5009 );
5010 assert_eq!(view.layout.resolved_style().text_scale, 1.1);
5011 assert_eq!(view.layout.resolved_style().system_gap, 2.0);
5012 }
5013
5014 #[test]
5015 fn score_style_defaults_are_inherited_then_overridden_by_view() {
5016 let mut score = Score::new("Style", 120, 4, 4, 0, 1);
5017 score.style_overrides = vec![
5018 ViewStyleOverride {
5019 property: ViewStyleProperty::StaffSpace,
5020 value: 1.2,
5021 },
5022 ViewStyleOverride {
5023 property: ViewStyleProperty::TextScale,
5024 value: 0.9,
5025 },
5026 ];
5027 let mut view = ScoreView::linked_part("part", "Part", 0);
5028 view.layout.typed_style_overrides.push(ViewStyleOverride {
5029 property: ViewStyleProperty::TextScale,
5030 value: 1.1,
5031 });
5032 let style = score.resolved_view_style(&view.layout);
5033 assert_eq!(style.staff_space, 1.2);
5034 assert_eq!(style.text_scale, 1.1);
5035 let restored: Score =
5036 serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
5037 assert_eq!(restored.style_overrides, score.style_overrides);
5038 }
5039
5040 #[test]
5041 fn legacy_measure_json_defaults_source_voice_numbers() {
5042 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5043 let mut value = serde_json::to_value(&score).expect("score serializes");
5044 value["parts"][0]["staves"][0]["measures"][0]
5045 .as_object_mut()
5046 .expect("measure is an object")
5047 .remove("source_voice_numbers");
5048
5049 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5050 assert_eq!(
5051 restored.parts[0].staves[0].measures[0].source_voice_numbers,
5052 [None; 4]
5053 );
5054 }
5055
5056 #[test]
5057 fn legacy_measure_json_defaults_tempo_ramp() {
5058 let measure: Measure = serde_json::from_str(
5059 r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
5060 )
5061 .expect("legacy measure deserializes");
5062 assert_eq!(measure.tempo_ramp_to, None);
5063 }
5064
5065 #[test]
5066 fn note_without_new_percussion_fields_uses_serde_defaults() {
5067 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5068 let mut value = serde_json::to_value(note).unwrap();
5069 let object = value.as_object_mut().unwrap();
5070 object.remove("is_unpitched");
5071 object.remove("instrument_id");
5072 let restored: Note = serde_json::from_value(value).unwrap();
5073 assert!(!restored.is_unpitched);
5074 assert_eq!(restored.instrument_id, None);
5075 }
5076
5077 #[test]
5078 fn percussion_instrument_resolution_prefers_id_then_display_key() {
5079 let mut part = Part::new("Drums", "Dr.");
5080 part.percussion_instruments = vec![
5081 PercussionInstrument {
5082 id: "snare".to_string(),
5083 name: Some("Acoustic Snare".to_string()),
5084 midi_unpitched: Some(38),
5085 staff_position: None,
5086 notehead: None,
5087 preferred_voice: None,
5088 techniques: Vec::new(),
5089 },
5090 PercussionInstrument {
5091 id: "rim".to_string(),
5092 name: Some("Side Stick".to_string()),
5093 midi_unpitched: Some(37),
5094 staff_position: None,
5095 notehead: None,
5096 preferred_voice: None,
5097 techniques: Vec::new(),
5098 },
5099 ];
5100 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
5101 note.is_unpitched = true;
5102 assert_eq!(
5103 part.percussion_instrument_for_note(¬e)
5104 .map(|instrument| instrument.id.as_str()),
5105 Some("snare")
5106 );
5107 note.instrument_id = Some("rim".to_string());
5108 assert_eq!(
5109 part.percussion_instrument_for_note(¬e)
5110 .map(|instrument| instrument.id.as_str()),
5111 Some("rim")
5112 );
5113 note.instrument_id = Some("missing".to_string());
5114 assert!(part.percussion_instrument_for_note(¬e).is_none());
5115 note.instrument_id = None;
5116 note.is_unpitched = false;
5117 assert!(part.percussion_instrument_for_note(¬e).is_none());
5118 }
5119
5120 #[test]
5121 fn percussion_kit_extensions_are_json_backward_compatible() {
5122 let mut instrument = PercussionInstrument {
5123 id: "snare".to_string(),
5124 name: Some("Acoustic Snare".to_string()),
5125 midi_unpitched: Some(38),
5126 staff_position: Some(0),
5127 notehead: Some(NoteHead::Cross),
5128 preferred_voice: Some(1),
5129 techniques: vec!["rim-shot".to_string()],
5130 };
5131 let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
5132 let object = value
5133 .as_object_mut()
5134 .expect("percussion instrument is an object");
5135 object.remove("staff_position");
5136 object.remove("notehead");
5137 object.remove("preferred_voice");
5138 object.remove("techniques");
5139 instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
5140 assert_eq!(instrument.staff_position, None);
5141 assert_eq!(instrument.notehead, None);
5142 assert_eq!(instrument.preferred_voice, None);
5143 assert!(instrument.techniques.is_empty());
5144 }
5145
5146 #[test]
5149 fn score_template_solo_has_one_part_treble() {
5150 let score = Score::template(ScoreTemplate::Solo);
5151 assert_eq!(score.parts.len(), 1);
5152 assert_eq!(score.parts[0].staves.len(), 1);
5153 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5154 assert_eq!(score.parts[0].midi_program, 0);
5155 }
5156
5157 #[test]
5158 fn score_template_piano_has_two_staves() {
5159 let score = Score::template(ScoreTemplate::Piano);
5160 assert_eq!(score.parts.len(), 1);
5161 assert_eq!(score.parts[0].staves.len(), 2);
5162 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5163 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
5164 }
5165
5166 #[test]
5167 fn score_template_string_quartet_has_four_parts() {
5168 let score = Score::template(ScoreTemplate::StringQuartet);
5169 assert_eq!(score.parts.len(), 4);
5170 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);
5173 assert_eq!(score.parts[3].midi_program, 42);
5174 }
5175
5176 #[test]
5177 fn score_template_string_orchestra_has_five_parts() {
5178 let score = Score::template(ScoreTemplate::StringOrchestra);
5179 assert_eq!(score.parts.len(), 5);
5180 assert_eq!(score.parts[4].midi_program, 43); }
5182
5183 #[test]
5184 fn score_template_brass_quintet_has_five_parts() {
5185 let score = Score::template(ScoreTemplate::BrassQuintet);
5186 assert_eq!(score.parts.len(), 5);
5187 assert_eq!(score.parts[2].midi_program, 60); }
5189
5190 #[test]
5191 fn score_template_default_measures_are_four() {
5192 let score = Score::template(ScoreTemplate::StringQuartet);
5193 for part in &score.parts {
5194 for staff in &part.staves {
5195 assert_eq!(staff.measures.len(), 4);
5196 }
5197 }
5198 }
5199
5200 #[test]
5203 fn measure_empty_has_no_breaks() {
5204 let m = Measure::empty(4, 4);
5205 assert!(!m.system_break);
5206 assert!(!m.page_break);
5207 assert!(!m.section_break);
5208 }
5209
5210 #[test]
5211 fn section_range_uses_semantic_boundaries_not_layout_breaks() {
5212 let mut score = Score::new("sections", 120, 4, 4, 0, 6);
5213 score.parts[0].staves[0].measures[0].section_break = true;
5214 score.parts[0].staves[0].measures[2].section_break = true;
5215 score.parts[0].staves[0].measures[4].section_break = true;
5216 score.parts[0].staves[0].measures[5].section_break = true;
5217 score.parts[0].staves[0].measures[1].system_break = true;
5218 score.parts[0].staves[0].measures[3].page_break = true;
5219 assert_eq!(score.section_range(0).unwrap(), 0..=1);
5220 assert_eq!(score.section_range(3).unwrap(), 2..=3);
5221 assert_eq!(score.section_range(4).unwrap(), 4..=4);
5222 assert_eq!(score.section_range(5).unwrap(), 5..=5);
5223 assert!(score.section_range(6).is_err());
5224 }
5225
5226 #[test]
5227 fn system_break_survives_json_roundtrip() {
5228 let mut m = Measure::empty(4, 4);
5229 m.system_break = true;
5230 let json = serde_json::to_string(&m).unwrap();
5231 let m2: Measure = serde_json::from_str(&json).unwrap();
5232 assert!(m2.system_break);
5233 assert!(!m2.page_break);
5234 }
5235
5236 #[test]
5239 fn diff_identical_scores_is_empty() {
5240 let s = Score::new("T", 120, 4, 4, 0, 2);
5241 assert!(diff(&s, &s).is_empty());
5242 }
5243
5244 #[test]
5245 fn score_patch_covers_measure_semantics_and_note_insert_index() {
5246 let mut a = Score::new("T", 120, 4, 4, 0, 1);
5247 a.parts[0].staves[0].measures[0].voices[0].clear();
5248 let mut b = a.clone();
5249 let measure = &mut b.parts[0].staves[0].measures[0];
5250 measure.key_sig = Some(KeySignature {
5251 fifths: -2,
5252 mode: "major".to_string(),
5253 });
5254 measure.time_sig = Some(TimeSignature {
5255 numerator: 3,
5256 denominator: 4,
5257 });
5258 measure.barline_left = Barline::RepeatStart;
5259 measure.barline_right = Barline::RepeatEnd;
5260 measure.rehearsal = Some("A".to_string());
5261 measure.volta = Some(VoltaBracket {
5262 number: 1,
5263 kind: "begin_end".to_string(),
5264 });
5265 measure.texts.push(StyledText {
5266 style: TextStyle::RehearsalMark,
5267 text: "A".to_string(),
5268 placement: None,
5269 offset_x: None,
5270 offset_y: None,
5271 relative_x: None,
5272 relative_y: None,
5273 });
5274 measure.figured_bass.push(FiguredBassFigure {
5275 number: "6".to_string(),
5276 alter: None,
5277 prefix: None,
5278 suffix: None,
5279 extender: false,
5280 });
5281 measure.harp_pedal_diagrams.push(HarpPedalDiagram {
5282 positions: [
5283 HarpPedalPosition::Flat,
5284 HarpPedalPosition::Natural,
5285 HarpPedalPosition::Sharp,
5286 HarpPedalPosition::Natural,
5287 HarpPedalPosition::Flat,
5288 HarpPedalPosition::Sharp,
5289 HarpPedalPosition::Natural,
5290 ],
5291 placement: Some("above".to_string()),
5292 });
5293 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
5294 let expected = b.parts[0].staves[0].measures[0].clone();
5295
5296 let patches = score_patch(&a, &b);
5297 assert!(
5298 patches
5299 .iter()
5300 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
5301 );
5302 assert!(
5303 patches
5304 .iter()
5305 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
5306 );
5307 assert!(
5308 patches
5309 .iter()
5310 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
5311 );
5312 assert!(
5313 patches
5314 .iter()
5315 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
5316 );
5317 assert!(
5318 patches
5319 .iter()
5320 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
5321 );
5322 assert!(
5323 patches
5324 .iter()
5325 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
5326 );
5327 assert!(
5328 patches
5329 .iter()
5330 .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
5331 );
5332 let result = apply_patch(&a, &patches).expect("patch application failed");
5333 let result_measure = &result.parts[0].staves[0].measures[0];
5334 assert_eq!(result_measure.key_sig, expected.key_sig);
5335 assert_eq!(result_measure.time_sig, expected.time_sig);
5336 assert_eq!(result_measure.barline_left, expected.barline_left);
5337 assert_eq!(result_measure.barline_right, expected.barline_right);
5338 assert_eq!(result_measure.rehearsal, expected.rehearsal);
5339 assert_eq!(result_measure.volta, expected.volta);
5340 assert_eq!(result_measure.texts, expected.texts);
5341 assert_eq!(result_measure.figured_bass, expected.figured_bass);
5342 assert_eq!(
5343 result_measure.harp_pedal_diagrams,
5344 expected.harp_pedal_diagrams
5345 );
5346 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
5347 }
5348
5349 #[test]
5350 fn diff_and_patch_preserve_score_level_texts() {
5351 let a = Score::new("T", 120, 4, 4, 0, 1);
5352 let mut b = a.clone();
5353 b.texts.push(StyledText {
5354 style: TextStyle::Expression,
5355 text: "Prelude".to_string(),
5356 placement: Some("above".to_string()),
5357 offset_x: Some(12.0),
5358 offset_y: Some(-8.0),
5359 relative_x: None,
5360 relative_y: None,
5361 });
5362
5363 let changes = diff(&a, &b);
5364 assert!(changes.iter().any(|change| matches!(
5365 change,
5366 ScoreChange::ScoreTextChanged { old, new }
5367 if old.is_empty() && new == &b.texts
5368 )));
5369
5370 let patches = score_patch(&a, &b);
5371 assert!(patches.iter().any(|patch| matches!(
5372 patch,
5373 ScorePatch::SetScoreTexts { value } if value == &b.texts
5374 )));
5375 let result = apply_patch(&a, &patches).expect("score text patch failed");
5376 assert_eq!(result.texts, b.texts);
5377 }
5378
5379 #[test]
5380 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
5381 let a = Score::new("T", 120, 4, 4, 0, 1);
5382 let mut b = a.clone();
5383 let measure = &mut b.parts[0].staves[0].measures[0];
5384 measure.number = 8;
5385 measure.clef = Some(Clef::Bass);
5386 measure.tempo_text = Some("Allegro".to_string());
5387 measure.navigation = Some("D.S.".to_string());
5388 measure.expression_text = Some("espressivo".to_string());
5389 measure.multi_rest_count = Some(3);
5390 measure.system_break = true;
5391 measure.page_break = true;
5392
5393 let changes = diff(&a, &b);
5394 assert!(changes.iter().any(|change| matches!(
5395 change,
5396 ScoreChange::MeasurePresentationChanged {
5397 part: 0,
5398 staff: 0,
5399 measure: 0,
5400 old_number: 1,
5401 new_number: 8,
5402 old_clef: None,
5403 new_clef: Some(Clef::Bass),
5404 new_tempo_text: Some(text),
5405 new_navigation: Some(navigation),
5406 new_expression_text: Some(expression),
5407 new_multi_rest_count: Some(3),
5408 old_system_break: false,
5409 new_system_break: true,
5410 old_page_break: false,
5411 new_page_break: true,
5412 ..
5413 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5414 )));
5415 assert!(!changes.iter().any(|change| matches!(
5416 change,
5417 ScoreChange::UnrepresentedFieldChanged { path }
5418 if path == "parts[0].staves[0].measures[0].number"
5419 )));
5420
5421 let patches = score_patch(&a, &b);
5422 assert!(patches.iter().any(|patch| matches!(
5423 patch,
5424 ScorePatch::SetMeasurePresentation {
5425 part: 0,
5426 staff: 0,
5427 measure: 0,
5428 number: 8,
5429 clef: Some(Clef::Bass),
5430 tempo_text: Some(text),
5431 navigation: Some(navigation),
5432 expression_text: Some(expression),
5433 multi_rest_count: Some(3),
5434 system_break: true,
5435 page_break: true,
5436 section_break: false,
5437 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5438 )));
5439 assert!(
5440 !patches
5441 .iter()
5442 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5443 );
5444 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
5445 let decoded: Vec<ScorePatch> =
5446 serde_json::from_str(&json).expect("measure presentation patch should decode");
5447 let mut legacy_json: serde_json::Value =
5448 serde_json::from_str(&json).expect("patch JSON value");
5449 for patch in legacy_json.as_array_mut().expect("patch JSON array") {
5450 patch
5451 .as_object_mut()
5452 .expect("patch JSON object")
5453 .remove("section_break");
5454 }
5455 let legacy: Vec<ScorePatch> =
5456 serde_json::from_value(legacy_json).expect("legacy patch should decode");
5457 assert!(matches!(
5458 legacy.first(),
5459 Some(ScorePatch::SetMeasurePresentation {
5460 section_break: false,
5461 ..
5462 })
5463 ));
5464 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
5465 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
5466 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
5467 assert_eq!(
5468 serde_json::to_value(&result).expect("patched score JSON"),
5469 serde_json::to_value(&b).expect("expected score JSON")
5470 );
5471 }
5472
5473 #[test]
5474 fn chord_definition_changes_use_typed_diff_and_local_patch() {
5475 let a = Score::new("T", 120, 4, 4, 0, 1);
5476 let mut b = a.clone();
5477 b.chord_definitions.push(ChordDefinition {
5478 id: Some("c-major".to_string()),
5479 label: Some("C".to_string()),
5480 kind: Some("major".to_string()),
5481 fret_position: Some(0),
5482 tab_strings: Some("x32010".to_string()),
5483 tab_courses: None,
5484 members: Vec::new(),
5485 barres: Vec::new(),
5486 });
5487
5488 let changes = diff(&a, &b);
5489 assert!(changes.iter().any(|change| matches!(
5490 change,
5491 ScoreChange::ChordDefinitionsChanged { old, new }
5492 if old.is_empty() && new == &b.chord_definitions
5493 )));
5494 assert!(!changes.iter().any(|change| matches!(
5495 change,
5496 ScoreChange::UnrepresentedFieldChanged { path }
5497 if path == "chord_definitions"
5498 )));
5499
5500 let patches = score_patch(&a, &b);
5501 assert!(patches.iter().any(|patch| matches!(
5502 patch,
5503 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
5504 )));
5505 assert!(
5506 !patches
5507 .iter()
5508 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5509 );
5510 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
5511 let decoded: Vec<ScorePatch> =
5512 serde_json::from_str(&json).expect("chord definition patch should decode");
5513 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
5514 assert_eq!(result.chord_definitions, b.chord_definitions);
5515 }
5516
5517 #[test]
5518 fn part_name_changes_use_typed_diff_and_local_patch() {
5519 let a = Score::new("T", 120, 4, 4, 0, 1);
5520 let mut b = a.clone();
5521 b.parts[0].name = "Violin".to_string();
5522 b.parts[0].short_name = "Vln.".to_string();
5523 b.parts[0].midi_channel = 4;
5524 b.parts[0].midi_program = 40;
5525 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
5526 tick: 120,
5527 channel: 4,
5528 value: 2048,
5529 });
5530 b.parts[0].midi_control_changes.push(MidiControlChange {
5531 tick: 240,
5532 channel: 4,
5533 controller: 64,
5534 value: 127,
5535 });
5536 b.parts[0].midi_program_changes.push(MidiProgramChange {
5537 tick: 0,
5538 channel: 4,
5539 program: 40,
5540 });
5541 b.parts[0].midi_aftertouch.push(MidiAftertouch {
5542 tick: 360,
5543 channel: 4,
5544 key: Some(64),
5545 value: 80,
5546 });
5547 b.parts[0].staves[0].clef = Clef::Bass;
5548 b.parts[0].staves[0].transpose_semitones = -2;
5549
5550 let changes = diff(&a, &b);
5551 assert!(changes.iter().any(|change| matches!(
5552 change,
5553 ScoreChange::PartNamesChanged {
5554 part: 0,
5555 old_name,
5556 new_name,
5557 old_short_name,
5558 new_short_name,
5559 } if old_name == "Piano"
5560 && new_name == "Violin"
5561 && old_short_name == "Pno."
5562 && new_short_name == "Vln."
5563 )));
5564 assert!(changes.iter().any(|change| matches!(
5565 change,
5566 ScoreChange::PartMidiChanged {
5567 part: 0,
5568 old_channel: 0,
5569 new_channel: 4,
5570 old_program: 0,
5571 new_program: 40,
5572 }
5573 )));
5574 assert!(changes.iter().any(|change| matches!(
5575 change,
5576 ScoreChange::PartMidiAutomationChanged {
5577 part: 0,
5578 new_pitch_bends,
5579 new_control_changes,
5580 new_program_changes,
5581 new_aftertouch,
5582 ..
5583 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
5584 && new_control_changes == &b.parts[0].midi_control_changes
5585 && new_program_changes == &b.parts[0].midi_program_changes
5586 && new_aftertouch == &b.parts[0].midi_aftertouch
5587 )));
5588 assert!(changes.iter().any(|change| matches!(
5589 change,
5590 ScoreChange::StaffConfigurationChanged {
5591 part: 0,
5592 staff: 0,
5593 old_clef: Clef::Treble,
5594 new_clef: Clef::Bass,
5595 old_transpose_semitones: 0,
5596 new_transpose_semitones: -2,
5597 }
5598 )));
5599 assert!(!changes.iter().any(|change| matches!(
5600 change,
5601 ScoreChange::UnrepresentedFieldChanged { path }
5602 if path == "parts[0].name"
5603 )));
5604
5605 let patches = score_patch(&a, &b);
5606 assert!(patches.iter().any(|patch| matches!(
5607 patch,
5608 ScorePatch::SetPartNames {
5609 part: 0,
5610 name,
5611 short_name,
5612 } if name == "Violin" && short_name == "Vln."
5613 )));
5614 assert!(
5615 !patches
5616 .iter()
5617 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5618 );
5619 assert!(patches.iter().any(|patch| matches!(
5620 patch,
5621 ScorePatch::SetPartMidi {
5622 part: 0,
5623 channel: 4,
5624 program: 40,
5625 }
5626 )));
5627 assert!(patches.iter().any(|patch| matches!(
5628 patch,
5629 ScorePatch::SetPartMidiAutomation {
5630 part: 0,
5631 pitch_bends,
5632 control_changes,
5633 program_changes,
5634 aftertouch,
5635 } if pitch_bends == &b.parts[0].midi_pitch_bends
5636 && control_changes == &b.parts[0].midi_control_changes
5637 && program_changes == &b.parts[0].midi_program_changes
5638 && aftertouch == &b.parts[0].midi_aftertouch
5639 )));
5640 assert!(patches.iter().any(|patch| matches!(
5641 patch,
5642 ScorePatch::SetStaffConfiguration {
5643 part: 0,
5644 staff: 0,
5645 clef: Clef::Bass,
5646 transpose_semitones: -2,
5647 }
5648 )));
5649 let result = apply_patch(&a, &patches).expect("part name patch failed");
5650 assert_eq!(result.parts[0].name, b.parts[0].name);
5651 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
5652 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
5653 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
5654 assert_eq!(
5655 result.parts[0].midi_pitch_bends,
5656 b.parts[0].midi_pitch_bends
5657 );
5658 assert_eq!(
5659 result.parts[0].midi_control_changes,
5660 b.parts[0].midi_control_changes
5661 );
5662 assert_eq!(
5663 result.parts[0].midi_program_changes,
5664 b.parts[0].midi_program_changes
5665 );
5666 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
5667 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
5668 assert_eq!(
5669 result.parts[0].staves[0].transpose_semitones,
5670 b.parts[0].staves[0].transpose_semitones
5671 );
5672 }
5673
5674 #[test]
5675 fn diff_reports_measure_text_and_figured_bass_changes() {
5676 let a = Score::new("T", 120, 4, 4, 0, 1);
5677 let mut b = a.clone();
5678 let measure = &mut b.parts[0].staves[0].measures[0];
5679 measure.texts.push(StyledText {
5680 style: TextStyle::Lyrics,
5681 text: "la".to_string(),
5682 placement: None,
5683 offset_x: None,
5684 offset_y: None,
5685 relative_x: None,
5686 relative_y: None,
5687 });
5688 measure.figured_bass.push(FiguredBassFigure {
5689 number: "6".to_string(),
5690 alter: None,
5691 prefix: None,
5692 suffix: None,
5693 extender: false,
5694 });
5695
5696 let changes = diff(&a, &b);
5697 assert!(changes.iter().any(|change| matches!(
5698 change,
5699 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
5700 if old.is_empty() && new.len() == 1
5701 )));
5702 assert!(changes.iter().any(|change| matches!(
5703 change,
5704 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
5705 if old.is_empty() && new.len() == 1
5706 )));
5707 }
5708
5709 #[test]
5710 fn diff_reports_tablature_changes_and_patches_them_locally() {
5711 let a = Score::new("T", 120, 4, 4, 0, 1);
5712 let mut b = a.clone();
5713 b.parts[0].staves[0].tablature = Some(TablatureConfig {
5714 lines: 6,
5715 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5716 capo: 2,
5717 });
5718
5719 let changes = diff(&a, &b);
5720 assert!(!changes.iter().any(|change| matches!(
5721 change,
5722 ScoreChange::UnrepresentedFieldChanged { path }
5723 if path == "parts[0].staves[0].tablature"
5724 )));
5725 assert!(changes.iter().any(|change| matches!(
5726 change,
5727 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
5728 if config.lines == 6 && config.capo == 2
5729 )));
5730 let patches = score_patch(&a, &b);
5731 assert!(patches.iter().any(|patch| matches!(
5732 patch,
5733 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
5734 if config.lines == 6 && config.capo == 2
5735 )));
5736 assert_eq!(
5737 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
5738 b.parts[0].staves[0].tablature
5739 );
5740 }
5741
5742 #[test]
5743 fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
5744 let mut a = Score::new("T", 120, 4, 4, 0, 2);
5745 a.parts[0].staves[0].tablature = Some(TablatureConfig {
5746 lines: 6,
5747 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5748 capo: 0,
5749 });
5750 let mut b = a.clone();
5751 b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
5752 lines: 6,
5753 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5754 capo: 3,
5755 });
5756
5757 let changes = diff(&a, &b);
5758 assert!(changes.iter().any(|change| matches!(
5759 change,
5760 ScoreChange::TablatureChangeChanged {
5761 part: 0,
5762 staff: 0,
5763 measure: 1,
5764 old: None,
5765 new: Some(config),
5766 } if config.capo == 3
5767 )));
5768 let patches = score_patch(&a, &b);
5769 assert!(patches.iter().any(|patch| matches!(
5770 patch,
5771 ScorePatch::SetMeasureTablatureChange {
5772 part: 0,
5773 staff: 0,
5774 measure: 1,
5775 value: Some(config),
5776 } if config.capo == 3
5777 )));
5778 assert_eq!(
5779 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
5780 b.parts[0].staves[0].measures[1].tablature_change
5781 );
5782 }
5783
5784 #[test]
5785 fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
5786 let a = Score::new("Ramp", 120, 4, 4, 0, 2);
5787 let mut b = a.clone();
5788 b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
5789
5790 assert!(diff(&a, &b).iter().any(|change| matches!(
5791 change,
5792 ScoreChange::MeasureTempoRampChanged {
5793 part: 0,
5794 staff: 0,
5795 measure: 1,
5796 old: None,
5797 new: Some(72),
5798 }
5799 )));
5800 let patches = score_patch(&a, &b);
5801 assert!(patches.iter().any(|patch| matches!(
5802 patch,
5803 ScorePatch::SetMeasureTempoRamp {
5804 part: 0,
5805 staff: 0,
5806 measure: 1,
5807 value: Some(72),
5808 }
5809 )));
5810 assert_eq!(
5811 apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
5812 .tempo_ramp_to,
5813 b.parts[0].staves[0].measures[1].tempo_ramp_to
5814 );
5815 }
5816
5817 #[test]
5818 fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
5819 let a = Score::new("T", 120, 4, 4, 0, 1);
5820 let mut b = a.clone();
5821 b.parts[0].staves[0].presentation = StaffPresentation {
5822 kind: StaffKind::Percussion,
5823 lines: 1,
5824 line_distance: 1.5,
5825 small: true,
5826 cutaway: true,
5827 visible: false,
5828 notehead_scheme: StaffNoteheadScheme::PitchNames,
5829 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
5830 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
5831 };
5832
5833 let changes = diff(&a, &b);
5834 assert!(changes.iter().any(|change| matches!(
5835 change,
5836 ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
5837 if new == &b.parts[0].staves[0].presentation
5838 )));
5839 let patches = score_patch(&a, &b);
5840 assert!(patches.iter().any(|patch| matches!(
5841 patch,
5842 ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
5843 if value == &b.parts[0].staves[0].presentation
5844 )));
5845 assert!(
5846 !patches
5847 .iter()
5848 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5849 );
5850 assert_eq!(
5851 apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
5852 b.parts[0].staves[0].presentation
5853 );
5854 }
5855
5856 #[test]
5857 fn score_patch_uses_local_presentation_patch_for_display_fields() {
5858 let a = Score::new("T", 120, 4, 4, 0, 1);
5859 let mut b = a.clone();
5860 b.parts[0].name = "Piano".to_string();
5861 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
5862 let patches = score_patch(&a, &b);
5863 assert!(patches.iter().any(|patch| matches!(
5864 patch,
5865 ScorePatch::SetMeasurePresentation {
5866 part: 0,
5867 staff: 0,
5868 measure: 0,
5869 expression_text: Some(text),
5870 ..
5871 } if text == "dolce"
5872 )));
5873 assert!(
5874 !patches
5875 .iter()
5876 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5877 );
5878 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
5879 assert_eq!(result.parts[0].name, "Piano");
5880 assert_eq!(
5881 result.parts[0].staves[0].measures[0].expression_text,
5882 Some("dolce".to_string())
5883 );
5884 }
5885
5886 #[test]
5887 fn diff_detects_tempo_change() {
5888 let a = Score::new("T", 120, 4, 4, 0, 1);
5889 let mut b = a.clone();
5890 b.settings.tempo_bpm = 90;
5891 let changes = diff(&a, &b);
5892 assert_eq!(changes.len(), 1);
5893 assert!(matches!(
5894 changes[0],
5895 ScoreChange::TempoChanged { old: 120, new: 90 }
5896 ));
5897 }
5898
5899 #[test]
5900 fn diff_detects_title_change() {
5901 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
5902 let mut b = a.clone();
5903 b.metadata.title = "New Title".to_string();
5904 let changes = diff(&a, &b);
5905 assert!(
5906 changes.iter().any(
5907 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
5908 )
5909 );
5910 }
5911
5912 #[test]
5913 fn diff_detects_note_modification() {
5914 let mut a = Score::new("T", 120, 4, 4, 0, 1);
5915 a.parts[0].staves[0].measures[0].voices[0] =
5916 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5917 let mut b = a.clone();
5918 b.parts[0].staves[0].measures[0].voices[0][0] =
5919 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
5920 let changes = diff(&a, &b);
5921 assert!(
5922 changes
5923 .iter()
5924 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
5925 );
5926 }
5927
5928 #[test]
5929 fn diff_detects_part_added() {
5930 let a = Score::new("T", 120, 4, 4, 0, 1);
5931 let mut b = a.clone();
5932 let mut p = Part::new("Violin", "Vln.");
5933 p.staves.push(Staff::new(Clef::Treble));
5934 b.parts.push(p);
5935 let changes = diff(&a, &b);
5936 assert!(
5937 changes
5938 .iter()
5939 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
5940 );
5941 }
5942
5943 #[test]
5944 fn diff_detects_measure_tempo_change() {
5945 let a = Score::new("T", 120, 4, 4, 0, 2);
5946 let mut b = a.clone();
5947 b.parts[0].staves[0].measures[1].tempo = Some(60);
5948 let changes = diff(&a, &b);
5949 assert!(changes.iter().any(|c| matches!(
5950 c,
5951 ScoreChange::MeasureTempoChanged {
5952 measure: 1,
5953 old: None,
5954 new: Some(60),
5955 ..
5956 }
5957 )));
5958 }
5959
5960 #[test]
5961 fn diff_detects_barline_change() {
5962 use crate::model::notation::Barline;
5963 let a = Score::new("T", 120, 4, 4, 0, 2);
5964 let mut b = a.clone();
5965 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
5966 let changes = diff(&a, &b);
5967 assert!(
5968 changes
5969 .iter()
5970 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
5971 );
5972 }
5973
5974 #[test]
5975 fn diff_detects_rehearsal_change() {
5976 let a = Score::new("T", 120, 4, 4, 0, 2);
5977 let mut b = a.clone();
5978 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
5979 let changes = diff(&a, &b);
5980 assert!(
5981 changes
5982 .iter()
5983 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
5984 );
5985 }
5986
5987 #[test]
5988 fn diff_detects_volta_change() {
5989 use super::VoltaBracket;
5990 let a = Score::new("T", 120, 4, 4, 0, 2);
5991 let mut b = a.clone();
5992 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
5993 number: 1,
5994 kind: "begin_end".into(),
5995 });
5996 let changes = diff(&a, &b);
5997 assert!(
5998 changes
5999 .iter()
6000 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
6001 );
6002 }
6003
6004 #[test]
6005 fn diff_detects_key_signature_change() {
6006 let a = Score::new("T", 120, 4, 4, 0, 1);
6007 let mut b = a.clone();
6008 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
6010 assert!(
6011 changes
6012 .iter()
6013 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
6014 );
6015 }
6016
6017 #[test]
6018 fn measure_key_signature_uses_typed_local_patch() {
6019 let a = Score::new("T", 120, 4, 4, 0, 1);
6020 let mut b = a.clone();
6021 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
6022 fifths: 2,
6023 mode: "major".to_string(),
6024 });
6025 let changes = diff(&a, &b);
6026 assert!(!changes.iter().any(|change| matches!(
6027 change,
6028 ScoreChange::UnrepresentedFieldChanged { path }
6029 if path == "parts[0].staves[0].measures[0].key_sig"
6030 )));
6031 let patches = score_patch(&a, &b);
6032 assert!(patches.iter().any(|patch| matches!(
6033 patch,
6034 ScorePatch::SetKeySignature {
6035 part: 0,
6036 staff: 0,
6037 measure: 0,
6038 value: Some(KeySignature { fifths: 2, .. }),
6039 }
6040 )));
6041 assert!(
6042 !patches
6043 .iter()
6044 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6045 );
6046 }
6047
6048 #[test]
6049 fn diff_same_key_signature_no_change() {
6050 let a = Score::new("T", 120, 4, 4, 2, 1);
6051 let changes = diff(&a, &a);
6052 assert!(changes.is_empty());
6053 }
6054
6055 #[test]
6056 fn score_duration_secs_region_partial() {
6057 use super::score_duration_secs_region;
6058 let score = Score::new("T", 120, 4, 4, 0, 4);
6060 let secs = score_duration_secs_region(&score, (1, 2));
6061 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
6062 }
6063
6064 #[test]
6065 fn score_duration_secs_region_single_measure() {
6066 use super::score_duration_secs_region;
6067 let score = Score::new("T", 120, 4, 4, 0, 4);
6069 let secs = score_duration_secs_region(&score, (0, 0));
6070 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
6071 }
6072
6073 #[test]
6076 fn measure_beats_remaining_empty_voice_returns_full() {
6077 use super::measure_beats_remaining;
6078 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6079 score.parts[0].staves[0].measures[0].voices[0].clear();
6080 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6081 assert!(
6082 (rem - 4.0).abs() < 1e-9,
6083 "expected 4.0 remaining, got {rem}"
6084 );
6085 }
6086
6087 #[test]
6088 fn measure_beats_remaining_half_full_returns_half() {
6089 use super::measure_beats_remaining;
6090 use crate::model::pitch::Step;
6091 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6092 score.parts[0].staves[0].measures[0].voices[0] = vec![
6093 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6094 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
6095 ];
6096 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6097 assert!(
6098 (rem - 2.0).abs() < 1e-9,
6099 "expected 2.0 remaining, got {rem}"
6100 );
6101 }
6102
6103 #[test]
6104 fn measure_beats_remaining_full_voice_returns_zero() {
6105 use super::measure_beats_remaining;
6106 use crate::model::pitch::Step;
6107 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6108 score.parts[0].staves[0].measures[0].voices[0] =
6109 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
6110 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6111 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
6112 }
6113
6114 #[test]
6115 fn measure_beats_remaining_tuplet_accounting() {
6116 use super::measure_beats_remaining;
6117 use crate::model::notation::TupletInfo;
6118 use crate::model::pitch::Step;
6119 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6121 let tuplet = TupletInfo {
6122 actual_notes: 3,
6123 normal_notes: 2,
6124 };
6125 let mk = |step| {
6126 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
6127 n.tuplet = Some(tuplet.clone());
6128 n
6129 };
6130 score.parts[0].staves[0].measures[0].voices[0] =
6131 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
6132 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6133 assert!(
6134 (rem - 2.0).abs() < 1e-9,
6135 "expected 2.0 remaining (triplets used 2.0), got {rem}"
6136 );
6137 }
6138
6139 #[test]
6140 fn measure_beats_remaining_out_of_range_returns_err() {
6141 use super::measure_beats_remaining;
6142 let score = Score::new("T", 120, 4, 4, 0, 1);
6143 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
6144 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
6145 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
6146 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
6147 }
6148
6149 #[test]
6150 fn note_content_eq_ignores_id() {
6151 use crate::model::pitch::Step;
6152 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
6153 let mut b = a.clone();
6154 b.id = "different-id".to_string();
6155 assert!(note_content_eq(&a, &b));
6156 b.pitches[0] = Pitch::new(Step::D, 4);
6158 assert!(!note_content_eq(&a, &b));
6159 let mut c = a.clone();
6161 a.stem_up = Some(true);
6162 c.stem_up = Some(false);
6163 assert!(!note_content_eq(&a, &c));
6164 }
6165
6166 #[test]
6167 fn suggested_stem_up_below_middle() {
6168 use crate::model::notation::Clef;
6169 let pitches = vec![Pitch::new(Step::C, 4)];
6171 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6172 }
6173
6174 #[test]
6175 fn suggested_stem_up_above_middle() {
6176 use crate::model::notation::Clef;
6177 let pitches = vec![Pitch::new(Step::G, 5)];
6179 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6180 }
6181
6182 #[test]
6183 fn suggested_stem_up_at_middle_line() {
6184 use crate::model::notation::Clef;
6185 let pitches = vec![Pitch::new(Step::B, 4)];
6187 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6188 }
6189
6190 #[test]
6191 fn suggested_stem_up_chord() {
6192 use crate::model::notation::Clef;
6193 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
6195 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6196 }
6197
6198 #[test]
6199 fn suggested_stem_up_bass_clef() {
6200 use crate::model::notation::Clef;
6201 let pitches = vec![Pitch::new(Step::D, 3)];
6203 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
6204 let pitches2 = vec![Pitch::new(Step::C, 3)];
6206 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
6207 }
6208
6209 #[test]
6210 fn suggested_stem_up_empty_pitches() {
6211 use crate::model::notation::Clef;
6212 assert!(suggested_stem_up(&[], &Clef::Treble));
6213 }
6214
6215 fn eighth(pitch: Pitch) -> Note {
6216 Note::new(pitch, Duration::Eighth)
6217 }
6218 fn quarter(pitch: Pitch) -> Note {
6219 Note::new(pitch, Duration::Quarter)
6220 }
6221 fn rest_eighth() -> Note {
6222 Note::rest(Duration::Eighth)
6223 }
6224
6225 #[test]
6226 fn compute_beams_4_4_four_eighths() {
6227 use crate::model::notation::{Clef, TimeSignature};
6228 let _ = Clef::Treble; let ts = TimeSignature {
6230 numerator: 4,
6231 denominator: 4,
6232 };
6233 let c4 = Pitch::new(Step::C, 4);
6234 let notes = vec![
6235 eighth(c4.clone()),
6236 eighth(c4.clone()),
6237 eighth(c4.clone()),
6238 eighth(c4.clone()),
6239 ];
6240 let beams = compute_beams(¬es, &ts);
6241 assert_eq!(beams[0], BeamState::Begin);
6243 assert_eq!(beams[1], BeamState::End);
6244 assert_eq!(beams[2], BeamState::Begin);
6245 assert_eq!(beams[3], BeamState::End);
6246 }
6247
6248 #[test]
6249 fn compute_beams_4_4_all_eighth_one_group() {
6250 use crate::model::notation::TimeSignature;
6251 let ts = TimeSignature {
6252 numerator: 4,
6253 denominator: 4,
6254 };
6255 let c4 = Pitch::new(Step::C, 4);
6256 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
6258 let beams = compute_beams(¬es, &ts);
6259 assert_eq!(beams[0], BeamState::Begin);
6260 assert_eq!(beams[1], BeamState::End);
6261 }
6262
6263 #[test]
6264 fn compute_beams_quarter_not_beamed() {
6265 use crate::model::notation::TimeSignature;
6266 let ts = TimeSignature {
6267 numerator: 4,
6268 denominator: 4,
6269 };
6270 let c4 = Pitch::new(Step::C, 4);
6271 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
6272 let beams = compute_beams(¬es, &ts);
6273 assert_eq!(beams[0], BeamState::None);
6274 assert_eq!(beams[1], BeamState::None);
6275 }
6276
6277 #[test]
6278 fn compute_beams_rest_breaks_beam() {
6279 use crate::model::notation::TimeSignature;
6280 let ts = TimeSignature {
6281 numerator: 4,
6282 denominator: 4,
6283 };
6284 let c4 = Pitch::new(Step::C, 4);
6285 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
6286 let beams = compute_beams(¬es, &ts);
6287 assert_eq!(beams[0], BeamState::None);
6289 assert_eq!(beams[1], BeamState::None);
6290 assert_eq!(beams[2], BeamState::None);
6291 }
6292
6293 #[test]
6294 fn compute_beams_6_8_compound() {
6295 use crate::model::notation::TimeSignature;
6296 let ts = TimeSignature {
6297 numerator: 6,
6298 denominator: 8,
6299 };
6300 let c4 = Pitch::new(Step::C, 4);
6301 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
6303 let beams = compute_beams(¬es, &ts);
6304 assert_eq!(beams[0], BeamState::Begin);
6305 assert_eq!(beams[1], BeamState::Continue);
6306 assert_eq!(beams[2], BeamState::End);
6307 assert_eq!(beams[3], BeamState::Begin);
6308 assert_eq!(beams[4], BeamState::Continue);
6309 assert_eq!(beams[5], BeamState::End);
6310 }
6311
6312 #[test]
6313 fn compute_beams_single_eighth() {
6314 use crate::model::notation::TimeSignature;
6315 let ts = TimeSignature {
6316 numerator: 4,
6317 denominator: 4,
6318 };
6319 let c4 = Pitch::new(Step::C, 4);
6320 let notes = vec![eighth(c4.clone())];
6321 let beams = compute_beams(¬es, &ts);
6322 assert_eq!(beams[0], BeamState::None);
6323 }
6324}