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 for measure in &mut staff.measures {
1376 if let Some(ref mut ks) = measure.key_sig {
1377 ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
1378 }
1379 for voice in &mut measure.voices {
1380 for note in voice.iter_mut() {
1381 for pitch in note.pitches.iter_mut() {
1382 *pitch = transpose_pitch(pitch, semitones);
1383 }
1384 }
1385 }
1386 }
1387 }
1388 }
1389 out
1390}
1391
1392pub fn transpose_checked(score: &Score, semitones: i8) -> Result<Score, Error> {
1394 if !super::validate::validate(score).is_valid() {
1395 return Err(Error::InvalidScore);
1396 }
1397 let transposed = transpose(score, semitones);
1398 if !super::validate::validate(&transposed).is_valid() {
1399 return Err(Error::InvalidScore);
1400 }
1401 Ok(transposed)
1402}
1403
1404#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1406#[serde(rename_all = "snake_case")]
1407pub enum RegionalTranspositionTarget {
1408 Written,
1410 Concert,
1412}
1413
1414pub fn transpose_staff_region_checked(
1417 score: &Score,
1418 part_index: usize,
1419 staff_index: usize,
1420 start_measure: usize,
1421 end_measure: usize,
1422 semitones: i8,
1423 target: RegionalTranspositionTarget,
1424) -> Result<Score, Error> {
1425 if !super::validate::validate(score).is_valid() {
1426 return Err(Error::InvalidScore);
1427 }
1428 let source_staff = score
1429 .parts
1430 .get(part_index)
1431 .ok_or(Error::PartNotFound(part_index))?
1432 .staves
1433 .get(staff_index)
1434 .ok_or(Error::StaffNotFound(staff_index))?;
1435 if start_measure >= end_measure || end_measure > source_staff.measures.len() {
1436 return Err(Error::InvalidCommand(format!(
1437 "invalid measure range {start_measure}..{end_measure}"
1438 )));
1439 }
1440 if target == RegionalTranspositionTarget::Concert
1441 && (start_measure != 0 || end_measure != source_staff.measures.len())
1442 {
1443 return Err(Error::InvalidCommand(
1444 "concert-pitch staff transposition requires the full staff range".into(),
1445 ));
1446 }
1447 let mut transformed = score.clone();
1448 let staff = &mut transformed.parts[part_index].staves[staff_index];
1449 match target {
1450 RegionalTranspositionTarget::Written => {
1451 for measure in &mut staff.measures[start_measure..end_measure] {
1452 if let Some(key) = &mut measure.key_sig {
1453 key.fifths = transpose_fifths(key.fifths, &key.mode, semitones);
1454 }
1455 for voice in &mut measure.voices {
1456 for note in voice {
1457 for pitch in &mut note.pitches {
1458 *pitch = transpose_pitch(pitch, semitones);
1459 }
1460 }
1461 }
1462 }
1463 }
1464 RegionalTranspositionTarget::Concert => {
1465 staff.transpose_semitones = staff.transpose_semitones.saturating_add(semitones);
1466 }
1467 }
1468 if !super::validate::validate(&transformed).is_valid() {
1469 return Err(Error::InvalidScore);
1470 }
1471 Ok(transformed)
1472}
1473
1474fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
1475 let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
1476 let pc = new_midi % 12;
1477 let oct = (new_midi / 12) as i8 - 1;
1478 let (step, alter): (Step, i8) = if semitones >= 0 {
1479 match pc {
1480 0 => (Step::C, 0),
1481 1 => (Step::C, 1),
1482 2 => (Step::D, 0),
1483 3 => (Step::D, 1),
1484 4 => (Step::E, 0),
1485 5 => (Step::F, 0),
1486 6 => (Step::F, 1),
1487 7 => (Step::G, 0),
1488 8 => (Step::G, 1),
1489 9 => (Step::A, 0),
1490 10 => (Step::A, 1),
1491 11 => (Step::B, 0),
1492 _ => (Step::C, 0),
1493 }
1494 } else {
1495 match pc {
1496 0 => (Step::C, 0),
1497 1 => (Step::D, -1),
1498 2 => (Step::D, 0),
1499 3 => (Step::E, -1),
1500 4 => (Step::E, 0),
1501 5 => (Step::F, 0),
1502 6 => (Step::G, -1),
1503 7 => (Step::G, 0),
1504 8 => (Step::A, -1),
1505 9 => (Step::A, 0),
1506 10 => (Step::B, -1),
1507 11 => (Step::B, 0),
1508 _ => (Step::C, 0),
1509 }
1510 };
1511 Pitch::with_microtone(step, oct, alter, pitch.microtone_cents)
1512}
1513
1514fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
1520 let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
1521 let tonic_pc = if mode == "minor" {
1522 ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
1523 } else {
1524 tonic_major_pc
1525 };
1526 let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
1527 let major_tonic = if mode == "minor" {
1528 ((new_tonic as i32 + 3).rem_euclid(12)) as u8
1529 } else {
1530 new_tonic
1531 };
1532 let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
1533 if raw > 6 { raw - 12 } else { raw }
1534}
1535
1536#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1538pub struct InstrumentRange {
1539 pub lowest: u8,
1540 pub highest: u8,
1541}
1542
1543#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1550pub struct InstrumentDefinition {
1551 pub id: String,
1553 #[serde(default)]
1554 pub name: String,
1555 #[serde(default)]
1556 pub short_name: String,
1557 #[serde(default)]
1558 pub family: Option<String>,
1559 #[serde(default)]
1560 pub transpose_semitones: i8,
1561 #[serde(default)]
1562 pub written_range: Option<InstrumentRange>,
1563 #[serde(default)]
1564 pub sounding_range: Option<InstrumentRange>,
1565 #[serde(default)]
1566 pub default_clefs: Vec<Clef>,
1567 #[serde(default = "default_instrument_staff_count")]
1568 pub staff_count: u8,
1569 #[serde(default)]
1570 pub staff_kind: StaffKind,
1571 #[serde(default)]
1572 pub midi_channel: u8,
1573 #[serde(default)]
1574 pub midi_program: u8,
1575 #[serde(default)]
1576 pub percussion_map_id: Option<String>,
1577}
1578
1579const fn default_instrument_staff_count() -> u8 {
1580 1
1581}
1582
1583impl InstrumentDefinition {
1584 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
1586 Self {
1587 id: id.into(),
1588 name: name.into(),
1589 short_name: String::new(),
1590 family: None,
1591 transpose_semitones: 0,
1592 written_range: None,
1593 sounding_range: None,
1594 default_clefs: Vec::new(),
1595 staff_count: 1,
1596 staff_kind: StaffKind::Standard,
1597 midi_channel: 0,
1598 midi_program: 0,
1599 percussion_map_id: None,
1600 }
1601 }
1602}
1603
1604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1606pub struct NamedInstrumentChannel {
1607 pub name: String,
1608 pub program: u8,
1609 pub midi_channel: u8,
1610}
1611
1612#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1614pub struct VoiceChannelSwitch {
1615 pub voice: usize,
1616 pub position_beats: f64,
1617 pub channel_name: String,
1618}
1619
1620#[derive(Debug, Clone, Serialize, Deserialize)]
1621pub struct Part {
1622 pub id: String,
1623 pub name: String,
1624 pub short_name: String,
1625 pub staves: Vec<Staff>,
1626 #[serde(default)]
1628 pub midi_channel: u8,
1629 #[serde(default)]
1631 pub midi_program: u8,
1632 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1634 pub named_channels: Vec<NamedInstrumentChannel>,
1635 #[serde(default)]
1637 pub midi_pitch_bends: Vec<MidiPitchBend>,
1638 #[serde(default)]
1640 pub midi_control_changes: Vec<MidiControlChange>,
1641 #[serde(default)]
1643 pub midi_program_changes: Vec<MidiProgramChange>,
1644 #[serde(default)]
1646 pub midi_aftertouch: Vec<MidiAftertouch>,
1647 #[serde(default)]
1649 pub percussion_instruments: Vec<PercussionInstrument>,
1650 #[serde(default)]
1652 pub staff_groups: Vec<StaffGroup>,
1653 #[serde(default)]
1655 pub instrument: Option<InstrumentDefinition>,
1656}
1657
1658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1659pub struct MidiPitchBend {
1660 pub tick: u64,
1661 pub channel: u8,
1662 pub value: i16,
1664}
1665
1666#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1667pub struct MidiControlChange {
1668 pub tick: u64,
1669 pub channel: u8,
1670 pub controller: u8,
1672 pub value: u8,
1674}
1675
1676#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1677pub struct MidiProgramChange {
1678 pub tick: u64,
1679 pub channel: u8,
1680 pub program: u8,
1682}
1683
1684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1685pub struct MidiAftertouch {
1686 pub tick: u64,
1687 pub channel: u8,
1688 pub key: Option<u8>,
1690 pub value: u8,
1692}
1693
1694impl PercussionInstrument {
1700 pub fn display_position(pitch: &Pitch) -> i8 {
1703 let step = "CDEFGAB"
1704 .find(pitch.step.to_char())
1705 .map_or(0, |index| index as i32);
1706 let steps = i32::from(pitch.octave) * 7 + step;
1707 (steps - (4 * 7 + 6)).clamp(i32::from(i8::MIN), i32::from(i8::MAX)) as i8
1708 }
1709}
1710
1711#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1712pub struct PercussionInstrument {
1713 pub id: String,
1715 #[serde(default)]
1716 pub name: Option<String>,
1717 #[serde(default)]
1721 pub midi_unpitched: Option<u8>,
1722 #[serde(default)]
1724 pub staff_position: Option<i8>,
1725 #[serde(default)]
1727 pub notehead: Option<NoteHead>,
1728 #[serde(default)]
1730 pub preferred_voice: Option<u8>,
1731 #[serde(default)]
1733 pub techniques: Vec<String>,
1734}
1735
1736impl Part {
1737 pub fn new(name: &str, short_name: &str) -> Self {
1738 Self {
1739 id: Uuid::new_v4().to_string(),
1740 name: name.to_string(),
1741 short_name: short_name.to_string(),
1742 staves: Vec::new(),
1743 midi_channel: 0,
1744 midi_program: 0,
1745 named_channels: Vec::new(),
1746 midi_pitch_bends: Vec::new(),
1747 midi_control_changes: Vec::new(),
1748 midi_program_changes: Vec::new(),
1749 midi_aftertouch: Vec::new(),
1750 percussion_instruments: Vec::new(),
1751 staff_groups: Vec::new(),
1752 instrument: None,
1753 }
1754 }
1755
1756 pub fn percussion_key(&self, note: &Note, pitch_index: usize) -> Option<u8> {
1762 if !note.is_unpitched {
1763 return None;
1764 }
1765 let pitch = note.pitches.get(pitch_index)?;
1766 let by_id = note
1767 .instrument_id
1768 .as_deref()
1769 .filter(|_| pitch_index == 0)
1770 .and_then(|id| {
1771 self.percussion_instruments
1772 .iter()
1773 .find(|instrument| instrument.id == id)
1774 })
1775 .and_then(|instrument| instrument.midi_unpitched);
1776 by_id.or_else(|| {
1777 let position = PercussionInstrument::display_position(pitch);
1778 self.percussion_instruments
1779 .iter()
1780 .filter(|instrument| instrument.staff_position == Some(position))
1781 .find_map(|instrument| instrument.midi_unpitched)
1782 })
1783 }
1784
1785 pub fn percussion_instrument_for_note(&self, note: &Note) -> Option<&PercussionInstrument> {
1793 if !note.is_unpitched {
1794 return None;
1795 }
1796 if let Some(instrument_id) = note.instrument_id.as_deref() {
1797 return self
1798 .percussion_instruments
1799 .iter()
1800 .find(|instrument| instrument.id == instrument_id);
1801 }
1802 let midi_key = u8::try_from(note.pitches.first()?.to_midi()).ok()?;
1803 self.percussion_instruments
1804 .iter()
1805 .find(|instrument| instrument.midi_unpitched == Some(midi_key))
1806 }
1807}
1808
1809#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1811#[serde(rename_all = "snake_case")]
1812pub enum StaffKind {
1813 #[default]
1814 Standard,
1815 Tablature,
1816 Percussion,
1817}
1818
1819#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1821#[serde(rename_all = "snake_case")]
1822pub enum StaffNoteheadScheme {
1823 #[default]
1824 Standard,
1825 PitchNames,
1826 ShapeNotes,
1827}
1828
1829#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1831#[serde(rename_all = "snake_case")]
1832pub enum TablatureRhythmDisplay {
1833 #[default]
1835 FretOnly,
1836 Stems,
1838}
1839
1840#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1846pub struct StaffPresentation {
1847 #[serde(default)]
1848 pub kind: StaffKind,
1849 #[serde(default = "default_staff_line_count")]
1850 pub lines: u8,
1851 #[serde(default = "default_staff_line_distance")]
1852 pub line_distance: f32,
1853 #[serde(default)]
1854 pub small: bool,
1855 #[serde(default)]
1856 pub cutaway: bool,
1857 #[serde(default = "default_staff_visible")]
1858 pub visible: bool,
1859 #[serde(default)]
1860 pub notehead_scheme: StaffNoteheadScheme,
1861 #[serde(default)]
1863 pub tablature_rhythm_display: TablatureRhythmDisplay,
1864 #[serde(default)]
1866 pub tablature_fret_mark_style: TablatureFretMarkStyle,
1867}
1868
1869const fn default_staff_line_count() -> u8 {
1870 5
1871}
1872
1873const fn default_staff_line_distance() -> f32 {
1874 1.0
1875}
1876
1877const fn default_staff_visible() -> bool {
1878 true
1879}
1880
1881impl Default for StaffPresentation {
1882 fn default() -> Self {
1883 Self {
1884 kind: StaffKind::Standard,
1885 lines: default_staff_line_count(),
1886 line_distance: default_staff_line_distance(),
1887 small: false,
1888 cutaway: false,
1889 visible: true,
1890 notehead_scheme: StaffNoteheadScheme::Standard,
1891 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
1892 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
1893 }
1894 }
1895}
1896
1897#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1899#[serde(rename_all = "snake_case")]
1900pub enum TablatureFretMarkStyle {
1901 #[default]
1902 Arabic,
1903 RomanUpper,
1904 RomanLower,
1905}
1906
1907impl StaffPresentation {
1908 fn for_clef(clef: &Clef) -> Self {
1909 let mut presentation = Self::default();
1910 if matches!(clef, Clef::Percussion) {
1911 presentation.kind = StaffKind::Percussion;
1912 }
1913 presentation
1914 }
1915}
1916
1917#[derive(Debug, Clone, Serialize, Deserialize)]
1918pub struct Staff {
1919 pub clef: Clef,
1920 pub measures: Vec<Measure>,
1921 #[serde(default)]
1924 pub transpose_semitones: i8,
1925 #[serde(default)]
1926 pub tablature: Option<TablatureConfig>,
1927 #[serde(default)]
1928 pub presentation: StaffPresentation,
1929}
1930
1931impl Staff {
1932 pub fn meter_at<'a>(
1936 &'a self,
1937 measure_index: usize,
1938 default: &'a TimeSignature,
1939 ) -> &'a TimeSignature {
1940 self.measures
1941 .iter()
1942 .take(measure_index.saturating_add(1))
1943 .rev()
1944 .find_map(|measure| measure.time_sig.as_ref())
1945 .unwrap_or(default)
1946 }
1947
1948 pub fn measure_beats(&self, measure_index: usize, default: &TimeSignature) -> f64 {
1950 self.measures.get(measure_index).map_or_else(
1951 || self.meter_at(measure_index, default).total_beats(),
1952 |measure| measure.duration_beats(self.meter_at(measure_index, default)),
1953 )
1954 }
1955
1956 pub fn new(clef: Clef) -> Self {
1957 Self {
1958 presentation: StaffPresentation::for_clef(&clef),
1959 clef,
1960 measures: Vec::new(),
1961 transpose_semitones: 0,
1962 tablature: None,
1963 }
1964 }
1965
1966 pub fn tablature_at(&self, measure_index: usize) -> Option<TablatureConfig> {
1972 let mut active = self.tablature.clone();
1973 for measure in self.measures.iter().take(measure_index.saturating_add(1)) {
1974 if let Some(change) = &measure.tablature_change {
1975 active = Some(change.clone());
1976 }
1977 }
1978 active
1979 }
1980}
1981
1982#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1983pub struct VoltaBracket {
1984 pub number: u8,
1986 pub kind: String,
1988 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1991 pub numbers: Vec<u8>,
1992}
1993
1994impl VoltaBracket {
1995 pub fn passes(&self) -> Vec<u8> {
1997 if self.numbers.is_empty() {
1998 vec![self.number]
1999 } else {
2000 self.numbers.clone()
2001 }
2002 }
2003
2004 pub fn plays_on(&self, pass: u8) -> bool {
2006 if self.numbers.is_empty() {
2007 self.number == pass
2008 } else {
2009 self.numbers.contains(&pass)
2010 }
2011 }
2012
2013 pub fn label(&self) -> String {
2015 let passes = self.passes();
2016 match passes.as_slice() {
2017 [first, .., last]
2018 if passes.len() >= 3 && passes.windows(2).all(|pair| pair[1] == pair[0] + 1) =>
2019 {
2020 format!("{first}-{last}")
2021 }
2022 _ => passes
2023 .iter()
2024 .map(u8::to_string)
2025 .collect::<Vec<_>>()
2026 .join(", "),
2027 }
2028 }
2029
2030 pub fn parse_passes(label: &str) -> Vec<u8> {
2032 let mut passes = Vec::new();
2033 for part in label
2034 .split([',', ' ', '.', '+', '/'])
2035 .filter(|part| !part.is_empty())
2036 {
2037 if let Some((from, to)) = part.split_once(['-', '–'])
2038 && let (Ok(from), Ok(to)) = (from.trim().parse::<u8>(), to.trim().parse::<u8>())
2039 && from <= to
2040 && to - from < 32
2041 {
2042 passes.extend(from..=to);
2043 } else if let Ok(pass) = part.trim().parse::<u8>() {
2044 passes.push(pass);
2045 }
2046 }
2047 passes.sort_unstable();
2048 passes.dedup();
2049 passes
2050 }
2051}
2052
2053#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2055#[serde(rename_all = "kebab-case")]
2056pub enum HarpPedalPosition {
2057 Flat,
2058 #[default]
2059 Natural,
2060 Sharp,
2061}
2062
2063#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2068pub struct HarpPedalDiagram {
2069 #[serde(default)]
2070 pub positions: [HarpPedalPosition; 7],
2071 #[serde(default, skip_serializing_if = "Option::is_none")]
2072 pub placement: Option<String>,
2073}
2074
2075impl Default for HarpPedalDiagram {
2076 fn default() -> Self {
2077 Self {
2078 positions: [HarpPedalPosition::Natural; 7],
2079 placement: None,
2080 }
2081 }
2082}
2083
2084#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2088pub struct MeasureLength {
2089 pub numerator: u32,
2090 pub denominator: u32,
2091}
2092
2093impl MeasureLength {
2094 pub const MAX_BEATS: f64 = 256.0;
2096
2097 pub fn beats(&self) -> Option<f64> {
2099 if self.numerator == 0 || self.denominator == 0 {
2100 return None;
2101 }
2102 let beats = 4.0 * f64::from(self.numerator) / f64::from(self.denominator);
2103 (beats.is_finite() && beats <= Self::MAX_BEATS).then_some(beats)
2104 }
2105
2106 pub fn from_beats(beats: f64) -> Option<Self> {
2109 const DIVISIONS: u32 = 3 * 5 * 7 * 64;
2110 if !beats.is_finite() || beats <= 0.0 || beats > Self::MAX_BEATS {
2111 return None;
2112 }
2113 let ticks = (beats * f64::from(DIVISIONS)).round();
2114 (ticks >= 1.0).then(|| Self::from_ticks(ticks as u32, DIVISIONS))?
2115 }
2116
2117 pub fn from_ticks(ticks: u32, divisions: u32) -> Option<Self> {
2119 let numerator = u64::from(ticks);
2120 let denominator = u64::from(divisions).checked_mul(4)?;
2121 if numerator == 0 || denominator == 0 {
2122 return None;
2123 }
2124 let gcd = {
2125 let (mut a, mut b) = (numerator, denominator);
2126 while b != 0 {
2127 (a, b) = (b, a % b);
2128 }
2129 a
2130 };
2131 let length = Self {
2132 numerator: u32::try_from(numerator / gcd).ok()?,
2133 denominator: u32::try_from(denominator / gcd).ok()?,
2134 };
2135 length.beats().map(|_| length)
2136 }
2137}
2138
2139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2143pub struct MidMeasureClef {
2144 pub offset: MeasureLength,
2145 pub clef: Clef,
2146}
2147
2148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2150pub struct MetronomeMark {
2151 pub beat_unit: Duration,
2152 #[serde(default)]
2153 pub beat_unit_dots: u8,
2154 pub per_minute: u16,
2155 #[serde(default)]
2156 pub parentheses: bool,
2157}
2158
2159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2161pub struct TimedPedalDirection {
2162 pub position_beats: f64,
2164 pub down: bool,
2166 pub number: u16,
2168 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2170 pub hide_sign: bool,
2171 #[serde(default, skip_serializing_if = "Option::is_none")]
2173 pub placement: Option<String>,
2174}
2175
2176#[derive(Debug, Clone, Serialize, Deserialize)]
2177pub struct Measure {
2178 pub number: u32,
2179 pub time_sig: Option<TimeSignature>,
2180 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2182 pub time_sig_hidden: bool,
2183 #[serde(default, skip_serializing_if = "Option::is_none")]
2185 pub time_symbol: Option<TimeSymbol>,
2186 pub key_sig: Option<KeySignature>,
2187 pub clef: Option<Clef>,
2188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2191 pub mid_clefs: Vec<MidMeasureClef>,
2192 pub tempo: Option<u16>,
2193 #[serde(default, skip_serializing_if = "Option::is_none")]
2195 pub metronome_mark: Option<MetronomeMark>,
2196 #[serde(default)]
2199 pub tempo_ramp_to: Option<u16>,
2200 #[serde(default)]
2202 pub instrument_change: Option<InstrumentDefinition>,
2203 #[serde(default)]
2205 pub tablature_change: Option<TablatureConfig>,
2206 pub barline_left: Barline,
2207 pub barline_right: Barline,
2208 #[serde(default)]
2209 pub volta: Option<VoltaBracket>,
2210 #[serde(default)]
2211 pub tempo_text: Option<String>,
2212 #[serde(default)]
2213 pub rehearsal: Option<String>,
2214 #[serde(default)]
2217 pub navigation: Option<String>,
2218 #[serde(default)]
2220 pub expression_text: Option<String>,
2221 #[serde(default)]
2222 pub texts: Vec<StyledText>,
2223 #[serde(default)]
2225 pub figured_bass: Vec<FiguredBassFigure>,
2226 #[serde(default)]
2228 pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
2229 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2231 pub channel_switches: Vec<VoiceChannelSwitch>,
2232 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2234 pub timed_pedals: Vec<TimedPedalDirection>,
2235 #[serde(default)]
2237 pub multi_rest_count: Option<u8>,
2238 #[serde(default)]
2240 pub system_break: bool,
2241 #[serde(default)]
2243 pub page_break: bool,
2244 #[serde(default)]
2247 pub section_break: bool,
2248 pub voices: [Vec<Note>; 4],
2250 #[serde(default)]
2256 pub source_voice_numbers: [Option<u32>; 4],
2257 #[serde(default, skip_serializing_if = "Option::is_none")]
2260 pub actual_length: Option<MeasureLength>,
2261 #[serde(default, skip_serializing_if = "Option::is_none")]
2265 pub measure_repeat: Option<u8>,
2266}
2267
2268impl Measure {
2269 pub fn clef_at(&self, start_clef: &Clef, beats: f64) -> Clef {
2272 self.mid_clefs
2273 .iter()
2274 .take_while(|change| {
2275 change
2276 .offset
2277 .beats()
2278 .is_some_and(|offset| offset <= beats + 1e-9)
2279 })
2280 .last()
2281 .map_or_else(|| start_clef.clone(), |change| change.clef.clone())
2282 }
2283
2284 pub fn empty(numerator: u8, denominator: u8) -> Self {
2285 let total_beats = TimeSignature {
2286 numerator,
2287 denominator,
2288 }
2289 .total_beats();
2290 let mut voice0: Vec<Note> = Vec::new();
2291 let mut remaining = total_beats;
2292 while remaining > 1e-9 {
2293 let dur = Duration::whole_filling_beats(remaining);
2294 remaining -= dur.beats(0);
2295 voice0.push(Note::rest(dur));
2296 }
2297 Self {
2298 number: 0,
2299 time_sig: None,
2300 time_sig_hidden: false,
2301 time_symbol: None,
2302 key_sig: None,
2303 clef: None,
2304 mid_clefs: Vec::new(),
2305 tempo: None,
2306 metronome_mark: None,
2307 tempo_ramp_to: None,
2308 instrument_change: None,
2309 tablature_change: None,
2310 barline_left: Barline::Normal,
2311 barline_right: Barline::Normal,
2312 volta: None,
2313 tempo_text: None,
2314 rehearsal: None,
2315 navigation: None,
2316 expression_text: None,
2317 texts: Vec::new(),
2318 figured_bass: Vec::new(),
2319 harp_pedal_diagrams: Vec::new(),
2320 channel_switches: Vec::new(),
2321 timed_pedals: Vec::new(),
2322 multi_rest_count: None,
2323 system_break: false,
2324 page_break: false,
2325 section_break: false,
2326 voices: [voice0, vec![], vec![], vec![]],
2327 source_voice_numbers: [None; 4],
2328 actual_length: None,
2329 measure_repeat: None,
2330 }
2331 }
2332
2333 pub fn repeat_content_from(&mut self, source: &Measure) {
2336 for (target, voice) in self.voices.iter_mut().zip(&source.voices) {
2337 *target = voice.iter().map(Note::repeated_copy).collect();
2338 }
2339 self.source_voice_numbers = source.source_voice_numbers;
2340 }
2341
2342 pub fn same_sounding_content(&self, other: &Measure) -> bool {
2345 self.voices.iter().zip(&other.voices).all(|(left, right)| {
2346 left.len() == right.len()
2347 && left.iter().zip(right).all(|(a, b)| {
2348 a.is_rest == b.is_rest
2349 && a.pitches == b.pitches
2350 && a.duration == b.duration
2351 && a.dot_count == b.dot_count
2352 && a.tuplet == b.tuplet
2353 && a.is_grace == b.is_grace
2354 })
2355 })
2356 }
2357
2358 pub fn duration_beats(&self, time_signature: &TimeSignature) -> f64 {
2361 self.actual_length
2362 .and_then(|length| length.beats())
2363 .unwrap_or_else(|| {
2364 self.time_sig
2365 .as_ref()
2366 .unwrap_or(time_signature)
2367 .total_beats()
2368 })
2369 }
2370
2371 pub fn renumber(&mut self, n: u32) {
2372 self.number = n;
2373 }
2374}
2375
2376#[derive(Debug, Clone, Serialize, Deserialize)]
2377pub struct Note {
2378 pub id: String,
2379 pub is_rest: bool,
2380 #[serde(default)]
2382 pub is_unpitched: bool,
2383 #[serde(default)]
2385 pub instrument_id: Option<String>,
2386 #[serde(default)]
2390 pub offset_x: Option<f64>,
2391 #[serde(default)]
2393 pub offset_y: Option<f64>,
2394 #[serde(default)]
2396 pub relative_x: Option<f64>,
2397 #[serde(default)]
2399 pub relative_y: Option<f64>,
2400 pub pitches: Vec<Pitch>,
2402 #[serde(default)]
2403 pub tab_position: Option<super::notation::TabPosition>,
2404 #[serde(default)]
2407 pub tab_positions: Vec<super::notation::TabPosition>,
2408 pub duration: Duration,
2409 pub dot_count: u8,
2410 pub tie_start: bool,
2411 pub tie_end: bool,
2412 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2416 pub pitch_tie_starts: Vec<bool>,
2417 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2419 pub pitch_tie_ends: Vec<bool>,
2420 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2424 pub pitch_accidentals: Vec<AccidentalDisplay>,
2425 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2430 pub pitch_parentheses: Vec<bool>,
2431 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2436 pub pitch_fingerings: Vec<Option<u8>>,
2437 pub beam: BeamState,
2438 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2441 pub explicit_unbeamed: bool,
2442 pub articulations: Vec<Articulation>,
2443 #[serde(default, skip_serializing_if = "Option::is_none")]
2445 pub tremolo_pair: Option<String>,
2446 pub dynamic: Option<Dynamic>,
2447 pub stem_up: Option<bool>,
2448 #[serde(default)]
2449 pub hairpin_start: Option<HairpinKind>,
2450 #[serde(default)]
2451 pub hairpin_end: bool,
2452 #[serde(default)]
2453 pub tuplet: Option<TupletInfo>,
2454 #[serde(default, skip_serializing_if = "Option::is_none")]
2456 pub tuplet_boundary: Option<TupletBoundary>,
2457 #[serde(default)]
2458 pub chord_symbol: Option<ChordSymbol>,
2459 #[serde(default)]
2460 pub is_grace: bool,
2461 #[serde(default)]
2463 pub grace_slash: bool,
2464 #[serde(default)]
2465 pub ottava_start: Option<OttavaKind>,
2466 #[serde(default)]
2467 pub ottava_end: bool,
2468 #[serde(default)]
2469 pub lyric: Option<Lyric>,
2470 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2472 pub additional_lyrics: Vec<VerseLyric>,
2473 #[serde(default)]
2474 pub pedal_start: bool,
2475 #[serde(default)]
2476 pub pedal_end: bool,
2477 #[serde(default)]
2478 pub slur_start: bool,
2479 #[serde(default)]
2480 pub slur_end: bool,
2481 #[serde(default)]
2483 pub arpeggiate: Option<bool>,
2484 #[serde(default)]
2486 pub technique_text: Option<String>,
2487 #[serde(default)]
2488 pub glissando_start: bool,
2489 #[serde(default)]
2490 pub glissando_end: bool,
2491 #[serde(default)]
2492 pub cross_staff: Option<CrossStaff>,
2493 #[serde(default)]
2495 pub fingering: Option<u8>,
2496 #[serde(default)]
2499 pub fingerings: Vec<u8>,
2500 #[serde(default)]
2502 pub string_number: Option<u8>,
2503 #[serde(default)]
2504 pub note_head: NoteHead,
2505 #[serde(default)]
2507 pub is_cue: bool,
2508 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2512 pub hidden: bool,
2513 #[serde(default)]
2515 pub trill_line_start: bool,
2516 #[serde(default)]
2518 pub trill_line_end: bool,
2519 #[serde(default)]
2521 pub guitar_technique: Option<GuitarTechnique>,
2522 #[serde(default)]
2524 pub guitar_bend_alter_cents: Option<i16>,
2525 #[serde(default)]
2527 pub guitar_bend_curve: Vec<GuitarBendPoint>,
2528}
2529
2530#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2532pub struct GuitarBendPoint {
2533 pub position_per_mille: u16,
2534 pub alter_cents: i16,
2535}
2536
2537impl Note {
2538 pub fn select_fingering(
2540 &self,
2541 policy: super::notation::FingeringSelectionPolicy,
2542 ) -> Option<u8> {
2543 let candidates = if self.fingerings.is_empty() {
2544 self.fingering.into_iter().collect::<Vec<_>>()
2545 } else {
2546 self.fingerings.clone()
2547 };
2548 match policy {
2549 super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
2550 super::notation::FingeringSelectionPolicy::LowestNumber => {
2551 candidates.iter().copied().min()
2552 }
2553 super::notation::FingeringSelectionPolicy::HighestNumber => {
2554 candidates.iter().copied().max()
2555 }
2556 }
2557 }
2558
2559 pub fn new(pitch: Pitch, duration: Duration) -> Self {
2560 Self {
2561 id: Uuid::new_v4().to_string(),
2562 is_rest: false,
2563 is_unpitched: false,
2564 instrument_id: None,
2565 offset_x: None,
2566 offset_y: None,
2567 relative_x: None,
2568 relative_y: None,
2569 pitches: vec![pitch],
2570 tab_position: None,
2571 tab_positions: Vec::new(),
2572 duration,
2573 dot_count: 0,
2574 tie_start: false,
2575 tie_end: false,
2576 pitch_tie_starts: Vec::new(),
2577 pitch_tie_ends: Vec::new(),
2578 pitch_accidentals: Vec::new(),
2579 pitch_parentheses: Vec::new(),
2580 pitch_fingerings: Vec::new(),
2581 beam: BeamState::None,
2582 explicit_unbeamed: false,
2583 articulations: Vec::new(),
2584 tremolo_pair: None,
2585 dynamic: None,
2586 stem_up: None,
2587 hairpin_start: None,
2588 hairpin_end: false,
2589 tuplet: None,
2590 tuplet_boundary: None,
2591 chord_symbol: None,
2592 is_grace: false,
2593 grace_slash: false,
2594 ottava_start: None,
2595 ottava_end: false,
2596 lyric: None,
2597 additional_lyrics: Vec::new(),
2598 pedal_start: false,
2599 pedal_end: false,
2600 slur_start: false,
2601 slur_end: false,
2602 arpeggiate: None,
2603 technique_text: None,
2604 glissando_start: false,
2605 glissando_end: false,
2606 cross_staff: None,
2607 fingering: None,
2608 fingerings: Vec::new(),
2609 string_number: None,
2610 note_head: NoteHead::Normal,
2611 is_cue: false,
2612 hidden: false,
2613 trill_line_start: false,
2614 trill_line_end: false,
2615 guitar_technique: None,
2616 guitar_bend_alter_cents: None,
2617 guitar_bend_curve: Vec::new(),
2618 }
2619 }
2620
2621 pub fn rest(duration: Duration) -> Self {
2622 Self {
2623 id: Uuid::new_v4().to_string(),
2624 is_rest: true,
2625 is_unpitched: false,
2626 instrument_id: None,
2627 offset_x: None,
2628 offset_y: None,
2629 relative_x: None,
2630 relative_y: None,
2631 pitches: Vec::new(),
2632 tab_position: None,
2633 tab_positions: Vec::new(),
2634 duration,
2635 dot_count: 0,
2636 tie_start: false,
2637 tie_end: false,
2638 pitch_tie_starts: Vec::new(),
2639 pitch_tie_ends: Vec::new(),
2640 pitch_accidentals: Vec::new(),
2641 pitch_parentheses: Vec::new(),
2642 pitch_fingerings: Vec::new(),
2643 beam: BeamState::None,
2644 explicit_unbeamed: false,
2645 articulations: Vec::new(),
2646 tremolo_pair: None,
2647 dynamic: None,
2648 stem_up: None,
2649 hairpin_start: None,
2650 hairpin_end: false,
2651 tuplet: None,
2652 tuplet_boundary: None,
2653 chord_symbol: None,
2654 is_grace: false,
2655 grace_slash: false,
2656 ottava_start: None,
2657 ottava_end: false,
2658 lyric: None,
2659 additional_lyrics: Vec::new(),
2660 pedal_start: false,
2661 pedal_end: false,
2662 slur_start: false,
2663 slur_end: false,
2664 arpeggiate: None,
2665 technique_text: None,
2666 glissando_start: false,
2667 glissando_end: false,
2668 cross_staff: None,
2669 fingering: None,
2670 fingerings: Vec::new(),
2671 string_number: None,
2672 note_head: NoteHead::Normal,
2673 is_cue: false,
2674 hidden: false,
2675 trill_line_start: false,
2676 trill_line_end: false,
2677 guitar_technique: None,
2678 guitar_bend_alter_cents: None,
2679 guitar_bend_curve: Vec::new(),
2680 }
2681 }
2682
2683 pub fn repeated_copy(&self) -> Note {
2686 let mut copy = self.clone();
2687 copy.id = Uuid::new_v4().to_string();
2688 copy.tie_start = false;
2689 copy.tie_end = false;
2690 copy.slur_start = false;
2691 copy.slur_end = false;
2692 copy.hairpin_start = None;
2693 copy.hairpin_end = false;
2694 copy.pedal_start = false;
2695 copy.pedal_end = false;
2696 copy.ottava_start = None;
2697 copy.ottava_end = false;
2698 copy.glissando_start = false;
2699 copy.glissando_end = false;
2700 copy.trill_line_start = false;
2701 copy.trill_line_end = false;
2702 copy.lyric = None;
2703 copy.additional_lyrics.clear();
2704 copy.chord_symbol = None;
2705 copy.dynamic = None;
2706 copy
2707 }
2708
2709 pub fn pitch_tie_start(&self, index: usize) -> bool {
2712 per_pitch_flag(
2713 &self.pitch_tie_starts,
2714 self.pitches.len(),
2715 index,
2716 self.tie_start,
2717 )
2718 }
2719
2720 pub fn pitch_tie_end(&self, index: usize) -> bool {
2722 per_pitch_flag(
2723 &self.pitch_tie_ends,
2724 self.pitches.len(),
2725 index,
2726 self.tie_end,
2727 )
2728 }
2729
2730 pub fn set_pitch_ties(&mut self, starts: &[bool], ends: &[bool]) {
2735 let count = self.pitches.len();
2736 if starts.len() == count {
2737 self.tie_start = starts.iter().any(|tied| *tied);
2738 self.pitch_tie_starts = if starts.iter().all(|tied| *tied == self.tie_start) {
2739 Vec::new()
2740 } else {
2741 starts.to_vec()
2742 };
2743 }
2744 if ends.len() == count {
2745 self.tie_end = ends.iter().any(|tied| *tied);
2746 self.pitch_tie_ends = if ends.iter().all(|tied| *tied == self.tie_end) {
2747 Vec::new()
2748 } else {
2749 ends.to_vec()
2750 };
2751 }
2752 }
2753
2754 pub fn accidental_display(&self, index: usize) -> AccidentalDisplay {
2756 if self.pitch_accidentals.len() > self.pitches.len() {
2757 return AccidentalDisplay::Auto;
2758 }
2759 self.pitch_accidentals
2760 .get(index)
2761 .copied()
2762 .unwrap_or_default()
2763 }
2764
2765 pub fn set_accidental_display(&mut self, index: usize, display: AccidentalDisplay) {
2768 if index >= self.pitches.len() {
2769 return;
2770 }
2771 if self.pitch_accidentals.len() > self.pitches.len() {
2772 self.pitch_accidentals.clear();
2773 }
2774 self.pitch_accidentals
2775 .resize(self.pitches.len(), AccidentalDisplay::Auto);
2776 self.pitch_accidentals[index] = display;
2777 if self
2778 .pitch_accidentals
2779 .iter()
2780 .all(|display| *display == AccidentalDisplay::Auto)
2781 {
2782 self.pitch_accidentals.clear();
2783 }
2784 }
2785
2786 pub fn pitch_fingering(&self, index: usize) -> Option<u8> {
2788 if self.pitch_fingerings.len() > self.pitches.len() {
2789 return None;
2790 }
2791 self.pitch_fingerings.get(index).copied().flatten()
2792 }
2793
2794 pub fn set_pitch_fingering(&mut self, index: usize, fingering: u8) {
2797 if index >= self.pitches.len() {
2798 return;
2799 }
2800 if self.pitch_fingerings.len() > self.pitches.len() {
2801 self.pitch_fingerings.clear();
2802 }
2803 if self.pitch_fingerings.len() <= index {
2804 self.pitch_fingerings.resize(index + 1, None);
2805 }
2806 self.pitch_fingerings[index] = Some(fingering);
2807 if self.fingering.is_none() {
2808 self.fingering = Some(fingering);
2809 }
2810 self.fingerings.push(fingering);
2811 }
2812
2813 pub fn is_parenthesized(&self, index: usize) -> bool {
2815 self.pitch_parentheses.len() <= self.pitches.len()
2816 && self.pitch_parentheses.get(index).copied().unwrap_or(false)
2817 }
2818
2819 pub fn set_parenthesized(&mut self, index: usize, parenthesized: bool) {
2822 if index >= self.pitches.len() {
2823 return;
2824 }
2825 if self.pitch_parentheses.len() > self.pitches.len() {
2826 self.pitch_parentheses.clear();
2827 }
2828 self.pitch_parentheses.resize(self.pitches.len(), false);
2829 self.pitch_parentheses[index] = parenthesized;
2830 if !self.pitch_parentheses.contains(&true) {
2831 self.pitch_parentheses.clear();
2832 }
2833 }
2834
2835 pub fn pitch_tie_starts_or_uniform(&self) -> Vec<bool> {
2837 (0..self.pitches.len())
2838 .map(|index| self.pitch_tie_start(index))
2839 .collect()
2840 }
2841
2842 pub fn pitch_tie_ends_or_uniform(&self) -> Vec<bool> {
2844 (0..self.pitches.len())
2845 .map(|index| self.pitch_tie_end(index))
2846 .collect()
2847 }
2848
2849 pub fn is_plain_whole_rest(&self) -> bool {
2853 self.is_rest
2854 && matches!(self.duration, Duration::Whole)
2855 && self.dot_count == 0
2856 && self.tuplet.is_none()
2857 && !self.is_grace
2858 && !self.is_cue
2859 }
2860
2861 pub fn beats(&self) -> f64 {
2862 if self.is_grace || self.is_cue {
2863 return 0.0;
2864 }
2865 let base = self.duration.beats(self.dot_count);
2866 if let Some(ref t) = self.tuplet {
2867 base * (t.normal_notes as f64) / (t.actual_notes as f64)
2868 } else {
2869 base
2870 }
2871 }
2872}
2873
2874impl Duration {
2875 pub fn whole_filling_beats(beats: f64) -> Duration {
2877 if beats >= 16.0 {
2878 Duration::Longa
2879 } else if beats >= 8.0 {
2880 Duration::Breve
2881 } else if beats >= 4.0 {
2882 Duration::Whole
2883 } else if beats >= 2.0 {
2884 Duration::Half
2885 } else if beats >= 1.0 {
2886 Duration::Quarter
2887 } else if beats >= 0.5 {
2888 Duration::Eighth
2889 } else if beats >= 0.25 {
2890 Duration::Sixteenth
2891 } else if beats >= 0.125 {
2892 Duration::ThirtySecond
2893 } else if beats >= 0.0625 {
2894 Duration::SixtyFourth
2895 } else {
2896 Duration::HundredTwentyEighth
2897 }
2898 }
2899}
2900
2901pub fn tuplet_group_ranges(voice: &[Note]) -> Vec<(usize, usize)> {
2904 let mut groups = Vec::new();
2905 let mut open: Option<(usize, TupletInfo, u64, bool)> = None;
2906 let mut previous_timed = 0;
2907 for (index, note) in voice.iter().enumerate() {
2908 if note.is_grace {
2909 continue;
2910 }
2911 let has_start = matches!(
2912 note.tuplet_boundary,
2913 Some(TupletBoundary::Start | TupletBoundary::StartStop)
2914 );
2915 let has_stop = matches!(
2916 note.tuplet_boundary,
2917 Some(TupletBoundary::Stop | TupletBoundary::StartStop)
2918 );
2919 let must_close = open
2920 .as_ref()
2921 .is_some_and(|(_, ratio, _, _)| note.tuplet.as_ref() != Some(ratio) || has_start);
2922 if must_close && let Some((start, _, _, _)) = open.take() {
2923 groups.push((start, previous_timed));
2924 }
2925 if let Some(ratio) = ¬e.tuplet {
2926 let (numerator, denominator) = note.duration.as_fraction();
2927 let base = 4096 * u64::from(numerator) / u64::from(denominator.max(1));
2928 let dots = u32::from(note.dot_count.min(4));
2929 let written = base * ((1u64 << (dots + 1)) - 1) / (1u64 << dots);
2930 let (_, current_ratio, total_written, explicit) =
2931 open.get_or_insert_with(|| (index, ratio.clone(), 0, has_start));
2932 *total_written += written;
2933 let count = u64::from(current_ratio.actual_notes.max(1));
2934 let unit = *total_written / count;
2935 let legacy_complete = !*explicit
2936 && *total_written % count == 0
2937 && unit > 0
2938 && unit.is_power_of_two()
2939 && unit <= 4096;
2940 if (has_stop || legacy_complete)
2941 && let Some((start, _, _, _)) = open.take()
2942 {
2943 groups.push((start, index));
2944 }
2945 }
2946 previous_timed = index;
2947 }
2948 if let Some((start, _, _, _)) = open {
2949 groups.push((start, previous_timed));
2950 }
2951 groups
2952}
2953
2954#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2958pub struct NoteAddr {
2959 pub part: usize,
2960 pub staff: usize,
2961 pub measure: usize,
2962 pub voice: usize,
2963 pub note: usize,
2964}
2965
2966#[derive(Debug, Clone, Serialize, Deserialize)]
2970pub enum ScoreChange {
2971 ScoreViewsChanged {
2972 old: Vec<ScoreView>,
2973 new: Vec<ScoreView>,
2974 },
2975 ScoreTextChanged {
2976 old: Vec<StyledText>,
2977 new: Vec<StyledText>,
2978 },
2979 MeasureTextChanged {
2980 part: usize,
2981 staff: usize,
2982 measure: usize,
2983 old: Vec<StyledText>,
2984 new: Vec<StyledText>,
2985 },
2986 FiguredBassChanged {
2987 part: usize,
2988 staff: usize,
2989 measure: usize,
2990 old: Vec<FiguredBassFigure>,
2991 new: Vec<FiguredBassFigure>,
2992 },
2993 HarpPedalDiagramsChanged {
2994 part: usize,
2995 staff: usize,
2996 measure: usize,
2997 old: Vec<HarpPedalDiagram>,
2998 new: Vec<HarpPedalDiagram>,
2999 },
3000 TimedPedalsChanged {
3001 part: usize,
3002 staff: usize,
3003 measure: usize,
3004 old: Vec<TimedPedalDirection>,
3005 new: Vec<TimedPedalDirection>,
3006 },
3007 ChordDefinitionsChanged {
3008 old: Vec<ChordDefinition>,
3009 new: Vec<ChordDefinition>,
3010 },
3011 PartNamesChanged {
3012 part: usize,
3013 old_name: String,
3014 new_name: String,
3015 old_short_name: String,
3016 new_short_name: String,
3017 },
3018 PartMidiChanged {
3019 part: usize,
3020 old_channel: u8,
3021 new_channel: u8,
3022 old_program: u8,
3023 new_program: u8,
3024 },
3025 InstrumentDefinitionChanged {
3026 part: usize,
3027 old: Option<InstrumentDefinition>,
3028 new: Option<InstrumentDefinition>,
3029 },
3030 PartMidiAutomationChanged {
3031 part: usize,
3032 old_pitch_bends: Vec<MidiPitchBend>,
3033 new_pitch_bends: Vec<MidiPitchBend>,
3034 old_control_changes: Vec<MidiControlChange>,
3035 new_control_changes: Vec<MidiControlChange>,
3036 old_program_changes: Vec<MidiProgramChange>,
3037 new_program_changes: Vec<MidiProgramChange>,
3038 old_aftertouch: Vec<MidiAftertouch>,
3039 new_aftertouch: Vec<MidiAftertouch>,
3040 },
3041 StaffConfigurationChanged {
3042 part: usize,
3043 staff: usize,
3044 old_clef: Clef,
3045 new_clef: Clef,
3046 old_transpose_semitones: i8,
3047 new_transpose_semitones: i8,
3048 },
3049 StaffPresentationChanged {
3050 part: usize,
3051 staff: usize,
3052 old: StaffPresentation,
3053 new: StaffPresentation,
3054 },
3055 MeasurePresentationChanged {
3056 part: usize,
3057 staff: usize,
3058 measure: usize,
3059 old_number: u32,
3060 new_number: u32,
3061 old_clef: Option<Clef>,
3062 new_clef: Option<Clef>,
3063 old_tempo_text: Option<String>,
3064 new_tempo_text: Option<String>,
3065 old_navigation: Option<String>,
3066 new_navigation: Option<String>,
3067 old_expression_text: Option<String>,
3068 new_expression_text: Option<String>,
3069 old_multi_rest_count: Option<u8>,
3070 new_multi_rest_count: Option<u8>,
3071 old_system_break: bool,
3072 new_system_break: bool,
3073 old_page_break: bool,
3074 new_page_break: bool,
3075 old_section_break: bool,
3076 new_section_break: bool,
3077 },
3078 TablatureConfigChanged {
3079 part: usize,
3080 staff: usize,
3081 old: Option<TablatureConfig>,
3082 new: Option<TablatureConfig>,
3083 },
3084 TablatureChangeChanged {
3085 part: usize,
3086 staff: usize,
3087 measure: usize,
3088 old: Option<TablatureConfig>,
3089 new: Option<TablatureConfig>,
3090 },
3091 MidMeasureClefsChanged {
3092 part: usize,
3093 staff: usize,
3094 measure: usize,
3095 old: Vec<MidMeasureClef>,
3096 new: Vec<MidMeasureClef>,
3097 },
3098 UnrepresentedFieldChanged {
3103 path: String,
3104 },
3105 MetadataChanged {
3106 field: String,
3107 old: String,
3108 new: String,
3109 },
3110 TempoChanged {
3111 old: u16,
3112 new: u16,
3113 },
3114 KeySignatureChanged {
3115 old: KeySignature,
3116 new: KeySignature,
3117 },
3118 PartAdded {
3119 part_index: usize,
3120 },
3121 PartRemoved {
3122 part_index: usize,
3123 name: String,
3124 },
3125 NoteAdded {
3126 part: usize,
3127 staff: usize,
3128 measure: usize,
3129 voice: usize,
3130 note_index: usize,
3131 },
3132 NoteRemoved {
3133 part: usize,
3134 staff: usize,
3135 measure: usize,
3136 voice: usize,
3137 note: Box<Note>,
3138 },
3139 NoteModified {
3140 part: usize,
3141 staff: usize,
3142 measure: usize,
3143 voice: usize,
3144 note_index: usize,
3145 old: Box<Note>,
3146 new: Box<Note>,
3147 },
3148 TimeSigChanged {
3149 part: usize,
3150 staff: usize,
3151 measure: usize,
3152 old: Option<TimeSignature>,
3153 new: Option<TimeSignature>,
3154 },
3155 TimeVisibilityChanged {
3156 part: usize,
3157 staff: usize,
3158 measure: usize,
3159 old: bool,
3160 new: bool,
3161 },
3162 TimeSymbolChanged {
3163 part: usize,
3164 staff: usize,
3165 measure: usize,
3166 old: Option<TimeSymbol>,
3167 new: Option<TimeSymbol>,
3168 },
3169 MeasureTempoChanged {
3170 part: usize,
3171 staff: usize,
3172 measure: usize,
3173 old: Option<u16>,
3174 new: Option<u16>,
3175 },
3176 MeasureMetronomeMarkChanged {
3177 part: usize,
3178 staff: usize,
3179 measure: usize,
3180 old: Option<MetronomeMark>,
3181 new: Option<MetronomeMark>,
3182 },
3183 MeasureTempoRampChanged {
3184 part: usize,
3185 staff: usize,
3186 measure: usize,
3187 old: Option<u16>,
3188 new: Option<u16>,
3189 },
3190 BarlineChanged {
3191 part: usize,
3192 staff: usize,
3193 measure: usize,
3194 },
3195 RehearsalMarkChanged {
3196 part: usize,
3197 staff: usize,
3198 measure: usize,
3199 old: Option<String>,
3200 new: Option<String>,
3201 },
3202 VoltaChanged {
3203 part: usize,
3204 staff: usize,
3205 measure: usize,
3206 },
3207}
3208
3209pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
3215 let mut changes: Vec<ScoreChange> = Vec::new();
3216
3217 if a.views != b.views {
3218 changes.push(ScoreChange::ScoreViewsChanged {
3219 old: a.views.clone(),
3220 new: b.views.clone(),
3221 });
3222 }
3223
3224 if a.texts != b.texts {
3225 changes.push(ScoreChange::ScoreTextChanged {
3226 old: a.texts.clone(),
3227 new: b.texts.clone(),
3228 });
3229 }
3230 if a.chord_definitions != b.chord_definitions {
3231 changes.push(ScoreChange::ChordDefinitionsChanged {
3232 old: a.chord_definitions.clone(),
3233 new: b.chord_definitions.clone(),
3234 });
3235 }
3236 if let Some(path) = first_unrepresented_field_change(a, b) {
3237 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
3238 }
3239
3240 macro_rules! meta {
3241 ($field:ident, $name:literal) => {
3242 if a.metadata.$field != b.metadata.$field {
3243 changes.push(ScoreChange::MetadataChanged {
3244 field: $name.to_string(),
3245 old: a.metadata.$field.clone(),
3246 new: b.metadata.$field.clone(),
3247 });
3248 }
3249 };
3250 }
3251 meta!(title, "title");
3252 meta!(composer, "composer");
3253 meta!(lyricist, "lyricist");
3254 meta!(copyright, "copyright");
3255 meta!(work_number, "work_number");
3256 meta!(movement_title, "movement_title");
3257
3258 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3259 changes.push(ScoreChange::TempoChanged {
3260 old: a.settings.tempo_bpm,
3261 new: b.settings.tempo_bpm,
3262 });
3263 }
3264 if a.settings.key_signature != b.settings.key_signature {
3265 changes.push(ScoreChange::KeySignatureChanged {
3266 old: a.settings.key_signature.clone(),
3267 new: b.settings.key_signature.clone(),
3268 });
3269 }
3270
3271 let a_len = a.parts.len();
3272 let b_len = b.parts.len();
3273 for i in b_len..a_len {
3274 changes.push(ScoreChange::PartRemoved {
3275 part_index: i,
3276 name: a.parts[i].name.clone(),
3277 });
3278 }
3279 for i in a_len..b_len {
3280 changes.push(ScoreChange::PartAdded { part_index: i });
3281 }
3282
3283 for pi in 0..a_len.min(b_len) {
3284 let ap = &a.parts[pi];
3285 let bp = &b.parts[pi];
3286 if ap.name != bp.name || ap.short_name != bp.short_name {
3287 changes.push(ScoreChange::PartNamesChanged {
3288 part: pi,
3289 old_name: ap.name.clone(),
3290 new_name: bp.name.clone(),
3291 old_short_name: ap.short_name.clone(),
3292 new_short_name: bp.short_name.clone(),
3293 });
3294 }
3295 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3296 changes.push(ScoreChange::PartMidiChanged {
3297 part: pi,
3298 old_channel: ap.midi_channel,
3299 new_channel: bp.midi_channel,
3300 old_program: ap.midi_program,
3301 new_program: bp.midi_program,
3302 });
3303 }
3304 if ap.instrument != bp.instrument {
3305 changes.push(ScoreChange::InstrumentDefinitionChanged {
3306 part: pi,
3307 old: ap.instrument.clone(),
3308 new: bp.instrument.clone(),
3309 });
3310 }
3311 if ap.midi_pitch_bends != bp.midi_pitch_bends
3312 || ap.midi_control_changes != bp.midi_control_changes
3313 || ap.midi_program_changes != bp.midi_program_changes
3314 || ap.midi_aftertouch != bp.midi_aftertouch
3315 {
3316 changes.push(ScoreChange::PartMidiAutomationChanged {
3317 part: pi,
3318 old_pitch_bends: ap.midi_pitch_bends.clone(),
3319 new_pitch_bends: bp.midi_pitch_bends.clone(),
3320 old_control_changes: ap.midi_control_changes.clone(),
3321 new_control_changes: bp.midi_control_changes.clone(),
3322 old_program_changes: ap.midi_program_changes.clone(),
3323 new_program_changes: bp.midi_program_changes.clone(),
3324 old_aftertouch: ap.midi_aftertouch.clone(),
3325 new_aftertouch: bp.midi_aftertouch.clone(),
3326 });
3327 }
3328 for si in 0..ap.staves.len().min(bp.staves.len()) {
3329 let a_staff = &ap.staves[si];
3330 let b_staff = &bp.staves[si];
3331 if a_staff.clef != b_staff.clef
3332 || a_staff.transpose_semitones != b_staff.transpose_semitones
3333 {
3334 changes.push(ScoreChange::StaffConfigurationChanged {
3335 part: pi,
3336 staff: si,
3337 old_clef: a_staff.clef.clone(),
3338 new_clef: b_staff.clef.clone(),
3339 old_transpose_semitones: a_staff.transpose_semitones,
3340 new_transpose_semitones: b_staff.transpose_semitones,
3341 });
3342 }
3343 if a_staff.tablature != b_staff.tablature {
3344 changes.push(ScoreChange::TablatureConfigChanged {
3345 part: pi,
3346 staff: si,
3347 old: a_staff.tablature.clone(),
3348 new: b_staff.tablature.clone(),
3349 });
3350 }
3351 if a_staff.presentation != b_staff.presentation {
3352 changes.push(ScoreChange::StaffPresentationChanged {
3353 part: pi,
3354 staff: si,
3355 old: a_staff.presentation.clone(),
3356 new: b_staff.presentation.clone(),
3357 });
3358 }
3359 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3360 let am = &a_staff.measures[mi];
3361 let bm = &b_staff.measures[mi];
3362 if am.tablature_change != bm.tablature_change {
3363 changes.push(ScoreChange::TablatureChangeChanged {
3364 part: pi,
3365 staff: si,
3366 measure: mi,
3367 old: am.tablature_change.clone(),
3368 new: bm.tablature_change.clone(),
3369 });
3370 }
3371 if am.mid_clefs != bm.mid_clefs {
3372 changes.push(ScoreChange::MidMeasureClefsChanged {
3373 part: pi,
3374 staff: si,
3375 measure: mi,
3376 old: am.mid_clefs.clone(),
3377 new: bm.mid_clefs.clone(),
3378 });
3379 }
3380 if am.number != bm.number
3381 || am.clef != bm.clef
3382 || am.tempo_text != bm.tempo_text
3383 || am.navigation != bm.navigation
3384 || am.expression_text != bm.expression_text
3385 || am.multi_rest_count != bm.multi_rest_count
3386 || am.system_break != bm.system_break
3387 || am.page_break != bm.page_break
3388 || am.section_break != bm.section_break
3389 {
3390 changes.push(ScoreChange::MeasurePresentationChanged {
3391 part: pi,
3392 staff: si,
3393 measure: mi,
3394 old_number: am.number,
3395 new_number: bm.number,
3396 old_clef: am.clef.clone(),
3397 new_clef: bm.clef.clone(),
3398 old_tempo_text: am.tempo_text.clone(),
3399 new_tempo_text: bm.tempo_text.clone(),
3400 old_navigation: am.navigation.clone(),
3401 new_navigation: bm.navigation.clone(),
3402 old_expression_text: am.expression_text.clone(),
3403 new_expression_text: bm.expression_text.clone(),
3404 old_multi_rest_count: am.multi_rest_count,
3405 new_multi_rest_count: bm.multi_rest_count,
3406 old_system_break: am.system_break,
3407 new_system_break: bm.system_break,
3408 old_page_break: am.page_break,
3409 new_page_break: bm.page_break,
3410 old_section_break: am.section_break,
3411 new_section_break: bm.section_break,
3412 });
3413 }
3414 for vi in 0..4usize {
3415 let av = &am.voices[vi];
3416 let bv = &bm.voices[vi];
3417 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3418 if !note_content_eq(a_note, b_note) {
3419 changes.push(ScoreChange::NoteModified {
3420 part: pi,
3421 staff: si,
3422 measure: mi,
3423 voice: vi,
3424 note_index: ni,
3425 old: Box::new(a_note.clone()),
3426 new: Box::new(b_note.clone()),
3427 });
3428 }
3429 }
3430 for note in av.iter().skip(bv.len()) {
3431 changes.push(ScoreChange::NoteRemoved {
3432 part: pi,
3433 staff: si,
3434 measure: mi,
3435 voice: vi,
3436 note: Box::new(note.clone()),
3437 });
3438 }
3439 for ni in av.len()..bv.len() {
3440 changes.push(ScoreChange::NoteAdded {
3441 part: pi,
3442 staff: si,
3443 measure: mi,
3444 voice: vi,
3445 note_index: ni,
3446 });
3447 }
3448 }
3449 if am.time_sig != bm.time_sig {
3450 changes.push(ScoreChange::TimeSigChanged {
3451 part: pi,
3452 staff: si,
3453 measure: mi,
3454 old: am.time_sig.clone(),
3455 new: bm.time_sig.clone(),
3456 });
3457 }
3458 if am.time_sig_hidden != bm.time_sig_hidden {
3459 changes.push(ScoreChange::TimeVisibilityChanged {
3460 part: pi,
3461 staff: si,
3462 measure: mi,
3463 old: am.time_sig_hidden,
3464 new: bm.time_sig_hidden,
3465 });
3466 }
3467 if am.time_symbol != bm.time_symbol {
3468 changes.push(ScoreChange::TimeSymbolChanged {
3469 part: pi,
3470 staff: si,
3471 measure: mi,
3472 old: am.time_symbol,
3473 new: bm.time_symbol,
3474 });
3475 }
3476 if am.tempo != bm.tempo {
3477 changes.push(ScoreChange::MeasureTempoChanged {
3478 part: pi,
3479 staff: si,
3480 measure: mi,
3481 old: am.tempo,
3482 new: bm.tempo,
3483 });
3484 }
3485 if am.metronome_mark != bm.metronome_mark {
3486 changes.push(ScoreChange::MeasureMetronomeMarkChanged {
3487 part: pi,
3488 staff: si,
3489 measure: mi,
3490 old: am.metronome_mark.clone(),
3491 new: bm.metronome_mark.clone(),
3492 });
3493 }
3494 if am.tempo_ramp_to != bm.tempo_ramp_to {
3495 changes.push(ScoreChange::MeasureTempoRampChanged {
3496 part: pi,
3497 staff: si,
3498 measure: mi,
3499 old: am.tempo_ramp_to,
3500 new: bm.tempo_ramp_to,
3501 });
3502 }
3503 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3504 changes.push(ScoreChange::BarlineChanged {
3505 part: pi,
3506 staff: si,
3507 measure: mi,
3508 });
3509 }
3510 if am.rehearsal != bm.rehearsal {
3511 changes.push(ScoreChange::RehearsalMarkChanged {
3512 part: pi,
3513 staff: si,
3514 measure: mi,
3515 old: am.rehearsal.clone(),
3516 new: bm.rehearsal.clone(),
3517 });
3518 }
3519 if am.volta != bm.volta {
3520 changes.push(ScoreChange::VoltaChanged {
3521 part: pi,
3522 staff: si,
3523 measure: mi,
3524 });
3525 }
3526 if am.texts != bm.texts {
3527 changes.push(ScoreChange::MeasureTextChanged {
3528 part: pi,
3529 staff: si,
3530 measure: mi,
3531 old: am.texts.clone(),
3532 new: bm.texts.clone(),
3533 });
3534 }
3535 if am.figured_bass != bm.figured_bass {
3536 changes.push(ScoreChange::FiguredBassChanged {
3537 part: pi,
3538 staff: si,
3539 measure: mi,
3540 old: am.figured_bass.clone(),
3541 new: bm.figured_bass.clone(),
3542 });
3543 }
3544 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3545 changes.push(ScoreChange::HarpPedalDiagramsChanged {
3546 part: pi,
3547 staff: si,
3548 measure: mi,
3549 old: am.harp_pedal_diagrams.clone(),
3550 new: bm.harp_pedal_diagrams.clone(),
3551 });
3552 }
3553 if am.timed_pedals != bm.timed_pedals {
3554 changes.push(ScoreChange::TimedPedalsChanged {
3555 part: pi,
3556 staff: si,
3557 measure: mi,
3558 old: am.timed_pedals.clone(),
3559 new: bm.timed_pedals.clone(),
3560 });
3561 }
3562 }
3563 }
3564 }
3565
3566 changes
3567}
3568
3569fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
3570 if a.settings.time_signature != b.settings.time_signature {
3571 return Some("settings.time_signature".to_string());
3572 }
3573 if a.part_groups.len() != b.part_groups.len()
3574 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3575 x.first_part != y.first_part
3576 || x.last_part != y.last_part
3577 || x.symbol != y.symbol
3578 || x.barlines_connect != y.barlines_connect
3579 })
3580 {
3581 return Some("part_groups".to_string());
3582 }
3583 a.parts
3584 .iter()
3585 .zip(&b.parts)
3586 .enumerate()
3587 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
3588}
3589
3590fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
3591 let prefix = format!("parts[{part_index}]");
3592 if a.named_channels != b.named_channels {
3593 return Some(format!("{prefix}.named_channels"));
3594 }
3595 if a.percussion_instruments != b.percussion_instruments {
3596 return Some(format!("{prefix}.percussion_instruments"));
3597 }
3598 if a.staff_groups != b.staff_groups {
3599 return Some(format!("{prefix}.staff_groups"));
3600 }
3601 if a.staves.len() != b.staves.len() {
3602 return Some(format!("{prefix}.staves"));
3603 }
3604 a.staves
3605 .iter()
3606 .zip(&b.staves)
3607 .enumerate()
3608 .find_map(|(staff_index, (a, b))| {
3609 first_unrepresented_staff_change(&prefix, staff_index, a, b)
3610 })
3611}
3612
3613fn first_unrepresented_staff_change(
3614 part_prefix: &str,
3615 staff_index: usize,
3616 a: &Staff,
3617 b: &Staff,
3618) -> Option<String> {
3619 let prefix = format!("{part_prefix}.staves[{staff_index}]");
3620 if a.measures.len() != b.measures.len() {
3621 return Some(format!("{prefix}.measures"));
3622 }
3623 a.measures
3624 .iter()
3625 .zip(&b.measures)
3626 .enumerate()
3627 .find_map(|(measure_index, (a, b))| {
3628 first_unrepresented_measure_change(&prefix, measure_index, a, b)
3629 })
3630}
3631
3632fn first_unrepresented_measure_change(
3633 staff_prefix: &str,
3634 measure_index: usize,
3635 a: &Measure,
3636 b: &Measure,
3637) -> Option<String> {
3638 (a.channel_switches != b.channel_switches)
3639 .then(|| format!("{staff_prefix}.measures[{measure_index}].channel_switches"))
3640}
3641
3642#[derive(Debug, Clone, Serialize, Deserialize)]
3649pub enum ScorePatch {
3650 SetScoreViews {
3651 value: Vec<ScoreView>,
3652 },
3653 SetScoreTexts {
3654 value: Vec<StyledText>,
3655 },
3656 SetMeasureTexts {
3657 part: usize,
3658 staff: usize,
3659 measure: usize,
3660 value: Vec<StyledText>,
3661 },
3662 SetFiguredBass {
3663 part: usize,
3664 staff: usize,
3665 measure: usize,
3666 value: Vec<FiguredBassFigure>,
3667 },
3668 SetHarpPedalDiagrams {
3669 part: usize,
3670 staff: usize,
3671 measure: usize,
3672 value: Vec<HarpPedalDiagram>,
3673 },
3674 SetTimedPedals {
3675 part: usize,
3676 staff: usize,
3677 measure: usize,
3678 value: Vec<TimedPedalDirection>,
3679 },
3680 SetChordDefinitions {
3681 value: Vec<ChordDefinition>,
3682 },
3683 SetPartNames {
3684 part: usize,
3685 name: String,
3686 short_name: String,
3687 },
3688 SetPartMidi {
3689 part: usize,
3690 channel: u8,
3691 program: u8,
3692 },
3693 SetInstrumentDefinition {
3694 part: usize,
3695 value: Option<InstrumentDefinition>,
3696 },
3697 SetPartMidiAutomation {
3698 part: usize,
3699 pitch_bends: Vec<MidiPitchBend>,
3700 control_changes: Vec<MidiControlChange>,
3701 program_changes: Vec<MidiProgramChange>,
3702 aftertouch: Vec<MidiAftertouch>,
3703 },
3704 SetStaffConfiguration {
3705 part: usize,
3706 staff: usize,
3707 clef: Clef,
3708 transpose_semitones: i8,
3709 },
3710 SetStaffPresentation {
3711 part: usize,
3712 staff: usize,
3713 value: StaffPresentation,
3714 },
3715 SetMeasurePresentation {
3716 part: usize,
3717 staff: usize,
3718 measure: usize,
3719 number: u32,
3720 clef: Option<Clef>,
3721 tempo_text: Option<String>,
3722 navigation: Option<String>,
3723 expression_text: Option<String>,
3724 multi_rest_count: Option<u8>,
3725 system_break: bool,
3726 page_break: bool,
3727 #[serde(default)]
3728 section_break: bool,
3729 },
3730 SetTablatureConfig {
3731 part: usize,
3732 staff: usize,
3733 value: Option<TablatureConfig>,
3734 },
3735 SetMeasureTablatureChange {
3736 part: usize,
3737 staff: usize,
3738 measure: usize,
3739 value: Option<TablatureConfig>,
3740 },
3741 SetMeasureMidClefs {
3742 part: usize,
3743 staff: usize,
3744 measure: usize,
3745 value: Vec<MidMeasureClef>,
3746 },
3747 SetMetadata {
3748 field: String,
3749 value: String,
3750 },
3751 SetTempo {
3752 value: u16,
3753 },
3754 SetKeySignature {
3755 part: usize,
3756 staff: usize,
3757 measure: usize,
3758 value: Option<KeySignature>,
3759 },
3760 SetTimeSignature {
3761 part: usize,
3762 staff: usize,
3763 measure: usize,
3764 value: Option<TimeSignature>,
3765 },
3766 SetTimeVisibility {
3767 part: usize,
3768 staff: usize,
3769 measure: usize,
3770 hidden: bool,
3771 },
3772 SetTimeSymbol {
3773 part: usize,
3774 staff: usize,
3775 measure: usize,
3776 value: Option<TimeSymbol>,
3777 },
3778 SetBarlines {
3779 part: usize,
3780 staff: usize,
3781 measure: usize,
3782 left: Barline,
3783 right: Barline,
3784 },
3785 SetRehearsal {
3786 part: usize,
3787 staff: usize,
3788 measure: usize,
3789 value: Option<String>,
3790 },
3791 SetVolta {
3792 part: usize,
3793 staff: usize,
3794 measure: usize,
3795 value: Option<VoltaBracket>,
3796 },
3797 AddNote {
3799 part: usize,
3800 staff: usize,
3801 measure: usize,
3802 voice: usize,
3803 #[serde(default = "legacy_append_index")]
3805 note_index: usize,
3806 note: Box<Note>,
3807 },
3808 RemoveNote {
3809 part: usize,
3810 staff: usize,
3811 measure: usize,
3812 voice: usize,
3813 note_index: usize,
3814 },
3815 ReplaceNote {
3817 part: usize,
3818 staff: usize,
3819 measure: usize,
3820 voice: usize,
3821 note_index: usize,
3822 note: Box<Note>,
3823 },
3824 SetMeasureTempo {
3825 part: usize,
3826 staff: usize,
3827 measure: usize,
3828 value: Option<u16>,
3829 },
3830 SetMeasureMetronomeMark {
3831 part: usize,
3832 staff: usize,
3833 measure: usize,
3834 value: Option<MetronomeMark>,
3835 },
3836 SetMeasureTempoRamp {
3837 part: usize,
3838 staff: usize,
3839 measure: usize,
3840 value: Option<u16>,
3841 },
3842 ReplaceScore {
3845 score: Box<Score>,
3846 },
3847}
3848
3849fn legacy_append_index() -> usize {
3850 usize::MAX
3851}
3852
3853fn patch_requires_replace(a: &Score, b: &Score) -> bool {
3857 if a.settings.time_signature != b.settings.time_signature
3858 || a.settings.key_signature != b.settings.key_signature
3859 || a.parts.len() != b.parts.len()
3860 || a.part_groups.len() != b.part_groups.len()
3861 {
3862 return true;
3863 }
3864 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3865 x.first_part != y.first_part
3866 || x.last_part != y.last_part
3867 || x.symbol != y.symbol
3868 || x.barlines_connect != y.barlines_connect
3869 }) {
3870 return true;
3871 }
3872 for (ap, bp) in a.parts.iter().zip(&b.parts) {
3873 if ap.percussion_instruments != bp.percussion_instruments
3874 || ap.named_channels != bp.named_channels
3875 || ap.staff_groups != bp.staff_groups
3876 || ap.staves.len() != bp.staves.len()
3877 {
3878 return true;
3879 }
3880 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
3881 if as_.measures.len() != bs.measures.len()
3882 || as_
3883 .measures
3884 .iter()
3885 .zip(&bs.measures)
3886 .any(|(a, b)| a.channel_switches != b.channel_switches)
3887 {
3888 return true;
3889 }
3890 }
3891 }
3892 false
3893}
3894
3895pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
3900 let mut patches: Vec<ScorePatch> = Vec::new();
3901
3902 if patch_requires_replace(a, b) {
3903 return vec![ScorePatch::ReplaceScore {
3904 score: Box::new(b.clone()),
3905 }];
3906 }
3907
3908 if a.views != b.views {
3909 patches.push(ScorePatch::SetScoreViews {
3910 value: b.views.clone(),
3911 });
3912 }
3913
3914 if a.texts != b.texts {
3915 patches.push(ScorePatch::SetScoreTexts {
3916 value: b.texts.clone(),
3917 });
3918 }
3919 if a.chord_definitions != b.chord_definitions {
3920 patches.push(ScorePatch::SetChordDefinitions {
3921 value: b.chord_definitions.clone(),
3922 });
3923 }
3924
3925 macro_rules! meta {
3926 ($field:ident, $name:literal) => {
3927 if a.metadata.$field != b.metadata.$field {
3928 patches.push(ScorePatch::SetMetadata {
3929 field: $name.to_string(),
3930 value: b.metadata.$field.clone(),
3931 });
3932 }
3933 };
3934 }
3935 meta!(title, "title");
3936 meta!(composer, "composer");
3937 meta!(lyricist, "lyricist");
3938 meta!(copyright, "copyright");
3939 meta!(work_number, "work_number");
3940 meta!(movement_title, "movement_title");
3941
3942 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3943 patches.push(ScorePatch::SetTempo {
3944 value: b.settings.tempo_bpm,
3945 });
3946 }
3947
3948 for pi in 0..a.parts.len().min(b.parts.len()) {
3949 let ap = &a.parts[pi];
3950 let bp = &b.parts[pi];
3951 if ap.name != bp.name || ap.short_name != bp.short_name {
3952 patches.push(ScorePatch::SetPartNames {
3953 part: pi,
3954 name: bp.name.clone(),
3955 short_name: bp.short_name.clone(),
3956 });
3957 }
3958 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3959 patches.push(ScorePatch::SetPartMidi {
3960 part: pi,
3961 channel: bp.midi_channel,
3962 program: bp.midi_program,
3963 });
3964 }
3965 if ap.instrument != bp.instrument {
3966 patches.push(ScorePatch::SetInstrumentDefinition {
3967 part: pi,
3968 value: bp.instrument.clone(),
3969 });
3970 }
3971 if ap.midi_pitch_bends != bp.midi_pitch_bends
3972 || ap.midi_control_changes != bp.midi_control_changes
3973 || ap.midi_program_changes != bp.midi_program_changes
3974 || ap.midi_aftertouch != bp.midi_aftertouch
3975 {
3976 patches.push(ScorePatch::SetPartMidiAutomation {
3977 part: pi,
3978 pitch_bends: bp.midi_pitch_bends.clone(),
3979 control_changes: bp.midi_control_changes.clone(),
3980 program_changes: bp.midi_program_changes.clone(),
3981 aftertouch: bp.midi_aftertouch.clone(),
3982 });
3983 }
3984 for si in 0..ap.staves.len().min(bp.staves.len()) {
3985 let a_staff = &ap.staves[si];
3986 let b_staff = &bp.staves[si];
3987 if a_staff.clef != b_staff.clef
3988 || a_staff.transpose_semitones != b_staff.transpose_semitones
3989 {
3990 patches.push(ScorePatch::SetStaffConfiguration {
3991 part: pi,
3992 staff: si,
3993 clef: b_staff.clef.clone(),
3994 transpose_semitones: b_staff.transpose_semitones,
3995 });
3996 }
3997 if a_staff.tablature != b_staff.tablature {
3998 patches.push(ScorePatch::SetTablatureConfig {
3999 part: pi,
4000 staff: si,
4001 value: b_staff.tablature.clone(),
4002 });
4003 }
4004 if a_staff.presentation != b_staff.presentation {
4005 patches.push(ScorePatch::SetStaffPresentation {
4006 part: pi,
4007 staff: si,
4008 value: b_staff.presentation.clone(),
4009 });
4010 }
4011 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
4012 let am = &a_staff.measures[mi];
4013 let bm = &b_staff.measures[mi];
4014
4015 if am.tablature_change != bm.tablature_change {
4016 patches.push(ScorePatch::SetMeasureTablatureChange {
4017 part: pi,
4018 staff: si,
4019 measure: mi,
4020 value: bm.tablature_change.clone(),
4021 });
4022 }
4023
4024 if am.mid_clefs != bm.mid_clefs {
4025 patches.push(ScorePatch::SetMeasureMidClefs {
4026 part: pi,
4027 staff: si,
4028 measure: mi,
4029 value: bm.mid_clefs.clone(),
4030 });
4031 }
4032
4033 if am.number != bm.number
4034 || am.clef != bm.clef
4035 || am.tempo_text != bm.tempo_text
4036 || am.navigation != bm.navigation
4037 || am.expression_text != bm.expression_text
4038 || am.multi_rest_count != bm.multi_rest_count
4039 || am.system_break != bm.system_break
4040 || am.page_break != bm.page_break
4041 || am.section_break != bm.section_break
4042 {
4043 patches.push(ScorePatch::SetMeasurePresentation {
4044 part: pi,
4045 staff: si,
4046 measure: mi,
4047 number: bm.number,
4048 clef: bm.clef.clone(),
4049 tempo_text: bm.tempo_text.clone(),
4050 navigation: bm.navigation.clone(),
4051 expression_text: bm.expression_text.clone(),
4052 multi_rest_count: bm.multi_rest_count,
4053 system_break: bm.system_break,
4054 page_break: bm.page_break,
4055 section_break: bm.section_break,
4056 });
4057 }
4058 if am.key_sig != bm.key_sig {
4059 patches.push(ScorePatch::SetKeySignature {
4060 part: pi,
4061 staff: si,
4062 measure: mi,
4063 value: bm.key_sig.clone(),
4064 });
4065 }
4066 if am.time_sig != bm.time_sig {
4067 patches.push(ScorePatch::SetTimeSignature {
4068 part: pi,
4069 staff: si,
4070 measure: mi,
4071 value: bm.time_sig.clone(),
4072 });
4073 }
4074 if am.time_sig_hidden != bm.time_sig_hidden {
4075 patches.push(ScorePatch::SetTimeVisibility {
4076 part: pi,
4077 staff: si,
4078 measure: mi,
4079 hidden: bm.time_sig_hidden,
4080 });
4081 }
4082 if am.time_symbol != bm.time_symbol {
4083 patches.push(ScorePatch::SetTimeSymbol {
4084 part: pi,
4085 staff: si,
4086 measure: mi,
4087 value: bm.time_symbol,
4088 });
4089 }
4090 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
4091 patches.push(ScorePatch::SetBarlines {
4092 part: pi,
4093 staff: si,
4094 measure: mi,
4095 left: bm.barline_left.clone(),
4096 right: bm.barline_right.clone(),
4097 });
4098 }
4099 if am.rehearsal != bm.rehearsal {
4100 patches.push(ScorePatch::SetRehearsal {
4101 part: pi,
4102 staff: si,
4103 measure: mi,
4104 value: bm.rehearsal.clone(),
4105 });
4106 }
4107 if am.volta != bm.volta {
4108 patches.push(ScorePatch::SetVolta {
4109 part: pi,
4110 staff: si,
4111 measure: mi,
4112 value: bm.volta.clone(),
4113 });
4114 }
4115 if am.tempo != bm.tempo {
4116 patches.push(ScorePatch::SetMeasureTempo {
4117 part: pi,
4118 staff: si,
4119 measure: mi,
4120 value: bm.tempo,
4121 });
4122 }
4123 if am.metronome_mark != bm.metronome_mark {
4124 patches.push(ScorePatch::SetMeasureMetronomeMark {
4125 part: pi,
4126 staff: si,
4127 measure: mi,
4128 value: bm.metronome_mark.clone(),
4129 });
4130 }
4131 if am.tempo_ramp_to != bm.tempo_ramp_to {
4132 patches.push(ScorePatch::SetMeasureTempoRamp {
4133 part: pi,
4134 staff: si,
4135 measure: mi,
4136 value: bm.tempo_ramp_to,
4137 });
4138 }
4139 if am.texts != bm.texts {
4140 patches.push(ScorePatch::SetMeasureTexts {
4141 part: pi,
4142 staff: si,
4143 measure: mi,
4144 value: bm.texts.clone(),
4145 });
4146 }
4147 if am.figured_bass != bm.figured_bass {
4148 patches.push(ScorePatch::SetFiguredBass {
4149 part: pi,
4150 staff: si,
4151 measure: mi,
4152 value: bm.figured_bass.clone(),
4153 });
4154 }
4155 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
4156 patches.push(ScorePatch::SetHarpPedalDiagrams {
4157 part: pi,
4158 staff: si,
4159 measure: mi,
4160 value: bm.harp_pedal_diagrams.clone(),
4161 });
4162 }
4163
4164 if am.timed_pedals != bm.timed_pedals {
4165 patches.push(ScorePatch::SetTimedPedals {
4166 part: pi,
4167 staff: si,
4168 measure: mi,
4169 value: bm.timed_pedals.clone(),
4170 });
4171 }
4172
4173 for vi in 0..4usize {
4174 let av = &am.voices[vi];
4175 let bv = &bm.voices[vi];
4176 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
4177 if !note_content_eq(a_note, b_note) {
4178 patches.push(ScorePatch::ReplaceNote {
4179 part: pi,
4180 staff: si,
4181 measure: mi,
4182 voice: vi,
4183 note_index: ni,
4184 note: Box::new(b_note.clone()),
4185 });
4186 }
4187 }
4188 for ni in (bv.len()..av.len()).rev() {
4190 patches.push(ScorePatch::RemoveNote {
4191 part: pi,
4192 staff: si,
4193 measure: mi,
4194 voice: vi,
4195 note_index: ni,
4196 });
4197 }
4198 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
4200 patches.push(ScorePatch::AddNote {
4201 part: pi,
4202 staff: si,
4203 measure: mi,
4204 voice: vi,
4205 note_index: av.len() + offset,
4206 note: Box::new(note.clone()),
4207 });
4208 }
4209 }
4210 }
4211 }
4212 }
4213
4214 patches
4215}
4216
4217pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
4222 let mut s = score.clone();
4223 for patch in patches {
4224 match patch {
4225 ScorePatch::ReplaceScore { score } => {
4226 s = (**score).clone();
4227 }
4228 ScorePatch::SetScoreViews { value } => {
4229 s.views = value.clone();
4230 }
4231 ScorePatch::SetScoreTexts { value } => {
4232 s.texts = value.clone();
4233 }
4234 ScorePatch::SetMeasureTexts {
4235 part,
4236 staff,
4237 measure,
4238 value,
4239 } => {
4240 s.parts
4241 .get_mut(*part)
4242 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4243 .staves
4244 .get_mut(*staff)
4245 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4246 .measures
4247 .get_mut(*measure)
4248 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4249 .texts = value.clone();
4250 }
4251 ScorePatch::SetFiguredBass {
4252 part,
4253 staff,
4254 measure,
4255 value,
4256 } => {
4257 s.parts
4258 .get_mut(*part)
4259 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4260 .staves
4261 .get_mut(*staff)
4262 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4263 .measures
4264 .get_mut(*measure)
4265 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4266 .figured_bass = value.clone();
4267 }
4268 ScorePatch::SetHarpPedalDiagrams {
4269 part,
4270 staff,
4271 measure,
4272 value,
4273 } => {
4274 s.parts
4275 .get_mut(*part)
4276 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4277 .staves
4278 .get_mut(*staff)
4279 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4280 .measures
4281 .get_mut(*measure)
4282 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4283 .harp_pedal_diagrams = value.clone();
4284 }
4285 ScorePatch::SetTimedPedals {
4286 part,
4287 staff,
4288 measure,
4289 value,
4290 } => {
4291 s.parts
4292 .get_mut(*part)
4293 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4294 .staves
4295 .get_mut(*staff)
4296 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4297 .measures
4298 .get_mut(*measure)
4299 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4300 .timed_pedals = value.clone();
4301 }
4302 ScorePatch::SetChordDefinitions { value } => {
4303 s.chord_definitions = value.clone();
4304 }
4305 ScorePatch::SetPartNames {
4306 part,
4307 name,
4308 short_name,
4309 } => {
4310 let target = s
4311 .parts
4312 .get_mut(*part)
4313 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4314 target.name = name.clone();
4315 target.short_name = short_name.clone();
4316 }
4317 ScorePatch::SetPartMidi {
4318 part,
4319 channel,
4320 program,
4321 } => {
4322 let target = s
4323 .parts
4324 .get_mut(*part)
4325 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4326 target.midi_channel = *channel;
4327 target.midi_program = *program;
4328 }
4329 ScorePatch::SetInstrumentDefinition { part, value } => {
4330 s.parts
4331 .get_mut(*part)
4332 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4333 .instrument = value.clone();
4334 }
4335 ScorePatch::SetPartMidiAutomation {
4336 part,
4337 pitch_bends,
4338 control_changes,
4339 program_changes,
4340 aftertouch,
4341 } => {
4342 let target = s
4343 .parts
4344 .get_mut(*part)
4345 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4346 target.midi_pitch_bends = pitch_bends.clone();
4347 target.midi_control_changes = control_changes.clone();
4348 target.midi_program_changes = program_changes.clone();
4349 target.midi_aftertouch = aftertouch.clone();
4350 }
4351 ScorePatch::SetStaffConfiguration {
4352 part,
4353 staff,
4354 clef,
4355 transpose_semitones,
4356 } => {
4357 let target = s
4358 .parts
4359 .get_mut(*part)
4360 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4361 .staves
4362 .get_mut(*staff)
4363 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
4364 target.clef = clef.clone();
4365 target.transpose_semitones = *transpose_semitones;
4366 }
4367 ScorePatch::SetStaffPresentation { part, staff, value } => {
4368 let target = s
4369 .parts
4370 .get_mut(*part)
4371 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4372 .staves
4373 .get_mut(*staff)
4374 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
4375 target.presentation = value.clone();
4376 }
4377 ScorePatch::SetMeasurePresentation {
4378 part,
4379 staff,
4380 measure,
4381 number,
4382 clef,
4383 tempo_text,
4384 navigation,
4385 expression_text,
4386 multi_rest_count,
4387 system_break,
4388 page_break,
4389 section_break,
4390 } => {
4391 let target = s
4392 .parts
4393 .get_mut(*part)
4394 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4395 .staves
4396 .get_mut(*staff)
4397 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4398 .measures
4399 .get_mut(*measure)
4400 .ok_or_else(|| {
4401 Error::InvalidPatch(format!("measure {measure} out of range"))
4402 })?;
4403 target.number = *number;
4404 target.clef = clef.clone();
4405 target.tempo_text = tempo_text.clone();
4406 target.navigation = navigation.clone();
4407 target.expression_text = expression_text.clone();
4408 target.multi_rest_count = *multi_rest_count;
4409 target.system_break = *system_break;
4410 target.page_break = *page_break;
4411 target.section_break = *section_break;
4412 }
4413 ScorePatch::SetTablatureConfig { part, staff, value } => {
4414 s.parts
4415 .get_mut(*part)
4416 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4417 .staves
4418 .get_mut(*staff)
4419 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4420 .tablature = value.clone();
4421 }
4422 ScorePatch::SetMeasureTablatureChange {
4423 part,
4424 staff,
4425 measure,
4426 value,
4427 } => {
4428 s.parts
4429 .get_mut(*part)
4430 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4431 .staves
4432 .get_mut(*staff)
4433 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4434 .measures
4435 .get_mut(*measure)
4436 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4437 .tablature_change = value.clone();
4438 }
4439 ScorePatch::SetMeasureMidClefs {
4440 part,
4441 staff,
4442 measure,
4443 value,
4444 } => {
4445 s.parts
4446 .get_mut(*part)
4447 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4448 .staves
4449 .get_mut(*staff)
4450 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4451 .measures
4452 .get_mut(*measure)
4453 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4454 .mid_clefs = value.clone();
4455 }
4456 ScorePatch::SetMetadata { field, value } => match field.as_str() {
4457 "title" => s.metadata.title = value.clone(),
4458 "composer" => s.metadata.composer = value.clone(),
4459 "lyricist" => s.metadata.lyricist = value.clone(),
4460 "copyright" => s.metadata.copyright = value.clone(),
4461 "work_number" => s.metadata.work_number = value.clone(),
4462 "movement_title" => s.metadata.movement_title = value.clone(),
4463 other => {
4464 return Err(Error::InvalidPatch(format!(
4465 "unknown metadata field: {other}"
4466 )));
4467 }
4468 },
4469 ScorePatch::SetTempo { value } => {
4470 s.settings.tempo_bpm = *value;
4471 }
4472 ScorePatch::SetKeySignature {
4473 part,
4474 staff,
4475 measure,
4476 value,
4477 } => {
4478 s.parts
4479 .get_mut(*part)
4480 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4481 .staves
4482 .get_mut(*staff)
4483 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4484 .measures
4485 .get_mut(*measure)
4486 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4487 .key_sig = value.clone();
4488 }
4489 ScorePatch::SetTimeSignature {
4490 part,
4491 staff,
4492 measure,
4493 value,
4494 } => {
4495 s.parts
4496 .get_mut(*part)
4497 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4498 .staves
4499 .get_mut(*staff)
4500 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4501 .measures
4502 .get_mut(*measure)
4503 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4504 .time_sig = value.clone();
4505 }
4506 ScorePatch::SetTimeVisibility {
4507 part,
4508 staff,
4509 measure,
4510 hidden,
4511 } => {
4512 s.parts
4513 .get_mut(*part)
4514 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4515 .staves
4516 .get_mut(*staff)
4517 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4518 .measures
4519 .get_mut(*measure)
4520 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4521 .time_sig_hidden = *hidden;
4522 }
4523 ScorePatch::SetTimeSymbol {
4524 part,
4525 staff,
4526 measure,
4527 value,
4528 } => {
4529 s.parts
4530 .get_mut(*part)
4531 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4532 .staves
4533 .get_mut(*staff)
4534 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4535 .measures
4536 .get_mut(*measure)
4537 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4538 .time_symbol = *value;
4539 }
4540 ScorePatch::SetBarlines {
4541 part,
4542 staff,
4543 measure,
4544 left,
4545 right,
4546 } => {
4547 let m = s
4548 .parts
4549 .get_mut(*part)
4550 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4551 .staves
4552 .get_mut(*staff)
4553 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4554 .measures
4555 .get_mut(*measure)
4556 .ok_or_else(|| {
4557 Error::InvalidPatch(format!("measure {measure} out of range"))
4558 })?;
4559 m.barline_left = left.clone();
4560 m.barline_right = right.clone();
4561 }
4562 ScorePatch::SetRehearsal {
4563 part,
4564 staff,
4565 measure,
4566 value,
4567 } => {
4568 s.parts
4569 .get_mut(*part)
4570 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4571 .staves
4572 .get_mut(*staff)
4573 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4574 .measures
4575 .get_mut(*measure)
4576 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4577 .rehearsal = value.clone();
4578 }
4579 ScorePatch::SetVolta {
4580 part,
4581 staff,
4582 measure,
4583 value,
4584 } => {
4585 s.parts
4586 .get_mut(*part)
4587 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4588 .staves
4589 .get_mut(*staff)
4590 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4591 .measures
4592 .get_mut(*measure)
4593 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4594 .volta = value.clone();
4595 }
4596 ScorePatch::AddNote {
4597 part,
4598 staff,
4599 measure,
4600 voice,
4601 note_index,
4602 note,
4603 } => {
4604 let v = s
4605 .parts
4606 .get_mut(*part)
4607 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4608 .staves
4609 .get_mut(*staff)
4610 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4611 .measures
4612 .get_mut(*measure)
4613 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4614 .voices
4615 .get_mut(*voice)
4616 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4617 let insert_at = if *note_index == usize::MAX {
4618 v.len()
4619 } else {
4620 *note_index
4621 };
4622 if insert_at > v.len() {
4623 return Err(Error::InvalidPatch(format!(
4624 "note_index {note_index} out of range"
4625 )));
4626 }
4627 v.insert(insert_at, *note.clone());
4628 }
4629 ScorePatch::RemoveNote {
4630 part,
4631 staff,
4632 measure,
4633 voice,
4634 note_index,
4635 } => {
4636 let v = s
4637 .parts
4638 .get_mut(*part)
4639 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4640 .staves
4641 .get_mut(*staff)
4642 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4643 .measures
4644 .get_mut(*measure)
4645 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4646 .voices
4647 .get_mut(*voice)
4648 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4649 if *note_index >= v.len() {
4650 return Err(Error::InvalidPatch(format!(
4651 "note_index {note_index} out of range"
4652 )));
4653 }
4654 v.remove(*note_index);
4655 }
4656 ScorePatch::ReplaceNote {
4657 part,
4658 staff,
4659 measure,
4660 voice,
4661 note_index,
4662 note,
4663 } => {
4664 let v = s
4665 .parts
4666 .get_mut(*part)
4667 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4668 .staves
4669 .get_mut(*staff)
4670 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4671 .measures
4672 .get_mut(*measure)
4673 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4674 .voices
4675 .get_mut(*voice)
4676 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4677 if *note_index >= v.len() {
4678 return Err(Error::InvalidPatch(format!(
4679 "note_index {note_index} out of range"
4680 )));
4681 }
4682 v[*note_index] = *note.clone();
4683 }
4684 ScorePatch::SetMeasureTempo {
4685 part,
4686 staff,
4687 measure,
4688 value,
4689 } => {
4690 s.parts
4691 .get_mut(*part)
4692 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4693 .staves
4694 .get_mut(*staff)
4695 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4696 .measures
4697 .get_mut(*measure)
4698 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4699 .tempo = *value;
4700 }
4701 ScorePatch::SetMeasureTempoRamp {
4702 part,
4703 staff,
4704 measure,
4705 value,
4706 } => {
4707 s.parts
4708 .get_mut(*part)
4709 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4710 .staves
4711 .get_mut(*staff)
4712 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4713 .measures
4714 .get_mut(*measure)
4715 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4716 .tempo_ramp_to = *value;
4717 }
4718 ScorePatch::SetMeasureMetronomeMark {
4719 part,
4720 staff,
4721 measure,
4722 value,
4723 } => {
4724 s.parts
4725 .get_mut(*part)
4726 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4727 .staves
4728 .get_mut(*staff)
4729 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4730 .measures
4731 .get_mut(*measure)
4732 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4733 .metronome_mark = value.clone();
4734 }
4735 }
4736 }
4737 if !super::validate::validate(&s).is_valid() {
4738 return Err(Error::InvalidScore);
4739 }
4740 Ok(s)
4741}
4742
4743pub fn staff_ottava_octave_shifts(
4748 staff: &Staff,
4749 default_time: &TimeSignature,
4750) -> std::collections::HashMap<(usize, usize, usize), i8> {
4751 if !staff
4753 .measures
4754 .iter()
4755 .flat_map(|m| &m.voices)
4756 .flatten()
4757 .any(|n| n.ottava_start.is_some())
4758 {
4759 return std::collections::HashMap::new();
4760 }
4761 type OttavaEvent = (f64, u8, i8, (usize, usize, usize));
4763 let mut events: Vec<OttavaEvent> = Vec::new();
4764 for voice in 0..4 {
4765 let mut measure_start = 0.0;
4766 let mut active = None;
4767 for (measure_index, measure) in staff.measures.iter().enumerate() {
4768 let mut position = measure_start;
4769 for (note_index, note) in measure.voices[voice].iter().enumerate() {
4770 let key = (measure_index, voice, note_index);
4771 if let Some(kind) = note.ottava_start {
4772 if let Some(previous) = active.take() {
4773 events.push((position, 0, previous, key));
4774 }
4775 let shift = match kind {
4776 OttavaKind::Va8 => 1,
4777 OttavaKind::Vb8 => -1,
4778 OttavaKind::Ma15 => 2,
4779 OttavaKind::Mb15 => -2,
4780 };
4781 events.push((position, 1, shift, key));
4782 active = Some(shift);
4783 }
4784 events.push((position, 2, 0, key));
4785 let duration = note.beats();
4786 if note.ottava_end
4787 && let Some(shift) = active.take()
4788 {
4789 events.push((position + duration.max(1e-9), 0, shift, key));
4790 }
4791 position += duration;
4792 }
4793 measure_start += staff.measure_beats(measure_index, default_time);
4794 }
4795 }
4796 events.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
4797 let mut current = 0_i8;
4801 let mut result = std::collections::HashMap::new();
4802 for (_, kind, shift, key) in events {
4803 match kind {
4804 0 => current = 0,
4805 1 => current = shift,
4806 _ if current != 0 => {
4807 result.insert(key, current);
4808 }
4809 _ => {}
4810 }
4811 }
4812 result
4813}
4814
4815pub fn voice_duration_beats(voice: &[Note], measure_beats: f64) -> f64 {
4819 match voice {
4820 [only] if only.is_plain_whole_rest() => measure_beats,
4821 _ => voice.iter().map(Note::beats).sum(),
4822 }
4823}
4824
4825pub fn respell_score(score: &mut Score, prefer_flat: bool) {
4829 for part in &mut score.parts {
4830 for staff in &mut part.staves {
4831 for measure in &mut staff.measures {
4832 for voice in &mut measure.voices {
4833 for note in voice.iter_mut() {
4834 for pitch in &mut note.pitches {
4835 *pitch = pitch.respell(prefer_flat);
4836 }
4837 }
4838 }
4839 }
4840 }
4841 }
4842}
4843
4844#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4846#[serde(rename_all = "snake_case")]
4847pub enum RespellPolicy {
4848 Flat,
4850 Sharp,
4852 Key,
4854}
4855
4856pub fn respell_staff_region(
4867 score: &mut Score,
4868 part_index: usize,
4869 staff_index: usize,
4870 start_measure: usize,
4871 end_measure: usize,
4872 policy: RespellPolicy,
4873) -> Result<(usize, usize), Error> {
4874 let score_key_flat = score.settings.key_signature.fifths < 0;
4875 let part = score
4876 .parts
4877 .get_mut(part_index)
4878 .ok_or(Error::PartNotFound(part_index))?;
4879 let staff_count = part.staves.len();
4880 if staff_index >= staff_count {
4881 return Err(Error::StaffNotFound(staff_index));
4882 }
4883 let measure_count = part.staves[staff_index].measures.len();
4884 if start_measure >= end_measure || end_measure > measure_count {
4885 return Err(Error::InvalidCommand(format!(
4886 "invalid measure range {start_measure}..{end_measure}"
4887 )));
4888 }
4889 let mut prefer_flat = Vec::with_capacity(end_measure);
4890 let mut running_flat = score_key_flat;
4891 for index in 0..end_measure {
4892 let key = part.staves[staff_index].measures[index]
4893 .key_sig
4894 .as_ref()
4895 .or_else(|| {
4896 part.staves[0]
4897 .measures
4898 .get(index)
4899 .and_then(|measure| measure.key_sig.as_ref())
4900 });
4901 if let Some(key) = key {
4902 running_flat = key.fifths < 0;
4903 }
4904 prefer_flat.push(match policy {
4905 RespellPolicy::Flat => true,
4906 RespellPolicy::Sharp => false,
4907 RespellPolicy::Key => running_flat,
4908 });
4909 }
4910
4911 let measures = &mut part.staves[staff_index].measures;
4912 for (index, measure) in measures
4913 .iter_mut()
4914 .enumerate()
4915 .take(end_measure)
4916 .skip(start_measure)
4917 {
4918 for voice in &mut measure.voices {
4919 for note in voice
4920 .iter_mut()
4921 .filter(|note| !note.is_rest && !note.is_unpitched)
4922 {
4923 for pitch in &mut note.pitches {
4924 *pitch = pitch.respell(prefer_flat[index]);
4925 }
4926 }
4927 }
4928 }
4929
4930 let mut changed = (start_measure, end_measure - 1);
4931 for voice in 0..4 {
4932 for measure in start_measure..end_measure {
4933 for note in 0..measures[measure].voices[voice].len() {
4934 if measures[measure].voices[voice][note].tie_start {
4935 let last = propagate_tied_spelling(measures, voice, (measure, note), true);
4936 changed.1 = changed.1.max(last);
4937 }
4938 }
4939 }
4940 if !measures[start_measure].voices[voice].is_empty() {
4941 let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
4942 changed.0 = changed.0.min(first);
4943 }
4944 }
4945 Ok(changed)
4946}
4947
4948fn propagate_tied_spelling(
4951 measures: &mut [Measure],
4952 voice: usize,
4953 from: (usize, usize),
4954 forward: bool,
4955) -> usize {
4956 let mut current = from;
4957 loop {
4958 let note = &measures[current.0].voices[voice][current.1];
4959 if !(if forward {
4960 note.tie_start
4961 } else {
4962 note.tie_end
4963 }) {
4964 return current.0;
4965 }
4966 let neighbor = if forward {
4967 if current.1 + 1 < measures[current.0].voices[voice].len() {
4968 Some((current.0, current.1 + 1))
4969 } else {
4970 measures
4971 .get(current.0 + 1)
4972 .filter(|measure| !measure.voices[voice].is_empty())
4973 .map(|_| (current.0 + 1, 0))
4974 }
4975 } else if current.1 > 0 {
4976 Some((current.0, current.1 - 1))
4977 } else {
4978 current.0.checked_sub(1).and_then(|previous| {
4979 measures[previous].voices[voice]
4980 .len()
4981 .checked_sub(1)
4982 .map(|last| (previous, last))
4983 })
4984 };
4985 let Some(next) = neighbor else {
4986 return current.0;
4987 };
4988 let next_note = &measures[next.0].voices[voice][next.1];
4989 if !(if forward {
4990 next_note.tie_end
4991 } else {
4992 next_note.tie_start
4993 }) {
4994 return current.0;
4995 }
4996 let source = measures[current.0].voices[voice][current.1].pitches.clone();
4997 for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
4998 if let Some(spelled) = source.iter().find(|candidate| {
4999 candidate.to_midi() == pitch.to_midi()
5000 && candidate.microtone_cents == pitch.microtone_cents
5001 }) {
5002 *pitch = spelled.clone();
5003 }
5004 }
5005 current = next;
5006 }
5007}
5008
5009pub fn respell_score_to_key(score: &mut Score) {
5013 let prefer_flat = score.settings.key_signature.fifths < 0;
5014 respell_score(score, prefer_flat);
5015}
5016
5017pub fn score_duration_secs(score: &Score) -> f64 {
5023 if score.settings.tempo_bpm == 0 {
5024 return 0.0;
5025 }
5026 let seq = measure_sequence(score);
5027 let mut total_secs = 0.0f64;
5028 let mut current_bpm = score.settings.tempo_bpm as f64;
5029 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
5030 for &idx in &seq {
5031 if let Some(m) = staff.measures.get(idx) {
5032 if let Some(b) = m.tempo {
5033 current_bpm = b as f64;
5034 }
5035 if current_bpm == 0.0 {
5036 continue;
5037 }
5038 let beats = voice_duration_beats(
5039 &m.voices[0],
5040 staff.measure_beats(idx, &score.settings.time_signature),
5041 );
5042 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
5043 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
5044 current_bpm = f64::from(target);
5045 }
5046 }
5047 }
5048 }
5049 total_secs + super::playback::fermata_total_secs(score, None)
5050}
5051
5052pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
5058 if score.settings.tempo_bpm == 0 {
5059 return 0.0;
5060 }
5061 let seq: Vec<usize> = measure_sequence(score)
5062 .into_iter()
5063 .filter(|&idx| idx >= region.0 && idx <= region.1)
5064 .collect();
5065 let mut total_secs = 0.0f64;
5066 let mut current_bpm = score.settings.tempo_bpm as f64;
5067 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
5068 for &idx in &seq {
5069 if let Some(m) = staff.measures.get(idx) {
5070 if let Some(b) = m.tempo {
5071 current_bpm = b as f64;
5072 }
5073 if current_bpm == 0.0 {
5074 continue;
5075 }
5076 let beats = voice_duration_beats(
5077 &m.voices[0],
5078 staff.measure_beats(idx, &score.settings.time_signature),
5079 );
5080 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
5081 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
5082 current_bpm = f64::from(target);
5083 }
5084 }
5085 }
5086 }
5087 total_secs + super::playback::fermata_total_secs(score, Some(region))
5088}
5089
5090fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
5091 let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
5092 return beats / start_bpm * 60.0;
5093 };
5094 let end_bpm = f64::from(target_bpm);
5095 let delta = end_bpm - start_bpm;
5096 if delta.abs() < f64::EPSILON {
5097 return beats / start_bpm * 60.0;
5098 }
5099 60.0 * beats / delta * (end_bpm / start_bpm).ln()
5100}
5101
5102pub fn measure_beats_remaining(
5107 score: &Score,
5108 part_index: usize,
5109 staff_index: usize,
5110 measure_index: usize,
5111 voice_index: usize,
5112) -> Result<f64, Error> {
5113 let part = score
5114 .parts
5115 .get(part_index)
5116 .ok_or(Error::PartNotFound(part_index))?;
5117 let staff = part
5118 .staves
5119 .get(staff_index)
5120 .ok_or(Error::StaffNotFound(staff_index))?;
5121 let measure = staff
5122 .measures
5123 .get(measure_index)
5124 .ok_or(Error::MeasureNotFound(measure_index))?;
5125 let voice = measure
5126 .voices
5127 .get(voice_index)
5128 .ok_or(Error::VoiceOutOfRange(voice_index))?;
5129 let capacity = staff.measure_beats(measure_index, &score.settings.time_signature);
5130 Ok((capacity - voice_duration_beats(voice, capacity)).max(0.0))
5131}
5132
5133pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
5139 if pitches.is_empty() {
5140 return true;
5141 }
5142 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
5143 avg < clef.middle_line_midi() as f64
5144}
5145
5146fn beam_beat_size(ts: &TimeSignature) -> f64 {
5147 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
5148 3.0 * 4.0 / ts.denominator as f64
5149 } else {
5150 4.0 / ts.denominator as f64
5151 }
5152}
5153
5154pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
5159 let beat_size = beam_beat_size(time_sig);
5160 let n = notes.len();
5161 let mut result = vec![BeamState::None; n];
5162
5163 let is_beamable = |note: &Note| -> bool {
5164 !note.is_rest
5165 && matches!(
5166 note.duration,
5167 Duration::Eighth
5168 | Duration::Sixteenth
5169 | Duration::ThirtySecond
5170 | Duration::SixtyFourth
5171 | Duration::HundredTwentyEighth
5172 )
5173 };
5174
5175 let mut starts = Vec::with_capacity(n);
5177 let mut pos = 0.0f64;
5178 for note in notes {
5179 starts.push(pos);
5180 pos += note.beats();
5181 }
5182
5183 let group_id = |i: usize| -> i64 {
5187 let beat = starts[i] / beat_size;
5188 let nearest = beat.round();
5189 if (beat - nearest).abs() <= 1e-9 {
5190 nearest as i64
5191 } else {
5192 beat.floor() as i64
5193 }
5194 };
5195
5196 let mut i = 0;
5197 while i < n {
5198 if !is_beamable(¬es[i]) {
5199 i += 1;
5200 continue;
5201 }
5202 let g = group_id(i);
5203 let mut j = i;
5205 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
5206 j += 1;
5207 }
5208 let run = j - i;
5209 if run == 1 {
5210 result[i] = BeamState::None;
5211 } else {
5212 result[i] = BeamState::Begin;
5213 result[i + 1..j - 1].fill(BeamState::Continue);
5214 result[j - 1] = BeamState::End;
5215 }
5216 i = j;
5217 }
5218 result
5219}
5220
5221fn note_content_eq(a: &Note, b: &Note) -> bool {
5222 a.is_rest == b.is_rest
5223 && a.is_unpitched == b.is_unpitched
5224 && a.instrument_id == b.instrument_id
5225 && a.offset_x == b.offset_x
5226 && a.offset_y == b.offset_y
5227 && a.relative_x == b.relative_x
5228 && a.relative_y == b.relative_y
5229 && a.pitches == b.pitches
5230 && a.duration == b.duration
5231 && a.dot_count == b.dot_count
5232 && a.tie_start == b.tie_start
5233 && a.tie_end == b.tie_end
5234 && a.beam == b.beam
5235 && a.explicit_unbeamed == b.explicit_unbeamed
5236 && a.articulations == b.articulations
5237 && a.dynamic == b.dynamic
5238 && a.stem_up == b.stem_up
5239 && a.hairpin_start == b.hairpin_start
5240 && a.hairpin_end == b.hairpin_end
5241 && a.tuplet == b.tuplet
5242 && a.chord_symbol == b.chord_symbol
5243 && a.is_grace == b.is_grace
5244 && a.grace_slash == b.grace_slash
5245 && a.ottava_start == b.ottava_start
5246 && a.ottava_end == b.ottava_end
5247 && a.lyric == b.lyric
5248 && a.additional_lyrics == b.additional_lyrics
5249 && a.pedal_start == b.pedal_start
5250 && a.pedal_end == b.pedal_end
5251 && a.slur_start == b.slur_start
5252 && a.slur_end == b.slur_end
5253 && a.arpeggiate == b.arpeggiate
5254 && a.tab_position == b.tab_position
5255 && a.tab_positions == b.tab_positions
5256 && a.guitar_technique == b.guitar_technique
5257 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
5258 && a.guitar_bend_curve == b.guitar_bend_curve
5259}
5260
5261fn per_pitch_flag(flags: &[bool], pitch_count: usize, index: usize, chord: bool) -> bool {
5262 if flags.len() == pitch_count {
5263 flags.get(index).copied().unwrap_or(chord)
5264 } else {
5265 chord
5266 }
5267}
5268
5269#[cfg(test)]
5270mod tests {
5271 #[test]
5272 fn ottava_interval_covers_other_voices_only_at_matching_times() {
5273 use super::*;
5274 use crate::model::{duration::Duration, pitch::Step};
5275 let mut score = Score::new("Ottava", 120, 4, 4, 0, 2);
5276 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5277 let staff = &mut score.parts[0].staves[0];
5278 let mut start = note();
5279 start.ottava_start = Some(OttavaKind::Va8);
5280 staff.measures[0].voices[0] = vec![Note::rest(Duration::Half), start, note()];
5281 let mut end = note();
5282 end.ottava_end = true;
5283 staff.measures[1].voices[0] = vec![end, note()];
5284 staff.measures[0].voices[1] = vec![note(), note(), note(), note()];
5285 staff.measures[1].voices[1] = vec![note(), note()];
5286 let shifts = staff_ottava_octave_shifts(staff, &score.settings.time_signature);
5287 assert_eq!(shifts.get(&(0, 1, 1)), None);
5288 assert_eq!(shifts.get(&(0, 1, 2)), Some(&1));
5289 assert_eq!(shifts.get(&(0, 1, 3)), Some(&1));
5290 assert_eq!(shifts.get(&(1, 1, 0)), Some(&1));
5291 assert_eq!(shifts.get(&(1, 1, 1)), None);
5292 }
5293
5294 use super::*;
5295 use crate::model::{
5296 notation::{FingeringSelectionPolicy, TextStyle},
5297 pitch::Step,
5298 };
5299
5300 #[test]
5301 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
5302 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5303 note.fingerings = vec![3, 1, 4];
5304 note.fingering = Some(3);
5305 assert_eq!(
5306 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
5307 Some(3)
5308 );
5309 assert_eq!(
5310 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
5311 Some(1)
5312 );
5313 assert_eq!(
5314 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
5315 Some(4)
5316 );
5317 assert_eq!(note.fingerings, vec![3, 1, 4]);
5318 assert_eq!(note.fingering, Some(3));
5319 }
5320
5321 #[test]
5322 fn default_score_has_one_part_four_measures() {
5323 let score = Score::default();
5324 assert_eq!(score.parts.len(), 1);
5325 assert_eq!(score.parts[0].staves.len(), 1);
5326 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
5327 }
5328
5329 #[test]
5330 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
5331 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5332 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5333 lines: 6,
5334 tuning_midi: vec![64, 59, 55, 50, 45, 40],
5335 capo: 2,
5336 });
5337 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
5338 Pitch::with_alter(Step::F, 4, 1),
5339 Duration::Quarter,
5340 ));
5341 score.parts[0].staves[0].measures[0].voices[0]
5342 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
5343 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
5344 Some(TabPosition { string: 6, fret: 7 });
5345
5346 assert_eq!(assign_tablature_positions(&mut score), 1);
5347 let notes = &score.parts[0].staves[0].measures[0].voices[0];
5348 assert_eq!(
5349 notes[1].tab_position,
5350 Some(TabPosition { string: 1, fret: 0 })
5351 );
5352 assert_eq!(notes[1].string_number, Some(1));
5353 assert_eq!(
5354 notes[2].tab_position,
5355 Some(TabPosition { string: 6, fret: 7 })
5356 );
5357 }
5358
5359 #[test]
5360 fn assign_tablature_positions_uses_measure_local_capo_change() {
5361 let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
5362 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5363 lines: 6,
5364 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5365 capo: 0,
5366 });
5367 score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
5368 lines: 6,
5369 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5370 capo: 2,
5371 });
5372 score.parts[0].staves[0].measures[1].voices[0]
5373 .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
5374
5375 assert_eq!(assign_tablature_positions(&mut score), 1);
5376 assert_eq!(
5377 score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
5378 Some(TabPosition { string: 5, fret: 3 })
5379 );
5380 }
5381
5382 #[test]
5383 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
5384 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5385 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5386 lines: 6,
5387 tuning_midi: vec![64, 59, 55, 50, 45, 40],
5388 capo: 0,
5389 });
5390 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
5391 chord.pitches.push(Pitch::new(Step::G, 4));
5392 score.parts[0].staves[0].measures[0].voices[0].push(chord);
5393
5394 assert_eq!(assign_tablature_positions(&mut score), 1);
5395 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
5396 assert_eq!(
5397 positions,
5398 &vec![
5399 TabPosition { string: 2, fret: 5 },
5400 TabPosition { string: 1, fret: 3 },
5401 ]
5402 );
5403 }
5404
5405 #[test]
5406 fn optimized_tablature_respects_an_authored_anchor_across_measures() {
5407 let mut score = Score::new("Two-string bass", 120, 4, 4, 0, 2);
5408 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5409 lines: 2,
5410 tuning_midi: vec![40, 45],
5411 capo: 0,
5412 });
5413 let a2 = Pitch::new(Step::A, 2);
5414 let mut anchor = Note::new(a2.clone(), Duration::Quarter);
5415 anchor.tab_position = Some(TabPosition { string: 1, fret: 5 });
5416 score.parts[0].staves[0].measures[0].voices[0] = vec![anchor];
5417 score.parts[0].staves[0].measures[1].voices[0] = vec![
5418 Note::rest(Duration::Quarter),
5419 Note::new(a2, Duration::Quarter),
5420 ];
5421
5422 assert_eq!(optimize_tablature_positions(&mut score), 1);
5423 let anchor = &score.parts[0].staves[0].measures[0].voices[0][0];
5424 let assigned = &score.parts[0].staves[0].measures[1].voices[0][1];
5425 assert_eq!(
5426 anchor.tab_position,
5427 Some(TabPosition { string: 1, fret: 5 })
5428 );
5429 assert_eq!(
5431 assigned.tab_position,
5432 Some(TabPosition { string: 1, fret: 5 })
5433 );
5434 assert_eq!(assigned.string_number, Some(1));
5435 assert_eq!(
5436 assigned.tab_positions,
5437 vec![TabPosition { string: 1, fret: 5 }]
5438 );
5439 assert_eq!(optimize_tablature_positions(&mut score), 0);
5440 }
5441
5442 #[test]
5443 fn optimized_tablature_assigns_distinct_strings_to_a_chord() {
5444 let mut score = Score::new("Guitar chord", 120, 4, 4, 0, 1);
5445 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5446 lines: 6,
5447 tuning_midi: vec![64, 59, 55, 50, 45, 40],
5448 capo: 0,
5449 });
5450 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
5451 chord.pitches.push(Pitch::new(Step::G, 4));
5452 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
5453
5454 assert_eq!(optimize_tablature_positions(&mut score), 1);
5455 let assigned = &score.parts[0].staves[0].measures[0].voices[0][0];
5456 assert_eq!(assigned.tab_positions.len(), 2);
5457 assert_ne!(
5458 assigned.tab_positions[0].string,
5459 assigned.tab_positions[1].string
5460 );
5461 for (pitch, position) in assigned.pitches.iter().zip(&assigned.tab_positions) {
5462 let open = score.parts[0].staves[0]
5463 .tablature
5464 .as_ref()
5465 .unwrap()
5466 .tuning_midi[usize::from(position.string - 1)];
5467 assert_eq!(pitch.to_midi(), open + i16::from(position.fret));
5468 }
5469 }
5470
5471 #[test]
5472 fn new_score_measure_count() {
5473 let score = Score::new("Test", 120, 4, 4, 0, 8);
5474 assert_eq!(score.measure_count(), 8);
5475 }
5476
5477 #[test]
5478 fn note_beats_quarter() {
5479 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5480 assert!((note.beats() - 1.0).abs() < 1e-9);
5481 }
5482
5483 #[test]
5484 fn note_beats_dotted_quarter() {
5485 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5486 note.dot_count = 1;
5487 assert!((note.beats() - 1.5).abs() < 1e-9);
5488 }
5489
5490 #[test]
5491 fn grace_note_beats_zero() {
5492 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
5493 note.is_grace = true;
5494 assert_eq!(note.beats(), 0.0);
5495 }
5496
5497 #[test]
5498 fn measure_empty_4_4_fills_four_beats() {
5499 let m = Measure::empty(4, 4);
5500 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
5501 assert!((total - 4.0).abs() < 1e-9);
5502 }
5503
5504 #[test]
5505 fn measure_empty_3_4_fills_three_beats() {
5506 let m = Measure::empty(3, 4);
5507 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
5508 assert!((total - 3.0).abs() < 1e-9);
5509 }
5510
5511 #[test]
5512 fn whole_filling_beats() {
5513 assert_eq!(Duration::whole_filling_beats(16.0), Duration::Longa);
5514 assert_eq!(Duration::whole_filling_beats(8.0), Duration::Breve);
5515 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
5516 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
5517 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
5518 }
5519
5520 #[test]
5523 fn statistics_default_score_all_rests() {
5524 let score = Score::default();
5525 let s = score.statistics();
5526 assert_eq!(s.part_count, 1);
5527 assert_eq!(s.measure_count, 4);
5528 assert_eq!(s.note_count, 0);
5529 assert!(s.rest_count > 0);
5530 }
5531
5532 #[test]
5533 fn statistics_duration_estimate() {
5534 let score = Score::new("T", 120, 4, 4, 0, 1);
5536 let s = score.statistics();
5537 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
5538 }
5539
5540 #[test]
5541 fn score_duration_secs_matches_statistics() {
5542 use super::score_duration_secs;
5543 let score = Score::new("T", 120, 4, 4, 0, 4);
5544 let secs = score_duration_secs(&score);
5545 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
5547 }
5548
5549 #[test]
5550 fn score_duration_secs_integrates_measure_tempo_ramp() {
5551 use super::score_duration_secs;
5552 let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
5553 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5554 let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
5555 assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
5556 }
5557
5558 #[test]
5559 fn score_duration_secs_zero_bpm_returns_zero() {
5560 use super::score_duration_secs;
5561 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5562 score.settings.tempo_bpm = 0;
5563 assert_eq!(score_duration_secs(&score), 0.0);
5564 }
5565
5566 #[test]
5567 fn score_duration_secs_per_measure_tempo() {
5568 use super::score_duration_secs;
5569 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5571 score.parts[0].staves[0].measures[1].tempo = Some(60);
5572 let secs = score_duration_secs(&score);
5573 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
5574 }
5575
5576 #[test]
5579 fn extract_part_returns_single_part_score() {
5580 let mut score = Score::default();
5581 let mut p2 = Part::new("Violin", "Vln.");
5582 p2.staves.push(Staff::new(Clef::Treble));
5583 score.parts.push(p2);
5584 let ex = score.extract_part(0).unwrap();
5585 assert_eq!(ex.parts.len(), 1);
5586 assert_ne!(ex.id, score.id);
5587 assert_eq!(ex.metadata.title, score.metadata.title);
5588 }
5589
5590 #[test]
5591 fn extract_part_out_of_range_is_none() {
5592 let score = Score::default();
5593 assert!(score.extract_part(99).is_none());
5594 }
5595
5596 #[test]
5597 fn extract_and_merge_remap_typed_spanner_part_addresses() {
5598 let mut left = Score::template(ScoreTemplate::StringQuartet);
5599 let address = NoteAddr {
5600 part: 1,
5601 staff: 0,
5602 measure: 0,
5603 voice: 0,
5604 note: 0,
5605 };
5606 left.spanners.push(NotationSpanner {
5607 id: "left-span".to_string(),
5608 kind: NotationSpannerKind::Slur,
5609 start: address.clone(),
5610 end: address,
5611 number: Some(1),
5612 line_type: None,
5613 text: None,
5614 placement: None,
5615 ottava_size: None,
5616 ottava_type: None,
5617 });
5618 let extracted = left.extract_part_checked(1).expect("valid extracted part");
5619 assert_eq!(extracted.spanners[0].start.part, 0);
5620 assert_eq!(extracted.spanners[0].end.part, 0);
5621
5622 let mut right = Score::new("R", 120, 4, 4, 0, 1);
5623 let right_address = NoteAddr {
5624 part: 0,
5625 staff: 0,
5626 measure: 0,
5627 voice: 0,
5628 note: 0,
5629 };
5630 right.spanners.push(NotationSpanner {
5631 id: "right-span".to_string(),
5632 kind: NotationSpannerKind::Pedal,
5633 start: right_address.clone(),
5634 end: right_address,
5635 number: Some(1),
5636 line_type: None,
5637 text: None,
5638 placement: None,
5639 ottava_size: None,
5640 ottava_type: None,
5641 });
5642 let merged = left.merge_checked(&right).expect("valid merged score");
5643 let right_span = merged
5644 .spanners
5645 .iter()
5646 .find(|spanner| spanner.id == "right-span")
5647 .expect("merged right span");
5648 assert_eq!(right_span.start.part, left.parts.len());
5649 assert_eq!(right_span.end.part, left.parts.len());
5650 }
5651
5652 #[test]
5655 fn transpose_zero_is_clone() {
5656 let score = Score::new("T", 120, 4, 4, 0, 1);
5657 let t = transpose(&score, 0);
5658 assert_eq!(t.settings.key_signature.fifths, 0);
5659 }
5660
5661 #[test]
5662 fn transpose_staff_region_rewrites_only_selected_written_measures() {
5663 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5664 for measure in &mut score.parts[0].staves[0].measures {
5665 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5666 }
5667 let transformed = transpose_staff_region_checked(
5668 &score,
5669 0,
5670 0,
5671 1,
5672 2,
5673 2,
5674 RegionalTranspositionTarget::Written,
5675 )
5676 .expect("region should transpose");
5677 assert_eq!(
5678 transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
5679 Pitch::new(Step::C, 4)
5680 );
5681 assert_eq!(
5682 transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
5683 Pitch::new(Step::D, 4)
5684 );
5685 assert_eq!(
5686 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
5687 Pitch::new(Step::C, 4)
5688 );
5689 }
5690
5691 #[test]
5692 fn regional_concert_transposition_requires_full_staff() {
5693 let score = Score::new("T", 120, 4, 4, 0, 2);
5694 assert!(
5695 transpose_staff_region_checked(
5696 &score,
5697 0,
5698 0,
5699 1,
5700 2,
5701 -2,
5702 RegionalTranspositionTarget::Concert,
5703 )
5704 .is_err()
5705 );
5706 }
5707
5708 #[test]
5709 fn transpose_c_major_up_2_to_d_major() {
5710 let score = Score::new("T", 120, 4, 4, 0, 1);
5711 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
5712 }
5713
5714 #[test]
5715 fn transpose_d_major_up_5_to_g_major() {
5716 let score = Score::new("T", 120, 4, 4, 2, 1);
5717 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
5718 }
5719
5720 #[test]
5721 fn transpose_c4_up_1_to_csharp4() {
5722 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
5723 assert_eq!(p.to_midi(), 61);
5724 assert_eq!(p.step, Step::C);
5725 assert_eq!(p.alter, 1);
5726 }
5727
5728 #[test]
5729 fn transpose_c4_down_1_to_b3() {
5730 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
5731 assert_eq!(p.to_midi(), 59);
5732 assert_eq!(p.step, Step::B);
5733 assert_eq!(p.alter, 0);
5734 }
5735
5736 #[test]
5737 fn transpose_up_octave_keeps_step() {
5738 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
5739 assert_eq!(p.to_midi(), 81);
5740 assert_eq!(p.step, Step::A);
5741 assert_eq!(p.octave, 5);
5742 }
5743
5744 #[test]
5745 fn statistics_with_repeat_doubles_duration() {
5746 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5748 score.parts[0].staves[0].measures[0].barline_left =
5749 crate::model::notation::Barline::RepeatStart;
5750 score.parts[0].staves[0].measures[1].barline_right =
5751 crate::model::notation::Barline::RepeatEnd;
5752 let s = score.statistics();
5753 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
5755 }
5756
5757 #[test]
5758 fn transpose_octave_boundary_b4_to_c5() {
5759 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
5761 assert_eq!(p.to_midi(), 72);
5762 assert_eq!(p.step, Step::C);
5763 assert_eq!(p.octave, 5);
5764 }
5765
5766 #[test]
5767 fn transpose_clamp_at_midi_127() {
5768 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
5770 assert_eq!(p.to_midi(), 127);
5771 }
5772
5773 #[test]
5776 fn merge_combines_parts() {
5777 let mut a = Score::new("A", 120, 4, 4, 0, 2);
5778 let b = Score::new("B", 120, 4, 4, 0, 2);
5779 let mut p2 = Part::new("Violin", "Vln.");
5781 p2.staves.push(Staff::new(Clef::Treble));
5782 for i in 0..2usize {
5783 let mut m = Measure::empty(4, 4);
5784 m.number = i as u32 + 1;
5785 p2.staves[0].measures.push(m);
5786 }
5787 a.parts.push(p2);
5788 let merged = a.merge(&b);
5789 assert_eq!(merged.parts.len(), 3);
5791 }
5792
5793 #[test]
5794 fn merge_pads_shorter_score() {
5795 let a = Score::new("A", 120, 4, 4, 0, 4);
5796 let b = Score::new("B", 120, 4, 4, 0, 2);
5797 let merged = a.merge(&b);
5798 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
5800 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
5801 }
5802
5803 #[test]
5804 fn merge_uses_self_metadata() {
5805 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
5806 a.metadata.composer = "Composer A".to_string();
5807 let b = Score::new("Title B", 120, 4, 4, 0, 2);
5808 let merged = a.merge(&b);
5809 assert_eq!(merged.metadata.title, "Title A");
5810 assert_eq!(merged.metadata.composer, "Composer A");
5811 }
5812
5813 #[test]
5814 fn merge_new_id_differs_from_both() {
5815 let a = Score::new("A", 120, 4, 4, 0, 2);
5816 let b = Score::new("B", 120, 4, 4, 0, 2);
5817 let merged = a.merge(&b);
5818 assert_ne!(merged.id, a.id);
5819 assert_ne!(merged.id, b.id);
5820 }
5821
5822 #[test]
5825 fn staff_default_transpose_is_zero() {
5826 let s = Staff::new(Clef::Treble);
5827 assert_eq!(s.transpose_semitones, 0);
5828 }
5829
5830 #[test]
5831 fn staff_presentation_defaults_and_tracks_percussion_clef() {
5832 let standard = Staff::new(Clef::Treble);
5833 assert_eq!(standard.presentation, StaffPresentation::default());
5834
5835 let percussion = Staff::new(Clef::Percussion);
5836 assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
5837 assert_eq!(percussion.presentation.lines, 5);
5838 assert!(percussion.presentation.visible);
5839 }
5840
5841 #[test]
5844 fn score_default_has_schema_version_1() {
5845 let score = Score::default();
5846 assert_eq!(score.schema_version, 1);
5847 }
5848
5849 #[test]
5850 fn score_new_has_schema_version_1() {
5851 let score = Score::new("T", 120, 4, 4, 0, 4);
5852 assert_eq!(score.schema_version, 1);
5853 }
5854
5855 #[test]
5856 fn chromatic_transposition_covers_every_pitch_class_and_keeps_microtones() {
5857 for midi in 1..=126 {
5858 let mut original = Pitch::from_midi(midi, false);
5859 original.microtone_cents = 25;
5860 for shift in [-1_i8, 1] {
5861 let shifted = transpose_pitch(&original, shift);
5862 assert_eq!(shifted.to_midi(), i16::from(midi) + i16::from(shift));
5863 assert_eq!(shifted.microtone_cents, 25);
5864 assert!(if shift > 0 {
5865 shifted.alter >= 0
5866 } else {
5867 shifted.alter <= 0
5868 });
5869 }
5870 }
5871 }
5872
5873 #[test]
5874 fn score_without_schema_version_deserializes_to_zero() {
5875 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":[]}"#;
5876 let score: Score = serde_json::from_str(json).unwrap();
5877 assert_eq!(score.schema_version, 0);
5878 }
5879
5880 #[test]
5881 fn legacy_score_json_defaults_typed_spanners() {
5882 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5883 let mut value = serde_json::to_value(score).expect("score serializes");
5884 value
5885 .as_object_mut()
5886 .expect("score is an object")
5887 .remove("spanners");
5888 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5889 assert!(restored.spanners.is_empty());
5890 }
5891
5892 #[test]
5893 fn legacy_staff_json_defaults_presentation() {
5894 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5895 let mut value = serde_json::to_value(score).expect("score serializes");
5896 value["parts"][0]["staves"][0]
5897 .as_object_mut()
5898 .expect("staff is an object")
5899 .remove("presentation");
5900
5901 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5902 assert_eq!(
5903 restored.parts[0].staves[0].presentation,
5904 StaffPresentation::default()
5905 );
5906 }
5907
5908 #[test]
5909 fn legacy_part_json_defaults_instrument_definition() {
5910 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5911 let mut value = serde_json::to_value(score).expect("score serializes");
5912 value["parts"][0]
5913 .as_object_mut()
5914 .expect("part is an object")
5915 .remove("instrument");
5916
5917 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5918 assert!(restored.parts[0].instrument.is_none());
5919 }
5920
5921 #[test]
5922 fn resolve_view_projects_linked_part_without_mutating_source_score() {
5923 let mut score = Score::template(ScoreTemplate::StringQuartet);
5924 let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
5925 view.layout.measures_per_row = Some(2);
5926 score.views.push(view);
5927
5928 let projected = score.resolve_view("violin-2").expect("view resolves");
5929 assert_eq!(projected.parts.len(), 1);
5930 assert_eq!(projected.parts[0].name, "Violin II");
5931 assert_eq!(projected.views.len(), 1);
5932 assert_eq!(projected.views[0].parts, vec![0]);
5933 assert_eq!(score.parts.len(), 4);
5934 assert_eq!(score.views[0].parts, vec![1]);
5935 }
5936
5937 #[test]
5938 fn linked_view_checks_staff_references_and_projects_part_groups() {
5939 let mut score = Score::template(ScoreTemplate::StringQuartet);
5940 score.part_groups.push(PartGroup {
5941 first_part: 1,
5942 last_part: 2,
5943 symbol: PartGroupSymbol::Bracket,
5944 barlines_connect: true,
5945 });
5946 let mut view = ScoreView::linked_standard_staff("middle", "Middle voices", 1, 0);
5947 view.parts.push(2);
5948 view.layout
5949 .hidden_staves
5950 .push(ViewStaffRef { part: 2, staff: 0 });
5951 score.views.push(view);
5952 let before = serde_json::to_value(&score).unwrap();
5953 let projected = score.resolve_view("middle").unwrap();
5954 assert_eq!(projected.parts.len(), 2);
5955 assert_eq!(
5956 (
5957 projected.part_groups[0].first_part,
5958 projected.part_groups[0].last_part
5959 ),
5960 (0, 1)
5961 );
5962 assert_eq!(projected.views[0].layout.hidden_staves[0].part, 1);
5963 assert_eq!(serde_json::to_value(&score).unwrap(), before);
5964
5965 let mut invalid = score.clone();
5966 invalid.views[0].parts.push(1);
5967 assert!(invalid.resolve_view("middle").is_err());
5968 invalid.views[0].parts.pop();
5969 invalid.views[0].layout.hidden_staves[0].part = 3;
5970 assert!(invalid.resolve_view("middle").is_err());
5971 invalid.views[0].layout.hidden_staves[0].part = 2;
5972 invalid.views[0].staff_kind_overrides[0].staff.staff = 9;
5973 assert!(invalid.resolve_view("middle").is_err());
5974 assert!(score.resolve_view("missing").is_err());
5975 }
5976
5977 #[test]
5978 fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
5979 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5980 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5981 lines: 6,
5982 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5983 capo: 0,
5984 });
5985 score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
5986 score.views.push(ScoreView::linked_tablature_staff(
5987 "guitar-tab",
5988 "Guitar Tab",
5989 0,
5990 0,
5991 ));
5992
5993 let projected = score.resolve_view("guitar-tab").expect("view resolves");
5994 assert_eq!(
5995 projected.parts[0].staves[0].presentation.kind,
5996 StaffKind::Tablature
5997 );
5998 assert_eq!(
5999 projected.views[0].staff_kind_overrides[0].staff,
6000 ViewStaffRef { part: 0, staff: 0 }
6001 );
6002 assert_eq!(
6003 score.parts[0].staves[0].presentation.kind,
6004 StaffKind::Standard
6005 );
6006 }
6007
6008 #[test]
6009 fn legacy_score_view_json_defaults_staff_kind_overrides() {
6010 let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
6011 score.views.push(ScoreView::linked_part("part", "Part", 0));
6012 let mut value = serde_json::to_value(score).expect("score serializes");
6013 value["views"][0]
6014 .as_object_mut()
6015 .expect("view is an object")
6016 .remove("staff_kind_overrides");
6017
6018 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
6019 assert!(restored.views[0].staff_kind_overrides.is_empty());
6020 }
6021
6022 #[test]
6023 fn typed_view_style_overrides_are_ordered_and_json_compatible() {
6024 let mut view = ScoreView::linked_part("part", "Part", 0);
6025 view.layout.typed_style_overrides = vec![
6026 ViewStyleOverride {
6027 property: ViewStyleProperty::TextScale,
6028 value: 0.9,
6029 },
6030 ViewStyleOverride {
6031 property: ViewStyleProperty::TextScale,
6032 value: 1.1,
6033 },
6034 ];
6035 assert_eq!(
6036 view.layout.style_value(ViewStyleProperty::TextScale),
6037 Some(1.1)
6038 );
6039 let restored: ScoreView =
6040 serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
6041 assert_eq!(
6042 restored.layout.typed_style_overrides,
6043 view.layout.typed_style_overrides
6044 );
6045 assert_eq!(view.layout.resolved_style().text_scale, 1.1);
6046 assert_eq!(view.layout.resolved_style().system_gap, 2.0);
6047 }
6048
6049 #[test]
6050 fn score_style_defaults_are_inherited_then_overridden_by_view() {
6051 let mut score = Score::new("Style", 120, 4, 4, 0, 1);
6052 score.style_overrides = vec![
6053 ViewStyleOverride {
6054 property: ViewStyleProperty::StaffSpace,
6055 value: 1.2,
6056 },
6057 ViewStyleOverride {
6058 property: ViewStyleProperty::TextScale,
6059 value: 0.9,
6060 },
6061 ];
6062 let mut view = ScoreView::linked_part("part", "Part", 0);
6063 view.layout.typed_style_overrides.push(ViewStyleOverride {
6064 property: ViewStyleProperty::TextScale,
6065 value: 1.1,
6066 });
6067 let style = score.resolved_view_style(&view.layout);
6068 assert_eq!(style.staff_space, 1.2);
6069 assert_eq!(style.text_scale, 1.1);
6070 let restored: Score =
6071 serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
6072 assert_eq!(restored.style_overrides, score.style_overrides);
6073 }
6074
6075 #[test]
6076 fn legacy_measure_json_defaults_source_voice_numbers() {
6077 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
6078 let mut value = serde_json::to_value(&score).expect("score serializes");
6079 value["parts"][0]["staves"][0]["measures"][0]
6080 .as_object_mut()
6081 .expect("measure is an object")
6082 .remove("source_voice_numbers");
6083
6084 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
6085 assert_eq!(
6086 restored.parts[0].staves[0].measures[0].source_voice_numbers,
6087 [None; 4]
6088 );
6089 }
6090
6091 #[test]
6092 fn legacy_measure_json_defaults_tempo_ramp() {
6093 let measure: Measure = serde_json::from_str(
6094 r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
6095 )
6096 .expect("legacy measure deserializes");
6097 assert_eq!(measure.tempo_ramp_to, None);
6098 }
6099
6100 #[test]
6101 fn note_without_new_percussion_fields_uses_serde_defaults() {
6102 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
6103 let mut value = serde_json::to_value(note).unwrap();
6104 let object = value.as_object_mut().unwrap();
6105 object.remove("is_unpitched");
6106 object.remove("instrument_id");
6107 let restored: Note = serde_json::from_value(value).unwrap();
6108 assert!(!restored.is_unpitched);
6109 assert_eq!(restored.instrument_id, None);
6110 }
6111
6112 #[test]
6113 fn percussion_instrument_resolution_prefers_id_then_display_key() {
6114 let mut part = Part::new("Drums", "Dr.");
6115 part.percussion_instruments = vec![
6116 PercussionInstrument {
6117 id: "snare".to_string(),
6118 name: Some("Acoustic Snare".to_string()),
6119 midi_unpitched: Some(38),
6120 staff_position: None,
6121 notehead: None,
6122 preferred_voice: None,
6123 techniques: Vec::new(),
6124 },
6125 PercussionInstrument {
6126 id: "rim".to_string(),
6127 name: Some("Side Stick".to_string()),
6128 midi_unpitched: Some(37),
6129 staff_position: None,
6130 notehead: None,
6131 preferred_voice: None,
6132 techniques: Vec::new(),
6133 },
6134 ];
6135 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
6136 note.is_unpitched = true;
6137 assert_eq!(
6138 part.percussion_instrument_for_note(¬e)
6139 .map(|instrument| instrument.id.as_str()),
6140 Some("snare")
6141 );
6142 note.instrument_id = Some("rim".to_string());
6143 assert_eq!(
6144 part.percussion_instrument_for_note(¬e)
6145 .map(|instrument| instrument.id.as_str()),
6146 Some("rim")
6147 );
6148 note.instrument_id = Some("missing".to_string());
6149 assert!(part.percussion_instrument_for_note(¬e).is_none());
6150 note.instrument_id = None;
6151 note.is_unpitched = false;
6152 assert!(part.percussion_instrument_for_note(¬e).is_none());
6153 }
6154
6155 #[test]
6156 fn percussion_kit_extensions_are_json_backward_compatible() {
6157 let mut instrument = PercussionInstrument {
6158 id: "snare".to_string(),
6159 name: Some("Acoustic Snare".to_string()),
6160 midi_unpitched: Some(38),
6161 staff_position: Some(0),
6162 notehead: Some(NoteHead::Cross),
6163 preferred_voice: Some(1),
6164 techniques: vec!["rim-shot".to_string()],
6165 };
6166 let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
6167 let object = value
6168 .as_object_mut()
6169 .expect("percussion instrument is an object");
6170 object.remove("staff_position");
6171 object.remove("notehead");
6172 object.remove("preferred_voice");
6173 object.remove("techniques");
6174 instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
6175 assert_eq!(instrument.staff_position, None);
6176 assert_eq!(instrument.notehead, None);
6177 assert_eq!(instrument.preferred_voice, None);
6178 assert!(instrument.techniques.is_empty());
6179 }
6180
6181 #[test]
6184 fn score_template_solo_has_one_part_treble() {
6185 let score = Score::template(ScoreTemplate::Solo);
6186 assert_eq!(score.parts.len(), 1);
6187 assert_eq!(score.parts[0].staves.len(), 1);
6188 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
6189 assert_eq!(score.parts[0].midi_program, 0);
6190 }
6191
6192 #[test]
6193 fn score_template_piano_has_two_staves() {
6194 let score = Score::template(ScoreTemplate::Piano);
6195 assert_eq!(score.parts.len(), 1);
6196 assert_eq!(score.parts[0].staves.len(), 2);
6197 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
6198 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
6199 }
6200
6201 #[test]
6202 fn score_template_string_quartet_has_four_parts() {
6203 let score = Score::template(ScoreTemplate::StringQuartet);
6204 assert_eq!(score.parts.len(), 4);
6205 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);
6208 assert_eq!(score.parts[3].midi_program, 42);
6209 }
6210
6211 #[test]
6212 fn score_template_string_orchestra_has_five_parts() {
6213 let score = Score::template(ScoreTemplate::StringOrchestra);
6214 assert_eq!(score.parts.len(), 5);
6215 assert_eq!(score.parts[4].midi_program, 43); }
6217
6218 #[test]
6219 fn score_template_brass_quintet_has_five_parts() {
6220 let score = Score::template(ScoreTemplate::BrassQuintet);
6221 assert_eq!(score.parts.len(), 5);
6222 assert_eq!(score.parts[2].midi_program, 60); }
6224
6225 #[test]
6226 fn score_template_default_measures_are_four() {
6227 let score = Score::template(ScoreTemplate::StringQuartet);
6228 for part in &score.parts {
6229 for staff in &part.staves {
6230 assert_eq!(staff.measures.len(), 4);
6231 }
6232 }
6233 }
6234
6235 #[test]
6238 fn measure_empty_has_no_breaks() {
6239 let m = Measure::empty(4, 4);
6240 assert!(!m.system_break);
6241 assert!(!m.page_break);
6242 assert!(!m.section_break);
6243 }
6244
6245 #[test]
6246 fn section_range_uses_semantic_boundaries_not_layout_breaks() {
6247 let mut score = Score::new("sections", 120, 4, 4, 0, 6);
6248 score.parts[0].staves[0].measures[0].section_break = true;
6249 score.parts[0].staves[0].measures[2].section_break = true;
6250 score.parts[0].staves[0].measures[4].section_break = true;
6251 score.parts[0].staves[0].measures[5].section_break = true;
6252 score.parts[0].staves[0].measures[1].system_break = true;
6253 score.parts[0].staves[0].measures[3].page_break = true;
6254 assert_eq!(score.section_range(0).unwrap(), 0..=1);
6255 assert_eq!(score.section_range(3).unwrap(), 2..=3);
6256 assert_eq!(score.section_range(4).unwrap(), 4..=4);
6257 assert_eq!(score.section_range(5).unwrap(), 5..=5);
6258 assert!(score.section_range(6).is_err());
6259 }
6260
6261 #[test]
6262 fn system_break_survives_json_roundtrip() {
6263 let mut m = Measure::empty(4, 4);
6264 m.system_break = true;
6265 let json = serde_json::to_string(&m).unwrap();
6266 let m2: Measure = serde_json::from_str(&json).unwrap();
6267 assert!(m2.system_break);
6268 assert!(!m2.page_break);
6269 }
6270
6271 #[test]
6274 fn diff_identical_scores_is_empty() {
6275 let s = Score::new("T", 120, 4, 4, 0, 2);
6276 assert!(diff(&s, &s).is_empty());
6277 }
6278
6279 #[test]
6280 fn score_patch_covers_measure_semantics_and_note_insert_index() {
6281 let mut a = Score::new("T", 120, 4, 4, 0, 1);
6282 a.parts[0].staves[0].measures[0].voices[0].clear();
6283 let mut b = a.clone();
6284 let measure = &mut b.parts[0].staves[0].measures[0];
6285 measure.key_sig = Some(KeySignature {
6286 fifths: -2,
6287 mode: "major".to_string(),
6288 });
6289 measure.time_sig = Some(TimeSignature {
6290 numerator: 3,
6291 denominator: 4,
6292 });
6293 measure.barline_left = Barline::RepeatStart;
6294 measure.barline_right = Barline::RepeatEnd;
6295 measure.rehearsal = Some("A".to_string());
6296 measure.volta = Some(VoltaBracket {
6297 number: 1,
6298 kind: "begin_end".to_string(),
6299 numbers: Vec::new(),
6300 });
6301 measure.texts.push(StyledText {
6302 position_beats: None,
6303 style: TextStyle::RehearsalMark,
6304 text: "A".to_string(),
6305 placement: None,
6306 offset_x: None,
6307 offset_y: None,
6308 relative_x: None,
6309 relative_y: None,
6310 });
6311 measure.figured_bass.push(FiguredBassFigure {
6312 number: "6".to_string(),
6313 alter: None,
6314 prefix: None,
6315 suffix: None,
6316 extender: false,
6317 });
6318 measure.harp_pedal_diagrams.push(HarpPedalDiagram {
6319 positions: [
6320 HarpPedalPosition::Flat,
6321 HarpPedalPosition::Natural,
6322 HarpPedalPosition::Sharp,
6323 HarpPedalPosition::Natural,
6324 HarpPedalPosition::Flat,
6325 HarpPedalPosition::Sharp,
6326 HarpPedalPosition::Natural,
6327 ],
6328 placement: Some("above".to_string()),
6329 });
6330 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
6331 let expected = b.parts[0].staves[0].measures[0].clone();
6332
6333 let patches = score_patch(&a, &b);
6334 assert!(
6335 patches
6336 .iter()
6337 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
6338 );
6339 assert!(
6340 patches
6341 .iter()
6342 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
6343 );
6344 assert!(
6345 patches
6346 .iter()
6347 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
6348 );
6349 assert!(
6350 patches
6351 .iter()
6352 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
6353 );
6354 assert!(
6355 patches
6356 .iter()
6357 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
6358 );
6359 assert!(
6360 patches
6361 .iter()
6362 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
6363 );
6364 assert!(
6365 patches
6366 .iter()
6367 .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
6368 );
6369 let result = apply_patch(&a, &patches).expect("patch application failed");
6370 let result_measure = &result.parts[0].staves[0].measures[0];
6371 assert_eq!(result_measure.key_sig, expected.key_sig);
6372 assert_eq!(result_measure.time_sig, expected.time_sig);
6373 assert_eq!(result_measure.barline_left, expected.barline_left);
6374 assert_eq!(result_measure.barline_right, expected.barline_right);
6375 assert_eq!(result_measure.rehearsal, expected.rehearsal);
6376 assert_eq!(result_measure.volta, expected.volta);
6377 assert_eq!(result_measure.texts, expected.texts);
6378 assert_eq!(result_measure.figured_bass, expected.figured_bass);
6379 assert_eq!(
6380 result_measure.harp_pedal_diagrams,
6381 expected.harp_pedal_diagrams
6382 );
6383 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
6384 }
6385
6386 #[test]
6387 fn malformed_note_patches_do_not_mutate_the_original_score() {
6388 let score = Score::new("Patch boundary", 120, 4, 4, 0, 1);
6389 let before = serde_json::to_value(&score).unwrap();
6390 let note = Box::new(Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
6391 for (part, staff, measure, voice, note_index) in [
6392 (8, 0, 0, 0, 0),
6393 (0, 8, 0, 0, 0),
6394 (0, 0, 8, 0, 0),
6395 (0, 0, 0, 8, 0),
6396 (0, 0, 0, 0, 99),
6397 ] {
6398 let patches = [
6399 ScorePatch::AddNote {
6400 part,
6401 staff,
6402 measure,
6403 voice,
6404 note_index,
6405 note: note.clone(),
6406 },
6407 ScorePatch::RemoveNote {
6408 part,
6409 staff,
6410 measure,
6411 voice,
6412 note_index,
6413 },
6414 ScorePatch::ReplaceNote {
6415 part,
6416 staff,
6417 measure,
6418 voice,
6419 note_index,
6420 note: note.clone(),
6421 },
6422 ];
6423 for patch in patches {
6424 assert!(apply_patch(&score, &[patch]).is_err());
6425 assert_eq!(serde_json::to_value(&score).unwrap(), before);
6426 }
6427 }
6428 }
6429
6430 #[test]
6431 fn measure_patches_reject_missing_part_staff_and_measure() {
6432 let score = Score::new("Patch boundary", 120, 4, 4, 0, 1);
6433 let before = serde_json::to_value(&score).unwrap();
6434 for (part, staff, measure) in [(8, 0, 0), (0, 8, 0), (0, 0, 8)] {
6435 let patches = vec![
6436 ScorePatch::SetMeasureTexts {
6437 part,
6438 staff,
6439 measure,
6440 value: vec![],
6441 },
6442 ScorePatch::SetFiguredBass {
6443 part,
6444 staff,
6445 measure,
6446 value: vec![],
6447 },
6448 ScorePatch::SetHarpPedalDiagrams {
6449 part,
6450 staff,
6451 measure,
6452 value: vec![],
6453 },
6454 ScorePatch::SetMeasureTablatureChange {
6455 part,
6456 staff,
6457 measure,
6458 value: None,
6459 },
6460 ScorePatch::SetMeasureMidClefs {
6461 part,
6462 staff,
6463 measure,
6464 value: vec![],
6465 },
6466 ScorePatch::SetKeySignature {
6467 part,
6468 staff,
6469 measure,
6470 value: None,
6471 },
6472 ScorePatch::SetTimeSignature {
6473 part,
6474 staff,
6475 measure,
6476 value: None,
6477 },
6478 ScorePatch::SetTimeSymbol {
6479 part,
6480 staff,
6481 measure,
6482 value: None,
6483 },
6484 ScorePatch::SetBarlines {
6485 part,
6486 staff,
6487 measure,
6488 left: Barline::default(),
6489 right: Barline::default(),
6490 },
6491 ScorePatch::SetRehearsal {
6492 part,
6493 staff,
6494 measure,
6495 value: None,
6496 },
6497 ScorePatch::SetVolta {
6498 part,
6499 staff,
6500 measure,
6501 value: None,
6502 },
6503 ScorePatch::SetMeasureTempo {
6504 part,
6505 staff,
6506 measure,
6507 value: None,
6508 },
6509 ScorePatch::SetMeasureTempoRamp {
6510 part,
6511 staff,
6512 measure,
6513 value: None,
6514 },
6515 ScorePatch::SetMeasureMetronomeMark {
6516 part,
6517 staff,
6518 measure,
6519 value: None,
6520 },
6521 ];
6522 for patch in patches {
6523 assert!(apply_patch(&score, &[patch]).is_err());
6524 assert_eq!(serde_json::to_value(&score).unwrap(), before);
6525 }
6526 }
6527 }
6528
6529 #[test]
6530 fn part_and_staff_patches_reject_missing_targets() {
6531 let score = Score::new("Patch targets", 120, 4, 4, 0, 1);
6532 let original = serde_json::to_value(&score).unwrap();
6533 let part = 99;
6534 let patches = vec![
6535 ScorePatch::SetPartNames {
6536 part,
6537 name: "Violin".into(),
6538 short_name: "Vln".into(),
6539 },
6540 ScorePatch::SetPartMidi {
6541 part,
6542 channel: 1,
6543 program: 40,
6544 },
6545 ScorePatch::SetInstrumentDefinition { part, value: None },
6546 ScorePatch::SetPartMidiAutomation {
6547 part,
6548 pitch_bends: vec![],
6549 control_changes: vec![],
6550 program_changes: vec![],
6551 aftertouch: vec![],
6552 },
6553 ];
6554 for patch in patches {
6555 assert!(apply_patch(&score, &[patch]).is_err());
6556 }
6557 for (part, staff) in [(99, 0), (0, 99)] {
6558 let patches = vec![
6559 ScorePatch::SetStaffConfiguration {
6560 part,
6561 staff,
6562 clef: Clef::Treble,
6563 transpose_semitones: 0,
6564 },
6565 ScorePatch::SetStaffPresentation {
6566 part,
6567 staff,
6568 value: StaffPresentation::default(),
6569 },
6570 ScorePatch::SetTablatureConfig {
6571 part,
6572 staff,
6573 value: None,
6574 },
6575 ];
6576 for patch in patches {
6577 assert!(apply_patch(&score, &[patch]).is_err());
6578 }
6579 }
6580 assert_eq!(serde_json::to_value(&score).unwrap(), original);
6581 }
6582
6583 #[test]
6584 fn diff_and_patch_preserve_score_level_texts() {
6585 let a = Score::new("T", 120, 4, 4, 0, 1);
6586 let mut b = a.clone();
6587 b.texts.push(StyledText {
6588 position_beats: None,
6589 style: TextStyle::Expression,
6590 text: "Prelude".to_string(),
6591 placement: Some("above".to_string()),
6592 offset_x: Some(12.0),
6593 offset_y: Some(-8.0),
6594 relative_x: None,
6595 relative_y: None,
6596 });
6597
6598 let changes = diff(&a, &b);
6599 assert!(changes.iter().any(|change| matches!(
6600 change,
6601 ScoreChange::ScoreTextChanged { old, new }
6602 if old.is_empty() && new == &b.texts
6603 )));
6604
6605 let patches = score_patch(&a, &b);
6606 assert!(patches.iter().any(|patch| matches!(
6607 patch,
6608 ScorePatch::SetScoreTexts { value } if value == &b.texts
6609 )));
6610 let result = apply_patch(&a, &patches).expect("score text patch failed");
6611 assert_eq!(result.texts, b.texts);
6612 }
6613
6614 #[test]
6615 fn mixed_editor_changes_round_trip_as_local_patches() {
6616 let mut before = Score::new("Before", 120, 4, 4, 0, 2);
6617 before.parts[0].staves[0].measures[0].voices[0] = vec![
6618 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6619 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
6620 ];
6621 before.parts[0].staves[0].measures[1].voices[0] = vec![
6622 Note::new(Pitch::new(Step::E, 4), Duration::Quarter),
6623 Note::new(Pitch::new(Step::F, 4), Duration::Quarter),
6624 ];
6625 let mut after = before.clone();
6626 after.metadata.title = "After".into();
6627 after.metadata.composer = "Composer".into();
6628 after.metadata.lyricist = "Lyricist".into();
6629 after.metadata.copyright = "Copyright".into();
6630 after.metadata.work_number = "Op. 1".into();
6631 after.metadata.movement_title = "Prelude".into();
6632 after.settings.tempo_bpm = 96;
6633 after.parts[0].name = "Piano".into();
6634 after.parts[0].short_name = "Pno.".into();
6635 after.parts[0].midi_channel = 1;
6636 after.parts[0].midi_program = 41;
6637 after.parts[0].midi_pitch_bends.push(MidiPitchBend {
6638 tick: 240,
6639 channel: 1,
6640 value: 512,
6641 });
6642 after.parts[0].staves[0].clef = Clef::Bass;
6643 after.parts[0].staves[0].measures[0].voices[0][0] =
6644 Note::new(Pitch::new(Step::G, 3), Duration::Quarter);
6645 after.parts[0].staves[0].measures[0].voices[0]
6646 .push(Note::new(Pitch::new(Step::A, 3), Duration::Quarter));
6647 after.parts[0].staves[0].measures[1].voices[0].pop();
6648
6649 let changes = diff(&before, &after);
6650 assert!(
6651 changes
6652 .iter()
6653 .any(|change| matches!(change, ScoreChange::PartMidiAutomationChanged { .. }))
6654 );
6655 assert!(
6656 changes
6657 .iter()
6658 .any(|change| matches!(change, ScoreChange::NoteModified { .. }))
6659 );
6660 assert!(
6661 changes
6662 .iter()
6663 .any(|change| matches!(change, ScoreChange::NoteAdded { .. }))
6664 );
6665 assert!(
6666 changes
6667 .iter()
6668 .any(|change| matches!(change, ScoreChange::NoteRemoved { .. }))
6669 );
6670 let patches = score_patch(&before, &after);
6671 assert!(
6672 !patches
6673 .iter()
6674 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6675 );
6676 let result = apply_patch(&before, &patches).expect("local patches apply");
6677 assert_eq!(
6678 serde_json::to_value(result).expect("patched score"),
6679 serde_json::to_value(after).expect("expected score")
6680 );
6681 }
6682
6683 #[test]
6684 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
6685 let a = Score::new("T", 120, 4, 4, 0, 1);
6686 let mut b = a.clone();
6687 let measure = &mut b.parts[0].staves[0].measures[0];
6688 measure.number = 8;
6689 measure.clef = Some(Clef::Bass);
6690 measure.tempo_text = Some("Allegro".to_string());
6691 measure.navigation = Some("D.S.".to_string());
6692 measure.expression_text = Some("espressivo".to_string());
6693 measure.multi_rest_count = Some(3);
6694 measure.system_break = true;
6695 measure.page_break = true;
6696
6697 let changes = diff(&a, &b);
6698 assert!(changes.iter().any(|change| matches!(
6699 change,
6700 ScoreChange::MeasurePresentationChanged {
6701 part: 0,
6702 staff: 0,
6703 measure: 0,
6704 old_number: 1,
6705 new_number: 8,
6706 old_clef: None,
6707 new_clef: Some(Clef::Bass),
6708 new_tempo_text: Some(text),
6709 new_navigation: Some(navigation),
6710 new_expression_text: Some(expression),
6711 new_multi_rest_count: Some(3),
6712 old_system_break: false,
6713 new_system_break: true,
6714 old_page_break: false,
6715 new_page_break: true,
6716 ..
6717 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
6718 )));
6719 assert!(!changes.iter().any(|change| matches!(
6720 change,
6721 ScoreChange::UnrepresentedFieldChanged { path }
6722 if path == "parts[0].staves[0].measures[0].number"
6723 )));
6724
6725 let patches = score_patch(&a, &b);
6726 assert!(patches.iter().any(|patch| matches!(
6727 patch,
6728 ScorePatch::SetMeasurePresentation {
6729 part: 0,
6730 staff: 0,
6731 measure: 0,
6732 number: 8,
6733 clef: Some(Clef::Bass),
6734 tempo_text: Some(text),
6735 navigation: Some(navigation),
6736 expression_text: Some(expression),
6737 multi_rest_count: Some(3),
6738 system_break: true,
6739 page_break: true,
6740 section_break: false,
6741 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
6742 )));
6743 assert!(
6744 !patches
6745 .iter()
6746 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6747 );
6748 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
6749 let decoded: Vec<ScorePatch> =
6750 serde_json::from_str(&json).expect("measure presentation patch should decode");
6751 let mut legacy_json: serde_json::Value =
6752 serde_json::from_str(&json).expect("patch JSON value");
6753 for patch in legacy_json.as_array_mut().expect("patch JSON array") {
6754 patch
6755 .as_object_mut()
6756 .expect("patch JSON object")
6757 .remove("section_break");
6758 }
6759 let legacy: Vec<ScorePatch> =
6760 serde_json::from_value(legacy_json).expect("legacy patch should decode");
6761 assert!(matches!(
6762 legacy.first(),
6763 Some(ScorePatch::SetMeasurePresentation {
6764 section_break: false,
6765 ..
6766 })
6767 ));
6768 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
6769 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
6770 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
6771 assert_eq!(
6772 serde_json::to_value(&result).expect("patched score JSON"),
6773 serde_json::to_value(&b).expect("expected score JSON")
6774 );
6775 }
6776
6777 #[test]
6778 fn chord_definition_changes_use_typed_diff_and_local_patch() {
6779 let a = Score::new("T", 120, 4, 4, 0, 1);
6780 let mut b = a.clone();
6781 b.chord_definitions.push(ChordDefinition {
6782 id: Some("c-major".to_string()),
6783 label: Some("C".to_string()),
6784 kind: Some("major".to_string()),
6785 fret_position: Some(0),
6786 tab_strings: Some("x32010".to_string()),
6787 tab_courses: None,
6788 members: Vec::new(),
6789 barres: Vec::new(),
6790 });
6791
6792 let changes = diff(&a, &b);
6793 assert!(changes.iter().any(|change| matches!(
6794 change,
6795 ScoreChange::ChordDefinitionsChanged { old, new }
6796 if old.is_empty() && new == &b.chord_definitions
6797 )));
6798 assert!(!changes.iter().any(|change| matches!(
6799 change,
6800 ScoreChange::UnrepresentedFieldChanged { path }
6801 if path == "chord_definitions"
6802 )));
6803
6804 let patches = score_patch(&a, &b);
6805 assert!(patches.iter().any(|patch| matches!(
6806 patch,
6807 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
6808 )));
6809 assert!(
6810 !patches
6811 .iter()
6812 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6813 );
6814 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
6815 let decoded: Vec<ScorePatch> =
6816 serde_json::from_str(&json).expect("chord definition patch should decode");
6817 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
6818 assert_eq!(result.chord_definitions, b.chord_definitions);
6819 }
6820
6821 #[test]
6822 fn part_name_changes_use_typed_diff_and_local_patch() {
6823 let a = Score::new("T", 120, 4, 4, 0, 1);
6824 let mut b = a.clone();
6825 b.parts[0].name = "Violin".to_string();
6826 b.parts[0].short_name = "Vln.".to_string();
6827 b.parts[0].midi_channel = 4;
6828 b.parts[0].midi_program = 40;
6829 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
6830 tick: 120,
6831 channel: 4,
6832 value: 2048,
6833 });
6834 b.parts[0].midi_control_changes.push(MidiControlChange {
6835 tick: 240,
6836 channel: 4,
6837 controller: 64,
6838 value: 127,
6839 });
6840 b.parts[0].midi_program_changes.push(MidiProgramChange {
6841 tick: 0,
6842 channel: 4,
6843 program: 40,
6844 });
6845 b.parts[0].midi_aftertouch.push(MidiAftertouch {
6846 tick: 360,
6847 channel: 4,
6848 key: Some(64),
6849 value: 80,
6850 });
6851 b.parts[0].staves[0].clef = Clef::Bass;
6852 b.parts[0].staves[0].transpose_semitones = -2;
6853
6854 let changes = diff(&a, &b);
6855 assert!(changes.iter().any(|change| matches!(
6856 change,
6857 ScoreChange::PartNamesChanged {
6858 part: 0,
6859 old_name,
6860 new_name,
6861 old_short_name,
6862 new_short_name,
6863 } if old_name == "Piano"
6864 && new_name == "Violin"
6865 && old_short_name == "Pno."
6866 && new_short_name == "Vln."
6867 )));
6868 assert!(changes.iter().any(|change| matches!(
6869 change,
6870 ScoreChange::PartMidiChanged {
6871 part: 0,
6872 old_channel: 0,
6873 new_channel: 4,
6874 old_program: 0,
6875 new_program: 40,
6876 }
6877 )));
6878 assert!(changes.iter().any(|change| matches!(
6879 change,
6880 ScoreChange::PartMidiAutomationChanged {
6881 part: 0,
6882 new_pitch_bends,
6883 new_control_changes,
6884 new_program_changes,
6885 new_aftertouch,
6886 ..
6887 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
6888 && new_control_changes == &b.parts[0].midi_control_changes
6889 && new_program_changes == &b.parts[0].midi_program_changes
6890 && new_aftertouch == &b.parts[0].midi_aftertouch
6891 )));
6892 assert!(changes.iter().any(|change| matches!(
6893 change,
6894 ScoreChange::StaffConfigurationChanged {
6895 part: 0,
6896 staff: 0,
6897 old_clef: Clef::Treble,
6898 new_clef: Clef::Bass,
6899 old_transpose_semitones: 0,
6900 new_transpose_semitones: -2,
6901 }
6902 )));
6903 assert!(!changes.iter().any(|change| matches!(
6904 change,
6905 ScoreChange::UnrepresentedFieldChanged { path }
6906 if path == "parts[0].name"
6907 )));
6908
6909 let patches = score_patch(&a, &b);
6910 assert!(patches.iter().any(|patch| matches!(
6911 patch,
6912 ScorePatch::SetPartNames {
6913 part: 0,
6914 name,
6915 short_name,
6916 } if name == "Violin" && short_name == "Vln."
6917 )));
6918 assert!(
6919 !patches
6920 .iter()
6921 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6922 );
6923 assert!(patches.iter().any(|patch| matches!(
6924 patch,
6925 ScorePatch::SetPartMidi {
6926 part: 0,
6927 channel: 4,
6928 program: 40,
6929 }
6930 )));
6931 assert!(patches.iter().any(|patch| matches!(
6932 patch,
6933 ScorePatch::SetPartMidiAutomation {
6934 part: 0,
6935 pitch_bends,
6936 control_changes,
6937 program_changes,
6938 aftertouch,
6939 } if pitch_bends == &b.parts[0].midi_pitch_bends
6940 && control_changes == &b.parts[0].midi_control_changes
6941 && program_changes == &b.parts[0].midi_program_changes
6942 && aftertouch == &b.parts[0].midi_aftertouch
6943 )));
6944 assert!(patches.iter().any(|patch| matches!(
6945 patch,
6946 ScorePatch::SetStaffConfiguration {
6947 part: 0,
6948 staff: 0,
6949 clef: Clef::Bass,
6950 transpose_semitones: -2,
6951 }
6952 )));
6953 let result = apply_patch(&a, &patches).expect("part name patch failed");
6954 assert_eq!(result.parts[0].name, b.parts[0].name);
6955 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
6956 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
6957 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
6958 assert_eq!(
6959 result.parts[0].midi_pitch_bends,
6960 b.parts[0].midi_pitch_bends
6961 );
6962 assert_eq!(
6963 result.parts[0].midi_control_changes,
6964 b.parts[0].midi_control_changes
6965 );
6966 assert_eq!(
6967 result.parts[0].midi_program_changes,
6968 b.parts[0].midi_program_changes
6969 );
6970 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
6971 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
6972 assert_eq!(
6973 result.parts[0].staves[0].transpose_semitones,
6974 b.parts[0].staves[0].transpose_semitones
6975 );
6976 }
6977
6978 #[test]
6979 fn diff_reports_measure_text_and_figured_bass_changes() {
6980 let a = Score::new("T", 120, 4, 4, 0, 1);
6981 let mut b = a.clone();
6982 let measure = &mut b.parts[0].staves[0].measures[0];
6983 measure.texts.push(StyledText {
6984 position_beats: None,
6985 style: TextStyle::Lyrics,
6986 text: "la".to_string(),
6987 placement: None,
6988 offset_x: None,
6989 offset_y: None,
6990 relative_x: None,
6991 relative_y: None,
6992 });
6993 measure.figured_bass.push(FiguredBassFigure {
6994 number: "6".to_string(),
6995 alter: None,
6996 prefix: None,
6997 suffix: None,
6998 extender: false,
6999 });
7000
7001 let changes = diff(&a, &b);
7002 assert!(changes.iter().any(|change| matches!(
7003 change,
7004 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
7005 if old.is_empty() && new.len() == 1
7006 )));
7007 assert!(changes.iter().any(|change| matches!(
7008 change,
7009 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
7010 if old.is_empty() && new.len() == 1
7011 )));
7012 }
7013
7014 #[test]
7015 fn diff_reports_tablature_changes_and_patches_them_locally() {
7016 let a = Score::new("T", 120, 4, 4, 0, 1);
7017 let mut b = a.clone();
7018 b.parts[0].staves[0].tablature = Some(TablatureConfig {
7019 lines: 6,
7020 tuning_midi: vec![40, 45, 50, 55, 59, 64],
7021 capo: 2,
7022 });
7023
7024 let changes = diff(&a, &b);
7025 assert!(!changes.iter().any(|change| matches!(
7026 change,
7027 ScoreChange::UnrepresentedFieldChanged { path }
7028 if path == "parts[0].staves[0].tablature"
7029 )));
7030 assert!(changes.iter().any(|change| matches!(
7031 change,
7032 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
7033 if config.lines == 6 && config.capo == 2
7034 )));
7035 let patches = score_patch(&a, &b);
7036 assert!(patches.iter().any(|patch| matches!(
7037 patch,
7038 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
7039 if config.lines == 6 && config.capo == 2
7040 )));
7041 assert_eq!(
7042 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
7043 b.parts[0].staves[0].tablature
7044 );
7045 }
7046
7047 #[test]
7048 fn diff_and_patch_preserve_hidden_time_signature() {
7049 let a = Score::new("Meter", 120, 4, 4, 0, 1);
7050 let mut b = a.clone();
7051 b.parts[0].staves[0].measures[0].time_sig_hidden = true;
7052 assert!(diff(&a, &b).iter().any(|change| matches!(
7053 change,
7054 ScoreChange::TimeVisibilityChanged {
7055 old: false,
7056 new: true,
7057 ..
7058 }
7059 )));
7060 let patch = score_patch(&a, &b);
7061 assert!(matches!(
7062 patch.as_slice(),
7063 [ScorePatch::SetTimeVisibility { hidden: true, .. }]
7064 ));
7065 assert!(apply_patch(&a, &patch).unwrap().parts[0].staves[0].measures[0].time_sig_hidden);
7066 }
7067
7068 #[test]
7069 fn diff_and_patch_preserve_common_time_symbol() {
7070 let a = Score::new("Meter", 120, 4, 4, 0, 1);
7071 let mut b = a.clone();
7072 b.parts[0].staves[0].measures[0].time_symbol = Some(TimeSymbol::Common);
7073 assert!(diff(&a, &b).iter().any(|change| matches!(
7074 change,
7075 ScoreChange::TimeSymbolChanged {
7076 part: 0,
7077 staff: 0,
7078 measure: 0,
7079 new: Some(TimeSymbol::Common),
7080 ..
7081 }
7082 )));
7083 let patches = score_patch(&a, &b);
7084 assert!(patches.iter().any(|patch| matches!(
7085 patch,
7086 ScorePatch::SetTimeSymbol {
7087 part: 0,
7088 staff: 0,
7089 measure: 0,
7090 value: Some(TimeSymbol::Common)
7091 }
7092 )));
7093 assert_eq!(
7094 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[0].time_symbol,
7095 Some(TimeSymbol::Common)
7096 );
7097 }
7098
7099 #[test]
7100 fn diff_and_patch_preserve_written_metronome_marks() {
7101 let a = Score::new("T", 120, 4, 4, 0, 1);
7102 let mut b = a.clone();
7103 b.parts[0].staves[0].measures[0].metronome_mark = Some(MetronomeMark {
7104 beat_unit: Duration::Quarter,
7105 beat_unit_dots: 0,
7106 per_minute: 69,
7107 parentheses: true,
7108 });
7109 let changes = diff(&a, &b);
7110 assert!(changes.iter().any(|change| matches!(
7111 change,
7112 ScoreChange::MeasureMetronomeMarkChanged {
7113 part: 0,
7114 staff: 0,
7115 measure: 0,
7116 ..
7117 }
7118 )));
7119 let patches = score_patch(&a, &b);
7120 assert!(patches.iter().any(|patch| matches!(
7121 patch,
7122 ScorePatch::SetMeasureMetronomeMark {
7123 part: 0,
7124 staff: 0,
7125 measure: 0,
7126 ..
7127 }
7128 )));
7129 assert_eq!(
7130 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[0].metronome_mark,
7131 b.parts[0].staves[0].measures[0].metronome_mark
7132 );
7133 }
7134
7135 #[test]
7136 fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
7137 let mut a = Score::new("T", 120, 4, 4, 0, 2);
7138 a.parts[0].staves[0].tablature = Some(TablatureConfig {
7139 lines: 6,
7140 tuning_midi: vec![40, 45, 50, 55, 59, 64],
7141 capo: 0,
7142 });
7143 let mut b = a.clone();
7144 b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
7145 lines: 6,
7146 tuning_midi: vec![40, 45, 50, 55, 59, 64],
7147 capo: 3,
7148 });
7149
7150 let changes = diff(&a, &b);
7151 assert!(changes.iter().any(|change| matches!(
7152 change,
7153 ScoreChange::TablatureChangeChanged {
7154 part: 0,
7155 staff: 0,
7156 measure: 1,
7157 old: None,
7158 new: Some(config),
7159 } if config.capo == 3
7160 )));
7161 let patches = score_patch(&a, &b);
7162 assert!(patches.iter().any(|patch| matches!(
7163 patch,
7164 ScorePatch::SetMeasureTablatureChange {
7165 part: 0,
7166 staff: 0,
7167 measure: 1,
7168 value: Some(config),
7169 } if config.capo == 3
7170 )));
7171 assert_eq!(
7172 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
7173 b.parts[0].staves[0].measures[1].tablature_change
7174 );
7175 }
7176
7177 #[test]
7178 fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
7179 let a = Score::new("Ramp", 120, 4, 4, 0, 2);
7180 let mut b = a.clone();
7181 b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
7182
7183 assert!(diff(&a, &b).iter().any(|change| matches!(
7184 change,
7185 ScoreChange::MeasureTempoRampChanged {
7186 part: 0,
7187 staff: 0,
7188 measure: 1,
7189 old: None,
7190 new: Some(72),
7191 }
7192 )));
7193 let patches = score_patch(&a, &b);
7194 assert!(patches.iter().any(|patch| matches!(
7195 patch,
7196 ScorePatch::SetMeasureTempoRamp {
7197 part: 0,
7198 staff: 0,
7199 measure: 1,
7200 value: Some(72),
7201 }
7202 )));
7203 assert_eq!(
7204 apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
7205 .tempo_ramp_to,
7206 b.parts[0].staves[0].measures[1].tempo_ramp_to
7207 );
7208 }
7209
7210 #[test]
7211 fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
7212 let a = Score::new("T", 120, 4, 4, 0, 1);
7213 let mut b = a.clone();
7214 b.parts[0].staves[0].presentation = StaffPresentation {
7215 kind: StaffKind::Percussion,
7216 lines: 1,
7217 line_distance: 1.5,
7218 small: true,
7219 cutaway: true,
7220 visible: false,
7221 notehead_scheme: StaffNoteheadScheme::PitchNames,
7222 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
7223 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
7224 };
7225
7226 let changes = diff(&a, &b);
7227 assert!(changes.iter().any(|change| matches!(
7228 change,
7229 ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
7230 if new == &b.parts[0].staves[0].presentation
7231 )));
7232 let patches = score_patch(&a, &b);
7233 assert!(patches.iter().any(|patch| matches!(
7234 patch,
7235 ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
7236 if value == &b.parts[0].staves[0].presentation
7237 )));
7238 assert!(
7239 !patches
7240 .iter()
7241 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
7242 );
7243 assert_eq!(
7244 apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
7245 b.parts[0].staves[0].presentation
7246 );
7247 }
7248
7249 #[test]
7250 fn score_patch_uses_local_presentation_patch_for_display_fields() {
7251 let a = Score::new("T", 120, 4, 4, 0, 1);
7252 let mut b = a.clone();
7253 b.parts[0].name = "Piano".to_string();
7254 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
7255 let patches = score_patch(&a, &b);
7256 assert!(patches.iter().any(|patch| matches!(
7257 patch,
7258 ScorePatch::SetMeasurePresentation {
7259 part: 0,
7260 staff: 0,
7261 measure: 0,
7262 expression_text: Some(text),
7263 ..
7264 } if text == "dolce"
7265 )));
7266 assert!(
7267 !patches
7268 .iter()
7269 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
7270 );
7271 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
7272 assert_eq!(result.parts[0].name, "Piano");
7273 assert_eq!(
7274 result.parts[0].staves[0].measures[0].expression_text,
7275 Some("dolce".to_string())
7276 );
7277 }
7278
7279 #[test]
7280 fn diff_detects_tempo_change() {
7281 let a = Score::new("T", 120, 4, 4, 0, 1);
7282 let mut b = a.clone();
7283 b.settings.tempo_bpm = 90;
7284 let changes = diff(&a, &b);
7285 assert_eq!(changes.len(), 1);
7286 assert!(matches!(
7287 changes[0],
7288 ScoreChange::TempoChanged { old: 120, new: 90 }
7289 ));
7290 }
7291
7292 #[test]
7293 fn diff_detects_title_change() {
7294 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
7295 let mut b = a.clone();
7296 b.metadata.title = "New Title".to_string();
7297 let changes = diff(&a, &b);
7298 assert!(
7299 changes.iter().any(
7300 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
7301 )
7302 );
7303 }
7304
7305 #[test]
7306 fn diff_detects_note_modification() {
7307 let mut a = Score::new("T", 120, 4, 4, 0, 1);
7308 a.parts[0].staves[0].measures[0].voices[0] =
7309 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
7310 let mut b = a.clone();
7311 b.parts[0].staves[0].measures[0].voices[0][0] =
7312 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
7313 let changes = diff(&a, &b);
7314 assert!(
7315 changes
7316 .iter()
7317 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
7318 );
7319 }
7320
7321 #[test]
7322 fn diff_detects_part_added() {
7323 let a = Score::new("T", 120, 4, 4, 0, 1);
7324 let mut b = a.clone();
7325 let mut p = Part::new("Violin", "Vln.");
7326 p.staves.push(Staff::new(Clef::Treble));
7327 b.parts.push(p);
7328 let changes = diff(&a, &b);
7329 assert!(
7330 changes
7331 .iter()
7332 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
7333 );
7334 }
7335
7336 #[test]
7337 fn diff_detects_measure_tempo_change() {
7338 let a = Score::new("T", 120, 4, 4, 0, 2);
7339 let mut b = a.clone();
7340 b.parts[0].staves[0].measures[1].tempo = Some(60);
7341 let changes = diff(&a, &b);
7342 assert!(changes.iter().any(|c| matches!(
7343 c,
7344 ScoreChange::MeasureTempoChanged {
7345 measure: 1,
7346 old: None,
7347 new: Some(60),
7348 ..
7349 }
7350 )));
7351 }
7352
7353 #[test]
7354 fn diff_detects_barline_change() {
7355 use crate::model::notation::Barline;
7356 let a = Score::new("T", 120, 4, 4, 0, 2);
7357 let mut b = a.clone();
7358 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
7359 let changes = diff(&a, &b);
7360 assert!(
7361 changes
7362 .iter()
7363 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
7364 );
7365 }
7366
7367 #[test]
7368 fn diff_detects_rehearsal_change() {
7369 let a = Score::new("T", 120, 4, 4, 0, 2);
7370 let mut b = a.clone();
7371 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
7372 let changes = diff(&a, &b);
7373 assert!(
7374 changes
7375 .iter()
7376 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
7377 );
7378 }
7379
7380 #[test]
7381 fn diff_detects_volta_change() {
7382 use super::VoltaBracket;
7383 let a = Score::new("T", 120, 4, 4, 0, 2);
7384 let mut b = a.clone();
7385 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
7386 number: 1,
7387 kind: "begin_end".into(),
7388 numbers: Vec::new(),
7389 });
7390 let changes = diff(&a, &b);
7391 assert!(
7392 changes
7393 .iter()
7394 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
7395 );
7396 }
7397
7398 #[test]
7399 fn diff_detects_key_signature_change() {
7400 let a = Score::new("T", 120, 4, 4, 0, 1);
7401 let mut b = a.clone();
7402 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
7404 assert!(
7405 changes
7406 .iter()
7407 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
7408 );
7409 }
7410
7411 #[test]
7412 fn measure_key_signature_uses_typed_local_patch() {
7413 let a = Score::new("T", 120, 4, 4, 0, 1);
7414 let mut b = a.clone();
7415 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
7416 fifths: 2,
7417 mode: "major".to_string(),
7418 });
7419 let changes = diff(&a, &b);
7420 assert!(!changes.iter().any(|change| matches!(
7421 change,
7422 ScoreChange::UnrepresentedFieldChanged { path }
7423 if path == "parts[0].staves[0].measures[0].key_sig"
7424 )));
7425 let patches = score_patch(&a, &b);
7426 assert!(patches.iter().any(|patch| matches!(
7427 patch,
7428 ScorePatch::SetKeySignature {
7429 part: 0,
7430 staff: 0,
7431 measure: 0,
7432 value: Some(KeySignature { fifths: 2, .. }),
7433 }
7434 )));
7435 assert!(
7436 !patches
7437 .iter()
7438 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
7439 );
7440 }
7441
7442 #[test]
7443 fn diff_same_key_signature_no_change() {
7444 let a = Score::new("T", 120, 4, 4, 2, 1);
7445 let changes = diff(&a, &a);
7446 assert!(changes.is_empty());
7447 }
7448
7449 #[test]
7450 fn score_duration_secs_region_partial() {
7451 use super::score_duration_secs_region;
7452 let score = Score::new("T", 120, 4, 4, 0, 4);
7454 let secs = score_duration_secs_region(&score, (1, 2));
7455 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
7456 }
7457
7458 #[test]
7459 fn score_duration_secs_region_single_measure() {
7460 use super::score_duration_secs_region;
7461 let score = Score::new("T", 120, 4, 4, 0, 4);
7463 let secs = score_duration_secs_region(&score, (0, 0));
7464 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
7465 }
7466
7467 #[test]
7470 fn measure_beats_remaining_empty_voice_returns_full() {
7471 use super::measure_beats_remaining;
7472 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7473 score.parts[0].staves[0].measures[0].voices[0].clear();
7474 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7475 assert!(
7476 (rem - 4.0).abs() < 1e-9,
7477 "expected 4.0 remaining, got {rem}"
7478 );
7479 }
7480
7481 #[test]
7482 fn measure_beats_remaining_half_full_returns_half() {
7483 use super::measure_beats_remaining;
7484 use crate::model::pitch::Step;
7485 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7486 score.parts[0].staves[0].measures[0].voices[0] = vec![
7487 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
7488 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
7489 ];
7490 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7491 assert!(
7492 (rem - 2.0).abs() < 1e-9,
7493 "expected 2.0 remaining, got {rem}"
7494 );
7495 }
7496
7497 #[test]
7498 fn measure_beats_remaining_full_voice_returns_zero() {
7499 use super::measure_beats_remaining;
7500 use crate::model::pitch::Step;
7501 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7502 score.parts[0].staves[0].measures[0].voices[0] =
7503 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7504 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7505 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
7506 }
7507
7508 #[test]
7509 fn measure_beats_remaining_tuplet_accounting() {
7510 use super::measure_beats_remaining;
7511 use crate::model::notation::TupletInfo;
7512 use crate::model::pitch::Step;
7513 let mut score = Score::new("T", 120, 4, 4, 0, 1);
7515 let tuplet = TupletInfo {
7516 actual_notes: 3,
7517 normal_notes: 2,
7518 };
7519 let mk = |step| {
7520 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
7521 n.tuplet = Some(tuplet.clone());
7522 n
7523 };
7524 score.parts[0].staves[0].measures[0].voices[0] =
7525 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
7526 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
7527 assert!(
7528 (rem - 2.0).abs() < 1e-9,
7529 "expected 2.0 remaining (triplets used 2.0), got {rem}"
7530 );
7531 }
7532
7533 #[test]
7534 fn measure_beats_remaining_out_of_range_returns_err() {
7535 use super::measure_beats_remaining;
7536 let score = Score::new("T", 120, 4, 4, 0, 1);
7537 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
7538 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
7539 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
7540 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
7541 }
7542
7543 #[test]
7544 fn note_content_eq_ignores_id() {
7545 use crate::model::pitch::Step;
7546 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
7547 let mut b = a.clone();
7548 b.id = "different-id".to_string();
7549 assert!(note_content_eq(&a, &b));
7550 b.pitches[0] = Pitch::new(Step::D, 4);
7552 assert!(!note_content_eq(&a, &b));
7553 let mut c = a.clone();
7555 a.stem_up = Some(true);
7556 c.stem_up = Some(false);
7557 assert!(!note_content_eq(&a, &c));
7558 }
7559
7560 #[test]
7561 fn suggested_stem_up_below_middle() {
7562 use crate::model::notation::Clef;
7563 let pitches = vec![Pitch::new(Step::C, 4)];
7565 assert!(suggested_stem_up(&pitches, &Clef::Treble));
7566 }
7567
7568 #[test]
7569 fn suggested_stem_up_above_middle() {
7570 use crate::model::notation::Clef;
7571 let pitches = vec![Pitch::new(Step::G, 5)];
7573 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
7574 }
7575
7576 #[test]
7577 fn suggested_stem_up_at_middle_line() {
7578 use crate::model::notation::Clef;
7579 let pitches = vec![Pitch::new(Step::B, 4)];
7581 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
7582 }
7583
7584 #[test]
7585 fn suggested_stem_up_chord() {
7586 use crate::model::notation::Clef;
7587 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
7589 assert!(suggested_stem_up(&pitches, &Clef::Treble));
7590 }
7591
7592 #[test]
7593 fn suggested_stem_up_bass_clef() {
7594 use crate::model::notation::Clef;
7595 let pitches = vec![Pitch::new(Step::D, 3)];
7597 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
7598 let pitches2 = vec![Pitch::new(Step::C, 3)];
7600 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
7601 }
7602
7603 #[test]
7604 fn suggested_stem_up_empty_pitches() {
7605 use crate::model::notation::Clef;
7606 assert!(suggested_stem_up(&[], &Clef::Treble));
7607 }
7608
7609 fn eighth(pitch: Pitch) -> Note {
7610 Note::new(pitch, Duration::Eighth)
7611 }
7612 fn quarter(pitch: Pitch) -> Note {
7613 Note::new(pitch, Duration::Quarter)
7614 }
7615 fn rest_eighth() -> Note {
7616 Note::rest(Duration::Eighth)
7617 }
7618
7619 #[test]
7620 fn compute_beams_4_4_four_eighths() {
7621 use crate::model::notation::{Clef, TimeSignature};
7622 let _ = Clef::Treble; let ts = TimeSignature {
7624 numerator: 4,
7625 denominator: 4,
7626 };
7627 let c4 = Pitch::new(Step::C, 4);
7628 let notes = vec![
7629 eighth(c4.clone()),
7630 eighth(c4.clone()),
7631 eighth(c4.clone()),
7632 eighth(c4.clone()),
7633 ];
7634 let beams = compute_beams(¬es, &ts);
7635 assert_eq!(beams[0], BeamState::Begin);
7637 assert_eq!(beams[1], BeamState::End);
7638 assert_eq!(beams[2], BeamState::Begin);
7639 assert_eq!(beams[3], BeamState::End);
7640 }
7641
7642 #[test]
7643 fn compute_beams_4_4_all_eighth_one_group() {
7644 use crate::model::notation::TimeSignature;
7645 let ts = TimeSignature {
7646 numerator: 4,
7647 denominator: 4,
7648 };
7649 let c4 = Pitch::new(Step::C, 4);
7650 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
7652 let beams = compute_beams(¬es, &ts);
7653 assert_eq!(beams[0], BeamState::Begin);
7654 assert_eq!(beams[1], BeamState::End);
7655 }
7656
7657 #[test]
7658 fn compute_beams_quarter_not_beamed() {
7659 use crate::model::notation::TimeSignature;
7660 let ts = TimeSignature {
7661 numerator: 4,
7662 denominator: 4,
7663 };
7664 let c4 = Pitch::new(Step::C, 4);
7665 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
7666 let beams = compute_beams(¬es, &ts);
7667 assert_eq!(beams[0], BeamState::None);
7668 assert_eq!(beams[1], BeamState::None);
7669 }
7670
7671 #[test]
7672 fn compute_beams_rest_breaks_beam() {
7673 use crate::model::notation::TimeSignature;
7674 let ts = TimeSignature {
7675 numerator: 4,
7676 denominator: 4,
7677 };
7678 let c4 = Pitch::new(Step::C, 4);
7679 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
7680 let beams = compute_beams(¬es, &ts);
7681 assert_eq!(beams[0], BeamState::None);
7683 assert_eq!(beams[1], BeamState::None);
7684 assert_eq!(beams[2], BeamState::None);
7685 }
7686
7687 #[test]
7688 fn compute_beams_6_8_compound() {
7689 use crate::model::notation::TimeSignature;
7690 let ts = TimeSignature {
7691 numerator: 6,
7692 denominator: 8,
7693 };
7694 let c4 = Pitch::new(Step::C, 4);
7695 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
7697 let beams = compute_beams(¬es, &ts);
7698 assert_eq!(beams[0], BeamState::Begin);
7699 assert_eq!(beams[1], BeamState::Continue);
7700 assert_eq!(beams[2], BeamState::End);
7701 assert_eq!(beams[3], BeamState::Begin);
7702 assert_eq!(beams[4], BeamState::Continue);
7703 assert_eq!(beams[5], BeamState::End);
7704 }
7705
7706 #[test]
7707 fn compute_beams_single_eighth() {
7708 use crate::model::notation::TimeSignature;
7709 let ts = TimeSignature {
7710 numerator: 4,
7711 denominator: 4,
7712 };
7713 let c4 = Pitch::new(Step::C, 4);
7714 let notes = vec![eighth(c4.clone())];
7715 let beams = compute_beams(¬es, &ts);
7716 assert_eq!(beams[0], BeamState::None);
7717 }
7718
7719 #[test]
7720 fn compute_beams_stops_fifteen_in_eight_tuplet_at_the_beat_boundary() {
7721 let ts = TimeSignature {
7722 numerator: 2,
7723 denominator: 4,
7724 };
7725 let pitch = Pitch::new(Step::C, 4);
7726 let mut tuplet_note = Note::new(pitch.clone(), Duration::ThirtySecond);
7727 tuplet_note.tuplet = Some(TupletInfo {
7728 actual_notes: 15,
7729 normal_notes: 8,
7730 });
7731 let mut notes = vec![tuplet_note; 15];
7732 notes.extend([eighth(pitch.clone()), eighth(pitch)]);
7733 let original = serde_json::to_value(¬es).unwrap();
7734
7735 let beams = compute_beams(¬es, &ts);
7736 let mut expected = vec![BeamState::Begin];
7737 expected.extend([BeamState::Continue; 13]);
7738 expected.extend([BeamState::End, BeamState::Begin, BeamState::End]);
7739 assert_eq!(beams, expected);
7740 assert_eq!(
7741 serde_json::to_value(¬es).unwrap(),
7742 original,
7743 "beam inference must not change notation or timing"
7744 );
7745 assert!((notes[..15].iter().map(Note::beats).sum::<f64>() - 1.0).abs() < 1e-12);
7746 }
7747}
7748
7749#[cfg(test)]
7750mod mid_clef_tests {
7751 use super::*;
7752 use crate::{Command, JoinMeasuresCmd, ScoreEngine, SetMidMeasureClefsCmd, SplitMeasureCmd};
7753
7754 fn quarter_notes() -> Vec<Note> {
7755 [Step::C, Step::D, Step::E, Step::F]
7756 .into_iter()
7757 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter))
7758 .collect()
7759 }
7760
7761 fn change(beats: f64, clef: Clef) -> MidMeasureClef {
7762 MidMeasureClef {
7763 offset: MeasureLength::from_beats(beats).unwrap(),
7764 clef,
7765 }
7766 }
7767
7768 #[test]
7769 fn measure_length_from_beats_is_exact_for_tuplet_subdivisions() {
7770 let third = MeasureLength::from_beats(1.0 / 3.0).unwrap();
7771 assert_eq!((third.numerator, third.denominator), (1, 12));
7772 let two = MeasureLength::from_beats(2.0).unwrap();
7773 assert_eq!((two.numerator, two.denominator), (1, 2));
7774 assert!(MeasureLength::from_beats(0.0).is_none());
7775 }
7776
7777 #[test]
7778 fn clef_at_follows_mid_bar_changes_and_json_omits_an_empty_list() {
7779 let mut measure = Measure::empty(4, 4);
7780 let json = serde_json::to_string(&measure).unwrap();
7781 assert!(!json.contains("mid_clefs"));
7782 measure.mid_clefs = vec![change(2.0, Clef::Bass)];
7783 assert_eq!(measure.clef_at(&Clef::Treble, 1.0), Clef::Treble);
7784 assert_eq!(measure.clef_at(&Clef::Treble, 2.0), Clef::Bass);
7785 let restored: Measure =
7786 serde_json::from_str(&serde_json::to_string(&measure).unwrap()).unwrap();
7787 assert_eq!(restored.mid_clefs, measure.mid_clefs);
7788 }
7789
7790 #[test]
7791 fn validation_rejects_mid_bar_clefs_outside_the_bar_or_out_of_order() {
7792 let mut score = Score::new("clefs", 120, 4, 4, 0, 1);
7793 score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
7794 score.parts[0].staves[0].measures[0].mid_clefs =
7795 vec![change(2.0, Clef::Bass), change(1.0, Clef::Treble)];
7796 let errors = crate::validate(&score).errors;
7797 assert!(errors.iter().any(|error| matches!(
7798 error,
7799 crate::ValidationError::InvalidMidMeasureClef { index: 1, .. }
7800 )));
7801 score.parts[0].staves[0].measures[0].mid_clefs = vec![change(4.0, Clef::Bass)];
7802 assert!(!crate::validate(&score).errors.is_empty());
7803 }
7804
7805 #[test]
7806 fn command_split_join_diff_and_patch_keep_mid_bar_clefs() {
7807 let mut engine = ScoreEngine::new();
7808 engine.score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
7809 engine
7810 .apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
7811 part_index: 0,
7812 staff_index: 0,
7813 measure_index: 0,
7814 clefs: vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)],
7815 }))
7816 .unwrap();
7817 let with_clefs = engine.score.clone();
7818 let plain = {
7819 let mut score = with_clefs.clone();
7820 score.parts[0].staves[0].measures[0].mid_clefs.clear();
7821 score
7822 };
7823 assert!(diff(&plain, &with_clefs).iter().any(|change| matches!(
7824 change,
7825 ScoreChange::MidMeasureClefsChanged { measure: 0, .. }
7826 )));
7827 let patched = apply_patch(&plain, &score_patch(&plain, &with_clefs)).unwrap();
7828 assert_eq!(
7829 patched.parts[0].staves[0].measures[0].mid_clefs,
7830 with_clefs.parts[0].staves[0].measures[0].mid_clefs
7831 );
7832
7833 engine
7834 .apply(Command::SplitMeasure(SplitMeasureCmd {
7835 measure_index: 0,
7836 split_at_beats: 1.0,
7837 }))
7838 .unwrap();
7839 let measures = &engine.score.parts[0].staves[0].measures;
7840 assert!(measures[0].mid_clefs.is_empty());
7841 assert_eq!(measures[1].clef, Some(Clef::Bass));
7842 assert_eq!(measures[1].mid_clefs, vec![change(2.0, Clef::Alto)]);
7843
7844 engine
7845 .apply(Command::JoinMeasures(JoinMeasuresCmd { measure_index: 0 }))
7846 .unwrap();
7847 let measure = &engine.score.parts[0].staves[0].measures[0];
7848 assert_eq!(
7849 measure.mid_clefs,
7850 vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)]
7851 );
7852
7853 let invalid = engine.apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
7854 part_index: 0,
7855 staff_index: 0,
7856 measure_index: 0,
7857 clefs: vec![change(0.0 + 5.0, Clef::Bass)],
7858 }));
7859 assert!(invalid.is_err());
7860 }
7861}
7862
7863#[cfg(test)]
7864mod percussion_key_tests {
7865 use super::*;
7866
7867 #[test]
7868 fn unpitched_chord_members_sound_their_kit_keys_by_display_position() {
7869 let mut part = Part::new("Drums", "Dr.");
7870 part.midi_channel = 9;
7871 let kit = |id: &str, key: u8, display: Pitch| PercussionInstrument {
7872 id: id.into(),
7873 name: None,
7874 midi_unpitched: Some(key),
7875 staff_position: Some(PercussionInstrument::display_position(&display)),
7876 notehead: None,
7877 preferred_voice: None,
7878 techniques: Vec::new(),
7879 };
7880 part.percussion_instruments = vec![
7881 kit("kick", 35, Pitch::new(Step::F, 4)),
7882 kit("snare", 38, Pitch::new(Step::C, 5)),
7883 ];
7884 let mut note = Note::new(Pitch::new(Step::C, 5), Duration::Quarter);
7885 note.pitches.push(Pitch::new(Step::F, 4));
7886 note.is_unpitched = true;
7887 note.instrument_id = Some("snare".into());
7888 assert_eq!(part.percussion_key(¬e, 0), Some(38));
7889 assert_eq!(part.percussion_key(¬e, 1), Some(35));
7890 note.is_unpitched = false;
7891 assert_eq!(part.percussion_key(¬e, 0), None);
7892 assert_eq!(
7893 PercussionInstrument::display_position(&Pitch::new(Step::B, 4)),
7894 0
7895 );
7896 }
7897}