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