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