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