1use super::{
2 duration::Duration,
3 notation::{
4 AccidentalDisplay, Articulation, Barline, BeamState, ChordDefinition, ChordSymbol, Clef,
5 CrossStaff, Dynamic, FiguredBassFigure, GuitarTechnique, HairpinKind, KeySignature, Lyric,
6 NoteHead, OttavaKind, StyledText, TabPosition, TablatureConfig, TimeSignature, TimeSymbol,
7 TupletBoundary, TupletInfo, VerseLyric,
8 },
9 pitch::Pitch,
10};
11use crate::Error;
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ScoreMetadata {
17 pub title: String,
18 pub composer: String,
19 pub lyricist: String,
20 pub copyright: String,
21 pub work_number: String,
22 pub movement_title: String,
23}
24
25impl Default for ScoreMetadata {
26 fn default() -> Self {
27 Self {
28 title: "Untitled Score".to_string(),
29 composer: String::new(),
30 lyricist: String::new(),
31 copyright: String::new(),
32 work_number: String::new(),
33 movement_title: String::new(),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ScoreSettings {
40 pub tempo_bpm: u16,
41 pub time_signature: TimeSignature,
42 pub key_signature: KeySignature,
43}
44
45impl Default for ScoreSettings {
46 fn default() -> Self {
47 Self {
48 tempo_bpm: 120,
49 time_signature: TimeSignature::default(),
50 key_signature: KeySignature::default(),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub enum PartGroupSymbol {
58 Bracket, Brace, Line, }
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct PartGroup {
66 pub first_part: usize,
68 pub last_part: usize,
70 pub symbol: PartGroupSymbol,
71 #[serde(default)]
73 pub barlines_connect: bool,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct StaffGroup {
83 pub first_staff: usize,
85 pub last_staff: usize,
87 pub symbol: PartGroupSymbol,
88 #[serde(default)]
90 pub barlines_connect: bool,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct Score {
95 pub id: String,
96 #[serde(default)]
98 pub schema_version: u32,
99 pub metadata: ScoreMetadata,
100 pub settings: ScoreSettings,
101 pub parts: Vec<Part>,
102 #[serde(default)]
103 pub part_groups: Vec<PartGroup>,
104 #[serde(default)]
106 pub texts: Vec<StyledText>,
107 #[serde(default)]
109 pub style_overrides: Vec<ViewStyleOverride>,
110 #[serde(default)]
112 pub object_style_overrides: Vec<ObjectStyleOverride>,
113 #[serde(default)]
115 pub chord_definitions: Vec<ChordDefinition>,
116 #[serde(default)]
118 pub spanners: Vec<NotationSpanner>,
119 #[serde(default)]
121 pub views: Vec<ScoreView>,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126pub struct ViewStaffRef {
127 pub part: usize,
128 pub staff: usize,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
133#[serde(rename_all = "snake_case")]
134pub enum ViewTranspositionPolicy {
135 #[default]
136 Written,
137 Concert,
138}
139
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
142pub struct ScoreViewLayoutOverrides {
143 #[serde(default)]
144 pub measures_per_row: Option<usize>,
145 #[serde(default)]
146 pub hidden_staves: Vec<ViewStaffRef>,
147 #[serde(default)]
148 pub system_breaks: Vec<usize>,
149 #[serde(default)]
150 pub page_breaks: Vec<usize>,
151 #[serde(default)]
153 pub style_overrides: Vec<(String, String)>,
154 #[serde(default)]
156 pub typed_style_overrides: Vec<ViewStyleOverride>,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum ViewStyleProperty {
163 StaffSpace,
164 TextScale,
165 AnnotationGap,
166 SystemGap,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
172pub struct ViewStyleOverride {
173 pub property: ViewStyleProperty,
174 pub value: f32,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case", tag = "kind")]
180pub enum ObjectStyleTarget {
181 ScoreText {
182 text_index: usize,
183 },
184 MeasureText {
185 part: usize,
186 staff: usize,
187 measure: usize,
188 text_index: usize,
189 },
190 Note {
191 address: NoteAddr,
192 },
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct StyleImportProvenance {
198 pub format: String,
199 pub source_location: String,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct ObjectStyleOverride {
205 pub target: ObjectStyleTarget,
206 pub property: ViewStyleProperty,
207 pub value: f32,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub provenance: Option<StyleImportProvenance>,
210}
211
212impl ScoreViewLayoutOverrides {
213 pub fn style_value(&self, property: ViewStyleProperty) -> Option<f32> {
215 self.typed_style_overrides
216 .iter()
217 .rev()
218 .find(|override_| override_.property == property)
219 .map(|override_| override_.value)
220 }
221
222 pub fn resolved_style(&self) -> ViewStyle {
224 let mut style = ViewStyle::default();
225 apply_style_overrides(&mut style, &self.typed_style_overrides);
226 style
227 }
228}
229
230fn apply_style_overrides(style: &mut ViewStyle, overrides: &[ViewStyleOverride]) {
231 for override_ in overrides {
232 match override_.property {
233 ViewStyleProperty::StaffSpace => style.staff_space = override_.value,
234 ViewStyleProperty::TextScale => style.text_scale = override_.value,
235 ViewStyleProperty::AnnotationGap => style.annotation_gap = override_.value,
236 ViewStyleProperty::SystemGap => style.system_gap = override_.value,
237 }
238 }
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
246pub struct ViewStyle {
247 pub staff_space: f32,
248 pub text_scale: f32,
249 pub annotation_gap: f32,
250 pub system_gap: f32,
251}
252
253impl Default for ViewStyle {
254 fn default() -> Self {
255 Self {
256 staff_space: 1.0,
257 text_scale: 1.0,
258 annotation_gap: 1.0,
259 system_gap: 2.0,
260 }
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
266pub struct ViewStaffKindOverride {
267 pub staff: ViewStaffRef,
268 pub kind: StaffKind,
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
273pub struct ScoreView {
274 pub id: String,
275 pub name: String,
276 #[serde(default)]
277 pub parts: Vec<usize>,
278 #[serde(default)]
279 pub transposition_policy: ViewTranspositionPolicy,
280 #[serde(default)]
282 pub staff_kind_overrides: Vec<ViewStaffKindOverride>,
283 #[serde(default)]
284 pub layout: ScoreViewLayoutOverrides,
285}
286
287impl ScoreView {
288 pub fn linked_part(id: impl Into<String>, name: impl Into<String>, part: usize) -> Self {
290 Self {
291 id: id.into(),
292 name: name.into(),
293 parts: vec![part],
294 transposition_policy: ViewTranspositionPolicy::Written,
295 staff_kind_overrides: Vec::new(),
296 layout: ScoreViewLayoutOverrides::default(),
297 }
298 }
299
300 pub fn linked_tablature_staff(
302 id: impl Into<String>,
303 name: impl Into<String>,
304 part: usize,
305 staff: usize,
306 ) -> Self {
307 let mut view = Self::linked_part(id, name, part);
308 view.staff_kind_overrides.push(ViewStaffKindOverride {
309 staff: ViewStaffRef { part, staff },
310 kind: StaffKind::Tablature,
311 });
312 view
313 }
314
315 pub fn linked_standard_staff(
317 id: impl Into<String>,
318 name: impl Into<String>,
319 part: usize,
320 staff: usize,
321 ) -> Self {
322 let mut view = Self::linked_part(id, name, part);
323 view.staff_kind_overrides.push(ViewStaffKindOverride {
324 staff: ViewStaffRef { part, staff },
325 kind: StaffKind::Standard,
326 });
327 view
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
333pub enum NotationSpannerKind {
334 Slur,
335 Glissando,
336 TrillLine,
337 Pedal,
338 Ottava,
339 Dashes,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346pub struct NotationSpanner {
347 pub id: String,
348 pub kind: NotationSpannerKind,
349 pub start: NoteAddr,
350 pub end: NoteAddr,
351 #[serde(default)]
352 pub number: Option<u16>,
353 #[serde(default)]
354 pub line_type: Option<String>,
355 #[serde(default)]
356 pub text: Option<String>,
357 #[serde(default)]
358 pub placement: Option<String>,
359 #[serde(default)]
360 pub ottava_size: Option<u8>,
361 #[serde(default)]
363 pub ottava_type: Option<String>,
364}
365
366impl Default for Score {
367 fn default() -> Self {
368 let mut part = Part::new("Piano", "Pno.");
369 part.staves.push(Staff::new(Clef::Treble));
370 for _ in 0..4 {
371 part.staves[0].measures.push(Measure::empty(4, 4));
372 }
373 Self {
374 id: Uuid::new_v4().to_string(),
375 schema_version: 1,
376 metadata: ScoreMetadata::default(),
377 settings: ScoreSettings::default(),
378 parts: vec![part],
379 part_groups: Vec::new(),
380 texts: Vec::new(),
381 style_overrides: Vec::new(),
382 object_style_overrides: Vec::new(),
383 chord_definitions: Vec::new(),
384 spanners: Vec::new(),
385 views: Vec::new(),
386 }
387 }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
392pub enum ScoreTemplate {
393 Solo,
395 Piano,
397 StringQuartet,
399 StringOrchestra,
401 BrassQuintet,
403}
404
405impl Score {
406 pub fn playback_measure_beats(&self, measure_index: usize) -> f64 {
409 for part in &self.parts {
410 for staff in &part.staves {
411 if let Some(beats) = staff
412 .measures
413 .get(measure_index)
414 .and_then(|measure| measure.actual_length)
415 .and_then(|length| length.beats())
416 {
417 return beats;
418 }
419 }
420 }
421 self.parts
422 .iter()
423 .flat_map(|part| &part.staves)
424 .find(|staff| staff.measures.get(measure_index).is_some())
425 .map_or_else(
426 || self.settings.time_signature.total_beats(),
427 |staff| staff.measure_beats(measure_index, &self.settings.time_signature),
428 )
429 }
430
431 pub fn with_legacy_spanner_flags(&self) -> std::borrow::Cow<'_, Score> {
437 use crate::model::notation::OttavaKind;
438 let missing = |spanner: &NotationSpanner| {
439 let start = self.note_at(&spanner.start);
440 let end = self.note_at(&spanner.end);
441 match spanner.kind {
442 NotationSpannerKind::Slur => {
443 !start.is_some_and(|n| n.slur_start) || !end.is_some_and(|n| n.slur_end)
444 }
445 NotationSpannerKind::Glissando => {
446 !start.is_some_and(|n| n.glissando_start)
447 || !end.is_some_and(|n| n.glissando_end)
448 }
449 NotationSpannerKind::TrillLine => {
450 !start.is_some_and(|n| n.trill_line_start)
451 || !end.is_some_and(|n| n.trill_line_end)
452 }
453 NotationSpannerKind::Pedal => {
454 !start.is_some_and(|n| n.pedal_start) || !end.is_some_and(|n| n.pedal_end)
455 }
456 NotationSpannerKind::Ottava => {
457 !start.is_some_and(|n| n.ottava_start.is_some())
458 || !end.is_some_and(|n| n.ottava_end)
459 }
460 NotationSpannerKind::Dashes => false,
462 }
463 };
464 if !self.spanners.iter().any(missing) {
465 return std::borrow::Cow::Borrowed(self);
466 }
467 let mut score = self.clone();
468 for spanner in &self.spanners {
469 if let Some(note) = score.note_at_mut(&spanner.start) {
470 match spanner.kind {
471 NotationSpannerKind::Slur => note.slur_start = true,
472 NotationSpannerKind::Glissando => note.glissando_start = true,
473 NotationSpannerKind::TrillLine => note.trill_line_start = true,
474 NotationSpannerKind::Pedal => note.pedal_start = true,
475 NotationSpannerKind::Ottava if note.ottava_start.is_none() => {
476 let two = spanner.ottava_size.is_some_and(|size| size >= 15);
477 let up = spanner.ottava_type.as_deref() == Some("up");
479 note.ottava_start = Some(match (up, two) {
480 (false, false) => OttavaKind::Va8,
481 (false, true) => OttavaKind::Ma15,
482 (true, false) => OttavaKind::Vb8,
483 (true, true) => OttavaKind::Mb15,
484 });
485 }
486 NotationSpannerKind::Ottava | NotationSpannerKind::Dashes => {}
487 }
488 }
489 if let Some(note) = score.note_at_mut(&spanner.end) {
490 match spanner.kind {
491 NotationSpannerKind::Slur => note.slur_end = true,
492 NotationSpannerKind::Glissando => note.glissando_end = true,
493 NotationSpannerKind::TrillLine => note.trill_line_end = true,
494 NotationSpannerKind::Pedal => note.pedal_end = true,
495 NotationSpannerKind::Ottava => note.ottava_end = true,
496 NotationSpannerKind::Dashes => {}
497 }
498 }
499 }
500 std::borrow::Cow::Owned(score)
501 }
502
503 fn note_at(&self, address: &NoteAddr) -> Option<&Note> {
504 self.parts
505 .get(address.part)?
506 .staves
507 .get(address.staff)?
508 .measures
509 .get(address.measure)?
510 .voices
511 .get(address.voice)?
512 .get(address.note)
513 }
514
515 fn note_at_mut(&mut self, address: &NoteAddr) -> Option<&mut Note> {
516 self.parts
517 .get_mut(address.part)?
518 .staves
519 .get_mut(address.staff)?
520 .measures
521 .get_mut(address.measure)?
522 .voices
523 .get_mut(address.voice)?
524 .get_mut(address.note)
525 }
526
527 pub fn resolved_view_style(&self, layout: &ScoreViewLayoutOverrides) -> ViewStyle {
529 let mut style = ViewStyle::default();
530 apply_style_overrides(&mut style, &self.style_overrides);
531 apply_style_overrides(&mut style, &layout.typed_style_overrides);
532 style
533 }
534
535 pub fn new(
536 title: &str,
537 tempo_bpm: u16,
538 numerator: u8,
539 denominator: u8,
540 fifths: i8,
541 measure_count: u32,
542 ) -> Self {
543 let mut score = Score::default();
544 score.metadata.title = title.to_string();
545 score.settings.tempo_bpm = tempo_bpm;
546 score.settings.time_signature = TimeSignature {
547 numerator,
548 denominator,
549 };
550 score.settings.key_signature = KeySignature {
551 fifths,
552 mode: "major".to_string(),
553 };
554
555 score.parts[0].staves[0].measures.clear();
556 for i in 0..measure_count {
557 let mut m = Measure::empty(numerator, denominator);
558 m.number = i + 1;
559 score.parts[0].staves[0].measures.push(m);
560 }
561 score
562 }
563
564 pub fn template(kind: ScoreTemplate) -> Self {
569 fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
570 (0..count)
571 .map(|i| {
572 let mut m = Measure::empty(num, den);
573 m.number = i + 1;
574 m
575 })
576 .collect()
577 }
578 fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
579 let mut p = Part::new(name, short);
580 p.midi_program = program;
581 let mut s = Staff::new(clef);
582 s.measures = measures(4, 4, 4);
583 p.staves.push(s);
584 p
585 }
586
587 let mut score = Score {
588 id: uuid::Uuid::new_v4().to_string(),
589 schema_version: 1,
590 metadata: ScoreMetadata::default(),
591 settings: ScoreSettings::default(),
592 parts: Vec::new(),
593 part_groups: Vec::new(),
594 texts: Vec::new(),
595 style_overrides: Vec::new(),
596 object_style_overrides: Vec::new(),
597 chord_definitions: Vec::new(),
598 spanners: Vec::new(),
599 views: Vec::new(),
600 };
601
602 match kind {
603 ScoreTemplate::Solo => {
604 score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
605 }
606 ScoreTemplate::Piano => {
607 let mut p = Part::new("Piano", "Pno.");
608 p.midi_program = 0;
609 let mut treble = Staff::new(Clef::Treble);
610 treble.measures = measures(4, 4, 4);
611 let mut bass = Staff::new(Clef::Bass);
612 bass.measures = measures(4, 4, 4);
613 p.staves.push(treble);
614 p.staves.push(bass);
615 score.parts.push(p);
616 }
617 ScoreTemplate::StringQuartet => {
618 score
619 .parts
620 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
621 score
622 .parts
623 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
624 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
625 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
626 }
627 ScoreTemplate::StringOrchestra => {
628 score
629 .parts
630 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
631 score
632 .parts
633 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
634 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
635 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
636 score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
637 }
638 ScoreTemplate::BrassQuintet => {
639 score
640 .parts
641 .push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
642 score
643 .parts
644 .push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
645 score
646 .parts
647 .push(part("French Horn", "Hn.", Clef::Treble, 60));
648 score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
649 score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
650 }
651 }
652 score
653 }
654
655 pub fn measure_count(&self) -> usize {
656 self.parts
657 .first()
658 .and_then(|p| p.staves.first())
659 .map(|s| s.measures.len())
660 .unwrap_or(0)
661 }
662
663 pub fn section_range(
668 &self,
669 measure_index: usize,
670 ) -> Result<std::ops::RangeInclusive<usize>, Error> {
671 let measures = self
672 .parts
673 .first()
674 .and_then(|part| part.staves.first())
675 .map(|staff| &staff.measures)
676 .ok_or(Error::MeasureNotFound(measure_index))?;
677 if measure_index >= measures.len() {
678 return Err(Error::MeasureNotFound(measure_index));
679 }
680 let start = (0..=measure_index)
681 .rev()
682 .find(|&index| measures[index].section_break)
683 .unwrap_or(0);
684 let end = ((measure_index + 1)..measures.len())
685 .find(|&index| measures[index].section_break)
686 .map(|index| index - 1)
687 .unwrap_or(measures.len() - 1);
688 Ok(start..=end)
689 }
690
691 pub fn statistics(&self) -> ScoreStats {
693 let measure_count = self.measure_count();
694 let part_count = self.parts.len();
695
696 let seq = measure_sequence(self);
698 let total_beats: f64 = self
699 .parts
700 .first()
701 .and_then(|p| p.staves.first())
702 .map(|s| {
703 seq.iter()
704 .filter_map(|&idx| s.measures.get(idx))
705 .flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
706 .map(|n| n.beats())
707 .sum()
708 })
709 .unwrap_or(0.0);
710
711 let mut note_count = 0usize;
712 let mut rest_count = 0usize;
713 for part in &self.parts {
714 for staff in &part.staves {
715 for measure in &staff.measures {
716 for voice in &measure.voices {
717 for note in voice {
718 if note.is_rest {
719 rest_count += 1;
720 } else {
721 note_count += 1;
722 }
723 }
724 }
725 }
726 }
727 }
728
729 let bpm = self.settings.tempo_bpm as f64;
730 let estimated_duration_secs = if bpm > 0.0 {
731 total_beats / bpm * 60.0
732 } else {
733 0.0
734 };
735
736 ScoreStats {
737 measure_count,
738 note_count,
739 rest_count,
740 part_count,
741 estimated_duration_secs,
742 }
743 }
744
745 pub fn resolve_view(&self, view_id: &str) -> Result<Score, Error> {
751 let view = self
752 .views
753 .iter()
754 .find(|view| view.id == view_id)
755 .ok_or_else(|| {
756 Error::InvalidCommand(format!("score view '{view_id}' does not exist"))
757 })?;
758 let mut source_to_target = vec![None; self.parts.len()];
759 let mut parts = Vec::with_capacity(view.parts.len());
760 for &source_index in &view.parts {
761 let source = self
762 .parts
763 .get(source_index)
764 .ok_or(Error::PartNotFound(source_index))?;
765 if source_to_target[source_index].is_some() {
766 return Err(Error::InvalidCommand(format!(
767 "score view '{}' selects part {} more than once",
768 view.id, source_index
769 )));
770 }
771 source_to_target[source_index] = Some(parts.len());
772 parts.push(source.clone());
773 }
774 for override_ in &view.staff_kind_overrides {
775 let target_part = source_to_target
776 .get(override_.staff.part)
777 .copied()
778 .flatten()
779 .ok_or_else(|| {
780 Error::InvalidCommand(
781 "view overrides a staff outside its selected parts".into(),
782 )
783 })?;
784 let target_staff = parts[target_part]
785 .staves
786 .get_mut(override_.staff.staff)
787 .ok_or_else(|| {
788 Error::InvalidCommand(format!(
789 "view overrides staff {} outside part {}",
790 override_.staff.staff, override_.staff.part
791 ))
792 })?;
793 if override_.kind == StaffKind::Tablature && target_staff.tablature.is_none() {
794 return Err(Error::InvalidCommand(
795 "tablature view requires a tablature configuration on its source staff".into(),
796 ));
797 }
798 target_staff.presentation.kind = override_.kind;
799 }
800 let spanners = self
801 .spanners
802 .iter()
803 .filter_map(|spanner| {
804 let start = source_to_target
805 .get(spanner.start.part)
806 .copied()
807 .flatten()?;
808 let end = source_to_target.get(spanner.end.part).copied().flatten()?;
809 let mut projected = spanner.clone();
810 projected.start.part = start;
811 projected.end.part = end;
812 Some(projected)
813 })
814 .collect();
815 let part_groups = self
816 .part_groups
817 .iter()
818 .filter_map(|group| {
819 let first = source_to_target.get(group.first_part).copied().flatten()?;
820 let last = source_to_target.get(group.last_part).copied().flatten()?;
821 Some(PartGroup {
822 first_part: first,
823 last_part: last,
824 symbol: group.symbol.clone(),
825 barlines_connect: group.barlines_connect,
826 })
827 })
828 .collect();
829 let mut projected_view = view.clone();
830 projected_view.parts = (0..parts.len()).collect();
831 for reference in &mut projected_view.layout.hidden_staves {
832 reference.part = source_to_target
833 .get(reference.part)
834 .copied()
835 .flatten()
836 .ok_or_else(|| {
837 Error::InvalidCommand("view hides a part outside its selection".into())
838 })?;
839 }
840 for override_ in &mut projected_view.staff_kind_overrides {
841 override_.staff.part = source_to_target
842 .get(override_.staff.part)
843 .copied()
844 .flatten()
845 .ok_or_else(|| {
846 Error::InvalidCommand("view overrides a staff outside its selection".into())
847 })?;
848 }
849 Ok(Score {
850 id: Uuid::new_v4().to_string(),
851 schema_version: self.schema_version,
852 metadata: self.metadata.clone(),
853 settings: self.settings.clone(),
854 parts,
855 part_groups,
856 texts: self.texts.clone(),
857 style_overrides: self.style_overrides.clone(),
858 object_style_overrides: self.object_style_overrides.clone(),
859 chord_definitions: self.chord_definitions.clone(),
860 spanners,
861 views: vec![projected_view],
862 })
863 }
864
865 pub fn extract_part(&self, part_index: usize) -> Option<Score> {
868 let part = self.parts.get(part_index)?.clone();
869 let spanners = self
870 .spanners
871 .iter()
872 .filter(|spanner| spanner.start.part == part_index && spanner.end.part == part_index)
873 .cloned()
874 .map(|mut spanner| {
875 spanner.start.part = 0;
876 spanner.end.part = 0;
877 spanner
878 })
879 .collect();
880 Some(Score {
881 id: Uuid::new_v4().to_string(),
882 schema_version: 1,
883 metadata: self.metadata.clone(),
884 settings: self.settings.clone(),
885 parts: vec![part],
886 part_groups: Vec::new(),
887 texts: self.texts.clone(),
888 style_overrides: self.style_overrides.clone(),
889 object_style_overrides: self.object_style_overrides.clone(),
890 chord_definitions: self.chord_definitions.clone(),
891 spanners,
892 views: Vec::new(),
893 })
894 }
895
896 pub fn extract_part_checked(&self, part_index: usize) -> Result<Score, Error> {
898 if !super::validate::validate(self).is_valid() {
899 return Err(Error::InvalidScore);
900 }
901 let extracted = self
902 .extract_part(part_index)
903 .ok_or(Error::PartNotFound(part_index))?;
904 if !super::validate::validate(&extracted).is_valid() {
905 return Err(Error::InvalidScore);
906 }
907 Ok(extracted)
908 }
909
910 pub fn merge(&self, other: &Score) -> Score {
914 let self_count = self.measure_count();
915 let other_count = other.measure_count();
916 let max_count = self_count.max(other_count);
917 let ts = self.settings.time_signature.clone();
918
919 let pad = |mut part: Part, from: usize| -> Part {
920 for staff in &mut part.staves {
921 for i in from..max_count {
922 let mut m = Measure::empty(ts.numerator, ts.denominator);
923 m.number = i as u32 + 1;
924 staff.measures.push(m);
925 }
926 }
927 part
928 };
929
930 let mut parts: Vec<Part> = self
931 .parts
932 .iter()
933 .cloned()
934 .map(|p| pad(p, self_count))
935 .collect();
936 for p in &other.parts {
937 parts.push(pad(p.clone(), other_count));
938 }
939 let self_part_count = self.parts.len();
940 let mut spanners = self.spanners.clone();
941 spanners.extend(other.spanners.iter().cloned().map(|mut spanner| {
942 spanner.start.part += self_part_count;
943 spanner.end.part += self_part_count;
944 spanner
945 }));
946
947 Score {
948 id: Uuid::new_v4().to_string(),
949 schema_version: 1,
950 metadata: self.metadata.clone(),
951 settings: self.settings.clone(),
952 parts,
953 part_groups: Vec::new(),
954 texts: self.texts.clone(),
955 style_overrides: self.style_overrides.clone(),
956 object_style_overrides: self.object_style_overrides.clone(),
957 chord_definitions: self.chord_definitions.clone(),
958 spanners,
959 views: Vec::new(),
960 }
961 }
962
963 pub fn merge_checked(&self, other: &Score) -> Result<Score, Error> {
965 if !super::validate::validate(self).is_valid()
966 || !super::validate::validate(other).is_valid()
967 {
968 return Err(Error::InvalidScore);
969 }
970 let merged = self.merge(other);
971 if !super::validate::validate(&merged).is_valid() {
972 return Err(Error::InvalidScore);
973 }
974 Ok(merged)
975 }
976}
977
978#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
980pub struct ScoreStats {
981 pub measure_count: usize,
982 pub note_count: usize,
984 pub rest_count: usize,
985 pub part_count: usize,
986 pub estimated_duration_secs: f64,
988}
989
990use super::pitch::Step;
993use super::repeat::measure_sequence;
994
995pub fn assign_tablature_positions(score: &mut Score) -> usize {
1007 const MAX_FRET: i16 = 24;
1008 let mut assigned = 0;
1009
1010 for part in &mut score.parts {
1011 for staff in &mut part.staves {
1012 if staff.tablature.is_none() {
1013 continue;
1014 }
1015 let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
1016 .map(|measure_index| staff.tablature_at(measure_index))
1017 .collect();
1018
1019 for (measure_index, measure) in staff.measures.iter_mut().enumerate() {
1020 let Some(tab) = configurations[measure_index].as_ref() else {
1021 continue;
1022 };
1023 for voice in &mut measure.voices {
1024 for note in voice.iter_mut() {
1025 if note.is_rest
1026 || note.tab_position.is_some()
1027 || !note.tab_positions.is_empty()
1028 || note.pitches.is_empty()
1029 {
1030 continue;
1031 }
1032 let pitches: Vec<i16> = note.pitches.iter().map(Pitch::to_midi).collect();
1033 let Some(positions) = best_tablature_assignment(
1034 &pitches,
1035 &tab.tuning_midi,
1036 tab.lines as usize,
1037 i16::from(tab.capo),
1038 MAX_FRET,
1039 ) else {
1040 continue;
1041 };
1042
1043 note.tab_position = positions.first().cloned();
1044 note.tab_positions = positions;
1045 note.string_number = note.tab_position.as_ref().map(|p| p.string);
1046 assigned += 1;
1047 }
1048 }
1049 }
1050 }
1051 }
1052
1053 assigned
1054}
1055
1056pub fn optimize_tablature_positions(score: &mut Score) -> usize {
1063 const MAX_FRET: i16 = 24;
1064 let mut assigned = 0;
1065
1066 for part in &mut score.parts {
1067 for staff in &mut part.staves {
1068 if staff.tablature.is_none() {
1069 continue;
1070 }
1071 let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
1072 .map(|measure_index| staff.tablature_at(measure_index))
1073 .collect();
1074 for voice in 0..4 {
1075 let mut locations = Vec::new();
1077 for (measure_index, measure) in staff.measures.iter().enumerate() {
1078 for (note_index, note) in measure.voices[voice].iter().enumerate() {
1079 if !note.is_rest && !note.pitches.is_empty() {
1080 locations.push((measure_index, note_index, note.clone()));
1081 }
1082 }
1083 }
1084 if locations.is_empty() {
1085 continue;
1086 }
1087 let candidates: Vec<Vec<Vec<TabPosition>>> = locations
1088 .iter()
1089 .map(|(measure_index, _, note)| {
1090 if let Some(positions) = if !note.tab_positions.is_empty() {
1091 Some(note.tab_positions.clone())
1092 } else {
1093 note.tab_position.clone().map(|position| vec![position])
1094 } {
1095 vec![positions]
1096 } else {
1097 let Some(tab) = configurations[*measure_index].as_ref() else {
1098 return Vec::new();
1099 };
1100 tablature_assignments(
1101 ¬e.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
1102 &tab.tuning_midi,
1103 tab.lines as usize,
1104 i16::from(tab.capo),
1105 MAX_FRET,
1106 )
1107 }
1108 })
1109 .collect();
1110 if candidates.iter().any(Vec::is_empty) {
1111 continue;
1112 }
1113
1114 let mut costs: Vec<Vec<(u32, Option<usize>)>> = candidates
1115 .iter()
1116 .map(|events| vec![(u32::MAX, None); events.len()])
1117 .collect();
1118 for (candidate_index, candidate) in candidates[0].iter().enumerate() {
1119 costs[0][candidate_index] = (tablature_load(candidate), None);
1120 }
1121 for event_index in 1..candidates.len() {
1122 for (candidate_index, candidate) in candidates[event_index].iter().enumerate() {
1123 let load = tablature_load(candidate);
1124 for (previous_index, previous) in
1125 candidates[event_index - 1].iter().enumerate()
1126 {
1127 let previous_cost = costs[event_index - 1][previous_index].0;
1128 let cost = previous_cost
1129 .saturating_add(load)
1130 .saturating_add(tablature_movement(previous, candidate));
1131 if cost < costs[event_index][candidate_index].0 {
1132 costs[event_index][candidate_index] = (cost, Some(previous_index));
1133 }
1134 }
1135 }
1136 }
1137 let mut selected = vec![0; candidates.len()];
1138 if let Some((last, _)) = costs.last().and_then(|row| {
1139 row.iter()
1140 .enumerate()
1141 .min_by_key(|(index, (cost, _))| (*cost, *index))
1142 }) {
1143 selected[candidates.len() - 1] = last;
1144 for event_index in (1..candidates.len()).rev() {
1145 selected[event_index - 1] =
1146 costs[event_index][selected[event_index]].1.unwrap_or(0);
1147 }
1148 }
1149
1150 for (((measure_index, note_index, original), event_candidates), selected_index) in
1151 locations.into_iter().zip(candidates).zip(selected)
1152 {
1153 if original.tab_position.is_none() && original.tab_positions.is_empty() {
1154 let note = &mut staff.measures[measure_index].voices[voice][note_index];
1155 let positions = event_candidates[selected_index].clone();
1156 note.tab_position = positions.first().cloned();
1157 note.tab_positions = positions;
1158 note.string_number = note.tab_position.as_ref().map(|p| p.string);
1159 assigned += 1;
1160 }
1161 }
1162 }
1163 }
1164 }
1165
1166 assigned
1167}
1168
1169fn tablature_load(positions: &[TabPosition]) -> u32 {
1170 let sum: u32 = positions
1171 .iter()
1172 .map(|position| u32::from(position.fret))
1173 .sum();
1174 let min = positions
1175 .iter()
1176 .map(|position| position.fret)
1177 .min()
1178 .unwrap_or(0);
1179 let max = positions
1180 .iter()
1181 .map(|position| position.fret)
1182 .max()
1183 .unwrap_or(0);
1184 let span = max - min;
1185 let stretch_penalty = span.saturating_sub(4) as u32 * 12;
1188 sum + u32::from(span) * 2 + stretch_penalty
1189}
1190
1191fn tablature_movement(previous: &[TabPosition], current: &[TabPosition]) -> u32 {
1192 previous
1193 .iter()
1194 .zip(current)
1195 .map(|(a, b)| u32::from(a.fret.abs_diff(b.fret)) + u32::from(a.string.abs_diff(b.string)))
1196 .sum()
1197}
1198
1199fn tablature_assignments(
1200 pitches: &[i16],
1201 tuning: &[i16],
1202 lines: usize,
1203 capo: i16,
1204 max_fret: i16,
1205) -> Vec<Vec<TabPosition>> {
1206 #[allow(clippy::too_many_arguments)]
1207 fn visit(
1208 pitches: &[i16],
1209 tuning: &[i16],
1210 lines: usize,
1211 capo: i16,
1212 max_fret: i16,
1213 index: usize,
1214 used: &mut [bool],
1215 current: &mut Vec<TabPosition>,
1216 output: &mut Vec<Vec<TabPosition>>,
1217 ) {
1218 if index == pitches.len() {
1219 output.push(current.clone());
1220 return;
1221 }
1222 for (string, open) in tuning.iter().enumerate().take(lines) {
1223 if used[string] {
1224 continue;
1225 }
1226 let fret = pitches[index] - *open - capo;
1227 if !(0..=max_fret).contains(&fret) {
1228 continue;
1229 }
1230 used[string] = true;
1231 current.push(TabPosition {
1232 string: (string + 1) as u8,
1233 fret: fret as u8,
1234 });
1235 visit(
1236 pitches,
1237 tuning,
1238 lines,
1239 capo,
1240 max_fret,
1241 index + 1,
1242 used,
1243 current,
1244 output,
1245 );
1246 current.pop();
1247 used[string] = false;
1248 }
1249 }
1250
1251 if pitches.is_empty() || pitches.len() > lines {
1252 return Vec::new();
1253 }
1254 let mut output = Vec::new();
1255 visit(
1256 pitches,
1257 tuning,
1258 lines,
1259 capo,
1260 max_fret,
1261 0,
1262 &mut vec![false; lines],
1263 &mut Vec::new(),
1264 &mut output,
1265 );
1266 output
1267}
1268
1269fn best_tablature_assignment(
1270 pitches: &[i16],
1271 tuning: &[i16],
1272 lines: usize,
1273 capo: i16,
1274 max_fret: i16,
1275) -> Option<Vec<TabPosition>> {
1276 type Assignment = (i16, i16, i16, Vec<u8>, Vec<TabPosition>);
1277
1278 #[allow(clippy::too_many_arguments)]
1279 fn search(
1280 pitches: &[i16],
1281 tuning: &[i16],
1282 lines: usize,
1283 capo: i16,
1284 max_fret: i16,
1285 index: usize,
1286 used: &mut [bool],
1287 current: &mut Vec<TabPosition>,
1288 best: &mut Option<Assignment>,
1289 ) {
1290 if index == pitches.len() {
1291 let sum: i16 = current.iter().map(|p| i16::from(p.fret)).sum();
1292 let min = current.iter().map(|p| p.fret).min().unwrap_or(0);
1293 let max = current.iter().map(|p| p.fret).max().unwrap_or(0);
1294 let strings: Vec<u8> = current.iter().map(|p| p.string).collect();
1295 let candidate = (
1296 sum,
1297 i16::from(max) - i16::from(min),
1298 i16::from(max),
1299 strings,
1300 current.clone(),
1301 );
1302 if best.as_ref().is_none_or(|existing| {
1303 (candidate.0, candidate.1, candidate.2, &candidate.3)
1304 < (existing.0, existing.1, existing.2, &existing.3)
1305 }) {
1306 *best = Some(candidate);
1307 }
1308 return;
1309 }
1310
1311 for (string, open) in tuning.iter().enumerate().take(lines) {
1312 if used[string] {
1313 continue;
1314 }
1315 let fret = pitches[index] - *open - capo;
1316 if !(0..=max_fret).contains(&fret) {
1317 continue;
1318 }
1319 used[string] = true;
1320 current.push(TabPosition {
1321 string: (string + 1) as u8,
1322 fret: fret as u8,
1323 });
1324 search(
1325 pitches,
1326 tuning,
1327 lines,
1328 capo,
1329 max_fret,
1330 index + 1,
1331 used,
1332 current,
1333 best,
1334 );
1335 current.pop();
1336 used[string] = false;
1337 }
1338 }
1339
1340 if pitches.len() > lines {
1341 return None;
1342 }
1343 let mut used = vec![false; lines];
1344 let mut current = Vec::with_capacity(pitches.len());
1345 let mut best = None;
1346 search(
1347 pitches,
1348 tuning,
1349 lines,
1350 capo,
1351 max_fret,
1352 0,
1353 &mut used,
1354 &mut current,
1355 &mut best,
1356 );
1357 best.map(|(_, _, _, _, positions)| positions)
1358}
1359
1360pub fn transpose(score: &Score, semitones: i8) -> Score {
1364 if semitones == 0 {
1365 return score.clone();
1366 }
1367 let mut out = score.clone();
1368 out.settings.key_signature.fifths = transpose_fifths(
1369 score.settings.key_signature.fifths,
1370 &score.settings.key_signature.mode,
1371 semitones,
1372 );
1373 for part in &mut out.parts {
1374 for staff in &mut part.staves {
1375 if part.midi_channel == 9 || staff.clef == Clef::Percussion {
1376 continue;
1377 }
1378 let mut source_key = score.settings.key_signature.fifths;
1379 let mut target_key = out.settings.key_signature.fifths;
1380 for measure in &mut staff.measures {
1381 if let Some(ref mut ks) = measure.key_sig {
1382 source_key = ks.fifths;
1383 ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
1384 target_key = ks.fifths;
1385 }
1386 for note in measure.voices.iter_mut().flatten() {
1387 if !note.is_rest && !note.is_unpitched {
1388 for pitch in &mut note.pitches {
1389 *pitch =
1390 transpose_spelled_pitch(pitch, semitones, source_key, target_key);
1391 }
1392 }
1393 if let Some(chord) = &mut note.chord_symbol {
1394 if let Some(root) =
1395 transpose_chord_name(&chord.root, semitones, source_key, target_key)
1396 {
1397 chord.root = root;
1398 }
1399 if let Some(bass) = &mut chord.bass
1400 && let Some(name) =
1401 transpose_chord_name(bass, semitones, source_key, target_key)
1402 {
1403 *bass = name;
1404 }
1405 }
1406 }
1407 }
1408 }
1409 }
1410 out
1411}
1412
1413pub fn transpose_checked(score: &Score, semitones: i8) -> Result<Score, Error> {
1415 if !super::validate::validate(score).is_valid() {
1416 return Err(Error::InvalidScore);
1417 }
1418 if semitones != 0 {
1421 for part in &score.parts {
1422 for staff in &part.staves {
1423 if part.midi_channel == 9 || staff.clef == Clef::Percussion {
1424 continue;
1425 }
1426 if staff.tablature.is_some()
1427 || staff
1428 .measures
1429 .iter()
1430 .any(|m| m.tablature_change.is_some() || !m.harp_pedal_diagrams.is_empty())
1431 {
1432 return Err(Error::InvalidCommand(
1433 "transposition requires tablature/pedal remapping".into(),
1434 ));
1435 }
1436 for note in staff.measures.iter().flat_map(|m| &m.voices).flatten() {
1437 if !note.is_rest
1438 && !note.is_unpitched
1439 && note
1440 .pitches
1441 .iter()
1442 .any(|p| !(0..=127).contains(&(p.to_midi() + i16::from(semitones))))
1443 {
1444 return Err(Error::InvalidCommand(
1445 "transposed pitch exceeds MIDI range".into(),
1446 ));
1447 }
1448 if note.tab_position.is_some() || !note.tab_positions.is_empty() {
1449 return Err(Error::InvalidCommand(
1450 "transposition requires fret remapping".into(),
1451 ));
1452 }
1453 if let Some(chord) = ¬e.chord_symbol
1454 && ((!chord.root.is_empty()
1455 && chord.kind != "none"
1456 && chord_name_pitch(&chord.root).is_none())
1457 || chord
1458 .bass
1459 .as_ref()
1460 .is_some_and(|bass| chord_name_pitch(bass).is_none()))
1461 {
1462 return Err(Error::InvalidCommand(
1463 "unsupported chord root or slash bass spelling".into(),
1464 ));
1465 }
1466 }
1467 }
1468 }
1469 }
1470 let transposed = transpose(score, semitones);
1471 if !super::validate::validate(&transposed).is_valid() {
1472 return Err(Error::InvalidScore);
1473 }
1474 Ok(transposed)
1475}
1476
1477#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1479#[serde(rename_all = "snake_case")]
1480pub enum RegionalTranspositionTarget {
1481 Written,
1483 Concert,
1485}
1486
1487pub fn transpose_staff_region_checked(
1490 score: &Score,
1491 part_index: usize,
1492 staff_index: usize,
1493 start_measure: usize,
1494 end_measure: usize,
1495 semitones: i8,
1496 target: RegionalTranspositionTarget,
1497) -> Result<Score, Error> {
1498 if !super::validate::validate(score).is_valid() {
1499 return Err(Error::InvalidScore);
1500 }
1501 let source_staff = score
1502 .parts
1503 .get(part_index)
1504 .ok_or(Error::PartNotFound(part_index))?
1505 .staves
1506 .get(staff_index)
1507 .ok_or(Error::StaffNotFound(staff_index))?;
1508 if start_measure >= end_measure || end_measure > source_staff.measures.len() {
1509 return Err(Error::InvalidCommand(format!(
1510 "invalid measure range {start_measure}..{end_measure}"
1511 )));
1512 }
1513 if target == RegionalTranspositionTarget::Concert
1514 && (start_measure != 0 || end_measure != source_staff.measures.len())
1515 {
1516 return Err(Error::InvalidCommand(
1517 "concert-pitch staff transposition requires the full staff range".into(),
1518 ));
1519 }
1520 let mut transformed = score.clone();
1521 let staff = &mut transformed.parts[part_index].staves[staff_index];
1522 match target {
1523 RegionalTranspositionTarget::Written => {
1524 for measure in &mut staff.measures[start_measure..end_measure] {
1525 if let Some(key) = &mut measure.key_sig {
1526 key.fifths = transpose_fifths(key.fifths, &key.mode, semitones);
1527 }
1528 for voice in &mut measure.voices {
1529 for note in voice {
1530 for pitch in &mut note.pitches {
1531 *pitch = transpose_pitch(pitch, semitones);
1532 }
1533 }
1534 }
1535 }
1536 }
1537 RegionalTranspositionTarget::Concert => {
1538 staff.transpose_semitones = staff.transpose_semitones.saturating_add(semitones);
1539 }
1540 }
1541 if !super::validate::validate(&transformed).is_valid() {
1542 return Err(Error::InvalidScore);
1543 }
1544 Ok(transformed)
1545}
1546
1547const TRANSPOSE_STEPS: [Step; 7] = [
1548 Step::C,
1549 Step::D,
1550 Step::E,
1551 Step::F,
1552 Step::G,
1553 Step::A,
1554 Step::B,
1555];
1556
1557fn chord_name_pitch(name: &str) -> Option<Pitch> {
1558 let mut chars = name.chars();
1559 let step = Step::from_char(chars.next()?)?;
1560 let mut alter = 0i8;
1561 for c in chars {
1562 alter = alter.checked_add(match c {
1563 '#' | '♯' => 1,
1564 'b' | '♭' => -1,
1565 'x' | '𝄪' => 2,
1566 _ => return None,
1567 })?;
1568 }
1569 (-2..=2)
1570 .contains(&alter)
1571 .then(|| Pitch::with_alter(step, 4, alter))
1572}
1573
1574fn transpose_chord_name(
1575 name: &str,
1576 semitones: i8,
1577 source_key: i8,
1578 target_key: i8,
1579) -> Option<String> {
1580 if semitones % 12 == 0 {
1581 return Some(name.to_string());
1582 }
1583 let pitch =
1584 transpose_spelled_pitch(&chord_name_pitch(name)?, semitones, source_key, target_key);
1585 let accidental = if pitch.alter < 0 { "b" } else { "#" };
1586 Some(format!(
1587 "{}{}",
1588 pitch.step.to_char(),
1589 accidental.repeat(pitch.alter.unsigned_abs() as usize)
1590 ))
1591}
1592
1593fn transpose_spelled_pitch(pitch: &Pitch, semitones: i8, source_key: i8, target_key: i8) -> Pitch {
1596 if semitones % 12 == 0 {
1597 let mut out = pitch.clone();
1598 out.octave = out.octave.saturating_add(semitones / 12);
1599 return out;
1600 }
1601 let tonic = |fifths: i8| {
1602 let index = (i16::from(fifths) * 4).rem_euclid(7);
1603 let natural = i16::from(TRANSPOSE_STEPS[index as usize].to_semitone());
1604 let pc = (i16::from(fifths) * 7).rem_euclid(12);
1605 let alter = (pc - natural + 6).rem_euclid(12) - 6;
1606 (index, natural + alter)
1607 };
1608 let (from_step, from_pc) = tonic(source_key);
1609 let (to_step, to_pc) = tonic(target_key);
1610 let shift = to_step - from_step + 7 * ((i16::from(semitones) - (to_pc - from_pc)) / 12);
1611 let step_index = match pitch.step {
1612 Step::C => 0,
1613 Step::D => 1,
1614 Step::E => 2,
1615 Step::F => 3,
1616 Step::G => 4,
1617 Step::A => 5,
1618 Step::B => 6,
1619 };
1620 let diatonic = i16::from(pitch.octave) * 7 + step_index + shift;
1621 let step = TRANSPOSE_STEPS[diatonic.rem_euclid(7) as usize].clone();
1622 let octave = diatonic.div_euclid(7) as i8;
1623 let midi = (pitch.to_midi() + i16::from(semitones)).clamp(0, 127);
1624 let alter = midi - (i16::from(octave) + 1) * 12 - i16::from(step.to_semitone());
1625 if (-2..=2).contains(&alter) {
1626 Pitch::with_microtone(step, octave, alter as i8, pitch.microtone_cents)
1627 } else {
1628 let mut out = Pitch::from_midi(midi as u8, target_key < 0);
1629 out.microtone_cents = pitch.microtone_cents;
1630 out
1631 }
1632}
1633
1634fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
1635 let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
1636 let pc = new_midi % 12;
1637 let oct = (new_midi / 12) as i8 - 1;
1638 let (step, alter): (Step, i8) = if semitones >= 0 {
1639 match pc {
1640 0 => (Step::C, 0),
1641 1 => (Step::C, 1),
1642 2 => (Step::D, 0),
1643 3 => (Step::D, 1),
1644 4 => (Step::E, 0),
1645 5 => (Step::F, 0),
1646 6 => (Step::F, 1),
1647 7 => (Step::G, 0),
1648 8 => (Step::G, 1),
1649 9 => (Step::A, 0),
1650 10 => (Step::A, 1),
1651 11 => (Step::B, 0),
1652 _ => (Step::C, 0),
1653 }
1654 } else {
1655 match pc {
1656 0 => (Step::C, 0),
1657 1 => (Step::D, -1),
1658 2 => (Step::D, 0),
1659 3 => (Step::E, -1),
1660 4 => (Step::E, 0),
1661 5 => (Step::F, 0),
1662 6 => (Step::G, -1),
1663 7 => (Step::G, 0),
1664 8 => (Step::A, -1),
1665 9 => (Step::A, 0),
1666 10 => (Step::B, -1),
1667 11 => (Step::B, 0),
1668 _ => (Step::C, 0),
1669 }
1670 };
1671 Pitch::with_microtone(step, oct, alter, pitch.microtone_cents)
1672}
1673
1674fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
1680 if semitones % 12 == 0 {
1681 return fifths;
1682 }
1683 let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
1684 let tonic_pc = if mode == "minor" {
1685 ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
1686 } else {
1687 tonic_major_pc
1688 };
1689 let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
1690 let major_tonic = if mode == "minor" {
1691 ((new_tonic as i32 + 3).rem_euclid(12)) as u8
1692 } else {
1693 new_tonic
1694 };
1695 let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
1696 if raw > 6 { raw - 12 } else { raw }
1697}
1698
1699#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1701pub struct InstrumentRange {
1702 pub lowest: u8,
1703 pub highest: u8,
1704}
1705
1706#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1713pub struct InstrumentDefinition {
1714 pub id: String,
1716 #[serde(default)]
1717 pub name: String,
1718 #[serde(default)]
1719 pub short_name: String,
1720 #[serde(default)]
1721 pub family: Option<String>,
1722 #[serde(default)]
1723 pub transpose_semitones: i8,
1724 #[serde(default)]
1725 pub written_range: Option<InstrumentRange>,
1726 #[serde(default)]
1727 pub sounding_range: Option<InstrumentRange>,
1728 #[serde(default)]
1729 pub default_clefs: Vec<Clef>,
1730 #[serde(default = "default_instrument_staff_count")]
1731 pub staff_count: u8,
1732 #[serde(default)]
1733 pub staff_kind: StaffKind,
1734 #[serde(default)]
1735 pub midi_channel: u8,
1736 #[serde(default)]
1737 pub midi_program: u8,
1738 #[serde(default)]
1739 pub percussion_map_id: Option<String>,
1740}
1741
1742const fn default_instrument_staff_count() -> u8 {
1743 1
1744}
1745
1746impl InstrumentDefinition {
1747 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
1749 Self {
1750 id: id.into(),
1751 name: name.into(),
1752 short_name: String::new(),
1753 family: None,
1754 transpose_semitones: 0,
1755 written_range: None,
1756 sounding_range: None,
1757 default_clefs: Vec::new(),
1758 staff_count: 1,
1759 staff_kind: StaffKind::Standard,
1760 midi_channel: 0,
1761 midi_program: 0,
1762 percussion_map_id: None,
1763 }
1764 }
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1769pub struct NamedInstrumentChannel {
1770 pub name: String,
1771 pub program: u8,
1772 pub midi_channel: u8,
1773}
1774
1775#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1777pub struct VoiceChannelSwitch {
1778 pub voice: usize,
1779 pub position_beats: f64,
1780 pub channel_name: String,
1781}
1782
1783#[derive(Debug, Clone, Serialize, Deserialize)]
1784pub struct Part {
1785 pub id: String,
1786 pub name: String,
1787 pub short_name: String,
1788 pub staves: Vec<Staff>,
1789 #[serde(default)]
1791 pub midi_channel: u8,
1792 #[serde(default)]
1794 pub midi_program: u8,
1795 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1797 pub named_channels: Vec<NamedInstrumentChannel>,
1798 #[serde(default)]
1800 pub midi_pitch_bends: Vec<MidiPitchBend>,
1801 #[serde(default)]
1803 pub midi_control_changes: Vec<MidiControlChange>,
1804 #[serde(default)]
1806 pub midi_program_changes: Vec<MidiProgramChange>,
1807 #[serde(default)]
1809 pub midi_aftertouch: Vec<MidiAftertouch>,
1810 #[serde(default)]
1812 pub percussion_instruments: Vec<PercussionInstrument>,
1813 #[serde(default)]
1815 pub staff_groups: Vec<StaffGroup>,
1816 #[serde(default)]
1818 pub instrument: Option<InstrumentDefinition>,
1819}
1820
1821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1822pub struct MidiPitchBend {
1823 pub tick: u64,
1824 pub channel: u8,
1825 pub value: i16,
1827}
1828
1829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1830pub struct MidiControlChange {
1831 pub tick: u64,
1832 pub channel: u8,
1833 pub controller: u8,
1835 pub value: u8,
1837}
1838
1839#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1840pub struct MidiProgramChange {
1841 pub tick: u64,
1842 pub channel: u8,
1843 pub program: u8,
1845}
1846
1847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1848pub struct MidiAftertouch {
1849 pub tick: u64,
1850 pub channel: u8,
1851 pub key: Option<u8>,
1853 pub value: u8,
1855}
1856
1857impl PercussionInstrument {
1863 pub fn display_position(pitch: &Pitch) -> i8 {
1866 let step = "CDEFGAB"
1867 .find(pitch.step.to_char())
1868 .map_or(0, |index| index as i32);
1869 let steps = i32::from(pitch.octave) * 7 + step;
1870 (steps - (4 * 7 + 6)).clamp(i32::from(i8::MIN), i32::from(i8::MAX)) as i8
1871 }
1872}
1873
1874#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1875pub struct PercussionInstrument {
1876 pub id: String,
1878 #[serde(default)]
1879 pub name: Option<String>,
1880 #[serde(default)]
1884 pub midi_unpitched: Option<u8>,
1885 #[serde(default)]
1887 pub staff_position: Option<i8>,
1888 #[serde(default)]
1890 pub notehead: Option<NoteHead>,
1891 #[serde(default)]
1893 pub preferred_voice: Option<u8>,
1894 #[serde(default)]
1896 pub techniques: Vec<String>,
1897}
1898
1899impl Part {
1900 pub fn new(name: &str, short_name: &str) -> Self {
1901 Self {
1902 id: Uuid::new_v4().to_string(),
1903 name: name.to_string(),
1904 short_name: short_name.to_string(),
1905 staves: Vec::new(),
1906 midi_channel: 0,
1907 midi_program: 0,
1908 named_channels: Vec::new(),
1909 midi_pitch_bends: Vec::new(),
1910 midi_control_changes: Vec::new(),
1911 midi_program_changes: Vec::new(),
1912 midi_aftertouch: Vec::new(),
1913 percussion_instruments: Vec::new(),
1914 staff_groups: Vec::new(),
1915 instrument: None,
1916 }
1917 }
1918
1919 pub fn percussion_key(&self, note: &Note, pitch_index: usize) -> Option<u8> {
1925 if !note.is_unpitched {
1926 return None;
1927 }
1928 let pitch = note.pitches.get(pitch_index)?;
1929 let by_id = note
1930 .instrument_id
1931 .as_deref()
1932 .filter(|_| pitch_index == 0)
1933 .and_then(|id| {
1934 self.percussion_instruments
1935 .iter()
1936 .find(|instrument| instrument.id == id)
1937 })
1938 .and_then(|instrument| instrument.midi_unpitched);
1939 by_id.or_else(|| {
1940 let position = PercussionInstrument::display_position(pitch);
1941 self.percussion_instruments
1942 .iter()
1943 .filter(|instrument| instrument.staff_position == Some(position))
1944 .find_map(|instrument| instrument.midi_unpitched)
1945 })
1946 }
1947
1948 pub fn percussion_instrument_for_note(&self, note: &Note) -> Option<&PercussionInstrument> {
1956 if !note.is_unpitched {
1957 return None;
1958 }
1959 if let Some(instrument_id) = note.instrument_id.as_deref() {
1960 return self
1961 .percussion_instruments
1962 .iter()
1963 .find(|instrument| instrument.id == instrument_id);
1964 }
1965 let midi_key = u8::try_from(note.pitches.first()?.to_midi()).ok()?;
1966 self.percussion_instruments
1967 .iter()
1968 .find(|instrument| instrument.midi_unpitched == Some(midi_key))
1969 }
1970}
1971
1972#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1974#[serde(rename_all = "snake_case")]
1975pub enum StaffKind {
1976 #[default]
1977 Standard,
1978 Tablature,
1979 Percussion,
1980}
1981
1982#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1984#[serde(rename_all = "snake_case")]
1985pub enum StaffNoteheadScheme {
1986 #[default]
1987 Standard,
1988 PitchNames,
1989 ShapeNotes,
1990}
1991
1992#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1994#[serde(rename_all = "snake_case")]
1995pub enum TablatureRhythmDisplay {
1996 #[default]
1998 FretOnly,
1999 Stems,
2001}
2002
2003#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2009pub struct StaffPresentation {
2010 #[serde(default)]
2011 pub kind: StaffKind,
2012 #[serde(default = "default_staff_line_count")]
2013 pub lines: u8,
2014 #[serde(default = "default_staff_line_distance")]
2015 pub line_distance: f32,
2016 #[serde(default)]
2017 pub small: bool,
2018 #[serde(default)]
2019 pub cutaway: bool,
2020 #[serde(default = "default_staff_visible")]
2021 pub visible: bool,
2022 #[serde(default)]
2023 pub notehead_scheme: StaffNoteheadScheme,
2024 #[serde(default)]
2026 pub tablature_rhythm_display: TablatureRhythmDisplay,
2027 #[serde(default)]
2029 pub tablature_fret_mark_style: TablatureFretMarkStyle,
2030}
2031
2032const fn default_staff_line_count() -> u8 {
2033 5
2034}
2035
2036const fn default_staff_line_distance() -> f32 {
2037 1.0
2038}
2039
2040const fn default_staff_visible() -> bool {
2041 true
2042}
2043
2044impl Default for StaffPresentation {
2045 fn default() -> Self {
2046 Self {
2047 kind: StaffKind::Standard,
2048 lines: default_staff_line_count(),
2049 line_distance: default_staff_line_distance(),
2050 small: false,
2051 cutaway: false,
2052 visible: true,
2053 notehead_scheme: StaffNoteheadScheme::Standard,
2054 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
2055 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
2056 }
2057 }
2058}
2059
2060#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2062#[serde(rename_all = "snake_case")]
2063pub enum TablatureFretMarkStyle {
2064 #[default]
2065 Arabic,
2066 RomanUpper,
2067 RomanLower,
2068}
2069
2070impl StaffPresentation {
2071 fn for_clef(clef: &Clef) -> Self {
2072 let mut presentation = Self::default();
2073 if matches!(clef, Clef::Percussion) {
2074 presentation.kind = StaffKind::Percussion;
2075 }
2076 presentation
2077 }
2078}
2079
2080#[derive(Debug, Clone, Serialize, Deserialize)]
2081pub struct Staff {
2082 pub clef: Clef,
2083 pub measures: Vec<Measure>,
2084 #[serde(default)]
2087 pub transpose_semitones: i8,
2088 #[serde(default)]
2089 pub tablature: Option<TablatureConfig>,
2090 #[serde(default)]
2091 pub presentation: StaffPresentation,
2092}
2093
2094impl Staff {
2095 pub fn meter_at<'a>(
2099 &'a self,
2100 measure_index: usize,
2101 default: &'a TimeSignature,
2102 ) -> &'a TimeSignature {
2103 self.measures
2104 .iter()
2105 .take(measure_index.saturating_add(1))
2106 .rev()
2107 .find_map(|measure| measure.time_sig.as_ref())
2108 .unwrap_or(default)
2109 }
2110
2111 pub fn measure_beats(&self, measure_index: usize, default: &TimeSignature) -> f64 {
2113 self.measures.get(measure_index).map_or_else(
2114 || self.meter_at(measure_index, default).total_beats(),
2115 |measure| measure.duration_beats(self.meter_at(measure_index, default)),
2116 )
2117 }
2118
2119 pub fn new(clef: Clef) -> Self {
2120 Self {
2121 presentation: StaffPresentation::for_clef(&clef),
2122 clef,
2123 measures: Vec::new(),
2124 transpose_semitones: 0,
2125 tablature: None,
2126 }
2127 }
2128
2129 pub fn tablature_at(&self, measure_index: usize) -> Option<TablatureConfig> {
2135 let mut active = self.tablature.clone();
2136 for measure in self.measures.iter().take(measure_index.saturating_add(1)) {
2137 if let Some(change) = &measure.tablature_change {
2138 active = Some(change.clone());
2139 }
2140 }
2141 active
2142 }
2143}
2144
2145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2146pub struct VoltaBracket {
2147 pub number: u8,
2149 pub kind: String,
2151 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2154 pub numbers: Vec<u8>,
2155}
2156
2157impl VoltaBracket {
2158 pub fn passes(&self) -> Vec<u8> {
2160 if self.numbers.is_empty() {
2161 vec![self.number]
2162 } else {
2163 self.numbers.clone()
2164 }
2165 }
2166
2167 pub fn plays_on(&self, pass: u8) -> bool {
2169 if self.numbers.is_empty() {
2170 self.number == pass
2171 } else {
2172 self.numbers.contains(&pass)
2173 }
2174 }
2175
2176 pub fn label(&self) -> String {
2178 let passes = self.passes();
2179 match passes.as_slice() {
2180 [first, .., last]
2181 if passes.len() >= 3 && passes.windows(2).all(|pair| pair[1] == pair[0] + 1) =>
2182 {
2183 format!("{first}-{last}")
2184 }
2185 _ => passes
2186 .iter()
2187 .map(u8::to_string)
2188 .collect::<Vec<_>>()
2189 .join(", "),
2190 }
2191 }
2192
2193 pub fn parse_passes(label: &str) -> Vec<u8> {
2195 let mut passes = Vec::new();
2196 for part in label
2197 .split([',', ' ', '.', '+', '/'])
2198 .filter(|part| !part.is_empty())
2199 {
2200 if let Some((from, to)) = part.split_once(['-', '–'])
2201 && let (Ok(from), Ok(to)) = (from.trim().parse::<u8>(), to.trim().parse::<u8>())
2202 && from <= to
2203 && to - from < 32
2204 {
2205 passes.extend(from..=to);
2206 } else if let Ok(pass) = part.trim().parse::<u8>() {
2207 passes.push(pass);
2208 }
2209 }
2210 passes.sort_unstable();
2211 passes.dedup();
2212 passes
2213 }
2214}
2215
2216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2218#[serde(rename_all = "kebab-case")]
2219pub enum HarpPedalPosition {
2220 Flat,
2221 #[default]
2222 Natural,
2223 Sharp,
2224}
2225
2226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2231pub struct HarpPedalDiagram {
2232 #[serde(default)]
2233 pub positions: [HarpPedalPosition; 7],
2234 #[serde(default, skip_serializing_if = "Option::is_none")]
2235 pub placement: Option<String>,
2236}
2237
2238impl Default for HarpPedalDiagram {
2239 fn default() -> Self {
2240 Self {
2241 positions: [HarpPedalPosition::Natural; 7],
2242 placement: None,
2243 }
2244 }
2245}
2246
2247#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2251pub struct MeasureLength {
2252 pub numerator: u32,
2253 pub denominator: u32,
2254}
2255
2256impl MeasureLength {
2257 pub const MAX_BEATS: f64 = 256.0;
2259
2260 pub fn beats(&self) -> Option<f64> {
2262 if self.numerator == 0 || self.denominator == 0 {
2263 return None;
2264 }
2265 let beats = 4.0 * f64::from(self.numerator) / f64::from(self.denominator);
2266 (beats.is_finite() && beats <= Self::MAX_BEATS).then_some(beats)
2267 }
2268
2269 pub fn from_beats(beats: f64) -> Option<Self> {
2272 const DIVISIONS: u32 = 3 * 5 * 7 * 64;
2273 if !beats.is_finite() || beats <= 0.0 || beats > Self::MAX_BEATS {
2274 return None;
2275 }
2276 let ticks = (beats * f64::from(DIVISIONS)).round();
2277 (ticks >= 1.0).then(|| Self::from_ticks(ticks as u32, DIVISIONS))?
2278 }
2279
2280 pub fn from_ticks(ticks: u32, divisions: u32) -> Option<Self> {
2282 let numerator = u64::from(ticks);
2283 let denominator = u64::from(divisions).checked_mul(4)?;
2284 if numerator == 0 || denominator == 0 {
2285 return None;
2286 }
2287 let gcd = {
2288 let (mut a, mut b) = (numerator, denominator);
2289 while b != 0 {
2290 (a, b) = (b, a % b);
2291 }
2292 a
2293 };
2294 let length = Self {
2295 numerator: u32::try_from(numerator / gcd).ok()?,
2296 denominator: u32::try_from(denominator / gcd).ok()?,
2297 };
2298 length.beats().map(|_| length)
2299 }
2300}
2301
2302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2306pub struct MidMeasureClef {
2307 pub offset: MeasureLength,
2308 pub clef: Clef,
2309}
2310
2311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2313pub struct MetronomeMark {
2314 pub beat_unit: Duration,
2315 #[serde(default)]
2316 pub beat_unit_dots: u8,
2317 pub per_minute: u16,
2318 #[serde(default)]
2319 pub parentheses: bool,
2320}
2321
2322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2324pub struct TimedPedalDirection {
2325 pub position_beats: f64,
2327 pub down: bool,
2329 pub number: u16,
2331 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2333 pub hide_sign: bool,
2334 #[serde(default, skip_serializing_if = "Option::is_none")]
2336 pub placement: Option<String>,
2337}
2338
2339#[derive(Debug, Clone, Serialize, Deserialize)]
2340pub struct Measure {
2341 pub number: u32,
2342 pub time_sig: Option<TimeSignature>,
2343 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2345 pub time_sig_hidden: bool,
2346 #[serde(default, skip_serializing_if = "Option::is_none")]
2348 pub time_symbol: Option<TimeSymbol>,
2349 pub key_sig: Option<KeySignature>,
2350 pub clef: Option<Clef>,
2351 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2354 pub mid_clefs: Vec<MidMeasureClef>,
2355 pub tempo: Option<u16>,
2356 #[serde(default, skip_serializing_if = "Option::is_none")]
2358 pub metronome_mark: Option<MetronomeMark>,
2359 #[serde(default)]
2362 pub tempo_ramp_to: Option<u16>,
2363 #[serde(default)]
2365 pub instrument_change: Option<InstrumentDefinition>,
2366 #[serde(default)]
2368 pub tablature_change: Option<TablatureConfig>,
2369 pub barline_left: Barline,
2370 pub barline_right: Barline,
2371 #[serde(default)]
2372 pub volta: Option<VoltaBracket>,
2373 #[serde(default)]
2374 pub tempo_text: Option<String>,
2375 #[serde(default)]
2376 pub rehearsal: Option<String>,
2377 #[serde(default)]
2380 pub navigation: Option<String>,
2381 #[serde(default)]
2383 pub expression_text: Option<String>,
2384 #[serde(default)]
2385 pub texts: Vec<StyledText>,
2386 #[serde(default)]
2388 pub figured_bass: Vec<FiguredBassFigure>,
2389 #[serde(default)]
2391 pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
2392 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2394 pub channel_switches: Vec<VoiceChannelSwitch>,
2395 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2397 pub timed_pedals: Vec<TimedPedalDirection>,
2398 #[serde(default)]
2400 pub multi_rest_count: Option<u8>,
2401 #[serde(default)]
2403 pub system_break: bool,
2404 #[serde(default)]
2406 pub page_break: bool,
2407 #[serde(default)]
2410 pub section_break: bool,
2411 pub voices: [Vec<Note>; 4],
2413 #[serde(default)]
2419 pub source_voice_numbers: [Option<u32>; 4],
2420 #[serde(default, skip_serializing_if = "Option::is_none")]
2423 pub actual_length: Option<MeasureLength>,
2424 #[serde(default, skip_serializing_if = "Option::is_none")]
2428 pub measure_repeat: Option<u8>,
2429}
2430
2431impl Measure {
2432 pub fn clef_at(&self, start_clef: &Clef, beats: f64) -> Clef {
2435 self.mid_clefs
2436 .iter()
2437 .take_while(|change| {
2438 change
2439 .offset
2440 .beats()
2441 .is_some_and(|offset| offset <= beats + 1e-9)
2442 })
2443 .last()
2444 .map_or_else(|| start_clef.clone(), |change| change.clef.clone())
2445 }
2446
2447 pub fn empty(numerator: u8, denominator: u8) -> Self {
2448 let total_beats = TimeSignature {
2449 numerator,
2450 denominator,
2451 }
2452 .total_beats();
2453 let mut voice0: Vec<Note> = Vec::new();
2454 let mut remaining = total_beats;
2455 while remaining > 1e-9 {
2456 let dur = Duration::whole_filling_beats(remaining);
2457 remaining -= dur.beats(0);
2458 voice0.push(Note::rest(dur));
2459 }
2460 Self {
2461 number: 0,
2462 time_sig: None,
2463 time_sig_hidden: false,
2464 time_symbol: None,
2465 key_sig: None,
2466 clef: None,
2467 mid_clefs: Vec::new(),
2468 tempo: None,
2469 metronome_mark: None,
2470 tempo_ramp_to: None,
2471 instrument_change: None,
2472 tablature_change: None,
2473 barline_left: Barline::Normal,
2474 barline_right: Barline::Normal,
2475 volta: None,
2476 tempo_text: None,
2477 rehearsal: None,
2478 navigation: None,
2479 expression_text: None,
2480 texts: Vec::new(),
2481 figured_bass: Vec::new(),
2482 harp_pedal_diagrams: Vec::new(),
2483 channel_switches: Vec::new(),
2484 timed_pedals: Vec::new(),
2485 multi_rest_count: None,
2486 system_break: false,
2487 page_break: false,
2488 section_break: false,
2489 voices: [voice0, vec![], vec![], vec![]],
2490 source_voice_numbers: [None; 4],
2491 actual_length: None,
2492 measure_repeat: None,
2493 }
2494 }
2495
2496 pub fn repeat_content_from(&mut self, source: &Measure) {
2499 for (target, voice) in self.voices.iter_mut().zip(&source.voices) {
2500 *target = voice.iter().map(Note::repeated_copy).collect();
2501 }
2502 self.source_voice_numbers = source.source_voice_numbers;
2503 }
2504
2505 pub fn same_sounding_content(&self, other: &Measure) -> bool {
2508 self.voices.iter().zip(&other.voices).all(|(left, right)| {
2509 left.len() == right.len()
2510 && left.iter().zip(right).all(|(a, b)| {
2511 a.is_rest == b.is_rest
2512 && a.pitches == b.pitches
2513 && a.duration == b.duration
2514 && a.dot_count == b.dot_count
2515 && a.tuplet == b.tuplet
2516 && a.is_grace == b.is_grace
2517 })
2518 })
2519 }
2520
2521 pub fn duration_beats(&self, time_signature: &TimeSignature) -> f64 {
2524 self.actual_length
2525 .and_then(|length| length.beats())
2526 .unwrap_or_else(|| {
2527 self.time_sig
2528 .as_ref()
2529 .unwrap_or(time_signature)
2530 .total_beats()
2531 })
2532 }
2533
2534 pub fn renumber(&mut self, n: u32) {
2535 self.number = n;
2536 }
2537}
2538
2539#[derive(Debug, Clone, Serialize, Deserialize)]
2540pub struct Note {
2541 pub id: String,
2542 pub is_rest: bool,
2543 #[serde(default)]
2545 pub is_unpitched: bool,
2546 #[serde(default)]
2548 pub instrument_id: Option<String>,
2549 #[serde(default)]
2553 pub offset_x: Option<f64>,
2554 #[serde(default)]
2556 pub offset_y: Option<f64>,
2557 #[serde(default)]
2559 pub relative_x: Option<f64>,
2560 #[serde(default)]
2562 pub relative_y: Option<f64>,
2563 pub pitches: Vec<Pitch>,
2565 #[serde(default)]
2566 pub tab_position: Option<super::notation::TabPosition>,
2567 #[serde(default)]
2570 pub tab_positions: Vec<super::notation::TabPosition>,
2571 pub duration: Duration,
2572 pub dot_count: u8,
2573 pub tie_start: bool,
2574 pub tie_end: bool,
2575 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2579 pub pitch_tie_starts: Vec<bool>,
2580 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2582 pub pitch_tie_ends: Vec<bool>,
2583 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2587 pub pitch_accidentals: Vec<AccidentalDisplay>,
2588 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2593 pub pitch_parentheses: Vec<bool>,
2594 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2599 pub pitch_fingerings: Vec<Option<u8>>,
2600 pub beam: BeamState,
2601 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2604 pub explicit_unbeamed: bool,
2605 pub articulations: Vec<Articulation>,
2606 #[serde(default, skip_serializing_if = "Option::is_none")]
2608 pub tremolo_pair: Option<String>,
2609 pub dynamic: Option<Dynamic>,
2610 pub stem_up: Option<bool>,
2611 #[serde(default)]
2612 pub hairpin_start: Option<HairpinKind>,
2613 #[serde(default, skip_serializing_if = "Option::is_none")]
2615 pub hairpin_above: Option<bool>,
2616 #[serde(default)]
2617 pub hairpin_end: bool,
2618 #[serde(default)]
2619 pub tuplet: Option<TupletInfo>,
2620 #[serde(default, skip_serializing_if = "Option::is_none")]
2622 pub tuplet_boundary: Option<TupletBoundary>,
2623 #[serde(default)]
2624 pub chord_symbol: Option<ChordSymbol>,
2625 #[serde(default)]
2626 pub is_grace: bool,
2627 #[serde(default)]
2629 pub grace_slash: bool,
2630 #[serde(default)]
2631 pub ottava_start: Option<OttavaKind>,
2632 #[serde(default)]
2633 pub ottava_end: bool,
2634 #[serde(default)]
2635 pub lyric: Option<Lyric>,
2636 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2638 pub additional_lyrics: Vec<VerseLyric>,
2639 #[serde(default)]
2640 pub pedal_start: bool,
2641 #[serde(default)]
2642 pub pedal_end: bool,
2643 #[serde(default)]
2644 pub slur_start: bool,
2645 #[serde(default)]
2646 pub slur_end: bool,
2647 #[serde(default)]
2649 pub arpeggiate: Option<bool>,
2650 #[serde(default)]
2652 pub technique_text: Option<String>,
2653 #[serde(default)]
2654 pub glissando_start: bool,
2655 #[serde(default)]
2656 pub glissando_end: bool,
2657 #[serde(default)]
2658 pub cross_staff: Option<CrossStaff>,
2659 #[serde(default)]
2661 pub fingering: Option<u8>,
2662 #[serde(default)]
2665 pub fingerings: Vec<u8>,
2666 #[serde(default)]
2668 pub string_number: Option<u8>,
2669 #[serde(default)]
2670 pub note_head: NoteHead,
2671 #[serde(default)]
2673 pub is_cue: bool,
2674 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2678 pub hidden: bool,
2679 #[serde(default)]
2681 pub trill_line_start: bool,
2682 #[serde(default)]
2684 pub trill_line_end: bool,
2685 #[serde(default)]
2687 pub guitar_technique: Option<GuitarTechnique>,
2688 #[serde(default)]
2690 pub guitar_bend_alter_cents: Option<i16>,
2691 #[serde(default)]
2693 pub guitar_bend_curve: Vec<GuitarBendPoint>,
2694}
2695
2696#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2698pub struct GuitarBendPoint {
2699 pub position_per_mille: u16,
2700 pub alter_cents: i16,
2701}
2702
2703impl Note {
2704 pub fn select_fingering(
2706 &self,
2707 policy: super::notation::FingeringSelectionPolicy,
2708 ) -> Option<u8> {
2709 let candidates = if self.fingerings.is_empty() {
2710 self.fingering.into_iter().collect::<Vec<_>>()
2711 } else {
2712 self.fingerings.clone()
2713 };
2714 match policy {
2715 super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
2716 super::notation::FingeringSelectionPolicy::LowestNumber => {
2717 candidates.iter().copied().min()
2718 }
2719 super::notation::FingeringSelectionPolicy::HighestNumber => {
2720 candidates.iter().copied().max()
2721 }
2722 }
2723 }
2724
2725 pub fn new(pitch: Pitch, duration: Duration) -> Self {
2726 Self {
2727 id: Uuid::new_v4().to_string(),
2728 is_rest: false,
2729 is_unpitched: false,
2730 instrument_id: None,
2731 offset_x: None,
2732 offset_y: None,
2733 relative_x: None,
2734 relative_y: None,
2735 pitches: vec![pitch],
2736 tab_position: None,
2737 tab_positions: Vec::new(),
2738 duration,
2739 dot_count: 0,
2740 tie_start: false,
2741 tie_end: false,
2742 pitch_tie_starts: Vec::new(),
2743 pitch_tie_ends: Vec::new(),
2744 pitch_accidentals: Vec::new(),
2745 pitch_parentheses: Vec::new(),
2746 pitch_fingerings: Vec::new(),
2747 beam: BeamState::None,
2748 explicit_unbeamed: false,
2749 articulations: Vec::new(),
2750 tremolo_pair: None,
2751 dynamic: None,
2752 stem_up: None,
2753 hairpin_start: None,
2754 hairpin_above: None,
2755 hairpin_end: false,
2756 tuplet: None,
2757 tuplet_boundary: None,
2758 chord_symbol: None,
2759 is_grace: false,
2760 grace_slash: false,
2761 ottava_start: None,
2762 ottava_end: false,
2763 lyric: None,
2764 additional_lyrics: Vec::new(),
2765 pedal_start: false,
2766 pedal_end: false,
2767 slur_start: false,
2768 slur_end: false,
2769 arpeggiate: None,
2770 technique_text: None,
2771 glissando_start: false,
2772 glissando_end: false,
2773 cross_staff: None,
2774 fingering: None,
2775 fingerings: Vec::new(),
2776 string_number: None,
2777 note_head: NoteHead::Normal,
2778 is_cue: false,
2779 hidden: false,
2780 trill_line_start: false,
2781 trill_line_end: false,
2782 guitar_technique: None,
2783 guitar_bend_alter_cents: None,
2784 guitar_bend_curve: Vec::new(),
2785 }
2786 }
2787
2788 pub fn rest(duration: Duration) -> Self {
2789 Self {
2790 id: Uuid::new_v4().to_string(),
2791 is_rest: true,
2792 is_unpitched: false,
2793 instrument_id: None,
2794 offset_x: None,
2795 offset_y: None,
2796 relative_x: None,
2797 relative_y: None,
2798 pitches: Vec::new(),
2799 tab_position: None,
2800 tab_positions: Vec::new(),
2801 duration,
2802 dot_count: 0,
2803 tie_start: false,
2804 tie_end: false,
2805 pitch_tie_starts: Vec::new(),
2806 pitch_tie_ends: Vec::new(),
2807 pitch_accidentals: Vec::new(),
2808 pitch_parentheses: Vec::new(),
2809 pitch_fingerings: Vec::new(),
2810 beam: BeamState::None,
2811 explicit_unbeamed: false,
2812 articulations: Vec::new(),
2813 tremolo_pair: None,
2814 dynamic: None,
2815 stem_up: None,
2816 hairpin_start: None,
2817 hairpin_above: None,
2818 hairpin_end: false,
2819 tuplet: None,
2820 tuplet_boundary: None,
2821 chord_symbol: None,
2822 is_grace: false,
2823 grace_slash: false,
2824 ottava_start: None,
2825 ottava_end: false,
2826 lyric: None,
2827 additional_lyrics: Vec::new(),
2828 pedal_start: false,
2829 pedal_end: false,
2830 slur_start: false,
2831 slur_end: false,
2832 arpeggiate: None,
2833 technique_text: None,
2834 glissando_start: false,
2835 glissando_end: false,
2836 cross_staff: None,
2837 fingering: None,
2838 fingerings: Vec::new(),
2839 string_number: None,
2840 note_head: NoteHead::Normal,
2841 is_cue: false,
2842 hidden: false,
2843 trill_line_start: false,
2844 trill_line_end: false,
2845 guitar_technique: None,
2846 guitar_bend_alter_cents: None,
2847 guitar_bend_curve: Vec::new(),
2848 }
2849 }
2850
2851 pub fn repeated_copy(&self) -> Note {
2854 let mut copy = self.clone();
2855 copy.id = Uuid::new_v4().to_string();
2856 copy.tie_start = false;
2857 copy.tie_end = false;
2858 copy.slur_start = false;
2859 copy.slur_end = false;
2860 copy.hairpin_start = None;
2861 copy.hairpin_above = None;
2862 copy.hairpin_end = false;
2863 copy.pedal_start = false;
2864 copy.pedal_end = false;
2865 copy.ottava_start = None;
2866 copy.ottava_end = false;
2867 copy.glissando_start = false;
2868 copy.glissando_end = false;
2869 copy.trill_line_start = false;
2870 copy.trill_line_end = false;
2871 copy.lyric = None;
2872 copy.additional_lyrics.clear();
2873 copy.chord_symbol = None;
2874 copy.dynamic = None;
2875 copy
2876 }
2877
2878 pub fn pitch_tie_start(&self, index: usize) -> bool {
2881 per_pitch_flag(
2882 &self.pitch_tie_starts,
2883 self.pitches.len(),
2884 index,
2885 self.tie_start,
2886 )
2887 }
2888
2889 pub fn pitch_tie_end(&self, index: usize) -> bool {
2891 per_pitch_flag(
2892 &self.pitch_tie_ends,
2893 self.pitches.len(),
2894 index,
2895 self.tie_end,
2896 )
2897 }
2898
2899 pub fn set_pitch_ties(&mut self, starts: &[bool], ends: &[bool]) {
2904 let count = self.pitches.len();
2905 if starts.len() == count {
2906 self.tie_start = starts.iter().any(|tied| *tied);
2907 self.pitch_tie_starts = if starts.iter().all(|tied| *tied == self.tie_start) {
2908 Vec::new()
2909 } else {
2910 starts.to_vec()
2911 };
2912 }
2913 if ends.len() == count {
2914 self.tie_end = ends.iter().any(|tied| *tied);
2915 self.pitch_tie_ends = if ends.iter().all(|tied| *tied == self.tie_end) {
2916 Vec::new()
2917 } else {
2918 ends.to_vec()
2919 };
2920 }
2921 }
2922
2923 pub fn accidental_display(&self, index: usize) -> AccidentalDisplay {
2925 if self.pitch_accidentals.len() > self.pitches.len() {
2926 return AccidentalDisplay::Auto;
2927 }
2928 self.pitch_accidentals
2929 .get(index)
2930 .copied()
2931 .unwrap_or_default()
2932 }
2933
2934 pub fn set_accidental_display(&mut self, index: usize, display: AccidentalDisplay) {
2937 if index >= self.pitches.len() {
2938 return;
2939 }
2940 if self.pitch_accidentals.len() > self.pitches.len() {
2941 self.pitch_accidentals.clear();
2942 }
2943 self.pitch_accidentals
2944 .resize(self.pitches.len(), AccidentalDisplay::Auto);
2945 self.pitch_accidentals[index] = display;
2946 if self
2947 .pitch_accidentals
2948 .iter()
2949 .all(|display| *display == AccidentalDisplay::Auto)
2950 {
2951 self.pitch_accidentals.clear();
2952 }
2953 }
2954
2955 pub fn pitch_fingering(&self, index: usize) -> Option<u8> {
2957 if self.pitch_fingerings.len() > self.pitches.len() {
2958 return None;
2959 }
2960 self.pitch_fingerings.get(index).copied().flatten()
2961 }
2962
2963 pub fn set_pitch_fingering(&mut self, index: usize, fingering: u8) {
2966 if index >= self.pitches.len() {
2967 return;
2968 }
2969 if self.pitch_fingerings.len() > self.pitches.len() {
2970 self.pitch_fingerings.clear();
2971 }
2972 if self.pitch_fingerings.len() <= index {
2973 self.pitch_fingerings.resize(index + 1, None);
2974 }
2975 self.pitch_fingerings[index] = Some(fingering);
2976 if self.fingering.is_none() {
2977 self.fingering = Some(fingering);
2978 }
2979 self.fingerings.push(fingering);
2980 }
2981
2982 pub fn is_parenthesized(&self, index: usize) -> bool {
2984 self.pitch_parentheses.len() <= self.pitches.len()
2985 && self.pitch_parentheses.get(index).copied().unwrap_or(false)
2986 }
2987
2988 pub fn set_parenthesized(&mut self, index: usize, parenthesized: bool) {
2991 if index >= self.pitches.len() {
2992 return;
2993 }
2994 if self.pitch_parentheses.len() > self.pitches.len() {
2995 self.pitch_parentheses.clear();
2996 }
2997 self.pitch_parentheses.resize(self.pitches.len(), false);
2998 self.pitch_parentheses[index] = parenthesized;
2999 if !self.pitch_parentheses.contains(&true) {
3000 self.pitch_parentheses.clear();
3001 }
3002 }
3003
3004 pub fn pitch_tie_starts_or_uniform(&self) -> Vec<bool> {
3006 (0..self.pitches.len())
3007 .map(|index| self.pitch_tie_start(index))
3008 .collect()
3009 }
3010
3011 pub fn pitch_tie_ends_or_uniform(&self) -> Vec<bool> {
3013 (0..self.pitches.len())
3014 .map(|index| self.pitch_tie_end(index))
3015 .collect()
3016 }
3017
3018 pub fn is_plain_whole_rest(&self) -> bool {
3022 self.is_rest
3023 && matches!(self.duration, Duration::Whole)
3024 && self.dot_count == 0
3025 && self.tuplet.is_none()
3026 && !self.is_grace
3027 && !self.is_cue
3028 }
3029
3030 pub fn beats(&self) -> f64 {
3031 if self.is_grace || self.is_cue {
3032 return 0.0;
3033 }
3034 let base = self.duration.beats(self.dot_count);
3035 if let Some(ref t) = self.tuplet {
3036 base * (t.normal_notes as f64) / (t.actual_notes as f64)
3037 } else {
3038 base
3039 }
3040 }
3041}
3042
3043impl Duration {
3044 pub fn whole_filling_beats(beats: f64) -> Duration {
3046 if beats >= 16.0 {
3047 Duration::Longa
3048 } else if beats >= 8.0 {
3049 Duration::Breve
3050 } else if beats >= 4.0 {
3051 Duration::Whole
3052 } else if beats >= 2.0 {
3053 Duration::Half
3054 } else if beats >= 1.0 {
3055 Duration::Quarter
3056 } else if beats >= 0.5 {
3057 Duration::Eighth
3058 } else if beats >= 0.25 {
3059 Duration::Sixteenth
3060 } else if beats >= 0.125 {
3061 Duration::ThirtySecond
3062 } else if beats >= 0.0625 {
3063 Duration::SixtyFourth
3064 } else {
3065 Duration::HundredTwentyEighth
3066 }
3067 }
3068}
3069
3070pub fn tuplet_group_ranges(voice: &[Note]) -> Vec<(usize, usize)> {
3073 let mut groups = Vec::new();
3074 let mut open: Option<(usize, TupletInfo, u64, bool)> = None;
3075 let mut previous_timed = 0;
3076 for (index, note) in voice.iter().enumerate() {
3077 if note.is_grace {
3078 continue;
3079 }
3080 let has_start = matches!(
3081 note.tuplet_boundary,
3082 Some(TupletBoundary::Start | TupletBoundary::StartStop)
3083 );
3084 let has_stop = matches!(
3085 note.tuplet_boundary,
3086 Some(TupletBoundary::Stop | TupletBoundary::StartStop)
3087 );
3088 let must_close = open
3089 .as_ref()
3090 .is_some_and(|(_, ratio, _, _)| note.tuplet.as_ref() != Some(ratio) || has_start);
3091 if must_close && let Some((start, _, _, _)) = open.take() {
3092 groups.push((start, previous_timed));
3093 }
3094 if let Some(ratio) = ¬e.tuplet {
3095 let (numerator, denominator) = note.duration.as_fraction();
3096 let base = 4096 * u64::from(numerator) / u64::from(denominator.max(1));
3097 let dots = u32::from(note.dot_count.min(4));
3098 let written = base * ((1u64 << (dots + 1)) - 1) / (1u64 << dots);
3099 let (_, current_ratio, total_written, explicit) =
3100 open.get_or_insert_with(|| (index, ratio.clone(), 0, has_start));
3101 *total_written += written;
3102 let count = u64::from(current_ratio.actual_notes.max(1));
3103 let unit = *total_written / count;
3104 let legacy_complete = !*explicit
3105 && *total_written % count == 0
3106 && unit > 0
3107 && unit.is_power_of_two()
3108 && unit <= 4096;
3109 if (has_stop || legacy_complete)
3110 && let Some((start, _, _, _)) = open.take()
3111 {
3112 groups.push((start, index));
3113 }
3114 }
3115 previous_timed = index;
3116 }
3117 if let Some((start, _, _, _)) = open {
3118 groups.push((start, previous_timed));
3119 }
3120 groups
3121}
3122
3123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3127pub struct NoteAddr {
3128 pub part: usize,
3129 pub staff: usize,
3130 pub measure: usize,
3131 pub voice: usize,
3132 pub note: usize,
3133}
3134
3135#[derive(Debug, Clone, Serialize, Deserialize)]
3139pub enum ScoreChange {
3140 ScoreViewsChanged {
3141 old: Vec<ScoreView>,
3142 new: Vec<ScoreView>,
3143 },
3144 ScoreTextChanged {
3145 old: Vec<StyledText>,
3146 new: Vec<StyledText>,
3147 },
3148 MeasureTextChanged {
3149 part: usize,
3150 staff: usize,
3151 measure: usize,
3152 old: Vec<StyledText>,
3153 new: Vec<StyledText>,
3154 },
3155 FiguredBassChanged {
3156 part: usize,
3157 staff: usize,
3158 measure: usize,
3159 old: Vec<FiguredBassFigure>,
3160 new: Vec<FiguredBassFigure>,
3161 },
3162 HarpPedalDiagramsChanged {
3163 part: usize,
3164 staff: usize,
3165 measure: usize,
3166 old: Vec<HarpPedalDiagram>,
3167 new: Vec<HarpPedalDiagram>,
3168 },
3169 TimedPedalsChanged {
3170 part: usize,
3171 staff: usize,
3172 measure: usize,
3173 old: Vec<TimedPedalDirection>,
3174 new: Vec<TimedPedalDirection>,
3175 },
3176 ChordDefinitionsChanged {
3177 old: Vec<ChordDefinition>,
3178 new: Vec<ChordDefinition>,
3179 },
3180 PartNamesChanged {
3181 part: usize,
3182 old_name: String,
3183 new_name: String,
3184 old_short_name: String,
3185 new_short_name: String,
3186 },
3187 PartMidiChanged {
3188 part: usize,
3189 old_channel: u8,
3190 new_channel: u8,
3191 old_program: u8,
3192 new_program: u8,
3193 },
3194 InstrumentDefinitionChanged {
3195 part: usize,
3196 old: Option<InstrumentDefinition>,
3197 new: Option<InstrumentDefinition>,
3198 },
3199 PartMidiAutomationChanged {
3200 part: usize,
3201 old_pitch_bends: Vec<MidiPitchBend>,
3202 new_pitch_bends: Vec<MidiPitchBend>,
3203 old_control_changes: Vec<MidiControlChange>,
3204 new_control_changes: Vec<MidiControlChange>,
3205 old_program_changes: Vec<MidiProgramChange>,
3206 new_program_changes: Vec<MidiProgramChange>,
3207 old_aftertouch: Vec<MidiAftertouch>,
3208 new_aftertouch: Vec<MidiAftertouch>,
3209 },
3210 StaffConfigurationChanged {
3211 part: usize,
3212 staff: usize,
3213 old_clef: Clef,
3214 new_clef: Clef,
3215 old_transpose_semitones: i8,
3216 new_transpose_semitones: i8,
3217 },
3218 StaffPresentationChanged {
3219 part: usize,
3220 staff: usize,
3221 old: StaffPresentation,
3222 new: StaffPresentation,
3223 },
3224 MeasurePresentationChanged {
3225 part: usize,
3226 staff: usize,
3227 measure: usize,
3228 old_number: u32,
3229 new_number: u32,
3230 old_clef: Option<Clef>,
3231 new_clef: Option<Clef>,
3232 old_tempo_text: Option<String>,
3233 new_tempo_text: Option<String>,
3234 old_navigation: Option<String>,
3235 new_navigation: Option<String>,
3236 old_expression_text: Option<String>,
3237 new_expression_text: Option<String>,
3238 old_multi_rest_count: Option<u8>,
3239 new_multi_rest_count: Option<u8>,
3240 old_system_break: bool,
3241 new_system_break: bool,
3242 old_page_break: bool,
3243 new_page_break: bool,
3244 old_section_break: bool,
3245 new_section_break: bool,
3246 },
3247 TablatureConfigChanged {
3248 part: usize,
3249 staff: usize,
3250 old: Option<TablatureConfig>,
3251 new: Option<TablatureConfig>,
3252 },
3253 TablatureChangeChanged {
3254 part: usize,
3255 staff: usize,
3256 measure: usize,
3257 old: Option<TablatureConfig>,
3258 new: Option<TablatureConfig>,
3259 },
3260 MidMeasureClefsChanged {
3261 part: usize,
3262 staff: usize,
3263 measure: usize,
3264 old: Vec<MidMeasureClef>,
3265 new: Vec<MidMeasureClef>,
3266 },
3267 UnrepresentedFieldChanged {
3272 path: String,
3273 },
3274 MetadataChanged {
3275 field: String,
3276 old: String,
3277 new: String,
3278 },
3279 TempoChanged {
3280 old: u16,
3281 new: u16,
3282 },
3283 KeySignatureChanged {
3284 old: KeySignature,
3285 new: KeySignature,
3286 },
3287 PartAdded {
3288 part_index: usize,
3289 },
3290 PartRemoved {
3291 part_index: usize,
3292 name: String,
3293 },
3294 NoteAdded {
3295 part: usize,
3296 staff: usize,
3297 measure: usize,
3298 voice: usize,
3299 note_index: usize,
3300 },
3301 NoteRemoved {
3302 part: usize,
3303 staff: usize,
3304 measure: usize,
3305 voice: usize,
3306 note: Box<Note>,
3307 },
3308 NoteModified {
3309 part: usize,
3310 staff: usize,
3311 measure: usize,
3312 voice: usize,
3313 note_index: usize,
3314 old: Box<Note>,
3315 new: Box<Note>,
3316 },
3317 TimeSigChanged {
3318 part: usize,
3319 staff: usize,
3320 measure: usize,
3321 old: Option<TimeSignature>,
3322 new: Option<TimeSignature>,
3323 },
3324 TimeVisibilityChanged {
3325 part: usize,
3326 staff: usize,
3327 measure: usize,
3328 old: bool,
3329 new: bool,
3330 },
3331 TimeSymbolChanged {
3332 part: usize,
3333 staff: usize,
3334 measure: usize,
3335 old: Option<TimeSymbol>,
3336 new: Option<TimeSymbol>,
3337 },
3338 MeasureTempoChanged {
3339 part: usize,
3340 staff: usize,
3341 measure: usize,
3342 old: Option<u16>,
3343 new: Option<u16>,
3344 },
3345 MeasureMetronomeMarkChanged {
3346 part: usize,
3347 staff: usize,
3348 measure: usize,
3349 old: Option<MetronomeMark>,
3350 new: Option<MetronomeMark>,
3351 },
3352 MeasureTempoRampChanged {
3353 part: usize,
3354 staff: usize,
3355 measure: usize,
3356 old: Option<u16>,
3357 new: Option<u16>,
3358 },
3359 BarlineChanged {
3360 part: usize,
3361 staff: usize,
3362 measure: usize,
3363 },
3364 RehearsalMarkChanged {
3365 part: usize,
3366 staff: usize,
3367 measure: usize,
3368 old: Option<String>,
3369 new: Option<String>,
3370 },
3371 VoltaChanged {
3372 part: usize,
3373 staff: usize,
3374 measure: usize,
3375 },
3376}
3377
3378pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
3384 let mut changes: Vec<ScoreChange> = Vec::new();
3385
3386 if a.views != b.views {
3387 changes.push(ScoreChange::ScoreViewsChanged {
3388 old: a.views.clone(),
3389 new: b.views.clone(),
3390 });
3391 }
3392
3393 if a.texts != b.texts {
3394 changes.push(ScoreChange::ScoreTextChanged {
3395 old: a.texts.clone(),
3396 new: b.texts.clone(),
3397 });
3398 }
3399 if a.chord_definitions != b.chord_definitions {
3400 changes.push(ScoreChange::ChordDefinitionsChanged {
3401 old: a.chord_definitions.clone(),
3402 new: b.chord_definitions.clone(),
3403 });
3404 }
3405 if let Some(path) = first_unrepresented_field_change(a, b) {
3406 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
3407 }
3408
3409 macro_rules! meta {
3410 ($field:ident, $name:literal) => {
3411 if a.metadata.$field != b.metadata.$field {
3412 changes.push(ScoreChange::MetadataChanged {
3413 field: $name.to_string(),
3414 old: a.metadata.$field.clone(),
3415 new: b.metadata.$field.clone(),
3416 });
3417 }
3418 };
3419 }
3420 meta!(title, "title");
3421 meta!(composer, "composer");
3422 meta!(lyricist, "lyricist");
3423 meta!(copyright, "copyright");
3424 meta!(work_number, "work_number");
3425 meta!(movement_title, "movement_title");
3426
3427 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3428 changes.push(ScoreChange::TempoChanged {
3429 old: a.settings.tempo_bpm,
3430 new: b.settings.tempo_bpm,
3431 });
3432 }
3433 if a.settings.key_signature != b.settings.key_signature {
3434 changes.push(ScoreChange::KeySignatureChanged {
3435 old: a.settings.key_signature.clone(),
3436 new: b.settings.key_signature.clone(),
3437 });
3438 }
3439
3440 let a_len = a.parts.len();
3441 let b_len = b.parts.len();
3442 for i in b_len..a_len {
3443 changes.push(ScoreChange::PartRemoved {
3444 part_index: i,
3445 name: a.parts[i].name.clone(),
3446 });
3447 }
3448 for i in a_len..b_len {
3449 changes.push(ScoreChange::PartAdded { part_index: i });
3450 }
3451
3452 for pi in 0..a_len.min(b_len) {
3453 let ap = &a.parts[pi];
3454 let bp = &b.parts[pi];
3455 if ap.name != bp.name || ap.short_name != bp.short_name {
3456 changes.push(ScoreChange::PartNamesChanged {
3457 part: pi,
3458 old_name: ap.name.clone(),
3459 new_name: bp.name.clone(),
3460 old_short_name: ap.short_name.clone(),
3461 new_short_name: bp.short_name.clone(),
3462 });
3463 }
3464 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3465 changes.push(ScoreChange::PartMidiChanged {
3466 part: pi,
3467 old_channel: ap.midi_channel,
3468 new_channel: bp.midi_channel,
3469 old_program: ap.midi_program,
3470 new_program: bp.midi_program,
3471 });
3472 }
3473 if ap.instrument != bp.instrument {
3474 changes.push(ScoreChange::InstrumentDefinitionChanged {
3475 part: pi,
3476 old: ap.instrument.clone(),
3477 new: bp.instrument.clone(),
3478 });
3479 }
3480 if ap.midi_pitch_bends != bp.midi_pitch_bends
3481 || ap.midi_control_changes != bp.midi_control_changes
3482 || ap.midi_program_changes != bp.midi_program_changes
3483 || ap.midi_aftertouch != bp.midi_aftertouch
3484 {
3485 changes.push(ScoreChange::PartMidiAutomationChanged {
3486 part: pi,
3487 old_pitch_bends: ap.midi_pitch_bends.clone(),
3488 new_pitch_bends: bp.midi_pitch_bends.clone(),
3489 old_control_changes: ap.midi_control_changes.clone(),
3490 new_control_changes: bp.midi_control_changes.clone(),
3491 old_program_changes: ap.midi_program_changes.clone(),
3492 new_program_changes: bp.midi_program_changes.clone(),
3493 old_aftertouch: ap.midi_aftertouch.clone(),
3494 new_aftertouch: bp.midi_aftertouch.clone(),
3495 });
3496 }
3497 for si in 0..ap.staves.len().min(bp.staves.len()) {
3498 let a_staff = &ap.staves[si];
3499 let b_staff = &bp.staves[si];
3500 if a_staff.clef != b_staff.clef
3501 || a_staff.transpose_semitones != b_staff.transpose_semitones
3502 {
3503 changes.push(ScoreChange::StaffConfigurationChanged {
3504 part: pi,
3505 staff: si,
3506 old_clef: a_staff.clef.clone(),
3507 new_clef: b_staff.clef.clone(),
3508 old_transpose_semitones: a_staff.transpose_semitones,
3509 new_transpose_semitones: b_staff.transpose_semitones,
3510 });
3511 }
3512 if a_staff.tablature != b_staff.tablature {
3513 changes.push(ScoreChange::TablatureConfigChanged {
3514 part: pi,
3515 staff: si,
3516 old: a_staff.tablature.clone(),
3517 new: b_staff.tablature.clone(),
3518 });
3519 }
3520 if a_staff.presentation != b_staff.presentation {
3521 changes.push(ScoreChange::StaffPresentationChanged {
3522 part: pi,
3523 staff: si,
3524 old: a_staff.presentation.clone(),
3525 new: b_staff.presentation.clone(),
3526 });
3527 }
3528 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3529 let am = &a_staff.measures[mi];
3530 let bm = &b_staff.measures[mi];
3531 if am.tablature_change != bm.tablature_change {
3532 changes.push(ScoreChange::TablatureChangeChanged {
3533 part: pi,
3534 staff: si,
3535 measure: mi,
3536 old: am.tablature_change.clone(),
3537 new: bm.tablature_change.clone(),
3538 });
3539 }
3540 if am.mid_clefs != bm.mid_clefs {
3541 changes.push(ScoreChange::MidMeasureClefsChanged {
3542 part: pi,
3543 staff: si,
3544 measure: mi,
3545 old: am.mid_clefs.clone(),
3546 new: bm.mid_clefs.clone(),
3547 });
3548 }
3549 if am.number != bm.number
3550 || am.clef != bm.clef
3551 || am.tempo_text != bm.tempo_text
3552 || am.navigation != bm.navigation
3553 || am.expression_text != bm.expression_text
3554 || am.multi_rest_count != bm.multi_rest_count
3555 || am.system_break != bm.system_break
3556 || am.page_break != bm.page_break
3557 || am.section_break != bm.section_break
3558 {
3559 changes.push(ScoreChange::MeasurePresentationChanged {
3560 part: pi,
3561 staff: si,
3562 measure: mi,
3563 old_number: am.number,
3564 new_number: bm.number,
3565 old_clef: am.clef.clone(),
3566 new_clef: bm.clef.clone(),
3567 old_tempo_text: am.tempo_text.clone(),
3568 new_tempo_text: bm.tempo_text.clone(),
3569 old_navigation: am.navigation.clone(),
3570 new_navigation: bm.navigation.clone(),
3571 old_expression_text: am.expression_text.clone(),
3572 new_expression_text: bm.expression_text.clone(),
3573 old_multi_rest_count: am.multi_rest_count,
3574 new_multi_rest_count: bm.multi_rest_count,
3575 old_system_break: am.system_break,
3576 new_system_break: bm.system_break,
3577 old_page_break: am.page_break,
3578 new_page_break: bm.page_break,
3579 old_section_break: am.section_break,
3580 new_section_break: bm.section_break,
3581 });
3582 }
3583 for vi in 0..4usize {
3584 let av = &am.voices[vi];
3585 let bv = &bm.voices[vi];
3586 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3587 if !note_content_eq(a_note, b_note) {
3588 changes.push(ScoreChange::NoteModified {
3589 part: pi,
3590 staff: si,
3591 measure: mi,
3592 voice: vi,
3593 note_index: ni,
3594 old: Box::new(a_note.clone()),
3595 new: Box::new(b_note.clone()),
3596 });
3597 }
3598 }
3599 for note in av.iter().skip(bv.len()) {
3600 changes.push(ScoreChange::NoteRemoved {
3601 part: pi,
3602 staff: si,
3603 measure: mi,
3604 voice: vi,
3605 note: Box::new(note.clone()),
3606 });
3607 }
3608 for ni in av.len()..bv.len() {
3609 changes.push(ScoreChange::NoteAdded {
3610 part: pi,
3611 staff: si,
3612 measure: mi,
3613 voice: vi,
3614 note_index: ni,
3615 });
3616 }
3617 }
3618 if am.time_sig != bm.time_sig {
3619 changes.push(ScoreChange::TimeSigChanged {
3620 part: pi,
3621 staff: si,
3622 measure: mi,
3623 old: am.time_sig.clone(),
3624 new: bm.time_sig.clone(),
3625 });
3626 }
3627 if am.time_sig_hidden != bm.time_sig_hidden {
3628 changes.push(ScoreChange::TimeVisibilityChanged {
3629 part: pi,
3630 staff: si,
3631 measure: mi,
3632 old: am.time_sig_hidden,
3633 new: bm.time_sig_hidden,
3634 });
3635 }
3636 if am.time_symbol != bm.time_symbol {
3637 changes.push(ScoreChange::TimeSymbolChanged {
3638 part: pi,
3639 staff: si,
3640 measure: mi,
3641 old: am.time_symbol,
3642 new: bm.time_symbol,
3643 });
3644 }
3645 if am.tempo != bm.tempo {
3646 changes.push(ScoreChange::MeasureTempoChanged {
3647 part: pi,
3648 staff: si,
3649 measure: mi,
3650 old: am.tempo,
3651 new: bm.tempo,
3652 });
3653 }
3654 if am.metronome_mark != bm.metronome_mark {
3655 changes.push(ScoreChange::MeasureMetronomeMarkChanged {
3656 part: pi,
3657 staff: si,
3658 measure: mi,
3659 old: am.metronome_mark.clone(),
3660 new: bm.metronome_mark.clone(),
3661 });
3662 }
3663 if am.tempo_ramp_to != bm.tempo_ramp_to {
3664 changes.push(ScoreChange::MeasureTempoRampChanged {
3665 part: pi,
3666 staff: si,
3667 measure: mi,
3668 old: am.tempo_ramp_to,
3669 new: bm.tempo_ramp_to,
3670 });
3671 }
3672 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3673 changes.push(ScoreChange::BarlineChanged {
3674 part: pi,
3675 staff: si,
3676 measure: mi,
3677 });
3678 }
3679 if am.rehearsal != bm.rehearsal {
3680 changes.push(ScoreChange::RehearsalMarkChanged {
3681 part: pi,
3682 staff: si,
3683 measure: mi,
3684 old: am.rehearsal.clone(),
3685 new: bm.rehearsal.clone(),
3686 });
3687 }
3688 if am.volta != bm.volta {
3689 changes.push(ScoreChange::VoltaChanged {
3690 part: pi,
3691 staff: si,
3692 measure: mi,
3693 });
3694 }
3695 if am.texts != bm.texts {
3696 changes.push(ScoreChange::MeasureTextChanged {
3697 part: pi,
3698 staff: si,
3699 measure: mi,
3700 old: am.texts.clone(),
3701 new: bm.texts.clone(),
3702 });
3703 }
3704 if am.figured_bass != bm.figured_bass {
3705 changes.push(ScoreChange::FiguredBassChanged {
3706 part: pi,
3707 staff: si,
3708 measure: mi,
3709 old: am.figured_bass.clone(),
3710 new: bm.figured_bass.clone(),
3711 });
3712 }
3713 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3714 changes.push(ScoreChange::HarpPedalDiagramsChanged {
3715 part: pi,
3716 staff: si,
3717 measure: mi,
3718 old: am.harp_pedal_diagrams.clone(),
3719 new: bm.harp_pedal_diagrams.clone(),
3720 });
3721 }
3722 if am.timed_pedals != bm.timed_pedals {
3723 changes.push(ScoreChange::TimedPedalsChanged {
3724 part: pi,
3725 staff: si,
3726 measure: mi,
3727 old: am.timed_pedals.clone(),
3728 new: bm.timed_pedals.clone(),
3729 });
3730 }
3731 }
3732 }
3733 }
3734
3735 changes
3736}
3737
3738fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
3739 if a.settings.time_signature != b.settings.time_signature {
3740 return Some("settings.time_signature".to_string());
3741 }
3742 if a.part_groups.len() != b.part_groups.len()
3743 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3744 x.first_part != y.first_part
3745 || x.last_part != y.last_part
3746 || x.symbol != y.symbol
3747 || x.barlines_connect != y.barlines_connect
3748 })
3749 {
3750 return Some("part_groups".to_string());
3751 }
3752 a.parts
3753 .iter()
3754 .zip(&b.parts)
3755 .enumerate()
3756 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
3757}
3758
3759fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
3760 let prefix = format!("parts[{part_index}]");
3761 if a.named_channels != b.named_channels {
3762 return Some(format!("{prefix}.named_channels"));
3763 }
3764 if a.percussion_instruments != b.percussion_instruments {
3765 return Some(format!("{prefix}.percussion_instruments"));
3766 }
3767 if a.staff_groups != b.staff_groups {
3768 return Some(format!("{prefix}.staff_groups"));
3769 }
3770 if a.staves.len() != b.staves.len() {
3771 return Some(format!("{prefix}.staves"));
3772 }
3773 a.staves
3774 .iter()
3775 .zip(&b.staves)
3776 .enumerate()
3777 .find_map(|(staff_index, (a, b))| {
3778 first_unrepresented_staff_change(&prefix, staff_index, a, b)
3779 })
3780}
3781
3782fn first_unrepresented_staff_change(
3783 part_prefix: &str,
3784 staff_index: usize,
3785 a: &Staff,
3786 b: &Staff,
3787) -> Option<String> {
3788 let prefix = format!("{part_prefix}.staves[{staff_index}]");
3789 if a.measures.len() != b.measures.len() {
3790 return Some(format!("{prefix}.measures"));
3791 }
3792 a.measures
3793 .iter()
3794 .zip(&b.measures)
3795 .enumerate()
3796 .find_map(|(measure_index, (a, b))| {
3797 first_unrepresented_measure_change(&prefix, measure_index, a, b)
3798 })
3799}
3800
3801fn first_unrepresented_measure_change(
3802 staff_prefix: &str,
3803 measure_index: usize,
3804 a: &Measure,
3805 b: &Measure,
3806) -> Option<String> {
3807 (a.channel_switches != b.channel_switches)
3808 .then(|| format!("{staff_prefix}.measures[{measure_index}].channel_switches"))
3809}
3810
3811#[derive(Debug, Clone, Serialize, Deserialize)]
3818pub enum ScorePatch {
3819 SetScoreViews {
3820 value: Vec<ScoreView>,
3821 },
3822 SetScoreTexts {
3823 value: Vec<StyledText>,
3824 },
3825 SetMeasureTexts {
3826 part: usize,
3827 staff: usize,
3828 measure: usize,
3829 value: Vec<StyledText>,
3830 },
3831 SetFiguredBass {
3832 part: usize,
3833 staff: usize,
3834 measure: usize,
3835 value: Vec<FiguredBassFigure>,
3836 },
3837 SetHarpPedalDiagrams {
3838 part: usize,
3839 staff: usize,
3840 measure: usize,
3841 value: Vec<HarpPedalDiagram>,
3842 },
3843 SetTimedPedals {
3844 part: usize,
3845 staff: usize,
3846 measure: usize,
3847 value: Vec<TimedPedalDirection>,
3848 },
3849 SetChordDefinitions {
3850 value: Vec<ChordDefinition>,
3851 },
3852 SetPartNames {
3853 part: usize,
3854 name: String,
3855 short_name: String,
3856 },
3857 SetPartMidi {
3858 part: usize,
3859 channel: u8,
3860 program: u8,
3861 },
3862 SetInstrumentDefinition {
3863 part: usize,
3864 value: Option<InstrumentDefinition>,
3865 },
3866 SetPartMidiAutomation {
3867 part: usize,
3868 pitch_bends: Vec<MidiPitchBend>,
3869 control_changes: Vec<MidiControlChange>,
3870 program_changes: Vec<MidiProgramChange>,
3871 aftertouch: Vec<MidiAftertouch>,
3872 },
3873 SetStaffConfiguration {
3874 part: usize,
3875 staff: usize,
3876 clef: Clef,
3877 transpose_semitones: i8,
3878 },
3879 SetStaffPresentation {
3880 part: usize,
3881 staff: usize,
3882 value: StaffPresentation,
3883 },
3884 SetMeasurePresentation {
3885 part: usize,
3886 staff: usize,
3887 measure: usize,
3888 number: u32,
3889 clef: Option<Clef>,
3890 tempo_text: Option<String>,
3891 navigation: Option<String>,
3892 expression_text: Option<String>,
3893 multi_rest_count: Option<u8>,
3894 system_break: bool,
3895 page_break: bool,
3896 #[serde(default)]
3897 section_break: bool,
3898 },
3899 SetTablatureConfig {
3900 part: usize,
3901 staff: usize,
3902 value: Option<TablatureConfig>,
3903 },
3904 SetMeasureTablatureChange {
3905 part: usize,
3906 staff: usize,
3907 measure: usize,
3908 value: Option<TablatureConfig>,
3909 },
3910 SetMeasureMidClefs {
3911 part: usize,
3912 staff: usize,
3913 measure: usize,
3914 value: Vec<MidMeasureClef>,
3915 },
3916 SetMetadata {
3917 field: String,
3918 value: String,
3919 },
3920 SetTempo {
3921 value: u16,
3922 },
3923 SetKeySignature {
3924 part: usize,
3925 staff: usize,
3926 measure: usize,
3927 value: Option<KeySignature>,
3928 },
3929 SetTimeSignature {
3930 part: usize,
3931 staff: usize,
3932 measure: usize,
3933 value: Option<TimeSignature>,
3934 },
3935 SetTimeVisibility {
3936 part: usize,
3937 staff: usize,
3938 measure: usize,
3939 hidden: bool,
3940 },
3941 SetTimeSymbol {
3942 part: usize,
3943 staff: usize,
3944 measure: usize,
3945 value: Option<TimeSymbol>,
3946 },
3947 SetBarlines {
3948 part: usize,
3949 staff: usize,
3950 measure: usize,
3951 left: Barline,
3952 right: Barline,
3953 },
3954 SetRehearsal {
3955 part: usize,
3956 staff: usize,
3957 measure: usize,
3958 value: Option<String>,
3959 },
3960 SetVolta {
3961 part: usize,
3962 staff: usize,
3963 measure: usize,
3964 value: Option<VoltaBracket>,
3965 },
3966 AddNote {
3968 part: usize,
3969 staff: usize,
3970 measure: usize,
3971 voice: usize,
3972 #[serde(default = "legacy_append_index")]
3974 note_index: usize,
3975 note: Box<Note>,
3976 },
3977 RemoveNote {
3978 part: usize,
3979 staff: usize,
3980 measure: usize,
3981 voice: usize,
3982 note_index: usize,
3983 },
3984 ReplaceNote {
3986 part: usize,
3987 staff: usize,
3988 measure: usize,
3989 voice: usize,
3990 note_index: usize,
3991 note: Box<Note>,
3992 },
3993 SetMeasureTempo {
3994 part: usize,
3995 staff: usize,
3996 measure: usize,
3997 value: Option<u16>,
3998 },
3999 SetMeasureMetronomeMark {
4000 part: usize,
4001 staff: usize,
4002 measure: usize,
4003 value: Option<MetronomeMark>,
4004 },
4005 SetMeasureTempoRamp {
4006 part: usize,
4007 staff: usize,
4008 measure: usize,
4009 value: Option<u16>,
4010 },
4011 ReplaceScore {
4014 score: Box<Score>,
4015 },
4016}
4017
4018fn legacy_append_index() -> usize {
4019 usize::MAX
4020}
4021
4022fn patch_requires_replace(a: &Score, b: &Score) -> bool {
4026 if a.settings.time_signature != b.settings.time_signature
4027 || a.settings.key_signature != b.settings.key_signature
4028 || a.parts.len() != b.parts.len()
4029 || a.part_groups.len() != b.part_groups.len()
4030 {
4031 return true;
4032 }
4033 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
4034 x.first_part != y.first_part
4035 || x.last_part != y.last_part
4036 || x.symbol != y.symbol
4037 || x.barlines_connect != y.barlines_connect
4038 }) {
4039 return true;
4040 }
4041 for (ap, bp) in a.parts.iter().zip(&b.parts) {
4042 if ap.percussion_instruments != bp.percussion_instruments
4043 || ap.named_channels != bp.named_channels
4044 || ap.staff_groups != bp.staff_groups
4045 || ap.staves.len() != bp.staves.len()
4046 {
4047 return true;
4048 }
4049 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
4050 if as_.measures.len() != bs.measures.len()
4051 || as_
4052 .measures
4053 .iter()
4054 .zip(&bs.measures)
4055 .any(|(a, b)| a.channel_switches != b.channel_switches)
4056 {
4057 return true;
4058 }
4059 }
4060 }
4061 false
4062}
4063
4064pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
4069 let mut patches: Vec<ScorePatch> = Vec::new();
4070
4071 if patch_requires_replace(a, b) {
4072 return vec![ScorePatch::ReplaceScore {
4073 score: Box::new(b.clone()),
4074 }];
4075 }
4076
4077 if a.views != b.views {
4078 patches.push(ScorePatch::SetScoreViews {
4079 value: b.views.clone(),
4080 });
4081 }
4082
4083 if a.texts != b.texts {
4084 patches.push(ScorePatch::SetScoreTexts {
4085 value: b.texts.clone(),
4086 });
4087 }
4088 if a.chord_definitions != b.chord_definitions {
4089 patches.push(ScorePatch::SetChordDefinitions {
4090 value: b.chord_definitions.clone(),
4091 });
4092 }
4093
4094 macro_rules! meta {
4095 ($field:ident, $name:literal) => {
4096 if a.metadata.$field != b.metadata.$field {
4097 patches.push(ScorePatch::SetMetadata {
4098 field: $name.to_string(),
4099 value: b.metadata.$field.clone(),
4100 });
4101 }
4102 };
4103 }
4104 meta!(title, "title");
4105 meta!(composer, "composer");
4106 meta!(lyricist, "lyricist");
4107 meta!(copyright, "copyright");
4108 meta!(work_number, "work_number");
4109 meta!(movement_title, "movement_title");
4110
4111 if a.settings.tempo_bpm != b.settings.tempo_bpm {
4112 patches.push(ScorePatch::SetTempo {
4113 value: b.settings.tempo_bpm,
4114 });
4115 }
4116
4117 for pi in 0..a.parts.len().min(b.parts.len()) {
4118 let ap = &a.parts[pi];
4119 let bp = &b.parts[pi];
4120 if ap.name != bp.name || ap.short_name != bp.short_name {
4121 patches.push(ScorePatch::SetPartNames {
4122 part: pi,
4123 name: bp.name.clone(),
4124 short_name: bp.short_name.clone(),
4125 });
4126 }
4127 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
4128 patches.push(ScorePatch::SetPartMidi {
4129 part: pi,
4130 channel: bp.midi_channel,
4131 program: bp.midi_program,
4132 });
4133 }
4134 if ap.instrument != bp.instrument {
4135 patches.push(ScorePatch::SetInstrumentDefinition {
4136 part: pi,
4137 value: bp.instrument.clone(),
4138 });
4139 }
4140 if ap.midi_pitch_bends != bp.midi_pitch_bends
4141 || ap.midi_control_changes != bp.midi_control_changes
4142 || ap.midi_program_changes != bp.midi_program_changes
4143 || ap.midi_aftertouch != bp.midi_aftertouch
4144 {
4145 patches.push(ScorePatch::SetPartMidiAutomation {
4146 part: pi,
4147 pitch_bends: bp.midi_pitch_bends.clone(),
4148 control_changes: bp.midi_control_changes.clone(),
4149 program_changes: bp.midi_program_changes.clone(),
4150 aftertouch: bp.midi_aftertouch.clone(),
4151 });
4152 }
4153 for si in 0..ap.staves.len().min(bp.staves.len()) {
4154 let a_staff = &ap.staves[si];
4155 let b_staff = &bp.staves[si];
4156 if a_staff.clef != b_staff.clef
4157 || a_staff.transpose_semitones != b_staff.transpose_semitones
4158 {
4159 patches.push(ScorePatch::SetStaffConfiguration {
4160 part: pi,
4161 staff: si,
4162 clef: b_staff.clef.clone(),
4163 transpose_semitones: b_staff.transpose_semitones,
4164 });
4165 }
4166 if a_staff.tablature != b_staff.tablature {
4167 patches.push(ScorePatch::SetTablatureConfig {
4168 part: pi,
4169 staff: si,
4170 value: b_staff.tablature.clone(),
4171 });
4172 }
4173 if a_staff.presentation != b_staff.presentation {
4174 patches.push(ScorePatch::SetStaffPresentation {
4175 part: pi,
4176 staff: si,
4177 value: b_staff.presentation.clone(),
4178 });
4179 }
4180 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
4181 let am = &a_staff.measures[mi];
4182 let bm = &b_staff.measures[mi];
4183
4184 if am.tablature_change != bm.tablature_change {
4185 patches.push(ScorePatch::SetMeasureTablatureChange {
4186 part: pi,
4187 staff: si,
4188 measure: mi,
4189 value: bm.tablature_change.clone(),
4190 });
4191 }
4192
4193 if am.mid_clefs != bm.mid_clefs {
4194 patches.push(ScorePatch::SetMeasureMidClefs {
4195 part: pi,
4196 staff: si,
4197 measure: mi,
4198 value: bm.mid_clefs.clone(),
4199 });
4200 }
4201
4202 if am.number != bm.number
4203 || am.clef != bm.clef
4204 || am.tempo_text != bm.tempo_text
4205 || am.navigation != bm.navigation
4206 || am.expression_text != bm.expression_text
4207 || am.multi_rest_count != bm.multi_rest_count
4208 || am.system_break != bm.system_break
4209 || am.page_break != bm.page_break
4210 || am.section_break != bm.section_break
4211 {
4212 patches.push(ScorePatch::SetMeasurePresentation {
4213 part: pi,
4214 staff: si,
4215 measure: mi,
4216 number: bm.number,
4217 clef: bm.clef.clone(),
4218 tempo_text: bm.tempo_text.clone(),
4219 navigation: bm.navigation.clone(),
4220 expression_text: bm.expression_text.clone(),
4221 multi_rest_count: bm.multi_rest_count,
4222 system_break: bm.system_break,
4223 page_break: bm.page_break,
4224 section_break: bm.section_break,
4225 });
4226 }
4227 if am.key_sig != bm.key_sig {
4228 patches.push(ScorePatch::SetKeySignature {
4229 part: pi,
4230 staff: si,
4231 measure: mi,
4232 value: bm.key_sig.clone(),
4233 });
4234 }
4235 if am.time_sig != bm.time_sig {
4236 patches.push(ScorePatch::SetTimeSignature {
4237 part: pi,
4238 staff: si,
4239 measure: mi,
4240 value: bm.time_sig.clone(),
4241 });
4242 }
4243 if am.time_sig_hidden != bm.time_sig_hidden {
4244 patches.push(ScorePatch::SetTimeVisibility {
4245 part: pi,
4246 staff: si,
4247 measure: mi,
4248 hidden: bm.time_sig_hidden,
4249 });
4250 }
4251 if am.time_symbol != bm.time_symbol {
4252 patches.push(ScorePatch::SetTimeSymbol {
4253 part: pi,
4254 staff: si,
4255 measure: mi,
4256 value: bm.time_symbol,
4257 });
4258 }
4259 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
4260 patches.push(ScorePatch::SetBarlines {
4261 part: pi,
4262 staff: si,
4263 measure: mi,
4264 left: bm.barline_left.clone(),
4265 right: bm.barline_right.clone(),
4266 });
4267 }
4268 if am.rehearsal != bm.rehearsal {
4269 patches.push(ScorePatch::SetRehearsal {
4270 part: pi,
4271 staff: si,
4272 measure: mi,
4273 value: bm.rehearsal.clone(),
4274 });
4275 }
4276 if am.volta != bm.volta {
4277 patches.push(ScorePatch::SetVolta {
4278 part: pi,
4279 staff: si,
4280 measure: mi,
4281 value: bm.volta.clone(),
4282 });
4283 }
4284 if am.tempo != bm.tempo {
4285 patches.push(ScorePatch::SetMeasureTempo {
4286 part: pi,
4287 staff: si,
4288 measure: mi,
4289 value: bm.tempo,
4290 });
4291 }
4292 if am.metronome_mark != bm.metronome_mark {
4293 patches.push(ScorePatch::SetMeasureMetronomeMark {
4294 part: pi,
4295 staff: si,
4296 measure: mi,
4297 value: bm.metronome_mark.clone(),
4298 });
4299 }
4300 if am.tempo_ramp_to != bm.tempo_ramp_to {
4301 patches.push(ScorePatch::SetMeasureTempoRamp {
4302 part: pi,
4303 staff: si,
4304 measure: mi,
4305 value: bm.tempo_ramp_to,
4306 });
4307 }
4308 if am.texts != bm.texts {
4309 patches.push(ScorePatch::SetMeasureTexts {
4310 part: pi,
4311 staff: si,
4312 measure: mi,
4313 value: bm.texts.clone(),
4314 });
4315 }
4316 if am.figured_bass != bm.figured_bass {
4317 patches.push(ScorePatch::SetFiguredBass {
4318 part: pi,
4319 staff: si,
4320 measure: mi,
4321 value: bm.figured_bass.clone(),
4322 });
4323 }
4324 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
4325 patches.push(ScorePatch::SetHarpPedalDiagrams {
4326 part: pi,
4327 staff: si,
4328 measure: mi,
4329 value: bm.harp_pedal_diagrams.clone(),
4330 });
4331 }
4332
4333 if am.timed_pedals != bm.timed_pedals {
4334 patches.push(ScorePatch::SetTimedPedals {
4335 part: pi,
4336 staff: si,
4337 measure: mi,
4338 value: bm.timed_pedals.clone(),
4339 });
4340 }
4341
4342 for vi in 0..4usize {
4343 let av = &am.voices[vi];
4344 let bv = &bm.voices[vi];
4345 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
4346 if !note_content_eq(a_note, b_note) {
4347 patches.push(ScorePatch::ReplaceNote {
4348 part: pi,
4349 staff: si,
4350 measure: mi,
4351 voice: vi,
4352 note_index: ni,
4353 note: Box::new(b_note.clone()),
4354 });
4355 }
4356 }
4357 for ni in (bv.len()..av.len()).rev() {
4359 patches.push(ScorePatch::RemoveNote {
4360 part: pi,
4361 staff: si,
4362 measure: mi,
4363 voice: vi,
4364 note_index: ni,
4365 });
4366 }
4367 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
4369 patches.push(ScorePatch::AddNote {
4370 part: pi,
4371 staff: si,
4372 measure: mi,
4373 voice: vi,
4374 note_index: av.len() + offset,
4375 note: Box::new(note.clone()),
4376 });
4377 }
4378 }
4379 }
4380 }
4381 }
4382
4383 patches
4384}
4385
4386pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
4391 let mut s = score.clone();
4392 for patch in patches {
4393 match patch {
4394 ScorePatch::ReplaceScore { score } => {
4395 s = (**score).clone();
4396 }
4397 ScorePatch::SetScoreViews { value } => {
4398 s.views = value.clone();
4399 }
4400 ScorePatch::SetScoreTexts { value } => {
4401 s.texts = value.clone();
4402 }
4403 ScorePatch::SetMeasureTexts {
4404 part,
4405 staff,
4406 measure,
4407 value,
4408 } => {
4409 s.parts
4410 .get_mut(*part)
4411 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4412 .staves
4413 .get_mut(*staff)
4414 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4415 .measures
4416 .get_mut(*measure)
4417 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4418 .texts = value.clone();
4419 }
4420 ScorePatch::SetFiguredBass {
4421 part,
4422 staff,
4423 measure,
4424 value,
4425 } => {
4426 s.parts
4427 .get_mut(*part)
4428 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4429 .staves
4430 .get_mut(*staff)
4431 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4432 .measures
4433 .get_mut(*measure)
4434 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4435 .figured_bass = value.clone();
4436 }
4437 ScorePatch::SetHarpPedalDiagrams {
4438 part,
4439 staff,
4440 measure,
4441 value,
4442 } => {
4443 s.parts
4444 .get_mut(*part)
4445 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4446 .staves
4447 .get_mut(*staff)
4448 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4449 .measures
4450 .get_mut(*measure)
4451 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4452 .harp_pedal_diagrams = value.clone();
4453 }
4454 ScorePatch::SetTimedPedals {
4455 part,
4456 staff,
4457 measure,
4458 value,
4459 } => {
4460 s.parts
4461 .get_mut(*part)
4462 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4463 .staves
4464 .get_mut(*staff)
4465 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4466 .measures
4467 .get_mut(*measure)
4468 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4469 .timed_pedals = value.clone();
4470 }
4471 ScorePatch::SetChordDefinitions { value } => {
4472 s.chord_definitions = value.clone();
4473 }
4474 ScorePatch::SetPartNames {
4475 part,
4476 name,
4477 short_name,
4478 } => {
4479 let target = s
4480 .parts
4481 .get_mut(*part)
4482 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4483 target.name = name.clone();
4484 target.short_name = short_name.clone();
4485 }
4486 ScorePatch::SetPartMidi {
4487 part,
4488 channel,
4489 program,
4490 } => {
4491 let target = s
4492 .parts
4493 .get_mut(*part)
4494 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4495 target.midi_channel = *channel;
4496 target.midi_program = *program;
4497 }
4498 ScorePatch::SetInstrumentDefinition { part, value } => {
4499 s.parts
4500 .get_mut(*part)
4501 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4502 .instrument = value.clone();
4503 }
4504 ScorePatch::SetPartMidiAutomation {
4505 part,
4506 pitch_bends,
4507 control_changes,
4508 program_changes,
4509 aftertouch,
4510 } => {
4511 let target = s
4512 .parts
4513 .get_mut(*part)
4514 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4515 target.midi_pitch_bends = pitch_bends.clone();
4516 target.midi_control_changes = control_changes.clone();
4517 target.midi_program_changes = program_changes.clone();
4518 target.midi_aftertouch = aftertouch.clone();
4519 }
4520 ScorePatch::SetStaffConfiguration {
4521 part,
4522 staff,
4523 clef,
4524 transpose_semitones,
4525 } => {
4526 let target = s
4527 .parts
4528 .get_mut(*part)
4529 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4530 .staves
4531 .get_mut(*staff)
4532 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
4533 target.clef = clef.clone();
4534 target.transpose_semitones = *transpose_semitones;
4535 }
4536 ScorePatch::SetStaffPresentation { part, staff, value } => {
4537 let target = s
4538 .parts
4539 .get_mut(*part)
4540 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4541 .staves
4542 .get_mut(*staff)
4543 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
4544 target.presentation = value.clone();
4545 }
4546 ScorePatch::SetMeasurePresentation {
4547 part,
4548 staff,
4549 measure,
4550 number,
4551 clef,
4552 tempo_text,
4553 navigation,
4554 expression_text,
4555 multi_rest_count,
4556 system_break,
4557 page_break,
4558 section_break,
4559 } => {
4560 let target = s
4561 .parts
4562 .get_mut(*part)
4563 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4564 .staves
4565 .get_mut(*staff)
4566 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4567 .measures
4568 .get_mut(*measure)
4569 .ok_or_else(|| {
4570 Error::InvalidPatch(format!("measure {measure} out of range"))
4571 })?;
4572 target.number = *number;
4573 target.clef = clef.clone();
4574 target.tempo_text = tempo_text.clone();
4575 target.navigation = navigation.clone();
4576 target.expression_text = expression_text.clone();
4577 target.multi_rest_count = *multi_rest_count;
4578 target.system_break = *system_break;
4579 target.page_break = *page_break;
4580 target.section_break = *section_break;
4581 }
4582 ScorePatch::SetTablatureConfig { part, staff, value } => {
4583 s.parts
4584 .get_mut(*part)
4585 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4586 .staves
4587 .get_mut(*staff)
4588 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4589 .tablature = value.clone();
4590 }
4591 ScorePatch::SetMeasureTablatureChange {
4592 part,
4593 staff,
4594 measure,
4595 value,
4596 } => {
4597 s.parts
4598 .get_mut(*part)
4599 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4600 .staves
4601 .get_mut(*staff)
4602 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4603 .measures
4604 .get_mut(*measure)
4605 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4606 .tablature_change = value.clone();
4607 }
4608 ScorePatch::SetMeasureMidClefs {
4609 part,
4610 staff,
4611 measure,
4612 value,
4613 } => {
4614 s.parts
4615 .get_mut(*part)
4616 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4617 .staves
4618 .get_mut(*staff)
4619 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4620 .measures
4621 .get_mut(*measure)
4622 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4623 .mid_clefs = value.clone();
4624 }
4625 ScorePatch::SetMetadata { field, value } => match field.as_str() {
4626 "title" => s.metadata.title = value.clone(),
4627 "composer" => s.metadata.composer = value.clone(),
4628 "lyricist" => s.metadata.lyricist = value.clone(),
4629 "copyright" => s.metadata.copyright = value.clone(),
4630 "work_number" => s.metadata.work_number = value.clone(),
4631 "movement_title" => s.metadata.movement_title = value.clone(),
4632 other => {
4633 return Err(Error::InvalidPatch(format!(
4634 "unknown metadata field: {other}"
4635 )));
4636 }
4637 },
4638 ScorePatch::SetTempo { value } => {
4639 s.settings.tempo_bpm = *value;
4640 }
4641 ScorePatch::SetKeySignature {
4642 part,
4643 staff,
4644 measure,
4645 value,
4646 } => {
4647 s.parts
4648 .get_mut(*part)
4649 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4650 .staves
4651 .get_mut(*staff)
4652 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4653 .measures
4654 .get_mut(*measure)
4655 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4656 .key_sig = value.clone();
4657 }
4658 ScorePatch::SetTimeSignature {
4659 part,
4660 staff,
4661 measure,
4662 value,
4663 } => {
4664 s.parts
4665 .get_mut(*part)
4666 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4667 .staves
4668 .get_mut(*staff)
4669 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4670 .measures
4671 .get_mut(*measure)
4672 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4673 .time_sig = value.clone();
4674 }
4675 ScorePatch::SetTimeVisibility {
4676 part,
4677 staff,
4678 measure,
4679 hidden,
4680 } => {
4681 s.parts
4682 .get_mut(*part)
4683 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4684 .staves
4685 .get_mut(*staff)
4686 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4687 .measures
4688 .get_mut(*measure)
4689 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4690 .time_sig_hidden = *hidden;
4691 }
4692 ScorePatch::SetTimeSymbol {
4693 part,
4694 staff,
4695 measure,
4696 value,
4697 } => {
4698 s.parts
4699 .get_mut(*part)
4700 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4701 .staves
4702 .get_mut(*staff)
4703 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4704 .measures
4705 .get_mut(*measure)
4706 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4707 .time_symbol = *value;
4708 }
4709 ScorePatch::SetBarlines {
4710 part,
4711 staff,
4712 measure,
4713 left,
4714 right,
4715 } => {
4716 let m = s
4717 .parts
4718 .get_mut(*part)
4719 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4720 .staves
4721 .get_mut(*staff)
4722 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4723 .measures
4724 .get_mut(*measure)
4725 .ok_or_else(|| {
4726 Error::InvalidPatch(format!("measure {measure} out of range"))
4727 })?;
4728 m.barline_left = left.clone();
4729 m.barline_right = right.clone();
4730 }
4731 ScorePatch::SetRehearsal {
4732 part,
4733 staff,
4734 measure,
4735 value,
4736 } => {
4737 s.parts
4738 .get_mut(*part)
4739 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4740 .staves
4741 .get_mut(*staff)
4742 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4743 .measures
4744 .get_mut(*measure)
4745 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4746 .rehearsal = value.clone();
4747 }
4748 ScorePatch::SetVolta {
4749 part,
4750 staff,
4751 measure,
4752 value,
4753 } => {
4754 s.parts
4755 .get_mut(*part)
4756 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4757 .staves
4758 .get_mut(*staff)
4759 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4760 .measures
4761 .get_mut(*measure)
4762 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4763 .volta = value.clone();
4764 }
4765 ScorePatch::AddNote {
4766 part,
4767 staff,
4768 measure,
4769 voice,
4770 note_index,
4771 note,
4772 } => {
4773 let v = s
4774 .parts
4775 .get_mut(*part)
4776 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4777 .staves
4778 .get_mut(*staff)
4779 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4780 .measures
4781 .get_mut(*measure)
4782 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4783 .voices
4784 .get_mut(*voice)
4785 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4786 let insert_at = if *note_index == usize::MAX {
4787 v.len()
4788 } else {
4789 *note_index
4790 };
4791 if insert_at > v.len() {
4792 return Err(Error::InvalidPatch(format!(
4793 "note_index {note_index} out of range"
4794 )));
4795 }
4796 v.insert(insert_at, *note.clone());
4797 }
4798 ScorePatch::RemoveNote {
4799 part,
4800 staff,
4801 measure,
4802 voice,
4803 note_index,
4804 } => {
4805 let v = s
4806 .parts
4807 .get_mut(*part)
4808 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4809 .staves
4810 .get_mut(*staff)
4811 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4812 .measures
4813 .get_mut(*measure)
4814 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4815 .voices
4816 .get_mut(*voice)
4817 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4818 if *note_index >= v.len() {
4819 return Err(Error::InvalidPatch(format!(
4820 "note_index {note_index} out of range"
4821 )));
4822 }
4823 v.remove(*note_index);
4824 }
4825 ScorePatch::ReplaceNote {
4826 part,
4827 staff,
4828 measure,
4829 voice,
4830 note_index,
4831 note,
4832 } => {
4833 let v = s
4834 .parts
4835 .get_mut(*part)
4836 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4837 .staves
4838 .get_mut(*staff)
4839 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4840 .measures
4841 .get_mut(*measure)
4842 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4843 .voices
4844 .get_mut(*voice)
4845 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4846 if *note_index >= v.len() {
4847 return Err(Error::InvalidPatch(format!(
4848 "note_index {note_index} out of range"
4849 )));
4850 }
4851 v[*note_index] = *note.clone();
4852 }
4853 ScorePatch::SetMeasureTempo {
4854 part,
4855 staff,
4856 measure,
4857 value,
4858 } => {
4859 s.parts
4860 .get_mut(*part)
4861 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4862 .staves
4863 .get_mut(*staff)
4864 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4865 .measures
4866 .get_mut(*measure)
4867 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4868 .tempo = *value;
4869 }
4870 ScorePatch::SetMeasureTempoRamp {
4871 part,
4872 staff,
4873 measure,
4874 value,
4875 } => {
4876 s.parts
4877 .get_mut(*part)
4878 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4879 .staves
4880 .get_mut(*staff)
4881 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4882 .measures
4883 .get_mut(*measure)
4884 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4885 .tempo_ramp_to = *value;
4886 }
4887 ScorePatch::SetMeasureMetronomeMark {
4888 part,
4889 staff,
4890 measure,
4891 value,
4892 } => {
4893 s.parts
4894 .get_mut(*part)
4895 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4896 .staves
4897 .get_mut(*staff)
4898 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4899 .measures
4900 .get_mut(*measure)
4901 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4902 .metronome_mark = value.clone();
4903 }
4904 }
4905 }
4906 if !super::validate::validate(&s).is_valid() {
4907 return Err(Error::InvalidScore);
4908 }
4909 Ok(s)
4910}
4911
4912pub fn staff_ottava_octave_shifts(
4917 staff: &Staff,
4918 default_time: &TimeSignature,
4919) -> std::collections::HashMap<(usize, usize, usize), i8> {
4920 if !staff
4922 .measures
4923 .iter()
4924 .flat_map(|m| &m.voices)
4925 .flatten()
4926 .any(|n| n.ottava_start.is_some())
4927 {
4928 return std::collections::HashMap::new();
4929 }
4930 type OttavaEvent = (f64, u8, i8, (usize, usize, usize));
4932 let mut events: Vec<OttavaEvent> = Vec::new();
4933 for voice in 0..4 {
4934 let mut measure_start = 0.0;
4935 let mut active = None;
4936 for (measure_index, measure) in staff.measures.iter().enumerate() {
4937 let mut position = measure_start;
4938 for (note_index, note) in measure.voices[voice].iter().enumerate() {
4939 let key = (measure_index, voice, note_index);
4940 if let Some(kind) = note.ottava_start {
4941 if let Some(previous) = active.take() {
4942 events.push((position, 0, previous, key));
4943 }
4944 let shift = match kind {
4945 OttavaKind::Va8 => 1,
4946 OttavaKind::Vb8 => -1,
4947 OttavaKind::Ma15 => 2,
4948 OttavaKind::Mb15 => -2,
4949 };
4950 events.push((position, 1, shift, key));
4951 active = Some(shift);
4952 }
4953 events.push((position, 2, 0, key));
4954 let duration = note.beats();
4955 if note.ottava_end
4956 && let Some(shift) = active.take()
4957 {
4958 events.push((position + duration.max(1e-9), 0, shift, key));
4959 }
4960 position += duration;
4961 }
4962 measure_start += staff.measure_beats(measure_index, default_time);
4963 }
4964 }
4965 events.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
4966 let mut current = 0_i8;
4970 let mut result = std::collections::HashMap::new();
4971 for (_, kind, shift, key) in events {
4972 match kind {
4973 0 => current = 0,
4974 1 => current = shift,
4975 _ if current != 0 => {
4976 result.insert(key, current);
4977 }
4978 _ => {}
4979 }
4980 }
4981 result
4982}
4983
4984pub fn voice_duration_beats(voice: &[Note], measure_beats: f64) -> f64 {
4988 match voice {
4989 [only] if only.is_plain_whole_rest() => measure_beats,
4990 _ => voice.iter().map(Note::beats).sum(),
4991 }
4992}
4993
4994pub fn respell_score(score: &mut Score, prefer_flat: bool) {
4998 for part in &mut score.parts {
4999 for staff in &mut part.staves {
5000 for measure in &mut staff.measures {
5001 for voice in &mut measure.voices {
5002 for note in voice.iter_mut() {
5003 for pitch in &mut note.pitches {
5004 *pitch = pitch.respell(prefer_flat);
5005 }
5006 }
5007 }
5008 }
5009 }
5010 }
5011}
5012
5013#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5015#[serde(rename_all = "snake_case")]
5016pub enum RespellPolicy {
5017 Flat,
5019 Sharp,
5021 Key,
5023}
5024
5025pub fn respell_staff_region(
5036 score: &mut Score,
5037 part_index: usize,
5038 staff_index: usize,
5039 start_measure: usize,
5040 end_measure: usize,
5041 policy: RespellPolicy,
5042) -> Result<(usize, usize), Error> {
5043 let score_key_flat = score.settings.key_signature.fifths < 0;
5044 let part = score
5045 .parts
5046 .get_mut(part_index)
5047 .ok_or(Error::PartNotFound(part_index))?;
5048 let staff_count = part.staves.len();
5049 if staff_index >= staff_count {
5050 return Err(Error::StaffNotFound(staff_index));
5051 }
5052 let measure_count = part.staves[staff_index].measures.len();
5053 if start_measure >= end_measure || end_measure > measure_count {
5054 return Err(Error::InvalidCommand(format!(
5055 "invalid measure range {start_measure}..{end_measure}"
5056 )));
5057 }
5058 let mut prefer_flat = Vec::with_capacity(end_measure);
5059 let mut running_flat = score_key_flat;
5060 for index in 0..end_measure {
5061 let key = part.staves[staff_index].measures[index]
5062 .key_sig
5063 .as_ref()
5064 .or_else(|| {
5065 part.staves[0]
5066 .measures
5067 .get(index)
5068 .and_then(|measure| measure.key_sig.as_ref())
5069 });
5070 if let Some(key) = key {
5071 running_flat = key.fifths < 0;
5072 }
5073 prefer_flat.push(match policy {
5074 RespellPolicy::Flat => true,
5075 RespellPolicy::Sharp => false,
5076 RespellPolicy::Key => running_flat,
5077 });
5078 }
5079
5080 let measures = &mut part.staves[staff_index].measures;
5081 for (index, measure) in measures
5082 .iter_mut()
5083 .enumerate()
5084 .take(end_measure)
5085 .skip(start_measure)
5086 {
5087 for voice in &mut measure.voices {
5088 for note in voice
5089 .iter_mut()
5090 .filter(|note| !note.is_rest && !note.is_unpitched)
5091 {
5092 for pitch in &mut note.pitches {
5093 *pitch = pitch.respell(prefer_flat[index]);
5094 }
5095 }
5096 }
5097 }
5098
5099 let mut changed = (start_measure, end_measure - 1);
5100 for voice in 0..4 {
5101 for measure in start_measure..end_measure {
5102 for note in 0..measures[measure].voices[voice].len() {
5103 if measures[measure].voices[voice][note].tie_start {
5104 let last = propagate_tied_spelling(measures, voice, (measure, note), true);
5105 changed.1 = changed.1.max(last);
5106 }
5107 }
5108 }
5109 if !measures[start_measure].voices[voice].is_empty() {
5110 let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
5111 changed.0 = changed.0.min(first);
5112 }
5113 }
5114 Ok(changed)
5115}
5116
5117fn propagate_tied_spelling(
5120 measures: &mut [Measure],
5121 voice: usize,
5122 from: (usize, usize),
5123 forward: bool,
5124) -> usize {
5125 let mut current = from;
5126 loop {
5127 let note = &measures[current.0].voices[voice][current.1];
5128 if !(if forward {
5129 note.tie_start
5130 } else {
5131 note.tie_end
5132 }) {
5133 return current.0;
5134 }
5135 let neighbor = if forward {
5136 if current.1 + 1 < measures[current.0].voices[voice].len() {
5137 Some((current.0, current.1 + 1))
5138 } else {
5139 measures
5140 .get(current.0 + 1)
5141 .filter(|measure| !measure.voices[voice].is_empty())
5142 .map(|_| (current.0 + 1, 0))
5143 }
5144 } else if current.1 > 0 {
5145 Some((current.0, current.1 - 1))
5146 } else {
5147 current.0.checked_sub(1).and_then(|previous| {
5148 measures[previous].voices[voice]
5149 .len()
5150 .checked_sub(1)
5151 .map(|last| (previous, last))
5152 })
5153 };
5154 let Some(next) = neighbor else {
5155 return current.0;
5156 };
5157 let next_note = &measures[next.0].voices[voice][next.1];
5158 if !(if forward {
5159 next_note.tie_end
5160 } else {
5161 next_note.tie_start
5162 }) {
5163 return current.0;
5164 }
5165 let source = measures[current.0].voices[voice][current.1].pitches.clone();
5166 for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
5167 if let Some(spelled) = source.iter().find(|candidate| {
5168 candidate.to_midi() == pitch.to_midi()
5169 && candidate.microtone_cents == pitch.microtone_cents
5170 }) {
5171 *pitch = spelled.clone();
5172 }
5173 }
5174 current = next;
5175 }
5176}
5177
5178pub fn respell_score_to_key(score: &mut Score) {
5182 let prefer_flat = score.settings.key_signature.fifths < 0;
5183 respell_score(score, prefer_flat);
5184}
5185
5186pub fn score_duration_secs(score: &Score) -> f64 {
5192 if score.settings.tempo_bpm == 0 {
5193 return 0.0;
5194 }
5195 let seq = measure_sequence(score);
5196 let mut total_secs = 0.0f64;
5197 let mut current_bpm = score.settings.tempo_bpm as f64;
5198 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
5199 for &idx in &seq {
5200 if let Some(m) = staff.measures.get(idx) {
5201 if let Some(b) = m.tempo {
5202 current_bpm = b as f64;
5203 }
5204 if current_bpm == 0.0 {
5205 continue;
5206 }
5207 let beats = voice_duration_beats(
5208 &m.voices[0],
5209 staff.measure_beats(idx, &score.settings.time_signature),
5210 );
5211 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
5212 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
5213 current_bpm = f64::from(target);
5214 }
5215 }
5216 }
5217 }
5218 total_secs + super::playback::fermata_total_secs(score, None)
5219}
5220
5221pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
5227 if score.settings.tempo_bpm == 0 {
5228 return 0.0;
5229 }
5230 let seq: Vec<usize> = measure_sequence(score)
5231 .into_iter()
5232 .filter(|&idx| idx >= region.0 && idx <= region.1)
5233 .collect();
5234 let mut total_secs = 0.0f64;
5235 let mut current_bpm = score.settings.tempo_bpm as f64;
5236 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
5237 for &idx in &seq {
5238 if let Some(m) = staff.measures.get(idx) {
5239 if let Some(b) = m.tempo {
5240 current_bpm = b as f64;
5241 }
5242 if current_bpm == 0.0 {
5243 continue;
5244 }
5245 let beats = voice_duration_beats(
5246 &m.voices[0],
5247 staff.measure_beats(idx, &score.settings.time_signature),
5248 );
5249 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
5250 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
5251 current_bpm = f64::from(target);
5252 }
5253 }
5254 }
5255 }
5256 total_secs + super::playback::fermata_total_secs(score, Some(region))
5257}
5258
5259fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
5260 let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
5261 return beats / start_bpm * 60.0;
5262 };
5263 let end_bpm = f64::from(target_bpm);
5264 let delta = end_bpm - start_bpm;
5265 if delta.abs() < f64::EPSILON {
5266 return beats / start_bpm * 60.0;
5267 }
5268 60.0 * beats / delta * (end_bpm / start_bpm).ln()
5269}
5270
5271pub fn measure_beats_remaining(
5276 score: &Score,
5277 part_index: usize,
5278 staff_index: usize,
5279 measure_index: usize,
5280 voice_index: usize,
5281) -> Result<f64, Error> {
5282 let part = score
5283 .parts
5284 .get(part_index)
5285 .ok_or(Error::PartNotFound(part_index))?;
5286 let staff = part
5287 .staves
5288 .get(staff_index)
5289 .ok_or(Error::StaffNotFound(staff_index))?;
5290 let measure = staff
5291 .measures
5292 .get(measure_index)
5293 .ok_or(Error::MeasureNotFound(measure_index))?;
5294 let voice = measure
5295 .voices
5296 .get(voice_index)
5297 .ok_or(Error::VoiceOutOfRange(voice_index))?;
5298 let capacity = staff.measure_beats(measure_index, &score.settings.time_signature);
5299 Ok((capacity - voice_duration_beats(voice, capacity)).max(0.0))
5300}
5301
5302pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
5308 if pitches.is_empty() {
5309 return true;
5310 }
5311 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
5312 avg < clef.middle_line_midi() as f64
5313}
5314
5315fn beam_beat_size(ts: &TimeSignature) -> f64 {
5316 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
5317 3.0 * 4.0 / ts.denominator as f64
5318 } else {
5319 4.0 / ts.denominator as f64
5320 }
5321}
5322
5323pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
5328 let beat_size = beam_beat_size(time_sig);
5329 let n = notes.len();
5330 let mut result = vec![BeamState::None; n];
5331
5332 let is_beamable = |note: &Note| -> bool {
5333 !note.is_rest
5334 && matches!(
5335 note.duration,
5336 Duration::Eighth
5337 | Duration::Sixteenth
5338 | Duration::ThirtySecond
5339 | Duration::SixtyFourth
5340 | Duration::HundredTwentyEighth
5341 )
5342 };
5343
5344 let mut starts = Vec::with_capacity(n);
5346 let mut pos = 0.0f64;
5347 for note in notes {
5348 starts.push(pos);
5349 pos += note.beats();
5350 }
5351
5352 let group_id = |i: usize| -> i64 {
5356 let beat = starts[i] / beat_size;
5357 let nearest = beat.round();
5358 if (beat - nearest).abs() <= 1e-9 {
5359 nearest as i64
5360 } else {
5361 beat.floor() as i64
5362 }
5363 };
5364
5365 let mut i = 0;
5366 while i < n {
5367 if !is_beamable(¬es[i]) {
5368 i += 1;
5369 continue;
5370 }
5371 let g = group_id(i);
5372 let mut j = i;
5374 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
5375 j += 1;
5376 }
5377 let run = j - i;
5378 if run == 1 {
5379 result[i] = BeamState::None;
5380 } else {
5381 result[i] = BeamState::Begin;
5382 result[i + 1..j - 1].fill(BeamState::Continue);
5383 result[j - 1] = BeamState::End;
5384 }
5385 i = j;
5386 }
5387 result
5388}
5389
5390fn note_content_eq(a: &Note, b: &Note) -> bool {
5391 a.is_rest == b.is_rest
5392 && a.is_unpitched == b.is_unpitched
5393 && a.instrument_id == b.instrument_id
5394 && a.offset_x == b.offset_x
5395 && a.offset_y == b.offset_y
5396 && a.relative_x == b.relative_x
5397 && a.relative_y == b.relative_y
5398 && a.pitches == b.pitches
5399 && a.duration == b.duration
5400 && a.dot_count == b.dot_count
5401 && a.tie_start == b.tie_start
5402 && a.tie_end == b.tie_end
5403 && a.beam == b.beam
5404 && a.explicit_unbeamed == b.explicit_unbeamed
5405 && a.articulations == b.articulations
5406 && a.dynamic == b.dynamic
5407 && a.stem_up == b.stem_up
5408 && a.hairpin_start == b.hairpin_start
5409 && a.hairpin_above == b.hairpin_above
5410 && a.hairpin_end == b.hairpin_end
5411 && a.tuplet == b.tuplet
5412 && a.chord_symbol == b.chord_symbol
5413 && a.is_grace == b.is_grace
5414 && a.grace_slash == b.grace_slash
5415 && a.ottava_start == b.ottava_start
5416 && a.ottava_end == b.ottava_end
5417 && a.lyric == b.lyric
5418 && a.additional_lyrics == b.additional_lyrics
5419 && a.pedal_start == b.pedal_start
5420 && a.pedal_end == b.pedal_end
5421 && a.slur_start == b.slur_start
5422 && a.slur_end == b.slur_end
5423 && a.arpeggiate == b.arpeggiate
5424 && a.tab_position == b.tab_position
5425 && a.tab_positions == b.tab_positions
5426 && a.guitar_technique == b.guitar_technique
5427 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
5428 && a.guitar_bend_curve == b.guitar_bend_curve
5429}
5430
5431fn per_pitch_flag(flags: &[bool], pitch_count: usize, index: usize, chord: bool) -> bool {
5432 if flags.len() == pitch_count {
5433 flags.get(index).copied().unwrap_or(chord)
5434 } else {
5435 chord
5436 }
5437}
5438
5439#[cfg(test)]
5440mod tests {
5441 #[test]
5442 fn ottava_interval_covers_other_voices_only_at_matching_times() {
5443 use super::*;
5444 use crate::model::{duration::Duration, pitch::Step};
5445 let mut score = Score::new("Ottava", 120, 4, 4, 0, 2);
5446 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5447 let staff = &mut score.parts[0].staves[0];
5448 let mut start = note();
5449 start.ottava_start = Some(OttavaKind::Va8);
5450 staff.measures[0].voices[0] = vec![Note::rest(Duration::Half), start, note()];
5451 let mut end = note();
5452 end.ottava_end = true;
5453 staff.measures[1].voices[0] = vec![end, note()];
5454 staff.measures[0].voices[1] = vec![note(), note(), note(), note()];
5455 staff.measures[1].voices[1] = vec![note(), note()];
5456 let shifts = staff_ottava_octave_shifts(staff, &score.settings.time_signature);
5457 assert_eq!(shifts.get(&(0, 1, 1)), None);
5458 assert_eq!(shifts.get(&(0, 1, 2)), Some(&1));
5459 assert_eq!(shifts.get(&(0, 1, 3)), Some(&1));
5460 assert_eq!(shifts.get(&(1, 1, 0)), Some(&1));
5461 assert_eq!(shifts.get(&(1, 1, 1)), None);
5462 }
5463
5464 use super::*;
5465 use crate::model::{
5466 notation::{FingeringSelectionPolicy, TextStyle},
5467 pitch::Step,
5468 };
5469
5470 #[test]
5471 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
5472 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5473 note.fingerings = vec![3, 1, 4];
5474 note.fingering = Some(3);
5475 assert_eq!(
5476 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
5477 Some(3)
5478 );
5479 assert_eq!(
5480 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
5481 Some(1)
5482 );
5483 assert_eq!(
5484 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
5485 Some(4)
5486 );
5487 assert_eq!(note.fingerings, vec![3, 1, 4]);
5488 assert_eq!(note.fingering, Some(3));
5489 }
5490
5491 #[test]
5492 fn default_score_has_one_part_four_measures() {
5493 let score = Score::default();
5494 assert_eq!(score.parts.len(), 1);
5495 assert_eq!(score.parts[0].staves.len(), 1);
5496 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
5497 }
5498
5499 #[test]
5500 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
5501 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5502 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5503 lines: 6,
5504 tuning_midi: vec![64, 59, 55, 50, 45, 40],
5505 capo: 2,
5506 });
5507 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
5508 Pitch::with_alter(Step::F, 4, 1),
5509 Duration::Quarter,
5510 ));
5511 score.parts[0].staves[0].measures[0].voices[0]
5512 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
5513 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
5514 Some(TabPosition { string: 6, fret: 7 });
5515
5516 assert_eq!(assign_tablature_positions(&mut score), 1);
5517 let notes = &score.parts[0].staves[0].measures[0].voices[0];
5518 assert_eq!(
5519 notes[1].tab_position,
5520 Some(TabPosition { string: 1, fret: 0 })
5521 );
5522 assert_eq!(notes[1].string_number, Some(1));
5523 assert_eq!(
5524 notes[2].tab_position,
5525 Some(TabPosition { string: 6, fret: 7 })
5526 );
5527 }
5528
5529 #[test]
5530 fn assign_tablature_positions_uses_measure_local_capo_change() {
5531 let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
5532 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5533 lines: 6,
5534 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5535 capo: 0,
5536 });
5537 score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
5538 lines: 6,
5539 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5540 capo: 2,
5541 });
5542 score.parts[0].staves[0].measures[1].voices[0]
5543 .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
5544
5545 assert_eq!(assign_tablature_positions(&mut score), 1);
5546 assert_eq!(
5547 score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
5548 Some(TabPosition { string: 5, fret: 3 })
5549 );
5550 }
5551
5552 #[test]
5553 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
5554 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5555 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5556 lines: 6,
5557 tuning_midi: vec![64, 59, 55, 50, 45, 40],
5558 capo: 0,
5559 });
5560 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
5561 chord.pitches.push(Pitch::new(Step::G, 4));
5562 score.parts[0].staves[0].measures[0].voices[0].push(chord);
5563
5564 assert_eq!(assign_tablature_positions(&mut score), 1);
5565 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
5566 assert_eq!(
5567 positions,
5568 &vec![
5569 TabPosition { string: 2, fret: 5 },
5570 TabPosition { string: 1, fret: 3 },
5571 ]
5572 );
5573 }
5574
5575 #[test]
5576 fn optimized_tablature_respects_an_authored_anchor_across_measures() {
5577 let mut score = Score::new("Two-string bass", 120, 4, 4, 0, 2);
5578 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5579 lines: 2,
5580 tuning_midi: vec![40, 45],
5581 capo: 0,
5582 });
5583 let a2 = Pitch::new(Step::A, 2);
5584 let mut anchor = Note::new(a2.clone(), Duration::Quarter);
5585 anchor.tab_position = Some(TabPosition { string: 1, fret: 5 });
5586 score.parts[0].staves[0].measures[0].voices[0] = vec![anchor];
5587 score.parts[0].staves[0].measures[1].voices[0] = vec![
5588 Note::rest(Duration::Quarter),
5589 Note::new(a2, Duration::Quarter),
5590 ];
5591
5592 assert_eq!(optimize_tablature_positions(&mut score), 1);
5593 let anchor = &score.parts[0].staves[0].measures[0].voices[0][0];
5594 let assigned = &score.parts[0].staves[0].measures[1].voices[0][1];
5595 assert_eq!(
5596 anchor.tab_position,
5597 Some(TabPosition { string: 1, fret: 5 })
5598 );
5599 assert_eq!(
5601 assigned.tab_position,
5602 Some(TabPosition { string: 1, fret: 5 })
5603 );
5604 assert_eq!(assigned.string_number, Some(1));
5605 assert_eq!(
5606 assigned.tab_positions,
5607 vec![TabPosition { string: 1, fret: 5 }]
5608 );
5609 assert_eq!(optimize_tablature_positions(&mut score), 0);
5610 }
5611
5612 #[test]
5613 fn optimized_tablature_assigns_distinct_strings_to_a_chord() {
5614 let mut score = Score::new("Guitar chord", 120, 4, 4, 0, 1);
5615 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5616 lines: 6,
5617 tuning_midi: vec![64, 59, 55, 50, 45, 40],
5618 capo: 0,
5619 });
5620 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
5621 chord.pitches.push(Pitch::new(Step::G, 4));
5622 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
5623
5624 assert_eq!(optimize_tablature_positions(&mut score), 1);
5625 let assigned = &score.parts[0].staves[0].measures[0].voices[0][0];
5626 assert_eq!(assigned.tab_positions.len(), 2);
5627 assert_ne!(
5628 assigned.tab_positions[0].string,
5629 assigned.tab_positions[1].string
5630 );
5631 for (pitch, position) in assigned.pitches.iter().zip(&assigned.tab_positions) {
5632 let open = score.parts[0].staves[0]
5633 .tablature
5634 .as_ref()
5635 .unwrap()
5636 .tuning_midi[usize::from(position.string - 1)];
5637 assert_eq!(pitch.to_midi(), open + i16::from(position.fret));
5638 }
5639 }
5640
5641 #[test]
5642 fn new_score_measure_count() {
5643 let score = Score::new("Test", 120, 4, 4, 0, 8);
5644 assert_eq!(score.measure_count(), 8);
5645 }
5646
5647 #[test]
5648 fn note_beats_quarter() {
5649 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5650 assert!((note.beats() - 1.0).abs() < 1e-9);
5651 }
5652
5653 #[test]
5654 fn note_beats_dotted_quarter() {
5655 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5656 note.dot_count = 1;
5657 assert!((note.beats() - 1.5).abs() < 1e-9);
5658 }
5659
5660 #[test]
5661 fn grace_note_beats_zero() {
5662 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
5663 note.is_grace = true;
5664 assert_eq!(note.beats(), 0.0);
5665 }
5666
5667 #[test]
5668 fn measure_empty_4_4_fills_four_beats() {
5669 let m = Measure::empty(4, 4);
5670 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
5671 assert!((total - 4.0).abs() < 1e-9);
5672 }
5673
5674 #[test]
5675 fn measure_empty_3_4_fills_three_beats() {
5676 let m = Measure::empty(3, 4);
5677 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
5678 assert!((total - 3.0).abs() < 1e-9);
5679 }
5680
5681 #[test]
5682 fn whole_filling_beats() {
5683 assert_eq!(Duration::whole_filling_beats(16.0), Duration::Longa);
5684 assert_eq!(Duration::whole_filling_beats(8.0), Duration::Breve);
5685 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
5686 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
5687 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
5688 }
5689
5690 #[test]
5693 fn statistics_default_score_all_rests() {
5694 let score = Score::default();
5695 let s = score.statistics();
5696 assert_eq!(s.part_count, 1);
5697 assert_eq!(s.measure_count, 4);
5698 assert_eq!(s.note_count, 0);
5699 assert!(s.rest_count > 0);
5700 }
5701
5702 #[test]
5703 fn statistics_duration_estimate() {
5704 let score = Score::new("T", 120, 4, 4, 0, 1);
5706 let s = score.statistics();
5707 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
5708 }
5709
5710 #[test]
5711 fn score_duration_secs_matches_statistics() {
5712 use super::score_duration_secs;
5713 let score = Score::new("T", 120, 4, 4, 0, 4);
5714 let secs = score_duration_secs(&score);
5715 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
5717 }
5718
5719 #[test]
5720 fn score_duration_secs_integrates_measure_tempo_ramp() {
5721 use super::score_duration_secs;
5722 let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
5723 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5724 let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
5725 assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
5726 }
5727
5728 #[test]
5729 fn score_duration_secs_zero_bpm_returns_zero() {
5730 use super::score_duration_secs;
5731 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5732 score.settings.tempo_bpm = 0;
5733 assert_eq!(score_duration_secs(&score), 0.0);
5734 }
5735
5736 #[test]
5737 fn score_duration_secs_per_measure_tempo() {
5738 use super::score_duration_secs;
5739 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5741 score.parts[0].staves[0].measures[1].tempo = Some(60);
5742 let secs = score_duration_secs(&score);
5743 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
5744 }
5745
5746 #[test]
5749 fn extract_part_returns_single_part_score() {
5750 let mut score = Score::default();
5751 let mut p2 = Part::new("Violin", "Vln.");
5752 p2.staves.push(Staff::new(Clef::Treble));
5753 score.parts.push(p2);
5754 let ex = score.extract_part(0).unwrap();
5755 assert_eq!(ex.parts.len(), 1);
5756 assert_ne!(ex.id, score.id);
5757 assert_eq!(ex.metadata.title, score.metadata.title);
5758 }
5759
5760 #[test]
5761 fn extract_part_out_of_range_is_none() {
5762 let score = Score::default();
5763 assert!(score.extract_part(99).is_none());
5764 }
5765
5766 #[test]
5767 fn extract_and_merge_remap_typed_spanner_part_addresses() {
5768 let mut left = Score::template(ScoreTemplate::StringQuartet);
5769 let address = NoteAddr {
5770 part: 1,
5771 staff: 0,
5772 measure: 0,
5773 voice: 0,
5774 note: 0,
5775 };
5776 left.spanners.push(NotationSpanner {
5777 id: "left-span".to_string(),
5778 kind: NotationSpannerKind::Slur,
5779 start: address.clone(),
5780 end: address,
5781 number: Some(1),
5782 line_type: None,
5783 text: None,
5784 placement: None,
5785 ottava_size: None,
5786 ottava_type: None,
5787 });
5788 let extracted = left.extract_part_checked(1).expect("valid extracted part");
5789 assert_eq!(extracted.spanners[0].start.part, 0);
5790 assert_eq!(extracted.spanners[0].end.part, 0);
5791
5792 let mut right = Score::new("R", 120, 4, 4, 0, 1);
5793 let right_address = NoteAddr {
5794 part: 0,
5795 staff: 0,
5796 measure: 0,
5797 voice: 0,
5798 note: 0,
5799 };
5800 right.spanners.push(NotationSpanner {
5801 id: "right-span".to_string(),
5802 kind: NotationSpannerKind::Pedal,
5803 start: right_address.clone(),
5804 end: right_address,
5805 number: Some(1),
5806 line_type: None,
5807 text: None,
5808 placement: None,
5809 ottava_size: None,
5810 ottava_type: None,
5811 });
5812 let merged = left.merge_checked(&right).expect("valid merged score");
5813 let right_span = merged
5814 .spanners
5815 .iter()
5816 .find(|spanner| spanner.id == "right-span")
5817 .expect("merged right span");
5818 assert_eq!(right_span.start.part, left.parts.len());
5819 assert_eq!(right_span.end.part, left.parts.len());
5820 }
5821
5822 #[test]
5825 fn transpose_zero_is_clone() {
5826 let score = Score::new("T", 120, 4, 4, 0, 1);
5827 let t = transpose(&score, 0);
5828 assert_eq!(t.settings.key_signature.fifths, 0);
5829 }
5830
5831 #[test]
5832 fn transpose_staff_region_rewrites_only_selected_written_measures() {
5833 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5834 for measure in &mut score.parts[0].staves[0].measures {
5835 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5836 }
5837 let transformed = transpose_staff_region_checked(
5838 &score,
5839 0,
5840 0,
5841 1,
5842 2,
5843 2,
5844 RegionalTranspositionTarget::Written,
5845 )
5846 .expect("region should transpose");
5847 assert_eq!(
5848 transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
5849 Pitch::new(Step::C, 4)
5850 );
5851 assert_eq!(
5852 transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
5853 Pitch::new(Step::D, 4)
5854 );
5855 assert_eq!(
5856 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
5857 Pitch::new(Step::C, 4)
5858 );
5859 }
5860
5861 #[test]
5862 fn regional_concert_transposition_requires_full_staff() {
5863 let score = Score::new("T", 120, 4, 4, 0, 2);
5864 assert!(
5865 transpose_staff_region_checked(
5866 &score,
5867 0,
5868 0,
5869 1,
5870 2,
5871 -2,
5872 RegionalTranspositionTarget::Concert,
5873 )
5874 .is_err()
5875 );
5876 }
5877
5878 #[test]
5879 fn transpose_c_major_up_2_to_d_major() {
5880 let score = Score::new("T", 120, 4, 4, 0, 1);
5881 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
5882 }
5883
5884 #[test]
5885 fn transpose_d_major_up_5_to_g_major() {
5886 let score = Score::new("T", 120, 4, 4, 2, 1);
5887 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
5888 }
5889
5890 #[test]
5891 fn transpose_c4_up_1_to_csharp4() {
5892 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
5893 assert_eq!(p.to_midi(), 61);
5894 assert_eq!(p.step, Step::C);
5895 assert_eq!(p.alter, 1);
5896 }
5897
5898 #[test]
5899 fn transpose_c4_down_1_to_b3() {
5900 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
5901 assert_eq!(p.to_midi(), 59);
5902 assert_eq!(p.step, Step::B);
5903 assert_eq!(p.alter, 0);
5904 }
5905
5906 #[test]
5907 fn transpose_up_octave_keeps_step() {
5908 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
5909 assert_eq!(p.to_midi(), 81);
5910 assert_eq!(p.step, Step::A);
5911 assert_eq!(p.octave, 5);
5912 }
5913
5914 #[test]
5915 fn statistics_with_repeat_doubles_duration() {
5916 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5918 score.parts[0].staves[0].measures[0].barline_left =
5919 crate::model::notation::Barline::RepeatStart;
5920 score.parts[0].staves[0].measures[1].barline_right =
5921 crate::model::notation::Barline::RepeatEnd;
5922 let s = score.statistics();
5923 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
5925 }
5926
5927 #[test]
5928 fn transpose_octave_boundary_b4_to_c5() {
5929 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
5931 assert_eq!(p.to_midi(), 72);
5932 assert_eq!(p.step, Step::C);
5933 assert_eq!(p.octave, 5);
5934 }
5935
5936 #[test]
5937 fn transpose_clamp_at_midi_127() {
5938 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
5940 assert_eq!(p.to_midi(), 127);
5941 }
5942
5943 #[test]
5946 fn merge_combines_parts() {
5947 let mut a = Score::new("A", 120, 4, 4, 0, 2);
5948 let b = Score::new("B", 120, 4, 4, 0, 2);
5949 let mut p2 = Part::new("Violin", "Vln.");
5951 p2.staves.push(Staff::new(Clef::Treble));
5952 for i in 0..2usize {
5953 let mut m = Measure::empty(4, 4);
5954 m.number = i as u32 + 1;
5955 p2.staves[0].measures.push(m);
5956 }
5957 a.parts.push(p2);
5958 let merged = a.merge(&b);
5959 assert_eq!(merged.parts.len(), 3);
5961 }
5962
5963 #[test]
5964 fn merge_pads_shorter_score() {
5965 let a = Score::new("A", 120, 4, 4, 0, 4);
5966 let b = Score::new("B", 120, 4, 4, 0, 2);
5967 let merged = a.merge(&b);
5968 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
5970 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
5971 }
5972
5973 #[test]
5974 fn merge_uses_self_metadata() {
5975 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
5976 a.metadata.composer = "Composer A".to_string();
5977 let b = Score::new("Title B", 120, 4, 4, 0, 2);
5978 let merged = a.merge(&b);
5979 assert_eq!(merged.metadata.title, "Title A");
5980 assert_eq!(merged.metadata.composer, "Composer A");
5981 }
5982
5983 #[test]
5984 fn merge_new_id_differs_from_both() {
5985 let a = Score::new("A", 120, 4, 4, 0, 2);
5986 let b = Score::new("B", 120, 4, 4, 0, 2);
5987 let merged = a.merge(&b);
5988 assert_ne!(merged.id, a.id);
5989 assert_ne!(merged.id, b.id);
5990 }
5991
5992 #[test]
5995 fn staff_default_transpose_is_zero() {
5996 let s = Staff::new(Clef::Treble);
5997 assert_eq!(s.transpose_semitones, 0);
5998 }
5999
6000 #[test]
6001 fn staff_presentation_defaults_and_tracks_percussion_clef() {
6002 let standard = Staff::new(Clef::Treble);
6003 assert_eq!(standard.presentation, StaffPresentation::default());
6004
6005 let percussion = Staff::new(Clef::Percussion);
6006 assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
6007 assert_eq!(percussion.presentation.lines, 5);
6008 assert!(percussion.presentation.visible);
6009 }
6010
6011 #[test]
6014 fn score_default_has_schema_version_1() {
6015 let score = Score::default();
6016 assert_eq!(score.schema_version, 1);
6017 }
6018
6019 #[test]
6020 fn score_new_has_schema_version_1() {
6021 let score = Score::new("T", 120, 4, 4, 0, 4);
6022 assert_eq!(score.schema_version, 1);
6023 }
6024
6025 #[test]
6026 fn chromatic_transposition_covers_every_pitch_class_and_keeps_microtones() {
6027 for midi in 1..=126 {
6028 let mut original = Pitch::from_midi(midi, false);
6029 original.microtone_cents = 25;
6030 for shift in [-1_i8, 1] {
6031 let shifted = transpose_pitch(&original, shift);
6032 assert_eq!(shifted.to_midi(), i16::from(midi) + i16::from(shift));
6033 assert_eq!(shifted.microtone_cents, 25);
6034 assert!(if shift > 0 {
6035 shifted.alter >= 0
6036 } else {
6037 shifted.alter <= 0
6038 });
6039 }
6040 }
6041 }
6042
6043 #[test]
6044 fn score_without_schema_version_deserializes_to_zero() {
6045 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":[]}"#;
6046 let score: Score = serde_json::from_str(json).unwrap();
6047 assert_eq!(score.schema_version, 0);
6048 }
6049
6050 #[test]
6051 fn legacy_score_json_defaults_typed_spanners() {
6052 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
6053 let mut value = serde_json::to_value(score).expect("score serializes");
6054 value
6055 .as_object_mut()
6056 .expect("score is an object")
6057 .remove("spanners");
6058 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
6059 assert!(restored.spanners.is_empty());
6060 }
6061
6062 #[test]
6063 fn legacy_staff_json_defaults_presentation() {
6064 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
6065 let mut value = serde_json::to_value(score).expect("score serializes");
6066 value["parts"][0]["staves"][0]
6067 .as_object_mut()
6068 .expect("staff is an object")
6069 .remove("presentation");
6070
6071 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
6072 assert_eq!(
6073 restored.parts[0].staves[0].presentation,
6074 StaffPresentation::default()
6075 );
6076 }
6077
6078 #[test]
6079 fn legacy_part_json_defaults_instrument_definition() {
6080 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
6081 let mut value = serde_json::to_value(score).expect("score serializes");
6082 value["parts"][0]
6083 .as_object_mut()
6084 .expect("part is an object")
6085 .remove("instrument");
6086
6087 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
6088 assert!(restored.parts[0].instrument.is_none());
6089 }
6090
6091 #[test]
6092 fn resolve_view_projects_linked_part_without_mutating_source_score() {
6093 let mut score = Score::template(ScoreTemplate::StringQuartet);
6094 let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
6095 view.layout.measures_per_row = Some(2);
6096 score.views.push(view);
6097
6098 let projected = score.resolve_view("violin-2").expect("view resolves");
6099 assert_eq!(projected.parts.len(), 1);
6100 assert_eq!(projected.parts[0].name, "Violin II");
6101 assert_eq!(projected.views.len(), 1);
6102 assert_eq!(projected.views[0].parts, vec![0]);
6103 assert_eq!(score.parts.len(), 4);
6104 assert_eq!(score.views[0].parts, vec![1]);
6105 }
6106
6107 #[test]
6108 fn linked_view_checks_staff_references_and_projects_part_groups() {
6109 let mut score = Score::template(ScoreTemplate::StringQuartet);
6110 score.part_groups.push(PartGroup {
6111 first_part: 1,
6112 last_part: 2,
6113 symbol: PartGroupSymbol::Bracket,
6114 barlines_connect: true,
6115 });
6116 let mut view = ScoreView::linked_standard_staff("middle", "Middle voices", 1, 0);
6117 view.parts.push(2);
6118 view.layout
6119 .hidden_staves
6120 .push(ViewStaffRef { part: 2, staff: 0 });
6121 score.views.push(view);
6122 let before = serde_json::to_value(&score).unwrap();
6123 let projected = score.resolve_view("middle").unwrap();
6124 assert_eq!(projected.parts.len(), 2);
6125 assert_eq!(
6126 (
6127 projected.part_groups[0].first_part,
6128 projected.part_groups[0].last_part
6129 ),
6130 (0, 1)
6131 );
6132 assert_eq!(projected.views[0].layout.hidden_staves[0].part, 1);
6133 assert_eq!(serde_json::to_value(&score).unwrap(), before);
6134
6135 let mut invalid = score.clone();
6136 invalid.views[0].parts.push(1);
6137 assert!(invalid.resolve_view("middle").is_err());
6138 invalid.views[0].parts.pop();
6139 invalid.views[0].layout.hidden_staves[0].part = 3;
6140 assert!(invalid.resolve_view("middle").is_err());
6141 invalid.views[0].layout.hidden_staves[0].part = 2;
6142 invalid.views[0].staff_kind_overrides[0].staff.staff = 9;
6143 assert!(invalid.resolve_view("middle").is_err());
6144 assert!(score.resolve_view("missing").is_err());
6145 }
6146
6147 #[test]
6148 fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
6149 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
6150 score.parts[0].staves[0].tablature = Some(TablatureConfig {
6151 lines: 6,
6152 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6153 capo: 0,
6154 });
6155 score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
6156 score.views.push(ScoreView::linked_tablature_staff(
6157 "guitar-tab",
6158 "Guitar Tab",
6159 0,
6160 0,
6161 ));
6162
6163 let projected = score.resolve_view("guitar-tab").expect("view resolves");
6164 assert_eq!(
6165 projected.parts[0].staves[0].presentation.kind,
6166 StaffKind::Tablature
6167 );
6168 assert_eq!(
6169 projected.views[0].staff_kind_overrides[0].staff,
6170 ViewStaffRef { part: 0, staff: 0 }
6171 );
6172 assert_eq!(
6173 score.parts[0].staves[0].presentation.kind,
6174 StaffKind::Standard
6175 );
6176 }
6177
6178 #[test]
6179 fn legacy_score_view_json_defaults_staff_kind_overrides() {
6180 let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
6181 score.views.push(ScoreView::linked_part("part", "Part", 0));
6182 let mut value = serde_json::to_value(score).expect("score serializes");
6183 value["views"][0]
6184 .as_object_mut()
6185 .expect("view is an object")
6186 .remove("staff_kind_overrides");
6187
6188 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
6189 assert!(restored.views[0].staff_kind_overrides.is_empty());
6190 }
6191
6192 #[test]
6193 fn typed_view_style_overrides_are_ordered_and_json_compatible() {
6194 let mut view = ScoreView::linked_part("part", "Part", 0);
6195 view.layout.typed_style_overrides = vec![
6196 ViewStyleOverride {
6197 property: ViewStyleProperty::TextScale,
6198 value: 0.9,
6199 },
6200 ViewStyleOverride {
6201 property: ViewStyleProperty::TextScale,
6202 value: 1.1,
6203 },
6204 ];
6205 assert_eq!(
6206 view.layout.style_value(ViewStyleProperty::TextScale),
6207 Some(1.1)
6208 );
6209 let restored: ScoreView =
6210 serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
6211 assert_eq!(
6212 restored.layout.typed_style_overrides,
6213 view.layout.typed_style_overrides
6214 );
6215 assert_eq!(view.layout.resolved_style().text_scale, 1.1);
6216 assert_eq!(view.layout.resolved_style().system_gap, 2.0);
6217 }
6218
6219 #[test]
6220 fn score_style_defaults_are_inherited_then_overridden_by_view() {
6221 let mut score = Score::new("Style", 120, 4, 4, 0, 1);
6222 score.style_overrides = vec![
6223 ViewStyleOverride {
6224 property: ViewStyleProperty::StaffSpace,
6225 value: 1.2,
6226 },
6227 ViewStyleOverride {
6228 property: ViewStyleProperty::TextScale,
6229 value: 0.9,
6230 },
6231 ];
6232 let mut view = ScoreView::linked_part("part", "Part", 0);
6233 view.layout.typed_style_overrides.push(ViewStyleOverride {
6234 property: ViewStyleProperty::TextScale,
6235 value: 1.1,
6236 });
6237 let style = score.resolved_view_style(&view.layout);
6238 assert_eq!(style.staff_space, 1.2);
6239 assert_eq!(style.text_scale, 1.1);
6240 let restored: Score =
6241 serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
6242 assert_eq!(restored.style_overrides, score.style_overrides);
6243 }
6244
6245 #[test]
6246 fn legacy_measure_json_defaults_source_voice_numbers() {
6247 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
6248 let mut value = serde_json::to_value(&score).expect("score serializes");
6249 value["parts"][0]["staves"][0]["measures"][0]
6250 .as_object_mut()
6251 .expect("measure is an object")
6252 .remove("source_voice_numbers");
6253
6254 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
6255 assert_eq!(
6256 restored.parts[0].staves[0].measures[0].source_voice_numbers,
6257 [None; 4]
6258 );
6259 }
6260
6261 #[test]
6262 fn legacy_measure_json_defaults_tempo_ramp() {
6263 let measure: Measure = serde_json::from_str(
6264 r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
6265 )
6266 .expect("legacy measure deserializes");
6267 assert_eq!(measure.tempo_ramp_to, None);
6268 }
6269
6270 #[test]
6271 fn note_without_new_percussion_fields_uses_serde_defaults() {
6272 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
6273 let mut value = serde_json::to_value(note).unwrap();
6274 let object = value.as_object_mut().unwrap();
6275 object.remove("is_unpitched");
6276 object.remove("instrument_id");
6277 let restored: Note = serde_json::from_value(value).unwrap();
6278 assert!(!restored.is_unpitched);
6279 assert_eq!(restored.instrument_id, None);
6280 }
6281
6282 #[test]
6283 fn percussion_instrument_resolution_prefers_id_then_display_key() {
6284 let mut part = Part::new("Drums", "Dr.");
6285 part.percussion_instruments = vec![
6286 PercussionInstrument {
6287 id: "snare".to_string(),
6288 name: Some("Acoustic Snare".to_string()),
6289 midi_unpitched: Some(38),
6290 staff_position: None,
6291 notehead: None,
6292 preferred_voice: None,
6293 techniques: Vec::new(),
6294 },
6295 PercussionInstrument {
6296 id: "rim".to_string(),
6297 name: Some("Side Stick".to_string()),
6298 midi_unpitched: Some(37),
6299 staff_position: None,
6300 notehead: None,
6301 preferred_voice: None,
6302 techniques: Vec::new(),
6303 },
6304 ];
6305 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
6306 note.is_unpitched = true;
6307 assert_eq!(
6308 part.percussion_instrument_for_note(¬e)
6309 .map(|instrument| instrument.id.as_str()),
6310 Some("snare")
6311 );
6312 note.instrument_id = Some("rim".to_string());
6313 assert_eq!(
6314 part.percussion_instrument_for_note(¬e)
6315 .map(|instrument| instrument.id.as_str()),
6316 Some("rim")
6317 );
6318 note.instrument_id = Some("missing".to_string());
6319 assert!(part.percussion_instrument_for_note(¬e).is_none());
6320 note.instrument_id = None;
6321 note.is_unpitched = false;
6322 assert!(part.percussion_instrument_for_note(¬e).is_none());
6323 }
6324
6325 #[test]
6326 fn percussion_kit_extensions_are_json_backward_compatible() {
6327 let mut instrument = PercussionInstrument {
6328 id: "snare".to_string(),
6329 name: Some("Acoustic Snare".to_string()),
6330 midi_unpitched: Some(38),
6331 staff_position: Some(0),
6332 notehead: Some(NoteHead::Cross),
6333 preferred_voice: Some(1),
6334 techniques: vec!["rim-shot".to_string()],
6335 };
6336 let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
6337 let object = value
6338 .as_object_mut()
6339 .expect("percussion instrument is an object");
6340 object.remove("staff_position");
6341 object.remove("notehead");
6342 object.remove("preferred_voice");
6343 object.remove("techniques");
6344 instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
6345 assert_eq!(instrument.staff_position, None);
6346 assert_eq!(instrument.notehead, None);
6347 assert_eq!(instrument.preferred_voice, None);
6348 assert!(instrument.techniques.is_empty());
6349 }
6350
6351 #[test]
6354 fn score_template_solo_has_one_part_treble() {
6355 let score = Score::template(ScoreTemplate::Solo);
6356 assert_eq!(score.parts.len(), 1);
6357 assert_eq!(score.parts[0].staves.len(), 1);
6358 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
6359 assert_eq!(score.parts[0].midi_program, 0);
6360 }
6361
6362 #[test]
6363 fn score_template_piano_has_two_staves() {
6364 let score = Score::template(ScoreTemplate::Piano);
6365 assert_eq!(score.parts.len(), 1);
6366 assert_eq!(score.parts[0].staves.len(), 2);
6367 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
6368 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
6369 }
6370
6371 #[test]
6372 fn score_template_string_quartet_has_four_parts() {
6373 let score = Score::template(ScoreTemplate::StringQuartet);
6374 assert_eq!(score.parts.len(), 4);
6375 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);
6378 assert_eq!(score.parts[3].midi_program, 42);
6379 }
6380
6381 #[test]
6382 fn score_template_string_orchestra_has_five_parts() {
6383 let score = Score::template(ScoreTemplate::StringOrchestra);
6384 assert_eq!(score.parts.len(), 5);
6385 assert_eq!(score.parts[4].midi_program, 43); }
6387
6388 #[test]
6389 fn score_template_brass_quintet_has_five_parts() {
6390 let score = Score::template(ScoreTemplate::BrassQuintet);
6391 assert_eq!(score.parts.len(), 5);
6392 assert_eq!(score.parts[2].midi_program, 60); }
6394
6395 #[test]
6396 fn score_template_default_measures_are_four() {
6397 let score = Score::template(ScoreTemplate::StringQuartet);
6398 for part in &score.parts {
6399 for staff in &part.staves {
6400 assert_eq!(staff.measures.len(), 4);
6401 }
6402 }
6403 }
6404
6405 #[test]
6408 fn measure_empty_has_no_breaks() {
6409 let m = Measure::empty(4, 4);
6410 assert!(!m.system_break);
6411 assert!(!m.page_break);
6412 assert!(!m.section_break);
6413 }
6414
6415 #[test]
6416 fn section_range_uses_semantic_boundaries_not_layout_breaks() {
6417 let mut score = Score::new("sections", 120, 4, 4, 0, 6);
6418 score.parts[0].staves[0].measures[0].section_break = true;
6419 score.parts[0].staves[0].measures[2].section_break = true;
6420 score.parts[0].staves[0].measures[4].section_break = true;
6421 score.parts[0].staves[0].measures[5].section_break = true;
6422 score.parts[0].staves[0].measures[1].system_break = true;
6423 score.parts[0].staves[0].measures[3].page_break = true;
6424 assert_eq!(score.section_range(0).unwrap(), 0..=1);
6425 assert_eq!(score.section_range(3).unwrap(), 2..=3);
6426 assert_eq!(score.section_range(4).unwrap(), 4..=4);
6427 assert_eq!(score.section_range(5).unwrap(), 5..=5);
6428 assert!(score.section_range(6).is_err());
6429 }
6430
6431 #[test]
6432 fn system_break_survives_json_roundtrip() {
6433 let mut m = Measure::empty(4, 4);
6434 m.system_break = true;
6435 let json = serde_json::to_string(&m).unwrap();
6436 let m2: Measure = serde_json::from_str(&json).unwrap();
6437 assert!(m2.system_break);
6438 assert!(!m2.page_break);
6439 }
6440
6441 #[test]
6444 fn diff_identical_scores_is_empty() {
6445 let s = Score::new("T", 120, 4, 4, 0, 2);
6446 assert!(diff(&s, &s).is_empty());
6447 }
6448
6449 #[test]
6450 fn score_patch_covers_measure_semantics_and_note_insert_index() {
6451 let mut a = Score::new("T", 120, 4, 4, 0, 1);
6452 a.parts[0].staves[0].measures[0].voices[0].clear();
6453 let mut b = a.clone();
6454 let measure = &mut b.parts[0].staves[0].measures[0];
6455 measure.key_sig = Some(KeySignature {
6456 fifths: -2,
6457 mode: "major".to_string(),
6458 });
6459 measure.time_sig = Some(TimeSignature {
6460 numerator: 3,
6461 denominator: 4,
6462 });
6463 measure.barline_left = Barline::RepeatStart;
6464 measure.barline_right = Barline::RepeatEnd;
6465 measure.rehearsal = Some("A".to_string());
6466 measure.volta = Some(VoltaBracket {
6467 number: 1,
6468 kind: "begin_end".to_string(),
6469 numbers: Vec::new(),
6470 });
6471 measure.texts.push(StyledText {
6472 position_beats: None,
6473 style: TextStyle::RehearsalMark,
6474 text: "A".to_string(),
6475 placement: None,
6476 offset_x: None,
6477 offset_y: None,
6478 relative_x: None,
6479 relative_y: None,
6480 });
6481 measure.figured_bass.push(FiguredBassFigure {
6482 number: "6".to_string(),
6483 alter: None,
6484 prefix: None,
6485 suffix: None,
6486 extender: false,
6487 });
6488 measure.harp_pedal_diagrams.push(HarpPedalDiagram {
6489 positions: [
6490 HarpPedalPosition::Flat,
6491 HarpPedalPosition::Natural,
6492 HarpPedalPosition::Sharp,
6493 HarpPedalPosition::Natural,
6494 HarpPedalPosition::Flat,
6495 HarpPedalPosition::Sharp,
6496 HarpPedalPosition::Natural,
6497 ],
6498 placement: Some("above".to_string()),
6499 });
6500 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
6501 let expected = b.parts[0].staves[0].measures[0].clone();
6502
6503 let patches = score_patch(&a, &b);
6504 assert!(
6505 patches
6506 .iter()
6507 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
6508 );
6509 assert!(
6510 patches
6511 .iter()
6512 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
6513 );
6514 assert!(
6515 patches
6516 .iter()
6517 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
6518 );
6519 assert!(
6520 patches
6521 .iter()
6522 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
6523 );
6524 assert!(
6525 patches
6526 .iter()
6527 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
6528 );
6529 assert!(
6530 patches
6531 .iter()
6532 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
6533 );
6534 assert!(
6535 patches
6536 .iter()
6537 .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
6538 );
6539 let result = apply_patch(&a, &patches).expect("patch application failed");
6540 let result_measure = &result.parts[0].staves[0].measures[0];
6541 assert_eq!(result_measure.key_sig, expected.key_sig);
6542 assert_eq!(result_measure.time_sig, expected.time_sig);
6543 assert_eq!(result_measure.barline_left, expected.barline_left);
6544 assert_eq!(result_measure.barline_right, expected.barline_right);
6545 assert_eq!(result_measure.rehearsal, expected.rehearsal);
6546 assert_eq!(result_measure.volta, expected.volta);
6547 assert_eq!(result_measure.texts, expected.texts);
6548 assert_eq!(result_measure.figured_bass, expected.figured_bass);
6549 assert_eq!(
6550 result_measure.harp_pedal_diagrams,
6551 expected.harp_pedal_diagrams
6552 );
6553 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
6554 }
6555
6556 #[test]
6557 fn malformed_note_patches_do_not_mutate_the_original_score() {
6558 let score = Score::new("Patch boundary", 120, 4, 4, 0, 1);
6559 let before = serde_json::to_value(&score).unwrap();
6560 let note = Box::new(Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
6561 for (part, staff, measure, voice, note_index) in [
6562 (8, 0, 0, 0, 0),
6563 (0, 8, 0, 0, 0),
6564 (0, 0, 8, 0, 0),
6565 (0, 0, 0, 8, 0),
6566 (0, 0, 0, 0, 99),
6567 ] {
6568 let patches = [
6569 ScorePatch::AddNote {
6570 part,
6571 staff,
6572 measure,
6573 voice,
6574 note_index,
6575 note: note.clone(),
6576 },
6577 ScorePatch::RemoveNote {
6578 part,
6579 staff,
6580 measure,
6581 voice,
6582 note_index,
6583 },
6584 ScorePatch::ReplaceNote {
6585 part,
6586 staff,
6587 measure,
6588 voice,
6589 note_index,
6590 note: note.clone(),
6591 },
6592 ];
6593 for patch in patches {
6594 assert!(apply_patch(&score, &[patch]).is_err());
6595 assert_eq!(serde_json::to_value(&score).unwrap(), before);
6596 }
6597 }
6598 }
6599
6600 #[test]
6601 fn measure_patches_reject_missing_part_staff_and_measure() {
6602 let score = Score::new("Patch boundary", 120, 4, 4, 0, 1);
6603 let before = serde_json::to_value(&score).unwrap();
6604 for (part, staff, measure) in [(8, 0, 0), (0, 8, 0), (0, 0, 8)] {
6605 let patches = vec![
6606 ScorePatch::SetMeasureTexts {
6607 part,
6608 staff,
6609 measure,
6610 value: vec![],
6611 },
6612 ScorePatch::SetFiguredBass {
6613 part,
6614 staff,
6615 measure,
6616 value: vec![],
6617 },
6618 ScorePatch::SetHarpPedalDiagrams {
6619 part,
6620 staff,
6621 measure,
6622 value: vec![],
6623 },
6624 ScorePatch::SetMeasureTablatureChange {
6625 part,
6626 staff,
6627 measure,
6628 value: None,
6629 },
6630 ScorePatch::SetMeasureMidClefs {
6631 part,
6632 staff,
6633 measure,
6634 value: vec![],
6635 },
6636 ScorePatch::SetKeySignature {
6637 part,
6638 staff,
6639 measure,
6640 value: None,
6641 },
6642 ScorePatch::SetTimeSignature {
6643 part,
6644 staff,
6645 measure,
6646 value: None,
6647 },
6648 ScorePatch::SetTimeSymbol {
6649 part,
6650 staff,
6651 measure,
6652 value: None,
6653 },
6654 ScorePatch::SetBarlines {
6655 part,
6656 staff,
6657 measure,
6658 left: Barline::default(),
6659 right: Barline::default(),
6660 },
6661 ScorePatch::SetRehearsal {
6662 part,
6663 staff,
6664 measure,
6665 value: None,
6666 },
6667 ScorePatch::SetVolta {
6668 part,
6669 staff,
6670 measure,
6671 value: None,
6672 },
6673 ScorePatch::SetMeasureTempo {
6674 part,
6675 staff,
6676 measure,
6677 value: None,
6678 },
6679 ScorePatch::SetMeasureTempoRamp {
6680 part,
6681 staff,
6682 measure,
6683 value: None,
6684 },
6685 ScorePatch::SetMeasureMetronomeMark {
6686 part,
6687 staff,
6688 measure,
6689 value: None,
6690 },
6691 ];
6692 for patch in patches {
6693 assert!(apply_patch(&score, &[patch]).is_err());
6694 assert_eq!(serde_json::to_value(&score).unwrap(), before);
6695 }
6696 }
6697 }
6698
6699 #[test]
6700 fn part_and_staff_patches_reject_missing_targets() {
6701 let score = Score::new("Patch targets", 120, 4, 4, 0, 1);
6702 let original = serde_json::to_value(&score).unwrap();
6703 let part = 99;
6704 let patches = vec![
6705 ScorePatch::SetPartNames {
6706 part,
6707 name: "Violin".into(),
6708 short_name: "Vln".into(),
6709 },
6710 ScorePatch::SetPartMidi {
6711 part,
6712 channel: 1,
6713 program: 40,
6714 },
6715 ScorePatch::SetInstrumentDefinition { part, value: None },
6716 ScorePatch::SetPartMidiAutomation {
6717 part,
6718 pitch_bends: vec![],
6719 control_changes: vec![],
6720 program_changes: vec![],
6721 aftertouch: vec![],
6722 },
6723 ];
6724 for patch in patches {
6725 assert!(apply_patch(&score, &[patch]).is_err());
6726 }
6727 for (part, staff) in [(99, 0), (0, 99)] {
6728 let patches = vec![
6729 ScorePatch::SetStaffConfiguration {
6730 part,
6731 staff,
6732 clef: Clef::Treble,
6733 transpose_semitones: 0,
6734 },
6735 ScorePatch::SetStaffPresentation {
6736 part,
6737 staff,
6738 value: StaffPresentation::default(),
6739 },
6740 ScorePatch::SetTablatureConfig {
6741 part,
6742 staff,
6743 value: None,
6744 },
6745 ];
6746 for patch in patches {
6747 assert!(apply_patch(&score, &[patch]).is_err());
6748 }
6749 }
6750 assert_eq!(serde_json::to_value(&score).unwrap(), original);
6751 }
6752
6753 #[test]
6754 fn diff_and_patch_preserve_score_level_texts() {
6755 let a = Score::new("T", 120, 4, 4, 0, 1);
6756 let mut b = a.clone();
6757 b.texts.push(StyledText {
6758 position_beats: None,
6759 style: TextStyle::Expression,
6760 text: "Prelude".to_string(),
6761 placement: Some("above".to_string()),
6762 offset_x: Some(12.0),
6763 offset_y: Some(-8.0),
6764 relative_x: None,
6765 relative_y: None,
6766 });
6767
6768 let changes = diff(&a, &b);
6769 assert!(changes.iter().any(|change| matches!(
6770 change,
6771 ScoreChange::ScoreTextChanged { old, new }
6772 if old.is_empty() && new == &b.texts
6773 )));
6774
6775 let patches = score_patch(&a, &b);
6776 assert!(patches.iter().any(|patch| matches!(
6777 patch,
6778 ScorePatch::SetScoreTexts { value } if value == &b.texts
6779 )));
6780 let result = apply_patch(&a, &patches).expect("score text patch failed");
6781 assert_eq!(result.texts, b.texts);
6782 }
6783
6784 #[test]
6785 fn mixed_editor_changes_round_trip_as_local_patches() {
6786 let mut before = Score::new("Before", 120, 4, 4, 0, 2);
6787 before.parts[0].staves[0].measures[0].voices[0] = vec![
6788 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6789 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
6790 ];
6791 before.parts[0].staves[0].measures[1].voices[0] = vec![
6792 Note::new(Pitch::new(Step::E, 4), Duration::Quarter),
6793 Note::new(Pitch::new(Step::F, 4), Duration::Quarter),
6794 ];
6795 let mut after = before.clone();
6796 after.metadata.title = "After".into();
6797 after.metadata.composer = "Composer".into();
6798 after.metadata.lyricist = "Lyricist".into();
6799 after.metadata.copyright = "Copyright".into();
6800 after.metadata.work_number = "Op. 1".into();
6801 after.metadata.movement_title = "Prelude".into();
6802 after.settings.tempo_bpm = 96;
6803 after.parts[0].name = "Piano".into();
6804 after.parts[0].short_name = "Pno.".into();
6805 after.parts[0].midi_channel = 1;
6806 after.parts[0].midi_program = 41;
6807 after.parts[0].midi_pitch_bends.push(MidiPitchBend {
6808 tick: 240,
6809 channel: 1,
6810 value: 512,
6811 });
6812 after.parts[0].staves[0].clef = Clef::Bass;
6813 after.parts[0].staves[0].measures[0].voices[0][0] =
6814 Note::new(Pitch::new(Step::G, 3), Duration::Quarter);
6815 after.parts[0].staves[0].measures[0].voices[0]
6816 .push(Note::new(Pitch::new(Step::A, 3), Duration::Quarter));
6817 after.parts[0].staves[0].measures[1].voices[0].pop();
6818
6819 let changes = diff(&before, &after);
6820 assert!(
6821 changes
6822 .iter()
6823 .any(|change| matches!(change, ScoreChange::PartMidiAutomationChanged { .. }))
6824 );
6825 assert!(
6826 changes
6827 .iter()
6828 .any(|change| matches!(change, ScoreChange::NoteModified { .. }))
6829 );
6830 assert!(
6831 changes
6832 .iter()
6833 .any(|change| matches!(change, ScoreChange::NoteAdded { .. }))
6834 );
6835 assert!(
6836 changes
6837 .iter()
6838 .any(|change| matches!(change, ScoreChange::NoteRemoved { .. }))
6839 );
6840 let patches = score_patch(&before, &after);
6841 assert!(
6842 !patches
6843 .iter()
6844 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6845 );
6846 let result = apply_patch(&before, &patches).expect("local patches apply");
6847 assert_eq!(
6848 serde_json::to_value(result).expect("patched score"),
6849 serde_json::to_value(after).expect("expected score")
6850 );
6851 }
6852
6853 #[test]
6854 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
6855 let a = Score::new("T", 120, 4, 4, 0, 1);
6856 let mut b = a.clone();
6857 let measure = &mut b.parts[0].staves[0].measures[0];
6858 measure.number = 8;
6859 measure.clef = Some(Clef::Bass);
6860 measure.tempo_text = Some("Allegro".to_string());
6861 measure.navigation = Some("D.S.".to_string());
6862 measure.expression_text = Some("espressivo".to_string());
6863 measure.multi_rest_count = Some(3);
6864 measure.system_break = true;
6865 measure.page_break = true;
6866
6867 let changes = diff(&a, &b);
6868 assert!(changes.iter().any(|change| matches!(
6869 change,
6870 ScoreChange::MeasurePresentationChanged {
6871 part: 0,
6872 staff: 0,
6873 measure: 0,
6874 old_number: 1,
6875 new_number: 8,
6876 old_clef: None,
6877 new_clef: Some(Clef::Bass),
6878 new_tempo_text: Some(text),
6879 new_navigation: Some(navigation),
6880 new_expression_text: Some(expression),
6881 new_multi_rest_count: Some(3),
6882 old_system_break: false,
6883 new_system_break: true,
6884 old_page_break: false,
6885 new_page_break: true,
6886 ..
6887 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
6888 )));
6889 assert!(!changes.iter().any(|change| matches!(
6890 change,
6891 ScoreChange::UnrepresentedFieldChanged { path }
6892 if path == "parts[0].staves[0].measures[0].number"
6893 )));
6894
6895 let patches = score_patch(&a, &b);
6896 assert!(patches.iter().any(|patch| matches!(
6897 patch,
6898 ScorePatch::SetMeasurePresentation {
6899 part: 0,
6900 staff: 0,
6901 measure: 0,
6902 number: 8,
6903 clef: Some(Clef::Bass),
6904 tempo_text: Some(text),
6905 navigation: Some(navigation),
6906 expression_text: Some(expression),
6907 multi_rest_count: Some(3),
6908 system_break: true,
6909 page_break: true,
6910 section_break: false,
6911 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
6912 )));
6913 assert!(
6914 !patches
6915 .iter()
6916 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6917 );
6918 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
6919 let decoded: Vec<ScorePatch> =
6920 serde_json::from_str(&json).expect("measure presentation patch should decode");
6921 let mut legacy_json: serde_json::Value =
6922 serde_json::from_str(&json).expect("patch JSON value");
6923 for patch in legacy_json.as_array_mut().expect("patch JSON array") {
6924 patch
6925 .as_object_mut()
6926 .expect("patch JSON object")
6927 .remove("section_break");
6928 }
6929 let legacy: Vec<ScorePatch> =
6930 serde_json::from_value(legacy_json).expect("legacy patch should decode");
6931 assert!(matches!(
6932 legacy.first(),
6933 Some(ScorePatch::SetMeasurePresentation {
6934 section_break: false,
6935 ..
6936 })
6937 ));
6938 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
6939 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
6940 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
6941 assert_eq!(
6942 serde_json::to_value(&result).expect("patched score JSON"),
6943 serde_json::to_value(&b).expect("expected score JSON")
6944 );
6945 }
6946
6947 #[test]
6948 fn chord_definition_changes_use_typed_diff_and_local_patch() {
6949 let a = Score::new("T", 120, 4, 4, 0, 1);
6950 let mut b = a.clone();
6951 b.chord_definitions.push(ChordDefinition {
6952 id: Some("c-major".to_string()),
6953 label: Some("C".to_string()),
6954 kind: Some("major".to_string()),
6955 fret_position: Some(0),
6956 tab_strings: Some("x32010".to_string()),
6957 tab_courses: None,
6958 members: Vec::new(),
6959 barres: Vec::new(),
6960 });
6961
6962 let changes = diff(&a, &b);
6963 assert!(changes.iter().any(|change| matches!(
6964 change,
6965 ScoreChange::ChordDefinitionsChanged { old, new }
6966 if old.is_empty() && new == &b.chord_definitions
6967 )));
6968 assert!(!changes.iter().any(|change| matches!(
6969 change,
6970 ScoreChange::UnrepresentedFieldChanged { path }
6971 if path == "chord_definitions"
6972 )));
6973
6974 let patches = score_patch(&a, &b);
6975 assert!(patches.iter().any(|patch| matches!(
6976 patch,
6977 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
6978 )));
6979 assert!(
6980 !patches
6981 .iter()
6982 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6983 );
6984 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
6985 let decoded: Vec<ScorePatch> =
6986 serde_json::from_str(&json).expect("chord definition patch should decode");
6987 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
6988 assert_eq!(result.chord_definitions, b.chord_definitions);
6989 }
6990
6991 #[test]
6992 fn part_name_changes_use_typed_diff_and_local_patch() {
6993 let a = Score::new("T", 120, 4, 4, 0, 1);
6994 let mut b = a.clone();
6995 b.parts[0].name = "Violin".to_string();
6996 b.parts[0].short_name = "Vln.".to_string();
6997 b.parts[0].midi_channel = 4;
6998 b.parts[0].midi_program = 40;
6999 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
7000 tick: 120,
7001 channel: 4,
7002 value: 2048,
7003 });
7004 b.parts[0].midi_control_changes.push(MidiControlChange {
7005 tick: 240,
7006 channel: 4,
7007 controller: 64,
7008 value: 127,
7009 });
7010 b.parts[0].midi_program_changes.push(MidiProgramChange {
7011 tick: 0,
7012 channel: 4,
7013 program: 40,
7014 });
7015 b.parts[0].midi_aftertouch.push(MidiAftertouch {
7016 tick: 360,
7017 channel: 4,
7018 key: Some(64),
7019 value: 80,
7020 });
7021 b.parts[0].staves[0].clef = Clef::Bass;
7022 b.parts[0].staves[0].transpose_semitones = -2;
7023
7024 let changes = diff(&a, &b);
7025 assert!(changes.iter().any(|change| matches!(
7026 change,
7027 ScoreChange::PartNamesChanged {
7028 part: 0,
7029 old_name,
7030 new_name,
7031 old_short_name,
7032 new_short_name,
7033 } if old_name == "Piano"
7034 && new_name == "Violin"
7035 && old_short_name == "Pno."
7036 && new_short_name == "Vln."
7037 )));
7038 assert!(changes.iter().any(|change| matches!(
7039 change,
7040 ScoreChange::PartMidiChanged {
7041 part: 0,
7042 old_channel: 0,
7043 new_channel: 4,
7044 old_program: 0,
7045 new_program: 40,
7046 }
7047 )));
7048 assert!(changes.iter().any(|change| matches!(
7049 change,
7050 ScoreChange::PartMidiAutomationChanged {
7051 part: 0,
7052 new_pitch_bends,
7053 new_control_changes,
7054 new_program_changes,
7055 new_aftertouch,
7056 ..
7057 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
7058 && new_control_changes == &b.parts[0].midi_control_changes
7059 && new_program_changes == &b.parts[0].midi_program_changes
7060 && new_aftertouch == &b.parts[0].midi_aftertouch
7061 )));
7062 assert!(changes.iter().any(|change| matches!(
7063 change,
7064 ScoreChange::StaffConfigurationChanged {
7065 part: 0,
7066 staff: 0,
7067 old_clef: Clef::Treble,
7068 new_clef: Clef::Bass,
7069 old_transpose_semitones: 0,
7070 new_transpose_semitones: -2,
7071 }
7072 )));
7073 assert!(!changes.iter().any(|change| matches!(
7074 change,
7075 ScoreChange::UnrepresentedFieldChanged { path }
7076 if path == "parts[0].name"
7077 )));
7078
7079 let patches = score_patch(&a, &b);
7080 assert!(patches.iter().any(|patch| matches!(
7081 patch,
7082 ScorePatch::SetPartNames {
7083 part: 0,
7084 name,
7085 short_name,
7086 } if name == "Violin" && short_name == "Vln."
7087 )));
7088 assert!(
7089 !patches
7090 .iter()
7091 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
7092 );
7093 assert!(patches.iter().any(|patch| matches!(
7094 patch,
7095 ScorePatch::SetPartMidi {
7096 part: 0,
7097 channel: 4,
7098 program: 40,
7099 }
7100 )));
7101 assert!(patches.iter().any(|patch| matches!(
7102 patch,
7103 ScorePatch::SetPartMidiAutomation {
7104 part: 0,
7105 pitch_bends,
7106 control_changes,
7107 program_changes,
7108 aftertouch,
7109 } if pitch_bends == &b.parts[0].midi_pitch_bends
7110 && control_changes == &b.parts[0].midi_control_changes
7111 && program_changes == &b.parts[0].midi_program_changes
7112 && aftertouch == &b.parts[0].midi_aftertouch
7113 )));
7114 assert!(patches.iter().any(|patch| matches!(
7115 patch,
7116 ScorePatch::SetStaffConfiguration {
7117 part: 0,
7118 staff: 0,
7119 clef: Clef::Bass,
7120 transpose_semitones: -2,
7121 }
7122 )));
7123 let result = apply_patch(&a, &patches).expect("part name patch failed");
7124 assert_eq!(result.parts[0].name, b.parts[0].name);
7125 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
7126 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
7127 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
7128 assert_eq!(
7129 result.parts[0].midi_pitch_bends,
7130 b.parts[0].midi_pitch_bends
7131 );
7132 assert_eq!(
7133 result.parts[0].midi_control_changes,
7134 b.parts[0].midi_control_changes
7135 );
7136 assert_eq!(
7137 result.parts[0].midi_program_changes,
7138 b.parts[0].midi_program_changes
7139 );
7140 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
7141 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
7142 assert_eq!(
7143 result.parts[0].staves[0].transpose_semitones,
7144 b.parts[0].staves[0].transpose_semitones
7145 );
7146 }
7147
7148 #[test]
7149 fn diff_reports_measure_text_and_figured_bass_changes() {
7150 let a = Score::new("T", 120, 4, 4, 0, 1);
7151 let mut b = a.clone();
7152 let measure = &mut b.parts[0].staves[0].measures[0];
7153 measure.texts.push(StyledText {
7154 position_beats: None,
7155 style: TextStyle::Lyrics,
7156 text: "la".to_string(),
7157 placement: None,
7158 offset_x: None,
7159 offset_y: None,
7160 relative_x: None,
7161 relative_y: None,
7162 });
7163 measure.figured_bass.push(FiguredBassFigure {
7164 number: "6".to_string(),
7165 alter: None,
7166 prefix: None,
7167 suffix: None,
7168 extender: false,
7169 });
7170
7171 let changes = diff(&a, &b);
7172 assert!(changes.iter().any(|change| matches!(
7173 change,
7174 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
7175 if old.is_empty() && new.len() == 1
7176 )));
7177 assert!(changes.iter().any(|change| matches!(
7178 change,
7179 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
7180 if old.is_empty() && new.len() == 1
7181 )));
7182 }
7183
7184 #[test]
7185 fn diff_reports_tablature_changes_and_patches_them_locally() {
7186 let a = Score::new("T", 120, 4, 4, 0, 1);
7187 let mut b = a.clone();
7188 b.parts[0].staves[0].tablature = Some(TablatureConfig {
7189 lines: 6,
7190 tuning_midi: vec![40, 45, 50, 55, 59, 64],
7191 capo: 2,
7192 });
7193
7194 let changes = diff(&a, &b);
7195 assert!(!changes.iter().any(|change| matches!(
7196 change,
7197 ScoreChange::UnrepresentedFieldChanged { path }
7198 if path == "parts[0].staves[0].tablature"
7199 )));
7200 assert!(changes.iter().any(|change| matches!(
7201 change,
7202 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
7203 if config.lines == 6 && config.capo == 2
7204 )));
7205 let patches = score_patch(&a, &b);
7206 assert!(patches.iter().any(|patch| matches!(
7207 patch,
7208 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
7209 if config.lines == 6 && config.capo == 2
7210 )));
7211 assert_eq!(
7212 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
7213 b.parts[0].staves[0].tablature
7214 );
7215 }
7216
7217 #[test]
7218 fn diff_and_patch_preserve_hidden_time_signature() {
7219 let a = Score::new("Meter", 120, 4, 4, 0, 1);
7220 let mut b = a.clone();
7221 b.parts[0].staves[0].measures[0].time_sig_hidden = true;
7222 assert!(diff(&a, &b).iter().any(|change| matches!(
7223 change,
7224 ScoreChange::TimeVisibilityChanged {
7225 old: false,
7226 new: true,
7227 ..
7228 }
7229 )));
7230 let patch = score_patch(&a, &b);
7231 assert!(matches!(
7232 patch.as_slice(),
7233 [ScorePatch::SetTimeVisibility { hidden: true, .. }]
7234 ));
7235 assert!(apply_patch(&a, &patch).unwrap().parts[0].staves[0].measures[0].time_sig_hidden);
7236 }
7237
7238 #[test]
7239 fn diff_and_patch_preserve_common_time_symbol() {
7240 let a = Score::new("Meter", 120, 4, 4, 0, 1);
7241 let mut b = a.clone();
7242 b.parts[0].staves[0].measures[0].time_symbol = Some(TimeSymbol::Common);
7243 assert!(diff(&a, &b).iter().any(|change| matches!(
7244 change,
7245 ScoreChange::TimeSymbolChanged {
7246 part: 0,
7247 staff: 0,
7248 measure: 0,
7249 new: Some(TimeSymbol::Common),
7250 ..
7251 }
7252 )));
7253 let patches = score_patch(&a, &b);
7254 assert!(patches.iter().any(|patch| matches!(
7255 patch,
7256 ScorePatch::SetTimeSymbol {
7257 part: 0,
7258 staff: 0,
7259 measure: 0,
7260 value: Some(TimeSymbol::Common)
7261 }
7262 )));
7263 assert_eq!(
7264 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[0].time_symbol,
7265 Some(TimeSymbol::Common)
7266 );
7267 }
7268
7269 #[test]
7270 fn diff_and_patch_preserve_written_metronome_marks() {
7271 let a = Score::new("T", 120, 4, 4, 0, 1);
7272 let mut b = a.clone();
7273 b.parts[0].staves[0].measures[0].metronome_mark = Some(MetronomeMark {
7274 beat_unit: Duration::Quarter,
7275 beat_unit_dots: 0,
7276 per_minute: 69,
7277 parentheses: true,
7278 });
7279 let changes = diff(&a, &b);
7280 assert!(changes.iter().any(|change| matches!(
7281 change,
7282 ScoreChange::MeasureMetronomeMarkChanged {
7283 part: 0,
7284 staff: 0,
7285 measure: 0,
7286 ..
7287 }
7288 )));
7289 let patches = score_patch(&a, &b);
7290 assert!(patches.iter().any(|patch| matches!(
7291 patch,
7292 ScorePatch::SetMeasureMetronomeMark {
7293 part: 0,
7294 staff: 0,
7295 measure: 0,
7296 ..
7297 }
7298 )));
7299 assert_eq!(
7300 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[0].metronome_mark,
7301 b.parts[0].staves[0].measures[0].metronome_mark
7302 );
7303 }
7304
7305 #[test]
7306 fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
7307 let mut a = Score::new("T", 120, 4, 4, 0, 2);
7308 a.parts[0].staves[0].tablature = Some(TablatureConfig {
7309 lines: 6,
7310 tuning_midi: vec![40, 45, 50, 55, 59, 64],
7311 capo: 0,
7312 });
7313 let mut b = a.clone();
7314 b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
7315 lines: 6,
7316 tuning_midi: vec![40, 45, 50, 55, 59, 64],
7317 capo: 3,
7318 });
7319
7320 let changes = diff(&a, &b);
7321 assert!(changes.iter().any(|change| matches!(
7322 change,
7323 ScoreChange::TablatureChangeChanged {
7324 part: 0,
7325 staff: 0,
7326 measure: 1,
7327 old: None,
7328 new: Some(config),
7329 } if config.capo == 3
7330 )));
7331 let patches = score_patch(&a, &b);
7332 assert!(patches.iter().any(|patch| matches!(
7333 patch,
7334 ScorePatch::SetMeasureTablatureChange {
7335 part: 0,
7336 staff: 0,
7337 measure: 1,
7338 value: Some(config),
7339 } if config.capo == 3
7340 )));
7341 assert_eq!(
7342 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
7343 b.parts[0].staves[0].measures[1].tablature_change
7344 );
7345 }
7346
7347 #[test]
7348 fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
7349 let a = Score::new("Ramp", 120, 4, 4, 0, 2);
7350 let mut b = a.clone();
7351 b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
7352
7353 assert!(diff(&a, &b).iter().any(|change| matches!(
7354 change,
7355 ScoreChange::MeasureTempoRampChanged {
7356 part: 0,
7357 staff: 0,
7358 measure: 1,
7359 old: None,
7360 new: Some(72),
7361 }
7362 )));
7363 let patches = score_patch(&a, &b);
7364 assert!(patches.iter().any(|patch| matches!(
7365 patch,
7366 ScorePatch::SetMeasureTempoRamp {
7367 part: 0,
7368 staff: 0,
7369 measure: 1,
7370 value: Some(72),
7371 }
7372 )));
7373 assert_eq!(
7374 apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
7375 .tempo_ramp_to,
7376 b.parts[0].staves[0].measures[1].tempo_ramp_to
7377 );
7378 }
7379
7380 #[test]
7381 fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
7382 let a = Score::new("T", 120, 4, 4, 0, 1);
7383 let mut b = a.clone();
7384 b.parts[0].staves[0].presentation = StaffPresentation {
7385 kind: StaffKind::Percussion,
7386 lines: 1,
7387 line_distance: 1.5,
7388 small: true,
7389 cutaway: true,
7390 visible: false,
7391 notehead_scheme: StaffNoteheadScheme::PitchNames,
7392 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
7393 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
7394 };
7395
7396 let changes = diff(&a, &b);
7397 assert!(changes.iter().any(|change| matches!(
7398 change,
7399 ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
7400 if new == &b.parts[0].staves[0].presentation
7401 )));
7402 let patches = score_patch(&a, &b);
7403 assert!(patches.iter().any(|patch| matches!(
7404 patch,
7405 ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
7406 if value == &b.parts[0].staves[0].presentation
7407 )));
7408 assert!(
7409 !patches
7410 .iter()
7411 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
7412 );
7413 assert_eq!(
7414 apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
7415 b.parts[0].staves[0].presentation
7416 );
7417 }
7418
7419 #[test]
7420 fn score_patch_uses_local_presentation_patch_for_display_fields() {
7421 let a = Score::new("T", 120, 4, 4, 0, 1);
7422 let mut b = a.clone();
7423 b.parts[0].name = "Piano".to_string();
7424 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
7425 let patches = score_patch(&a, &b);
7426 assert!(patches.iter().any(|patch| matches!(
7427 patch,
7428 ScorePatch::SetMeasurePresentation {
7429 part: 0,
7430 staff: 0,
7431 measure: 0,
7432 expression_text: Some(text),
7433 ..
7434 } if text == "dolce"
7435 )));
7436 assert!(
7437 !patches
7438 .iter()
7439 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
7440 );
7441 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
7442 assert_eq!(result.parts[0].name, "Piano");
7443 assert_eq!(
7444 result.parts[0].staves[0].measures[0].expression_text,
7445 Some("dolce".to_string())
7446 );
7447 }
7448
7449 #[test]
7450 fn diff_detects_tempo_change() {
7451 let a = Score::new("T", 120, 4, 4, 0, 1);
7452 let mut b = a.clone();
7453 b.settings.tempo_bpm = 90;
7454 let changes = diff(&a, &b);
7455 assert_eq!(changes.len(), 1);
7456 assert!(matches!(
7457 changes[0],
7458 ScoreChange::TempoChanged { old: 120, new: 90 }
7459 ));
7460 }
7461
7462 #[test]
7463 fn diff_detects_title_change() {
7464 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
7465 let mut b = a.clone();
7466 b.metadata.title = "New Title".to_string();
7467 let changes = diff(&a, &b);
7468 assert!(
7469 changes.iter().any(
7470 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
7471 )
7472 );
7473 }
7474
7475 #[test]
7476 fn diff_detects_note_modification() {
7477 let mut a = Score::new("T", 120, 4, 4, 0, 1);
7478 a.parts[0].staves[0].measures[0].voices[0] =
7479 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
7480 let mut b = a.clone();
7481 b.parts[0].staves[0].measures[0].voices[0][0] =
7482 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
7483 let changes = diff(&a, &b);
7484 assert!(
7485 changes
7486 .iter()
7487 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
7488 );
7489 }
7490
7491 #[test]
7492 fn diff_detects_part_added() {
7493 let a = Score::new("T", 120, 4, 4, 0, 1);
7494 let mut b = a.clone();
7495 let mut p = Part::new("Violin", "Vln.");
7496 p.staves.push(Staff::new(Clef::Treble));
7497 b.parts.push(p);
7498 let changes = diff(&a, &b);
7499 assert!(
7500 changes
7501 .iter()
7502 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
7503 );
7504 }
7505
7506 #[test]
7507 fn diff_detects_measure_tempo_change() {
7508 let a = Score::new("T", 120, 4, 4, 0, 2);
7509 let mut b = a.clone();
7510 b.parts[0].staves[0].measures[1].tempo = Some(60);
7511 let changes = diff(&a, &b);
7512 assert!(changes.iter().any(|c| matches!(
7513 c,
7514 ScoreChange::MeasureTempoChanged {
7515 measure: 1,
7516 old: None,
7517 new: Some(60),
7518 ..
7519 }
7520 )));
7521 }
7522
7523 #[test]
7524 fn diff_detects_barline_change() {
7525 use crate::model::notation::Barline;
7526 let a = Score::new("T", 120, 4, 4, 0, 2);
7527 let mut b = a.clone();
7528 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
7529 let changes = diff(&a, &b);
7530 assert!(
7531 changes
7532 .iter()
7533 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
7534 );
7535 }
7536
7537 #[test]
7538 fn diff_detects_rehearsal_change() {
7539 let a = Score::new("T", 120, 4, 4, 0, 2);
7540 let mut b = a.clone();
7541 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
7542 let changes = diff(&a, &b);
7543 assert!(
7544 changes
7545 .iter()
7546 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
7547 );
7548 }
7549
7550 #[test]
7551 fn diff_detects_volta_change() {
7552 use super::VoltaBracket;
7553 let a = Score::new("T", 120, 4, 4, 0, 2);
7554 let mut b = a.clone();
7555 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
7556 number: 1,
7557 kind: "begin_end".into(),
7558 numbers: Vec::new(),
7559 });
7560 let changes = diff(&a, &b);
7561 assert!(
7562 changes
7563 .iter()
7564 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
7565 );
7566 }
7567
7568 #[test]
7569 fn diff_detects_key_signature_change() {
7570 let a = Score::new("T", 120, 4, 4, 0, 1);
7571 let mut b = a.clone();
7572 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
7574 assert!(
7575 changes
7576 .iter()
7577 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
7578 );
7579 }
7580
7581 #[test]
7582 fn measure_key_signature_uses_typed_local_patch() {
7583 let a = Score::new("T", 120, 4, 4, 0, 1);
7584 let mut b = a.clone();
7585 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
7586 fifths: 2,
7587 mode: "major".to_string(),
7588 });
7589 let changes = diff(&a, &b);
7590 assert!(!changes.iter().any(|change| matches!(
7591 change,
7592 ScoreChange::UnrepresentedFieldChanged { path }
7593 if path == "parts[0].staves[0].measures[0].key_sig"
7594 )));
7595 let patches = score_patch(&a, &b);
7596 assert!(patches.iter().any(|patch| matches!(
7597 patch,
7598 ScorePatch::SetKeySignature {
7599 part: 0,
7600 staff: 0,
7601 measure: 0,
7602 value: Some(KeySignature { fifths: 2, .. }),
7603 }
7604 )));
7605 assert!(
7606 !patches
7607 .iter()
7608 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
7609 );
7610 }
7611
7612 #[test]
7613 fn diff_same_key_signature_no_change() {
7614 let a = Score::new("T", 120, 4, 4, 2, 1);
7615 let changes = diff(&a, &a);
7616 assert!(changes.is_empty());
7617 }
7618
7619 #[test]
7620 fn score_duration_secs_region_partial() {
7621 use super::score_duration_secs_region;
7622 let score = Score::new("T", 120, 4, 4, 0, 4);
7624 let secs = score_duration_secs_region(&score, (1, 2));
7625 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
7626 }
7627
7628 #[test]
7629 fn score_duration_secs_region_single_measure() {
7630 use super::score_duration_secs_region;
7631 let score = Score::new("T", 120, 4, 4, 0, 4);
7633 let secs = score_duration_secs_region(&score, (0, 0));
7634 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
7635 }
7636
7637 #[test]
7640 fn measure_beats_remaining_empty_voice_returns_full() {
7641 use super::measure_beats_remaining;
7642 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7643 score.parts[0].staves[0].measures[0].voices[0].clear();
7644 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7645 assert!(
7646 (rem - 4.0).abs() < 1e-9,
7647 "expected 4.0 remaining, got {rem}"
7648 );
7649 }
7650
7651 #[test]
7652 fn measure_beats_remaining_half_full_returns_half() {
7653 use super::measure_beats_remaining;
7654 use crate::model::pitch::Step;
7655 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7656 score.parts[0].staves[0].measures[0].voices[0] = vec![
7657 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
7658 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
7659 ];
7660 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7661 assert!(
7662 (rem - 2.0).abs() < 1e-9,
7663 "expected 2.0 remaining, got {rem}"
7664 );
7665 }
7666
7667 #[test]
7668 fn measure_beats_remaining_full_voice_returns_zero() {
7669 use super::measure_beats_remaining;
7670 use crate::model::pitch::Step;
7671 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7672 score.parts[0].staves[0].measures[0].voices[0] =
7673 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7674 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7675 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
7676 }
7677
7678 #[test]
7679 fn measure_beats_remaining_tuplet_accounting() {
7680 use super::measure_beats_remaining;
7681 use crate::model::notation::TupletInfo;
7682 use crate::model::pitch::Step;
7683 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7685 let tuplet = TupletInfo {
7686 actual_notes: 3,
7687 normal_notes: 2,
7688 };
7689 let mk = |step| {
7690 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
7691 n.tuplet = Some(tuplet.clone());
7692 n
7693 };
7694 score.parts[0].staves[0].measures[0].voices[0] =
7695 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
7696 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7697 assert!(
7698 (rem - 2.0).abs() < 1e-9,
7699 "expected 2.0 remaining (triplets used 2.0), got {rem}"
7700 );
7701 }
7702
7703 #[test]
7704 fn measure_beats_remaining_out_of_range_returns_err() {
7705 use super::measure_beats_remaining;
7706 let score = Score::new("T", 120, 4, 4, 0, 1);
7707 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
7708 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
7709 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
7710 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
7711 }
7712
7713 #[test]
7714 fn note_content_eq_ignores_id() {
7715 use crate::model::pitch::Step;
7716 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
7717 let mut b = a.clone();
7718 b.id = "different-id".to_string();
7719 assert!(note_content_eq(&a, &b));
7720 b.pitches[0] = Pitch::new(Step::D, 4);
7722 assert!(!note_content_eq(&a, &b));
7723 let mut c = a.clone();
7725 a.stem_up = Some(true);
7726 c.stem_up = Some(false);
7727 assert!(!note_content_eq(&a, &c));
7728 }
7729
7730 #[test]
7731 fn suggested_stem_up_below_middle() {
7732 use crate::model::notation::Clef;
7733 let pitches = vec![Pitch::new(Step::C, 4)];
7735 assert!(suggested_stem_up(&pitches, &Clef::Treble));
7736 }
7737
7738 #[test]
7739 fn suggested_stem_up_above_middle() {
7740 use crate::model::notation::Clef;
7741 let pitches = vec![Pitch::new(Step::G, 5)];
7743 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
7744 }
7745
7746 #[test]
7747 fn suggested_stem_up_at_middle_line() {
7748 use crate::model::notation::Clef;
7749 let pitches = vec![Pitch::new(Step::B, 4)];
7751 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
7752 }
7753
7754 #[test]
7755 fn suggested_stem_up_chord() {
7756 use crate::model::notation::Clef;
7757 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
7759 assert!(suggested_stem_up(&pitches, &Clef::Treble));
7760 }
7761
7762 #[test]
7763 fn suggested_stem_up_bass_clef() {
7764 use crate::model::notation::Clef;
7765 let pitches = vec![Pitch::new(Step::D, 3)];
7767 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
7768 let pitches2 = vec![Pitch::new(Step::C, 3)];
7770 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
7771 }
7772
7773 #[test]
7774 fn suggested_stem_up_empty_pitches() {
7775 use crate::model::notation::Clef;
7776 assert!(suggested_stem_up(&[], &Clef::Treble));
7777 }
7778
7779 fn eighth(pitch: Pitch) -> Note {
7780 Note::new(pitch, Duration::Eighth)
7781 }
7782 fn quarter(pitch: Pitch) -> Note {
7783 Note::new(pitch, Duration::Quarter)
7784 }
7785 fn rest_eighth() -> Note {
7786 Note::rest(Duration::Eighth)
7787 }
7788
7789 #[test]
7790 fn compute_beams_4_4_four_eighths() {
7791 use crate::model::notation::{Clef, TimeSignature};
7792 let _ = Clef::Treble; let ts = TimeSignature {
7794 numerator: 4,
7795 denominator: 4,
7796 };
7797 let c4 = Pitch::new(Step::C, 4);
7798 let notes = vec![
7799 eighth(c4.clone()),
7800 eighth(c4.clone()),
7801 eighth(c4.clone()),
7802 eighth(c4.clone()),
7803 ];
7804 let beams = compute_beams(¬es, &ts);
7805 assert_eq!(beams[0], BeamState::Begin);
7807 assert_eq!(beams[1], BeamState::End);
7808 assert_eq!(beams[2], BeamState::Begin);
7809 assert_eq!(beams[3], BeamState::End);
7810 }
7811
7812 #[test]
7813 fn compute_beams_4_4_all_eighth_one_group() {
7814 use crate::model::notation::TimeSignature;
7815 let ts = TimeSignature {
7816 numerator: 4,
7817 denominator: 4,
7818 };
7819 let c4 = Pitch::new(Step::C, 4);
7820 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
7822 let beams = compute_beams(¬es, &ts);
7823 assert_eq!(beams[0], BeamState::Begin);
7824 assert_eq!(beams[1], BeamState::End);
7825 }
7826
7827 #[test]
7828 fn compute_beams_quarter_not_beamed() {
7829 use crate::model::notation::TimeSignature;
7830 let ts = TimeSignature {
7831 numerator: 4,
7832 denominator: 4,
7833 };
7834 let c4 = Pitch::new(Step::C, 4);
7835 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
7836 let beams = compute_beams(¬es, &ts);
7837 assert_eq!(beams[0], BeamState::None);
7838 assert_eq!(beams[1], BeamState::None);
7839 }
7840
7841 #[test]
7842 fn compute_beams_rest_breaks_beam() {
7843 use crate::model::notation::TimeSignature;
7844 let ts = TimeSignature {
7845 numerator: 4,
7846 denominator: 4,
7847 };
7848 let c4 = Pitch::new(Step::C, 4);
7849 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
7850 let beams = compute_beams(¬es, &ts);
7851 assert_eq!(beams[0], BeamState::None);
7853 assert_eq!(beams[1], BeamState::None);
7854 assert_eq!(beams[2], BeamState::None);
7855 }
7856
7857 #[test]
7858 fn compute_beams_6_8_compound() {
7859 use crate::model::notation::TimeSignature;
7860 let ts = TimeSignature {
7861 numerator: 6,
7862 denominator: 8,
7863 };
7864 let c4 = Pitch::new(Step::C, 4);
7865 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
7867 let beams = compute_beams(¬es, &ts);
7868 assert_eq!(beams[0], BeamState::Begin);
7869 assert_eq!(beams[1], BeamState::Continue);
7870 assert_eq!(beams[2], BeamState::End);
7871 assert_eq!(beams[3], BeamState::Begin);
7872 assert_eq!(beams[4], BeamState::Continue);
7873 assert_eq!(beams[5], BeamState::End);
7874 }
7875
7876 #[test]
7877 fn compute_beams_single_eighth() {
7878 use crate::model::notation::TimeSignature;
7879 let ts = TimeSignature {
7880 numerator: 4,
7881 denominator: 4,
7882 };
7883 let c4 = Pitch::new(Step::C, 4);
7884 let notes = vec![eighth(c4.clone())];
7885 let beams = compute_beams(¬es, &ts);
7886 assert_eq!(beams[0], BeamState::None);
7887 }
7888
7889 #[test]
7890 fn compute_beams_stops_fifteen_in_eight_tuplet_at_the_beat_boundary() {
7891 let ts = TimeSignature {
7892 numerator: 2,
7893 denominator: 4,
7894 };
7895 let pitch = Pitch::new(Step::C, 4);
7896 let mut tuplet_note = Note::new(pitch.clone(), Duration::ThirtySecond);
7897 tuplet_note.tuplet = Some(TupletInfo {
7898 actual_notes: 15,
7899 normal_notes: 8,
7900 });
7901 let mut notes = vec![tuplet_note; 15];
7902 notes.extend([eighth(pitch.clone()), eighth(pitch)]);
7903 let original = serde_json::to_value(¬es).unwrap();
7904
7905 let beams = compute_beams(¬es, &ts);
7906 let mut expected = vec![BeamState::Begin];
7907 expected.extend([BeamState::Continue; 13]);
7908 expected.extend([BeamState::End, BeamState::Begin, BeamState::End]);
7909 assert_eq!(beams, expected);
7910 assert_eq!(
7911 serde_json::to_value(¬es).unwrap(),
7912 original,
7913 "beam inference must not change notation or timing"
7914 );
7915 assert!((notes[..15].iter().map(Note::beats).sum::<f64>() - 1.0).abs() < 1e-12);
7916 }
7917}
7918
7919#[cfg(test)]
7920mod mid_clef_tests {
7921 use super::*;
7922 use crate::{Command, JoinMeasuresCmd, ScoreEngine, SetMidMeasureClefsCmd, SplitMeasureCmd};
7923
7924 fn quarter_notes() -> Vec<Note> {
7925 [Step::C, Step::D, Step::E, Step::F]
7926 .into_iter()
7927 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter))
7928 .collect()
7929 }
7930
7931 fn change(beats: f64, clef: Clef) -> MidMeasureClef {
7932 MidMeasureClef {
7933 offset: MeasureLength::from_beats(beats).unwrap(),
7934 clef,
7935 }
7936 }
7937
7938 #[test]
7939 fn measure_length_from_beats_is_exact_for_tuplet_subdivisions() {
7940 let third = MeasureLength::from_beats(1.0 / 3.0).unwrap();
7941 assert_eq!((third.numerator, third.denominator), (1, 12));
7942 let two = MeasureLength::from_beats(2.0).unwrap();
7943 assert_eq!((two.numerator, two.denominator), (1, 2));
7944 assert!(MeasureLength::from_beats(0.0).is_none());
7945 }
7946
7947 #[test]
7948 fn clef_at_follows_mid_bar_changes_and_json_omits_an_empty_list() {
7949 let mut measure = Measure::empty(4, 4);
7950 let json = serde_json::to_string(&measure).unwrap();
7951 assert!(!json.contains("mid_clefs"));
7952 measure.mid_clefs = vec![change(2.0, Clef::Bass)];
7953 assert_eq!(measure.clef_at(&Clef::Treble, 1.0), Clef::Treble);
7954 assert_eq!(measure.clef_at(&Clef::Treble, 2.0), Clef::Bass);
7955 let restored: Measure =
7956 serde_json::from_str(&serde_json::to_string(&measure).unwrap()).unwrap();
7957 assert_eq!(restored.mid_clefs, measure.mid_clefs);
7958 }
7959
7960 #[test]
7961 fn validation_rejects_mid_bar_clefs_outside_the_bar_or_out_of_order() {
7962 let mut score = Score::new("clefs", 120, 4, 4, 0, 1);
7963 score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
7964 score.parts[0].staves[0].measures[0].mid_clefs =
7965 vec![change(2.0, Clef::Bass), change(1.0, Clef::Treble)];
7966 let errors = crate::validate(&score).errors;
7967 assert!(errors.iter().any(|error| matches!(
7968 error,
7969 crate::ValidationError::InvalidMidMeasureClef { index: 1, .. }
7970 )));
7971 score.parts[0].staves[0].measures[0].mid_clefs = vec![change(4.0, Clef::Bass)];
7972 assert!(!crate::validate(&score).errors.is_empty());
7973 }
7974
7975 #[test]
7976 fn command_split_join_diff_and_patch_keep_mid_bar_clefs() {
7977 let mut engine = ScoreEngine::new();
7978 engine.score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
7979 engine
7980 .apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
7981 part_index: 0,
7982 staff_index: 0,
7983 measure_index: 0,
7984 clefs: vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)],
7985 }))
7986 .unwrap();
7987 let with_clefs = engine.score.clone();
7988 let plain = {
7989 let mut score = with_clefs.clone();
7990 score.parts[0].staves[0].measures[0].mid_clefs.clear();
7991 score
7992 };
7993 assert!(diff(&plain, &with_clefs).iter().any(|change| matches!(
7994 change,
7995 ScoreChange::MidMeasureClefsChanged { measure: 0, .. }
7996 )));
7997 let patched = apply_patch(&plain, &score_patch(&plain, &with_clefs)).unwrap();
7998 assert_eq!(
7999 patched.parts[0].staves[0].measures[0].mid_clefs,
8000 with_clefs.parts[0].staves[0].measures[0].mid_clefs
8001 );
8002
8003 engine
8004 .apply(Command::SplitMeasure(SplitMeasureCmd {
8005 measure_index: 0,
8006 split_at_beats: 1.0,
8007 }))
8008 .unwrap();
8009 let measures = &engine.score.parts[0].staves[0].measures;
8010 assert!(measures[0].mid_clefs.is_empty());
8011 assert_eq!(measures[1].clef, Some(Clef::Bass));
8012 assert_eq!(measures[1].mid_clefs, vec![change(2.0, Clef::Alto)]);
8013
8014 engine
8015 .apply(Command::JoinMeasures(JoinMeasuresCmd { measure_index: 0 }))
8016 .unwrap();
8017 let measure = &engine.score.parts[0].staves[0].measures[0];
8018 assert_eq!(
8019 measure.mid_clefs,
8020 vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)]
8021 );
8022
8023 let invalid = engine.apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
8024 part_index: 0,
8025 staff_index: 0,
8026 measure_index: 0,
8027 clefs: vec![change(0.0 + 5.0, Clef::Bass)],
8028 }));
8029 assert!(invalid.is_err());
8030 }
8031}
8032
8033#[cfg(test)]
8034mod percussion_key_tests {
8035 use super::*;
8036
8037 #[test]
8038 fn unpitched_chord_members_sound_their_kit_keys_by_display_position() {
8039 let mut part = Part::new("Drums", "Dr.");
8040 part.midi_channel = 9;
8041 let kit = |id: &str, key: u8, display: Pitch| PercussionInstrument {
8042 id: id.into(),
8043 name: None,
8044 midi_unpitched: Some(key),
8045 staff_position: Some(PercussionInstrument::display_position(&display)),
8046 notehead: None,
8047 preferred_voice: None,
8048 techniques: Vec::new(),
8049 };
8050 part.percussion_instruments = vec![
8051 kit("kick", 35, Pitch::new(Step::F, 4)),
8052 kit("snare", 38, Pitch::new(Step::C, 5)),
8053 ];
8054 let mut note = Note::new(Pitch::new(Step::C, 5), Duration::Quarter);
8055 note.pitches.push(Pitch::new(Step::F, 4));
8056 note.is_unpitched = true;
8057 note.instrument_id = Some("snare".into());
8058 assert_eq!(part.percussion_key(¬e, 0), Some(38));
8059 assert_eq!(part.percussion_key(¬e, 1), Some(35));
8060 note.is_unpitched = false;
8061 assert_eq!(part.percussion_key(¬e, 0), None);
8062 assert_eq!(
8063 PercussionInstrument::display_position(&Pitch::new(Step::B, 4)),
8064 0
8065 );
8066 }
8067}