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