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