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