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