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