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 >= 4.0 {
2766 Duration::Whole
2767 } else if beats >= 2.0 {
2768 Duration::Half
2769 } else if beats >= 1.0 {
2770 Duration::Quarter
2771 } else if beats >= 0.5 {
2772 Duration::Eighth
2773 } else if beats >= 0.25 {
2774 Duration::Sixteenth
2775 } else if beats >= 0.125 {
2776 Duration::ThirtySecond
2777 } else if beats >= 0.0625 {
2778 Duration::SixtyFourth
2779 } else {
2780 Duration::HundredTwentyEighth
2781 }
2782 }
2783}
2784
2785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2789pub struct NoteAddr {
2790 pub part: usize,
2791 pub staff: usize,
2792 pub measure: usize,
2793 pub voice: usize,
2794 pub note: usize,
2795}
2796
2797#[derive(Debug, Clone, Serialize, Deserialize)]
2801pub enum ScoreChange {
2802 ScoreViewsChanged {
2803 old: Vec<ScoreView>,
2804 new: Vec<ScoreView>,
2805 },
2806 ScoreTextChanged {
2807 old: Vec<StyledText>,
2808 new: Vec<StyledText>,
2809 },
2810 MeasureTextChanged {
2811 part: usize,
2812 staff: usize,
2813 measure: usize,
2814 old: Vec<StyledText>,
2815 new: Vec<StyledText>,
2816 },
2817 FiguredBassChanged {
2818 part: usize,
2819 staff: usize,
2820 measure: usize,
2821 old: Vec<FiguredBassFigure>,
2822 new: Vec<FiguredBassFigure>,
2823 },
2824 HarpPedalDiagramsChanged {
2825 part: usize,
2826 staff: usize,
2827 measure: usize,
2828 old: Vec<HarpPedalDiagram>,
2829 new: Vec<HarpPedalDiagram>,
2830 },
2831 ChordDefinitionsChanged {
2832 old: Vec<ChordDefinition>,
2833 new: Vec<ChordDefinition>,
2834 },
2835 PartNamesChanged {
2836 part: usize,
2837 old_name: String,
2838 new_name: String,
2839 old_short_name: String,
2840 new_short_name: String,
2841 },
2842 PartMidiChanged {
2843 part: usize,
2844 old_channel: u8,
2845 new_channel: u8,
2846 old_program: u8,
2847 new_program: u8,
2848 },
2849 InstrumentDefinitionChanged {
2850 part: usize,
2851 old: Option<InstrumentDefinition>,
2852 new: Option<InstrumentDefinition>,
2853 },
2854 PartMidiAutomationChanged {
2855 part: usize,
2856 old_pitch_bends: Vec<MidiPitchBend>,
2857 new_pitch_bends: Vec<MidiPitchBend>,
2858 old_control_changes: Vec<MidiControlChange>,
2859 new_control_changes: Vec<MidiControlChange>,
2860 old_program_changes: Vec<MidiProgramChange>,
2861 new_program_changes: Vec<MidiProgramChange>,
2862 old_aftertouch: Vec<MidiAftertouch>,
2863 new_aftertouch: Vec<MidiAftertouch>,
2864 },
2865 StaffConfigurationChanged {
2866 part: usize,
2867 staff: usize,
2868 old_clef: Clef,
2869 new_clef: Clef,
2870 old_transpose_semitones: i8,
2871 new_transpose_semitones: i8,
2872 },
2873 StaffPresentationChanged {
2874 part: usize,
2875 staff: usize,
2876 old: StaffPresentation,
2877 new: StaffPresentation,
2878 },
2879 MeasurePresentationChanged {
2880 part: usize,
2881 staff: usize,
2882 measure: usize,
2883 old_number: u32,
2884 new_number: u32,
2885 old_clef: Option<Clef>,
2886 new_clef: Option<Clef>,
2887 old_tempo_text: Option<String>,
2888 new_tempo_text: Option<String>,
2889 old_navigation: Option<String>,
2890 new_navigation: Option<String>,
2891 old_expression_text: Option<String>,
2892 new_expression_text: Option<String>,
2893 old_multi_rest_count: Option<u8>,
2894 new_multi_rest_count: Option<u8>,
2895 old_system_break: bool,
2896 new_system_break: bool,
2897 old_page_break: bool,
2898 new_page_break: bool,
2899 old_section_break: bool,
2900 new_section_break: bool,
2901 },
2902 TablatureConfigChanged {
2903 part: usize,
2904 staff: usize,
2905 old: Option<TablatureConfig>,
2906 new: Option<TablatureConfig>,
2907 },
2908 TablatureChangeChanged {
2909 part: usize,
2910 staff: usize,
2911 measure: usize,
2912 old: Option<TablatureConfig>,
2913 new: Option<TablatureConfig>,
2914 },
2915 MidMeasureClefsChanged {
2916 part: usize,
2917 staff: usize,
2918 measure: usize,
2919 old: Vec<MidMeasureClef>,
2920 new: Vec<MidMeasureClef>,
2921 },
2922 UnrepresentedFieldChanged {
2927 path: String,
2928 },
2929 MetadataChanged {
2930 field: String,
2931 old: String,
2932 new: String,
2933 },
2934 TempoChanged {
2935 old: u16,
2936 new: u16,
2937 },
2938 KeySignatureChanged {
2939 old: KeySignature,
2940 new: KeySignature,
2941 },
2942 PartAdded {
2943 part_index: usize,
2944 },
2945 PartRemoved {
2946 part_index: usize,
2947 name: String,
2948 },
2949 NoteAdded {
2950 part: usize,
2951 staff: usize,
2952 measure: usize,
2953 voice: usize,
2954 note_index: usize,
2955 },
2956 NoteRemoved {
2957 part: usize,
2958 staff: usize,
2959 measure: usize,
2960 voice: usize,
2961 note: Box<Note>,
2962 },
2963 NoteModified {
2964 part: usize,
2965 staff: usize,
2966 measure: usize,
2967 voice: usize,
2968 note_index: usize,
2969 old: Box<Note>,
2970 new: Box<Note>,
2971 },
2972 TimeSigChanged {
2973 part: usize,
2974 staff: usize,
2975 measure: usize,
2976 old: Option<TimeSignature>,
2977 new: Option<TimeSignature>,
2978 },
2979 MeasureTempoChanged {
2980 part: usize,
2981 staff: usize,
2982 measure: usize,
2983 old: Option<u16>,
2984 new: Option<u16>,
2985 },
2986 MeasureTempoRampChanged {
2987 part: usize,
2988 staff: usize,
2989 measure: usize,
2990 old: Option<u16>,
2991 new: Option<u16>,
2992 },
2993 BarlineChanged {
2994 part: usize,
2995 staff: usize,
2996 measure: usize,
2997 },
2998 RehearsalMarkChanged {
2999 part: usize,
3000 staff: usize,
3001 measure: usize,
3002 old: Option<String>,
3003 new: Option<String>,
3004 },
3005 VoltaChanged {
3006 part: usize,
3007 staff: usize,
3008 measure: usize,
3009 },
3010}
3011
3012pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
3018 let mut changes: Vec<ScoreChange> = Vec::new();
3019
3020 if a.views != b.views {
3021 changes.push(ScoreChange::ScoreViewsChanged {
3022 old: a.views.clone(),
3023 new: b.views.clone(),
3024 });
3025 }
3026
3027 if a.texts != b.texts {
3028 changes.push(ScoreChange::ScoreTextChanged {
3029 old: a.texts.clone(),
3030 new: b.texts.clone(),
3031 });
3032 }
3033 if a.chord_definitions != b.chord_definitions {
3034 changes.push(ScoreChange::ChordDefinitionsChanged {
3035 old: a.chord_definitions.clone(),
3036 new: b.chord_definitions.clone(),
3037 });
3038 }
3039 if let Some(path) = first_unrepresented_field_change(a, b) {
3040 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
3041 }
3042
3043 macro_rules! meta {
3044 ($field:ident, $name:literal) => {
3045 if a.metadata.$field != b.metadata.$field {
3046 changes.push(ScoreChange::MetadataChanged {
3047 field: $name.to_string(),
3048 old: a.metadata.$field.clone(),
3049 new: b.metadata.$field.clone(),
3050 });
3051 }
3052 };
3053 }
3054 meta!(title, "title");
3055 meta!(composer, "composer");
3056 meta!(lyricist, "lyricist");
3057 meta!(copyright, "copyright");
3058 meta!(work_number, "work_number");
3059 meta!(movement_title, "movement_title");
3060
3061 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3062 changes.push(ScoreChange::TempoChanged {
3063 old: a.settings.tempo_bpm,
3064 new: b.settings.tempo_bpm,
3065 });
3066 }
3067 if a.settings.key_signature != b.settings.key_signature {
3068 changes.push(ScoreChange::KeySignatureChanged {
3069 old: a.settings.key_signature.clone(),
3070 new: b.settings.key_signature.clone(),
3071 });
3072 }
3073
3074 let a_len = a.parts.len();
3075 let b_len = b.parts.len();
3076 for i in b_len..a_len {
3077 changes.push(ScoreChange::PartRemoved {
3078 part_index: i,
3079 name: a.parts[i].name.clone(),
3080 });
3081 }
3082 for i in a_len..b_len {
3083 changes.push(ScoreChange::PartAdded { part_index: i });
3084 }
3085
3086 for pi in 0..a_len.min(b_len) {
3087 let ap = &a.parts[pi];
3088 let bp = &b.parts[pi];
3089 if ap.name != bp.name || ap.short_name != bp.short_name {
3090 changes.push(ScoreChange::PartNamesChanged {
3091 part: pi,
3092 old_name: ap.name.clone(),
3093 new_name: bp.name.clone(),
3094 old_short_name: ap.short_name.clone(),
3095 new_short_name: bp.short_name.clone(),
3096 });
3097 }
3098 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3099 changes.push(ScoreChange::PartMidiChanged {
3100 part: pi,
3101 old_channel: ap.midi_channel,
3102 new_channel: bp.midi_channel,
3103 old_program: ap.midi_program,
3104 new_program: bp.midi_program,
3105 });
3106 }
3107 if ap.instrument != bp.instrument {
3108 changes.push(ScoreChange::InstrumentDefinitionChanged {
3109 part: pi,
3110 old: ap.instrument.clone(),
3111 new: bp.instrument.clone(),
3112 });
3113 }
3114 if ap.midi_pitch_bends != bp.midi_pitch_bends
3115 || ap.midi_control_changes != bp.midi_control_changes
3116 || ap.midi_program_changes != bp.midi_program_changes
3117 || ap.midi_aftertouch != bp.midi_aftertouch
3118 {
3119 changes.push(ScoreChange::PartMidiAutomationChanged {
3120 part: pi,
3121 old_pitch_bends: ap.midi_pitch_bends.clone(),
3122 new_pitch_bends: bp.midi_pitch_bends.clone(),
3123 old_control_changes: ap.midi_control_changes.clone(),
3124 new_control_changes: bp.midi_control_changes.clone(),
3125 old_program_changes: ap.midi_program_changes.clone(),
3126 new_program_changes: bp.midi_program_changes.clone(),
3127 old_aftertouch: ap.midi_aftertouch.clone(),
3128 new_aftertouch: bp.midi_aftertouch.clone(),
3129 });
3130 }
3131 for si in 0..ap.staves.len().min(bp.staves.len()) {
3132 let a_staff = &ap.staves[si];
3133 let b_staff = &bp.staves[si];
3134 if a_staff.clef != b_staff.clef
3135 || a_staff.transpose_semitones != b_staff.transpose_semitones
3136 {
3137 changes.push(ScoreChange::StaffConfigurationChanged {
3138 part: pi,
3139 staff: si,
3140 old_clef: a_staff.clef.clone(),
3141 new_clef: b_staff.clef.clone(),
3142 old_transpose_semitones: a_staff.transpose_semitones,
3143 new_transpose_semitones: b_staff.transpose_semitones,
3144 });
3145 }
3146 if a_staff.tablature != b_staff.tablature {
3147 changes.push(ScoreChange::TablatureConfigChanged {
3148 part: pi,
3149 staff: si,
3150 old: a_staff.tablature.clone(),
3151 new: b_staff.tablature.clone(),
3152 });
3153 }
3154 if a_staff.presentation != b_staff.presentation {
3155 changes.push(ScoreChange::StaffPresentationChanged {
3156 part: pi,
3157 staff: si,
3158 old: a_staff.presentation.clone(),
3159 new: b_staff.presentation.clone(),
3160 });
3161 }
3162 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3163 let am = &a_staff.measures[mi];
3164 let bm = &b_staff.measures[mi];
3165 if am.tablature_change != bm.tablature_change {
3166 changes.push(ScoreChange::TablatureChangeChanged {
3167 part: pi,
3168 staff: si,
3169 measure: mi,
3170 old: am.tablature_change.clone(),
3171 new: bm.tablature_change.clone(),
3172 });
3173 }
3174 if am.mid_clefs != bm.mid_clefs {
3175 changes.push(ScoreChange::MidMeasureClefsChanged {
3176 part: pi,
3177 staff: si,
3178 measure: mi,
3179 old: am.mid_clefs.clone(),
3180 new: bm.mid_clefs.clone(),
3181 });
3182 }
3183 if am.number != bm.number
3184 || am.clef != bm.clef
3185 || am.tempo_text != bm.tempo_text
3186 || am.navigation != bm.navigation
3187 || am.expression_text != bm.expression_text
3188 || am.multi_rest_count != bm.multi_rest_count
3189 || am.system_break != bm.system_break
3190 || am.page_break != bm.page_break
3191 || am.section_break != bm.section_break
3192 {
3193 changes.push(ScoreChange::MeasurePresentationChanged {
3194 part: pi,
3195 staff: si,
3196 measure: mi,
3197 old_number: am.number,
3198 new_number: bm.number,
3199 old_clef: am.clef.clone(),
3200 new_clef: bm.clef.clone(),
3201 old_tempo_text: am.tempo_text.clone(),
3202 new_tempo_text: bm.tempo_text.clone(),
3203 old_navigation: am.navigation.clone(),
3204 new_navigation: bm.navigation.clone(),
3205 old_expression_text: am.expression_text.clone(),
3206 new_expression_text: bm.expression_text.clone(),
3207 old_multi_rest_count: am.multi_rest_count,
3208 new_multi_rest_count: bm.multi_rest_count,
3209 old_system_break: am.system_break,
3210 new_system_break: bm.system_break,
3211 old_page_break: am.page_break,
3212 new_page_break: bm.page_break,
3213 old_section_break: am.section_break,
3214 new_section_break: bm.section_break,
3215 });
3216 }
3217 for vi in 0..4usize {
3218 let av = &am.voices[vi];
3219 let bv = &bm.voices[vi];
3220 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3221 if !note_content_eq(a_note, b_note) {
3222 changes.push(ScoreChange::NoteModified {
3223 part: pi,
3224 staff: si,
3225 measure: mi,
3226 voice: vi,
3227 note_index: ni,
3228 old: Box::new(a_note.clone()),
3229 new: Box::new(b_note.clone()),
3230 });
3231 }
3232 }
3233 for note in av.iter().skip(bv.len()) {
3234 changes.push(ScoreChange::NoteRemoved {
3235 part: pi,
3236 staff: si,
3237 measure: mi,
3238 voice: vi,
3239 note: Box::new(note.clone()),
3240 });
3241 }
3242 for ni in av.len()..bv.len() {
3243 changes.push(ScoreChange::NoteAdded {
3244 part: pi,
3245 staff: si,
3246 measure: mi,
3247 voice: vi,
3248 note_index: ni,
3249 });
3250 }
3251 }
3252 if am.time_sig != bm.time_sig {
3253 changes.push(ScoreChange::TimeSigChanged {
3254 part: pi,
3255 staff: si,
3256 measure: mi,
3257 old: am.time_sig.clone(),
3258 new: bm.time_sig.clone(),
3259 });
3260 }
3261 if am.tempo != bm.tempo {
3262 changes.push(ScoreChange::MeasureTempoChanged {
3263 part: pi,
3264 staff: si,
3265 measure: mi,
3266 old: am.tempo,
3267 new: bm.tempo,
3268 });
3269 }
3270 if am.tempo_ramp_to != bm.tempo_ramp_to {
3271 changes.push(ScoreChange::MeasureTempoRampChanged {
3272 part: pi,
3273 staff: si,
3274 measure: mi,
3275 old: am.tempo_ramp_to,
3276 new: bm.tempo_ramp_to,
3277 });
3278 }
3279 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3280 changes.push(ScoreChange::BarlineChanged {
3281 part: pi,
3282 staff: si,
3283 measure: mi,
3284 });
3285 }
3286 if am.rehearsal != bm.rehearsal {
3287 changes.push(ScoreChange::RehearsalMarkChanged {
3288 part: pi,
3289 staff: si,
3290 measure: mi,
3291 old: am.rehearsal.clone(),
3292 new: bm.rehearsal.clone(),
3293 });
3294 }
3295 if am.volta != bm.volta {
3296 changes.push(ScoreChange::VoltaChanged {
3297 part: pi,
3298 staff: si,
3299 measure: mi,
3300 });
3301 }
3302 if am.texts != bm.texts {
3303 changes.push(ScoreChange::MeasureTextChanged {
3304 part: pi,
3305 staff: si,
3306 measure: mi,
3307 old: am.texts.clone(),
3308 new: bm.texts.clone(),
3309 });
3310 }
3311 if am.figured_bass != bm.figured_bass {
3312 changes.push(ScoreChange::FiguredBassChanged {
3313 part: pi,
3314 staff: si,
3315 measure: mi,
3316 old: am.figured_bass.clone(),
3317 new: bm.figured_bass.clone(),
3318 });
3319 }
3320 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3321 changes.push(ScoreChange::HarpPedalDiagramsChanged {
3322 part: pi,
3323 staff: si,
3324 measure: mi,
3325 old: am.harp_pedal_diagrams.clone(),
3326 new: bm.harp_pedal_diagrams.clone(),
3327 });
3328 }
3329 }
3330 }
3331 }
3332
3333 changes
3334}
3335
3336fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
3337 if a.settings.time_signature != b.settings.time_signature {
3338 return Some("settings.time_signature".to_string());
3339 }
3340 if a.part_groups.len() != b.part_groups.len()
3341 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3342 x.first_part != y.first_part
3343 || x.last_part != y.last_part
3344 || x.symbol != y.symbol
3345 || x.barlines_connect != y.barlines_connect
3346 })
3347 {
3348 return Some("part_groups".to_string());
3349 }
3350 a.parts
3351 .iter()
3352 .zip(&b.parts)
3353 .enumerate()
3354 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
3355}
3356
3357fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
3358 let prefix = format!("parts[{part_index}]");
3359 if a.percussion_instruments != b.percussion_instruments {
3360 return Some(format!("{prefix}.percussion_instruments"));
3361 }
3362 if a.staff_groups != b.staff_groups {
3363 return Some(format!("{prefix}.staff_groups"));
3364 }
3365 if a.staves.len() != b.staves.len() {
3366 return Some(format!("{prefix}.staves"));
3367 }
3368 a.staves
3369 .iter()
3370 .zip(&b.staves)
3371 .enumerate()
3372 .find_map(|(staff_index, (a, b))| {
3373 first_unrepresented_staff_change(&prefix, staff_index, a, b)
3374 })
3375}
3376
3377fn first_unrepresented_staff_change(
3378 part_prefix: &str,
3379 staff_index: usize,
3380 a: &Staff,
3381 b: &Staff,
3382) -> Option<String> {
3383 let prefix = format!("{part_prefix}.staves[{staff_index}]");
3384 if a.measures.len() != b.measures.len() {
3385 return Some(format!("{prefix}.measures"));
3386 }
3387 a.measures
3388 .iter()
3389 .zip(&b.measures)
3390 .enumerate()
3391 .find_map(|(measure_index, (a, b))| {
3392 first_unrepresented_measure_change(&prefix, measure_index, a, b)
3393 })
3394}
3395
3396fn first_unrepresented_measure_change(
3397 _staff_prefix: &str,
3398 _measure_index: usize,
3399 _a: &Measure,
3400 _b: &Measure,
3401) -> Option<String> {
3402 None
3403}
3404
3405#[derive(Debug, Clone, Serialize, Deserialize)]
3412pub enum ScorePatch {
3413 SetScoreViews {
3414 value: Vec<ScoreView>,
3415 },
3416 SetScoreTexts {
3417 value: Vec<StyledText>,
3418 },
3419 SetMeasureTexts {
3420 part: usize,
3421 staff: usize,
3422 measure: usize,
3423 value: Vec<StyledText>,
3424 },
3425 SetFiguredBass {
3426 part: usize,
3427 staff: usize,
3428 measure: usize,
3429 value: Vec<FiguredBassFigure>,
3430 },
3431 SetHarpPedalDiagrams {
3432 part: usize,
3433 staff: usize,
3434 measure: usize,
3435 value: Vec<HarpPedalDiagram>,
3436 },
3437 SetChordDefinitions {
3438 value: Vec<ChordDefinition>,
3439 },
3440 SetPartNames {
3441 part: usize,
3442 name: String,
3443 short_name: String,
3444 },
3445 SetPartMidi {
3446 part: usize,
3447 channel: u8,
3448 program: u8,
3449 },
3450 SetInstrumentDefinition {
3451 part: usize,
3452 value: Option<InstrumentDefinition>,
3453 },
3454 SetPartMidiAutomation {
3455 part: usize,
3456 pitch_bends: Vec<MidiPitchBend>,
3457 control_changes: Vec<MidiControlChange>,
3458 program_changes: Vec<MidiProgramChange>,
3459 aftertouch: Vec<MidiAftertouch>,
3460 },
3461 SetStaffConfiguration {
3462 part: usize,
3463 staff: usize,
3464 clef: Clef,
3465 transpose_semitones: i8,
3466 },
3467 SetStaffPresentation {
3468 part: usize,
3469 staff: usize,
3470 value: StaffPresentation,
3471 },
3472 SetMeasurePresentation {
3473 part: usize,
3474 staff: usize,
3475 measure: usize,
3476 number: u32,
3477 clef: Option<Clef>,
3478 tempo_text: Option<String>,
3479 navigation: Option<String>,
3480 expression_text: Option<String>,
3481 multi_rest_count: Option<u8>,
3482 system_break: bool,
3483 page_break: bool,
3484 #[serde(default)]
3485 section_break: bool,
3486 },
3487 SetTablatureConfig {
3488 part: usize,
3489 staff: usize,
3490 value: Option<TablatureConfig>,
3491 },
3492 SetMeasureTablatureChange {
3493 part: usize,
3494 staff: usize,
3495 measure: usize,
3496 value: Option<TablatureConfig>,
3497 },
3498 SetMeasureMidClefs {
3499 part: usize,
3500 staff: usize,
3501 measure: usize,
3502 value: Vec<MidMeasureClef>,
3503 },
3504 SetMetadata {
3505 field: String,
3506 value: String,
3507 },
3508 SetTempo {
3509 value: u16,
3510 },
3511 SetKeySignature {
3512 part: usize,
3513 staff: usize,
3514 measure: usize,
3515 value: Option<KeySignature>,
3516 },
3517 SetTimeSignature {
3518 part: usize,
3519 staff: usize,
3520 measure: usize,
3521 value: Option<TimeSignature>,
3522 },
3523 SetBarlines {
3524 part: usize,
3525 staff: usize,
3526 measure: usize,
3527 left: Barline,
3528 right: Barline,
3529 },
3530 SetRehearsal {
3531 part: usize,
3532 staff: usize,
3533 measure: usize,
3534 value: Option<String>,
3535 },
3536 SetVolta {
3537 part: usize,
3538 staff: usize,
3539 measure: usize,
3540 value: Option<VoltaBracket>,
3541 },
3542 AddNote {
3544 part: usize,
3545 staff: usize,
3546 measure: usize,
3547 voice: usize,
3548 #[serde(default = "legacy_append_index")]
3550 note_index: usize,
3551 note: Box<Note>,
3552 },
3553 RemoveNote {
3554 part: usize,
3555 staff: usize,
3556 measure: usize,
3557 voice: usize,
3558 note_index: usize,
3559 },
3560 ReplaceNote {
3562 part: usize,
3563 staff: usize,
3564 measure: usize,
3565 voice: usize,
3566 note_index: usize,
3567 note: Box<Note>,
3568 },
3569 SetMeasureTempo {
3570 part: usize,
3571 staff: usize,
3572 measure: usize,
3573 value: Option<u16>,
3574 },
3575 SetMeasureTempoRamp {
3576 part: usize,
3577 staff: usize,
3578 measure: usize,
3579 value: Option<u16>,
3580 },
3581 ReplaceScore {
3584 score: Box<Score>,
3585 },
3586}
3587
3588fn legacy_append_index() -> usize {
3589 usize::MAX
3590}
3591
3592fn patch_requires_replace(a: &Score, b: &Score) -> bool {
3596 if a.settings.time_signature != b.settings.time_signature
3597 || a.settings.key_signature != b.settings.key_signature
3598 || a.parts.len() != b.parts.len()
3599 || a.part_groups.len() != b.part_groups.len()
3600 {
3601 return true;
3602 }
3603 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3604 x.first_part != y.first_part
3605 || x.last_part != y.last_part
3606 || x.symbol != y.symbol
3607 || x.barlines_connect != y.barlines_connect
3608 }) {
3609 return true;
3610 }
3611 for (ap, bp) in a.parts.iter().zip(&b.parts) {
3612 if ap.percussion_instruments != bp.percussion_instruments
3613 || ap.staff_groups != bp.staff_groups
3614 || ap.staves.len() != bp.staves.len()
3615 {
3616 return true;
3617 }
3618 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
3619 if as_.measures.len() != bs.measures.len() {
3620 return true;
3621 }
3622 }
3623 }
3624 false
3625}
3626
3627pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
3632 let mut patches: Vec<ScorePatch> = Vec::new();
3633
3634 if patch_requires_replace(a, b) {
3635 return vec![ScorePatch::ReplaceScore {
3636 score: Box::new(b.clone()),
3637 }];
3638 }
3639
3640 if a.views != b.views {
3641 patches.push(ScorePatch::SetScoreViews {
3642 value: b.views.clone(),
3643 });
3644 }
3645
3646 if a.texts != b.texts {
3647 patches.push(ScorePatch::SetScoreTexts {
3648 value: b.texts.clone(),
3649 });
3650 }
3651 if a.chord_definitions != b.chord_definitions {
3652 patches.push(ScorePatch::SetChordDefinitions {
3653 value: b.chord_definitions.clone(),
3654 });
3655 }
3656
3657 macro_rules! meta {
3658 ($field:ident, $name:literal) => {
3659 if a.metadata.$field != b.metadata.$field {
3660 patches.push(ScorePatch::SetMetadata {
3661 field: $name.to_string(),
3662 value: b.metadata.$field.clone(),
3663 });
3664 }
3665 };
3666 }
3667 meta!(title, "title");
3668 meta!(composer, "composer");
3669 meta!(lyricist, "lyricist");
3670 meta!(copyright, "copyright");
3671 meta!(work_number, "work_number");
3672 meta!(movement_title, "movement_title");
3673
3674 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3675 patches.push(ScorePatch::SetTempo {
3676 value: b.settings.tempo_bpm,
3677 });
3678 }
3679
3680 for pi in 0..a.parts.len().min(b.parts.len()) {
3681 let ap = &a.parts[pi];
3682 let bp = &b.parts[pi];
3683 if ap.name != bp.name || ap.short_name != bp.short_name {
3684 patches.push(ScorePatch::SetPartNames {
3685 part: pi,
3686 name: bp.name.clone(),
3687 short_name: bp.short_name.clone(),
3688 });
3689 }
3690 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3691 patches.push(ScorePatch::SetPartMidi {
3692 part: pi,
3693 channel: bp.midi_channel,
3694 program: bp.midi_program,
3695 });
3696 }
3697 if ap.instrument != bp.instrument {
3698 patches.push(ScorePatch::SetInstrumentDefinition {
3699 part: pi,
3700 value: bp.instrument.clone(),
3701 });
3702 }
3703 if ap.midi_pitch_bends != bp.midi_pitch_bends
3704 || ap.midi_control_changes != bp.midi_control_changes
3705 || ap.midi_program_changes != bp.midi_program_changes
3706 || ap.midi_aftertouch != bp.midi_aftertouch
3707 {
3708 patches.push(ScorePatch::SetPartMidiAutomation {
3709 part: pi,
3710 pitch_bends: bp.midi_pitch_bends.clone(),
3711 control_changes: bp.midi_control_changes.clone(),
3712 program_changes: bp.midi_program_changes.clone(),
3713 aftertouch: bp.midi_aftertouch.clone(),
3714 });
3715 }
3716 for si in 0..ap.staves.len().min(bp.staves.len()) {
3717 let a_staff = &ap.staves[si];
3718 let b_staff = &bp.staves[si];
3719 if a_staff.clef != b_staff.clef
3720 || a_staff.transpose_semitones != b_staff.transpose_semitones
3721 {
3722 patches.push(ScorePatch::SetStaffConfiguration {
3723 part: pi,
3724 staff: si,
3725 clef: b_staff.clef.clone(),
3726 transpose_semitones: b_staff.transpose_semitones,
3727 });
3728 }
3729 if a_staff.tablature != b_staff.tablature {
3730 patches.push(ScorePatch::SetTablatureConfig {
3731 part: pi,
3732 staff: si,
3733 value: b_staff.tablature.clone(),
3734 });
3735 }
3736 if a_staff.presentation != b_staff.presentation {
3737 patches.push(ScorePatch::SetStaffPresentation {
3738 part: pi,
3739 staff: si,
3740 value: b_staff.presentation.clone(),
3741 });
3742 }
3743 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3744 let am = &a_staff.measures[mi];
3745 let bm = &b_staff.measures[mi];
3746
3747 if am.tablature_change != bm.tablature_change {
3748 patches.push(ScorePatch::SetMeasureTablatureChange {
3749 part: pi,
3750 staff: si,
3751 measure: mi,
3752 value: bm.tablature_change.clone(),
3753 });
3754 }
3755
3756 if am.mid_clefs != bm.mid_clefs {
3757 patches.push(ScorePatch::SetMeasureMidClefs {
3758 part: pi,
3759 staff: si,
3760 measure: mi,
3761 value: bm.mid_clefs.clone(),
3762 });
3763 }
3764
3765 if am.number != bm.number
3766 || am.clef != bm.clef
3767 || am.tempo_text != bm.tempo_text
3768 || am.navigation != bm.navigation
3769 || am.expression_text != bm.expression_text
3770 || am.multi_rest_count != bm.multi_rest_count
3771 || am.system_break != bm.system_break
3772 || am.page_break != bm.page_break
3773 || am.section_break != bm.section_break
3774 {
3775 patches.push(ScorePatch::SetMeasurePresentation {
3776 part: pi,
3777 staff: si,
3778 measure: mi,
3779 number: bm.number,
3780 clef: bm.clef.clone(),
3781 tempo_text: bm.tempo_text.clone(),
3782 navigation: bm.navigation.clone(),
3783 expression_text: bm.expression_text.clone(),
3784 multi_rest_count: bm.multi_rest_count,
3785 system_break: bm.system_break,
3786 page_break: bm.page_break,
3787 section_break: bm.section_break,
3788 });
3789 }
3790 if am.key_sig != bm.key_sig {
3791 patches.push(ScorePatch::SetKeySignature {
3792 part: pi,
3793 staff: si,
3794 measure: mi,
3795 value: bm.key_sig.clone(),
3796 });
3797 }
3798 if am.time_sig != bm.time_sig {
3799 patches.push(ScorePatch::SetTimeSignature {
3800 part: pi,
3801 staff: si,
3802 measure: mi,
3803 value: bm.time_sig.clone(),
3804 });
3805 }
3806 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3807 patches.push(ScorePatch::SetBarlines {
3808 part: pi,
3809 staff: si,
3810 measure: mi,
3811 left: bm.barline_left.clone(),
3812 right: bm.barline_right.clone(),
3813 });
3814 }
3815 if am.rehearsal != bm.rehearsal {
3816 patches.push(ScorePatch::SetRehearsal {
3817 part: pi,
3818 staff: si,
3819 measure: mi,
3820 value: bm.rehearsal.clone(),
3821 });
3822 }
3823 if am.volta != bm.volta {
3824 patches.push(ScorePatch::SetVolta {
3825 part: pi,
3826 staff: si,
3827 measure: mi,
3828 value: bm.volta.clone(),
3829 });
3830 }
3831 if am.tempo != bm.tempo {
3832 patches.push(ScorePatch::SetMeasureTempo {
3833 part: pi,
3834 staff: si,
3835 measure: mi,
3836 value: bm.tempo,
3837 });
3838 }
3839 if am.tempo_ramp_to != bm.tempo_ramp_to {
3840 patches.push(ScorePatch::SetMeasureTempoRamp {
3841 part: pi,
3842 staff: si,
3843 measure: mi,
3844 value: bm.tempo_ramp_to,
3845 });
3846 }
3847 if am.texts != bm.texts {
3848 patches.push(ScorePatch::SetMeasureTexts {
3849 part: pi,
3850 staff: si,
3851 measure: mi,
3852 value: bm.texts.clone(),
3853 });
3854 }
3855 if am.figured_bass != bm.figured_bass {
3856 patches.push(ScorePatch::SetFiguredBass {
3857 part: pi,
3858 staff: si,
3859 measure: mi,
3860 value: bm.figured_bass.clone(),
3861 });
3862 }
3863 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3864 patches.push(ScorePatch::SetHarpPedalDiagrams {
3865 part: pi,
3866 staff: si,
3867 measure: mi,
3868 value: bm.harp_pedal_diagrams.clone(),
3869 });
3870 }
3871
3872 for vi in 0..4usize {
3873 let av = &am.voices[vi];
3874 let bv = &bm.voices[vi];
3875 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3876 if !note_content_eq(a_note, b_note) {
3877 patches.push(ScorePatch::ReplaceNote {
3878 part: pi,
3879 staff: si,
3880 measure: mi,
3881 voice: vi,
3882 note_index: ni,
3883 note: Box::new(b_note.clone()),
3884 });
3885 }
3886 }
3887 for ni in (bv.len()..av.len()).rev() {
3889 patches.push(ScorePatch::RemoveNote {
3890 part: pi,
3891 staff: si,
3892 measure: mi,
3893 voice: vi,
3894 note_index: ni,
3895 });
3896 }
3897 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
3899 patches.push(ScorePatch::AddNote {
3900 part: pi,
3901 staff: si,
3902 measure: mi,
3903 voice: vi,
3904 note_index: av.len() + offset,
3905 note: Box::new(note.clone()),
3906 });
3907 }
3908 }
3909 }
3910 }
3911 }
3912
3913 patches
3914}
3915
3916pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
3921 let mut s = score.clone();
3922 for patch in patches {
3923 match patch {
3924 ScorePatch::ReplaceScore { score } => {
3925 s = (**score).clone();
3926 }
3927 ScorePatch::SetScoreViews { value } => {
3928 s.views = value.clone();
3929 }
3930 ScorePatch::SetScoreTexts { value } => {
3931 s.texts = value.clone();
3932 }
3933 ScorePatch::SetMeasureTexts {
3934 part,
3935 staff,
3936 measure,
3937 value,
3938 } => {
3939 s.parts
3940 .get_mut(*part)
3941 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3942 .staves
3943 .get_mut(*staff)
3944 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3945 .measures
3946 .get_mut(*measure)
3947 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3948 .texts = value.clone();
3949 }
3950 ScorePatch::SetFiguredBass {
3951 part,
3952 staff,
3953 measure,
3954 value,
3955 } => {
3956 s.parts
3957 .get_mut(*part)
3958 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3959 .staves
3960 .get_mut(*staff)
3961 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3962 .measures
3963 .get_mut(*measure)
3964 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3965 .figured_bass = value.clone();
3966 }
3967 ScorePatch::SetHarpPedalDiagrams {
3968 part,
3969 staff,
3970 measure,
3971 value,
3972 } => {
3973 s.parts
3974 .get_mut(*part)
3975 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3976 .staves
3977 .get_mut(*staff)
3978 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3979 .measures
3980 .get_mut(*measure)
3981 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3982 .harp_pedal_diagrams = value.clone();
3983 }
3984 ScorePatch::SetChordDefinitions { value } => {
3985 s.chord_definitions = value.clone();
3986 }
3987 ScorePatch::SetPartNames {
3988 part,
3989 name,
3990 short_name,
3991 } => {
3992 let target = s
3993 .parts
3994 .get_mut(*part)
3995 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3996 target.name = name.clone();
3997 target.short_name = short_name.clone();
3998 }
3999 ScorePatch::SetPartMidi {
4000 part,
4001 channel,
4002 program,
4003 } => {
4004 let target = s
4005 .parts
4006 .get_mut(*part)
4007 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4008 target.midi_channel = *channel;
4009 target.midi_program = *program;
4010 }
4011 ScorePatch::SetInstrumentDefinition { part, value } => {
4012 s.parts
4013 .get_mut(*part)
4014 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4015 .instrument = value.clone();
4016 }
4017 ScorePatch::SetPartMidiAutomation {
4018 part,
4019 pitch_bends,
4020 control_changes,
4021 program_changes,
4022 aftertouch,
4023 } => {
4024 let target = s
4025 .parts
4026 .get_mut(*part)
4027 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
4028 target.midi_pitch_bends = pitch_bends.clone();
4029 target.midi_control_changes = control_changes.clone();
4030 target.midi_program_changes = program_changes.clone();
4031 target.midi_aftertouch = aftertouch.clone();
4032 }
4033 ScorePatch::SetStaffConfiguration {
4034 part,
4035 staff,
4036 clef,
4037 transpose_semitones,
4038 } => {
4039 let target = s
4040 .parts
4041 .get_mut(*part)
4042 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4043 .staves
4044 .get_mut(*staff)
4045 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
4046 target.clef = clef.clone();
4047 target.transpose_semitones = *transpose_semitones;
4048 }
4049 ScorePatch::SetStaffPresentation { part, staff, value } => {
4050 let target = s
4051 .parts
4052 .get_mut(*part)
4053 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4054 .staves
4055 .get_mut(*staff)
4056 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
4057 target.presentation = value.clone();
4058 }
4059 ScorePatch::SetMeasurePresentation {
4060 part,
4061 staff,
4062 measure,
4063 number,
4064 clef,
4065 tempo_text,
4066 navigation,
4067 expression_text,
4068 multi_rest_count,
4069 system_break,
4070 page_break,
4071 section_break,
4072 } => {
4073 let target = s
4074 .parts
4075 .get_mut(*part)
4076 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4077 .staves
4078 .get_mut(*staff)
4079 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4080 .measures
4081 .get_mut(*measure)
4082 .ok_or_else(|| {
4083 Error::InvalidPatch(format!("measure {measure} out of range"))
4084 })?;
4085 target.number = *number;
4086 target.clef = clef.clone();
4087 target.tempo_text = tempo_text.clone();
4088 target.navigation = navigation.clone();
4089 target.expression_text = expression_text.clone();
4090 target.multi_rest_count = *multi_rest_count;
4091 target.system_break = *system_break;
4092 target.page_break = *page_break;
4093 target.section_break = *section_break;
4094 }
4095 ScorePatch::SetTablatureConfig { part, staff, value } => {
4096 s.parts
4097 .get_mut(*part)
4098 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4099 .staves
4100 .get_mut(*staff)
4101 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4102 .tablature = value.clone();
4103 }
4104 ScorePatch::SetMeasureTablatureChange {
4105 part,
4106 staff,
4107 measure,
4108 value,
4109 } => {
4110 s.parts
4111 .get_mut(*part)
4112 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4113 .staves
4114 .get_mut(*staff)
4115 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4116 .measures
4117 .get_mut(*measure)
4118 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4119 .tablature_change = value.clone();
4120 }
4121 ScorePatch::SetMeasureMidClefs {
4122 part,
4123 staff,
4124 measure,
4125 value,
4126 } => {
4127 s.parts
4128 .get_mut(*part)
4129 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4130 .staves
4131 .get_mut(*staff)
4132 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4133 .measures
4134 .get_mut(*measure)
4135 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4136 .mid_clefs = value.clone();
4137 }
4138 ScorePatch::SetMetadata { field, value } => match field.as_str() {
4139 "title" => s.metadata.title = value.clone(),
4140 "composer" => s.metadata.composer = value.clone(),
4141 "lyricist" => s.metadata.lyricist = value.clone(),
4142 "copyright" => s.metadata.copyright = value.clone(),
4143 "work_number" => s.metadata.work_number = value.clone(),
4144 "movement_title" => s.metadata.movement_title = value.clone(),
4145 other => {
4146 return Err(Error::InvalidPatch(format!(
4147 "unknown metadata field: {other}"
4148 )));
4149 }
4150 },
4151 ScorePatch::SetTempo { value } => {
4152 s.settings.tempo_bpm = *value;
4153 }
4154 ScorePatch::SetKeySignature {
4155 part,
4156 staff,
4157 measure,
4158 value,
4159 } => {
4160 s.parts
4161 .get_mut(*part)
4162 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4163 .staves
4164 .get_mut(*staff)
4165 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4166 .measures
4167 .get_mut(*measure)
4168 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4169 .key_sig = value.clone();
4170 }
4171 ScorePatch::SetTimeSignature {
4172 part,
4173 staff,
4174 measure,
4175 value,
4176 } => {
4177 s.parts
4178 .get_mut(*part)
4179 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4180 .staves
4181 .get_mut(*staff)
4182 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4183 .measures
4184 .get_mut(*measure)
4185 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4186 .time_sig = value.clone();
4187 }
4188 ScorePatch::SetBarlines {
4189 part,
4190 staff,
4191 measure,
4192 left,
4193 right,
4194 } => {
4195 let m = s
4196 .parts
4197 .get_mut(*part)
4198 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4199 .staves
4200 .get_mut(*staff)
4201 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4202 .measures
4203 .get_mut(*measure)
4204 .ok_or_else(|| {
4205 Error::InvalidPatch(format!("measure {measure} out of range"))
4206 })?;
4207 m.barline_left = left.clone();
4208 m.barline_right = right.clone();
4209 }
4210 ScorePatch::SetRehearsal {
4211 part,
4212 staff,
4213 measure,
4214 value,
4215 } => {
4216 s.parts
4217 .get_mut(*part)
4218 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4219 .staves
4220 .get_mut(*staff)
4221 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4222 .measures
4223 .get_mut(*measure)
4224 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4225 .rehearsal = value.clone();
4226 }
4227 ScorePatch::SetVolta {
4228 part,
4229 staff,
4230 measure,
4231 value,
4232 } => {
4233 s.parts
4234 .get_mut(*part)
4235 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4236 .staves
4237 .get_mut(*staff)
4238 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4239 .measures
4240 .get_mut(*measure)
4241 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4242 .volta = value.clone();
4243 }
4244 ScorePatch::AddNote {
4245 part,
4246 staff,
4247 measure,
4248 voice,
4249 note_index,
4250 note,
4251 } => {
4252 let v = s
4253 .parts
4254 .get_mut(*part)
4255 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4256 .staves
4257 .get_mut(*staff)
4258 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4259 .measures
4260 .get_mut(*measure)
4261 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4262 .voices
4263 .get_mut(*voice)
4264 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4265 let insert_at = if *note_index == usize::MAX {
4266 v.len()
4267 } else {
4268 *note_index
4269 };
4270 if insert_at > v.len() {
4271 return Err(Error::InvalidPatch(format!(
4272 "note_index {note_index} out of range"
4273 )));
4274 }
4275 v.insert(insert_at, *note.clone());
4276 }
4277 ScorePatch::RemoveNote {
4278 part,
4279 staff,
4280 measure,
4281 voice,
4282 note_index,
4283 } => {
4284 let v = s
4285 .parts
4286 .get_mut(*part)
4287 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4288 .staves
4289 .get_mut(*staff)
4290 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4291 .measures
4292 .get_mut(*measure)
4293 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4294 .voices
4295 .get_mut(*voice)
4296 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4297 if *note_index >= v.len() {
4298 return Err(Error::InvalidPatch(format!(
4299 "note_index {note_index} out of range"
4300 )));
4301 }
4302 v.remove(*note_index);
4303 }
4304 ScorePatch::ReplaceNote {
4305 part,
4306 staff,
4307 measure,
4308 voice,
4309 note_index,
4310 note,
4311 } => {
4312 let v = s
4313 .parts
4314 .get_mut(*part)
4315 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4316 .staves
4317 .get_mut(*staff)
4318 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4319 .measures
4320 .get_mut(*measure)
4321 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4322 .voices
4323 .get_mut(*voice)
4324 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4325 if *note_index >= v.len() {
4326 return Err(Error::InvalidPatch(format!(
4327 "note_index {note_index} out of range"
4328 )));
4329 }
4330 v[*note_index] = *note.clone();
4331 }
4332 ScorePatch::SetMeasureTempo {
4333 part,
4334 staff,
4335 measure,
4336 value,
4337 } => {
4338 s.parts
4339 .get_mut(*part)
4340 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4341 .staves
4342 .get_mut(*staff)
4343 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4344 .measures
4345 .get_mut(*measure)
4346 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4347 .tempo = *value;
4348 }
4349 ScorePatch::SetMeasureTempoRamp {
4350 part,
4351 staff,
4352 measure,
4353 value,
4354 } => {
4355 s.parts
4356 .get_mut(*part)
4357 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4358 .staves
4359 .get_mut(*staff)
4360 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4361 .measures
4362 .get_mut(*measure)
4363 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4364 .tempo_ramp_to = *value;
4365 }
4366 }
4367 }
4368 if !super::validate::validate(&s).is_valid() {
4369 return Err(Error::InvalidScore);
4370 }
4371 Ok(s)
4372}
4373
4374pub fn voice_duration_beats(voice: &[Note], measure_beats: f64) -> f64 {
4378 match voice {
4379 [only] if only.is_plain_whole_rest() => measure_beats,
4380 _ => voice.iter().map(Note::beats).sum(),
4381 }
4382}
4383
4384pub fn respell_score(score: &mut Score, prefer_flat: bool) {
4388 for part in &mut score.parts {
4389 for staff in &mut part.staves {
4390 for measure in &mut staff.measures {
4391 for voice in &mut measure.voices {
4392 for note in voice.iter_mut() {
4393 for pitch in &mut note.pitches {
4394 *pitch = pitch.respell(prefer_flat);
4395 }
4396 }
4397 }
4398 }
4399 }
4400 }
4401}
4402
4403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4405#[serde(rename_all = "snake_case")]
4406pub enum RespellPolicy {
4407 Flat,
4409 Sharp,
4411 Key,
4413}
4414
4415pub fn respell_staff_region(
4426 score: &mut Score,
4427 part_index: usize,
4428 staff_index: usize,
4429 start_measure: usize,
4430 end_measure: usize,
4431 policy: RespellPolicy,
4432) -> Result<(usize, usize), Error> {
4433 let score_key_flat = score.settings.key_signature.fifths < 0;
4434 let part = score
4435 .parts
4436 .get_mut(part_index)
4437 .ok_or(Error::PartNotFound(part_index))?;
4438 let staff_count = part.staves.len();
4439 if staff_index >= staff_count {
4440 return Err(Error::StaffNotFound(staff_index));
4441 }
4442 let measure_count = part.staves[staff_index].measures.len();
4443 if start_measure >= end_measure || end_measure > measure_count {
4444 return Err(Error::InvalidCommand(format!(
4445 "invalid measure range {start_measure}..{end_measure}"
4446 )));
4447 }
4448 let mut prefer_flat = Vec::with_capacity(end_measure);
4449 let mut running_flat = score_key_flat;
4450 for index in 0..end_measure {
4451 let key = part.staves[staff_index].measures[index]
4452 .key_sig
4453 .as_ref()
4454 .or_else(|| {
4455 part.staves[0]
4456 .measures
4457 .get(index)
4458 .and_then(|measure| measure.key_sig.as_ref())
4459 });
4460 if let Some(key) = key {
4461 running_flat = key.fifths < 0;
4462 }
4463 prefer_flat.push(match policy {
4464 RespellPolicy::Flat => true,
4465 RespellPolicy::Sharp => false,
4466 RespellPolicy::Key => running_flat,
4467 });
4468 }
4469
4470 let measures = &mut part.staves[staff_index].measures;
4471 for (index, measure) in measures
4472 .iter_mut()
4473 .enumerate()
4474 .take(end_measure)
4475 .skip(start_measure)
4476 {
4477 for voice in &mut measure.voices {
4478 for note in voice
4479 .iter_mut()
4480 .filter(|note| !note.is_rest && !note.is_unpitched)
4481 {
4482 for pitch in &mut note.pitches {
4483 *pitch = pitch.respell(prefer_flat[index]);
4484 }
4485 }
4486 }
4487 }
4488
4489 let mut changed = (start_measure, end_measure - 1);
4490 for voice in 0..4 {
4491 for measure in start_measure..end_measure {
4492 for note in 0..measures[measure].voices[voice].len() {
4493 if measures[measure].voices[voice][note].tie_start {
4494 let last = propagate_tied_spelling(measures, voice, (measure, note), true);
4495 changed.1 = changed.1.max(last);
4496 }
4497 }
4498 }
4499 if !measures[start_measure].voices[voice].is_empty() {
4500 let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
4501 changed.0 = changed.0.min(first);
4502 }
4503 }
4504 Ok(changed)
4505}
4506
4507fn propagate_tied_spelling(
4510 measures: &mut [Measure],
4511 voice: usize,
4512 from: (usize, usize),
4513 forward: bool,
4514) -> usize {
4515 let mut current = from;
4516 loop {
4517 let note = &measures[current.0].voices[voice][current.1];
4518 if !(if forward {
4519 note.tie_start
4520 } else {
4521 note.tie_end
4522 }) {
4523 return current.0;
4524 }
4525 let neighbor = if forward {
4526 if current.1 + 1 < measures[current.0].voices[voice].len() {
4527 Some((current.0, current.1 + 1))
4528 } else {
4529 measures
4530 .get(current.0 + 1)
4531 .filter(|measure| !measure.voices[voice].is_empty())
4532 .map(|_| (current.0 + 1, 0))
4533 }
4534 } else if current.1 > 0 {
4535 Some((current.0, current.1 - 1))
4536 } else {
4537 current.0.checked_sub(1).and_then(|previous| {
4538 measures[previous].voices[voice]
4539 .len()
4540 .checked_sub(1)
4541 .map(|last| (previous, last))
4542 })
4543 };
4544 let Some(next) = neighbor else {
4545 return current.0;
4546 };
4547 let next_note = &measures[next.0].voices[voice][next.1];
4548 if !(if forward {
4549 next_note.tie_end
4550 } else {
4551 next_note.tie_start
4552 }) {
4553 return current.0;
4554 }
4555 let source = measures[current.0].voices[voice][current.1].pitches.clone();
4556 for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
4557 if let Some(spelled) = source.iter().find(|candidate| {
4558 candidate.to_midi() == pitch.to_midi()
4559 && candidate.microtone_cents == pitch.microtone_cents
4560 }) {
4561 *pitch = spelled.clone();
4562 }
4563 }
4564 current = next;
4565 }
4566}
4567
4568pub fn respell_score_to_key(score: &mut Score) {
4572 let prefer_flat = score.settings.key_signature.fifths < 0;
4573 respell_score(score, prefer_flat);
4574}
4575
4576pub fn score_duration_secs(score: &Score) -> f64 {
4581 if score.settings.tempo_bpm == 0 {
4582 return 0.0;
4583 }
4584 let seq = measure_sequence(score);
4585 let mut total_secs = 0.0f64;
4586 let mut current_bpm = score.settings.tempo_bpm as f64;
4587 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4588 for &idx in &seq {
4589 if let Some(m) = staff.measures.get(idx) {
4590 if let Some(b) = m.tempo {
4591 current_bpm = b as f64;
4592 }
4593 if current_bpm == 0.0 {
4594 continue;
4595 }
4596 let beats = voice_duration_beats(
4597 &m.voices[0],
4598 staff.measure_beats(idx, &score.settings.time_signature),
4599 );
4600 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4601 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4602 current_bpm = f64::from(target);
4603 }
4604 }
4605 }
4606 }
4607 total_secs
4608}
4609
4610pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
4615 if score.settings.tempo_bpm == 0 {
4616 return 0.0;
4617 }
4618 let seq: Vec<usize> = measure_sequence(score)
4619 .into_iter()
4620 .filter(|&idx| idx >= region.0 && idx <= region.1)
4621 .collect();
4622 let mut total_secs = 0.0f64;
4623 let mut current_bpm = score.settings.tempo_bpm as f64;
4624 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4625 for &idx in &seq {
4626 if let Some(m) = staff.measures.get(idx) {
4627 if let Some(b) = m.tempo {
4628 current_bpm = b as f64;
4629 }
4630 if current_bpm == 0.0 {
4631 continue;
4632 }
4633 let beats = voice_duration_beats(
4634 &m.voices[0],
4635 staff.measure_beats(idx, &score.settings.time_signature),
4636 );
4637 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4638 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4639 current_bpm = f64::from(target);
4640 }
4641 }
4642 }
4643 }
4644 total_secs
4645}
4646
4647fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
4648 let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
4649 return beats / start_bpm * 60.0;
4650 };
4651 let end_bpm = f64::from(target_bpm);
4652 let delta = end_bpm - start_bpm;
4653 if delta.abs() < f64::EPSILON {
4654 return beats / start_bpm * 60.0;
4655 }
4656 60.0 * beats / delta * (end_bpm / start_bpm).ln()
4657}
4658
4659pub fn measure_beats_remaining(
4664 score: &Score,
4665 part_index: usize,
4666 staff_index: usize,
4667 measure_index: usize,
4668 voice_index: usize,
4669) -> Result<f64, Error> {
4670 let part = score
4671 .parts
4672 .get(part_index)
4673 .ok_or(Error::PartNotFound(part_index))?;
4674 let staff = part
4675 .staves
4676 .get(staff_index)
4677 .ok_or(Error::StaffNotFound(staff_index))?;
4678 let measure = staff
4679 .measures
4680 .get(measure_index)
4681 .ok_or(Error::MeasureNotFound(measure_index))?;
4682 let voice = measure
4683 .voices
4684 .get(voice_index)
4685 .ok_or(Error::VoiceOutOfRange(voice_index))?;
4686 let capacity = staff.measure_beats(measure_index, &score.settings.time_signature);
4687 Ok((capacity - voice_duration_beats(voice, capacity)).max(0.0))
4688}
4689
4690pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
4696 if pitches.is_empty() {
4697 return true;
4698 }
4699 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
4700 avg < clef.middle_line_midi() as f64
4701}
4702
4703fn beam_beat_size(ts: &TimeSignature) -> f64 {
4704 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
4705 3.0 * 4.0 / ts.denominator as f64
4706 } else {
4707 4.0 / ts.denominator as f64
4708 }
4709}
4710
4711pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
4716 let beat_size = beam_beat_size(time_sig);
4717 let n = notes.len();
4718 let mut result = vec![BeamState::None; n];
4719
4720 let is_beamable = |note: &Note| -> bool {
4721 !note.is_rest
4722 && matches!(
4723 note.duration,
4724 Duration::Eighth
4725 | Duration::Sixteenth
4726 | Duration::ThirtySecond
4727 | Duration::SixtyFourth
4728 | Duration::HundredTwentyEighth
4729 )
4730 };
4731
4732 let mut starts = Vec::with_capacity(n);
4734 let mut pos = 0.0f64;
4735 for note in notes {
4736 starts.push(pos);
4737 pos += note.beats();
4738 }
4739
4740 let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
4742
4743 let mut i = 0;
4744 while i < n {
4745 if !is_beamable(¬es[i]) {
4746 i += 1;
4747 continue;
4748 }
4749 let g = group_id(i);
4750 let mut j = i;
4752 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
4753 j += 1;
4754 }
4755 let run = j - i;
4756 if run == 1 {
4757 result[i] = BeamState::None;
4758 } else {
4759 result[i] = BeamState::Begin;
4760 result[i + 1..j - 1].fill(BeamState::Continue);
4761 result[j - 1] = BeamState::End;
4762 }
4763 i = j;
4764 }
4765 result
4766}
4767
4768fn note_content_eq(a: &Note, b: &Note) -> bool {
4769 a.is_rest == b.is_rest
4770 && a.is_unpitched == b.is_unpitched
4771 && a.instrument_id == b.instrument_id
4772 && a.offset_x == b.offset_x
4773 && a.offset_y == b.offset_y
4774 && a.relative_x == b.relative_x
4775 && a.relative_y == b.relative_y
4776 && a.pitches == b.pitches
4777 && a.duration == b.duration
4778 && a.dot_count == b.dot_count
4779 && a.tie_start == b.tie_start
4780 && a.tie_end == b.tie_end
4781 && a.beam == b.beam
4782 && a.articulations == b.articulations
4783 && a.dynamic == b.dynamic
4784 && a.stem_up == b.stem_up
4785 && a.hairpin_start == b.hairpin_start
4786 && a.hairpin_end == b.hairpin_end
4787 && a.tuplet == b.tuplet
4788 && a.chord_symbol == b.chord_symbol
4789 && a.is_grace == b.is_grace
4790 && a.grace_slash == b.grace_slash
4791 && a.ottava_start == b.ottava_start
4792 && a.ottava_end == b.ottava_end
4793 && a.lyric == b.lyric
4794 && a.additional_lyrics == b.additional_lyrics
4795 && a.pedal_start == b.pedal_start
4796 && a.pedal_end == b.pedal_end
4797 && a.slur_start == b.slur_start
4798 && a.slur_end == b.slur_end
4799 && a.arpeggiate == b.arpeggiate
4800 && a.tab_position == b.tab_position
4801 && a.tab_positions == b.tab_positions
4802 && a.guitar_technique == b.guitar_technique
4803 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
4804 && a.guitar_bend_curve == b.guitar_bend_curve
4805}
4806
4807fn per_pitch_flag(flags: &[bool], pitch_count: usize, index: usize, chord: bool) -> bool {
4808 if flags.len() == pitch_count {
4809 flags.get(index).copied().unwrap_or(chord)
4810 } else {
4811 chord
4812 }
4813}
4814
4815#[cfg(test)]
4816mod tests {
4817 use super::*;
4818 use crate::model::{
4819 notation::{FingeringSelectionPolicy, TextStyle},
4820 pitch::Step,
4821 };
4822
4823 #[test]
4824 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
4825 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4826 note.fingerings = vec![3, 1, 4];
4827 note.fingering = Some(3);
4828 assert_eq!(
4829 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
4830 Some(3)
4831 );
4832 assert_eq!(
4833 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
4834 Some(1)
4835 );
4836 assert_eq!(
4837 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
4838 Some(4)
4839 );
4840 assert_eq!(note.fingerings, vec![3, 1, 4]);
4841 assert_eq!(note.fingering, Some(3));
4842 }
4843
4844 #[test]
4845 fn default_score_has_one_part_four_measures() {
4846 let score = Score::default();
4847 assert_eq!(score.parts.len(), 1);
4848 assert_eq!(score.parts[0].staves.len(), 1);
4849 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
4850 }
4851
4852 #[test]
4853 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
4854 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4855 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4856 lines: 6,
4857 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4858 capo: 2,
4859 });
4860 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
4861 Pitch::with_alter(Step::F, 4, 1),
4862 Duration::Quarter,
4863 ));
4864 score.parts[0].staves[0].measures[0].voices[0]
4865 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
4866 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
4867 Some(TabPosition { string: 6, fret: 7 });
4868
4869 assert_eq!(assign_tablature_positions(&mut score), 1);
4870 let notes = &score.parts[0].staves[0].measures[0].voices[0];
4871 assert_eq!(
4872 notes[1].tab_position,
4873 Some(TabPosition { string: 1, fret: 0 })
4874 );
4875 assert_eq!(notes[1].string_number, Some(1));
4876 assert_eq!(
4877 notes[2].tab_position,
4878 Some(TabPosition { string: 6, fret: 7 })
4879 );
4880 }
4881
4882 #[test]
4883 fn assign_tablature_positions_uses_measure_local_capo_change() {
4884 let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
4885 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4886 lines: 6,
4887 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4888 capo: 0,
4889 });
4890 score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
4891 lines: 6,
4892 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4893 capo: 2,
4894 });
4895 score.parts[0].staves[0].measures[1].voices[0]
4896 .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
4897
4898 assert_eq!(assign_tablature_positions(&mut score), 1);
4899 assert_eq!(
4900 score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
4901 Some(TabPosition { string: 5, fret: 3 })
4902 );
4903 }
4904
4905 #[test]
4906 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
4907 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4908 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4909 lines: 6,
4910 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4911 capo: 0,
4912 });
4913 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4914 chord.pitches.push(Pitch::new(Step::G, 4));
4915 score.parts[0].staves[0].measures[0].voices[0].push(chord);
4916
4917 assert_eq!(assign_tablature_positions(&mut score), 1);
4918 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
4919 assert_eq!(
4920 positions,
4921 &vec![
4922 TabPosition { string: 2, fret: 5 },
4923 TabPosition { string: 1, fret: 3 },
4924 ]
4925 );
4926 }
4927
4928 #[test]
4929 fn new_score_measure_count() {
4930 let score = Score::new("Test", 120, 4, 4, 0, 8);
4931 assert_eq!(score.measure_count(), 8);
4932 }
4933
4934 #[test]
4935 fn note_beats_quarter() {
4936 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4937 assert!((note.beats() - 1.0).abs() < 1e-9);
4938 }
4939
4940 #[test]
4941 fn note_beats_dotted_quarter() {
4942 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4943 note.dot_count = 1;
4944 assert!((note.beats() - 1.5).abs() < 1e-9);
4945 }
4946
4947 #[test]
4948 fn grace_note_beats_zero() {
4949 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
4950 note.is_grace = true;
4951 assert_eq!(note.beats(), 0.0);
4952 }
4953
4954 #[test]
4955 fn measure_empty_4_4_fills_four_beats() {
4956 let m = Measure::empty(4, 4);
4957 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4958 assert!((total - 4.0).abs() < 1e-9);
4959 }
4960
4961 #[test]
4962 fn measure_empty_3_4_fills_three_beats() {
4963 let m = Measure::empty(3, 4);
4964 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4965 assert!((total - 3.0).abs() < 1e-9);
4966 }
4967
4968 #[test]
4969 fn whole_filling_beats() {
4970 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
4971 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
4972 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
4973 }
4974
4975 #[test]
4978 fn statistics_default_score_all_rests() {
4979 let score = Score::default();
4980 let s = score.statistics();
4981 assert_eq!(s.part_count, 1);
4982 assert_eq!(s.measure_count, 4);
4983 assert_eq!(s.note_count, 0);
4984 assert!(s.rest_count > 0);
4985 }
4986
4987 #[test]
4988 fn statistics_duration_estimate() {
4989 let score = Score::new("T", 120, 4, 4, 0, 1);
4991 let s = score.statistics();
4992 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
4993 }
4994
4995 #[test]
4996 fn score_duration_secs_matches_statistics() {
4997 use super::score_duration_secs;
4998 let score = Score::new("T", 120, 4, 4, 0, 4);
4999 let secs = score_duration_secs(&score);
5000 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
5002 }
5003
5004 #[test]
5005 fn score_duration_secs_integrates_measure_tempo_ramp() {
5006 use super::score_duration_secs;
5007 let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
5008 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5009 let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
5010 assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
5011 }
5012
5013 #[test]
5014 fn score_duration_secs_zero_bpm_returns_zero() {
5015 use super::score_duration_secs;
5016 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5017 score.settings.tempo_bpm = 0;
5018 assert_eq!(score_duration_secs(&score), 0.0);
5019 }
5020
5021 #[test]
5022 fn score_duration_secs_per_measure_tempo() {
5023 use super::score_duration_secs;
5024 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5026 score.parts[0].staves[0].measures[1].tempo = Some(60);
5027 let secs = score_duration_secs(&score);
5028 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
5029 }
5030
5031 #[test]
5034 fn extract_part_returns_single_part_score() {
5035 let mut score = Score::default();
5036 let mut p2 = Part::new("Violin", "Vln.");
5037 p2.staves.push(Staff::new(Clef::Treble));
5038 score.parts.push(p2);
5039 let ex = score.extract_part(0).unwrap();
5040 assert_eq!(ex.parts.len(), 1);
5041 assert_ne!(ex.id, score.id);
5042 assert_eq!(ex.metadata.title, score.metadata.title);
5043 }
5044
5045 #[test]
5046 fn extract_part_out_of_range_is_none() {
5047 let score = Score::default();
5048 assert!(score.extract_part(99).is_none());
5049 }
5050
5051 #[test]
5052 fn extract_and_merge_remap_typed_spanner_part_addresses() {
5053 let mut left = Score::template(ScoreTemplate::StringQuartet);
5054 let address = NoteAddr {
5055 part: 1,
5056 staff: 0,
5057 measure: 0,
5058 voice: 0,
5059 note: 0,
5060 };
5061 left.spanners.push(NotationSpanner {
5062 id: "left-span".to_string(),
5063 kind: NotationSpannerKind::Slur,
5064 start: address.clone(),
5065 end: address,
5066 number: Some(1),
5067 line_type: None,
5068 text: None,
5069 placement: None,
5070 ottava_size: None,
5071 ottava_type: None,
5072 });
5073 let extracted = left.extract_part_checked(1).expect("valid extracted part");
5074 assert_eq!(extracted.spanners[0].start.part, 0);
5075 assert_eq!(extracted.spanners[0].end.part, 0);
5076
5077 let mut right = Score::new("R", 120, 4, 4, 0, 1);
5078 let right_address = NoteAddr {
5079 part: 0,
5080 staff: 0,
5081 measure: 0,
5082 voice: 0,
5083 note: 0,
5084 };
5085 right.spanners.push(NotationSpanner {
5086 id: "right-span".to_string(),
5087 kind: NotationSpannerKind::Pedal,
5088 start: right_address.clone(),
5089 end: right_address,
5090 number: Some(1),
5091 line_type: None,
5092 text: None,
5093 placement: None,
5094 ottava_size: None,
5095 ottava_type: None,
5096 });
5097 let merged = left.merge_checked(&right).expect("valid merged score");
5098 let right_span = merged
5099 .spanners
5100 .iter()
5101 .find(|spanner| spanner.id == "right-span")
5102 .expect("merged right span");
5103 assert_eq!(right_span.start.part, left.parts.len());
5104 assert_eq!(right_span.end.part, left.parts.len());
5105 }
5106
5107 #[test]
5110 fn transpose_zero_is_clone() {
5111 let score = Score::new("T", 120, 4, 4, 0, 1);
5112 let t = transpose(&score, 0);
5113 assert_eq!(t.settings.key_signature.fifths, 0);
5114 }
5115
5116 #[test]
5117 fn transpose_staff_region_rewrites_only_selected_written_measures() {
5118 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5119 for measure in &mut score.parts[0].staves[0].measures {
5120 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5121 }
5122 let transformed = transpose_staff_region_checked(
5123 &score,
5124 0,
5125 0,
5126 1,
5127 2,
5128 2,
5129 RegionalTranspositionTarget::Written,
5130 )
5131 .expect("region should transpose");
5132 assert_eq!(
5133 transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
5134 Pitch::new(Step::C, 4)
5135 );
5136 assert_eq!(
5137 transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
5138 Pitch::new(Step::D, 4)
5139 );
5140 assert_eq!(
5141 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
5142 Pitch::new(Step::C, 4)
5143 );
5144 }
5145
5146 #[test]
5147 fn regional_concert_transposition_requires_full_staff() {
5148 let score = Score::new("T", 120, 4, 4, 0, 2);
5149 assert!(
5150 transpose_staff_region_checked(
5151 &score,
5152 0,
5153 0,
5154 1,
5155 2,
5156 -2,
5157 RegionalTranspositionTarget::Concert,
5158 )
5159 .is_err()
5160 );
5161 }
5162
5163 #[test]
5164 fn transpose_c_major_up_2_to_d_major() {
5165 let score = Score::new("T", 120, 4, 4, 0, 1);
5166 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
5167 }
5168
5169 #[test]
5170 fn transpose_d_major_up_5_to_g_major() {
5171 let score = Score::new("T", 120, 4, 4, 2, 1);
5172 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
5173 }
5174
5175 #[test]
5176 fn transpose_c4_up_1_to_csharp4() {
5177 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
5178 assert_eq!(p.to_midi(), 61);
5179 assert_eq!(p.step, Step::C);
5180 assert_eq!(p.alter, 1);
5181 }
5182
5183 #[test]
5184 fn transpose_c4_down_1_to_b3() {
5185 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
5186 assert_eq!(p.to_midi(), 59);
5187 assert_eq!(p.step, Step::B);
5188 assert_eq!(p.alter, 0);
5189 }
5190
5191 #[test]
5192 fn transpose_up_octave_keeps_step() {
5193 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
5194 assert_eq!(p.to_midi(), 81);
5195 assert_eq!(p.step, Step::A);
5196 assert_eq!(p.octave, 5);
5197 }
5198
5199 #[test]
5200 fn statistics_with_repeat_doubles_duration() {
5201 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5203 score.parts[0].staves[0].measures[0].barline_left =
5204 crate::model::notation::Barline::RepeatStart;
5205 score.parts[0].staves[0].measures[1].barline_right =
5206 crate::model::notation::Barline::RepeatEnd;
5207 let s = score.statistics();
5208 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
5210 }
5211
5212 #[test]
5213 fn transpose_octave_boundary_b4_to_c5() {
5214 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
5216 assert_eq!(p.to_midi(), 72);
5217 assert_eq!(p.step, Step::C);
5218 assert_eq!(p.octave, 5);
5219 }
5220
5221 #[test]
5222 fn transpose_clamp_at_midi_127() {
5223 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
5225 assert_eq!(p.to_midi(), 127);
5226 }
5227
5228 #[test]
5231 fn merge_combines_parts() {
5232 let mut a = Score::new("A", 120, 4, 4, 0, 2);
5233 let b = Score::new("B", 120, 4, 4, 0, 2);
5234 let mut p2 = Part::new("Violin", "Vln.");
5236 p2.staves.push(Staff::new(Clef::Treble));
5237 for i in 0..2usize {
5238 let mut m = Measure::empty(4, 4);
5239 m.number = i as u32 + 1;
5240 p2.staves[0].measures.push(m);
5241 }
5242 a.parts.push(p2);
5243 let merged = a.merge(&b);
5244 assert_eq!(merged.parts.len(), 3);
5246 }
5247
5248 #[test]
5249 fn merge_pads_shorter_score() {
5250 let a = Score::new("A", 120, 4, 4, 0, 4);
5251 let b = Score::new("B", 120, 4, 4, 0, 2);
5252 let merged = a.merge(&b);
5253 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
5255 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
5256 }
5257
5258 #[test]
5259 fn merge_uses_self_metadata() {
5260 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
5261 a.metadata.composer = "Composer A".to_string();
5262 let b = Score::new("Title B", 120, 4, 4, 0, 2);
5263 let merged = a.merge(&b);
5264 assert_eq!(merged.metadata.title, "Title A");
5265 assert_eq!(merged.metadata.composer, "Composer A");
5266 }
5267
5268 #[test]
5269 fn merge_new_id_differs_from_both() {
5270 let a = Score::new("A", 120, 4, 4, 0, 2);
5271 let b = Score::new("B", 120, 4, 4, 0, 2);
5272 let merged = a.merge(&b);
5273 assert_ne!(merged.id, a.id);
5274 assert_ne!(merged.id, b.id);
5275 }
5276
5277 #[test]
5280 fn staff_default_transpose_is_zero() {
5281 let s = Staff::new(Clef::Treble);
5282 assert_eq!(s.transpose_semitones, 0);
5283 }
5284
5285 #[test]
5286 fn staff_presentation_defaults_and_tracks_percussion_clef() {
5287 let standard = Staff::new(Clef::Treble);
5288 assert_eq!(standard.presentation, StaffPresentation::default());
5289
5290 let percussion = Staff::new(Clef::Percussion);
5291 assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
5292 assert_eq!(percussion.presentation.lines, 5);
5293 assert!(percussion.presentation.visible);
5294 }
5295
5296 #[test]
5299 fn score_default_has_schema_version_1() {
5300 let score = Score::default();
5301 assert_eq!(score.schema_version, 1);
5302 }
5303
5304 #[test]
5305 fn score_new_has_schema_version_1() {
5306 let score = Score::new("T", 120, 4, 4, 0, 4);
5307 assert_eq!(score.schema_version, 1);
5308 }
5309
5310 #[test]
5311 fn score_without_schema_version_deserializes_to_zero() {
5312 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":[]}"#;
5313 let score: Score = serde_json::from_str(json).unwrap();
5314 assert_eq!(score.schema_version, 0);
5315 }
5316
5317 #[test]
5318 fn legacy_score_json_defaults_typed_spanners() {
5319 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5320 let mut value = serde_json::to_value(score).expect("score serializes");
5321 value
5322 .as_object_mut()
5323 .expect("score is an object")
5324 .remove("spanners");
5325 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5326 assert!(restored.spanners.is_empty());
5327 }
5328
5329 #[test]
5330 fn legacy_staff_json_defaults_presentation() {
5331 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5332 let mut value = serde_json::to_value(score).expect("score serializes");
5333 value["parts"][0]["staves"][0]
5334 .as_object_mut()
5335 .expect("staff is an object")
5336 .remove("presentation");
5337
5338 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5339 assert_eq!(
5340 restored.parts[0].staves[0].presentation,
5341 StaffPresentation::default()
5342 );
5343 }
5344
5345 #[test]
5346 fn legacy_part_json_defaults_instrument_definition() {
5347 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5348 let mut value = serde_json::to_value(score).expect("score serializes");
5349 value["parts"][0]
5350 .as_object_mut()
5351 .expect("part is an object")
5352 .remove("instrument");
5353
5354 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5355 assert!(restored.parts[0].instrument.is_none());
5356 }
5357
5358 #[test]
5359 fn resolve_view_projects_linked_part_without_mutating_source_score() {
5360 let mut score = Score::template(ScoreTemplate::StringQuartet);
5361 let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
5362 view.layout.measures_per_row = Some(2);
5363 score.views.push(view);
5364
5365 let projected = score.resolve_view("violin-2").expect("view resolves");
5366 assert_eq!(projected.parts.len(), 1);
5367 assert_eq!(projected.parts[0].name, "Violin II");
5368 assert_eq!(projected.views.len(), 1);
5369 assert_eq!(projected.views[0].parts, vec![0]);
5370 assert_eq!(score.parts.len(), 4);
5371 assert_eq!(score.views[0].parts, vec![1]);
5372 }
5373
5374 #[test]
5375 fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
5376 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5377 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5378 lines: 6,
5379 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5380 capo: 0,
5381 });
5382 score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
5383 score.views.push(ScoreView::linked_tablature_staff(
5384 "guitar-tab",
5385 "Guitar Tab",
5386 0,
5387 0,
5388 ));
5389
5390 let projected = score.resolve_view("guitar-tab").expect("view resolves");
5391 assert_eq!(
5392 projected.parts[0].staves[0].presentation.kind,
5393 StaffKind::Tablature
5394 );
5395 assert_eq!(
5396 projected.views[0].staff_kind_overrides[0].staff,
5397 ViewStaffRef { part: 0, staff: 0 }
5398 );
5399 assert_eq!(
5400 score.parts[0].staves[0].presentation.kind,
5401 StaffKind::Standard
5402 );
5403 }
5404
5405 #[test]
5406 fn legacy_score_view_json_defaults_staff_kind_overrides() {
5407 let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
5408 score.views.push(ScoreView::linked_part("part", "Part", 0));
5409 let mut value = serde_json::to_value(score).expect("score serializes");
5410 value["views"][0]
5411 .as_object_mut()
5412 .expect("view is an object")
5413 .remove("staff_kind_overrides");
5414
5415 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5416 assert!(restored.views[0].staff_kind_overrides.is_empty());
5417 }
5418
5419 #[test]
5420 fn typed_view_style_overrides_are_ordered_and_json_compatible() {
5421 let mut view = ScoreView::linked_part("part", "Part", 0);
5422 view.layout.typed_style_overrides = vec![
5423 ViewStyleOverride {
5424 property: ViewStyleProperty::TextScale,
5425 value: 0.9,
5426 },
5427 ViewStyleOverride {
5428 property: ViewStyleProperty::TextScale,
5429 value: 1.1,
5430 },
5431 ];
5432 assert_eq!(
5433 view.layout.style_value(ViewStyleProperty::TextScale),
5434 Some(1.1)
5435 );
5436 let restored: ScoreView =
5437 serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
5438 assert_eq!(
5439 restored.layout.typed_style_overrides,
5440 view.layout.typed_style_overrides
5441 );
5442 assert_eq!(view.layout.resolved_style().text_scale, 1.1);
5443 assert_eq!(view.layout.resolved_style().system_gap, 2.0);
5444 }
5445
5446 #[test]
5447 fn score_style_defaults_are_inherited_then_overridden_by_view() {
5448 let mut score = Score::new("Style", 120, 4, 4, 0, 1);
5449 score.style_overrides = vec![
5450 ViewStyleOverride {
5451 property: ViewStyleProperty::StaffSpace,
5452 value: 1.2,
5453 },
5454 ViewStyleOverride {
5455 property: ViewStyleProperty::TextScale,
5456 value: 0.9,
5457 },
5458 ];
5459 let mut view = ScoreView::linked_part("part", "Part", 0);
5460 view.layout.typed_style_overrides.push(ViewStyleOverride {
5461 property: ViewStyleProperty::TextScale,
5462 value: 1.1,
5463 });
5464 let style = score.resolved_view_style(&view.layout);
5465 assert_eq!(style.staff_space, 1.2);
5466 assert_eq!(style.text_scale, 1.1);
5467 let restored: Score =
5468 serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
5469 assert_eq!(restored.style_overrides, score.style_overrides);
5470 }
5471
5472 #[test]
5473 fn legacy_measure_json_defaults_source_voice_numbers() {
5474 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5475 let mut value = serde_json::to_value(&score).expect("score serializes");
5476 value["parts"][0]["staves"][0]["measures"][0]
5477 .as_object_mut()
5478 .expect("measure is an object")
5479 .remove("source_voice_numbers");
5480
5481 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5482 assert_eq!(
5483 restored.parts[0].staves[0].measures[0].source_voice_numbers,
5484 [None; 4]
5485 );
5486 }
5487
5488 #[test]
5489 fn legacy_measure_json_defaults_tempo_ramp() {
5490 let measure: Measure = serde_json::from_str(
5491 r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
5492 )
5493 .expect("legacy measure deserializes");
5494 assert_eq!(measure.tempo_ramp_to, None);
5495 }
5496
5497 #[test]
5498 fn note_without_new_percussion_fields_uses_serde_defaults() {
5499 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5500 let mut value = serde_json::to_value(note).unwrap();
5501 let object = value.as_object_mut().unwrap();
5502 object.remove("is_unpitched");
5503 object.remove("instrument_id");
5504 let restored: Note = serde_json::from_value(value).unwrap();
5505 assert!(!restored.is_unpitched);
5506 assert_eq!(restored.instrument_id, None);
5507 }
5508
5509 #[test]
5510 fn percussion_instrument_resolution_prefers_id_then_display_key() {
5511 let mut part = Part::new("Drums", "Dr.");
5512 part.percussion_instruments = vec![
5513 PercussionInstrument {
5514 id: "snare".to_string(),
5515 name: Some("Acoustic Snare".to_string()),
5516 midi_unpitched: Some(38),
5517 staff_position: None,
5518 notehead: None,
5519 preferred_voice: None,
5520 techniques: Vec::new(),
5521 },
5522 PercussionInstrument {
5523 id: "rim".to_string(),
5524 name: Some("Side Stick".to_string()),
5525 midi_unpitched: Some(37),
5526 staff_position: None,
5527 notehead: None,
5528 preferred_voice: None,
5529 techniques: Vec::new(),
5530 },
5531 ];
5532 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
5533 note.is_unpitched = true;
5534 assert_eq!(
5535 part.percussion_instrument_for_note(¬e)
5536 .map(|instrument| instrument.id.as_str()),
5537 Some("snare")
5538 );
5539 note.instrument_id = Some("rim".to_string());
5540 assert_eq!(
5541 part.percussion_instrument_for_note(¬e)
5542 .map(|instrument| instrument.id.as_str()),
5543 Some("rim")
5544 );
5545 note.instrument_id = Some("missing".to_string());
5546 assert!(part.percussion_instrument_for_note(¬e).is_none());
5547 note.instrument_id = None;
5548 note.is_unpitched = false;
5549 assert!(part.percussion_instrument_for_note(¬e).is_none());
5550 }
5551
5552 #[test]
5553 fn percussion_kit_extensions_are_json_backward_compatible() {
5554 let mut instrument = PercussionInstrument {
5555 id: "snare".to_string(),
5556 name: Some("Acoustic Snare".to_string()),
5557 midi_unpitched: Some(38),
5558 staff_position: Some(0),
5559 notehead: Some(NoteHead::Cross),
5560 preferred_voice: Some(1),
5561 techniques: vec!["rim-shot".to_string()],
5562 };
5563 let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
5564 let object = value
5565 .as_object_mut()
5566 .expect("percussion instrument is an object");
5567 object.remove("staff_position");
5568 object.remove("notehead");
5569 object.remove("preferred_voice");
5570 object.remove("techniques");
5571 instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
5572 assert_eq!(instrument.staff_position, None);
5573 assert_eq!(instrument.notehead, None);
5574 assert_eq!(instrument.preferred_voice, None);
5575 assert!(instrument.techniques.is_empty());
5576 }
5577
5578 #[test]
5581 fn score_template_solo_has_one_part_treble() {
5582 let score = Score::template(ScoreTemplate::Solo);
5583 assert_eq!(score.parts.len(), 1);
5584 assert_eq!(score.parts[0].staves.len(), 1);
5585 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5586 assert_eq!(score.parts[0].midi_program, 0);
5587 }
5588
5589 #[test]
5590 fn score_template_piano_has_two_staves() {
5591 let score = Score::template(ScoreTemplate::Piano);
5592 assert_eq!(score.parts.len(), 1);
5593 assert_eq!(score.parts[0].staves.len(), 2);
5594 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5595 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
5596 }
5597
5598 #[test]
5599 fn score_template_string_quartet_has_four_parts() {
5600 let score = Score::template(ScoreTemplate::StringQuartet);
5601 assert_eq!(score.parts.len(), 4);
5602 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);
5605 assert_eq!(score.parts[3].midi_program, 42);
5606 }
5607
5608 #[test]
5609 fn score_template_string_orchestra_has_five_parts() {
5610 let score = Score::template(ScoreTemplate::StringOrchestra);
5611 assert_eq!(score.parts.len(), 5);
5612 assert_eq!(score.parts[4].midi_program, 43); }
5614
5615 #[test]
5616 fn score_template_brass_quintet_has_five_parts() {
5617 let score = Score::template(ScoreTemplate::BrassQuintet);
5618 assert_eq!(score.parts.len(), 5);
5619 assert_eq!(score.parts[2].midi_program, 60); }
5621
5622 #[test]
5623 fn score_template_default_measures_are_four() {
5624 let score = Score::template(ScoreTemplate::StringQuartet);
5625 for part in &score.parts {
5626 for staff in &part.staves {
5627 assert_eq!(staff.measures.len(), 4);
5628 }
5629 }
5630 }
5631
5632 #[test]
5635 fn measure_empty_has_no_breaks() {
5636 let m = Measure::empty(4, 4);
5637 assert!(!m.system_break);
5638 assert!(!m.page_break);
5639 assert!(!m.section_break);
5640 }
5641
5642 #[test]
5643 fn section_range_uses_semantic_boundaries_not_layout_breaks() {
5644 let mut score = Score::new("sections", 120, 4, 4, 0, 6);
5645 score.parts[0].staves[0].measures[0].section_break = true;
5646 score.parts[0].staves[0].measures[2].section_break = true;
5647 score.parts[0].staves[0].measures[4].section_break = true;
5648 score.parts[0].staves[0].measures[5].section_break = true;
5649 score.parts[0].staves[0].measures[1].system_break = true;
5650 score.parts[0].staves[0].measures[3].page_break = true;
5651 assert_eq!(score.section_range(0).unwrap(), 0..=1);
5652 assert_eq!(score.section_range(3).unwrap(), 2..=3);
5653 assert_eq!(score.section_range(4).unwrap(), 4..=4);
5654 assert_eq!(score.section_range(5).unwrap(), 5..=5);
5655 assert!(score.section_range(6).is_err());
5656 }
5657
5658 #[test]
5659 fn system_break_survives_json_roundtrip() {
5660 let mut m = Measure::empty(4, 4);
5661 m.system_break = true;
5662 let json = serde_json::to_string(&m).unwrap();
5663 let m2: Measure = serde_json::from_str(&json).unwrap();
5664 assert!(m2.system_break);
5665 assert!(!m2.page_break);
5666 }
5667
5668 #[test]
5671 fn diff_identical_scores_is_empty() {
5672 let s = Score::new("T", 120, 4, 4, 0, 2);
5673 assert!(diff(&s, &s).is_empty());
5674 }
5675
5676 #[test]
5677 fn score_patch_covers_measure_semantics_and_note_insert_index() {
5678 let mut a = Score::new("T", 120, 4, 4, 0, 1);
5679 a.parts[0].staves[0].measures[0].voices[0].clear();
5680 let mut b = a.clone();
5681 let measure = &mut b.parts[0].staves[0].measures[0];
5682 measure.key_sig = Some(KeySignature {
5683 fifths: -2,
5684 mode: "major".to_string(),
5685 });
5686 measure.time_sig = Some(TimeSignature {
5687 numerator: 3,
5688 denominator: 4,
5689 });
5690 measure.barline_left = Barline::RepeatStart;
5691 measure.barline_right = Barline::RepeatEnd;
5692 measure.rehearsal = Some("A".to_string());
5693 measure.volta = Some(VoltaBracket {
5694 number: 1,
5695 kind: "begin_end".to_string(),
5696 numbers: Vec::new(),
5697 });
5698 measure.texts.push(StyledText {
5699 style: TextStyle::RehearsalMark,
5700 text: "A".to_string(),
5701 placement: None,
5702 offset_x: None,
5703 offset_y: None,
5704 relative_x: None,
5705 relative_y: None,
5706 });
5707 measure.figured_bass.push(FiguredBassFigure {
5708 number: "6".to_string(),
5709 alter: None,
5710 prefix: None,
5711 suffix: None,
5712 extender: false,
5713 });
5714 measure.harp_pedal_diagrams.push(HarpPedalDiagram {
5715 positions: [
5716 HarpPedalPosition::Flat,
5717 HarpPedalPosition::Natural,
5718 HarpPedalPosition::Sharp,
5719 HarpPedalPosition::Natural,
5720 HarpPedalPosition::Flat,
5721 HarpPedalPosition::Sharp,
5722 HarpPedalPosition::Natural,
5723 ],
5724 placement: Some("above".to_string()),
5725 });
5726 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
5727 let expected = b.parts[0].staves[0].measures[0].clone();
5728
5729 let patches = score_patch(&a, &b);
5730 assert!(
5731 patches
5732 .iter()
5733 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
5734 );
5735 assert!(
5736 patches
5737 .iter()
5738 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
5739 );
5740 assert!(
5741 patches
5742 .iter()
5743 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
5744 );
5745 assert!(
5746 patches
5747 .iter()
5748 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
5749 );
5750 assert!(
5751 patches
5752 .iter()
5753 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
5754 );
5755 assert!(
5756 patches
5757 .iter()
5758 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
5759 );
5760 assert!(
5761 patches
5762 .iter()
5763 .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
5764 );
5765 let result = apply_patch(&a, &patches).expect("patch application failed");
5766 let result_measure = &result.parts[0].staves[0].measures[0];
5767 assert_eq!(result_measure.key_sig, expected.key_sig);
5768 assert_eq!(result_measure.time_sig, expected.time_sig);
5769 assert_eq!(result_measure.barline_left, expected.barline_left);
5770 assert_eq!(result_measure.barline_right, expected.barline_right);
5771 assert_eq!(result_measure.rehearsal, expected.rehearsal);
5772 assert_eq!(result_measure.volta, expected.volta);
5773 assert_eq!(result_measure.texts, expected.texts);
5774 assert_eq!(result_measure.figured_bass, expected.figured_bass);
5775 assert_eq!(
5776 result_measure.harp_pedal_diagrams,
5777 expected.harp_pedal_diagrams
5778 );
5779 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
5780 }
5781
5782 #[test]
5783 fn diff_and_patch_preserve_score_level_texts() {
5784 let a = Score::new("T", 120, 4, 4, 0, 1);
5785 let mut b = a.clone();
5786 b.texts.push(StyledText {
5787 style: TextStyle::Expression,
5788 text: "Prelude".to_string(),
5789 placement: Some("above".to_string()),
5790 offset_x: Some(12.0),
5791 offset_y: Some(-8.0),
5792 relative_x: None,
5793 relative_y: None,
5794 });
5795
5796 let changes = diff(&a, &b);
5797 assert!(changes.iter().any(|change| matches!(
5798 change,
5799 ScoreChange::ScoreTextChanged { old, new }
5800 if old.is_empty() && new == &b.texts
5801 )));
5802
5803 let patches = score_patch(&a, &b);
5804 assert!(patches.iter().any(|patch| matches!(
5805 patch,
5806 ScorePatch::SetScoreTexts { value } if value == &b.texts
5807 )));
5808 let result = apply_patch(&a, &patches).expect("score text patch failed");
5809 assert_eq!(result.texts, b.texts);
5810 }
5811
5812 #[test]
5813 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
5814 let a = Score::new("T", 120, 4, 4, 0, 1);
5815 let mut b = a.clone();
5816 let measure = &mut b.parts[0].staves[0].measures[0];
5817 measure.number = 8;
5818 measure.clef = Some(Clef::Bass);
5819 measure.tempo_text = Some("Allegro".to_string());
5820 measure.navigation = Some("D.S.".to_string());
5821 measure.expression_text = Some("espressivo".to_string());
5822 measure.multi_rest_count = Some(3);
5823 measure.system_break = true;
5824 measure.page_break = true;
5825
5826 let changes = diff(&a, &b);
5827 assert!(changes.iter().any(|change| matches!(
5828 change,
5829 ScoreChange::MeasurePresentationChanged {
5830 part: 0,
5831 staff: 0,
5832 measure: 0,
5833 old_number: 1,
5834 new_number: 8,
5835 old_clef: None,
5836 new_clef: Some(Clef::Bass),
5837 new_tempo_text: Some(text),
5838 new_navigation: Some(navigation),
5839 new_expression_text: Some(expression),
5840 new_multi_rest_count: Some(3),
5841 old_system_break: false,
5842 new_system_break: true,
5843 old_page_break: false,
5844 new_page_break: true,
5845 ..
5846 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5847 )));
5848 assert!(!changes.iter().any(|change| matches!(
5849 change,
5850 ScoreChange::UnrepresentedFieldChanged { path }
5851 if path == "parts[0].staves[0].measures[0].number"
5852 )));
5853
5854 let patches = score_patch(&a, &b);
5855 assert!(patches.iter().any(|patch| matches!(
5856 patch,
5857 ScorePatch::SetMeasurePresentation {
5858 part: 0,
5859 staff: 0,
5860 measure: 0,
5861 number: 8,
5862 clef: Some(Clef::Bass),
5863 tempo_text: Some(text),
5864 navigation: Some(navigation),
5865 expression_text: Some(expression),
5866 multi_rest_count: Some(3),
5867 system_break: true,
5868 page_break: true,
5869 section_break: false,
5870 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5871 )));
5872 assert!(
5873 !patches
5874 .iter()
5875 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5876 );
5877 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
5878 let decoded: Vec<ScorePatch> =
5879 serde_json::from_str(&json).expect("measure presentation patch should decode");
5880 let mut legacy_json: serde_json::Value =
5881 serde_json::from_str(&json).expect("patch JSON value");
5882 for patch in legacy_json.as_array_mut().expect("patch JSON array") {
5883 patch
5884 .as_object_mut()
5885 .expect("patch JSON object")
5886 .remove("section_break");
5887 }
5888 let legacy: Vec<ScorePatch> =
5889 serde_json::from_value(legacy_json).expect("legacy patch should decode");
5890 assert!(matches!(
5891 legacy.first(),
5892 Some(ScorePatch::SetMeasurePresentation {
5893 section_break: false,
5894 ..
5895 })
5896 ));
5897 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
5898 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
5899 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
5900 assert_eq!(
5901 serde_json::to_value(&result).expect("patched score JSON"),
5902 serde_json::to_value(&b).expect("expected score JSON")
5903 );
5904 }
5905
5906 #[test]
5907 fn chord_definition_changes_use_typed_diff_and_local_patch() {
5908 let a = Score::new("T", 120, 4, 4, 0, 1);
5909 let mut b = a.clone();
5910 b.chord_definitions.push(ChordDefinition {
5911 id: Some("c-major".to_string()),
5912 label: Some("C".to_string()),
5913 kind: Some("major".to_string()),
5914 fret_position: Some(0),
5915 tab_strings: Some("x32010".to_string()),
5916 tab_courses: None,
5917 members: Vec::new(),
5918 barres: Vec::new(),
5919 });
5920
5921 let changes = diff(&a, &b);
5922 assert!(changes.iter().any(|change| matches!(
5923 change,
5924 ScoreChange::ChordDefinitionsChanged { old, new }
5925 if old.is_empty() && new == &b.chord_definitions
5926 )));
5927 assert!(!changes.iter().any(|change| matches!(
5928 change,
5929 ScoreChange::UnrepresentedFieldChanged { path }
5930 if path == "chord_definitions"
5931 )));
5932
5933 let patches = score_patch(&a, &b);
5934 assert!(patches.iter().any(|patch| matches!(
5935 patch,
5936 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
5937 )));
5938 assert!(
5939 !patches
5940 .iter()
5941 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5942 );
5943 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
5944 let decoded: Vec<ScorePatch> =
5945 serde_json::from_str(&json).expect("chord definition patch should decode");
5946 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
5947 assert_eq!(result.chord_definitions, b.chord_definitions);
5948 }
5949
5950 #[test]
5951 fn part_name_changes_use_typed_diff_and_local_patch() {
5952 let a = Score::new("T", 120, 4, 4, 0, 1);
5953 let mut b = a.clone();
5954 b.parts[0].name = "Violin".to_string();
5955 b.parts[0].short_name = "Vln.".to_string();
5956 b.parts[0].midi_channel = 4;
5957 b.parts[0].midi_program = 40;
5958 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
5959 tick: 120,
5960 channel: 4,
5961 value: 2048,
5962 });
5963 b.parts[0].midi_control_changes.push(MidiControlChange {
5964 tick: 240,
5965 channel: 4,
5966 controller: 64,
5967 value: 127,
5968 });
5969 b.parts[0].midi_program_changes.push(MidiProgramChange {
5970 tick: 0,
5971 channel: 4,
5972 program: 40,
5973 });
5974 b.parts[0].midi_aftertouch.push(MidiAftertouch {
5975 tick: 360,
5976 channel: 4,
5977 key: Some(64),
5978 value: 80,
5979 });
5980 b.parts[0].staves[0].clef = Clef::Bass;
5981 b.parts[0].staves[0].transpose_semitones = -2;
5982
5983 let changes = diff(&a, &b);
5984 assert!(changes.iter().any(|change| matches!(
5985 change,
5986 ScoreChange::PartNamesChanged {
5987 part: 0,
5988 old_name,
5989 new_name,
5990 old_short_name,
5991 new_short_name,
5992 } if old_name == "Piano"
5993 && new_name == "Violin"
5994 && old_short_name == "Pno."
5995 && new_short_name == "Vln."
5996 )));
5997 assert!(changes.iter().any(|change| matches!(
5998 change,
5999 ScoreChange::PartMidiChanged {
6000 part: 0,
6001 old_channel: 0,
6002 new_channel: 4,
6003 old_program: 0,
6004 new_program: 40,
6005 }
6006 )));
6007 assert!(changes.iter().any(|change| matches!(
6008 change,
6009 ScoreChange::PartMidiAutomationChanged {
6010 part: 0,
6011 new_pitch_bends,
6012 new_control_changes,
6013 new_program_changes,
6014 new_aftertouch,
6015 ..
6016 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
6017 && new_control_changes == &b.parts[0].midi_control_changes
6018 && new_program_changes == &b.parts[0].midi_program_changes
6019 && new_aftertouch == &b.parts[0].midi_aftertouch
6020 )));
6021 assert!(changes.iter().any(|change| matches!(
6022 change,
6023 ScoreChange::StaffConfigurationChanged {
6024 part: 0,
6025 staff: 0,
6026 old_clef: Clef::Treble,
6027 new_clef: Clef::Bass,
6028 old_transpose_semitones: 0,
6029 new_transpose_semitones: -2,
6030 }
6031 )));
6032 assert!(!changes.iter().any(|change| matches!(
6033 change,
6034 ScoreChange::UnrepresentedFieldChanged { path }
6035 if path == "parts[0].name"
6036 )));
6037
6038 let patches = score_patch(&a, &b);
6039 assert!(patches.iter().any(|patch| matches!(
6040 patch,
6041 ScorePatch::SetPartNames {
6042 part: 0,
6043 name,
6044 short_name,
6045 } if name == "Violin" && short_name == "Vln."
6046 )));
6047 assert!(
6048 !patches
6049 .iter()
6050 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6051 );
6052 assert!(patches.iter().any(|patch| matches!(
6053 patch,
6054 ScorePatch::SetPartMidi {
6055 part: 0,
6056 channel: 4,
6057 program: 40,
6058 }
6059 )));
6060 assert!(patches.iter().any(|patch| matches!(
6061 patch,
6062 ScorePatch::SetPartMidiAutomation {
6063 part: 0,
6064 pitch_bends,
6065 control_changes,
6066 program_changes,
6067 aftertouch,
6068 } if pitch_bends == &b.parts[0].midi_pitch_bends
6069 && control_changes == &b.parts[0].midi_control_changes
6070 && program_changes == &b.parts[0].midi_program_changes
6071 && aftertouch == &b.parts[0].midi_aftertouch
6072 )));
6073 assert!(patches.iter().any(|patch| matches!(
6074 patch,
6075 ScorePatch::SetStaffConfiguration {
6076 part: 0,
6077 staff: 0,
6078 clef: Clef::Bass,
6079 transpose_semitones: -2,
6080 }
6081 )));
6082 let result = apply_patch(&a, &patches).expect("part name patch failed");
6083 assert_eq!(result.parts[0].name, b.parts[0].name);
6084 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
6085 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
6086 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
6087 assert_eq!(
6088 result.parts[0].midi_pitch_bends,
6089 b.parts[0].midi_pitch_bends
6090 );
6091 assert_eq!(
6092 result.parts[0].midi_control_changes,
6093 b.parts[0].midi_control_changes
6094 );
6095 assert_eq!(
6096 result.parts[0].midi_program_changes,
6097 b.parts[0].midi_program_changes
6098 );
6099 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
6100 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
6101 assert_eq!(
6102 result.parts[0].staves[0].transpose_semitones,
6103 b.parts[0].staves[0].transpose_semitones
6104 );
6105 }
6106
6107 #[test]
6108 fn diff_reports_measure_text_and_figured_bass_changes() {
6109 let a = Score::new("T", 120, 4, 4, 0, 1);
6110 let mut b = a.clone();
6111 let measure = &mut b.parts[0].staves[0].measures[0];
6112 measure.texts.push(StyledText {
6113 style: TextStyle::Lyrics,
6114 text: "la".to_string(),
6115 placement: None,
6116 offset_x: None,
6117 offset_y: None,
6118 relative_x: None,
6119 relative_y: None,
6120 });
6121 measure.figured_bass.push(FiguredBassFigure {
6122 number: "6".to_string(),
6123 alter: None,
6124 prefix: None,
6125 suffix: None,
6126 extender: false,
6127 });
6128
6129 let changes = diff(&a, &b);
6130 assert!(changes.iter().any(|change| matches!(
6131 change,
6132 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
6133 if old.is_empty() && new.len() == 1
6134 )));
6135 assert!(changes.iter().any(|change| matches!(
6136 change,
6137 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
6138 if old.is_empty() && new.len() == 1
6139 )));
6140 }
6141
6142 #[test]
6143 fn diff_reports_tablature_changes_and_patches_them_locally() {
6144 let a = Score::new("T", 120, 4, 4, 0, 1);
6145 let mut b = a.clone();
6146 b.parts[0].staves[0].tablature = Some(TablatureConfig {
6147 lines: 6,
6148 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6149 capo: 2,
6150 });
6151
6152 let changes = diff(&a, &b);
6153 assert!(!changes.iter().any(|change| matches!(
6154 change,
6155 ScoreChange::UnrepresentedFieldChanged { path }
6156 if path == "parts[0].staves[0].tablature"
6157 )));
6158 assert!(changes.iter().any(|change| matches!(
6159 change,
6160 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
6161 if config.lines == 6 && config.capo == 2
6162 )));
6163 let patches = score_patch(&a, &b);
6164 assert!(patches.iter().any(|patch| matches!(
6165 patch,
6166 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
6167 if config.lines == 6 && config.capo == 2
6168 )));
6169 assert_eq!(
6170 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
6171 b.parts[0].staves[0].tablature
6172 );
6173 }
6174
6175 #[test]
6176 fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
6177 let mut a = Score::new("T", 120, 4, 4, 0, 2);
6178 a.parts[0].staves[0].tablature = Some(TablatureConfig {
6179 lines: 6,
6180 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6181 capo: 0,
6182 });
6183 let mut b = a.clone();
6184 b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
6185 lines: 6,
6186 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6187 capo: 3,
6188 });
6189
6190 let changes = diff(&a, &b);
6191 assert!(changes.iter().any(|change| matches!(
6192 change,
6193 ScoreChange::TablatureChangeChanged {
6194 part: 0,
6195 staff: 0,
6196 measure: 1,
6197 old: None,
6198 new: Some(config),
6199 } if config.capo == 3
6200 )));
6201 let patches = score_patch(&a, &b);
6202 assert!(patches.iter().any(|patch| matches!(
6203 patch,
6204 ScorePatch::SetMeasureTablatureChange {
6205 part: 0,
6206 staff: 0,
6207 measure: 1,
6208 value: Some(config),
6209 } if config.capo == 3
6210 )));
6211 assert_eq!(
6212 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
6213 b.parts[0].staves[0].measures[1].tablature_change
6214 );
6215 }
6216
6217 #[test]
6218 fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
6219 let a = Score::new("Ramp", 120, 4, 4, 0, 2);
6220 let mut b = a.clone();
6221 b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
6222
6223 assert!(diff(&a, &b).iter().any(|change| matches!(
6224 change,
6225 ScoreChange::MeasureTempoRampChanged {
6226 part: 0,
6227 staff: 0,
6228 measure: 1,
6229 old: None,
6230 new: Some(72),
6231 }
6232 )));
6233 let patches = score_patch(&a, &b);
6234 assert!(patches.iter().any(|patch| matches!(
6235 patch,
6236 ScorePatch::SetMeasureTempoRamp {
6237 part: 0,
6238 staff: 0,
6239 measure: 1,
6240 value: Some(72),
6241 }
6242 )));
6243 assert_eq!(
6244 apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
6245 .tempo_ramp_to,
6246 b.parts[0].staves[0].measures[1].tempo_ramp_to
6247 );
6248 }
6249
6250 #[test]
6251 fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
6252 let a = Score::new("T", 120, 4, 4, 0, 1);
6253 let mut b = a.clone();
6254 b.parts[0].staves[0].presentation = StaffPresentation {
6255 kind: StaffKind::Percussion,
6256 lines: 1,
6257 line_distance: 1.5,
6258 small: true,
6259 cutaway: true,
6260 visible: false,
6261 notehead_scheme: StaffNoteheadScheme::PitchNames,
6262 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
6263 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
6264 };
6265
6266 let changes = diff(&a, &b);
6267 assert!(changes.iter().any(|change| matches!(
6268 change,
6269 ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
6270 if new == &b.parts[0].staves[0].presentation
6271 )));
6272 let patches = score_patch(&a, &b);
6273 assert!(patches.iter().any(|patch| matches!(
6274 patch,
6275 ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
6276 if value == &b.parts[0].staves[0].presentation
6277 )));
6278 assert!(
6279 !patches
6280 .iter()
6281 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6282 );
6283 assert_eq!(
6284 apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
6285 b.parts[0].staves[0].presentation
6286 );
6287 }
6288
6289 #[test]
6290 fn score_patch_uses_local_presentation_patch_for_display_fields() {
6291 let a = Score::new("T", 120, 4, 4, 0, 1);
6292 let mut b = a.clone();
6293 b.parts[0].name = "Piano".to_string();
6294 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
6295 let patches = score_patch(&a, &b);
6296 assert!(patches.iter().any(|patch| matches!(
6297 patch,
6298 ScorePatch::SetMeasurePresentation {
6299 part: 0,
6300 staff: 0,
6301 measure: 0,
6302 expression_text: Some(text),
6303 ..
6304 } if text == "dolce"
6305 )));
6306 assert!(
6307 !patches
6308 .iter()
6309 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6310 );
6311 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
6312 assert_eq!(result.parts[0].name, "Piano");
6313 assert_eq!(
6314 result.parts[0].staves[0].measures[0].expression_text,
6315 Some("dolce".to_string())
6316 );
6317 }
6318
6319 #[test]
6320 fn diff_detects_tempo_change() {
6321 let a = Score::new("T", 120, 4, 4, 0, 1);
6322 let mut b = a.clone();
6323 b.settings.tempo_bpm = 90;
6324 let changes = diff(&a, &b);
6325 assert_eq!(changes.len(), 1);
6326 assert!(matches!(
6327 changes[0],
6328 ScoreChange::TempoChanged { old: 120, new: 90 }
6329 ));
6330 }
6331
6332 #[test]
6333 fn diff_detects_title_change() {
6334 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
6335 let mut b = a.clone();
6336 b.metadata.title = "New Title".to_string();
6337 let changes = diff(&a, &b);
6338 assert!(
6339 changes.iter().any(
6340 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
6341 )
6342 );
6343 }
6344
6345 #[test]
6346 fn diff_detects_note_modification() {
6347 let mut a = Score::new("T", 120, 4, 4, 0, 1);
6348 a.parts[0].staves[0].measures[0].voices[0] =
6349 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6350 let mut b = a.clone();
6351 b.parts[0].staves[0].measures[0].voices[0][0] =
6352 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
6353 let changes = diff(&a, &b);
6354 assert!(
6355 changes
6356 .iter()
6357 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
6358 );
6359 }
6360
6361 #[test]
6362 fn diff_detects_part_added() {
6363 let a = Score::new("T", 120, 4, 4, 0, 1);
6364 let mut b = a.clone();
6365 let mut p = Part::new("Violin", "Vln.");
6366 p.staves.push(Staff::new(Clef::Treble));
6367 b.parts.push(p);
6368 let changes = diff(&a, &b);
6369 assert!(
6370 changes
6371 .iter()
6372 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
6373 );
6374 }
6375
6376 #[test]
6377 fn diff_detects_measure_tempo_change() {
6378 let a = Score::new("T", 120, 4, 4, 0, 2);
6379 let mut b = a.clone();
6380 b.parts[0].staves[0].measures[1].tempo = Some(60);
6381 let changes = diff(&a, &b);
6382 assert!(changes.iter().any(|c| matches!(
6383 c,
6384 ScoreChange::MeasureTempoChanged {
6385 measure: 1,
6386 old: None,
6387 new: Some(60),
6388 ..
6389 }
6390 )));
6391 }
6392
6393 #[test]
6394 fn diff_detects_barline_change() {
6395 use crate::model::notation::Barline;
6396 let a = Score::new("T", 120, 4, 4, 0, 2);
6397 let mut b = a.clone();
6398 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
6399 let changes = diff(&a, &b);
6400 assert!(
6401 changes
6402 .iter()
6403 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
6404 );
6405 }
6406
6407 #[test]
6408 fn diff_detects_rehearsal_change() {
6409 let a = Score::new("T", 120, 4, 4, 0, 2);
6410 let mut b = a.clone();
6411 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
6412 let changes = diff(&a, &b);
6413 assert!(
6414 changes
6415 .iter()
6416 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
6417 );
6418 }
6419
6420 #[test]
6421 fn diff_detects_volta_change() {
6422 use super::VoltaBracket;
6423 let a = Score::new("T", 120, 4, 4, 0, 2);
6424 let mut b = a.clone();
6425 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
6426 number: 1,
6427 kind: "begin_end".into(),
6428 numbers: Vec::new(),
6429 });
6430 let changes = diff(&a, &b);
6431 assert!(
6432 changes
6433 .iter()
6434 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
6435 );
6436 }
6437
6438 #[test]
6439 fn diff_detects_key_signature_change() {
6440 let a = Score::new("T", 120, 4, 4, 0, 1);
6441 let mut b = a.clone();
6442 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
6444 assert!(
6445 changes
6446 .iter()
6447 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
6448 );
6449 }
6450
6451 #[test]
6452 fn measure_key_signature_uses_typed_local_patch() {
6453 let a = Score::new("T", 120, 4, 4, 0, 1);
6454 let mut b = a.clone();
6455 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
6456 fifths: 2,
6457 mode: "major".to_string(),
6458 });
6459 let changes = diff(&a, &b);
6460 assert!(!changes.iter().any(|change| matches!(
6461 change,
6462 ScoreChange::UnrepresentedFieldChanged { path }
6463 if path == "parts[0].staves[0].measures[0].key_sig"
6464 )));
6465 let patches = score_patch(&a, &b);
6466 assert!(patches.iter().any(|patch| matches!(
6467 patch,
6468 ScorePatch::SetKeySignature {
6469 part: 0,
6470 staff: 0,
6471 measure: 0,
6472 value: Some(KeySignature { fifths: 2, .. }),
6473 }
6474 )));
6475 assert!(
6476 !patches
6477 .iter()
6478 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6479 );
6480 }
6481
6482 #[test]
6483 fn diff_same_key_signature_no_change() {
6484 let a = Score::new("T", 120, 4, 4, 2, 1);
6485 let changes = diff(&a, &a);
6486 assert!(changes.is_empty());
6487 }
6488
6489 #[test]
6490 fn score_duration_secs_region_partial() {
6491 use super::score_duration_secs_region;
6492 let score = Score::new("T", 120, 4, 4, 0, 4);
6494 let secs = score_duration_secs_region(&score, (1, 2));
6495 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
6496 }
6497
6498 #[test]
6499 fn score_duration_secs_region_single_measure() {
6500 use super::score_duration_secs_region;
6501 let score = Score::new("T", 120, 4, 4, 0, 4);
6503 let secs = score_duration_secs_region(&score, (0, 0));
6504 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
6505 }
6506
6507 #[test]
6510 fn measure_beats_remaining_empty_voice_returns_full() {
6511 use super::measure_beats_remaining;
6512 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6513 score.parts[0].staves[0].measures[0].voices[0].clear();
6514 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6515 assert!(
6516 (rem - 4.0).abs() < 1e-9,
6517 "expected 4.0 remaining, got {rem}"
6518 );
6519 }
6520
6521 #[test]
6522 fn measure_beats_remaining_half_full_returns_half() {
6523 use super::measure_beats_remaining;
6524 use crate::model::pitch::Step;
6525 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6526 score.parts[0].staves[0].measures[0].voices[0] = vec![
6527 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6528 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
6529 ];
6530 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6531 assert!(
6532 (rem - 2.0).abs() < 1e-9,
6533 "expected 2.0 remaining, got {rem}"
6534 );
6535 }
6536
6537 #[test]
6538 fn measure_beats_remaining_full_voice_returns_zero() {
6539 use super::measure_beats_remaining;
6540 use crate::model::pitch::Step;
6541 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6542 score.parts[0].staves[0].measures[0].voices[0] =
6543 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
6544 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6545 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
6546 }
6547
6548 #[test]
6549 fn measure_beats_remaining_tuplet_accounting() {
6550 use super::measure_beats_remaining;
6551 use crate::model::notation::TupletInfo;
6552 use crate::model::pitch::Step;
6553 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6555 let tuplet = TupletInfo {
6556 actual_notes: 3,
6557 normal_notes: 2,
6558 };
6559 let mk = |step| {
6560 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
6561 n.tuplet = Some(tuplet.clone());
6562 n
6563 };
6564 score.parts[0].staves[0].measures[0].voices[0] =
6565 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
6566 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6567 assert!(
6568 (rem - 2.0).abs() < 1e-9,
6569 "expected 2.0 remaining (triplets used 2.0), got {rem}"
6570 );
6571 }
6572
6573 #[test]
6574 fn measure_beats_remaining_out_of_range_returns_err() {
6575 use super::measure_beats_remaining;
6576 let score = Score::new("T", 120, 4, 4, 0, 1);
6577 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
6578 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
6579 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
6580 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
6581 }
6582
6583 #[test]
6584 fn note_content_eq_ignores_id() {
6585 use crate::model::pitch::Step;
6586 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
6587 let mut b = a.clone();
6588 b.id = "different-id".to_string();
6589 assert!(note_content_eq(&a, &b));
6590 b.pitches[0] = Pitch::new(Step::D, 4);
6592 assert!(!note_content_eq(&a, &b));
6593 let mut c = a.clone();
6595 a.stem_up = Some(true);
6596 c.stem_up = Some(false);
6597 assert!(!note_content_eq(&a, &c));
6598 }
6599
6600 #[test]
6601 fn suggested_stem_up_below_middle() {
6602 use crate::model::notation::Clef;
6603 let pitches = vec![Pitch::new(Step::C, 4)];
6605 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6606 }
6607
6608 #[test]
6609 fn suggested_stem_up_above_middle() {
6610 use crate::model::notation::Clef;
6611 let pitches = vec![Pitch::new(Step::G, 5)];
6613 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6614 }
6615
6616 #[test]
6617 fn suggested_stem_up_at_middle_line() {
6618 use crate::model::notation::Clef;
6619 let pitches = vec![Pitch::new(Step::B, 4)];
6621 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6622 }
6623
6624 #[test]
6625 fn suggested_stem_up_chord() {
6626 use crate::model::notation::Clef;
6627 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
6629 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6630 }
6631
6632 #[test]
6633 fn suggested_stem_up_bass_clef() {
6634 use crate::model::notation::Clef;
6635 let pitches = vec![Pitch::new(Step::D, 3)];
6637 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
6638 let pitches2 = vec![Pitch::new(Step::C, 3)];
6640 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
6641 }
6642
6643 #[test]
6644 fn suggested_stem_up_empty_pitches() {
6645 use crate::model::notation::Clef;
6646 assert!(suggested_stem_up(&[], &Clef::Treble));
6647 }
6648
6649 fn eighth(pitch: Pitch) -> Note {
6650 Note::new(pitch, Duration::Eighth)
6651 }
6652 fn quarter(pitch: Pitch) -> Note {
6653 Note::new(pitch, Duration::Quarter)
6654 }
6655 fn rest_eighth() -> Note {
6656 Note::rest(Duration::Eighth)
6657 }
6658
6659 #[test]
6660 fn compute_beams_4_4_four_eighths() {
6661 use crate::model::notation::{Clef, TimeSignature};
6662 let _ = Clef::Treble; let ts = TimeSignature {
6664 numerator: 4,
6665 denominator: 4,
6666 };
6667 let c4 = Pitch::new(Step::C, 4);
6668 let notes = vec![
6669 eighth(c4.clone()),
6670 eighth(c4.clone()),
6671 eighth(c4.clone()),
6672 eighth(c4.clone()),
6673 ];
6674 let beams = compute_beams(¬es, &ts);
6675 assert_eq!(beams[0], BeamState::Begin);
6677 assert_eq!(beams[1], BeamState::End);
6678 assert_eq!(beams[2], BeamState::Begin);
6679 assert_eq!(beams[3], BeamState::End);
6680 }
6681
6682 #[test]
6683 fn compute_beams_4_4_all_eighth_one_group() {
6684 use crate::model::notation::TimeSignature;
6685 let ts = TimeSignature {
6686 numerator: 4,
6687 denominator: 4,
6688 };
6689 let c4 = Pitch::new(Step::C, 4);
6690 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
6692 let beams = compute_beams(¬es, &ts);
6693 assert_eq!(beams[0], BeamState::Begin);
6694 assert_eq!(beams[1], BeamState::End);
6695 }
6696
6697 #[test]
6698 fn compute_beams_quarter_not_beamed() {
6699 use crate::model::notation::TimeSignature;
6700 let ts = TimeSignature {
6701 numerator: 4,
6702 denominator: 4,
6703 };
6704 let c4 = Pitch::new(Step::C, 4);
6705 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
6706 let beams = compute_beams(¬es, &ts);
6707 assert_eq!(beams[0], BeamState::None);
6708 assert_eq!(beams[1], BeamState::None);
6709 }
6710
6711 #[test]
6712 fn compute_beams_rest_breaks_beam() {
6713 use crate::model::notation::TimeSignature;
6714 let ts = TimeSignature {
6715 numerator: 4,
6716 denominator: 4,
6717 };
6718 let c4 = Pitch::new(Step::C, 4);
6719 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
6720 let beams = compute_beams(¬es, &ts);
6721 assert_eq!(beams[0], BeamState::None);
6723 assert_eq!(beams[1], BeamState::None);
6724 assert_eq!(beams[2], BeamState::None);
6725 }
6726
6727 #[test]
6728 fn compute_beams_6_8_compound() {
6729 use crate::model::notation::TimeSignature;
6730 let ts = TimeSignature {
6731 numerator: 6,
6732 denominator: 8,
6733 };
6734 let c4 = Pitch::new(Step::C, 4);
6735 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
6737 let beams = compute_beams(¬es, &ts);
6738 assert_eq!(beams[0], BeamState::Begin);
6739 assert_eq!(beams[1], BeamState::Continue);
6740 assert_eq!(beams[2], BeamState::End);
6741 assert_eq!(beams[3], BeamState::Begin);
6742 assert_eq!(beams[4], BeamState::Continue);
6743 assert_eq!(beams[5], BeamState::End);
6744 }
6745
6746 #[test]
6747 fn compute_beams_single_eighth() {
6748 use crate::model::notation::TimeSignature;
6749 let ts = TimeSignature {
6750 numerator: 4,
6751 denominator: 4,
6752 };
6753 let c4 = Pitch::new(Step::C, 4);
6754 let notes = vec![eighth(c4.clone())];
6755 let beams = compute_beams(¬es, &ts);
6756 assert_eq!(beams[0], BeamState::None);
6757 }
6758}
6759
6760#[cfg(test)]
6761mod mid_clef_tests {
6762 use super::*;
6763 use crate::{Command, JoinMeasuresCmd, ScoreEngine, SetMidMeasureClefsCmd, SplitMeasureCmd};
6764
6765 fn quarter_notes() -> Vec<Note> {
6766 [Step::C, Step::D, Step::E, Step::F]
6767 .into_iter()
6768 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter))
6769 .collect()
6770 }
6771
6772 fn change(beats: f64, clef: Clef) -> MidMeasureClef {
6773 MidMeasureClef {
6774 offset: MeasureLength::from_beats(beats).unwrap(),
6775 clef,
6776 }
6777 }
6778
6779 #[test]
6780 fn measure_length_from_beats_is_exact_for_tuplet_subdivisions() {
6781 let third = MeasureLength::from_beats(1.0 / 3.0).unwrap();
6782 assert_eq!((third.numerator, third.denominator), (1, 12));
6783 let two = MeasureLength::from_beats(2.0).unwrap();
6784 assert_eq!((two.numerator, two.denominator), (1, 2));
6785 assert!(MeasureLength::from_beats(0.0).is_none());
6786 }
6787
6788 #[test]
6789 fn clef_at_follows_mid_bar_changes_and_json_omits_an_empty_list() {
6790 let mut measure = Measure::empty(4, 4);
6791 let json = serde_json::to_string(&measure).unwrap();
6792 assert!(!json.contains("mid_clefs"));
6793 measure.mid_clefs = vec![change(2.0, Clef::Bass)];
6794 assert_eq!(measure.clef_at(&Clef::Treble, 1.0), Clef::Treble);
6795 assert_eq!(measure.clef_at(&Clef::Treble, 2.0), Clef::Bass);
6796 let restored: Measure =
6797 serde_json::from_str(&serde_json::to_string(&measure).unwrap()).unwrap();
6798 assert_eq!(restored.mid_clefs, measure.mid_clefs);
6799 }
6800
6801 #[test]
6802 fn validation_rejects_mid_bar_clefs_outside_the_bar_or_out_of_order() {
6803 let mut score = Score::new("clefs", 120, 4, 4, 0, 1);
6804 score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6805 score.parts[0].staves[0].measures[0].mid_clefs =
6806 vec![change(2.0, Clef::Bass), change(1.0, Clef::Treble)];
6807 let errors = crate::validate(&score).errors;
6808 assert!(errors.iter().any(|error| matches!(
6809 error,
6810 crate::ValidationError::InvalidMidMeasureClef { index: 1, .. }
6811 )));
6812 score.parts[0].staves[0].measures[0].mid_clefs = vec![change(4.0, Clef::Bass)];
6813 assert!(!crate::validate(&score).errors.is_empty());
6814 }
6815
6816 #[test]
6817 fn command_split_join_diff_and_patch_keep_mid_bar_clefs() {
6818 let mut engine = ScoreEngine::new();
6819 engine.score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6820 engine
6821 .apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6822 part_index: 0,
6823 staff_index: 0,
6824 measure_index: 0,
6825 clefs: vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)],
6826 }))
6827 .unwrap();
6828 let with_clefs = engine.score.clone();
6829 let plain = {
6830 let mut score = with_clefs.clone();
6831 score.parts[0].staves[0].measures[0].mid_clefs.clear();
6832 score
6833 };
6834 assert!(diff(&plain, &with_clefs).iter().any(|change| matches!(
6835 change,
6836 ScoreChange::MidMeasureClefsChanged { measure: 0, .. }
6837 )));
6838 let patched = apply_patch(&plain, &score_patch(&plain, &with_clefs)).unwrap();
6839 assert_eq!(
6840 patched.parts[0].staves[0].measures[0].mid_clefs,
6841 with_clefs.parts[0].staves[0].measures[0].mid_clefs
6842 );
6843
6844 engine
6845 .apply(Command::SplitMeasure(SplitMeasureCmd {
6846 measure_index: 0,
6847 split_at_beats: 1.0,
6848 }))
6849 .unwrap();
6850 let measures = &engine.score.parts[0].staves[0].measures;
6851 assert!(measures[0].mid_clefs.is_empty());
6852 assert_eq!(measures[1].clef, Some(Clef::Bass));
6853 assert_eq!(measures[1].mid_clefs, vec![change(2.0, Clef::Alto)]);
6854
6855 engine
6856 .apply(Command::JoinMeasures(JoinMeasuresCmd { measure_index: 0 }))
6857 .unwrap();
6858 let measure = &engine.score.parts[0].staves[0].measures[0];
6859 assert_eq!(
6860 measure.mid_clefs,
6861 vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)]
6862 );
6863
6864 let invalid = engine.apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6865 part_index: 0,
6866 staff_index: 0,
6867 measure_index: 0,
6868 clefs: vec![change(0.0 + 5.0, Clef::Bass)],
6869 }));
6870 assert!(invalid.is_err());
6871 }
6872}
6873
6874#[cfg(test)]
6875mod percussion_key_tests {
6876 use super::*;
6877
6878 #[test]
6879 fn unpitched_chord_members_sound_their_kit_keys_by_display_position() {
6880 let mut part = Part::new("Drums", "Dr.");
6881 part.midi_channel = 9;
6882 let kit = |id: &str, key: u8, display: Pitch| PercussionInstrument {
6883 id: id.into(),
6884 name: None,
6885 midi_unpitched: Some(key),
6886 staff_position: Some(PercussionInstrument::display_position(&display)),
6887 notehead: None,
6888 preferred_voice: None,
6889 techniques: Vec::new(),
6890 };
6891 part.percussion_instruments = vec![
6892 kit("kick", 35, Pitch::new(Step::F, 4)),
6893 kit("snare", 38, Pitch::new(Step::C, 5)),
6894 ];
6895 let mut note = Note::new(Pitch::new(Step::C, 5), Duration::Quarter);
6896 note.pitches.push(Pitch::new(Step::F, 4));
6897 note.is_unpitched = true;
6898 note.instrument_id = Some("snare".into());
6899 assert_eq!(part.percussion_key(¬e, 0), Some(38));
6900 assert_eq!(part.percussion_key(¬e, 1), Some(35));
6901 note.is_unpitched = false;
6902 assert_eq!(part.percussion_key(¬e, 0), None);
6903 assert_eq!(
6904 PercussionInstrument::display_position(&Pitch::new(Step::B, 4)),
6905 0
6906 );
6907 }
6908}