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