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