1use super::{
2 duration::Duration,
3 notation::{
4 Articulation, Barline, BeamState, ChordDefinition, ChordSymbol, Clef, CrossStaff, Dynamic,
5 FiguredBassFigure, GuitarTechnique, HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind,
6 StyledText, TabPosition, TablatureConfig, TimeSignature, TupletInfo,
7 },
8 pitch::Pitch,
9};
10use crate::Error;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ScoreMetadata {
16 pub title: String,
17 pub composer: String,
18 pub lyricist: String,
19 pub copyright: String,
20 pub work_number: String,
21 pub movement_title: String,
22}
23
24impl Default for ScoreMetadata {
25 fn default() -> Self {
26 Self {
27 title: "Untitled Score".to_string(),
28 composer: String::new(),
29 lyricist: String::new(),
30 copyright: String::new(),
31 work_number: String::new(),
32 movement_title: String::new(),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScoreSettings {
39 pub tempo_bpm: u16,
40 pub time_signature: TimeSignature,
41 pub key_signature: KeySignature,
42}
43
44impl Default for ScoreSettings {
45 fn default() -> Self {
46 Self {
47 tempo_bpm: 120,
48 time_signature: TimeSignature::default(),
49 key_signature: KeySignature::default(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum PartGroupSymbol {
57 Bracket, Brace, Line, }
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PartGroup {
65 pub first_part: usize,
67 pub last_part: usize,
69 pub symbol: PartGroupSymbol,
70 #[serde(default)]
72 pub barlines_connect: bool,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct StaffGroup {
82 pub first_staff: usize,
84 pub last_staff: usize,
86 pub symbol: PartGroupSymbol,
87 #[serde(default)]
89 pub barlines_connect: bool,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Score {
94 pub id: String,
95 #[serde(default)]
97 pub schema_version: u32,
98 pub metadata: ScoreMetadata,
99 pub settings: ScoreSettings,
100 pub parts: Vec<Part>,
101 #[serde(default)]
102 pub part_groups: Vec<PartGroup>,
103 #[serde(default)]
105 pub texts: Vec<StyledText>,
106 #[serde(default)]
108 pub style_overrides: Vec<ViewStyleOverride>,
109 #[serde(default)]
111 pub object_style_overrides: Vec<ObjectStyleOverride>,
112 #[serde(default)]
114 pub chord_definitions: Vec<ChordDefinition>,
115 #[serde(default)]
117 pub spanners: Vec<NotationSpanner>,
118 #[serde(default)]
120 pub views: Vec<ScoreView>,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub struct ViewStaffRef {
126 pub part: usize,
127 pub staff: usize,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
132#[serde(rename_all = "snake_case")]
133pub enum ViewTranspositionPolicy {
134 #[default]
135 Written,
136 Concert,
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
141pub struct ScoreViewLayoutOverrides {
142 #[serde(default)]
143 pub measures_per_row: Option<usize>,
144 #[serde(default)]
145 pub hidden_staves: Vec<ViewStaffRef>,
146 #[serde(default)]
147 pub system_breaks: Vec<usize>,
148 #[serde(default)]
149 pub page_breaks: Vec<usize>,
150 #[serde(default)]
152 pub style_overrides: Vec<(String, String)>,
153 #[serde(default)]
155 pub typed_style_overrides: Vec<ViewStyleOverride>,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum ViewStyleProperty {
162 StaffSpace,
163 TextScale,
164 AnnotationGap,
165 SystemGap,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
171pub struct ViewStyleOverride {
172 pub property: ViewStyleProperty,
173 pub value: f32,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case", tag = "kind")]
179pub enum ObjectStyleTarget {
180 ScoreText {
181 text_index: usize,
182 },
183 MeasureText {
184 part: usize,
185 staff: usize,
186 measure: usize,
187 text_index: usize,
188 },
189 Note {
190 address: NoteAddr,
191 },
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct StyleImportProvenance {
197 pub format: String,
198 pub source_location: String,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct ObjectStyleOverride {
204 pub target: ObjectStyleTarget,
205 pub property: ViewStyleProperty,
206 pub value: f32,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub provenance: Option<StyleImportProvenance>,
209}
210
211impl ScoreViewLayoutOverrides {
212 pub fn style_value(&self, property: ViewStyleProperty) -> Option<f32> {
214 self.typed_style_overrides
215 .iter()
216 .rev()
217 .find(|override_| override_.property == property)
218 .map(|override_| override_.value)
219 }
220
221 pub fn resolved_style(&self) -> ViewStyle {
223 let mut style = ViewStyle::default();
224 apply_style_overrides(&mut style, &self.typed_style_overrides);
225 style
226 }
227}
228
229fn apply_style_overrides(style: &mut ViewStyle, overrides: &[ViewStyleOverride]) {
230 for override_ in overrides {
231 match override_.property {
232 ViewStyleProperty::StaffSpace => style.staff_space = override_.value,
233 ViewStyleProperty::TextScale => style.text_scale = override_.value,
234 ViewStyleProperty::AnnotationGap => style.annotation_gap = override_.value,
235 ViewStyleProperty::SystemGap => style.system_gap = override_.value,
236 }
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
245pub struct ViewStyle {
246 pub staff_space: f32,
247 pub text_scale: f32,
248 pub annotation_gap: f32,
249 pub system_gap: f32,
250}
251
252impl Default for ViewStyle {
253 fn default() -> Self {
254 Self {
255 staff_space: 1.0,
256 text_scale: 1.0,
257 annotation_gap: 1.0,
258 system_gap: 2.0,
259 }
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265pub struct ViewStaffKindOverride {
266 pub staff: ViewStaffRef,
267 pub kind: StaffKind,
268}
269
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272pub struct ScoreView {
273 pub id: String,
274 pub name: String,
275 #[serde(default)]
276 pub parts: Vec<usize>,
277 #[serde(default)]
278 pub transposition_policy: ViewTranspositionPolicy,
279 #[serde(default)]
281 pub staff_kind_overrides: Vec<ViewStaffKindOverride>,
282 #[serde(default)]
283 pub layout: ScoreViewLayoutOverrides,
284}
285
286impl ScoreView {
287 pub fn linked_part(id: impl Into<String>, name: impl Into<String>, part: usize) -> Self {
289 Self {
290 id: id.into(),
291 name: name.into(),
292 parts: vec![part],
293 transposition_policy: ViewTranspositionPolicy::Written,
294 staff_kind_overrides: Vec::new(),
295 layout: ScoreViewLayoutOverrides::default(),
296 }
297 }
298
299 pub fn linked_tablature_staff(
301 id: impl Into<String>,
302 name: impl Into<String>,
303 part: usize,
304 staff: usize,
305 ) -> Self {
306 let mut view = Self::linked_part(id, name, part);
307 view.staff_kind_overrides.push(ViewStaffKindOverride {
308 staff: ViewStaffRef { part, staff },
309 kind: StaffKind::Tablature,
310 });
311 view
312 }
313
314 pub fn linked_standard_staff(
316 id: impl Into<String>,
317 name: impl Into<String>,
318 part: usize,
319 staff: usize,
320 ) -> Self {
321 let mut view = Self::linked_part(id, name, part);
322 view.staff_kind_overrides.push(ViewStaffKindOverride {
323 staff: ViewStaffRef { part, staff },
324 kind: StaffKind::Standard,
325 });
326 view
327 }
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
332pub enum NotationSpannerKind {
333 Slur,
334 Glissando,
335 TrillLine,
336 Pedal,
337 Ottava,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct NotationSpanner {
343 pub id: String,
344 pub kind: NotationSpannerKind,
345 pub start: NoteAddr,
346 pub end: NoteAddr,
347 #[serde(default)]
348 pub number: Option<u16>,
349 #[serde(default)]
350 pub line_type: Option<String>,
351 #[serde(default)]
352 pub text: Option<String>,
353 #[serde(default)]
354 pub placement: Option<String>,
355 #[serde(default)]
356 pub ottava_size: Option<u8>,
357 #[serde(default)]
359 pub ottava_type: Option<String>,
360}
361
362impl Default for Score {
363 fn default() -> Self {
364 let mut part = Part::new("Piano", "Pno.");
365 part.staves.push(Staff::new(Clef::Treble));
366 for _ in 0..4 {
367 part.staves[0].measures.push(Measure::empty(4, 4));
368 }
369 Self {
370 id: Uuid::new_v4().to_string(),
371 schema_version: 1,
372 metadata: ScoreMetadata::default(),
373 settings: ScoreSettings::default(),
374 parts: vec![part],
375 part_groups: Vec::new(),
376 texts: Vec::new(),
377 style_overrides: Vec::new(),
378 object_style_overrides: Vec::new(),
379 chord_definitions: Vec::new(),
380 spanners: Vec::new(),
381 views: Vec::new(),
382 }
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
388pub enum ScoreTemplate {
389 Solo,
391 Piano,
393 StringQuartet,
395 StringOrchestra,
397 BrassQuintet,
399}
400
401impl Score {
402 pub fn resolved_view_style(&self, layout: &ScoreViewLayoutOverrides) -> ViewStyle {
404 let mut style = ViewStyle::default();
405 apply_style_overrides(&mut style, &self.style_overrides);
406 apply_style_overrides(&mut style, &layout.typed_style_overrides);
407 style
408 }
409
410 pub fn new(
411 title: &str,
412 tempo_bpm: u16,
413 numerator: u8,
414 denominator: u8,
415 fifths: i8,
416 measure_count: u32,
417 ) -> Self {
418 let mut score = Score::default();
419 score.metadata.title = title.to_string();
420 score.settings.tempo_bpm = tempo_bpm;
421 score.settings.time_signature = TimeSignature {
422 numerator,
423 denominator,
424 };
425 score.settings.key_signature = KeySignature {
426 fifths,
427 mode: "major".to_string(),
428 };
429
430 score.parts[0].staves[0].measures.clear();
431 for i in 0..measure_count {
432 let mut m = Measure::empty(numerator, denominator);
433 m.number = i + 1;
434 score.parts[0].staves[0].measures.push(m);
435 }
436 score
437 }
438
439 pub fn template(kind: ScoreTemplate) -> Self {
444 fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
445 (0..count)
446 .map(|i| {
447 let mut m = Measure::empty(num, den);
448 m.number = i + 1;
449 m
450 })
451 .collect()
452 }
453 fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
454 let mut p = Part::new(name, short);
455 p.midi_program = program;
456 let mut s = Staff::new(clef);
457 s.measures = measures(4, 4, 4);
458 p.staves.push(s);
459 p
460 }
461
462 let mut score = Score {
463 id: uuid::Uuid::new_v4().to_string(),
464 schema_version: 1,
465 metadata: ScoreMetadata::default(),
466 settings: ScoreSettings::default(),
467 parts: Vec::new(),
468 part_groups: Vec::new(),
469 texts: Vec::new(),
470 style_overrides: Vec::new(),
471 object_style_overrides: Vec::new(),
472 chord_definitions: Vec::new(),
473 spanners: Vec::new(),
474 views: Vec::new(),
475 };
476
477 match kind {
478 ScoreTemplate::Solo => {
479 score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
480 }
481 ScoreTemplate::Piano => {
482 let mut p = Part::new("Piano", "Pno.");
483 p.midi_program = 0;
484 let mut treble = Staff::new(Clef::Treble);
485 treble.measures = measures(4, 4, 4);
486 let mut bass = Staff::new(Clef::Bass);
487 bass.measures = measures(4, 4, 4);
488 p.staves.push(treble);
489 p.staves.push(bass);
490 score.parts.push(p);
491 }
492 ScoreTemplate::StringQuartet => {
493 score
494 .parts
495 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
496 score
497 .parts
498 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
499 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
500 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
501 }
502 ScoreTemplate::StringOrchestra => {
503 score
504 .parts
505 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
506 score
507 .parts
508 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
509 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
510 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
511 score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
512 }
513 ScoreTemplate::BrassQuintet => {
514 score
515 .parts
516 .push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
517 score
518 .parts
519 .push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
520 score
521 .parts
522 .push(part("French Horn", "Hn.", Clef::Treble, 60));
523 score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
524 score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
525 }
526 }
527 score
528 }
529
530 pub fn measure_count(&self) -> usize {
531 self.parts
532 .first()
533 .and_then(|p| p.staves.first())
534 .map(|s| s.measures.len())
535 .unwrap_or(0)
536 }
537
538 pub fn section_range(
543 &self,
544 measure_index: usize,
545 ) -> Result<std::ops::RangeInclusive<usize>, Error> {
546 let measures = self
547 .parts
548 .first()
549 .and_then(|part| part.staves.first())
550 .map(|staff| &staff.measures)
551 .ok_or(Error::MeasureNotFound(measure_index))?;
552 if measure_index >= measures.len() {
553 return Err(Error::MeasureNotFound(measure_index));
554 }
555 let start = (0..=measure_index)
556 .rev()
557 .find(|&index| measures[index].section_break)
558 .unwrap_or(0);
559 let end = ((measure_index + 1)..measures.len())
560 .find(|&index| measures[index].section_break)
561 .map(|index| index - 1)
562 .unwrap_or(measures.len() - 1);
563 Ok(start..=end)
564 }
565
566 pub fn statistics(&self) -> ScoreStats {
568 let measure_count = self.measure_count();
569 let part_count = self.parts.len();
570
571 let seq = measure_sequence(self);
573 let total_beats: f64 = self
574 .parts
575 .first()
576 .and_then(|p| p.staves.first())
577 .map(|s| {
578 seq.iter()
579 .filter_map(|&idx| s.measures.get(idx))
580 .flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
581 .map(|n| n.beats())
582 .sum()
583 })
584 .unwrap_or(0.0);
585
586 let mut note_count = 0usize;
587 let mut rest_count = 0usize;
588 for part in &self.parts {
589 for staff in &part.staves {
590 for measure in &staff.measures {
591 for voice in &measure.voices {
592 for note in voice {
593 if note.is_rest {
594 rest_count += 1;
595 } else {
596 note_count += 1;
597 }
598 }
599 }
600 }
601 }
602 }
603
604 let bpm = self.settings.tempo_bpm as f64;
605 let estimated_duration_secs = if bpm > 0.0 {
606 total_beats / bpm * 60.0
607 } else {
608 0.0
609 };
610
611 ScoreStats {
612 measure_count,
613 note_count,
614 rest_count,
615 part_count,
616 estimated_duration_secs,
617 }
618 }
619
620 pub fn resolve_view(&self, view_id: &str) -> Result<Score, Error> {
626 let view = self
627 .views
628 .iter()
629 .find(|view| view.id == view_id)
630 .ok_or_else(|| {
631 Error::InvalidCommand(format!("score view '{view_id}' does not exist"))
632 })?;
633 let mut source_to_target = vec![None; self.parts.len()];
634 let mut parts = Vec::with_capacity(view.parts.len());
635 for &source_index in &view.parts {
636 let source = self
637 .parts
638 .get(source_index)
639 .ok_or(Error::PartNotFound(source_index))?;
640 if source_to_target[source_index].is_some() {
641 return Err(Error::InvalidCommand(format!(
642 "score view '{}' selects part {} more than once",
643 view.id, source_index
644 )));
645 }
646 source_to_target[source_index] = Some(parts.len());
647 parts.push(source.clone());
648 }
649 for override_ in &view.staff_kind_overrides {
650 let target_part = source_to_target
651 .get(override_.staff.part)
652 .copied()
653 .flatten()
654 .ok_or_else(|| {
655 Error::InvalidCommand(
656 "view overrides a staff outside its selected parts".into(),
657 )
658 })?;
659 let target_staff = parts[target_part]
660 .staves
661 .get_mut(override_.staff.staff)
662 .ok_or_else(|| {
663 Error::InvalidCommand(format!(
664 "view overrides staff {} outside part {}",
665 override_.staff.staff, override_.staff.part
666 ))
667 })?;
668 if override_.kind == StaffKind::Tablature && target_staff.tablature.is_none() {
669 return Err(Error::InvalidCommand(
670 "tablature view requires a tablature configuration on its source staff".into(),
671 ));
672 }
673 target_staff.presentation.kind = override_.kind;
674 }
675 let spanners = self
676 .spanners
677 .iter()
678 .filter_map(|spanner| {
679 let start = source_to_target
680 .get(spanner.start.part)
681 .copied()
682 .flatten()?;
683 let end = source_to_target.get(spanner.end.part).copied().flatten()?;
684 let mut projected = spanner.clone();
685 projected.start.part = start;
686 projected.end.part = end;
687 Some(projected)
688 })
689 .collect();
690 let part_groups = self
691 .part_groups
692 .iter()
693 .filter_map(|group| {
694 let first = source_to_target.get(group.first_part).copied().flatten()?;
695 let last = source_to_target.get(group.last_part).copied().flatten()?;
696 Some(PartGroup {
697 first_part: first,
698 last_part: last,
699 symbol: group.symbol.clone(),
700 barlines_connect: group.barlines_connect,
701 })
702 })
703 .collect();
704 let mut projected_view = view.clone();
705 projected_view.parts = (0..parts.len()).collect();
706 for reference in &mut projected_view.layout.hidden_staves {
707 reference.part = source_to_target
708 .get(reference.part)
709 .copied()
710 .flatten()
711 .ok_or_else(|| {
712 Error::InvalidCommand("view hides a part outside its selection".into())
713 })?;
714 }
715 for override_ in &mut projected_view.staff_kind_overrides {
716 override_.staff.part = source_to_target
717 .get(override_.staff.part)
718 .copied()
719 .flatten()
720 .ok_or_else(|| {
721 Error::InvalidCommand("view overrides a staff outside its selection".into())
722 })?;
723 }
724 Ok(Score {
725 id: Uuid::new_v4().to_string(),
726 schema_version: self.schema_version,
727 metadata: self.metadata.clone(),
728 settings: self.settings.clone(),
729 parts,
730 part_groups,
731 texts: self.texts.clone(),
732 style_overrides: self.style_overrides.clone(),
733 object_style_overrides: self.object_style_overrides.clone(),
734 chord_definitions: self.chord_definitions.clone(),
735 spanners,
736 views: vec![projected_view],
737 })
738 }
739
740 pub fn extract_part(&self, part_index: usize) -> Option<Score> {
743 let part = self.parts.get(part_index)?.clone();
744 let spanners = self
745 .spanners
746 .iter()
747 .filter(|spanner| spanner.start.part == part_index && spanner.end.part == part_index)
748 .cloned()
749 .map(|mut spanner| {
750 spanner.start.part = 0;
751 spanner.end.part = 0;
752 spanner
753 })
754 .collect();
755 Some(Score {
756 id: Uuid::new_v4().to_string(),
757 schema_version: 1,
758 metadata: self.metadata.clone(),
759 settings: self.settings.clone(),
760 parts: vec![part],
761 part_groups: Vec::new(),
762 texts: self.texts.clone(),
763 style_overrides: self.style_overrides.clone(),
764 object_style_overrides: self.object_style_overrides.clone(),
765 chord_definitions: self.chord_definitions.clone(),
766 spanners,
767 views: Vec::new(),
768 })
769 }
770
771 pub fn extract_part_checked(&self, part_index: usize) -> Result<Score, Error> {
773 if !super::validate::validate(self).is_valid() {
774 return Err(Error::InvalidScore);
775 }
776 let extracted = self
777 .extract_part(part_index)
778 .ok_or(Error::PartNotFound(part_index))?;
779 if !super::validate::validate(&extracted).is_valid() {
780 return Err(Error::InvalidScore);
781 }
782 Ok(extracted)
783 }
784
785 pub fn merge(&self, other: &Score) -> Score {
789 let self_count = self.measure_count();
790 let other_count = other.measure_count();
791 let max_count = self_count.max(other_count);
792 let ts = self.settings.time_signature.clone();
793
794 let pad = |mut part: Part, from: usize| -> Part {
795 for staff in &mut part.staves {
796 for i in from..max_count {
797 let mut m = Measure::empty(ts.numerator, ts.denominator);
798 m.number = i as u32 + 1;
799 staff.measures.push(m);
800 }
801 }
802 part
803 };
804
805 let mut parts: Vec<Part> = self
806 .parts
807 .iter()
808 .cloned()
809 .map(|p| pad(p, self_count))
810 .collect();
811 for p in &other.parts {
812 parts.push(pad(p.clone(), other_count));
813 }
814 let self_part_count = self.parts.len();
815 let mut spanners = self.spanners.clone();
816 spanners.extend(other.spanners.iter().cloned().map(|mut spanner| {
817 spanner.start.part += self_part_count;
818 spanner.end.part += self_part_count;
819 spanner
820 }));
821
822 Score {
823 id: Uuid::new_v4().to_string(),
824 schema_version: 1,
825 metadata: self.metadata.clone(),
826 settings: self.settings.clone(),
827 parts,
828 part_groups: Vec::new(),
829 texts: self.texts.clone(),
830 style_overrides: self.style_overrides.clone(),
831 object_style_overrides: self.object_style_overrides.clone(),
832 chord_definitions: self.chord_definitions.clone(),
833 spanners,
834 views: Vec::new(),
835 }
836 }
837
838 pub fn merge_checked(&self, other: &Score) -> Result<Score, Error> {
840 if !super::validate::validate(self).is_valid()
841 || !super::validate::validate(other).is_valid()
842 {
843 return Err(Error::InvalidScore);
844 }
845 let merged = self.merge(other);
846 if !super::validate::validate(&merged).is_valid() {
847 return Err(Error::InvalidScore);
848 }
849 Ok(merged)
850 }
851}
852
853#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
855pub struct ScoreStats {
856 pub measure_count: usize,
857 pub note_count: usize,
859 pub rest_count: usize,
860 pub part_count: usize,
861 pub estimated_duration_secs: f64,
863}
864
865use super::pitch::Step;
868use super::repeat::measure_sequence;
869
870pub fn assign_tablature_positions(score: &mut Score) -> usize {
882 const MAX_FRET: i16 = 24;
883 let mut assigned = 0;
884
885 for part in &mut score.parts {
886 for staff in &mut part.staves {
887 if staff.tablature.is_none() {
888 continue;
889 }
890 let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
891 .map(|measure_index| staff.tablature_at(measure_index))
892 .collect();
893
894 for (measure_index, measure) in staff.measures.iter_mut().enumerate() {
895 let Some(tab) = configurations[measure_index].as_ref() else {
896 continue;
897 };
898 for voice in &mut measure.voices {
899 for note in voice.iter_mut() {
900 if note.is_rest
901 || note.tab_position.is_some()
902 || !note.tab_positions.is_empty()
903 || note.pitches.is_empty()
904 {
905 continue;
906 }
907 let pitches: Vec<i16> = note.pitches.iter().map(Pitch::to_midi).collect();
908 let Some(positions) = best_tablature_assignment(
909 &pitches,
910 &tab.tuning_midi,
911 tab.lines as usize,
912 i16::from(tab.capo),
913 MAX_FRET,
914 ) else {
915 continue;
916 };
917
918 note.tab_position = positions.first().cloned();
919 note.tab_positions = positions;
920 note.string_number = note.tab_position.as_ref().map(|p| p.string);
921 assigned += 1;
922 }
923 }
924 }
925 }
926 }
927
928 assigned
929}
930
931pub fn optimize_tablature_positions(score: &mut Score) -> usize {
938 const MAX_FRET: i16 = 24;
939 let mut assigned = 0;
940
941 for part in &mut score.parts {
942 for staff in &mut part.staves {
943 if staff.tablature.is_none() {
944 continue;
945 }
946 let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
947 .map(|measure_index| staff.tablature_at(measure_index))
948 .collect();
949 for voice in 0..4 {
950 let mut locations = Vec::new();
952 for (measure_index, measure) in staff.measures.iter().enumerate() {
953 for (note_index, note) in measure.voices[voice].iter().enumerate() {
954 if !note.is_rest && !note.pitches.is_empty() {
955 locations.push((measure_index, note_index, note.clone()));
956 }
957 }
958 }
959 let candidates: Vec<Vec<Vec<TabPosition>>> = locations
960 .iter()
961 .map(|(measure_index, _, note)| {
962 if let Some(positions) = if !note.tab_positions.is_empty() {
963 Some(note.tab_positions.clone())
964 } else {
965 note.tab_position.clone().map(|position| vec![position])
966 } {
967 vec![positions]
968 } else {
969 let Some(tab) = configurations[*measure_index].as_ref() else {
970 return Vec::new();
971 };
972 tablature_assignments(
973 ¬e.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
974 &tab.tuning_midi,
975 tab.lines as usize,
976 i16::from(tab.capo),
977 MAX_FRET,
978 )
979 }
980 })
981 .collect();
982 if candidates.iter().any(Vec::is_empty) {
983 continue;
984 }
985
986 let mut costs: Vec<Vec<(u32, Option<usize>)>> = candidates
987 .iter()
988 .map(|events| vec![(u32::MAX, None); events.len()])
989 .collect();
990 for (candidate_index, candidate) in candidates[0].iter().enumerate() {
991 costs[0][candidate_index] = (tablature_load(candidate), None);
992 }
993 for event_index in 1..candidates.len() {
994 for (candidate_index, candidate) in candidates[event_index].iter().enumerate() {
995 let load = tablature_load(candidate);
996 for (previous_index, previous) in
997 candidates[event_index - 1].iter().enumerate()
998 {
999 let previous_cost = costs[event_index - 1][previous_index].0;
1000 let cost = previous_cost
1001 .saturating_add(load)
1002 .saturating_add(tablature_movement(previous, candidate));
1003 if cost < costs[event_index][candidate_index].0 {
1004 costs[event_index][candidate_index] = (cost, Some(previous_index));
1005 }
1006 }
1007 }
1008 }
1009 let mut selected = vec![0; candidates.len()];
1010 if let Some((last, _)) = costs.last().and_then(|row| {
1011 row.iter()
1012 .enumerate()
1013 .min_by_key(|(index, (cost, _))| (*cost, *index))
1014 }) {
1015 selected[candidates.len() - 1] = last;
1016 for event_index in (1..candidates.len()).rev() {
1017 selected[event_index - 1] =
1018 costs[event_index][selected[event_index]].1.unwrap_or(0);
1019 }
1020 }
1021
1022 for (((measure_index, note_index, original), event_candidates), selected_index) in
1023 locations.into_iter().zip(candidates).zip(selected)
1024 {
1025 if original.tab_position.is_none() && original.tab_positions.is_empty() {
1026 let note = &mut staff.measures[measure_index].voices[voice][note_index];
1027 let positions = event_candidates[selected_index].clone();
1028 note.tab_position = positions.first().cloned();
1029 note.tab_positions = positions;
1030 note.string_number = note.tab_position.as_ref().map(|p| p.string);
1031 assigned += 1;
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 assigned
1039}
1040
1041fn tablature_load(positions: &[TabPosition]) -> u32 {
1042 let sum: u32 = positions
1043 .iter()
1044 .map(|position| u32::from(position.fret))
1045 .sum();
1046 let min = positions
1047 .iter()
1048 .map(|position| position.fret)
1049 .min()
1050 .unwrap_or(0);
1051 let max = positions
1052 .iter()
1053 .map(|position| position.fret)
1054 .max()
1055 .unwrap_or(0);
1056 let span = max - min;
1057 let stretch_penalty = span.saturating_sub(4) as u32 * 12;
1060 sum + u32::from(span) * 2 + stretch_penalty
1061}
1062
1063fn tablature_movement(previous: &[TabPosition], current: &[TabPosition]) -> u32 {
1064 previous
1065 .iter()
1066 .zip(current)
1067 .map(|(a, b)| u32::from(a.fret.abs_diff(b.fret)) + u32::from(a.string.abs_diff(b.string)))
1068 .sum()
1069}
1070
1071fn tablature_assignments(
1072 pitches: &[i16],
1073 tuning: &[i16],
1074 lines: usize,
1075 capo: i16,
1076 max_fret: i16,
1077) -> Vec<Vec<TabPosition>> {
1078 #[allow(clippy::too_many_arguments)]
1079 fn visit(
1080 pitches: &[i16],
1081 tuning: &[i16],
1082 lines: usize,
1083 capo: i16,
1084 max_fret: i16,
1085 index: usize,
1086 used: &mut [bool],
1087 current: &mut Vec<TabPosition>,
1088 output: &mut Vec<Vec<TabPosition>>,
1089 ) {
1090 if index == pitches.len() {
1091 output.push(current.clone());
1092 return;
1093 }
1094 for (string, open) in tuning.iter().enumerate().take(lines) {
1095 if used[string] {
1096 continue;
1097 }
1098 let fret = pitches[index] - *open - capo;
1099 if !(0..=max_fret).contains(&fret) {
1100 continue;
1101 }
1102 used[string] = true;
1103 current.push(TabPosition {
1104 string: (string + 1) as u8,
1105 fret: fret as u8,
1106 });
1107 visit(
1108 pitches,
1109 tuning,
1110 lines,
1111 capo,
1112 max_fret,
1113 index + 1,
1114 used,
1115 current,
1116 output,
1117 );
1118 current.pop();
1119 used[string] = false;
1120 }
1121 }
1122
1123 if pitches.is_empty() || pitches.len() > lines {
1124 return Vec::new();
1125 }
1126 let mut output = Vec::new();
1127 visit(
1128 pitches,
1129 tuning,
1130 lines,
1131 capo,
1132 max_fret,
1133 0,
1134 &mut vec![false; lines],
1135 &mut Vec::new(),
1136 &mut output,
1137 );
1138 output
1139}
1140
1141fn best_tablature_assignment(
1142 pitches: &[i16],
1143 tuning: &[i16],
1144 lines: usize,
1145 capo: i16,
1146 max_fret: i16,
1147) -> Option<Vec<TabPosition>> {
1148 type Assignment = (i16, i16, i16, Vec<u8>, Vec<TabPosition>);
1149
1150 #[allow(clippy::too_many_arguments)]
1151 fn search(
1152 pitches: &[i16],
1153 tuning: &[i16],
1154 lines: usize,
1155 capo: i16,
1156 max_fret: i16,
1157 index: usize,
1158 used: &mut [bool],
1159 current: &mut Vec<TabPosition>,
1160 best: &mut Option<Assignment>,
1161 ) {
1162 if index == pitches.len() {
1163 let sum: i16 = current.iter().map(|p| i16::from(p.fret)).sum();
1164 let min = current.iter().map(|p| p.fret).min().unwrap_or(0);
1165 let max = current.iter().map(|p| p.fret).max().unwrap_or(0);
1166 let strings: Vec<u8> = current.iter().map(|p| p.string).collect();
1167 let candidate = (
1168 sum,
1169 i16::from(max) - i16::from(min),
1170 i16::from(max),
1171 strings,
1172 current.clone(),
1173 );
1174 if best.as_ref().is_none_or(|existing| {
1175 (candidate.0, candidate.1, candidate.2, &candidate.3)
1176 < (existing.0, existing.1, existing.2, &existing.3)
1177 }) {
1178 *best = Some(candidate);
1179 }
1180 return;
1181 }
1182
1183 for (string, open) in tuning.iter().enumerate().take(lines) {
1184 if used[string] {
1185 continue;
1186 }
1187 let fret = pitches[index] - *open - capo;
1188 if !(0..=max_fret).contains(&fret) {
1189 continue;
1190 }
1191 used[string] = true;
1192 current.push(TabPosition {
1193 string: (string + 1) as u8,
1194 fret: fret as u8,
1195 });
1196 search(
1197 pitches,
1198 tuning,
1199 lines,
1200 capo,
1201 max_fret,
1202 index + 1,
1203 used,
1204 current,
1205 best,
1206 );
1207 current.pop();
1208 used[string] = false;
1209 }
1210 }
1211
1212 if pitches.len() > lines {
1213 return None;
1214 }
1215 let mut used = vec![false; lines];
1216 let mut current = Vec::with_capacity(pitches.len());
1217 let mut best = None;
1218 search(
1219 pitches,
1220 tuning,
1221 lines,
1222 capo,
1223 max_fret,
1224 0,
1225 &mut used,
1226 &mut current,
1227 &mut best,
1228 );
1229 best.map(|(_, _, _, _, positions)| positions)
1230}
1231
1232pub fn transpose(score: &Score, semitones: i8) -> Score {
1236 if semitones == 0 {
1237 return score.clone();
1238 }
1239 let mut out = score.clone();
1240 out.settings.key_signature.fifths = transpose_fifths(
1241 score.settings.key_signature.fifths,
1242 &score.settings.key_signature.mode,
1243 semitones,
1244 );
1245 for part in &mut out.parts {
1246 for staff in &mut part.staves {
1247 for measure in &mut staff.measures {
1248 if let Some(ref mut ks) = measure.key_sig {
1249 ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
1250 }
1251 for voice in &mut measure.voices {
1252 for note in voice.iter_mut() {
1253 for pitch in note.pitches.iter_mut() {
1254 *pitch = transpose_pitch(pitch, semitones);
1255 }
1256 }
1257 }
1258 }
1259 }
1260 }
1261 out
1262}
1263
1264pub fn transpose_checked(score: &Score, semitones: i8) -> Result<Score, Error> {
1266 if !super::validate::validate(score).is_valid() {
1267 return Err(Error::InvalidScore);
1268 }
1269 let transposed = transpose(score, semitones);
1270 if !super::validate::validate(&transposed).is_valid() {
1271 return Err(Error::InvalidScore);
1272 }
1273 Ok(transposed)
1274}
1275
1276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1278#[serde(rename_all = "snake_case")]
1279pub enum RegionalTranspositionTarget {
1280 Written,
1282 Concert,
1284}
1285
1286pub fn transpose_staff_region_checked(
1289 score: &Score,
1290 part_index: usize,
1291 staff_index: usize,
1292 start_measure: usize,
1293 end_measure: usize,
1294 semitones: i8,
1295 target: RegionalTranspositionTarget,
1296) -> Result<Score, Error> {
1297 if !super::validate::validate(score).is_valid() {
1298 return Err(Error::InvalidScore);
1299 }
1300 let source_staff = score
1301 .parts
1302 .get(part_index)
1303 .ok_or(Error::PartNotFound(part_index))?
1304 .staves
1305 .get(staff_index)
1306 .ok_or(Error::StaffNotFound(staff_index))?;
1307 if start_measure >= end_measure || end_measure > source_staff.measures.len() {
1308 return Err(Error::InvalidCommand(format!(
1309 "invalid measure range {start_measure}..{end_measure}"
1310 )));
1311 }
1312 if target == RegionalTranspositionTarget::Concert
1313 && (start_measure != 0 || end_measure != source_staff.measures.len())
1314 {
1315 return Err(Error::InvalidCommand(
1316 "concert-pitch staff transposition requires the full staff range".into(),
1317 ));
1318 }
1319 let mut transformed = score.clone();
1320 let staff = &mut transformed.parts[part_index].staves[staff_index];
1321 match target {
1322 RegionalTranspositionTarget::Written => {
1323 for measure in &mut staff.measures[start_measure..end_measure] {
1324 if let Some(key) = &mut measure.key_sig {
1325 key.fifths = transpose_fifths(key.fifths, &key.mode, semitones);
1326 }
1327 for voice in &mut measure.voices {
1328 for note in voice {
1329 for pitch in &mut note.pitches {
1330 *pitch = transpose_pitch(pitch, semitones);
1331 }
1332 }
1333 }
1334 }
1335 }
1336 RegionalTranspositionTarget::Concert => {
1337 staff.transpose_semitones = staff.transpose_semitones.saturating_add(semitones);
1338 }
1339 }
1340 if !super::validate::validate(&transformed).is_valid() {
1341 return Err(Error::InvalidScore);
1342 }
1343 Ok(transformed)
1344}
1345
1346fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
1347 let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
1348 let pc = new_midi % 12;
1349 let oct = (new_midi / 12) as i8 - 1;
1350 let (step, alter): (Step, i8) = if semitones >= 0 {
1351 match pc {
1352 0 => (Step::C, 0),
1353 1 => (Step::C, 1),
1354 2 => (Step::D, 0),
1355 3 => (Step::D, 1),
1356 4 => (Step::E, 0),
1357 5 => (Step::F, 0),
1358 6 => (Step::F, 1),
1359 7 => (Step::G, 0),
1360 8 => (Step::G, 1),
1361 9 => (Step::A, 0),
1362 10 => (Step::A, 1),
1363 11 => (Step::B, 0),
1364 _ => (Step::C, 0),
1365 }
1366 } else {
1367 match pc {
1368 0 => (Step::C, 0),
1369 1 => (Step::D, -1),
1370 2 => (Step::D, 0),
1371 3 => (Step::E, -1),
1372 4 => (Step::E, 0),
1373 5 => (Step::F, 0),
1374 6 => (Step::G, -1),
1375 7 => (Step::G, 0),
1376 8 => (Step::A, -1),
1377 9 => (Step::A, 0),
1378 10 => (Step::B, -1),
1379 11 => (Step::B, 0),
1380 _ => (Step::C, 0),
1381 }
1382 };
1383 Pitch::with_microtone(step, oct, alter, pitch.microtone_cents)
1384}
1385
1386fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
1392 let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
1393 let tonic_pc = if mode == "minor" {
1394 ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
1395 } else {
1396 tonic_major_pc
1397 };
1398 let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
1399 let major_tonic = if mode == "minor" {
1400 ((new_tonic as i32 + 3).rem_euclid(12)) as u8
1401 } else {
1402 new_tonic
1403 };
1404 let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
1405 if raw > 6 { raw - 12 } else { raw }
1406}
1407
1408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1410pub struct InstrumentRange {
1411 pub lowest: u8,
1412 pub highest: u8,
1413}
1414
1415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1422pub struct InstrumentDefinition {
1423 pub id: String,
1425 #[serde(default)]
1426 pub name: String,
1427 #[serde(default)]
1428 pub short_name: String,
1429 #[serde(default)]
1430 pub family: Option<String>,
1431 #[serde(default)]
1432 pub transpose_semitones: i8,
1433 #[serde(default)]
1434 pub written_range: Option<InstrumentRange>,
1435 #[serde(default)]
1436 pub sounding_range: Option<InstrumentRange>,
1437 #[serde(default)]
1438 pub default_clefs: Vec<Clef>,
1439 #[serde(default = "default_instrument_staff_count")]
1440 pub staff_count: u8,
1441 #[serde(default)]
1442 pub staff_kind: StaffKind,
1443 #[serde(default)]
1444 pub midi_channel: u8,
1445 #[serde(default)]
1446 pub midi_program: u8,
1447 #[serde(default)]
1448 pub percussion_map_id: Option<String>,
1449}
1450
1451const fn default_instrument_staff_count() -> u8 {
1452 1
1453}
1454
1455impl InstrumentDefinition {
1456 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
1458 Self {
1459 id: id.into(),
1460 name: name.into(),
1461 short_name: String::new(),
1462 family: None,
1463 transpose_semitones: 0,
1464 written_range: None,
1465 sounding_range: None,
1466 default_clefs: Vec::new(),
1467 staff_count: 1,
1468 staff_kind: StaffKind::Standard,
1469 midi_channel: 0,
1470 midi_program: 0,
1471 percussion_map_id: None,
1472 }
1473 }
1474}
1475
1476#[derive(Debug, Clone, Serialize, Deserialize)]
1477pub struct Part {
1478 pub id: String,
1479 pub name: String,
1480 pub short_name: String,
1481 pub staves: Vec<Staff>,
1482 #[serde(default)]
1484 pub midi_channel: u8,
1485 #[serde(default)]
1487 pub midi_program: u8,
1488 #[serde(default)]
1490 pub midi_pitch_bends: Vec<MidiPitchBend>,
1491 #[serde(default)]
1493 pub midi_control_changes: Vec<MidiControlChange>,
1494 #[serde(default)]
1496 pub midi_program_changes: Vec<MidiProgramChange>,
1497 #[serde(default)]
1499 pub midi_aftertouch: Vec<MidiAftertouch>,
1500 #[serde(default)]
1502 pub percussion_instruments: Vec<PercussionInstrument>,
1503 #[serde(default)]
1505 pub staff_groups: Vec<StaffGroup>,
1506 #[serde(default)]
1508 pub instrument: Option<InstrumentDefinition>,
1509}
1510
1511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1512pub struct MidiPitchBend {
1513 pub tick: u64,
1514 pub channel: u8,
1515 pub value: i16,
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1520pub struct MidiControlChange {
1521 pub tick: u64,
1522 pub channel: u8,
1523 pub controller: u8,
1525 pub value: u8,
1527}
1528
1529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1530pub struct MidiProgramChange {
1531 pub tick: u64,
1532 pub channel: u8,
1533 pub program: u8,
1535}
1536
1537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1538pub struct MidiAftertouch {
1539 pub tick: u64,
1540 pub channel: u8,
1541 pub key: Option<u8>,
1543 pub value: u8,
1545}
1546
1547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1553pub struct PercussionInstrument {
1554 pub id: String,
1556 #[serde(default)]
1557 pub name: Option<String>,
1558 #[serde(default)]
1560 pub midi_unpitched: Option<u8>,
1561 #[serde(default)]
1563 pub staff_position: Option<i8>,
1564 #[serde(default)]
1566 pub notehead: Option<NoteHead>,
1567 #[serde(default)]
1569 pub preferred_voice: Option<u8>,
1570 #[serde(default)]
1572 pub techniques: Vec<String>,
1573}
1574
1575impl Part {
1576 pub fn new(name: &str, short_name: &str) -> Self {
1577 Self {
1578 id: Uuid::new_v4().to_string(),
1579 name: name.to_string(),
1580 short_name: short_name.to_string(),
1581 staves: Vec::new(),
1582 midi_channel: 0,
1583 midi_program: 0,
1584 midi_pitch_bends: Vec::new(),
1585 midi_control_changes: Vec::new(),
1586 midi_program_changes: Vec::new(),
1587 midi_aftertouch: Vec::new(),
1588 percussion_instruments: Vec::new(),
1589 staff_groups: Vec::new(),
1590 instrument: None,
1591 }
1592 }
1593
1594 pub fn percussion_instrument_for_note(&self, note: &Note) -> Option<&PercussionInstrument> {
1602 if !note.is_unpitched {
1603 return None;
1604 }
1605 if let Some(instrument_id) = note.instrument_id.as_deref() {
1606 return self
1607 .percussion_instruments
1608 .iter()
1609 .find(|instrument| instrument.id == instrument_id);
1610 }
1611 let midi_key = u8::try_from(note.pitches.first()?.to_midi()).ok()?;
1612 self.percussion_instruments
1613 .iter()
1614 .find(|instrument| instrument.midi_unpitched == Some(midi_key))
1615 }
1616}
1617
1618#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1620#[serde(rename_all = "snake_case")]
1621pub enum StaffKind {
1622 #[default]
1623 Standard,
1624 Tablature,
1625 Percussion,
1626}
1627
1628#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1630#[serde(rename_all = "snake_case")]
1631pub enum StaffNoteheadScheme {
1632 #[default]
1633 Standard,
1634 PitchNames,
1635 ShapeNotes,
1636}
1637
1638#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1640#[serde(rename_all = "snake_case")]
1641pub enum TablatureRhythmDisplay {
1642 #[default]
1644 FretOnly,
1645 Stems,
1647}
1648
1649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1655pub struct StaffPresentation {
1656 #[serde(default)]
1657 pub kind: StaffKind,
1658 #[serde(default = "default_staff_line_count")]
1659 pub lines: u8,
1660 #[serde(default = "default_staff_line_distance")]
1661 pub line_distance: f32,
1662 #[serde(default)]
1663 pub small: bool,
1664 #[serde(default)]
1665 pub cutaway: bool,
1666 #[serde(default = "default_staff_visible")]
1667 pub visible: bool,
1668 #[serde(default)]
1669 pub notehead_scheme: StaffNoteheadScheme,
1670 #[serde(default)]
1672 pub tablature_rhythm_display: TablatureRhythmDisplay,
1673 #[serde(default)]
1675 pub tablature_fret_mark_style: TablatureFretMarkStyle,
1676}
1677
1678const fn default_staff_line_count() -> u8 {
1679 5
1680}
1681
1682const fn default_staff_line_distance() -> f32 {
1683 1.0
1684}
1685
1686const fn default_staff_visible() -> bool {
1687 true
1688}
1689
1690impl Default for StaffPresentation {
1691 fn default() -> Self {
1692 Self {
1693 kind: StaffKind::Standard,
1694 lines: default_staff_line_count(),
1695 line_distance: default_staff_line_distance(),
1696 small: false,
1697 cutaway: false,
1698 visible: true,
1699 notehead_scheme: StaffNoteheadScheme::Standard,
1700 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
1701 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
1702 }
1703 }
1704}
1705
1706#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1708#[serde(rename_all = "snake_case")]
1709pub enum TablatureFretMarkStyle {
1710 #[default]
1711 Arabic,
1712 RomanUpper,
1713 RomanLower,
1714}
1715
1716impl StaffPresentation {
1717 fn for_clef(clef: &Clef) -> Self {
1718 let mut presentation = Self::default();
1719 if matches!(clef, Clef::Percussion) {
1720 presentation.kind = StaffKind::Percussion;
1721 }
1722 presentation
1723 }
1724}
1725
1726#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub struct Staff {
1728 pub clef: Clef,
1729 pub measures: Vec<Measure>,
1730 #[serde(default)]
1733 pub transpose_semitones: i8,
1734 #[serde(default)]
1735 pub tablature: Option<TablatureConfig>,
1736 #[serde(default)]
1737 pub presentation: StaffPresentation,
1738}
1739
1740impl Staff {
1741 pub fn new(clef: Clef) -> Self {
1742 Self {
1743 presentation: StaffPresentation::for_clef(&clef),
1744 clef,
1745 measures: Vec::new(),
1746 transpose_semitones: 0,
1747 tablature: None,
1748 }
1749 }
1750
1751 pub fn tablature_at(&self, measure_index: usize) -> Option<TablatureConfig> {
1757 let mut active = self.tablature.clone();
1758 for measure in self.measures.iter().take(measure_index.saturating_add(1)) {
1759 if let Some(change) = &measure.tablature_change {
1760 active = Some(change.clone());
1761 }
1762 }
1763 active
1764 }
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1768pub struct VoltaBracket {
1769 pub number: u8,
1771 pub kind: String,
1773}
1774
1775#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1777#[serde(rename_all = "kebab-case")]
1778pub enum HarpPedalPosition {
1779 Flat,
1780 #[default]
1781 Natural,
1782 Sharp,
1783}
1784
1785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1790pub struct HarpPedalDiagram {
1791 #[serde(default)]
1792 pub positions: [HarpPedalPosition; 7],
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1794 pub placement: Option<String>,
1795}
1796
1797impl Default for HarpPedalDiagram {
1798 fn default() -> Self {
1799 Self {
1800 positions: [HarpPedalPosition::Natural; 7],
1801 placement: None,
1802 }
1803 }
1804}
1805
1806#[derive(Debug, Clone, Serialize, Deserialize)]
1807pub struct Measure {
1808 pub number: u32,
1809 pub time_sig: Option<TimeSignature>,
1810 pub key_sig: Option<KeySignature>,
1811 pub clef: Option<Clef>,
1812 pub tempo: Option<u16>,
1813 #[serde(default)]
1816 pub tempo_ramp_to: Option<u16>,
1817 #[serde(default)]
1819 pub instrument_change: Option<InstrumentDefinition>,
1820 #[serde(default)]
1822 pub tablature_change: Option<TablatureConfig>,
1823 pub barline_left: Barline,
1824 pub barline_right: Barline,
1825 #[serde(default)]
1826 pub volta: Option<VoltaBracket>,
1827 #[serde(default)]
1828 pub tempo_text: Option<String>,
1829 #[serde(default)]
1830 pub rehearsal: Option<String>,
1831 #[serde(default)]
1834 pub navigation: Option<String>,
1835 #[serde(default)]
1837 pub expression_text: Option<String>,
1838 #[serde(default)]
1839 pub texts: Vec<StyledText>,
1840 #[serde(default)]
1842 pub figured_bass: Vec<FiguredBassFigure>,
1843 #[serde(default)]
1845 pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
1846 #[serde(default)]
1848 pub multi_rest_count: Option<u8>,
1849 #[serde(default)]
1851 pub system_break: bool,
1852 #[serde(default)]
1854 pub page_break: bool,
1855 #[serde(default)]
1858 pub section_break: bool,
1859 pub voices: [Vec<Note>; 4],
1861 #[serde(default)]
1867 pub source_voice_numbers: [Option<u32>; 4],
1868}
1869
1870impl Measure {
1871 pub fn empty(numerator: u8, denominator: u8) -> Self {
1872 let total_beats = TimeSignature {
1873 numerator,
1874 denominator,
1875 }
1876 .total_beats();
1877 let mut voice0: Vec<Note> = Vec::new();
1878 let mut remaining = total_beats;
1879 while remaining > 1e-9 {
1880 let dur = Duration::whole_filling_beats(remaining);
1881 remaining -= dur.beats(0);
1882 voice0.push(Note::rest(dur));
1883 }
1884 Self {
1885 number: 0,
1886 time_sig: None,
1887 key_sig: None,
1888 clef: None,
1889 tempo: None,
1890 tempo_ramp_to: None,
1891 instrument_change: None,
1892 tablature_change: None,
1893 barline_left: Barline::Normal,
1894 barline_right: Barline::Normal,
1895 volta: None,
1896 tempo_text: None,
1897 rehearsal: None,
1898 navigation: None,
1899 expression_text: None,
1900 texts: Vec::new(),
1901 figured_bass: Vec::new(),
1902 harp_pedal_diagrams: Vec::new(),
1903 multi_rest_count: None,
1904 system_break: false,
1905 page_break: false,
1906 section_break: false,
1907 voices: [voice0, vec![], vec![], vec![]],
1908 source_voice_numbers: [None; 4],
1909 }
1910 }
1911
1912 pub fn renumber(&mut self, n: u32) {
1913 self.number = n;
1914 }
1915}
1916
1917#[derive(Debug, Clone, Serialize, Deserialize)]
1918pub struct Note {
1919 pub id: String,
1920 pub is_rest: bool,
1921 #[serde(default)]
1923 pub is_unpitched: bool,
1924 #[serde(default)]
1926 pub instrument_id: Option<String>,
1927 #[serde(default)]
1929 pub offset_x: Option<f64>,
1930 #[serde(default)]
1932 pub offset_y: Option<f64>,
1933 #[serde(default)]
1935 pub relative_x: Option<f64>,
1936 #[serde(default)]
1938 pub relative_y: Option<f64>,
1939 pub pitches: Vec<Pitch>,
1941 #[serde(default)]
1942 pub tab_position: Option<super::notation::TabPosition>,
1943 #[serde(default)]
1946 pub tab_positions: Vec<super::notation::TabPosition>,
1947 pub duration: Duration,
1948 pub dot_count: u8,
1949 pub tie_start: bool,
1950 pub tie_end: bool,
1951 pub beam: BeamState,
1952 pub articulations: Vec<Articulation>,
1953 pub dynamic: Option<Dynamic>,
1954 pub stem_up: Option<bool>,
1955 #[serde(default)]
1956 pub hairpin_start: Option<HairpinKind>,
1957 #[serde(default)]
1958 pub hairpin_end: bool,
1959 #[serde(default)]
1960 pub tuplet: Option<TupletInfo>,
1961 #[serde(default)]
1962 pub chord_symbol: Option<ChordSymbol>,
1963 #[serde(default)]
1964 pub is_grace: bool,
1965 #[serde(default)]
1967 pub grace_slash: bool,
1968 #[serde(default)]
1969 pub ottava_start: Option<OttavaKind>,
1970 #[serde(default)]
1971 pub ottava_end: bool,
1972 #[serde(default)]
1973 pub lyric: Option<Lyric>,
1974 #[serde(default)]
1975 pub pedal_start: bool,
1976 #[serde(default)]
1977 pub pedal_end: bool,
1978 #[serde(default)]
1979 pub slur_start: bool,
1980 #[serde(default)]
1981 pub slur_end: bool,
1982 #[serde(default)]
1984 pub arpeggiate: Option<bool>,
1985 #[serde(default)]
1987 pub technique_text: Option<String>,
1988 #[serde(default)]
1989 pub glissando_start: bool,
1990 #[serde(default)]
1991 pub glissando_end: bool,
1992 #[serde(default)]
1993 pub cross_staff: Option<CrossStaff>,
1994 #[serde(default)]
1996 pub fingering: Option<u8>,
1997 #[serde(default)]
2000 pub fingerings: Vec<u8>,
2001 #[serde(default)]
2003 pub string_number: Option<u8>,
2004 #[serde(default)]
2005 pub note_head: NoteHead,
2006 #[serde(default)]
2008 pub is_cue: bool,
2009 #[serde(default)]
2011 pub trill_line_start: bool,
2012 #[serde(default)]
2014 pub trill_line_end: bool,
2015 #[serde(default)]
2017 pub guitar_technique: Option<GuitarTechnique>,
2018 #[serde(default)]
2020 pub guitar_bend_alter_cents: Option<i16>,
2021 #[serde(default)]
2023 pub guitar_bend_curve: Vec<GuitarBendPoint>,
2024}
2025
2026#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2028pub struct GuitarBendPoint {
2029 pub position_per_mille: u16,
2030 pub alter_cents: i16,
2031}
2032
2033impl Note {
2034 pub fn select_fingering(
2036 &self,
2037 policy: super::notation::FingeringSelectionPolicy,
2038 ) -> Option<u8> {
2039 let candidates = if self.fingerings.is_empty() {
2040 self.fingering.into_iter().collect::<Vec<_>>()
2041 } else {
2042 self.fingerings.clone()
2043 };
2044 match policy {
2045 super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
2046 super::notation::FingeringSelectionPolicy::LowestNumber => {
2047 candidates.iter().copied().min()
2048 }
2049 super::notation::FingeringSelectionPolicy::HighestNumber => {
2050 candidates.iter().copied().max()
2051 }
2052 }
2053 }
2054
2055 pub fn new(pitch: Pitch, duration: Duration) -> Self {
2056 Self {
2057 id: Uuid::new_v4().to_string(),
2058 is_rest: false,
2059 is_unpitched: false,
2060 instrument_id: None,
2061 offset_x: None,
2062 offset_y: None,
2063 relative_x: None,
2064 relative_y: None,
2065 pitches: vec![pitch],
2066 tab_position: None,
2067 tab_positions: Vec::new(),
2068 duration,
2069 dot_count: 0,
2070 tie_start: false,
2071 tie_end: false,
2072 beam: BeamState::None,
2073 articulations: Vec::new(),
2074 dynamic: None,
2075 stem_up: None,
2076 hairpin_start: None,
2077 hairpin_end: false,
2078 tuplet: None,
2079 chord_symbol: None,
2080 is_grace: false,
2081 grace_slash: false,
2082 ottava_start: None,
2083 ottava_end: false,
2084 lyric: None,
2085 pedal_start: false,
2086 pedal_end: false,
2087 slur_start: false,
2088 slur_end: false,
2089 arpeggiate: None,
2090 technique_text: None,
2091 glissando_start: false,
2092 glissando_end: false,
2093 cross_staff: None,
2094 fingering: None,
2095 fingerings: Vec::new(),
2096 string_number: None,
2097 note_head: NoteHead::Normal,
2098 is_cue: false,
2099 trill_line_start: false,
2100 trill_line_end: false,
2101 guitar_technique: None,
2102 guitar_bend_alter_cents: None,
2103 guitar_bend_curve: Vec::new(),
2104 }
2105 }
2106
2107 pub fn rest(duration: Duration) -> Self {
2108 Self {
2109 id: Uuid::new_v4().to_string(),
2110 is_rest: true,
2111 is_unpitched: false,
2112 instrument_id: None,
2113 offset_x: None,
2114 offset_y: None,
2115 relative_x: None,
2116 relative_y: None,
2117 pitches: Vec::new(),
2118 tab_position: None,
2119 tab_positions: Vec::new(),
2120 duration,
2121 dot_count: 0,
2122 tie_start: false,
2123 tie_end: false,
2124 beam: BeamState::None,
2125 articulations: Vec::new(),
2126 dynamic: None,
2127 stem_up: None,
2128 hairpin_start: None,
2129 hairpin_end: false,
2130 tuplet: None,
2131 chord_symbol: None,
2132 is_grace: false,
2133 grace_slash: false,
2134 ottava_start: None,
2135 ottava_end: false,
2136 lyric: None,
2137 pedal_start: false,
2138 pedal_end: false,
2139 slur_start: false,
2140 slur_end: false,
2141 arpeggiate: None,
2142 technique_text: None,
2143 glissando_start: false,
2144 glissando_end: false,
2145 cross_staff: None,
2146 fingering: None,
2147 fingerings: Vec::new(),
2148 string_number: None,
2149 note_head: NoteHead::Normal,
2150 is_cue: false,
2151 trill_line_start: false,
2152 trill_line_end: false,
2153 guitar_technique: None,
2154 guitar_bend_alter_cents: None,
2155 guitar_bend_curve: Vec::new(),
2156 }
2157 }
2158
2159 pub fn beats(&self) -> f64 {
2160 if self.is_grace || self.is_cue {
2161 return 0.0;
2162 }
2163 let base = self.duration.beats(self.dot_count);
2164 if let Some(ref t) = self.tuplet {
2165 base * (t.normal_notes as f64) / (t.actual_notes as f64)
2166 } else {
2167 base
2168 }
2169 }
2170}
2171
2172impl Duration {
2173 pub fn whole_filling_beats(beats: f64) -> Duration {
2175 if beats >= 4.0 {
2176 Duration::Whole
2177 } else if beats >= 2.0 {
2178 Duration::Half
2179 } else if beats >= 1.0 {
2180 Duration::Quarter
2181 } else if beats >= 0.5 {
2182 Duration::Eighth
2183 } else if beats >= 0.25 {
2184 Duration::Sixteenth
2185 } else if beats >= 0.125 {
2186 Duration::ThirtySecond
2187 } else {
2188 Duration::SixtyFourth
2189 }
2190 }
2191}
2192
2193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2197pub struct NoteAddr {
2198 pub part: usize,
2199 pub staff: usize,
2200 pub measure: usize,
2201 pub voice: usize,
2202 pub note: usize,
2203}
2204
2205#[derive(Debug, Clone, Serialize, Deserialize)]
2209pub enum ScoreChange {
2210 ScoreViewsChanged {
2211 old: Vec<ScoreView>,
2212 new: Vec<ScoreView>,
2213 },
2214 ScoreTextChanged {
2215 old: Vec<StyledText>,
2216 new: Vec<StyledText>,
2217 },
2218 MeasureTextChanged {
2219 part: usize,
2220 staff: usize,
2221 measure: usize,
2222 old: Vec<StyledText>,
2223 new: Vec<StyledText>,
2224 },
2225 FiguredBassChanged {
2226 part: usize,
2227 staff: usize,
2228 measure: usize,
2229 old: Vec<FiguredBassFigure>,
2230 new: Vec<FiguredBassFigure>,
2231 },
2232 HarpPedalDiagramsChanged {
2233 part: usize,
2234 staff: usize,
2235 measure: usize,
2236 old: Vec<HarpPedalDiagram>,
2237 new: Vec<HarpPedalDiagram>,
2238 },
2239 ChordDefinitionsChanged {
2240 old: Vec<ChordDefinition>,
2241 new: Vec<ChordDefinition>,
2242 },
2243 PartNamesChanged {
2244 part: usize,
2245 old_name: String,
2246 new_name: String,
2247 old_short_name: String,
2248 new_short_name: String,
2249 },
2250 PartMidiChanged {
2251 part: usize,
2252 old_channel: u8,
2253 new_channel: u8,
2254 old_program: u8,
2255 new_program: u8,
2256 },
2257 InstrumentDefinitionChanged {
2258 part: usize,
2259 old: Option<InstrumentDefinition>,
2260 new: Option<InstrumentDefinition>,
2261 },
2262 PartMidiAutomationChanged {
2263 part: usize,
2264 old_pitch_bends: Vec<MidiPitchBend>,
2265 new_pitch_bends: Vec<MidiPitchBend>,
2266 old_control_changes: Vec<MidiControlChange>,
2267 new_control_changes: Vec<MidiControlChange>,
2268 old_program_changes: Vec<MidiProgramChange>,
2269 new_program_changes: Vec<MidiProgramChange>,
2270 old_aftertouch: Vec<MidiAftertouch>,
2271 new_aftertouch: Vec<MidiAftertouch>,
2272 },
2273 StaffConfigurationChanged {
2274 part: usize,
2275 staff: usize,
2276 old_clef: Clef,
2277 new_clef: Clef,
2278 old_transpose_semitones: i8,
2279 new_transpose_semitones: i8,
2280 },
2281 StaffPresentationChanged {
2282 part: usize,
2283 staff: usize,
2284 old: StaffPresentation,
2285 new: StaffPresentation,
2286 },
2287 MeasurePresentationChanged {
2288 part: usize,
2289 staff: usize,
2290 measure: usize,
2291 old_number: u32,
2292 new_number: u32,
2293 old_clef: Option<Clef>,
2294 new_clef: Option<Clef>,
2295 old_tempo_text: Option<String>,
2296 new_tempo_text: Option<String>,
2297 old_navigation: Option<String>,
2298 new_navigation: Option<String>,
2299 old_expression_text: Option<String>,
2300 new_expression_text: Option<String>,
2301 old_multi_rest_count: Option<u8>,
2302 new_multi_rest_count: Option<u8>,
2303 old_system_break: bool,
2304 new_system_break: bool,
2305 old_page_break: bool,
2306 new_page_break: bool,
2307 old_section_break: bool,
2308 new_section_break: bool,
2309 },
2310 TablatureConfigChanged {
2311 part: usize,
2312 staff: usize,
2313 old: Option<TablatureConfig>,
2314 new: Option<TablatureConfig>,
2315 },
2316 TablatureChangeChanged {
2317 part: usize,
2318 staff: usize,
2319 measure: usize,
2320 old: Option<TablatureConfig>,
2321 new: Option<TablatureConfig>,
2322 },
2323 UnrepresentedFieldChanged {
2328 path: String,
2329 },
2330 MetadataChanged {
2331 field: String,
2332 old: String,
2333 new: String,
2334 },
2335 TempoChanged {
2336 old: u16,
2337 new: u16,
2338 },
2339 KeySignatureChanged {
2340 old: KeySignature,
2341 new: KeySignature,
2342 },
2343 PartAdded {
2344 part_index: usize,
2345 },
2346 PartRemoved {
2347 part_index: usize,
2348 name: String,
2349 },
2350 NoteAdded {
2351 part: usize,
2352 staff: usize,
2353 measure: usize,
2354 voice: usize,
2355 note_index: usize,
2356 },
2357 NoteRemoved {
2358 part: usize,
2359 staff: usize,
2360 measure: usize,
2361 voice: usize,
2362 note: Box<Note>,
2363 },
2364 NoteModified {
2365 part: usize,
2366 staff: usize,
2367 measure: usize,
2368 voice: usize,
2369 note_index: usize,
2370 old: Box<Note>,
2371 new: Box<Note>,
2372 },
2373 TimeSigChanged {
2374 part: usize,
2375 staff: usize,
2376 measure: usize,
2377 old: Option<TimeSignature>,
2378 new: Option<TimeSignature>,
2379 },
2380 MeasureTempoChanged {
2381 part: usize,
2382 staff: usize,
2383 measure: usize,
2384 old: Option<u16>,
2385 new: Option<u16>,
2386 },
2387 MeasureTempoRampChanged {
2388 part: usize,
2389 staff: usize,
2390 measure: usize,
2391 old: Option<u16>,
2392 new: Option<u16>,
2393 },
2394 BarlineChanged {
2395 part: usize,
2396 staff: usize,
2397 measure: usize,
2398 },
2399 RehearsalMarkChanged {
2400 part: usize,
2401 staff: usize,
2402 measure: usize,
2403 old: Option<String>,
2404 new: Option<String>,
2405 },
2406 VoltaChanged {
2407 part: usize,
2408 staff: usize,
2409 measure: usize,
2410 },
2411}
2412
2413pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
2419 let mut changes: Vec<ScoreChange> = Vec::new();
2420
2421 if a.views != b.views {
2422 changes.push(ScoreChange::ScoreViewsChanged {
2423 old: a.views.clone(),
2424 new: b.views.clone(),
2425 });
2426 }
2427
2428 if a.texts != b.texts {
2429 changes.push(ScoreChange::ScoreTextChanged {
2430 old: a.texts.clone(),
2431 new: b.texts.clone(),
2432 });
2433 }
2434 if a.chord_definitions != b.chord_definitions {
2435 changes.push(ScoreChange::ChordDefinitionsChanged {
2436 old: a.chord_definitions.clone(),
2437 new: b.chord_definitions.clone(),
2438 });
2439 }
2440 if let Some(path) = first_unrepresented_field_change(a, b) {
2441 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
2442 }
2443
2444 macro_rules! meta {
2445 ($field:ident, $name:literal) => {
2446 if a.metadata.$field != b.metadata.$field {
2447 changes.push(ScoreChange::MetadataChanged {
2448 field: $name.to_string(),
2449 old: a.metadata.$field.clone(),
2450 new: b.metadata.$field.clone(),
2451 });
2452 }
2453 };
2454 }
2455 meta!(title, "title");
2456 meta!(composer, "composer");
2457 meta!(lyricist, "lyricist");
2458 meta!(copyright, "copyright");
2459 meta!(work_number, "work_number");
2460 meta!(movement_title, "movement_title");
2461
2462 if a.settings.tempo_bpm != b.settings.tempo_bpm {
2463 changes.push(ScoreChange::TempoChanged {
2464 old: a.settings.tempo_bpm,
2465 new: b.settings.tempo_bpm,
2466 });
2467 }
2468 if a.settings.key_signature != b.settings.key_signature {
2469 changes.push(ScoreChange::KeySignatureChanged {
2470 old: a.settings.key_signature.clone(),
2471 new: b.settings.key_signature.clone(),
2472 });
2473 }
2474
2475 let a_len = a.parts.len();
2476 let b_len = b.parts.len();
2477 for i in b_len..a_len {
2478 changes.push(ScoreChange::PartRemoved {
2479 part_index: i,
2480 name: a.parts[i].name.clone(),
2481 });
2482 }
2483 for i in a_len..b_len {
2484 changes.push(ScoreChange::PartAdded { part_index: i });
2485 }
2486
2487 for pi in 0..a_len.min(b_len) {
2488 let ap = &a.parts[pi];
2489 let bp = &b.parts[pi];
2490 if ap.name != bp.name || ap.short_name != bp.short_name {
2491 changes.push(ScoreChange::PartNamesChanged {
2492 part: pi,
2493 old_name: ap.name.clone(),
2494 new_name: bp.name.clone(),
2495 old_short_name: ap.short_name.clone(),
2496 new_short_name: bp.short_name.clone(),
2497 });
2498 }
2499 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
2500 changes.push(ScoreChange::PartMidiChanged {
2501 part: pi,
2502 old_channel: ap.midi_channel,
2503 new_channel: bp.midi_channel,
2504 old_program: ap.midi_program,
2505 new_program: bp.midi_program,
2506 });
2507 }
2508 if ap.instrument != bp.instrument {
2509 changes.push(ScoreChange::InstrumentDefinitionChanged {
2510 part: pi,
2511 old: ap.instrument.clone(),
2512 new: bp.instrument.clone(),
2513 });
2514 }
2515 if ap.midi_pitch_bends != bp.midi_pitch_bends
2516 || ap.midi_control_changes != bp.midi_control_changes
2517 || ap.midi_program_changes != bp.midi_program_changes
2518 || ap.midi_aftertouch != bp.midi_aftertouch
2519 {
2520 changes.push(ScoreChange::PartMidiAutomationChanged {
2521 part: pi,
2522 old_pitch_bends: ap.midi_pitch_bends.clone(),
2523 new_pitch_bends: bp.midi_pitch_bends.clone(),
2524 old_control_changes: ap.midi_control_changes.clone(),
2525 new_control_changes: bp.midi_control_changes.clone(),
2526 old_program_changes: ap.midi_program_changes.clone(),
2527 new_program_changes: bp.midi_program_changes.clone(),
2528 old_aftertouch: ap.midi_aftertouch.clone(),
2529 new_aftertouch: bp.midi_aftertouch.clone(),
2530 });
2531 }
2532 for si in 0..ap.staves.len().min(bp.staves.len()) {
2533 let a_staff = &ap.staves[si];
2534 let b_staff = &bp.staves[si];
2535 if a_staff.clef != b_staff.clef
2536 || a_staff.transpose_semitones != b_staff.transpose_semitones
2537 {
2538 changes.push(ScoreChange::StaffConfigurationChanged {
2539 part: pi,
2540 staff: si,
2541 old_clef: a_staff.clef.clone(),
2542 new_clef: b_staff.clef.clone(),
2543 old_transpose_semitones: a_staff.transpose_semitones,
2544 new_transpose_semitones: b_staff.transpose_semitones,
2545 });
2546 }
2547 if a_staff.tablature != b_staff.tablature {
2548 changes.push(ScoreChange::TablatureConfigChanged {
2549 part: pi,
2550 staff: si,
2551 old: a_staff.tablature.clone(),
2552 new: b_staff.tablature.clone(),
2553 });
2554 }
2555 if a_staff.presentation != b_staff.presentation {
2556 changes.push(ScoreChange::StaffPresentationChanged {
2557 part: pi,
2558 staff: si,
2559 old: a_staff.presentation.clone(),
2560 new: b_staff.presentation.clone(),
2561 });
2562 }
2563 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
2564 let am = &a_staff.measures[mi];
2565 let bm = &b_staff.measures[mi];
2566 if am.tablature_change != bm.tablature_change {
2567 changes.push(ScoreChange::TablatureChangeChanged {
2568 part: pi,
2569 staff: si,
2570 measure: mi,
2571 old: am.tablature_change.clone(),
2572 new: bm.tablature_change.clone(),
2573 });
2574 }
2575 if am.number != bm.number
2576 || am.clef != bm.clef
2577 || am.tempo_text != bm.tempo_text
2578 || am.navigation != bm.navigation
2579 || am.expression_text != bm.expression_text
2580 || am.multi_rest_count != bm.multi_rest_count
2581 || am.system_break != bm.system_break
2582 || am.page_break != bm.page_break
2583 || am.section_break != bm.section_break
2584 {
2585 changes.push(ScoreChange::MeasurePresentationChanged {
2586 part: pi,
2587 staff: si,
2588 measure: mi,
2589 old_number: am.number,
2590 new_number: bm.number,
2591 old_clef: am.clef.clone(),
2592 new_clef: bm.clef.clone(),
2593 old_tempo_text: am.tempo_text.clone(),
2594 new_tempo_text: bm.tempo_text.clone(),
2595 old_navigation: am.navigation.clone(),
2596 new_navigation: bm.navigation.clone(),
2597 old_expression_text: am.expression_text.clone(),
2598 new_expression_text: bm.expression_text.clone(),
2599 old_multi_rest_count: am.multi_rest_count,
2600 new_multi_rest_count: bm.multi_rest_count,
2601 old_system_break: am.system_break,
2602 new_system_break: bm.system_break,
2603 old_page_break: am.page_break,
2604 new_page_break: bm.page_break,
2605 old_section_break: am.section_break,
2606 new_section_break: bm.section_break,
2607 });
2608 }
2609 for vi in 0..4usize {
2610 let av = &am.voices[vi];
2611 let bv = &bm.voices[vi];
2612 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
2613 if !note_content_eq(a_note, b_note) {
2614 changes.push(ScoreChange::NoteModified {
2615 part: pi,
2616 staff: si,
2617 measure: mi,
2618 voice: vi,
2619 note_index: ni,
2620 old: Box::new(a_note.clone()),
2621 new: Box::new(b_note.clone()),
2622 });
2623 }
2624 }
2625 for note in av.iter().skip(bv.len()) {
2626 changes.push(ScoreChange::NoteRemoved {
2627 part: pi,
2628 staff: si,
2629 measure: mi,
2630 voice: vi,
2631 note: Box::new(note.clone()),
2632 });
2633 }
2634 for ni in av.len()..bv.len() {
2635 changes.push(ScoreChange::NoteAdded {
2636 part: pi,
2637 staff: si,
2638 measure: mi,
2639 voice: vi,
2640 note_index: ni,
2641 });
2642 }
2643 }
2644 if am.time_sig != bm.time_sig {
2645 changes.push(ScoreChange::TimeSigChanged {
2646 part: pi,
2647 staff: si,
2648 measure: mi,
2649 old: am.time_sig.clone(),
2650 new: bm.time_sig.clone(),
2651 });
2652 }
2653 if am.tempo != bm.tempo {
2654 changes.push(ScoreChange::MeasureTempoChanged {
2655 part: pi,
2656 staff: si,
2657 measure: mi,
2658 old: am.tempo,
2659 new: bm.tempo,
2660 });
2661 }
2662 if am.tempo_ramp_to != bm.tempo_ramp_to {
2663 changes.push(ScoreChange::MeasureTempoRampChanged {
2664 part: pi,
2665 staff: si,
2666 measure: mi,
2667 old: am.tempo_ramp_to,
2668 new: bm.tempo_ramp_to,
2669 });
2670 }
2671 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
2672 changes.push(ScoreChange::BarlineChanged {
2673 part: pi,
2674 staff: si,
2675 measure: mi,
2676 });
2677 }
2678 if am.rehearsal != bm.rehearsal {
2679 changes.push(ScoreChange::RehearsalMarkChanged {
2680 part: pi,
2681 staff: si,
2682 measure: mi,
2683 old: am.rehearsal.clone(),
2684 new: bm.rehearsal.clone(),
2685 });
2686 }
2687 if am.volta != bm.volta {
2688 changes.push(ScoreChange::VoltaChanged {
2689 part: pi,
2690 staff: si,
2691 measure: mi,
2692 });
2693 }
2694 if am.texts != bm.texts {
2695 changes.push(ScoreChange::MeasureTextChanged {
2696 part: pi,
2697 staff: si,
2698 measure: mi,
2699 old: am.texts.clone(),
2700 new: bm.texts.clone(),
2701 });
2702 }
2703 if am.figured_bass != bm.figured_bass {
2704 changes.push(ScoreChange::FiguredBassChanged {
2705 part: pi,
2706 staff: si,
2707 measure: mi,
2708 old: am.figured_bass.clone(),
2709 new: bm.figured_bass.clone(),
2710 });
2711 }
2712 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
2713 changes.push(ScoreChange::HarpPedalDiagramsChanged {
2714 part: pi,
2715 staff: si,
2716 measure: mi,
2717 old: am.harp_pedal_diagrams.clone(),
2718 new: bm.harp_pedal_diagrams.clone(),
2719 });
2720 }
2721 }
2722 }
2723 }
2724
2725 changes
2726}
2727
2728fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
2729 if a.settings.time_signature != b.settings.time_signature {
2730 return Some("settings.time_signature".to_string());
2731 }
2732 if a.part_groups.len() != b.part_groups.len()
2733 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
2734 x.first_part != y.first_part
2735 || x.last_part != y.last_part
2736 || x.symbol != y.symbol
2737 || x.barlines_connect != y.barlines_connect
2738 })
2739 {
2740 return Some("part_groups".to_string());
2741 }
2742 a.parts
2743 .iter()
2744 .zip(&b.parts)
2745 .enumerate()
2746 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
2747}
2748
2749fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
2750 let prefix = format!("parts[{part_index}]");
2751 if a.percussion_instruments != b.percussion_instruments {
2752 return Some(format!("{prefix}.percussion_instruments"));
2753 }
2754 if a.staff_groups != b.staff_groups {
2755 return Some(format!("{prefix}.staff_groups"));
2756 }
2757 if a.staves.len() != b.staves.len() {
2758 return Some(format!("{prefix}.staves"));
2759 }
2760 a.staves
2761 .iter()
2762 .zip(&b.staves)
2763 .enumerate()
2764 .find_map(|(staff_index, (a, b))| {
2765 first_unrepresented_staff_change(&prefix, staff_index, a, b)
2766 })
2767}
2768
2769fn first_unrepresented_staff_change(
2770 part_prefix: &str,
2771 staff_index: usize,
2772 a: &Staff,
2773 b: &Staff,
2774) -> Option<String> {
2775 let prefix = format!("{part_prefix}.staves[{staff_index}]");
2776 if a.measures.len() != b.measures.len() {
2777 return Some(format!("{prefix}.measures"));
2778 }
2779 a.measures
2780 .iter()
2781 .zip(&b.measures)
2782 .enumerate()
2783 .find_map(|(measure_index, (a, b))| {
2784 first_unrepresented_measure_change(&prefix, measure_index, a, b)
2785 })
2786}
2787
2788fn first_unrepresented_measure_change(
2789 _staff_prefix: &str,
2790 _measure_index: usize,
2791 _a: &Measure,
2792 _b: &Measure,
2793) -> Option<String> {
2794 None
2795}
2796
2797#[derive(Debug, Clone, Serialize, Deserialize)]
2804pub enum ScorePatch {
2805 SetScoreViews {
2806 value: Vec<ScoreView>,
2807 },
2808 SetScoreTexts {
2809 value: Vec<StyledText>,
2810 },
2811 SetMeasureTexts {
2812 part: usize,
2813 staff: usize,
2814 measure: usize,
2815 value: Vec<StyledText>,
2816 },
2817 SetFiguredBass {
2818 part: usize,
2819 staff: usize,
2820 measure: usize,
2821 value: Vec<FiguredBassFigure>,
2822 },
2823 SetHarpPedalDiagrams {
2824 part: usize,
2825 staff: usize,
2826 measure: usize,
2827 value: Vec<HarpPedalDiagram>,
2828 },
2829 SetChordDefinitions {
2830 value: Vec<ChordDefinition>,
2831 },
2832 SetPartNames {
2833 part: usize,
2834 name: String,
2835 short_name: String,
2836 },
2837 SetPartMidi {
2838 part: usize,
2839 channel: u8,
2840 program: u8,
2841 },
2842 SetInstrumentDefinition {
2843 part: usize,
2844 value: Option<InstrumentDefinition>,
2845 },
2846 SetPartMidiAutomation {
2847 part: usize,
2848 pitch_bends: Vec<MidiPitchBend>,
2849 control_changes: Vec<MidiControlChange>,
2850 program_changes: Vec<MidiProgramChange>,
2851 aftertouch: Vec<MidiAftertouch>,
2852 },
2853 SetStaffConfiguration {
2854 part: usize,
2855 staff: usize,
2856 clef: Clef,
2857 transpose_semitones: i8,
2858 },
2859 SetStaffPresentation {
2860 part: usize,
2861 staff: usize,
2862 value: StaffPresentation,
2863 },
2864 SetMeasurePresentation {
2865 part: usize,
2866 staff: usize,
2867 measure: usize,
2868 number: u32,
2869 clef: Option<Clef>,
2870 tempo_text: Option<String>,
2871 navigation: Option<String>,
2872 expression_text: Option<String>,
2873 multi_rest_count: Option<u8>,
2874 system_break: bool,
2875 page_break: bool,
2876 #[serde(default)]
2877 section_break: bool,
2878 },
2879 SetTablatureConfig {
2880 part: usize,
2881 staff: usize,
2882 value: Option<TablatureConfig>,
2883 },
2884 SetMeasureTablatureChange {
2885 part: usize,
2886 staff: usize,
2887 measure: usize,
2888 value: Option<TablatureConfig>,
2889 },
2890 SetMetadata {
2891 field: String,
2892 value: String,
2893 },
2894 SetTempo {
2895 value: u16,
2896 },
2897 SetKeySignature {
2898 part: usize,
2899 staff: usize,
2900 measure: usize,
2901 value: Option<KeySignature>,
2902 },
2903 SetTimeSignature {
2904 part: usize,
2905 staff: usize,
2906 measure: usize,
2907 value: Option<TimeSignature>,
2908 },
2909 SetBarlines {
2910 part: usize,
2911 staff: usize,
2912 measure: usize,
2913 left: Barline,
2914 right: Barline,
2915 },
2916 SetRehearsal {
2917 part: usize,
2918 staff: usize,
2919 measure: usize,
2920 value: Option<String>,
2921 },
2922 SetVolta {
2923 part: usize,
2924 staff: usize,
2925 measure: usize,
2926 value: Option<VoltaBracket>,
2927 },
2928 AddNote {
2930 part: usize,
2931 staff: usize,
2932 measure: usize,
2933 voice: usize,
2934 #[serde(default = "legacy_append_index")]
2936 note_index: usize,
2937 note: Box<Note>,
2938 },
2939 RemoveNote {
2940 part: usize,
2941 staff: usize,
2942 measure: usize,
2943 voice: usize,
2944 note_index: usize,
2945 },
2946 ReplaceNote {
2948 part: usize,
2949 staff: usize,
2950 measure: usize,
2951 voice: usize,
2952 note_index: usize,
2953 note: Box<Note>,
2954 },
2955 SetMeasureTempo {
2956 part: usize,
2957 staff: usize,
2958 measure: usize,
2959 value: Option<u16>,
2960 },
2961 SetMeasureTempoRamp {
2962 part: usize,
2963 staff: usize,
2964 measure: usize,
2965 value: Option<u16>,
2966 },
2967 ReplaceScore {
2970 score: Box<Score>,
2971 },
2972}
2973
2974fn legacy_append_index() -> usize {
2975 usize::MAX
2976}
2977
2978fn patch_requires_replace(a: &Score, b: &Score) -> bool {
2982 if a.settings.time_signature != b.settings.time_signature
2983 || a.settings.key_signature != b.settings.key_signature
2984 || a.parts.len() != b.parts.len()
2985 || a.part_groups.len() != b.part_groups.len()
2986 {
2987 return true;
2988 }
2989 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
2990 x.first_part != y.first_part
2991 || x.last_part != y.last_part
2992 || x.symbol != y.symbol
2993 || x.barlines_connect != y.barlines_connect
2994 }) {
2995 return true;
2996 }
2997 for (ap, bp) in a.parts.iter().zip(&b.parts) {
2998 if ap.percussion_instruments != bp.percussion_instruments
2999 || ap.staff_groups != bp.staff_groups
3000 || ap.staves.len() != bp.staves.len()
3001 {
3002 return true;
3003 }
3004 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
3005 if as_.measures.len() != bs.measures.len() {
3006 return true;
3007 }
3008 }
3009 }
3010 false
3011}
3012
3013pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
3018 let mut patches: Vec<ScorePatch> = Vec::new();
3019
3020 if patch_requires_replace(a, b) {
3021 return vec![ScorePatch::ReplaceScore {
3022 score: Box::new(b.clone()),
3023 }];
3024 }
3025
3026 if a.views != b.views {
3027 patches.push(ScorePatch::SetScoreViews {
3028 value: b.views.clone(),
3029 });
3030 }
3031
3032 if a.texts != b.texts {
3033 patches.push(ScorePatch::SetScoreTexts {
3034 value: b.texts.clone(),
3035 });
3036 }
3037 if a.chord_definitions != b.chord_definitions {
3038 patches.push(ScorePatch::SetChordDefinitions {
3039 value: b.chord_definitions.clone(),
3040 });
3041 }
3042
3043 macro_rules! meta {
3044 ($field:ident, $name:literal) => {
3045 if a.metadata.$field != b.metadata.$field {
3046 patches.push(ScorePatch::SetMetadata {
3047 field: $name.to_string(),
3048 value: b.metadata.$field.clone(),
3049 });
3050 }
3051 };
3052 }
3053 meta!(title, "title");
3054 meta!(composer, "composer");
3055 meta!(lyricist, "lyricist");
3056 meta!(copyright, "copyright");
3057 meta!(work_number, "work_number");
3058 meta!(movement_title, "movement_title");
3059
3060 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3061 patches.push(ScorePatch::SetTempo {
3062 value: b.settings.tempo_bpm,
3063 });
3064 }
3065
3066 for pi in 0..a.parts.len().min(b.parts.len()) {
3067 let ap = &a.parts[pi];
3068 let bp = &b.parts[pi];
3069 if ap.name != bp.name || ap.short_name != bp.short_name {
3070 patches.push(ScorePatch::SetPartNames {
3071 part: pi,
3072 name: bp.name.clone(),
3073 short_name: bp.short_name.clone(),
3074 });
3075 }
3076 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3077 patches.push(ScorePatch::SetPartMidi {
3078 part: pi,
3079 channel: bp.midi_channel,
3080 program: bp.midi_program,
3081 });
3082 }
3083 if ap.instrument != bp.instrument {
3084 patches.push(ScorePatch::SetInstrumentDefinition {
3085 part: pi,
3086 value: bp.instrument.clone(),
3087 });
3088 }
3089 if ap.midi_pitch_bends != bp.midi_pitch_bends
3090 || ap.midi_control_changes != bp.midi_control_changes
3091 || ap.midi_program_changes != bp.midi_program_changes
3092 || ap.midi_aftertouch != bp.midi_aftertouch
3093 {
3094 patches.push(ScorePatch::SetPartMidiAutomation {
3095 part: pi,
3096 pitch_bends: bp.midi_pitch_bends.clone(),
3097 control_changes: bp.midi_control_changes.clone(),
3098 program_changes: bp.midi_program_changes.clone(),
3099 aftertouch: bp.midi_aftertouch.clone(),
3100 });
3101 }
3102 for si in 0..ap.staves.len().min(bp.staves.len()) {
3103 let a_staff = &ap.staves[si];
3104 let b_staff = &bp.staves[si];
3105 if a_staff.clef != b_staff.clef
3106 || a_staff.transpose_semitones != b_staff.transpose_semitones
3107 {
3108 patches.push(ScorePatch::SetStaffConfiguration {
3109 part: pi,
3110 staff: si,
3111 clef: b_staff.clef.clone(),
3112 transpose_semitones: b_staff.transpose_semitones,
3113 });
3114 }
3115 if a_staff.tablature != b_staff.tablature {
3116 patches.push(ScorePatch::SetTablatureConfig {
3117 part: pi,
3118 staff: si,
3119 value: b_staff.tablature.clone(),
3120 });
3121 }
3122 if a_staff.presentation != b_staff.presentation {
3123 patches.push(ScorePatch::SetStaffPresentation {
3124 part: pi,
3125 staff: si,
3126 value: b_staff.presentation.clone(),
3127 });
3128 }
3129 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3130 let am = &a_staff.measures[mi];
3131 let bm = &b_staff.measures[mi];
3132
3133 if am.tablature_change != bm.tablature_change {
3134 patches.push(ScorePatch::SetMeasureTablatureChange {
3135 part: pi,
3136 staff: si,
3137 measure: mi,
3138 value: bm.tablature_change.clone(),
3139 });
3140 }
3141
3142 if am.number != bm.number
3143 || am.clef != bm.clef
3144 || am.tempo_text != bm.tempo_text
3145 || am.navigation != bm.navigation
3146 || am.expression_text != bm.expression_text
3147 || am.multi_rest_count != bm.multi_rest_count
3148 || am.system_break != bm.system_break
3149 || am.page_break != bm.page_break
3150 || am.section_break != bm.section_break
3151 {
3152 patches.push(ScorePatch::SetMeasurePresentation {
3153 part: pi,
3154 staff: si,
3155 measure: mi,
3156 number: bm.number,
3157 clef: bm.clef.clone(),
3158 tempo_text: bm.tempo_text.clone(),
3159 navigation: bm.navigation.clone(),
3160 expression_text: bm.expression_text.clone(),
3161 multi_rest_count: bm.multi_rest_count,
3162 system_break: bm.system_break,
3163 page_break: bm.page_break,
3164 section_break: bm.section_break,
3165 });
3166 }
3167 if am.key_sig != bm.key_sig {
3168 patches.push(ScorePatch::SetKeySignature {
3169 part: pi,
3170 staff: si,
3171 measure: mi,
3172 value: bm.key_sig.clone(),
3173 });
3174 }
3175 if am.time_sig != bm.time_sig {
3176 patches.push(ScorePatch::SetTimeSignature {
3177 part: pi,
3178 staff: si,
3179 measure: mi,
3180 value: bm.time_sig.clone(),
3181 });
3182 }
3183 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3184 patches.push(ScorePatch::SetBarlines {
3185 part: pi,
3186 staff: si,
3187 measure: mi,
3188 left: bm.barline_left.clone(),
3189 right: bm.barline_right.clone(),
3190 });
3191 }
3192 if am.rehearsal != bm.rehearsal {
3193 patches.push(ScorePatch::SetRehearsal {
3194 part: pi,
3195 staff: si,
3196 measure: mi,
3197 value: bm.rehearsal.clone(),
3198 });
3199 }
3200 if am.volta != bm.volta {
3201 patches.push(ScorePatch::SetVolta {
3202 part: pi,
3203 staff: si,
3204 measure: mi,
3205 value: bm.volta.clone(),
3206 });
3207 }
3208 if am.tempo != bm.tempo {
3209 patches.push(ScorePatch::SetMeasureTempo {
3210 part: pi,
3211 staff: si,
3212 measure: mi,
3213 value: bm.tempo,
3214 });
3215 }
3216 if am.tempo_ramp_to != bm.tempo_ramp_to {
3217 patches.push(ScorePatch::SetMeasureTempoRamp {
3218 part: pi,
3219 staff: si,
3220 measure: mi,
3221 value: bm.tempo_ramp_to,
3222 });
3223 }
3224 if am.texts != bm.texts {
3225 patches.push(ScorePatch::SetMeasureTexts {
3226 part: pi,
3227 staff: si,
3228 measure: mi,
3229 value: bm.texts.clone(),
3230 });
3231 }
3232 if am.figured_bass != bm.figured_bass {
3233 patches.push(ScorePatch::SetFiguredBass {
3234 part: pi,
3235 staff: si,
3236 measure: mi,
3237 value: bm.figured_bass.clone(),
3238 });
3239 }
3240 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3241 patches.push(ScorePatch::SetHarpPedalDiagrams {
3242 part: pi,
3243 staff: si,
3244 measure: mi,
3245 value: bm.harp_pedal_diagrams.clone(),
3246 });
3247 }
3248
3249 for vi in 0..4usize {
3250 let av = &am.voices[vi];
3251 let bv = &bm.voices[vi];
3252 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3253 if !note_content_eq(a_note, b_note) {
3254 patches.push(ScorePatch::ReplaceNote {
3255 part: pi,
3256 staff: si,
3257 measure: mi,
3258 voice: vi,
3259 note_index: ni,
3260 note: Box::new(b_note.clone()),
3261 });
3262 }
3263 }
3264 for ni in (bv.len()..av.len()).rev() {
3266 patches.push(ScorePatch::RemoveNote {
3267 part: pi,
3268 staff: si,
3269 measure: mi,
3270 voice: vi,
3271 note_index: ni,
3272 });
3273 }
3274 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
3276 patches.push(ScorePatch::AddNote {
3277 part: pi,
3278 staff: si,
3279 measure: mi,
3280 voice: vi,
3281 note_index: av.len() + offset,
3282 note: Box::new(note.clone()),
3283 });
3284 }
3285 }
3286 }
3287 }
3288 }
3289
3290 patches
3291}
3292
3293pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
3298 let mut s = score.clone();
3299 for patch in patches {
3300 match patch {
3301 ScorePatch::ReplaceScore { score } => {
3302 s = (**score).clone();
3303 }
3304 ScorePatch::SetScoreViews { value } => {
3305 s.views = value.clone();
3306 }
3307 ScorePatch::SetScoreTexts { value } => {
3308 s.texts = value.clone();
3309 }
3310 ScorePatch::SetMeasureTexts {
3311 part,
3312 staff,
3313 measure,
3314 value,
3315 } => {
3316 s.parts
3317 .get_mut(*part)
3318 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3319 .staves
3320 .get_mut(*staff)
3321 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3322 .measures
3323 .get_mut(*measure)
3324 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3325 .texts = value.clone();
3326 }
3327 ScorePatch::SetFiguredBass {
3328 part,
3329 staff,
3330 measure,
3331 value,
3332 } => {
3333 s.parts
3334 .get_mut(*part)
3335 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3336 .staves
3337 .get_mut(*staff)
3338 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3339 .measures
3340 .get_mut(*measure)
3341 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3342 .figured_bass = value.clone();
3343 }
3344 ScorePatch::SetHarpPedalDiagrams {
3345 part,
3346 staff,
3347 measure,
3348 value,
3349 } => {
3350 s.parts
3351 .get_mut(*part)
3352 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3353 .staves
3354 .get_mut(*staff)
3355 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3356 .measures
3357 .get_mut(*measure)
3358 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3359 .harp_pedal_diagrams = value.clone();
3360 }
3361 ScorePatch::SetChordDefinitions { value } => {
3362 s.chord_definitions = value.clone();
3363 }
3364 ScorePatch::SetPartNames {
3365 part,
3366 name,
3367 short_name,
3368 } => {
3369 let target = s
3370 .parts
3371 .get_mut(*part)
3372 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3373 target.name = name.clone();
3374 target.short_name = short_name.clone();
3375 }
3376 ScorePatch::SetPartMidi {
3377 part,
3378 channel,
3379 program,
3380 } => {
3381 let target = s
3382 .parts
3383 .get_mut(*part)
3384 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3385 target.midi_channel = *channel;
3386 target.midi_program = *program;
3387 }
3388 ScorePatch::SetInstrumentDefinition { part, value } => {
3389 s.parts
3390 .get_mut(*part)
3391 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3392 .instrument = value.clone();
3393 }
3394 ScorePatch::SetPartMidiAutomation {
3395 part,
3396 pitch_bends,
3397 control_changes,
3398 program_changes,
3399 aftertouch,
3400 } => {
3401 let target = s
3402 .parts
3403 .get_mut(*part)
3404 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3405 target.midi_pitch_bends = pitch_bends.clone();
3406 target.midi_control_changes = control_changes.clone();
3407 target.midi_program_changes = program_changes.clone();
3408 target.midi_aftertouch = aftertouch.clone();
3409 }
3410 ScorePatch::SetStaffConfiguration {
3411 part,
3412 staff,
3413 clef,
3414 transpose_semitones,
3415 } => {
3416 let target = s
3417 .parts
3418 .get_mut(*part)
3419 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3420 .staves
3421 .get_mut(*staff)
3422 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3423 target.clef = clef.clone();
3424 target.transpose_semitones = *transpose_semitones;
3425 }
3426 ScorePatch::SetStaffPresentation { part, staff, value } => {
3427 let target = s
3428 .parts
3429 .get_mut(*part)
3430 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3431 .staves
3432 .get_mut(*staff)
3433 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3434 target.presentation = value.clone();
3435 }
3436 ScorePatch::SetMeasurePresentation {
3437 part,
3438 staff,
3439 measure,
3440 number,
3441 clef,
3442 tempo_text,
3443 navigation,
3444 expression_text,
3445 multi_rest_count,
3446 system_break,
3447 page_break,
3448 section_break,
3449 } => {
3450 let target = s
3451 .parts
3452 .get_mut(*part)
3453 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3454 .staves
3455 .get_mut(*staff)
3456 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3457 .measures
3458 .get_mut(*measure)
3459 .ok_or_else(|| {
3460 Error::InvalidPatch(format!("measure {measure} out of range"))
3461 })?;
3462 target.number = *number;
3463 target.clef = clef.clone();
3464 target.tempo_text = tempo_text.clone();
3465 target.navigation = navigation.clone();
3466 target.expression_text = expression_text.clone();
3467 target.multi_rest_count = *multi_rest_count;
3468 target.system_break = *system_break;
3469 target.page_break = *page_break;
3470 target.section_break = *section_break;
3471 }
3472 ScorePatch::SetTablatureConfig { part, staff, value } => {
3473 s.parts
3474 .get_mut(*part)
3475 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3476 .staves
3477 .get_mut(*staff)
3478 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3479 .tablature = value.clone();
3480 }
3481 ScorePatch::SetMeasureTablatureChange {
3482 part,
3483 staff,
3484 measure,
3485 value,
3486 } => {
3487 s.parts
3488 .get_mut(*part)
3489 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3490 .staves
3491 .get_mut(*staff)
3492 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3493 .measures
3494 .get_mut(*measure)
3495 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3496 .tablature_change = value.clone();
3497 }
3498 ScorePatch::SetMetadata { field, value } => match field.as_str() {
3499 "title" => s.metadata.title = value.clone(),
3500 "composer" => s.metadata.composer = value.clone(),
3501 "lyricist" => s.metadata.lyricist = value.clone(),
3502 "copyright" => s.metadata.copyright = value.clone(),
3503 "work_number" => s.metadata.work_number = value.clone(),
3504 "movement_title" => s.metadata.movement_title = value.clone(),
3505 other => {
3506 return Err(Error::InvalidPatch(format!(
3507 "unknown metadata field: {other}"
3508 )));
3509 }
3510 },
3511 ScorePatch::SetTempo { value } => {
3512 s.settings.tempo_bpm = *value;
3513 }
3514 ScorePatch::SetKeySignature {
3515 part,
3516 staff,
3517 measure,
3518 value,
3519 } => {
3520 s.parts
3521 .get_mut(*part)
3522 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3523 .staves
3524 .get_mut(*staff)
3525 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3526 .measures
3527 .get_mut(*measure)
3528 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3529 .key_sig = value.clone();
3530 }
3531 ScorePatch::SetTimeSignature {
3532 part,
3533 staff,
3534 measure,
3535 value,
3536 } => {
3537 s.parts
3538 .get_mut(*part)
3539 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3540 .staves
3541 .get_mut(*staff)
3542 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3543 .measures
3544 .get_mut(*measure)
3545 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3546 .time_sig = value.clone();
3547 }
3548 ScorePatch::SetBarlines {
3549 part,
3550 staff,
3551 measure,
3552 left,
3553 right,
3554 } => {
3555 let m = s
3556 .parts
3557 .get_mut(*part)
3558 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3559 .staves
3560 .get_mut(*staff)
3561 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3562 .measures
3563 .get_mut(*measure)
3564 .ok_or_else(|| {
3565 Error::InvalidPatch(format!("measure {measure} out of range"))
3566 })?;
3567 m.barline_left = left.clone();
3568 m.barline_right = right.clone();
3569 }
3570 ScorePatch::SetRehearsal {
3571 part,
3572 staff,
3573 measure,
3574 value,
3575 } => {
3576 s.parts
3577 .get_mut(*part)
3578 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3579 .staves
3580 .get_mut(*staff)
3581 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3582 .measures
3583 .get_mut(*measure)
3584 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3585 .rehearsal = value.clone();
3586 }
3587 ScorePatch::SetVolta {
3588 part,
3589 staff,
3590 measure,
3591 value,
3592 } => {
3593 s.parts
3594 .get_mut(*part)
3595 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3596 .staves
3597 .get_mut(*staff)
3598 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3599 .measures
3600 .get_mut(*measure)
3601 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3602 .volta = value.clone();
3603 }
3604 ScorePatch::AddNote {
3605 part,
3606 staff,
3607 measure,
3608 voice,
3609 note_index,
3610 note,
3611 } => {
3612 let v = s
3613 .parts
3614 .get_mut(*part)
3615 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3616 .staves
3617 .get_mut(*staff)
3618 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3619 .measures
3620 .get_mut(*measure)
3621 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3622 .voices
3623 .get_mut(*voice)
3624 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3625 let insert_at = if *note_index == usize::MAX {
3626 v.len()
3627 } else {
3628 *note_index
3629 };
3630 if insert_at > v.len() {
3631 return Err(Error::InvalidPatch(format!(
3632 "note_index {note_index} out of range"
3633 )));
3634 }
3635 v.insert(insert_at, *note.clone());
3636 }
3637 ScorePatch::RemoveNote {
3638 part,
3639 staff,
3640 measure,
3641 voice,
3642 note_index,
3643 } => {
3644 let v = s
3645 .parts
3646 .get_mut(*part)
3647 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3648 .staves
3649 .get_mut(*staff)
3650 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3651 .measures
3652 .get_mut(*measure)
3653 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3654 .voices
3655 .get_mut(*voice)
3656 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3657 if *note_index >= v.len() {
3658 return Err(Error::InvalidPatch(format!(
3659 "note_index {note_index} out of range"
3660 )));
3661 }
3662 v.remove(*note_index);
3663 }
3664 ScorePatch::ReplaceNote {
3665 part,
3666 staff,
3667 measure,
3668 voice,
3669 note_index,
3670 note,
3671 } => {
3672 let v = s
3673 .parts
3674 .get_mut(*part)
3675 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3676 .staves
3677 .get_mut(*staff)
3678 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3679 .measures
3680 .get_mut(*measure)
3681 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3682 .voices
3683 .get_mut(*voice)
3684 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3685 if *note_index >= v.len() {
3686 return Err(Error::InvalidPatch(format!(
3687 "note_index {note_index} out of range"
3688 )));
3689 }
3690 v[*note_index] = *note.clone();
3691 }
3692 ScorePatch::SetMeasureTempo {
3693 part,
3694 staff,
3695 measure,
3696 value,
3697 } => {
3698 s.parts
3699 .get_mut(*part)
3700 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3701 .staves
3702 .get_mut(*staff)
3703 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3704 .measures
3705 .get_mut(*measure)
3706 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3707 .tempo = *value;
3708 }
3709 ScorePatch::SetMeasureTempoRamp {
3710 part,
3711 staff,
3712 measure,
3713 value,
3714 } => {
3715 s.parts
3716 .get_mut(*part)
3717 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3718 .staves
3719 .get_mut(*staff)
3720 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3721 .measures
3722 .get_mut(*measure)
3723 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3724 .tempo_ramp_to = *value;
3725 }
3726 }
3727 }
3728 if !super::validate::validate(&s).is_valid() {
3729 return Err(Error::InvalidScore);
3730 }
3731 Ok(s)
3732}
3733
3734pub fn respell_score(score: &mut Score, prefer_flat: bool) {
3738 for part in &mut score.parts {
3739 for staff in &mut part.staves {
3740 for measure in &mut staff.measures {
3741 for voice in &mut measure.voices {
3742 for note in voice.iter_mut() {
3743 for pitch in &mut note.pitches {
3744 *pitch = pitch.respell(prefer_flat);
3745 }
3746 }
3747 }
3748 }
3749 }
3750 }
3751}
3752
3753#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3755#[serde(rename_all = "snake_case")]
3756pub enum RespellPolicy {
3757 Flat,
3759 Sharp,
3761 Key,
3763}
3764
3765pub fn respell_staff_region(
3776 score: &mut Score,
3777 part_index: usize,
3778 staff_index: usize,
3779 start_measure: usize,
3780 end_measure: usize,
3781 policy: RespellPolicy,
3782) -> Result<(usize, usize), Error> {
3783 let score_key_flat = score.settings.key_signature.fifths < 0;
3784 let part = score
3785 .parts
3786 .get_mut(part_index)
3787 .ok_or(Error::PartNotFound(part_index))?;
3788 let staff_count = part.staves.len();
3789 if staff_index >= staff_count {
3790 return Err(Error::StaffNotFound(staff_index));
3791 }
3792 let measure_count = part.staves[staff_index].measures.len();
3793 if start_measure >= end_measure || end_measure > measure_count {
3794 return Err(Error::InvalidCommand(format!(
3795 "invalid measure range {start_measure}..{end_measure}"
3796 )));
3797 }
3798 let mut prefer_flat = Vec::with_capacity(end_measure);
3799 let mut running_flat = score_key_flat;
3800 for index in 0..end_measure {
3801 let key = part.staves[staff_index].measures[index]
3802 .key_sig
3803 .as_ref()
3804 .or_else(|| {
3805 part.staves[0]
3806 .measures
3807 .get(index)
3808 .and_then(|measure| measure.key_sig.as_ref())
3809 });
3810 if let Some(key) = key {
3811 running_flat = key.fifths < 0;
3812 }
3813 prefer_flat.push(match policy {
3814 RespellPolicy::Flat => true,
3815 RespellPolicy::Sharp => false,
3816 RespellPolicy::Key => running_flat,
3817 });
3818 }
3819
3820 let measures = &mut part.staves[staff_index].measures;
3821 for (index, measure) in measures
3822 .iter_mut()
3823 .enumerate()
3824 .take(end_measure)
3825 .skip(start_measure)
3826 {
3827 for voice in &mut measure.voices {
3828 for note in voice
3829 .iter_mut()
3830 .filter(|note| !note.is_rest && !note.is_unpitched)
3831 {
3832 for pitch in &mut note.pitches {
3833 *pitch = pitch.respell(prefer_flat[index]);
3834 }
3835 }
3836 }
3837 }
3838
3839 let mut changed = (start_measure, end_measure - 1);
3840 for voice in 0..4 {
3841 for measure in start_measure..end_measure {
3842 for note in 0..measures[measure].voices[voice].len() {
3843 if measures[measure].voices[voice][note].tie_start {
3844 let last = propagate_tied_spelling(measures, voice, (measure, note), true);
3845 changed.1 = changed.1.max(last);
3846 }
3847 }
3848 }
3849 if !measures[start_measure].voices[voice].is_empty() {
3850 let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
3851 changed.0 = changed.0.min(first);
3852 }
3853 }
3854 Ok(changed)
3855}
3856
3857fn propagate_tied_spelling(
3860 measures: &mut [Measure],
3861 voice: usize,
3862 from: (usize, usize),
3863 forward: bool,
3864) -> usize {
3865 let mut current = from;
3866 loop {
3867 let note = &measures[current.0].voices[voice][current.1];
3868 if !(if forward {
3869 note.tie_start
3870 } else {
3871 note.tie_end
3872 }) {
3873 return current.0;
3874 }
3875 let neighbor = if forward {
3876 if current.1 + 1 < measures[current.0].voices[voice].len() {
3877 Some((current.0, current.1 + 1))
3878 } else {
3879 measures
3880 .get(current.0 + 1)
3881 .filter(|measure| !measure.voices[voice].is_empty())
3882 .map(|_| (current.0 + 1, 0))
3883 }
3884 } else if current.1 > 0 {
3885 Some((current.0, current.1 - 1))
3886 } else {
3887 current.0.checked_sub(1).and_then(|previous| {
3888 measures[previous].voices[voice]
3889 .len()
3890 .checked_sub(1)
3891 .map(|last| (previous, last))
3892 })
3893 };
3894 let Some(next) = neighbor else {
3895 return current.0;
3896 };
3897 let next_note = &measures[next.0].voices[voice][next.1];
3898 if !(if forward {
3899 next_note.tie_end
3900 } else {
3901 next_note.tie_start
3902 }) {
3903 return current.0;
3904 }
3905 let source = measures[current.0].voices[voice][current.1].pitches.clone();
3906 for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
3907 if let Some(spelled) = source.iter().find(|candidate| {
3908 candidate.to_midi() == pitch.to_midi()
3909 && candidate.microtone_cents == pitch.microtone_cents
3910 }) {
3911 *pitch = spelled.clone();
3912 }
3913 }
3914 current = next;
3915 }
3916}
3917
3918pub fn respell_score_to_key(score: &mut Score) {
3922 let prefer_flat = score.settings.key_signature.fifths < 0;
3923 respell_score(score, prefer_flat);
3924}
3925
3926pub fn score_duration_secs(score: &Score) -> f64 {
3931 if score.settings.tempo_bpm == 0 {
3932 return 0.0;
3933 }
3934 let seq = measure_sequence(score);
3935 let mut total_secs = 0.0f64;
3936 let mut current_bpm = score.settings.tempo_bpm as f64;
3937 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
3938 for &idx in &seq {
3939 if let Some(m) = staff.measures.get(idx) {
3940 if let Some(b) = m.tempo {
3941 current_bpm = b as f64;
3942 }
3943 if current_bpm == 0.0 {
3944 continue;
3945 }
3946 let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
3947 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
3948 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
3949 current_bpm = f64::from(target);
3950 }
3951 }
3952 }
3953 }
3954 total_secs
3955}
3956
3957pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
3962 if score.settings.tempo_bpm == 0 {
3963 return 0.0;
3964 }
3965 let seq: Vec<usize> = measure_sequence(score)
3966 .into_iter()
3967 .filter(|&idx| idx >= region.0 && idx <= region.1)
3968 .collect();
3969 let mut total_secs = 0.0f64;
3970 let mut current_bpm = score.settings.tempo_bpm as f64;
3971 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
3972 for &idx in &seq {
3973 if let Some(m) = staff.measures.get(idx) {
3974 if let Some(b) = m.tempo {
3975 current_bpm = b as f64;
3976 }
3977 if current_bpm == 0.0 {
3978 continue;
3979 }
3980 let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
3981 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
3982 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
3983 current_bpm = f64::from(target);
3984 }
3985 }
3986 }
3987 }
3988 total_secs
3989}
3990
3991fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
3992 let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
3993 return beats / start_bpm * 60.0;
3994 };
3995 let end_bpm = f64::from(target_bpm);
3996 let delta = end_bpm - start_bpm;
3997 if delta.abs() < f64::EPSILON {
3998 return beats / start_bpm * 60.0;
3999 }
4000 60.0 * beats / delta * (end_bpm / start_bpm).ln()
4001}
4002
4003pub fn measure_beats_remaining(
4008 score: &Score,
4009 part_index: usize,
4010 staff_index: usize,
4011 measure_index: usize,
4012 voice_index: usize,
4013) -> Result<f64, Error> {
4014 let part = score
4015 .parts
4016 .get(part_index)
4017 .ok_or(Error::PartNotFound(part_index))?;
4018 let staff = part
4019 .staves
4020 .get(staff_index)
4021 .ok_or(Error::StaffNotFound(staff_index))?;
4022 let measure = staff
4023 .measures
4024 .get(measure_index)
4025 .ok_or(Error::MeasureNotFound(measure_index))?;
4026 let voice = measure
4027 .voices
4028 .get(voice_index)
4029 .ok_or(Error::VoiceOutOfRange(voice_index))?;
4030 let ts = measure
4031 .time_sig
4032 .as_ref()
4033 .unwrap_or(&score.settings.time_signature);
4034 let used: f64 = voice.iter().map(|n| n.beats()).sum();
4035 Ok((ts.total_beats() - used).max(0.0))
4036}
4037
4038pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
4044 if pitches.is_empty() {
4045 return true;
4046 }
4047 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
4048 avg < clef.middle_line_midi() as f64
4049}
4050
4051fn beam_beat_size(ts: &TimeSignature) -> f64 {
4052 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
4053 3.0 * 4.0 / ts.denominator as f64
4054 } else {
4055 4.0 / ts.denominator as f64
4056 }
4057}
4058
4059pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
4064 let beat_size = beam_beat_size(time_sig);
4065 let n = notes.len();
4066 let mut result = vec![BeamState::None; n];
4067
4068 let is_beamable = |note: &Note| -> bool {
4069 !note.is_rest
4070 && matches!(
4071 note.duration,
4072 Duration::Eighth
4073 | Duration::Sixteenth
4074 | Duration::ThirtySecond
4075 | Duration::SixtyFourth
4076 )
4077 };
4078
4079 let mut starts = Vec::with_capacity(n);
4081 let mut pos = 0.0f64;
4082 for note in notes {
4083 starts.push(pos);
4084 pos += note.beats();
4085 }
4086
4087 let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
4089
4090 let mut i = 0;
4091 while i < n {
4092 if !is_beamable(¬es[i]) {
4093 i += 1;
4094 continue;
4095 }
4096 let g = group_id(i);
4097 let mut j = i;
4099 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
4100 j += 1;
4101 }
4102 let run = j - i;
4103 if run == 1 {
4104 result[i] = BeamState::None;
4105 } else {
4106 result[i] = BeamState::Begin;
4107 result[i + 1..j - 1].fill(BeamState::Continue);
4108 result[j - 1] = BeamState::End;
4109 }
4110 i = j;
4111 }
4112 result
4113}
4114
4115fn note_content_eq(a: &Note, b: &Note) -> bool {
4116 a.is_rest == b.is_rest
4117 && a.is_unpitched == b.is_unpitched
4118 && a.instrument_id == b.instrument_id
4119 && a.offset_x == b.offset_x
4120 && a.offset_y == b.offset_y
4121 && a.relative_x == b.relative_x
4122 && a.relative_y == b.relative_y
4123 && a.pitches == b.pitches
4124 && a.duration == b.duration
4125 && a.dot_count == b.dot_count
4126 && a.tie_start == b.tie_start
4127 && a.tie_end == b.tie_end
4128 && a.beam == b.beam
4129 && a.articulations == b.articulations
4130 && a.dynamic == b.dynamic
4131 && a.stem_up == b.stem_up
4132 && a.hairpin_start == b.hairpin_start
4133 && a.hairpin_end == b.hairpin_end
4134 && a.tuplet == b.tuplet
4135 && a.chord_symbol == b.chord_symbol
4136 && a.is_grace == b.is_grace
4137 && a.grace_slash == b.grace_slash
4138 && a.ottava_start == b.ottava_start
4139 && a.ottava_end == b.ottava_end
4140 && a.lyric == b.lyric
4141 && a.pedal_start == b.pedal_start
4142 && a.pedal_end == b.pedal_end
4143 && a.slur_start == b.slur_start
4144 && a.slur_end == b.slur_end
4145 && a.arpeggiate == b.arpeggiate
4146 && a.tab_position == b.tab_position
4147 && a.tab_positions == b.tab_positions
4148 && a.guitar_technique == b.guitar_technique
4149 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
4150 && a.guitar_bend_curve == b.guitar_bend_curve
4151}
4152
4153#[cfg(test)]
4154mod tests {
4155 use super::*;
4156 use crate::model::{
4157 notation::{FingeringSelectionPolicy, TextStyle},
4158 pitch::Step,
4159 };
4160
4161 #[test]
4162 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
4163 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4164 note.fingerings = vec![3, 1, 4];
4165 note.fingering = Some(3);
4166 assert_eq!(
4167 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
4168 Some(3)
4169 );
4170 assert_eq!(
4171 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
4172 Some(1)
4173 );
4174 assert_eq!(
4175 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
4176 Some(4)
4177 );
4178 assert_eq!(note.fingerings, vec![3, 1, 4]);
4179 assert_eq!(note.fingering, Some(3));
4180 }
4181
4182 #[test]
4183 fn default_score_has_one_part_four_measures() {
4184 let score = Score::default();
4185 assert_eq!(score.parts.len(), 1);
4186 assert_eq!(score.parts[0].staves.len(), 1);
4187 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
4188 }
4189
4190 #[test]
4191 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
4192 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4193 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4194 lines: 6,
4195 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4196 capo: 2,
4197 });
4198 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
4199 Pitch::with_alter(Step::F, 4, 1),
4200 Duration::Quarter,
4201 ));
4202 score.parts[0].staves[0].measures[0].voices[0]
4203 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
4204 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
4205 Some(TabPosition { string: 6, fret: 7 });
4206
4207 assert_eq!(assign_tablature_positions(&mut score), 1);
4208 let notes = &score.parts[0].staves[0].measures[0].voices[0];
4209 assert_eq!(
4210 notes[1].tab_position,
4211 Some(TabPosition { string: 1, fret: 0 })
4212 );
4213 assert_eq!(notes[1].string_number, Some(1));
4214 assert_eq!(
4215 notes[2].tab_position,
4216 Some(TabPosition { string: 6, fret: 7 })
4217 );
4218 }
4219
4220 #[test]
4221 fn assign_tablature_positions_uses_measure_local_capo_change() {
4222 let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
4223 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4224 lines: 6,
4225 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4226 capo: 0,
4227 });
4228 score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
4229 lines: 6,
4230 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4231 capo: 2,
4232 });
4233 score.parts[0].staves[0].measures[1].voices[0]
4234 .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
4235
4236 assert_eq!(assign_tablature_positions(&mut score), 1);
4237 assert_eq!(
4238 score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
4239 Some(TabPosition { string: 5, fret: 3 })
4240 );
4241 }
4242
4243 #[test]
4244 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
4245 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4246 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4247 lines: 6,
4248 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4249 capo: 0,
4250 });
4251 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4252 chord.pitches.push(Pitch::new(Step::G, 4));
4253 score.parts[0].staves[0].measures[0].voices[0].push(chord);
4254
4255 assert_eq!(assign_tablature_positions(&mut score), 1);
4256 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
4257 assert_eq!(
4258 positions,
4259 &vec![
4260 TabPosition { string: 2, fret: 5 },
4261 TabPosition { string: 1, fret: 3 },
4262 ]
4263 );
4264 }
4265
4266 #[test]
4267 fn new_score_measure_count() {
4268 let score = Score::new("Test", 120, 4, 4, 0, 8);
4269 assert_eq!(score.measure_count(), 8);
4270 }
4271
4272 #[test]
4273 fn note_beats_quarter() {
4274 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4275 assert!((note.beats() - 1.0).abs() < 1e-9);
4276 }
4277
4278 #[test]
4279 fn note_beats_dotted_quarter() {
4280 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4281 note.dot_count = 1;
4282 assert!((note.beats() - 1.5).abs() < 1e-9);
4283 }
4284
4285 #[test]
4286 fn grace_note_beats_zero() {
4287 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
4288 note.is_grace = true;
4289 assert_eq!(note.beats(), 0.0);
4290 }
4291
4292 #[test]
4293 fn measure_empty_4_4_fills_four_beats() {
4294 let m = Measure::empty(4, 4);
4295 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4296 assert!((total - 4.0).abs() < 1e-9);
4297 }
4298
4299 #[test]
4300 fn measure_empty_3_4_fills_three_beats() {
4301 let m = Measure::empty(3, 4);
4302 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4303 assert!((total - 3.0).abs() < 1e-9);
4304 }
4305
4306 #[test]
4307 fn whole_filling_beats() {
4308 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
4309 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
4310 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
4311 }
4312
4313 #[test]
4316 fn statistics_default_score_all_rests() {
4317 let score = Score::default();
4318 let s = score.statistics();
4319 assert_eq!(s.part_count, 1);
4320 assert_eq!(s.measure_count, 4);
4321 assert_eq!(s.note_count, 0);
4322 assert!(s.rest_count > 0);
4323 }
4324
4325 #[test]
4326 fn statistics_duration_estimate() {
4327 let score = Score::new("T", 120, 4, 4, 0, 1);
4329 let s = score.statistics();
4330 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
4331 }
4332
4333 #[test]
4334 fn score_duration_secs_matches_statistics() {
4335 use super::score_duration_secs;
4336 let score = Score::new("T", 120, 4, 4, 0, 4);
4337 let secs = score_duration_secs(&score);
4338 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
4340 }
4341
4342 #[test]
4343 fn score_duration_secs_integrates_measure_tempo_ramp() {
4344 use super::score_duration_secs;
4345 let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
4346 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4347 let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
4348 assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
4349 }
4350
4351 #[test]
4352 fn score_duration_secs_zero_bpm_returns_zero() {
4353 use super::score_duration_secs;
4354 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4355 score.settings.tempo_bpm = 0;
4356 assert_eq!(score_duration_secs(&score), 0.0);
4357 }
4358
4359 #[test]
4360 fn score_duration_secs_per_measure_tempo() {
4361 use super::score_duration_secs;
4362 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4364 score.parts[0].staves[0].measures[1].tempo = Some(60);
4365 let secs = score_duration_secs(&score);
4366 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
4367 }
4368
4369 #[test]
4372 fn extract_part_returns_single_part_score() {
4373 let mut score = Score::default();
4374 let mut p2 = Part::new("Violin", "Vln.");
4375 p2.staves.push(Staff::new(Clef::Treble));
4376 score.parts.push(p2);
4377 let ex = score.extract_part(0).unwrap();
4378 assert_eq!(ex.parts.len(), 1);
4379 assert_ne!(ex.id, score.id);
4380 assert_eq!(ex.metadata.title, score.metadata.title);
4381 }
4382
4383 #[test]
4384 fn extract_part_out_of_range_is_none() {
4385 let score = Score::default();
4386 assert!(score.extract_part(99).is_none());
4387 }
4388
4389 #[test]
4390 fn extract_and_merge_remap_typed_spanner_part_addresses() {
4391 let mut left = Score::template(ScoreTemplate::StringQuartet);
4392 let address = NoteAddr {
4393 part: 1,
4394 staff: 0,
4395 measure: 0,
4396 voice: 0,
4397 note: 0,
4398 };
4399 left.spanners.push(NotationSpanner {
4400 id: "left-span".to_string(),
4401 kind: NotationSpannerKind::Slur,
4402 start: address.clone(),
4403 end: address,
4404 number: Some(1),
4405 line_type: None,
4406 text: None,
4407 placement: None,
4408 ottava_size: None,
4409 ottava_type: None,
4410 });
4411 let extracted = left.extract_part_checked(1).expect("valid extracted part");
4412 assert_eq!(extracted.spanners[0].start.part, 0);
4413 assert_eq!(extracted.spanners[0].end.part, 0);
4414
4415 let mut right = Score::new("R", 120, 4, 4, 0, 1);
4416 let right_address = NoteAddr {
4417 part: 0,
4418 staff: 0,
4419 measure: 0,
4420 voice: 0,
4421 note: 0,
4422 };
4423 right.spanners.push(NotationSpanner {
4424 id: "right-span".to_string(),
4425 kind: NotationSpannerKind::Pedal,
4426 start: right_address.clone(),
4427 end: right_address,
4428 number: Some(1),
4429 line_type: None,
4430 text: None,
4431 placement: None,
4432 ottava_size: None,
4433 ottava_type: None,
4434 });
4435 let merged = left.merge_checked(&right).expect("valid merged score");
4436 let right_span = merged
4437 .spanners
4438 .iter()
4439 .find(|spanner| spanner.id == "right-span")
4440 .expect("merged right span");
4441 assert_eq!(right_span.start.part, left.parts.len());
4442 assert_eq!(right_span.end.part, left.parts.len());
4443 }
4444
4445 #[test]
4448 fn transpose_zero_is_clone() {
4449 let score = Score::new("T", 120, 4, 4, 0, 1);
4450 let t = transpose(&score, 0);
4451 assert_eq!(t.settings.key_signature.fifths, 0);
4452 }
4453
4454 #[test]
4455 fn transpose_staff_region_rewrites_only_selected_written_measures() {
4456 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4457 for measure in &mut score.parts[0].staves[0].measures {
4458 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4459 }
4460 let transformed = transpose_staff_region_checked(
4461 &score,
4462 0,
4463 0,
4464 1,
4465 2,
4466 2,
4467 RegionalTranspositionTarget::Written,
4468 )
4469 .expect("region should transpose");
4470 assert_eq!(
4471 transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
4472 Pitch::new(Step::C, 4)
4473 );
4474 assert_eq!(
4475 transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4476 Pitch::new(Step::D, 4)
4477 );
4478 assert_eq!(
4479 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4480 Pitch::new(Step::C, 4)
4481 );
4482 }
4483
4484 #[test]
4485 fn regional_concert_transposition_requires_full_staff() {
4486 let score = Score::new("T", 120, 4, 4, 0, 2);
4487 assert!(
4488 transpose_staff_region_checked(
4489 &score,
4490 0,
4491 0,
4492 1,
4493 2,
4494 -2,
4495 RegionalTranspositionTarget::Concert,
4496 )
4497 .is_err()
4498 );
4499 }
4500
4501 #[test]
4502 fn transpose_c_major_up_2_to_d_major() {
4503 let score = Score::new("T", 120, 4, 4, 0, 1);
4504 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
4505 }
4506
4507 #[test]
4508 fn transpose_d_major_up_5_to_g_major() {
4509 let score = Score::new("T", 120, 4, 4, 2, 1);
4510 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
4511 }
4512
4513 #[test]
4514 fn transpose_c4_up_1_to_csharp4() {
4515 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
4516 assert_eq!(p.to_midi(), 61);
4517 assert_eq!(p.step, Step::C);
4518 assert_eq!(p.alter, 1);
4519 }
4520
4521 #[test]
4522 fn transpose_c4_down_1_to_b3() {
4523 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
4524 assert_eq!(p.to_midi(), 59);
4525 assert_eq!(p.step, Step::B);
4526 assert_eq!(p.alter, 0);
4527 }
4528
4529 #[test]
4530 fn transpose_up_octave_keeps_step() {
4531 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
4532 assert_eq!(p.to_midi(), 81);
4533 assert_eq!(p.step, Step::A);
4534 assert_eq!(p.octave, 5);
4535 }
4536
4537 #[test]
4538 fn statistics_with_repeat_doubles_duration() {
4539 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4541 score.parts[0].staves[0].measures[0].barline_left =
4542 crate::model::notation::Barline::RepeatStart;
4543 score.parts[0].staves[0].measures[1].barline_right =
4544 crate::model::notation::Barline::RepeatEnd;
4545 let s = score.statistics();
4546 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
4548 }
4549
4550 #[test]
4551 fn transpose_octave_boundary_b4_to_c5() {
4552 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
4554 assert_eq!(p.to_midi(), 72);
4555 assert_eq!(p.step, Step::C);
4556 assert_eq!(p.octave, 5);
4557 }
4558
4559 #[test]
4560 fn transpose_clamp_at_midi_127() {
4561 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
4563 assert_eq!(p.to_midi(), 127);
4564 }
4565
4566 #[test]
4569 fn merge_combines_parts() {
4570 let mut a = Score::new("A", 120, 4, 4, 0, 2);
4571 let b = Score::new("B", 120, 4, 4, 0, 2);
4572 let mut p2 = Part::new("Violin", "Vln.");
4574 p2.staves.push(Staff::new(Clef::Treble));
4575 for i in 0..2usize {
4576 let mut m = Measure::empty(4, 4);
4577 m.number = i as u32 + 1;
4578 p2.staves[0].measures.push(m);
4579 }
4580 a.parts.push(p2);
4581 let merged = a.merge(&b);
4582 assert_eq!(merged.parts.len(), 3);
4584 }
4585
4586 #[test]
4587 fn merge_pads_shorter_score() {
4588 let a = Score::new("A", 120, 4, 4, 0, 4);
4589 let b = Score::new("B", 120, 4, 4, 0, 2);
4590 let merged = a.merge(&b);
4591 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
4593 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
4594 }
4595
4596 #[test]
4597 fn merge_uses_self_metadata() {
4598 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
4599 a.metadata.composer = "Composer A".to_string();
4600 let b = Score::new("Title B", 120, 4, 4, 0, 2);
4601 let merged = a.merge(&b);
4602 assert_eq!(merged.metadata.title, "Title A");
4603 assert_eq!(merged.metadata.composer, "Composer A");
4604 }
4605
4606 #[test]
4607 fn merge_new_id_differs_from_both() {
4608 let a = Score::new("A", 120, 4, 4, 0, 2);
4609 let b = Score::new("B", 120, 4, 4, 0, 2);
4610 let merged = a.merge(&b);
4611 assert_ne!(merged.id, a.id);
4612 assert_ne!(merged.id, b.id);
4613 }
4614
4615 #[test]
4618 fn staff_default_transpose_is_zero() {
4619 let s = Staff::new(Clef::Treble);
4620 assert_eq!(s.transpose_semitones, 0);
4621 }
4622
4623 #[test]
4624 fn staff_presentation_defaults_and_tracks_percussion_clef() {
4625 let standard = Staff::new(Clef::Treble);
4626 assert_eq!(standard.presentation, StaffPresentation::default());
4627
4628 let percussion = Staff::new(Clef::Percussion);
4629 assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
4630 assert_eq!(percussion.presentation.lines, 5);
4631 assert!(percussion.presentation.visible);
4632 }
4633
4634 #[test]
4637 fn score_default_has_schema_version_1() {
4638 let score = Score::default();
4639 assert_eq!(score.schema_version, 1);
4640 }
4641
4642 #[test]
4643 fn score_new_has_schema_version_1() {
4644 let score = Score::new("T", 120, 4, 4, 0, 4);
4645 assert_eq!(score.schema_version, 1);
4646 }
4647
4648 #[test]
4649 fn score_without_schema_version_deserializes_to_zero() {
4650 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":[]}"#;
4651 let score: Score = serde_json::from_str(json).unwrap();
4652 assert_eq!(score.schema_version, 0);
4653 }
4654
4655 #[test]
4656 fn legacy_score_json_defaults_typed_spanners() {
4657 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4658 let mut value = serde_json::to_value(score).expect("score serializes");
4659 value
4660 .as_object_mut()
4661 .expect("score is an object")
4662 .remove("spanners");
4663 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4664 assert!(restored.spanners.is_empty());
4665 }
4666
4667 #[test]
4668 fn legacy_staff_json_defaults_presentation() {
4669 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4670 let mut value = serde_json::to_value(score).expect("score serializes");
4671 value["parts"][0]["staves"][0]
4672 .as_object_mut()
4673 .expect("staff is an object")
4674 .remove("presentation");
4675
4676 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4677 assert_eq!(
4678 restored.parts[0].staves[0].presentation,
4679 StaffPresentation::default()
4680 );
4681 }
4682
4683 #[test]
4684 fn legacy_part_json_defaults_instrument_definition() {
4685 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4686 let mut value = serde_json::to_value(score).expect("score serializes");
4687 value["parts"][0]
4688 .as_object_mut()
4689 .expect("part is an object")
4690 .remove("instrument");
4691
4692 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4693 assert!(restored.parts[0].instrument.is_none());
4694 }
4695
4696 #[test]
4697 fn resolve_view_projects_linked_part_without_mutating_source_score() {
4698 let mut score = Score::template(ScoreTemplate::StringQuartet);
4699 let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
4700 view.layout.measures_per_row = Some(2);
4701 score.views.push(view);
4702
4703 let projected = score.resolve_view("violin-2").expect("view resolves");
4704 assert_eq!(projected.parts.len(), 1);
4705 assert_eq!(projected.parts[0].name, "Violin II");
4706 assert_eq!(projected.views.len(), 1);
4707 assert_eq!(projected.views[0].parts, vec![0]);
4708 assert_eq!(score.parts.len(), 4);
4709 assert_eq!(score.views[0].parts, vec![1]);
4710 }
4711
4712 #[test]
4713 fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
4714 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4715 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4716 lines: 6,
4717 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4718 capo: 0,
4719 });
4720 score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
4721 score.views.push(ScoreView::linked_tablature_staff(
4722 "guitar-tab",
4723 "Guitar Tab",
4724 0,
4725 0,
4726 ));
4727
4728 let projected = score.resolve_view("guitar-tab").expect("view resolves");
4729 assert_eq!(
4730 projected.parts[0].staves[0].presentation.kind,
4731 StaffKind::Tablature
4732 );
4733 assert_eq!(
4734 projected.views[0].staff_kind_overrides[0].staff,
4735 ViewStaffRef { part: 0, staff: 0 }
4736 );
4737 assert_eq!(
4738 score.parts[0].staves[0].presentation.kind,
4739 StaffKind::Standard
4740 );
4741 }
4742
4743 #[test]
4744 fn legacy_score_view_json_defaults_staff_kind_overrides() {
4745 let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
4746 score.views.push(ScoreView::linked_part("part", "Part", 0));
4747 let mut value = serde_json::to_value(score).expect("score serializes");
4748 value["views"][0]
4749 .as_object_mut()
4750 .expect("view is an object")
4751 .remove("staff_kind_overrides");
4752
4753 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4754 assert!(restored.views[0].staff_kind_overrides.is_empty());
4755 }
4756
4757 #[test]
4758 fn typed_view_style_overrides_are_ordered_and_json_compatible() {
4759 let mut view = ScoreView::linked_part("part", "Part", 0);
4760 view.layout.typed_style_overrides = vec![
4761 ViewStyleOverride {
4762 property: ViewStyleProperty::TextScale,
4763 value: 0.9,
4764 },
4765 ViewStyleOverride {
4766 property: ViewStyleProperty::TextScale,
4767 value: 1.1,
4768 },
4769 ];
4770 assert_eq!(
4771 view.layout.style_value(ViewStyleProperty::TextScale),
4772 Some(1.1)
4773 );
4774 let restored: ScoreView =
4775 serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
4776 assert_eq!(
4777 restored.layout.typed_style_overrides,
4778 view.layout.typed_style_overrides
4779 );
4780 assert_eq!(view.layout.resolved_style().text_scale, 1.1);
4781 assert_eq!(view.layout.resolved_style().system_gap, 2.0);
4782 }
4783
4784 #[test]
4785 fn score_style_defaults_are_inherited_then_overridden_by_view() {
4786 let mut score = Score::new("Style", 120, 4, 4, 0, 1);
4787 score.style_overrides = vec![
4788 ViewStyleOverride {
4789 property: ViewStyleProperty::StaffSpace,
4790 value: 1.2,
4791 },
4792 ViewStyleOverride {
4793 property: ViewStyleProperty::TextScale,
4794 value: 0.9,
4795 },
4796 ];
4797 let mut view = ScoreView::linked_part("part", "Part", 0);
4798 view.layout.typed_style_overrides.push(ViewStyleOverride {
4799 property: ViewStyleProperty::TextScale,
4800 value: 1.1,
4801 });
4802 let style = score.resolved_view_style(&view.layout);
4803 assert_eq!(style.staff_space, 1.2);
4804 assert_eq!(style.text_scale, 1.1);
4805 let restored: Score =
4806 serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
4807 assert_eq!(restored.style_overrides, score.style_overrides);
4808 }
4809
4810 #[test]
4811 fn legacy_measure_json_defaults_source_voice_numbers() {
4812 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4813 let mut value = serde_json::to_value(&score).expect("score serializes");
4814 value["parts"][0]["staves"][0]["measures"][0]
4815 .as_object_mut()
4816 .expect("measure is an object")
4817 .remove("source_voice_numbers");
4818
4819 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4820 assert_eq!(
4821 restored.parts[0].staves[0].measures[0].source_voice_numbers,
4822 [None; 4]
4823 );
4824 }
4825
4826 #[test]
4827 fn legacy_measure_json_defaults_tempo_ramp() {
4828 let measure: Measure = serde_json::from_str(
4829 r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
4830 )
4831 .expect("legacy measure deserializes");
4832 assert_eq!(measure.tempo_ramp_to, None);
4833 }
4834
4835 #[test]
4836 fn note_without_new_percussion_fields_uses_serde_defaults() {
4837 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4838 let mut value = serde_json::to_value(note).unwrap();
4839 let object = value.as_object_mut().unwrap();
4840 object.remove("is_unpitched");
4841 object.remove("instrument_id");
4842 let restored: Note = serde_json::from_value(value).unwrap();
4843 assert!(!restored.is_unpitched);
4844 assert_eq!(restored.instrument_id, None);
4845 }
4846
4847 #[test]
4848 fn percussion_instrument_resolution_prefers_id_then_display_key() {
4849 let mut part = Part::new("Drums", "Dr.");
4850 part.percussion_instruments = vec![
4851 PercussionInstrument {
4852 id: "snare".to_string(),
4853 name: Some("Acoustic Snare".to_string()),
4854 midi_unpitched: Some(38),
4855 staff_position: None,
4856 notehead: None,
4857 preferred_voice: None,
4858 techniques: Vec::new(),
4859 },
4860 PercussionInstrument {
4861 id: "rim".to_string(),
4862 name: Some("Side Stick".to_string()),
4863 midi_unpitched: Some(37),
4864 staff_position: None,
4865 notehead: None,
4866 preferred_voice: None,
4867 techniques: Vec::new(),
4868 },
4869 ];
4870 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
4871 note.is_unpitched = true;
4872 assert_eq!(
4873 part.percussion_instrument_for_note(¬e)
4874 .map(|instrument| instrument.id.as_str()),
4875 Some("snare")
4876 );
4877 note.instrument_id = Some("rim".to_string());
4878 assert_eq!(
4879 part.percussion_instrument_for_note(¬e)
4880 .map(|instrument| instrument.id.as_str()),
4881 Some("rim")
4882 );
4883 note.instrument_id = Some("missing".to_string());
4884 assert!(part.percussion_instrument_for_note(¬e).is_none());
4885 note.instrument_id = None;
4886 note.is_unpitched = false;
4887 assert!(part.percussion_instrument_for_note(¬e).is_none());
4888 }
4889
4890 #[test]
4891 fn percussion_kit_extensions_are_json_backward_compatible() {
4892 let mut instrument = PercussionInstrument {
4893 id: "snare".to_string(),
4894 name: Some("Acoustic Snare".to_string()),
4895 midi_unpitched: Some(38),
4896 staff_position: Some(0),
4897 notehead: Some(NoteHead::Cross),
4898 preferred_voice: Some(1),
4899 techniques: vec!["rim-shot".to_string()],
4900 };
4901 let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
4902 let object = value
4903 .as_object_mut()
4904 .expect("percussion instrument is an object");
4905 object.remove("staff_position");
4906 object.remove("notehead");
4907 object.remove("preferred_voice");
4908 object.remove("techniques");
4909 instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
4910 assert_eq!(instrument.staff_position, None);
4911 assert_eq!(instrument.notehead, None);
4912 assert_eq!(instrument.preferred_voice, None);
4913 assert!(instrument.techniques.is_empty());
4914 }
4915
4916 #[test]
4919 fn score_template_solo_has_one_part_treble() {
4920 let score = Score::template(ScoreTemplate::Solo);
4921 assert_eq!(score.parts.len(), 1);
4922 assert_eq!(score.parts[0].staves.len(), 1);
4923 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
4924 assert_eq!(score.parts[0].midi_program, 0);
4925 }
4926
4927 #[test]
4928 fn score_template_piano_has_two_staves() {
4929 let score = Score::template(ScoreTemplate::Piano);
4930 assert_eq!(score.parts.len(), 1);
4931 assert_eq!(score.parts[0].staves.len(), 2);
4932 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
4933 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
4934 }
4935
4936 #[test]
4937 fn score_template_string_quartet_has_four_parts() {
4938 let score = Score::template(ScoreTemplate::StringQuartet);
4939 assert_eq!(score.parts.len(), 4);
4940 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);
4943 assert_eq!(score.parts[3].midi_program, 42);
4944 }
4945
4946 #[test]
4947 fn score_template_string_orchestra_has_five_parts() {
4948 let score = Score::template(ScoreTemplate::StringOrchestra);
4949 assert_eq!(score.parts.len(), 5);
4950 assert_eq!(score.parts[4].midi_program, 43); }
4952
4953 #[test]
4954 fn score_template_brass_quintet_has_five_parts() {
4955 let score = Score::template(ScoreTemplate::BrassQuintet);
4956 assert_eq!(score.parts.len(), 5);
4957 assert_eq!(score.parts[2].midi_program, 60); }
4959
4960 #[test]
4961 fn score_template_default_measures_are_four() {
4962 let score = Score::template(ScoreTemplate::StringQuartet);
4963 for part in &score.parts {
4964 for staff in &part.staves {
4965 assert_eq!(staff.measures.len(), 4);
4966 }
4967 }
4968 }
4969
4970 #[test]
4973 fn measure_empty_has_no_breaks() {
4974 let m = Measure::empty(4, 4);
4975 assert!(!m.system_break);
4976 assert!(!m.page_break);
4977 assert!(!m.section_break);
4978 }
4979
4980 #[test]
4981 fn section_range_uses_semantic_boundaries_not_layout_breaks() {
4982 let mut score = Score::new("sections", 120, 4, 4, 0, 6);
4983 score.parts[0].staves[0].measures[0].section_break = true;
4984 score.parts[0].staves[0].measures[2].section_break = true;
4985 score.parts[0].staves[0].measures[4].section_break = true;
4986 score.parts[0].staves[0].measures[5].section_break = true;
4987 score.parts[0].staves[0].measures[1].system_break = true;
4988 score.parts[0].staves[0].measures[3].page_break = true;
4989 assert_eq!(score.section_range(0).unwrap(), 0..=1);
4990 assert_eq!(score.section_range(3).unwrap(), 2..=3);
4991 assert_eq!(score.section_range(4).unwrap(), 4..=4);
4992 assert_eq!(score.section_range(5).unwrap(), 5..=5);
4993 assert!(score.section_range(6).is_err());
4994 }
4995
4996 #[test]
4997 fn system_break_survives_json_roundtrip() {
4998 let mut m = Measure::empty(4, 4);
4999 m.system_break = true;
5000 let json = serde_json::to_string(&m).unwrap();
5001 let m2: Measure = serde_json::from_str(&json).unwrap();
5002 assert!(m2.system_break);
5003 assert!(!m2.page_break);
5004 }
5005
5006 #[test]
5009 fn diff_identical_scores_is_empty() {
5010 let s = Score::new("T", 120, 4, 4, 0, 2);
5011 assert!(diff(&s, &s).is_empty());
5012 }
5013
5014 #[test]
5015 fn score_patch_covers_measure_semantics_and_note_insert_index() {
5016 let mut a = Score::new("T", 120, 4, 4, 0, 1);
5017 a.parts[0].staves[0].measures[0].voices[0].clear();
5018 let mut b = a.clone();
5019 let measure = &mut b.parts[0].staves[0].measures[0];
5020 measure.key_sig = Some(KeySignature {
5021 fifths: -2,
5022 mode: "major".to_string(),
5023 });
5024 measure.time_sig = Some(TimeSignature {
5025 numerator: 3,
5026 denominator: 4,
5027 });
5028 measure.barline_left = Barline::RepeatStart;
5029 measure.barline_right = Barline::RepeatEnd;
5030 measure.rehearsal = Some("A".to_string());
5031 measure.volta = Some(VoltaBracket {
5032 number: 1,
5033 kind: "begin_end".to_string(),
5034 });
5035 measure.texts.push(StyledText {
5036 style: TextStyle::RehearsalMark,
5037 text: "A".to_string(),
5038 placement: None,
5039 offset_x: None,
5040 offset_y: None,
5041 relative_x: None,
5042 relative_y: None,
5043 });
5044 measure.figured_bass.push(FiguredBassFigure {
5045 number: "6".to_string(),
5046 alter: None,
5047 prefix: None,
5048 suffix: None,
5049 extender: false,
5050 });
5051 measure.harp_pedal_diagrams.push(HarpPedalDiagram {
5052 positions: [
5053 HarpPedalPosition::Flat,
5054 HarpPedalPosition::Natural,
5055 HarpPedalPosition::Sharp,
5056 HarpPedalPosition::Natural,
5057 HarpPedalPosition::Flat,
5058 HarpPedalPosition::Sharp,
5059 HarpPedalPosition::Natural,
5060 ],
5061 placement: Some("above".to_string()),
5062 });
5063 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
5064 let expected = b.parts[0].staves[0].measures[0].clone();
5065
5066 let patches = score_patch(&a, &b);
5067 assert!(
5068 patches
5069 .iter()
5070 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
5071 );
5072 assert!(
5073 patches
5074 .iter()
5075 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
5076 );
5077 assert!(
5078 patches
5079 .iter()
5080 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
5081 );
5082 assert!(
5083 patches
5084 .iter()
5085 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
5086 );
5087 assert!(
5088 patches
5089 .iter()
5090 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
5091 );
5092 assert!(
5093 patches
5094 .iter()
5095 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
5096 );
5097 assert!(
5098 patches
5099 .iter()
5100 .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
5101 );
5102 let result = apply_patch(&a, &patches).expect("patch application failed");
5103 let result_measure = &result.parts[0].staves[0].measures[0];
5104 assert_eq!(result_measure.key_sig, expected.key_sig);
5105 assert_eq!(result_measure.time_sig, expected.time_sig);
5106 assert_eq!(result_measure.barline_left, expected.barline_left);
5107 assert_eq!(result_measure.barline_right, expected.barline_right);
5108 assert_eq!(result_measure.rehearsal, expected.rehearsal);
5109 assert_eq!(result_measure.volta, expected.volta);
5110 assert_eq!(result_measure.texts, expected.texts);
5111 assert_eq!(result_measure.figured_bass, expected.figured_bass);
5112 assert_eq!(
5113 result_measure.harp_pedal_diagrams,
5114 expected.harp_pedal_diagrams
5115 );
5116 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
5117 }
5118
5119 #[test]
5120 fn diff_and_patch_preserve_score_level_texts() {
5121 let a = Score::new("T", 120, 4, 4, 0, 1);
5122 let mut b = a.clone();
5123 b.texts.push(StyledText {
5124 style: TextStyle::Expression,
5125 text: "Prelude".to_string(),
5126 placement: Some("above".to_string()),
5127 offset_x: Some(12.0),
5128 offset_y: Some(-8.0),
5129 relative_x: None,
5130 relative_y: None,
5131 });
5132
5133 let changes = diff(&a, &b);
5134 assert!(changes.iter().any(|change| matches!(
5135 change,
5136 ScoreChange::ScoreTextChanged { old, new }
5137 if old.is_empty() && new == &b.texts
5138 )));
5139
5140 let patches = score_patch(&a, &b);
5141 assert!(patches.iter().any(|patch| matches!(
5142 patch,
5143 ScorePatch::SetScoreTexts { value } if value == &b.texts
5144 )));
5145 let result = apply_patch(&a, &patches).expect("score text patch failed");
5146 assert_eq!(result.texts, b.texts);
5147 }
5148
5149 #[test]
5150 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
5151 let a = Score::new("T", 120, 4, 4, 0, 1);
5152 let mut b = a.clone();
5153 let measure = &mut b.parts[0].staves[0].measures[0];
5154 measure.number = 8;
5155 measure.clef = Some(Clef::Bass);
5156 measure.tempo_text = Some("Allegro".to_string());
5157 measure.navigation = Some("D.S.".to_string());
5158 measure.expression_text = Some("espressivo".to_string());
5159 measure.multi_rest_count = Some(3);
5160 measure.system_break = true;
5161 measure.page_break = true;
5162
5163 let changes = diff(&a, &b);
5164 assert!(changes.iter().any(|change| matches!(
5165 change,
5166 ScoreChange::MeasurePresentationChanged {
5167 part: 0,
5168 staff: 0,
5169 measure: 0,
5170 old_number: 1,
5171 new_number: 8,
5172 old_clef: None,
5173 new_clef: Some(Clef::Bass),
5174 new_tempo_text: Some(text),
5175 new_navigation: Some(navigation),
5176 new_expression_text: Some(expression),
5177 new_multi_rest_count: Some(3),
5178 old_system_break: false,
5179 new_system_break: true,
5180 old_page_break: false,
5181 new_page_break: true,
5182 ..
5183 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5184 )));
5185 assert!(!changes.iter().any(|change| matches!(
5186 change,
5187 ScoreChange::UnrepresentedFieldChanged { path }
5188 if path == "parts[0].staves[0].measures[0].number"
5189 )));
5190
5191 let patches = score_patch(&a, &b);
5192 assert!(patches.iter().any(|patch| matches!(
5193 patch,
5194 ScorePatch::SetMeasurePresentation {
5195 part: 0,
5196 staff: 0,
5197 measure: 0,
5198 number: 8,
5199 clef: Some(Clef::Bass),
5200 tempo_text: Some(text),
5201 navigation: Some(navigation),
5202 expression_text: Some(expression),
5203 multi_rest_count: Some(3),
5204 system_break: true,
5205 page_break: true,
5206 section_break: false,
5207 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5208 )));
5209 assert!(
5210 !patches
5211 .iter()
5212 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5213 );
5214 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
5215 let decoded: Vec<ScorePatch> =
5216 serde_json::from_str(&json).expect("measure presentation patch should decode");
5217 let mut legacy_json: serde_json::Value =
5218 serde_json::from_str(&json).expect("patch JSON value");
5219 for patch in legacy_json.as_array_mut().expect("patch JSON array") {
5220 patch
5221 .as_object_mut()
5222 .expect("patch JSON object")
5223 .remove("section_break");
5224 }
5225 let legacy: Vec<ScorePatch> =
5226 serde_json::from_value(legacy_json).expect("legacy patch should decode");
5227 assert!(matches!(
5228 legacy.first(),
5229 Some(ScorePatch::SetMeasurePresentation {
5230 section_break: false,
5231 ..
5232 })
5233 ));
5234 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
5235 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
5236 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
5237 assert_eq!(
5238 serde_json::to_value(&result).expect("patched score JSON"),
5239 serde_json::to_value(&b).expect("expected score JSON")
5240 );
5241 }
5242
5243 #[test]
5244 fn chord_definition_changes_use_typed_diff_and_local_patch() {
5245 let a = Score::new("T", 120, 4, 4, 0, 1);
5246 let mut b = a.clone();
5247 b.chord_definitions.push(ChordDefinition {
5248 id: Some("c-major".to_string()),
5249 label: Some("C".to_string()),
5250 kind: Some("major".to_string()),
5251 fret_position: Some(0),
5252 tab_strings: Some("x32010".to_string()),
5253 tab_courses: None,
5254 members: Vec::new(),
5255 barres: Vec::new(),
5256 });
5257
5258 let changes = diff(&a, &b);
5259 assert!(changes.iter().any(|change| matches!(
5260 change,
5261 ScoreChange::ChordDefinitionsChanged { old, new }
5262 if old.is_empty() && new == &b.chord_definitions
5263 )));
5264 assert!(!changes.iter().any(|change| matches!(
5265 change,
5266 ScoreChange::UnrepresentedFieldChanged { path }
5267 if path == "chord_definitions"
5268 )));
5269
5270 let patches = score_patch(&a, &b);
5271 assert!(patches.iter().any(|patch| matches!(
5272 patch,
5273 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
5274 )));
5275 assert!(
5276 !patches
5277 .iter()
5278 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5279 );
5280 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
5281 let decoded: Vec<ScorePatch> =
5282 serde_json::from_str(&json).expect("chord definition patch should decode");
5283 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
5284 assert_eq!(result.chord_definitions, b.chord_definitions);
5285 }
5286
5287 #[test]
5288 fn part_name_changes_use_typed_diff_and_local_patch() {
5289 let a = Score::new("T", 120, 4, 4, 0, 1);
5290 let mut b = a.clone();
5291 b.parts[0].name = "Violin".to_string();
5292 b.parts[0].short_name = "Vln.".to_string();
5293 b.parts[0].midi_channel = 4;
5294 b.parts[0].midi_program = 40;
5295 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
5296 tick: 120,
5297 channel: 4,
5298 value: 2048,
5299 });
5300 b.parts[0].midi_control_changes.push(MidiControlChange {
5301 tick: 240,
5302 channel: 4,
5303 controller: 64,
5304 value: 127,
5305 });
5306 b.parts[0].midi_program_changes.push(MidiProgramChange {
5307 tick: 0,
5308 channel: 4,
5309 program: 40,
5310 });
5311 b.parts[0].midi_aftertouch.push(MidiAftertouch {
5312 tick: 360,
5313 channel: 4,
5314 key: Some(64),
5315 value: 80,
5316 });
5317 b.parts[0].staves[0].clef = Clef::Bass;
5318 b.parts[0].staves[0].transpose_semitones = -2;
5319
5320 let changes = diff(&a, &b);
5321 assert!(changes.iter().any(|change| matches!(
5322 change,
5323 ScoreChange::PartNamesChanged {
5324 part: 0,
5325 old_name,
5326 new_name,
5327 old_short_name,
5328 new_short_name,
5329 } if old_name == "Piano"
5330 && new_name == "Violin"
5331 && old_short_name == "Pno."
5332 && new_short_name == "Vln."
5333 )));
5334 assert!(changes.iter().any(|change| matches!(
5335 change,
5336 ScoreChange::PartMidiChanged {
5337 part: 0,
5338 old_channel: 0,
5339 new_channel: 4,
5340 old_program: 0,
5341 new_program: 40,
5342 }
5343 )));
5344 assert!(changes.iter().any(|change| matches!(
5345 change,
5346 ScoreChange::PartMidiAutomationChanged {
5347 part: 0,
5348 new_pitch_bends,
5349 new_control_changes,
5350 new_program_changes,
5351 new_aftertouch,
5352 ..
5353 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
5354 && new_control_changes == &b.parts[0].midi_control_changes
5355 && new_program_changes == &b.parts[0].midi_program_changes
5356 && new_aftertouch == &b.parts[0].midi_aftertouch
5357 )));
5358 assert!(changes.iter().any(|change| matches!(
5359 change,
5360 ScoreChange::StaffConfigurationChanged {
5361 part: 0,
5362 staff: 0,
5363 old_clef: Clef::Treble,
5364 new_clef: Clef::Bass,
5365 old_transpose_semitones: 0,
5366 new_transpose_semitones: -2,
5367 }
5368 )));
5369 assert!(!changes.iter().any(|change| matches!(
5370 change,
5371 ScoreChange::UnrepresentedFieldChanged { path }
5372 if path == "parts[0].name"
5373 )));
5374
5375 let patches = score_patch(&a, &b);
5376 assert!(patches.iter().any(|patch| matches!(
5377 patch,
5378 ScorePatch::SetPartNames {
5379 part: 0,
5380 name,
5381 short_name,
5382 } if name == "Violin" && short_name == "Vln."
5383 )));
5384 assert!(
5385 !patches
5386 .iter()
5387 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5388 );
5389 assert!(patches.iter().any(|patch| matches!(
5390 patch,
5391 ScorePatch::SetPartMidi {
5392 part: 0,
5393 channel: 4,
5394 program: 40,
5395 }
5396 )));
5397 assert!(patches.iter().any(|patch| matches!(
5398 patch,
5399 ScorePatch::SetPartMidiAutomation {
5400 part: 0,
5401 pitch_bends,
5402 control_changes,
5403 program_changes,
5404 aftertouch,
5405 } if pitch_bends == &b.parts[0].midi_pitch_bends
5406 && control_changes == &b.parts[0].midi_control_changes
5407 && program_changes == &b.parts[0].midi_program_changes
5408 && aftertouch == &b.parts[0].midi_aftertouch
5409 )));
5410 assert!(patches.iter().any(|patch| matches!(
5411 patch,
5412 ScorePatch::SetStaffConfiguration {
5413 part: 0,
5414 staff: 0,
5415 clef: Clef::Bass,
5416 transpose_semitones: -2,
5417 }
5418 )));
5419 let result = apply_patch(&a, &patches).expect("part name patch failed");
5420 assert_eq!(result.parts[0].name, b.parts[0].name);
5421 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
5422 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
5423 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
5424 assert_eq!(
5425 result.parts[0].midi_pitch_bends,
5426 b.parts[0].midi_pitch_bends
5427 );
5428 assert_eq!(
5429 result.parts[0].midi_control_changes,
5430 b.parts[0].midi_control_changes
5431 );
5432 assert_eq!(
5433 result.parts[0].midi_program_changes,
5434 b.parts[0].midi_program_changes
5435 );
5436 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
5437 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
5438 assert_eq!(
5439 result.parts[0].staves[0].transpose_semitones,
5440 b.parts[0].staves[0].transpose_semitones
5441 );
5442 }
5443
5444 #[test]
5445 fn diff_reports_measure_text_and_figured_bass_changes() {
5446 let a = Score::new("T", 120, 4, 4, 0, 1);
5447 let mut b = a.clone();
5448 let measure = &mut b.parts[0].staves[0].measures[0];
5449 measure.texts.push(StyledText {
5450 style: TextStyle::Lyrics,
5451 text: "la".to_string(),
5452 placement: None,
5453 offset_x: None,
5454 offset_y: None,
5455 relative_x: None,
5456 relative_y: None,
5457 });
5458 measure.figured_bass.push(FiguredBassFigure {
5459 number: "6".to_string(),
5460 alter: None,
5461 prefix: None,
5462 suffix: None,
5463 extender: false,
5464 });
5465
5466 let changes = diff(&a, &b);
5467 assert!(changes.iter().any(|change| matches!(
5468 change,
5469 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
5470 if old.is_empty() && new.len() == 1
5471 )));
5472 assert!(changes.iter().any(|change| matches!(
5473 change,
5474 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
5475 if old.is_empty() && new.len() == 1
5476 )));
5477 }
5478
5479 #[test]
5480 fn diff_reports_tablature_changes_and_patches_them_locally() {
5481 let a = Score::new("T", 120, 4, 4, 0, 1);
5482 let mut b = a.clone();
5483 b.parts[0].staves[0].tablature = Some(TablatureConfig {
5484 lines: 6,
5485 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5486 capo: 2,
5487 });
5488
5489 let changes = diff(&a, &b);
5490 assert!(!changes.iter().any(|change| matches!(
5491 change,
5492 ScoreChange::UnrepresentedFieldChanged { path }
5493 if path == "parts[0].staves[0].tablature"
5494 )));
5495 assert!(changes.iter().any(|change| matches!(
5496 change,
5497 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
5498 if config.lines == 6 && config.capo == 2
5499 )));
5500 let patches = score_patch(&a, &b);
5501 assert!(patches.iter().any(|patch| matches!(
5502 patch,
5503 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
5504 if config.lines == 6 && config.capo == 2
5505 )));
5506 assert_eq!(
5507 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
5508 b.parts[0].staves[0].tablature
5509 );
5510 }
5511
5512 #[test]
5513 fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
5514 let mut a = Score::new("T", 120, 4, 4, 0, 2);
5515 a.parts[0].staves[0].tablature = Some(TablatureConfig {
5516 lines: 6,
5517 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5518 capo: 0,
5519 });
5520 let mut b = a.clone();
5521 b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
5522 lines: 6,
5523 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5524 capo: 3,
5525 });
5526
5527 let changes = diff(&a, &b);
5528 assert!(changes.iter().any(|change| matches!(
5529 change,
5530 ScoreChange::TablatureChangeChanged {
5531 part: 0,
5532 staff: 0,
5533 measure: 1,
5534 old: None,
5535 new: Some(config),
5536 } if config.capo == 3
5537 )));
5538 let patches = score_patch(&a, &b);
5539 assert!(patches.iter().any(|patch| matches!(
5540 patch,
5541 ScorePatch::SetMeasureTablatureChange {
5542 part: 0,
5543 staff: 0,
5544 measure: 1,
5545 value: Some(config),
5546 } if config.capo == 3
5547 )));
5548 assert_eq!(
5549 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
5550 b.parts[0].staves[0].measures[1].tablature_change
5551 );
5552 }
5553
5554 #[test]
5555 fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
5556 let a = Score::new("Ramp", 120, 4, 4, 0, 2);
5557 let mut b = a.clone();
5558 b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
5559
5560 assert!(diff(&a, &b).iter().any(|change| matches!(
5561 change,
5562 ScoreChange::MeasureTempoRampChanged {
5563 part: 0,
5564 staff: 0,
5565 measure: 1,
5566 old: None,
5567 new: Some(72),
5568 }
5569 )));
5570 let patches = score_patch(&a, &b);
5571 assert!(patches.iter().any(|patch| matches!(
5572 patch,
5573 ScorePatch::SetMeasureTempoRamp {
5574 part: 0,
5575 staff: 0,
5576 measure: 1,
5577 value: Some(72),
5578 }
5579 )));
5580 assert_eq!(
5581 apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
5582 .tempo_ramp_to,
5583 b.parts[0].staves[0].measures[1].tempo_ramp_to
5584 );
5585 }
5586
5587 #[test]
5588 fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
5589 let a = Score::new("T", 120, 4, 4, 0, 1);
5590 let mut b = a.clone();
5591 b.parts[0].staves[0].presentation = StaffPresentation {
5592 kind: StaffKind::Percussion,
5593 lines: 1,
5594 line_distance: 1.5,
5595 small: true,
5596 cutaway: true,
5597 visible: false,
5598 notehead_scheme: StaffNoteheadScheme::PitchNames,
5599 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
5600 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
5601 };
5602
5603 let changes = diff(&a, &b);
5604 assert!(changes.iter().any(|change| matches!(
5605 change,
5606 ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
5607 if new == &b.parts[0].staves[0].presentation
5608 )));
5609 let patches = score_patch(&a, &b);
5610 assert!(patches.iter().any(|patch| matches!(
5611 patch,
5612 ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
5613 if value == &b.parts[0].staves[0].presentation
5614 )));
5615 assert!(
5616 !patches
5617 .iter()
5618 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5619 );
5620 assert_eq!(
5621 apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
5622 b.parts[0].staves[0].presentation
5623 );
5624 }
5625
5626 #[test]
5627 fn score_patch_uses_local_presentation_patch_for_display_fields() {
5628 let a = Score::new("T", 120, 4, 4, 0, 1);
5629 let mut b = a.clone();
5630 b.parts[0].name = "Piano".to_string();
5631 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
5632 let patches = score_patch(&a, &b);
5633 assert!(patches.iter().any(|patch| matches!(
5634 patch,
5635 ScorePatch::SetMeasurePresentation {
5636 part: 0,
5637 staff: 0,
5638 measure: 0,
5639 expression_text: Some(text),
5640 ..
5641 } if text == "dolce"
5642 )));
5643 assert!(
5644 !patches
5645 .iter()
5646 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5647 );
5648 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
5649 assert_eq!(result.parts[0].name, "Piano");
5650 assert_eq!(
5651 result.parts[0].staves[0].measures[0].expression_text,
5652 Some("dolce".to_string())
5653 );
5654 }
5655
5656 #[test]
5657 fn diff_detects_tempo_change() {
5658 let a = Score::new("T", 120, 4, 4, 0, 1);
5659 let mut b = a.clone();
5660 b.settings.tempo_bpm = 90;
5661 let changes = diff(&a, &b);
5662 assert_eq!(changes.len(), 1);
5663 assert!(matches!(
5664 changes[0],
5665 ScoreChange::TempoChanged { old: 120, new: 90 }
5666 ));
5667 }
5668
5669 #[test]
5670 fn diff_detects_title_change() {
5671 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
5672 let mut b = a.clone();
5673 b.metadata.title = "New Title".to_string();
5674 let changes = diff(&a, &b);
5675 assert!(
5676 changes.iter().any(
5677 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
5678 )
5679 );
5680 }
5681
5682 #[test]
5683 fn diff_detects_note_modification() {
5684 let mut a = Score::new("T", 120, 4, 4, 0, 1);
5685 a.parts[0].staves[0].measures[0].voices[0] =
5686 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5687 let mut b = a.clone();
5688 b.parts[0].staves[0].measures[0].voices[0][0] =
5689 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
5690 let changes = diff(&a, &b);
5691 assert!(
5692 changes
5693 .iter()
5694 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
5695 );
5696 }
5697
5698 #[test]
5699 fn diff_detects_part_added() {
5700 let a = Score::new("T", 120, 4, 4, 0, 1);
5701 let mut b = a.clone();
5702 let mut p = Part::new("Violin", "Vln.");
5703 p.staves.push(Staff::new(Clef::Treble));
5704 b.parts.push(p);
5705 let changes = diff(&a, &b);
5706 assert!(
5707 changes
5708 .iter()
5709 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
5710 );
5711 }
5712
5713 #[test]
5714 fn diff_detects_measure_tempo_change() {
5715 let a = Score::new("T", 120, 4, 4, 0, 2);
5716 let mut b = a.clone();
5717 b.parts[0].staves[0].measures[1].tempo = Some(60);
5718 let changes = diff(&a, &b);
5719 assert!(changes.iter().any(|c| matches!(
5720 c,
5721 ScoreChange::MeasureTempoChanged {
5722 measure: 1,
5723 old: None,
5724 new: Some(60),
5725 ..
5726 }
5727 )));
5728 }
5729
5730 #[test]
5731 fn diff_detects_barline_change() {
5732 use crate::model::notation::Barline;
5733 let a = Score::new("T", 120, 4, 4, 0, 2);
5734 let mut b = a.clone();
5735 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
5736 let changes = diff(&a, &b);
5737 assert!(
5738 changes
5739 .iter()
5740 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
5741 );
5742 }
5743
5744 #[test]
5745 fn diff_detects_rehearsal_change() {
5746 let a = Score::new("T", 120, 4, 4, 0, 2);
5747 let mut b = a.clone();
5748 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
5749 let changes = diff(&a, &b);
5750 assert!(
5751 changes
5752 .iter()
5753 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
5754 );
5755 }
5756
5757 #[test]
5758 fn diff_detects_volta_change() {
5759 use super::VoltaBracket;
5760 let a = Score::new("T", 120, 4, 4, 0, 2);
5761 let mut b = a.clone();
5762 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
5763 number: 1,
5764 kind: "begin_end".into(),
5765 });
5766 let changes = diff(&a, &b);
5767 assert!(
5768 changes
5769 .iter()
5770 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
5771 );
5772 }
5773
5774 #[test]
5775 fn diff_detects_key_signature_change() {
5776 let a = Score::new("T", 120, 4, 4, 0, 1);
5777 let mut b = a.clone();
5778 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
5780 assert!(
5781 changes
5782 .iter()
5783 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
5784 );
5785 }
5786
5787 #[test]
5788 fn measure_key_signature_uses_typed_local_patch() {
5789 let a = Score::new("T", 120, 4, 4, 0, 1);
5790 let mut b = a.clone();
5791 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
5792 fifths: 2,
5793 mode: "major".to_string(),
5794 });
5795 let changes = diff(&a, &b);
5796 assert!(!changes.iter().any(|change| matches!(
5797 change,
5798 ScoreChange::UnrepresentedFieldChanged { path }
5799 if path == "parts[0].staves[0].measures[0].key_sig"
5800 )));
5801 let patches = score_patch(&a, &b);
5802 assert!(patches.iter().any(|patch| matches!(
5803 patch,
5804 ScorePatch::SetKeySignature {
5805 part: 0,
5806 staff: 0,
5807 measure: 0,
5808 value: Some(KeySignature { fifths: 2, .. }),
5809 }
5810 )));
5811 assert!(
5812 !patches
5813 .iter()
5814 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5815 );
5816 }
5817
5818 #[test]
5819 fn diff_same_key_signature_no_change() {
5820 let a = Score::new("T", 120, 4, 4, 2, 1);
5821 let changes = diff(&a, &a);
5822 assert!(changes.is_empty());
5823 }
5824
5825 #[test]
5826 fn score_duration_secs_region_partial() {
5827 use super::score_duration_secs_region;
5828 let score = Score::new("T", 120, 4, 4, 0, 4);
5830 let secs = score_duration_secs_region(&score, (1, 2));
5831 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
5832 }
5833
5834 #[test]
5835 fn score_duration_secs_region_single_measure() {
5836 use super::score_duration_secs_region;
5837 let score = Score::new("T", 120, 4, 4, 0, 4);
5839 let secs = score_duration_secs_region(&score, (0, 0));
5840 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
5841 }
5842
5843 #[test]
5846 fn measure_beats_remaining_empty_voice_returns_full() {
5847 use super::measure_beats_remaining;
5848 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5849 score.parts[0].staves[0].measures[0].voices[0].clear();
5850 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
5851 assert!(
5852 (rem - 4.0).abs() < 1e-9,
5853 "expected 4.0 remaining, got {rem}"
5854 );
5855 }
5856
5857 #[test]
5858 fn measure_beats_remaining_half_full_returns_half() {
5859 use super::measure_beats_remaining;
5860 use crate::model::pitch::Step;
5861 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5862 score.parts[0].staves[0].measures[0].voices[0] = vec![
5863 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
5864 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5865 ];
5866 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
5867 assert!(
5868 (rem - 2.0).abs() < 1e-9,
5869 "expected 2.0 remaining, got {rem}"
5870 );
5871 }
5872
5873 #[test]
5874 fn measure_beats_remaining_full_voice_returns_zero() {
5875 use super::measure_beats_remaining;
5876 use crate::model::pitch::Step;
5877 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5878 score.parts[0].staves[0].measures[0].voices[0] =
5879 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5880 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
5881 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
5882 }
5883
5884 #[test]
5885 fn measure_beats_remaining_tuplet_accounting() {
5886 use super::measure_beats_remaining;
5887 use crate::model::notation::TupletInfo;
5888 use crate::model::pitch::Step;
5889 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5891 let tuplet = TupletInfo {
5892 actual_notes: 3,
5893 normal_notes: 2,
5894 };
5895 let mk = |step| {
5896 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
5897 n.tuplet = Some(tuplet.clone());
5898 n
5899 };
5900 score.parts[0].staves[0].measures[0].voices[0] =
5901 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
5902 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
5903 assert!(
5904 (rem - 2.0).abs() < 1e-9,
5905 "expected 2.0 remaining (triplets used 2.0), got {rem}"
5906 );
5907 }
5908
5909 #[test]
5910 fn measure_beats_remaining_out_of_range_returns_err() {
5911 use super::measure_beats_remaining;
5912 let score = Score::new("T", 120, 4, 4, 0, 1);
5913 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
5914 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
5915 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
5916 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
5917 }
5918
5919 #[test]
5920 fn note_content_eq_ignores_id() {
5921 use crate::model::pitch::Step;
5922 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5923 let mut b = a.clone();
5924 b.id = "different-id".to_string();
5925 assert!(note_content_eq(&a, &b));
5926 b.pitches[0] = Pitch::new(Step::D, 4);
5928 assert!(!note_content_eq(&a, &b));
5929 let mut c = a.clone();
5931 a.stem_up = Some(true);
5932 c.stem_up = Some(false);
5933 assert!(!note_content_eq(&a, &c));
5934 }
5935
5936 #[test]
5937 fn suggested_stem_up_below_middle() {
5938 use crate::model::notation::Clef;
5939 let pitches = vec![Pitch::new(Step::C, 4)];
5941 assert!(suggested_stem_up(&pitches, &Clef::Treble));
5942 }
5943
5944 #[test]
5945 fn suggested_stem_up_above_middle() {
5946 use crate::model::notation::Clef;
5947 let pitches = vec![Pitch::new(Step::G, 5)];
5949 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
5950 }
5951
5952 #[test]
5953 fn suggested_stem_up_at_middle_line() {
5954 use crate::model::notation::Clef;
5955 let pitches = vec![Pitch::new(Step::B, 4)];
5957 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
5958 }
5959
5960 #[test]
5961 fn suggested_stem_up_chord() {
5962 use crate::model::notation::Clef;
5963 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
5965 assert!(suggested_stem_up(&pitches, &Clef::Treble));
5966 }
5967
5968 #[test]
5969 fn suggested_stem_up_bass_clef() {
5970 use crate::model::notation::Clef;
5971 let pitches = vec![Pitch::new(Step::D, 3)];
5973 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
5974 let pitches2 = vec![Pitch::new(Step::C, 3)];
5976 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
5977 }
5978
5979 #[test]
5980 fn suggested_stem_up_empty_pitches() {
5981 use crate::model::notation::Clef;
5982 assert!(suggested_stem_up(&[], &Clef::Treble));
5983 }
5984
5985 fn eighth(pitch: Pitch) -> Note {
5986 Note::new(pitch, Duration::Eighth)
5987 }
5988 fn quarter(pitch: Pitch) -> Note {
5989 Note::new(pitch, Duration::Quarter)
5990 }
5991 fn rest_eighth() -> Note {
5992 Note::rest(Duration::Eighth)
5993 }
5994
5995 #[test]
5996 fn compute_beams_4_4_four_eighths() {
5997 use crate::model::notation::{Clef, TimeSignature};
5998 let _ = Clef::Treble; let ts = TimeSignature {
6000 numerator: 4,
6001 denominator: 4,
6002 };
6003 let c4 = Pitch::new(Step::C, 4);
6004 let notes = vec![
6005 eighth(c4.clone()),
6006 eighth(c4.clone()),
6007 eighth(c4.clone()),
6008 eighth(c4.clone()),
6009 ];
6010 let beams = compute_beams(¬es, &ts);
6011 assert_eq!(beams[0], BeamState::Begin);
6013 assert_eq!(beams[1], BeamState::End);
6014 assert_eq!(beams[2], BeamState::Begin);
6015 assert_eq!(beams[3], BeamState::End);
6016 }
6017
6018 #[test]
6019 fn compute_beams_4_4_all_eighth_one_group() {
6020 use crate::model::notation::TimeSignature;
6021 let ts = TimeSignature {
6022 numerator: 4,
6023 denominator: 4,
6024 };
6025 let c4 = Pitch::new(Step::C, 4);
6026 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
6028 let beams = compute_beams(¬es, &ts);
6029 assert_eq!(beams[0], BeamState::Begin);
6030 assert_eq!(beams[1], BeamState::End);
6031 }
6032
6033 #[test]
6034 fn compute_beams_quarter_not_beamed() {
6035 use crate::model::notation::TimeSignature;
6036 let ts = TimeSignature {
6037 numerator: 4,
6038 denominator: 4,
6039 };
6040 let c4 = Pitch::new(Step::C, 4);
6041 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
6042 let beams = compute_beams(¬es, &ts);
6043 assert_eq!(beams[0], BeamState::None);
6044 assert_eq!(beams[1], BeamState::None);
6045 }
6046
6047 #[test]
6048 fn compute_beams_rest_breaks_beam() {
6049 use crate::model::notation::TimeSignature;
6050 let ts = TimeSignature {
6051 numerator: 4,
6052 denominator: 4,
6053 };
6054 let c4 = Pitch::new(Step::C, 4);
6055 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
6056 let beams = compute_beams(¬es, &ts);
6057 assert_eq!(beams[0], BeamState::None);
6059 assert_eq!(beams[1], BeamState::None);
6060 assert_eq!(beams[2], BeamState::None);
6061 }
6062
6063 #[test]
6064 fn compute_beams_6_8_compound() {
6065 use crate::model::notation::TimeSignature;
6066 let ts = TimeSignature {
6067 numerator: 6,
6068 denominator: 8,
6069 };
6070 let c4 = Pitch::new(Step::C, 4);
6071 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
6073 let beams = compute_beams(¬es, &ts);
6074 assert_eq!(beams[0], BeamState::Begin);
6075 assert_eq!(beams[1], BeamState::Continue);
6076 assert_eq!(beams[2], BeamState::End);
6077 assert_eq!(beams[3], BeamState::Begin);
6078 assert_eq!(beams[4], BeamState::Continue);
6079 assert_eq!(beams[5], BeamState::End);
6080 }
6081
6082 #[test]
6083 fn compute_beams_single_eighth() {
6084 use crate::model::notation::TimeSignature;
6085 let ts = TimeSignature {
6086 numerator: 4,
6087 denominator: 4,
6088 };
6089 let c4 = Pitch::new(Step::C, 4);
6090 let notes = vec![eighth(c4.clone())];
6091 let beams = compute_beams(¬es, &ts);
6092 assert_eq!(beams[0], BeamState::None);
6093 }
6094}