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 meter_at<'a>(
1887 &'a self,
1888 measure_index: usize,
1889 default: &'a TimeSignature,
1890 ) -> &'a TimeSignature {
1891 self.measures
1892 .iter()
1893 .take(measure_index.saturating_add(1))
1894 .rev()
1895 .find_map(|measure| measure.time_sig.as_ref())
1896 .unwrap_or(default)
1897 }
1898
1899 pub fn measure_beats(&self, measure_index: usize, default: &TimeSignature) -> f64 {
1901 self.measures.get(measure_index).map_or_else(
1902 || self.meter_at(measure_index, default).total_beats(),
1903 |measure| measure.duration_beats(self.meter_at(measure_index, default)),
1904 )
1905 }
1906
1907 pub fn new(clef: Clef) -> Self {
1908 Self {
1909 presentation: StaffPresentation::for_clef(&clef),
1910 clef,
1911 measures: Vec::new(),
1912 transpose_semitones: 0,
1913 tablature: None,
1914 }
1915 }
1916
1917 pub fn tablature_at(&self, measure_index: usize) -> Option<TablatureConfig> {
1923 let mut active = self.tablature.clone();
1924 for measure in self.measures.iter().take(measure_index.saturating_add(1)) {
1925 if let Some(change) = &measure.tablature_change {
1926 active = Some(change.clone());
1927 }
1928 }
1929 active
1930 }
1931}
1932
1933#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1934pub struct VoltaBracket {
1935 pub number: u8,
1937 pub kind: String,
1939}
1940
1941#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1943#[serde(rename_all = "kebab-case")]
1944pub enum HarpPedalPosition {
1945 Flat,
1946 #[default]
1947 Natural,
1948 Sharp,
1949}
1950
1951#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1956pub struct HarpPedalDiagram {
1957 #[serde(default)]
1958 pub positions: [HarpPedalPosition; 7],
1959 #[serde(default, skip_serializing_if = "Option::is_none")]
1960 pub placement: Option<String>,
1961}
1962
1963impl Default for HarpPedalDiagram {
1964 fn default() -> Self {
1965 Self {
1966 positions: [HarpPedalPosition::Natural; 7],
1967 placement: None,
1968 }
1969 }
1970}
1971
1972#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1976pub struct MeasureLength {
1977 pub numerator: u32,
1978 pub denominator: u32,
1979}
1980
1981impl MeasureLength {
1982 pub const MAX_BEATS: f64 = 256.0;
1984
1985 pub fn beats(&self) -> Option<f64> {
1987 if self.numerator == 0 || self.denominator == 0 {
1988 return None;
1989 }
1990 let beats = 4.0 * f64::from(self.numerator) / f64::from(self.denominator);
1991 (beats.is_finite() && beats <= Self::MAX_BEATS).then_some(beats)
1992 }
1993
1994 pub fn from_beats(beats: f64) -> Option<Self> {
1997 const DIVISIONS: u32 = 3 * 5 * 7 * 64;
1998 if !beats.is_finite() || beats <= 0.0 || beats > Self::MAX_BEATS {
1999 return None;
2000 }
2001 let ticks = (beats * f64::from(DIVISIONS)).round();
2002 (ticks >= 1.0).then(|| Self::from_ticks(ticks as u32, DIVISIONS))?
2003 }
2004
2005 pub fn from_ticks(ticks: u32, divisions: u32) -> Option<Self> {
2007 let numerator = u64::from(ticks);
2008 let denominator = u64::from(divisions).checked_mul(4)?;
2009 if numerator == 0 || denominator == 0 {
2010 return None;
2011 }
2012 let gcd = {
2013 let (mut a, mut b) = (numerator, denominator);
2014 while b != 0 {
2015 (a, b) = (b, a % b);
2016 }
2017 a
2018 };
2019 let length = Self {
2020 numerator: u32::try_from(numerator / gcd).ok()?,
2021 denominator: u32::try_from(denominator / gcd).ok()?,
2022 };
2023 length.beats().map(|_| length)
2024 }
2025}
2026
2027#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2031pub struct MidMeasureClef {
2032 pub offset: MeasureLength,
2033 pub clef: Clef,
2034}
2035
2036#[derive(Debug, Clone, Serialize, Deserialize)]
2037pub struct Measure {
2038 pub number: u32,
2039 pub time_sig: Option<TimeSignature>,
2040 pub key_sig: Option<KeySignature>,
2041 pub clef: Option<Clef>,
2042 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2045 pub mid_clefs: Vec<MidMeasureClef>,
2046 pub tempo: Option<u16>,
2047 #[serde(default)]
2050 pub tempo_ramp_to: Option<u16>,
2051 #[serde(default)]
2053 pub instrument_change: Option<InstrumentDefinition>,
2054 #[serde(default)]
2056 pub tablature_change: Option<TablatureConfig>,
2057 pub barline_left: Barline,
2058 pub barline_right: Barline,
2059 #[serde(default)]
2060 pub volta: Option<VoltaBracket>,
2061 #[serde(default)]
2062 pub tempo_text: Option<String>,
2063 #[serde(default)]
2064 pub rehearsal: Option<String>,
2065 #[serde(default)]
2068 pub navigation: Option<String>,
2069 #[serde(default)]
2071 pub expression_text: Option<String>,
2072 #[serde(default)]
2073 pub texts: Vec<StyledText>,
2074 #[serde(default)]
2076 pub figured_bass: Vec<FiguredBassFigure>,
2077 #[serde(default)]
2079 pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
2080 #[serde(default)]
2082 pub multi_rest_count: Option<u8>,
2083 #[serde(default)]
2085 pub system_break: bool,
2086 #[serde(default)]
2088 pub page_break: bool,
2089 #[serde(default)]
2092 pub section_break: bool,
2093 pub voices: [Vec<Note>; 4],
2095 #[serde(default)]
2101 pub source_voice_numbers: [Option<u32>; 4],
2102 #[serde(default, skip_serializing_if = "Option::is_none")]
2105 pub actual_length: Option<MeasureLength>,
2106 #[serde(default, skip_serializing_if = "Option::is_none")]
2110 pub measure_repeat: Option<u8>,
2111}
2112
2113impl Measure {
2114 pub fn clef_at(&self, start_clef: &Clef, beats: f64) -> Clef {
2117 self.mid_clefs
2118 .iter()
2119 .take_while(|change| {
2120 change
2121 .offset
2122 .beats()
2123 .is_some_and(|offset| offset <= beats + 1e-9)
2124 })
2125 .last()
2126 .map_or_else(|| start_clef.clone(), |change| change.clef.clone())
2127 }
2128
2129 pub fn empty(numerator: u8, denominator: u8) -> Self {
2130 let total_beats = TimeSignature {
2131 numerator,
2132 denominator,
2133 }
2134 .total_beats();
2135 let mut voice0: Vec<Note> = Vec::new();
2136 let mut remaining = total_beats;
2137 while remaining > 1e-9 {
2138 let dur = Duration::whole_filling_beats(remaining);
2139 remaining -= dur.beats(0);
2140 voice0.push(Note::rest(dur));
2141 }
2142 Self {
2143 number: 0,
2144 time_sig: None,
2145 key_sig: None,
2146 clef: None,
2147 mid_clefs: Vec::new(),
2148 tempo: None,
2149 tempo_ramp_to: None,
2150 instrument_change: None,
2151 tablature_change: None,
2152 barline_left: Barline::Normal,
2153 barline_right: Barline::Normal,
2154 volta: None,
2155 tempo_text: None,
2156 rehearsal: None,
2157 navigation: None,
2158 expression_text: None,
2159 texts: Vec::new(),
2160 figured_bass: Vec::new(),
2161 harp_pedal_diagrams: Vec::new(),
2162 multi_rest_count: None,
2163 system_break: false,
2164 page_break: false,
2165 section_break: false,
2166 voices: [voice0, vec![], vec![], vec![]],
2167 source_voice_numbers: [None; 4],
2168 actual_length: None,
2169 measure_repeat: None,
2170 }
2171 }
2172
2173 pub fn repeat_content_from(&mut self, source: &Measure) {
2176 for (target, voice) in self.voices.iter_mut().zip(&source.voices) {
2177 *target = voice.iter().map(Note::repeated_copy).collect();
2178 }
2179 self.source_voice_numbers = source.source_voice_numbers;
2180 }
2181
2182 pub fn same_sounding_content(&self, other: &Measure) -> bool {
2185 self.voices.iter().zip(&other.voices).all(|(left, right)| {
2186 left.len() == right.len()
2187 && left.iter().zip(right).all(|(a, b)| {
2188 a.is_rest == b.is_rest
2189 && a.pitches == b.pitches
2190 && a.duration == b.duration
2191 && a.dot_count == b.dot_count
2192 && a.tuplet == b.tuplet
2193 && a.is_grace == b.is_grace
2194 })
2195 })
2196 }
2197
2198 pub fn duration_beats(&self, time_signature: &TimeSignature) -> f64 {
2201 self.actual_length
2202 .and_then(|length| length.beats())
2203 .unwrap_or_else(|| {
2204 self.time_sig
2205 .as_ref()
2206 .unwrap_or(time_signature)
2207 .total_beats()
2208 })
2209 }
2210
2211 pub fn renumber(&mut self, n: u32) {
2212 self.number = n;
2213 }
2214}
2215
2216#[derive(Debug, Clone, Serialize, Deserialize)]
2217pub struct Note {
2218 pub id: String,
2219 pub is_rest: bool,
2220 #[serde(default)]
2222 pub is_unpitched: bool,
2223 #[serde(default)]
2225 pub instrument_id: Option<String>,
2226 #[serde(default)]
2230 pub offset_x: Option<f64>,
2231 #[serde(default)]
2233 pub offset_y: Option<f64>,
2234 #[serde(default)]
2236 pub relative_x: Option<f64>,
2237 #[serde(default)]
2239 pub relative_y: Option<f64>,
2240 pub pitches: Vec<Pitch>,
2242 #[serde(default)]
2243 pub tab_position: Option<super::notation::TabPosition>,
2244 #[serde(default)]
2247 pub tab_positions: Vec<super::notation::TabPosition>,
2248 pub duration: Duration,
2249 pub dot_count: u8,
2250 pub tie_start: bool,
2251 pub tie_end: bool,
2252 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2256 pub pitch_tie_starts: Vec<bool>,
2257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2259 pub pitch_tie_ends: Vec<bool>,
2260 pub beam: BeamState,
2261 pub articulations: Vec<Articulation>,
2262 pub dynamic: Option<Dynamic>,
2263 pub stem_up: Option<bool>,
2264 #[serde(default)]
2265 pub hairpin_start: Option<HairpinKind>,
2266 #[serde(default)]
2267 pub hairpin_end: bool,
2268 #[serde(default)]
2269 pub tuplet: Option<TupletInfo>,
2270 #[serde(default)]
2271 pub chord_symbol: Option<ChordSymbol>,
2272 #[serde(default)]
2273 pub is_grace: bool,
2274 #[serde(default)]
2276 pub grace_slash: bool,
2277 #[serde(default)]
2278 pub ottava_start: Option<OttavaKind>,
2279 #[serde(default)]
2280 pub ottava_end: bool,
2281 #[serde(default)]
2282 pub lyric: Option<Lyric>,
2283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2285 pub additional_lyrics: Vec<VerseLyric>,
2286 #[serde(default)]
2287 pub pedal_start: bool,
2288 #[serde(default)]
2289 pub pedal_end: bool,
2290 #[serde(default)]
2291 pub slur_start: bool,
2292 #[serde(default)]
2293 pub slur_end: bool,
2294 #[serde(default)]
2296 pub arpeggiate: Option<bool>,
2297 #[serde(default)]
2299 pub technique_text: Option<String>,
2300 #[serde(default)]
2301 pub glissando_start: bool,
2302 #[serde(default)]
2303 pub glissando_end: bool,
2304 #[serde(default)]
2305 pub cross_staff: Option<CrossStaff>,
2306 #[serde(default)]
2308 pub fingering: Option<u8>,
2309 #[serde(default)]
2312 pub fingerings: Vec<u8>,
2313 #[serde(default)]
2315 pub string_number: Option<u8>,
2316 #[serde(default)]
2317 pub note_head: NoteHead,
2318 #[serde(default)]
2320 pub is_cue: bool,
2321 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2325 pub hidden: bool,
2326 #[serde(default)]
2328 pub trill_line_start: bool,
2329 #[serde(default)]
2331 pub trill_line_end: bool,
2332 #[serde(default)]
2334 pub guitar_technique: Option<GuitarTechnique>,
2335 #[serde(default)]
2337 pub guitar_bend_alter_cents: Option<i16>,
2338 #[serde(default)]
2340 pub guitar_bend_curve: Vec<GuitarBendPoint>,
2341}
2342
2343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2345pub struct GuitarBendPoint {
2346 pub position_per_mille: u16,
2347 pub alter_cents: i16,
2348}
2349
2350impl Note {
2351 pub fn select_fingering(
2353 &self,
2354 policy: super::notation::FingeringSelectionPolicy,
2355 ) -> Option<u8> {
2356 let candidates = if self.fingerings.is_empty() {
2357 self.fingering.into_iter().collect::<Vec<_>>()
2358 } else {
2359 self.fingerings.clone()
2360 };
2361 match policy {
2362 super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
2363 super::notation::FingeringSelectionPolicy::LowestNumber => {
2364 candidates.iter().copied().min()
2365 }
2366 super::notation::FingeringSelectionPolicy::HighestNumber => {
2367 candidates.iter().copied().max()
2368 }
2369 }
2370 }
2371
2372 pub fn new(pitch: Pitch, duration: Duration) -> Self {
2373 Self {
2374 id: Uuid::new_v4().to_string(),
2375 is_rest: false,
2376 is_unpitched: false,
2377 instrument_id: None,
2378 offset_x: None,
2379 offset_y: None,
2380 relative_x: None,
2381 relative_y: None,
2382 pitches: vec![pitch],
2383 tab_position: None,
2384 tab_positions: Vec::new(),
2385 duration,
2386 dot_count: 0,
2387 tie_start: false,
2388 tie_end: false,
2389 pitch_tie_starts: Vec::new(),
2390 pitch_tie_ends: Vec::new(),
2391 beam: BeamState::None,
2392 articulations: Vec::new(),
2393 dynamic: None,
2394 stem_up: None,
2395 hairpin_start: None,
2396 hairpin_end: false,
2397 tuplet: None,
2398 chord_symbol: None,
2399 is_grace: false,
2400 grace_slash: false,
2401 ottava_start: None,
2402 ottava_end: false,
2403 lyric: None,
2404 additional_lyrics: Vec::new(),
2405 pedal_start: false,
2406 pedal_end: false,
2407 slur_start: false,
2408 slur_end: false,
2409 arpeggiate: None,
2410 technique_text: None,
2411 glissando_start: false,
2412 glissando_end: false,
2413 cross_staff: None,
2414 fingering: None,
2415 fingerings: Vec::new(),
2416 string_number: None,
2417 note_head: NoteHead::Normal,
2418 is_cue: false,
2419 hidden: false,
2420 trill_line_start: false,
2421 trill_line_end: false,
2422 guitar_technique: None,
2423 guitar_bend_alter_cents: None,
2424 guitar_bend_curve: Vec::new(),
2425 }
2426 }
2427
2428 pub fn rest(duration: Duration) -> Self {
2429 Self {
2430 id: Uuid::new_v4().to_string(),
2431 is_rest: true,
2432 is_unpitched: false,
2433 instrument_id: None,
2434 offset_x: None,
2435 offset_y: None,
2436 relative_x: None,
2437 relative_y: None,
2438 pitches: Vec::new(),
2439 tab_position: None,
2440 tab_positions: Vec::new(),
2441 duration,
2442 dot_count: 0,
2443 tie_start: false,
2444 tie_end: false,
2445 pitch_tie_starts: Vec::new(),
2446 pitch_tie_ends: Vec::new(),
2447 beam: BeamState::None,
2448 articulations: Vec::new(),
2449 dynamic: None,
2450 stem_up: None,
2451 hairpin_start: None,
2452 hairpin_end: false,
2453 tuplet: None,
2454 chord_symbol: None,
2455 is_grace: false,
2456 grace_slash: false,
2457 ottava_start: None,
2458 ottava_end: false,
2459 lyric: None,
2460 additional_lyrics: Vec::new(),
2461 pedal_start: false,
2462 pedal_end: false,
2463 slur_start: false,
2464 slur_end: false,
2465 arpeggiate: None,
2466 technique_text: None,
2467 glissando_start: false,
2468 glissando_end: false,
2469 cross_staff: None,
2470 fingering: None,
2471 fingerings: Vec::new(),
2472 string_number: None,
2473 note_head: NoteHead::Normal,
2474 is_cue: false,
2475 hidden: false,
2476 trill_line_start: false,
2477 trill_line_end: false,
2478 guitar_technique: None,
2479 guitar_bend_alter_cents: None,
2480 guitar_bend_curve: Vec::new(),
2481 }
2482 }
2483
2484 pub fn repeated_copy(&self) -> Note {
2487 let mut copy = self.clone();
2488 copy.id = Uuid::new_v4().to_string();
2489 copy.tie_start = false;
2490 copy.tie_end = false;
2491 copy.slur_start = false;
2492 copy.slur_end = false;
2493 copy.hairpin_start = None;
2494 copy.hairpin_end = false;
2495 copy.pedal_start = false;
2496 copy.pedal_end = false;
2497 copy.ottava_start = None;
2498 copy.ottava_end = false;
2499 copy.glissando_start = false;
2500 copy.glissando_end = false;
2501 copy.trill_line_start = false;
2502 copy.trill_line_end = false;
2503 copy.lyric = None;
2504 copy.additional_lyrics.clear();
2505 copy.chord_symbol = None;
2506 copy.dynamic = None;
2507 copy
2508 }
2509
2510 pub fn pitch_tie_start(&self, index: usize) -> bool {
2513 per_pitch_flag(
2514 &self.pitch_tie_starts,
2515 self.pitches.len(),
2516 index,
2517 self.tie_start,
2518 )
2519 }
2520
2521 pub fn pitch_tie_end(&self, index: usize) -> bool {
2523 per_pitch_flag(
2524 &self.pitch_tie_ends,
2525 self.pitches.len(),
2526 index,
2527 self.tie_end,
2528 )
2529 }
2530
2531 pub fn set_pitch_ties(&mut self, starts: &[bool], ends: &[bool]) {
2536 let count = self.pitches.len();
2537 if starts.len() == count {
2538 self.tie_start = starts.iter().any(|tied| *tied);
2539 self.pitch_tie_starts = if starts.iter().all(|tied| *tied == self.tie_start) {
2540 Vec::new()
2541 } else {
2542 starts.to_vec()
2543 };
2544 }
2545 if ends.len() == count {
2546 self.tie_end = ends.iter().any(|tied| *tied);
2547 self.pitch_tie_ends = if ends.iter().all(|tied| *tied == self.tie_end) {
2548 Vec::new()
2549 } else {
2550 ends.to_vec()
2551 };
2552 }
2553 }
2554
2555 pub fn pitch_tie_starts_or_uniform(&self) -> Vec<bool> {
2557 (0..self.pitches.len())
2558 .map(|index| self.pitch_tie_start(index))
2559 .collect()
2560 }
2561
2562 pub fn pitch_tie_ends_or_uniform(&self) -> Vec<bool> {
2564 (0..self.pitches.len())
2565 .map(|index| self.pitch_tie_end(index))
2566 .collect()
2567 }
2568
2569 pub fn is_plain_whole_rest(&self) -> bool {
2573 self.is_rest
2574 && matches!(self.duration, Duration::Whole)
2575 && self.dot_count == 0
2576 && self.tuplet.is_none()
2577 && !self.is_grace
2578 && !self.is_cue
2579 }
2580
2581 pub fn beats(&self) -> f64 {
2582 if self.is_grace || self.is_cue {
2583 return 0.0;
2584 }
2585 let base = self.duration.beats(self.dot_count);
2586 if let Some(ref t) = self.tuplet {
2587 base * (t.normal_notes as f64) / (t.actual_notes as f64)
2588 } else {
2589 base
2590 }
2591 }
2592}
2593
2594impl Duration {
2595 pub fn whole_filling_beats(beats: f64) -> Duration {
2597 if beats >= 4.0 {
2598 Duration::Whole
2599 } else if beats >= 2.0 {
2600 Duration::Half
2601 } else if beats >= 1.0 {
2602 Duration::Quarter
2603 } else if beats >= 0.5 {
2604 Duration::Eighth
2605 } else if beats >= 0.25 {
2606 Duration::Sixteenth
2607 } else if beats >= 0.125 {
2608 Duration::ThirtySecond
2609 } else {
2610 Duration::SixtyFourth
2611 }
2612 }
2613}
2614
2615#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2619pub struct NoteAddr {
2620 pub part: usize,
2621 pub staff: usize,
2622 pub measure: usize,
2623 pub voice: usize,
2624 pub note: usize,
2625}
2626
2627#[derive(Debug, Clone, Serialize, Deserialize)]
2631pub enum ScoreChange {
2632 ScoreViewsChanged {
2633 old: Vec<ScoreView>,
2634 new: Vec<ScoreView>,
2635 },
2636 ScoreTextChanged {
2637 old: Vec<StyledText>,
2638 new: Vec<StyledText>,
2639 },
2640 MeasureTextChanged {
2641 part: usize,
2642 staff: usize,
2643 measure: usize,
2644 old: Vec<StyledText>,
2645 new: Vec<StyledText>,
2646 },
2647 FiguredBassChanged {
2648 part: usize,
2649 staff: usize,
2650 measure: usize,
2651 old: Vec<FiguredBassFigure>,
2652 new: Vec<FiguredBassFigure>,
2653 },
2654 HarpPedalDiagramsChanged {
2655 part: usize,
2656 staff: usize,
2657 measure: usize,
2658 old: Vec<HarpPedalDiagram>,
2659 new: Vec<HarpPedalDiagram>,
2660 },
2661 ChordDefinitionsChanged {
2662 old: Vec<ChordDefinition>,
2663 new: Vec<ChordDefinition>,
2664 },
2665 PartNamesChanged {
2666 part: usize,
2667 old_name: String,
2668 new_name: String,
2669 old_short_name: String,
2670 new_short_name: String,
2671 },
2672 PartMidiChanged {
2673 part: usize,
2674 old_channel: u8,
2675 new_channel: u8,
2676 old_program: u8,
2677 new_program: u8,
2678 },
2679 InstrumentDefinitionChanged {
2680 part: usize,
2681 old: Option<InstrumentDefinition>,
2682 new: Option<InstrumentDefinition>,
2683 },
2684 PartMidiAutomationChanged {
2685 part: usize,
2686 old_pitch_bends: Vec<MidiPitchBend>,
2687 new_pitch_bends: Vec<MidiPitchBend>,
2688 old_control_changes: Vec<MidiControlChange>,
2689 new_control_changes: Vec<MidiControlChange>,
2690 old_program_changes: Vec<MidiProgramChange>,
2691 new_program_changes: Vec<MidiProgramChange>,
2692 old_aftertouch: Vec<MidiAftertouch>,
2693 new_aftertouch: Vec<MidiAftertouch>,
2694 },
2695 StaffConfigurationChanged {
2696 part: usize,
2697 staff: usize,
2698 old_clef: Clef,
2699 new_clef: Clef,
2700 old_transpose_semitones: i8,
2701 new_transpose_semitones: i8,
2702 },
2703 StaffPresentationChanged {
2704 part: usize,
2705 staff: usize,
2706 old: StaffPresentation,
2707 new: StaffPresentation,
2708 },
2709 MeasurePresentationChanged {
2710 part: usize,
2711 staff: usize,
2712 measure: usize,
2713 old_number: u32,
2714 new_number: u32,
2715 old_clef: Option<Clef>,
2716 new_clef: Option<Clef>,
2717 old_tempo_text: Option<String>,
2718 new_tempo_text: Option<String>,
2719 old_navigation: Option<String>,
2720 new_navigation: Option<String>,
2721 old_expression_text: Option<String>,
2722 new_expression_text: Option<String>,
2723 old_multi_rest_count: Option<u8>,
2724 new_multi_rest_count: Option<u8>,
2725 old_system_break: bool,
2726 new_system_break: bool,
2727 old_page_break: bool,
2728 new_page_break: bool,
2729 old_section_break: bool,
2730 new_section_break: bool,
2731 },
2732 TablatureConfigChanged {
2733 part: usize,
2734 staff: usize,
2735 old: Option<TablatureConfig>,
2736 new: Option<TablatureConfig>,
2737 },
2738 TablatureChangeChanged {
2739 part: usize,
2740 staff: usize,
2741 measure: usize,
2742 old: Option<TablatureConfig>,
2743 new: Option<TablatureConfig>,
2744 },
2745 MidMeasureClefsChanged {
2746 part: usize,
2747 staff: usize,
2748 measure: usize,
2749 old: Vec<MidMeasureClef>,
2750 new: Vec<MidMeasureClef>,
2751 },
2752 UnrepresentedFieldChanged {
2757 path: String,
2758 },
2759 MetadataChanged {
2760 field: String,
2761 old: String,
2762 new: String,
2763 },
2764 TempoChanged {
2765 old: u16,
2766 new: u16,
2767 },
2768 KeySignatureChanged {
2769 old: KeySignature,
2770 new: KeySignature,
2771 },
2772 PartAdded {
2773 part_index: usize,
2774 },
2775 PartRemoved {
2776 part_index: usize,
2777 name: String,
2778 },
2779 NoteAdded {
2780 part: usize,
2781 staff: usize,
2782 measure: usize,
2783 voice: usize,
2784 note_index: usize,
2785 },
2786 NoteRemoved {
2787 part: usize,
2788 staff: usize,
2789 measure: usize,
2790 voice: usize,
2791 note: Box<Note>,
2792 },
2793 NoteModified {
2794 part: usize,
2795 staff: usize,
2796 measure: usize,
2797 voice: usize,
2798 note_index: usize,
2799 old: Box<Note>,
2800 new: Box<Note>,
2801 },
2802 TimeSigChanged {
2803 part: usize,
2804 staff: usize,
2805 measure: usize,
2806 old: Option<TimeSignature>,
2807 new: Option<TimeSignature>,
2808 },
2809 MeasureTempoChanged {
2810 part: usize,
2811 staff: usize,
2812 measure: usize,
2813 old: Option<u16>,
2814 new: Option<u16>,
2815 },
2816 MeasureTempoRampChanged {
2817 part: usize,
2818 staff: usize,
2819 measure: usize,
2820 old: Option<u16>,
2821 new: Option<u16>,
2822 },
2823 BarlineChanged {
2824 part: usize,
2825 staff: usize,
2826 measure: usize,
2827 },
2828 RehearsalMarkChanged {
2829 part: usize,
2830 staff: usize,
2831 measure: usize,
2832 old: Option<String>,
2833 new: Option<String>,
2834 },
2835 VoltaChanged {
2836 part: usize,
2837 staff: usize,
2838 measure: usize,
2839 },
2840}
2841
2842pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
2848 let mut changes: Vec<ScoreChange> = Vec::new();
2849
2850 if a.views != b.views {
2851 changes.push(ScoreChange::ScoreViewsChanged {
2852 old: a.views.clone(),
2853 new: b.views.clone(),
2854 });
2855 }
2856
2857 if a.texts != b.texts {
2858 changes.push(ScoreChange::ScoreTextChanged {
2859 old: a.texts.clone(),
2860 new: b.texts.clone(),
2861 });
2862 }
2863 if a.chord_definitions != b.chord_definitions {
2864 changes.push(ScoreChange::ChordDefinitionsChanged {
2865 old: a.chord_definitions.clone(),
2866 new: b.chord_definitions.clone(),
2867 });
2868 }
2869 if let Some(path) = first_unrepresented_field_change(a, b) {
2870 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
2871 }
2872
2873 macro_rules! meta {
2874 ($field:ident, $name:literal) => {
2875 if a.metadata.$field != b.metadata.$field {
2876 changes.push(ScoreChange::MetadataChanged {
2877 field: $name.to_string(),
2878 old: a.metadata.$field.clone(),
2879 new: b.metadata.$field.clone(),
2880 });
2881 }
2882 };
2883 }
2884 meta!(title, "title");
2885 meta!(composer, "composer");
2886 meta!(lyricist, "lyricist");
2887 meta!(copyright, "copyright");
2888 meta!(work_number, "work_number");
2889 meta!(movement_title, "movement_title");
2890
2891 if a.settings.tempo_bpm != b.settings.tempo_bpm {
2892 changes.push(ScoreChange::TempoChanged {
2893 old: a.settings.tempo_bpm,
2894 new: b.settings.tempo_bpm,
2895 });
2896 }
2897 if a.settings.key_signature != b.settings.key_signature {
2898 changes.push(ScoreChange::KeySignatureChanged {
2899 old: a.settings.key_signature.clone(),
2900 new: b.settings.key_signature.clone(),
2901 });
2902 }
2903
2904 let a_len = a.parts.len();
2905 let b_len = b.parts.len();
2906 for i in b_len..a_len {
2907 changes.push(ScoreChange::PartRemoved {
2908 part_index: i,
2909 name: a.parts[i].name.clone(),
2910 });
2911 }
2912 for i in a_len..b_len {
2913 changes.push(ScoreChange::PartAdded { part_index: i });
2914 }
2915
2916 for pi in 0..a_len.min(b_len) {
2917 let ap = &a.parts[pi];
2918 let bp = &b.parts[pi];
2919 if ap.name != bp.name || ap.short_name != bp.short_name {
2920 changes.push(ScoreChange::PartNamesChanged {
2921 part: pi,
2922 old_name: ap.name.clone(),
2923 new_name: bp.name.clone(),
2924 old_short_name: ap.short_name.clone(),
2925 new_short_name: bp.short_name.clone(),
2926 });
2927 }
2928 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
2929 changes.push(ScoreChange::PartMidiChanged {
2930 part: pi,
2931 old_channel: ap.midi_channel,
2932 new_channel: bp.midi_channel,
2933 old_program: ap.midi_program,
2934 new_program: bp.midi_program,
2935 });
2936 }
2937 if ap.instrument != bp.instrument {
2938 changes.push(ScoreChange::InstrumentDefinitionChanged {
2939 part: pi,
2940 old: ap.instrument.clone(),
2941 new: bp.instrument.clone(),
2942 });
2943 }
2944 if ap.midi_pitch_bends != bp.midi_pitch_bends
2945 || ap.midi_control_changes != bp.midi_control_changes
2946 || ap.midi_program_changes != bp.midi_program_changes
2947 || ap.midi_aftertouch != bp.midi_aftertouch
2948 {
2949 changes.push(ScoreChange::PartMidiAutomationChanged {
2950 part: pi,
2951 old_pitch_bends: ap.midi_pitch_bends.clone(),
2952 new_pitch_bends: bp.midi_pitch_bends.clone(),
2953 old_control_changes: ap.midi_control_changes.clone(),
2954 new_control_changes: bp.midi_control_changes.clone(),
2955 old_program_changes: ap.midi_program_changes.clone(),
2956 new_program_changes: bp.midi_program_changes.clone(),
2957 old_aftertouch: ap.midi_aftertouch.clone(),
2958 new_aftertouch: bp.midi_aftertouch.clone(),
2959 });
2960 }
2961 for si in 0..ap.staves.len().min(bp.staves.len()) {
2962 let a_staff = &ap.staves[si];
2963 let b_staff = &bp.staves[si];
2964 if a_staff.clef != b_staff.clef
2965 || a_staff.transpose_semitones != b_staff.transpose_semitones
2966 {
2967 changes.push(ScoreChange::StaffConfigurationChanged {
2968 part: pi,
2969 staff: si,
2970 old_clef: a_staff.clef.clone(),
2971 new_clef: b_staff.clef.clone(),
2972 old_transpose_semitones: a_staff.transpose_semitones,
2973 new_transpose_semitones: b_staff.transpose_semitones,
2974 });
2975 }
2976 if a_staff.tablature != b_staff.tablature {
2977 changes.push(ScoreChange::TablatureConfigChanged {
2978 part: pi,
2979 staff: si,
2980 old: a_staff.tablature.clone(),
2981 new: b_staff.tablature.clone(),
2982 });
2983 }
2984 if a_staff.presentation != b_staff.presentation {
2985 changes.push(ScoreChange::StaffPresentationChanged {
2986 part: pi,
2987 staff: si,
2988 old: a_staff.presentation.clone(),
2989 new: b_staff.presentation.clone(),
2990 });
2991 }
2992 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
2993 let am = &a_staff.measures[mi];
2994 let bm = &b_staff.measures[mi];
2995 if am.tablature_change != bm.tablature_change {
2996 changes.push(ScoreChange::TablatureChangeChanged {
2997 part: pi,
2998 staff: si,
2999 measure: mi,
3000 old: am.tablature_change.clone(),
3001 new: bm.tablature_change.clone(),
3002 });
3003 }
3004 if am.mid_clefs != bm.mid_clefs {
3005 changes.push(ScoreChange::MidMeasureClefsChanged {
3006 part: pi,
3007 staff: si,
3008 measure: mi,
3009 old: am.mid_clefs.clone(),
3010 new: bm.mid_clefs.clone(),
3011 });
3012 }
3013 if am.number != bm.number
3014 || am.clef != bm.clef
3015 || am.tempo_text != bm.tempo_text
3016 || am.navigation != bm.navigation
3017 || am.expression_text != bm.expression_text
3018 || am.multi_rest_count != bm.multi_rest_count
3019 || am.system_break != bm.system_break
3020 || am.page_break != bm.page_break
3021 || am.section_break != bm.section_break
3022 {
3023 changes.push(ScoreChange::MeasurePresentationChanged {
3024 part: pi,
3025 staff: si,
3026 measure: mi,
3027 old_number: am.number,
3028 new_number: bm.number,
3029 old_clef: am.clef.clone(),
3030 new_clef: bm.clef.clone(),
3031 old_tempo_text: am.tempo_text.clone(),
3032 new_tempo_text: bm.tempo_text.clone(),
3033 old_navigation: am.navigation.clone(),
3034 new_navigation: bm.navigation.clone(),
3035 old_expression_text: am.expression_text.clone(),
3036 new_expression_text: bm.expression_text.clone(),
3037 old_multi_rest_count: am.multi_rest_count,
3038 new_multi_rest_count: bm.multi_rest_count,
3039 old_system_break: am.system_break,
3040 new_system_break: bm.system_break,
3041 old_page_break: am.page_break,
3042 new_page_break: bm.page_break,
3043 old_section_break: am.section_break,
3044 new_section_break: bm.section_break,
3045 });
3046 }
3047 for vi in 0..4usize {
3048 let av = &am.voices[vi];
3049 let bv = &bm.voices[vi];
3050 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3051 if !note_content_eq(a_note, b_note) {
3052 changes.push(ScoreChange::NoteModified {
3053 part: pi,
3054 staff: si,
3055 measure: mi,
3056 voice: vi,
3057 note_index: ni,
3058 old: Box::new(a_note.clone()),
3059 new: Box::new(b_note.clone()),
3060 });
3061 }
3062 }
3063 for note in av.iter().skip(bv.len()) {
3064 changes.push(ScoreChange::NoteRemoved {
3065 part: pi,
3066 staff: si,
3067 measure: mi,
3068 voice: vi,
3069 note: Box::new(note.clone()),
3070 });
3071 }
3072 for ni in av.len()..bv.len() {
3073 changes.push(ScoreChange::NoteAdded {
3074 part: pi,
3075 staff: si,
3076 measure: mi,
3077 voice: vi,
3078 note_index: ni,
3079 });
3080 }
3081 }
3082 if am.time_sig != bm.time_sig {
3083 changes.push(ScoreChange::TimeSigChanged {
3084 part: pi,
3085 staff: si,
3086 measure: mi,
3087 old: am.time_sig.clone(),
3088 new: bm.time_sig.clone(),
3089 });
3090 }
3091 if am.tempo != bm.tempo {
3092 changes.push(ScoreChange::MeasureTempoChanged {
3093 part: pi,
3094 staff: si,
3095 measure: mi,
3096 old: am.tempo,
3097 new: bm.tempo,
3098 });
3099 }
3100 if am.tempo_ramp_to != bm.tempo_ramp_to {
3101 changes.push(ScoreChange::MeasureTempoRampChanged {
3102 part: pi,
3103 staff: si,
3104 measure: mi,
3105 old: am.tempo_ramp_to,
3106 new: bm.tempo_ramp_to,
3107 });
3108 }
3109 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3110 changes.push(ScoreChange::BarlineChanged {
3111 part: pi,
3112 staff: si,
3113 measure: mi,
3114 });
3115 }
3116 if am.rehearsal != bm.rehearsal {
3117 changes.push(ScoreChange::RehearsalMarkChanged {
3118 part: pi,
3119 staff: si,
3120 measure: mi,
3121 old: am.rehearsal.clone(),
3122 new: bm.rehearsal.clone(),
3123 });
3124 }
3125 if am.volta != bm.volta {
3126 changes.push(ScoreChange::VoltaChanged {
3127 part: pi,
3128 staff: si,
3129 measure: mi,
3130 });
3131 }
3132 if am.texts != bm.texts {
3133 changes.push(ScoreChange::MeasureTextChanged {
3134 part: pi,
3135 staff: si,
3136 measure: mi,
3137 old: am.texts.clone(),
3138 new: bm.texts.clone(),
3139 });
3140 }
3141 if am.figured_bass != bm.figured_bass {
3142 changes.push(ScoreChange::FiguredBassChanged {
3143 part: pi,
3144 staff: si,
3145 measure: mi,
3146 old: am.figured_bass.clone(),
3147 new: bm.figured_bass.clone(),
3148 });
3149 }
3150 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3151 changes.push(ScoreChange::HarpPedalDiagramsChanged {
3152 part: pi,
3153 staff: si,
3154 measure: mi,
3155 old: am.harp_pedal_diagrams.clone(),
3156 new: bm.harp_pedal_diagrams.clone(),
3157 });
3158 }
3159 }
3160 }
3161 }
3162
3163 changes
3164}
3165
3166fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
3167 if a.settings.time_signature != b.settings.time_signature {
3168 return Some("settings.time_signature".to_string());
3169 }
3170 if a.part_groups.len() != b.part_groups.len()
3171 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3172 x.first_part != y.first_part
3173 || x.last_part != y.last_part
3174 || x.symbol != y.symbol
3175 || x.barlines_connect != y.barlines_connect
3176 })
3177 {
3178 return Some("part_groups".to_string());
3179 }
3180 a.parts
3181 .iter()
3182 .zip(&b.parts)
3183 .enumerate()
3184 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
3185}
3186
3187fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
3188 let prefix = format!("parts[{part_index}]");
3189 if a.percussion_instruments != b.percussion_instruments {
3190 return Some(format!("{prefix}.percussion_instruments"));
3191 }
3192 if a.staff_groups != b.staff_groups {
3193 return Some(format!("{prefix}.staff_groups"));
3194 }
3195 if a.staves.len() != b.staves.len() {
3196 return Some(format!("{prefix}.staves"));
3197 }
3198 a.staves
3199 .iter()
3200 .zip(&b.staves)
3201 .enumerate()
3202 .find_map(|(staff_index, (a, b))| {
3203 first_unrepresented_staff_change(&prefix, staff_index, a, b)
3204 })
3205}
3206
3207fn first_unrepresented_staff_change(
3208 part_prefix: &str,
3209 staff_index: usize,
3210 a: &Staff,
3211 b: &Staff,
3212) -> Option<String> {
3213 let prefix = format!("{part_prefix}.staves[{staff_index}]");
3214 if a.measures.len() != b.measures.len() {
3215 return Some(format!("{prefix}.measures"));
3216 }
3217 a.measures
3218 .iter()
3219 .zip(&b.measures)
3220 .enumerate()
3221 .find_map(|(measure_index, (a, b))| {
3222 first_unrepresented_measure_change(&prefix, measure_index, a, b)
3223 })
3224}
3225
3226fn first_unrepresented_measure_change(
3227 _staff_prefix: &str,
3228 _measure_index: usize,
3229 _a: &Measure,
3230 _b: &Measure,
3231) -> Option<String> {
3232 None
3233}
3234
3235#[derive(Debug, Clone, Serialize, Deserialize)]
3242pub enum ScorePatch {
3243 SetScoreViews {
3244 value: Vec<ScoreView>,
3245 },
3246 SetScoreTexts {
3247 value: Vec<StyledText>,
3248 },
3249 SetMeasureTexts {
3250 part: usize,
3251 staff: usize,
3252 measure: usize,
3253 value: Vec<StyledText>,
3254 },
3255 SetFiguredBass {
3256 part: usize,
3257 staff: usize,
3258 measure: usize,
3259 value: Vec<FiguredBassFigure>,
3260 },
3261 SetHarpPedalDiagrams {
3262 part: usize,
3263 staff: usize,
3264 measure: usize,
3265 value: Vec<HarpPedalDiagram>,
3266 },
3267 SetChordDefinitions {
3268 value: Vec<ChordDefinition>,
3269 },
3270 SetPartNames {
3271 part: usize,
3272 name: String,
3273 short_name: String,
3274 },
3275 SetPartMidi {
3276 part: usize,
3277 channel: u8,
3278 program: u8,
3279 },
3280 SetInstrumentDefinition {
3281 part: usize,
3282 value: Option<InstrumentDefinition>,
3283 },
3284 SetPartMidiAutomation {
3285 part: usize,
3286 pitch_bends: Vec<MidiPitchBend>,
3287 control_changes: Vec<MidiControlChange>,
3288 program_changes: Vec<MidiProgramChange>,
3289 aftertouch: Vec<MidiAftertouch>,
3290 },
3291 SetStaffConfiguration {
3292 part: usize,
3293 staff: usize,
3294 clef: Clef,
3295 transpose_semitones: i8,
3296 },
3297 SetStaffPresentation {
3298 part: usize,
3299 staff: usize,
3300 value: StaffPresentation,
3301 },
3302 SetMeasurePresentation {
3303 part: usize,
3304 staff: usize,
3305 measure: usize,
3306 number: u32,
3307 clef: Option<Clef>,
3308 tempo_text: Option<String>,
3309 navigation: Option<String>,
3310 expression_text: Option<String>,
3311 multi_rest_count: Option<u8>,
3312 system_break: bool,
3313 page_break: bool,
3314 #[serde(default)]
3315 section_break: bool,
3316 },
3317 SetTablatureConfig {
3318 part: usize,
3319 staff: usize,
3320 value: Option<TablatureConfig>,
3321 },
3322 SetMeasureTablatureChange {
3323 part: usize,
3324 staff: usize,
3325 measure: usize,
3326 value: Option<TablatureConfig>,
3327 },
3328 SetMeasureMidClefs {
3329 part: usize,
3330 staff: usize,
3331 measure: usize,
3332 value: Vec<MidMeasureClef>,
3333 },
3334 SetMetadata {
3335 field: String,
3336 value: String,
3337 },
3338 SetTempo {
3339 value: u16,
3340 },
3341 SetKeySignature {
3342 part: usize,
3343 staff: usize,
3344 measure: usize,
3345 value: Option<KeySignature>,
3346 },
3347 SetTimeSignature {
3348 part: usize,
3349 staff: usize,
3350 measure: usize,
3351 value: Option<TimeSignature>,
3352 },
3353 SetBarlines {
3354 part: usize,
3355 staff: usize,
3356 measure: usize,
3357 left: Barline,
3358 right: Barline,
3359 },
3360 SetRehearsal {
3361 part: usize,
3362 staff: usize,
3363 measure: usize,
3364 value: Option<String>,
3365 },
3366 SetVolta {
3367 part: usize,
3368 staff: usize,
3369 measure: usize,
3370 value: Option<VoltaBracket>,
3371 },
3372 AddNote {
3374 part: usize,
3375 staff: usize,
3376 measure: usize,
3377 voice: usize,
3378 #[serde(default = "legacy_append_index")]
3380 note_index: usize,
3381 note: Box<Note>,
3382 },
3383 RemoveNote {
3384 part: usize,
3385 staff: usize,
3386 measure: usize,
3387 voice: usize,
3388 note_index: usize,
3389 },
3390 ReplaceNote {
3392 part: usize,
3393 staff: usize,
3394 measure: usize,
3395 voice: usize,
3396 note_index: usize,
3397 note: Box<Note>,
3398 },
3399 SetMeasureTempo {
3400 part: usize,
3401 staff: usize,
3402 measure: usize,
3403 value: Option<u16>,
3404 },
3405 SetMeasureTempoRamp {
3406 part: usize,
3407 staff: usize,
3408 measure: usize,
3409 value: Option<u16>,
3410 },
3411 ReplaceScore {
3414 score: Box<Score>,
3415 },
3416}
3417
3418fn legacy_append_index() -> usize {
3419 usize::MAX
3420}
3421
3422fn patch_requires_replace(a: &Score, b: &Score) -> bool {
3426 if a.settings.time_signature != b.settings.time_signature
3427 || a.settings.key_signature != b.settings.key_signature
3428 || a.parts.len() != b.parts.len()
3429 || a.part_groups.len() != b.part_groups.len()
3430 {
3431 return true;
3432 }
3433 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3434 x.first_part != y.first_part
3435 || x.last_part != y.last_part
3436 || x.symbol != y.symbol
3437 || x.barlines_connect != y.barlines_connect
3438 }) {
3439 return true;
3440 }
3441 for (ap, bp) in a.parts.iter().zip(&b.parts) {
3442 if ap.percussion_instruments != bp.percussion_instruments
3443 || ap.staff_groups != bp.staff_groups
3444 || ap.staves.len() != bp.staves.len()
3445 {
3446 return true;
3447 }
3448 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
3449 if as_.measures.len() != bs.measures.len() {
3450 return true;
3451 }
3452 }
3453 }
3454 false
3455}
3456
3457pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
3462 let mut patches: Vec<ScorePatch> = Vec::new();
3463
3464 if patch_requires_replace(a, b) {
3465 return vec![ScorePatch::ReplaceScore {
3466 score: Box::new(b.clone()),
3467 }];
3468 }
3469
3470 if a.views != b.views {
3471 patches.push(ScorePatch::SetScoreViews {
3472 value: b.views.clone(),
3473 });
3474 }
3475
3476 if a.texts != b.texts {
3477 patches.push(ScorePatch::SetScoreTexts {
3478 value: b.texts.clone(),
3479 });
3480 }
3481 if a.chord_definitions != b.chord_definitions {
3482 patches.push(ScorePatch::SetChordDefinitions {
3483 value: b.chord_definitions.clone(),
3484 });
3485 }
3486
3487 macro_rules! meta {
3488 ($field:ident, $name:literal) => {
3489 if a.metadata.$field != b.metadata.$field {
3490 patches.push(ScorePatch::SetMetadata {
3491 field: $name.to_string(),
3492 value: b.metadata.$field.clone(),
3493 });
3494 }
3495 };
3496 }
3497 meta!(title, "title");
3498 meta!(composer, "composer");
3499 meta!(lyricist, "lyricist");
3500 meta!(copyright, "copyright");
3501 meta!(work_number, "work_number");
3502 meta!(movement_title, "movement_title");
3503
3504 if a.settings.tempo_bpm != b.settings.tempo_bpm {
3505 patches.push(ScorePatch::SetTempo {
3506 value: b.settings.tempo_bpm,
3507 });
3508 }
3509
3510 for pi in 0..a.parts.len().min(b.parts.len()) {
3511 let ap = &a.parts[pi];
3512 let bp = &b.parts[pi];
3513 if ap.name != bp.name || ap.short_name != bp.short_name {
3514 patches.push(ScorePatch::SetPartNames {
3515 part: pi,
3516 name: bp.name.clone(),
3517 short_name: bp.short_name.clone(),
3518 });
3519 }
3520 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3521 patches.push(ScorePatch::SetPartMidi {
3522 part: pi,
3523 channel: bp.midi_channel,
3524 program: bp.midi_program,
3525 });
3526 }
3527 if ap.instrument != bp.instrument {
3528 patches.push(ScorePatch::SetInstrumentDefinition {
3529 part: pi,
3530 value: bp.instrument.clone(),
3531 });
3532 }
3533 if ap.midi_pitch_bends != bp.midi_pitch_bends
3534 || ap.midi_control_changes != bp.midi_control_changes
3535 || ap.midi_program_changes != bp.midi_program_changes
3536 || ap.midi_aftertouch != bp.midi_aftertouch
3537 {
3538 patches.push(ScorePatch::SetPartMidiAutomation {
3539 part: pi,
3540 pitch_bends: bp.midi_pitch_bends.clone(),
3541 control_changes: bp.midi_control_changes.clone(),
3542 program_changes: bp.midi_program_changes.clone(),
3543 aftertouch: bp.midi_aftertouch.clone(),
3544 });
3545 }
3546 for si in 0..ap.staves.len().min(bp.staves.len()) {
3547 let a_staff = &ap.staves[si];
3548 let b_staff = &bp.staves[si];
3549 if a_staff.clef != b_staff.clef
3550 || a_staff.transpose_semitones != b_staff.transpose_semitones
3551 {
3552 patches.push(ScorePatch::SetStaffConfiguration {
3553 part: pi,
3554 staff: si,
3555 clef: b_staff.clef.clone(),
3556 transpose_semitones: b_staff.transpose_semitones,
3557 });
3558 }
3559 if a_staff.tablature != b_staff.tablature {
3560 patches.push(ScorePatch::SetTablatureConfig {
3561 part: pi,
3562 staff: si,
3563 value: b_staff.tablature.clone(),
3564 });
3565 }
3566 if a_staff.presentation != b_staff.presentation {
3567 patches.push(ScorePatch::SetStaffPresentation {
3568 part: pi,
3569 staff: si,
3570 value: b_staff.presentation.clone(),
3571 });
3572 }
3573 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3574 let am = &a_staff.measures[mi];
3575 let bm = &b_staff.measures[mi];
3576
3577 if am.tablature_change != bm.tablature_change {
3578 patches.push(ScorePatch::SetMeasureTablatureChange {
3579 part: pi,
3580 staff: si,
3581 measure: mi,
3582 value: bm.tablature_change.clone(),
3583 });
3584 }
3585
3586 if am.mid_clefs != bm.mid_clefs {
3587 patches.push(ScorePatch::SetMeasureMidClefs {
3588 part: pi,
3589 staff: si,
3590 measure: mi,
3591 value: bm.mid_clefs.clone(),
3592 });
3593 }
3594
3595 if am.number != bm.number
3596 || am.clef != bm.clef
3597 || am.tempo_text != bm.tempo_text
3598 || am.navigation != bm.navigation
3599 || am.expression_text != bm.expression_text
3600 || am.multi_rest_count != bm.multi_rest_count
3601 || am.system_break != bm.system_break
3602 || am.page_break != bm.page_break
3603 || am.section_break != bm.section_break
3604 {
3605 patches.push(ScorePatch::SetMeasurePresentation {
3606 part: pi,
3607 staff: si,
3608 measure: mi,
3609 number: bm.number,
3610 clef: bm.clef.clone(),
3611 tempo_text: bm.tempo_text.clone(),
3612 navigation: bm.navigation.clone(),
3613 expression_text: bm.expression_text.clone(),
3614 multi_rest_count: bm.multi_rest_count,
3615 system_break: bm.system_break,
3616 page_break: bm.page_break,
3617 section_break: bm.section_break,
3618 });
3619 }
3620 if am.key_sig != bm.key_sig {
3621 patches.push(ScorePatch::SetKeySignature {
3622 part: pi,
3623 staff: si,
3624 measure: mi,
3625 value: bm.key_sig.clone(),
3626 });
3627 }
3628 if am.time_sig != bm.time_sig {
3629 patches.push(ScorePatch::SetTimeSignature {
3630 part: pi,
3631 staff: si,
3632 measure: mi,
3633 value: bm.time_sig.clone(),
3634 });
3635 }
3636 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3637 patches.push(ScorePatch::SetBarlines {
3638 part: pi,
3639 staff: si,
3640 measure: mi,
3641 left: bm.barline_left.clone(),
3642 right: bm.barline_right.clone(),
3643 });
3644 }
3645 if am.rehearsal != bm.rehearsal {
3646 patches.push(ScorePatch::SetRehearsal {
3647 part: pi,
3648 staff: si,
3649 measure: mi,
3650 value: bm.rehearsal.clone(),
3651 });
3652 }
3653 if am.volta != bm.volta {
3654 patches.push(ScorePatch::SetVolta {
3655 part: pi,
3656 staff: si,
3657 measure: mi,
3658 value: bm.volta.clone(),
3659 });
3660 }
3661 if am.tempo != bm.tempo {
3662 patches.push(ScorePatch::SetMeasureTempo {
3663 part: pi,
3664 staff: si,
3665 measure: mi,
3666 value: bm.tempo,
3667 });
3668 }
3669 if am.tempo_ramp_to != bm.tempo_ramp_to {
3670 patches.push(ScorePatch::SetMeasureTempoRamp {
3671 part: pi,
3672 staff: si,
3673 measure: mi,
3674 value: bm.tempo_ramp_to,
3675 });
3676 }
3677 if am.texts != bm.texts {
3678 patches.push(ScorePatch::SetMeasureTexts {
3679 part: pi,
3680 staff: si,
3681 measure: mi,
3682 value: bm.texts.clone(),
3683 });
3684 }
3685 if am.figured_bass != bm.figured_bass {
3686 patches.push(ScorePatch::SetFiguredBass {
3687 part: pi,
3688 staff: si,
3689 measure: mi,
3690 value: bm.figured_bass.clone(),
3691 });
3692 }
3693 if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3694 patches.push(ScorePatch::SetHarpPedalDiagrams {
3695 part: pi,
3696 staff: si,
3697 measure: mi,
3698 value: bm.harp_pedal_diagrams.clone(),
3699 });
3700 }
3701
3702 for vi in 0..4usize {
3703 let av = &am.voices[vi];
3704 let bv = &bm.voices[vi];
3705 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3706 if !note_content_eq(a_note, b_note) {
3707 patches.push(ScorePatch::ReplaceNote {
3708 part: pi,
3709 staff: si,
3710 measure: mi,
3711 voice: vi,
3712 note_index: ni,
3713 note: Box::new(b_note.clone()),
3714 });
3715 }
3716 }
3717 for ni in (bv.len()..av.len()).rev() {
3719 patches.push(ScorePatch::RemoveNote {
3720 part: pi,
3721 staff: si,
3722 measure: mi,
3723 voice: vi,
3724 note_index: ni,
3725 });
3726 }
3727 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
3729 patches.push(ScorePatch::AddNote {
3730 part: pi,
3731 staff: si,
3732 measure: mi,
3733 voice: vi,
3734 note_index: av.len() + offset,
3735 note: Box::new(note.clone()),
3736 });
3737 }
3738 }
3739 }
3740 }
3741 }
3742
3743 patches
3744}
3745
3746pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
3751 let mut s = score.clone();
3752 for patch in patches {
3753 match patch {
3754 ScorePatch::ReplaceScore { score } => {
3755 s = (**score).clone();
3756 }
3757 ScorePatch::SetScoreViews { value } => {
3758 s.views = value.clone();
3759 }
3760 ScorePatch::SetScoreTexts { value } => {
3761 s.texts = value.clone();
3762 }
3763 ScorePatch::SetMeasureTexts {
3764 part,
3765 staff,
3766 measure,
3767 value,
3768 } => {
3769 s.parts
3770 .get_mut(*part)
3771 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3772 .staves
3773 .get_mut(*staff)
3774 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3775 .measures
3776 .get_mut(*measure)
3777 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3778 .texts = value.clone();
3779 }
3780 ScorePatch::SetFiguredBass {
3781 part,
3782 staff,
3783 measure,
3784 value,
3785 } => {
3786 s.parts
3787 .get_mut(*part)
3788 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3789 .staves
3790 .get_mut(*staff)
3791 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3792 .measures
3793 .get_mut(*measure)
3794 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3795 .figured_bass = value.clone();
3796 }
3797 ScorePatch::SetHarpPedalDiagrams {
3798 part,
3799 staff,
3800 measure,
3801 value,
3802 } => {
3803 s.parts
3804 .get_mut(*part)
3805 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3806 .staves
3807 .get_mut(*staff)
3808 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3809 .measures
3810 .get_mut(*measure)
3811 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3812 .harp_pedal_diagrams = value.clone();
3813 }
3814 ScorePatch::SetChordDefinitions { value } => {
3815 s.chord_definitions = value.clone();
3816 }
3817 ScorePatch::SetPartNames {
3818 part,
3819 name,
3820 short_name,
3821 } => {
3822 let target = s
3823 .parts
3824 .get_mut(*part)
3825 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3826 target.name = name.clone();
3827 target.short_name = short_name.clone();
3828 }
3829 ScorePatch::SetPartMidi {
3830 part,
3831 channel,
3832 program,
3833 } => {
3834 let target = s
3835 .parts
3836 .get_mut(*part)
3837 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3838 target.midi_channel = *channel;
3839 target.midi_program = *program;
3840 }
3841 ScorePatch::SetInstrumentDefinition { part, value } => {
3842 s.parts
3843 .get_mut(*part)
3844 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3845 .instrument = value.clone();
3846 }
3847 ScorePatch::SetPartMidiAutomation {
3848 part,
3849 pitch_bends,
3850 control_changes,
3851 program_changes,
3852 aftertouch,
3853 } => {
3854 let target = s
3855 .parts
3856 .get_mut(*part)
3857 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3858 target.midi_pitch_bends = pitch_bends.clone();
3859 target.midi_control_changes = control_changes.clone();
3860 target.midi_program_changes = program_changes.clone();
3861 target.midi_aftertouch = aftertouch.clone();
3862 }
3863 ScorePatch::SetStaffConfiguration {
3864 part,
3865 staff,
3866 clef,
3867 transpose_semitones,
3868 } => {
3869 let target = s
3870 .parts
3871 .get_mut(*part)
3872 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3873 .staves
3874 .get_mut(*staff)
3875 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3876 target.clef = clef.clone();
3877 target.transpose_semitones = *transpose_semitones;
3878 }
3879 ScorePatch::SetStaffPresentation { part, staff, value } => {
3880 let target = s
3881 .parts
3882 .get_mut(*part)
3883 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3884 .staves
3885 .get_mut(*staff)
3886 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3887 target.presentation = value.clone();
3888 }
3889 ScorePatch::SetMeasurePresentation {
3890 part,
3891 staff,
3892 measure,
3893 number,
3894 clef,
3895 tempo_text,
3896 navigation,
3897 expression_text,
3898 multi_rest_count,
3899 system_break,
3900 page_break,
3901 section_break,
3902 } => {
3903 let target = s
3904 .parts
3905 .get_mut(*part)
3906 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3907 .staves
3908 .get_mut(*staff)
3909 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3910 .measures
3911 .get_mut(*measure)
3912 .ok_or_else(|| {
3913 Error::InvalidPatch(format!("measure {measure} out of range"))
3914 })?;
3915 target.number = *number;
3916 target.clef = clef.clone();
3917 target.tempo_text = tempo_text.clone();
3918 target.navigation = navigation.clone();
3919 target.expression_text = expression_text.clone();
3920 target.multi_rest_count = *multi_rest_count;
3921 target.system_break = *system_break;
3922 target.page_break = *page_break;
3923 target.section_break = *section_break;
3924 }
3925 ScorePatch::SetTablatureConfig { part, staff, value } => {
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 .tablature = value.clone();
3933 }
3934 ScorePatch::SetMeasureTablatureChange {
3935 part,
3936 staff,
3937 measure,
3938 value,
3939 } => {
3940 s.parts
3941 .get_mut(*part)
3942 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3943 .staves
3944 .get_mut(*staff)
3945 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3946 .measures
3947 .get_mut(*measure)
3948 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3949 .tablature_change = value.clone();
3950 }
3951 ScorePatch::SetMeasureMidClefs {
3952 part,
3953 staff,
3954 measure,
3955 value,
3956 } => {
3957 s.parts
3958 .get_mut(*part)
3959 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3960 .staves
3961 .get_mut(*staff)
3962 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3963 .measures
3964 .get_mut(*measure)
3965 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3966 .mid_clefs = value.clone();
3967 }
3968 ScorePatch::SetMetadata { field, value } => match field.as_str() {
3969 "title" => s.metadata.title = value.clone(),
3970 "composer" => s.metadata.composer = value.clone(),
3971 "lyricist" => s.metadata.lyricist = value.clone(),
3972 "copyright" => s.metadata.copyright = value.clone(),
3973 "work_number" => s.metadata.work_number = value.clone(),
3974 "movement_title" => s.metadata.movement_title = value.clone(),
3975 other => {
3976 return Err(Error::InvalidPatch(format!(
3977 "unknown metadata field: {other}"
3978 )));
3979 }
3980 },
3981 ScorePatch::SetTempo { value } => {
3982 s.settings.tempo_bpm = *value;
3983 }
3984 ScorePatch::SetKeySignature {
3985 part,
3986 staff,
3987 measure,
3988 value,
3989 } => {
3990 s.parts
3991 .get_mut(*part)
3992 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3993 .staves
3994 .get_mut(*staff)
3995 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3996 .measures
3997 .get_mut(*measure)
3998 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3999 .key_sig = value.clone();
4000 }
4001 ScorePatch::SetTimeSignature {
4002 part,
4003 staff,
4004 measure,
4005 value,
4006 } => {
4007 s.parts
4008 .get_mut(*part)
4009 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4010 .staves
4011 .get_mut(*staff)
4012 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4013 .measures
4014 .get_mut(*measure)
4015 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4016 .time_sig = value.clone();
4017 }
4018 ScorePatch::SetBarlines {
4019 part,
4020 staff,
4021 measure,
4022 left,
4023 right,
4024 } => {
4025 let m = s
4026 .parts
4027 .get_mut(*part)
4028 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4029 .staves
4030 .get_mut(*staff)
4031 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4032 .measures
4033 .get_mut(*measure)
4034 .ok_or_else(|| {
4035 Error::InvalidPatch(format!("measure {measure} out of range"))
4036 })?;
4037 m.barline_left = left.clone();
4038 m.barline_right = right.clone();
4039 }
4040 ScorePatch::SetRehearsal {
4041 part,
4042 staff,
4043 measure,
4044 value,
4045 } => {
4046 s.parts
4047 .get_mut(*part)
4048 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4049 .staves
4050 .get_mut(*staff)
4051 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4052 .measures
4053 .get_mut(*measure)
4054 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4055 .rehearsal = value.clone();
4056 }
4057 ScorePatch::SetVolta {
4058 part,
4059 staff,
4060 measure,
4061 value,
4062 } => {
4063 s.parts
4064 .get_mut(*part)
4065 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4066 .staves
4067 .get_mut(*staff)
4068 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4069 .measures
4070 .get_mut(*measure)
4071 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4072 .volta = value.clone();
4073 }
4074 ScorePatch::AddNote {
4075 part,
4076 staff,
4077 measure,
4078 voice,
4079 note_index,
4080 note,
4081 } => {
4082 let v = s
4083 .parts
4084 .get_mut(*part)
4085 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4086 .staves
4087 .get_mut(*staff)
4088 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4089 .measures
4090 .get_mut(*measure)
4091 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4092 .voices
4093 .get_mut(*voice)
4094 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4095 let insert_at = if *note_index == usize::MAX {
4096 v.len()
4097 } else {
4098 *note_index
4099 };
4100 if insert_at > v.len() {
4101 return Err(Error::InvalidPatch(format!(
4102 "note_index {note_index} out of range"
4103 )));
4104 }
4105 v.insert(insert_at, *note.clone());
4106 }
4107 ScorePatch::RemoveNote {
4108 part,
4109 staff,
4110 measure,
4111 voice,
4112 note_index,
4113 } => {
4114 let v = s
4115 .parts
4116 .get_mut(*part)
4117 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4118 .staves
4119 .get_mut(*staff)
4120 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4121 .measures
4122 .get_mut(*measure)
4123 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4124 .voices
4125 .get_mut(*voice)
4126 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4127 if *note_index >= v.len() {
4128 return Err(Error::InvalidPatch(format!(
4129 "note_index {note_index} out of range"
4130 )));
4131 }
4132 v.remove(*note_index);
4133 }
4134 ScorePatch::ReplaceNote {
4135 part,
4136 staff,
4137 measure,
4138 voice,
4139 note_index,
4140 note,
4141 } => {
4142 let v = s
4143 .parts
4144 .get_mut(*part)
4145 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4146 .staves
4147 .get_mut(*staff)
4148 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4149 .measures
4150 .get_mut(*measure)
4151 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4152 .voices
4153 .get_mut(*voice)
4154 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
4155 if *note_index >= v.len() {
4156 return Err(Error::InvalidPatch(format!(
4157 "note_index {note_index} out of range"
4158 )));
4159 }
4160 v[*note_index] = *note.clone();
4161 }
4162 ScorePatch::SetMeasureTempo {
4163 part,
4164 staff,
4165 measure,
4166 value,
4167 } => {
4168 s.parts
4169 .get_mut(*part)
4170 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4171 .staves
4172 .get_mut(*staff)
4173 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4174 .measures
4175 .get_mut(*measure)
4176 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4177 .tempo = *value;
4178 }
4179 ScorePatch::SetMeasureTempoRamp {
4180 part,
4181 staff,
4182 measure,
4183 value,
4184 } => {
4185 s.parts
4186 .get_mut(*part)
4187 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4188 .staves
4189 .get_mut(*staff)
4190 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4191 .measures
4192 .get_mut(*measure)
4193 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4194 .tempo_ramp_to = *value;
4195 }
4196 }
4197 }
4198 if !super::validate::validate(&s).is_valid() {
4199 return Err(Error::InvalidScore);
4200 }
4201 Ok(s)
4202}
4203
4204pub fn voice_duration_beats(voice: &[Note], measure_beats: f64) -> f64 {
4208 match voice {
4209 [only] if only.is_plain_whole_rest() => measure_beats,
4210 _ => voice.iter().map(Note::beats).sum(),
4211 }
4212}
4213
4214pub fn respell_score(score: &mut Score, prefer_flat: bool) {
4218 for part in &mut score.parts {
4219 for staff in &mut part.staves {
4220 for measure in &mut staff.measures {
4221 for voice in &mut measure.voices {
4222 for note in voice.iter_mut() {
4223 for pitch in &mut note.pitches {
4224 *pitch = pitch.respell(prefer_flat);
4225 }
4226 }
4227 }
4228 }
4229 }
4230 }
4231}
4232
4233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4235#[serde(rename_all = "snake_case")]
4236pub enum RespellPolicy {
4237 Flat,
4239 Sharp,
4241 Key,
4243}
4244
4245pub fn respell_staff_region(
4256 score: &mut Score,
4257 part_index: usize,
4258 staff_index: usize,
4259 start_measure: usize,
4260 end_measure: usize,
4261 policy: RespellPolicy,
4262) -> Result<(usize, usize), Error> {
4263 let score_key_flat = score.settings.key_signature.fifths < 0;
4264 let part = score
4265 .parts
4266 .get_mut(part_index)
4267 .ok_or(Error::PartNotFound(part_index))?;
4268 let staff_count = part.staves.len();
4269 if staff_index >= staff_count {
4270 return Err(Error::StaffNotFound(staff_index));
4271 }
4272 let measure_count = part.staves[staff_index].measures.len();
4273 if start_measure >= end_measure || end_measure > measure_count {
4274 return Err(Error::InvalidCommand(format!(
4275 "invalid measure range {start_measure}..{end_measure}"
4276 )));
4277 }
4278 let mut prefer_flat = Vec::with_capacity(end_measure);
4279 let mut running_flat = score_key_flat;
4280 for index in 0..end_measure {
4281 let key = part.staves[staff_index].measures[index]
4282 .key_sig
4283 .as_ref()
4284 .or_else(|| {
4285 part.staves[0]
4286 .measures
4287 .get(index)
4288 .and_then(|measure| measure.key_sig.as_ref())
4289 });
4290 if let Some(key) = key {
4291 running_flat = key.fifths < 0;
4292 }
4293 prefer_flat.push(match policy {
4294 RespellPolicy::Flat => true,
4295 RespellPolicy::Sharp => false,
4296 RespellPolicy::Key => running_flat,
4297 });
4298 }
4299
4300 let measures = &mut part.staves[staff_index].measures;
4301 for (index, measure) in measures
4302 .iter_mut()
4303 .enumerate()
4304 .take(end_measure)
4305 .skip(start_measure)
4306 {
4307 for voice in &mut measure.voices {
4308 for note in voice
4309 .iter_mut()
4310 .filter(|note| !note.is_rest && !note.is_unpitched)
4311 {
4312 for pitch in &mut note.pitches {
4313 *pitch = pitch.respell(prefer_flat[index]);
4314 }
4315 }
4316 }
4317 }
4318
4319 let mut changed = (start_measure, end_measure - 1);
4320 for voice in 0..4 {
4321 for measure in start_measure..end_measure {
4322 for note in 0..measures[measure].voices[voice].len() {
4323 if measures[measure].voices[voice][note].tie_start {
4324 let last = propagate_tied_spelling(measures, voice, (measure, note), true);
4325 changed.1 = changed.1.max(last);
4326 }
4327 }
4328 }
4329 if !measures[start_measure].voices[voice].is_empty() {
4330 let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
4331 changed.0 = changed.0.min(first);
4332 }
4333 }
4334 Ok(changed)
4335}
4336
4337fn propagate_tied_spelling(
4340 measures: &mut [Measure],
4341 voice: usize,
4342 from: (usize, usize),
4343 forward: bool,
4344) -> usize {
4345 let mut current = from;
4346 loop {
4347 let note = &measures[current.0].voices[voice][current.1];
4348 if !(if forward {
4349 note.tie_start
4350 } else {
4351 note.tie_end
4352 }) {
4353 return current.0;
4354 }
4355 let neighbor = if forward {
4356 if current.1 + 1 < measures[current.0].voices[voice].len() {
4357 Some((current.0, current.1 + 1))
4358 } else {
4359 measures
4360 .get(current.0 + 1)
4361 .filter(|measure| !measure.voices[voice].is_empty())
4362 .map(|_| (current.0 + 1, 0))
4363 }
4364 } else if current.1 > 0 {
4365 Some((current.0, current.1 - 1))
4366 } else {
4367 current.0.checked_sub(1).and_then(|previous| {
4368 measures[previous].voices[voice]
4369 .len()
4370 .checked_sub(1)
4371 .map(|last| (previous, last))
4372 })
4373 };
4374 let Some(next) = neighbor else {
4375 return current.0;
4376 };
4377 let next_note = &measures[next.0].voices[voice][next.1];
4378 if !(if forward {
4379 next_note.tie_end
4380 } else {
4381 next_note.tie_start
4382 }) {
4383 return current.0;
4384 }
4385 let source = measures[current.0].voices[voice][current.1].pitches.clone();
4386 for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
4387 if let Some(spelled) = source.iter().find(|candidate| {
4388 candidate.to_midi() == pitch.to_midi()
4389 && candidate.microtone_cents == pitch.microtone_cents
4390 }) {
4391 *pitch = spelled.clone();
4392 }
4393 }
4394 current = next;
4395 }
4396}
4397
4398pub fn respell_score_to_key(score: &mut Score) {
4402 let prefer_flat = score.settings.key_signature.fifths < 0;
4403 respell_score(score, prefer_flat);
4404}
4405
4406pub fn score_duration_secs(score: &Score) -> f64 {
4411 if score.settings.tempo_bpm == 0 {
4412 return 0.0;
4413 }
4414 let seq = measure_sequence(score);
4415 let mut total_secs = 0.0f64;
4416 let mut current_bpm = score.settings.tempo_bpm as f64;
4417 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4418 for &idx in &seq {
4419 if let Some(m) = staff.measures.get(idx) {
4420 if let Some(b) = m.tempo {
4421 current_bpm = b as f64;
4422 }
4423 if current_bpm == 0.0 {
4424 continue;
4425 }
4426 let beats = voice_duration_beats(
4427 &m.voices[0],
4428 staff.measure_beats(idx, &score.settings.time_signature),
4429 );
4430 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4431 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4432 current_bpm = f64::from(target);
4433 }
4434 }
4435 }
4436 }
4437 total_secs
4438}
4439
4440pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
4445 if score.settings.tempo_bpm == 0 {
4446 return 0.0;
4447 }
4448 let seq: Vec<usize> = measure_sequence(score)
4449 .into_iter()
4450 .filter(|&idx| idx >= region.0 && idx <= region.1)
4451 .collect();
4452 let mut total_secs = 0.0f64;
4453 let mut current_bpm = score.settings.tempo_bpm as f64;
4454 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4455 for &idx in &seq {
4456 if let Some(m) = staff.measures.get(idx) {
4457 if let Some(b) = m.tempo {
4458 current_bpm = b as f64;
4459 }
4460 if current_bpm == 0.0 {
4461 continue;
4462 }
4463 let beats = voice_duration_beats(
4464 &m.voices[0],
4465 staff.measure_beats(idx, &score.settings.time_signature),
4466 );
4467 total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4468 if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4469 current_bpm = f64::from(target);
4470 }
4471 }
4472 }
4473 }
4474 total_secs
4475}
4476
4477fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
4478 let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
4479 return beats / start_bpm * 60.0;
4480 };
4481 let end_bpm = f64::from(target_bpm);
4482 let delta = end_bpm - start_bpm;
4483 if delta.abs() < f64::EPSILON {
4484 return beats / start_bpm * 60.0;
4485 }
4486 60.0 * beats / delta * (end_bpm / start_bpm).ln()
4487}
4488
4489pub fn measure_beats_remaining(
4494 score: &Score,
4495 part_index: usize,
4496 staff_index: usize,
4497 measure_index: usize,
4498 voice_index: usize,
4499) -> Result<f64, Error> {
4500 let part = score
4501 .parts
4502 .get(part_index)
4503 .ok_or(Error::PartNotFound(part_index))?;
4504 let staff = part
4505 .staves
4506 .get(staff_index)
4507 .ok_or(Error::StaffNotFound(staff_index))?;
4508 let measure = staff
4509 .measures
4510 .get(measure_index)
4511 .ok_or(Error::MeasureNotFound(measure_index))?;
4512 let voice = measure
4513 .voices
4514 .get(voice_index)
4515 .ok_or(Error::VoiceOutOfRange(voice_index))?;
4516 let capacity = staff.measure_beats(measure_index, &score.settings.time_signature);
4517 Ok((capacity - voice_duration_beats(voice, capacity)).max(0.0))
4518}
4519
4520pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
4526 if pitches.is_empty() {
4527 return true;
4528 }
4529 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
4530 avg < clef.middle_line_midi() as f64
4531}
4532
4533fn beam_beat_size(ts: &TimeSignature) -> f64 {
4534 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
4535 3.0 * 4.0 / ts.denominator as f64
4536 } else {
4537 4.0 / ts.denominator as f64
4538 }
4539}
4540
4541pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
4546 let beat_size = beam_beat_size(time_sig);
4547 let n = notes.len();
4548 let mut result = vec![BeamState::None; n];
4549
4550 let is_beamable = |note: &Note| -> bool {
4551 !note.is_rest
4552 && matches!(
4553 note.duration,
4554 Duration::Eighth
4555 | Duration::Sixteenth
4556 | Duration::ThirtySecond
4557 | Duration::SixtyFourth
4558 )
4559 };
4560
4561 let mut starts = Vec::with_capacity(n);
4563 let mut pos = 0.0f64;
4564 for note in notes {
4565 starts.push(pos);
4566 pos += note.beats();
4567 }
4568
4569 let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
4571
4572 let mut i = 0;
4573 while i < n {
4574 if !is_beamable(¬es[i]) {
4575 i += 1;
4576 continue;
4577 }
4578 let g = group_id(i);
4579 let mut j = i;
4581 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
4582 j += 1;
4583 }
4584 let run = j - i;
4585 if run == 1 {
4586 result[i] = BeamState::None;
4587 } else {
4588 result[i] = BeamState::Begin;
4589 result[i + 1..j - 1].fill(BeamState::Continue);
4590 result[j - 1] = BeamState::End;
4591 }
4592 i = j;
4593 }
4594 result
4595}
4596
4597fn note_content_eq(a: &Note, b: &Note) -> bool {
4598 a.is_rest == b.is_rest
4599 && a.is_unpitched == b.is_unpitched
4600 && a.instrument_id == b.instrument_id
4601 && a.offset_x == b.offset_x
4602 && a.offset_y == b.offset_y
4603 && a.relative_x == b.relative_x
4604 && a.relative_y == b.relative_y
4605 && a.pitches == b.pitches
4606 && a.duration == b.duration
4607 && a.dot_count == b.dot_count
4608 && a.tie_start == b.tie_start
4609 && a.tie_end == b.tie_end
4610 && a.beam == b.beam
4611 && a.articulations == b.articulations
4612 && a.dynamic == b.dynamic
4613 && a.stem_up == b.stem_up
4614 && a.hairpin_start == b.hairpin_start
4615 && a.hairpin_end == b.hairpin_end
4616 && a.tuplet == b.tuplet
4617 && a.chord_symbol == b.chord_symbol
4618 && a.is_grace == b.is_grace
4619 && a.grace_slash == b.grace_slash
4620 && a.ottava_start == b.ottava_start
4621 && a.ottava_end == b.ottava_end
4622 && a.lyric == b.lyric
4623 && a.additional_lyrics == b.additional_lyrics
4624 && a.pedal_start == b.pedal_start
4625 && a.pedal_end == b.pedal_end
4626 && a.slur_start == b.slur_start
4627 && a.slur_end == b.slur_end
4628 && a.arpeggiate == b.arpeggiate
4629 && a.tab_position == b.tab_position
4630 && a.tab_positions == b.tab_positions
4631 && a.guitar_technique == b.guitar_technique
4632 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
4633 && a.guitar_bend_curve == b.guitar_bend_curve
4634}
4635
4636fn per_pitch_flag(flags: &[bool], pitch_count: usize, index: usize, chord: bool) -> bool {
4637 if flags.len() == pitch_count {
4638 flags.get(index).copied().unwrap_or(chord)
4639 } else {
4640 chord
4641 }
4642}
4643
4644#[cfg(test)]
4645mod tests {
4646 use super::*;
4647 use crate::model::{
4648 notation::{FingeringSelectionPolicy, TextStyle},
4649 pitch::Step,
4650 };
4651
4652 #[test]
4653 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
4654 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4655 note.fingerings = vec![3, 1, 4];
4656 note.fingering = Some(3);
4657 assert_eq!(
4658 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
4659 Some(3)
4660 );
4661 assert_eq!(
4662 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
4663 Some(1)
4664 );
4665 assert_eq!(
4666 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
4667 Some(4)
4668 );
4669 assert_eq!(note.fingerings, vec![3, 1, 4]);
4670 assert_eq!(note.fingering, Some(3));
4671 }
4672
4673 #[test]
4674 fn default_score_has_one_part_four_measures() {
4675 let score = Score::default();
4676 assert_eq!(score.parts.len(), 1);
4677 assert_eq!(score.parts[0].staves.len(), 1);
4678 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
4679 }
4680
4681 #[test]
4682 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
4683 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4684 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4685 lines: 6,
4686 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4687 capo: 2,
4688 });
4689 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
4690 Pitch::with_alter(Step::F, 4, 1),
4691 Duration::Quarter,
4692 ));
4693 score.parts[0].staves[0].measures[0].voices[0]
4694 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
4695 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
4696 Some(TabPosition { string: 6, fret: 7 });
4697
4698 assert_eq!(assign_tablature_positions(&mut score), 1);
4699 let notes = &score.parts[0].staves[0].measures[0].voices[0];
4700 assert_eq!(
4701 notes[1].tab_position,
4702 Some(TabPosition { string: 1, fret: 0 })
4703 );
4704 assert_eq!(notes[1].string_number, Some(1));
4705 assert_eq!(
4706 notes[2].tab_position,
4707 Some(TabPosition { string: 6, fret: 7 })
4708 );
4709 }
4710
4711 #[test]
4712 fn assign_tablature_positions_uses_measure_local_capo_change() {
4713 let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
4714 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4715 lines: 6,
4716 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4717 capo: 0,
4718 });
4719 score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
4720 lines: 6,
4721 tuning_midi: vec![40, 45, 50, 55, 59, 64],
4722 capo: 2,
4723 });
4724 score.parts[0].staves[0].measures[1].voices[0]
4725 .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
4726
4727 assert_eq!(assign_tablature_positions(&mut score), 1);
4728 assert_eq!(
4729 score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
4730 Some(TabPosition { string: 5, fret: 3 })
4731 );
4732 }
4733
4734 #[test]
4735 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
4736 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4737 score.parts[0].staves[0].tablature = Some(TablatureConfig {
4738 lines: 6,
4739 tuning_midi: vec![64, 59, 55, 50, 45, 40],
4740 capo: 0,
4741 });
4742 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4743 chord.pitches.push(Pitch::new(Step::G, 4));
4744 score.parts[0].staves[0].measures[0].voices[0].push(chord);
4745
4746 assert_eq!(assign_tablature_positions(&mut score), 1);
4747 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
4748 assert_eq!(
4749 positions,
4750 &vec![
4751 TabPosition { string: 2, fret: 5 },
4752 TabPosition { string: 1, fret: 3 },
4753 ]
4754 );
4755 }
4756
4757 #[test]
4758 fn new_score_measure_count() {
4759 let score = Score::new("Test", 120, 4, 4, 0, 8);
4760 assert_eq!(score.measure_count(), 8);
4761 }
4762
4763 #[test]
4764 fn note_beats_quarter() {
4765 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4766 assert!((note.beats() - 1.0).abs() < 1e-9);
4767 }
4768
4769 #[test]
4770 fn note_beats_dotted_quarter() {
4771 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4772 note.dot_count = 1;
4773 assert!((note.beats() - 1.5).abs() < 1e-9);
4774 }
4775
4776 #[test]
4777 fn grace_note_beats_zero() {
4778 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
4779 note.is_grace = true;
4780 assert_eq!(note.beats(), 0.0);
4781 }
4782
4783 #[test]
4784 fn measure_empty_4_4_fills_four_beats() {
4785 let m = Measure::empty(4, 4);
4786 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4787 assert!((total - 4.0).abs() < 1e-9);
4788 }
4789
4790 #[test]
4791 fn measure_empty_3_4_fills_three_beats() {
4792 let m = Measure::empty(3, 4);
4793 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4794 assert!((total - 3.0).abs() < 1e-9);
4795 }
4796
4797 #[test]
4798 fn whole_filling_beats() {
4799 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
4800 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
4801 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
4802 }
4803
4804 #[test]
4807 fn statistics_default_score_all_rests() {
4808 let score = Score::default();
4809 let s = score.statistics();
4810 assert_eq!(s.part_count, 1);
4811 assert_eq!(s.measure_count, 4);
4812 assert_eq!(s.note_count, 0);
4813 assert!(s.rest_count > 0);
4814 }
4815
4816 #[test]
4817 fn statistics_duration_estimate() {
4818 let score = Score::new("T", 120, 4, 4, 0, 1);
4820 let s = score.statistics();
4821 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
4822 }
4823
4824 #[test]
4825 fn score_duration_secs_matches_statistics() {
4826 use super::score_duration_secs;
4827 let score = Score::new("T", 120, 4, 4, 0, 4);
4828 let secs = score_duration_secs(&score);
4829 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
4831 }
4832
4833 #[test]
4834 fn score_duration_secs_integrates_measure_tempo_ramp() {
4835 use super::score_duration_secs;
4836 let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
4837 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4838 let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
4839 assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
4840 }
4841
4842 #[test]
4843 fn score_duration_secs_zero_bpm_returns_zero() {
4844 use super::score_duration_secs;
4845 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4846 score.settings.tempo_bpm = 0;
4847 assert_eq!(score_duration_secs(&score), 0.0);
4848 }
4849
4850 #[test]
4851 fn score_duration_secs_per_measure_tempo() {
4852 use super::score_duration_secs;
4853 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4855 score.parts[0].staves[0].measures[1].tempo = Some(60);
4856 let secs = score_duration_secs(&score);
4857 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
4858 }
4859
4860 #[test]
4863 fn extract_part_returns_single_part_score() {
4864 let mut score = Score::default();
4865 let mut p2 = Part::new("Violin", "Vln.");
4866 p2.staves.push(Staff::new(Clef::Treble));
4867 score.parts.push(p2);
4868 let ex = score.extract_part(0).unwrap();
4869 assert_eq!(ex.parts.len(), 1);
4870 assert_ne!(ex.id, score.id);
4871 assert_eq!(ex.metadata.title, score.metadata.title);
4872 }
4873
4874 #[test]
4875 fn extract_part_out_of_range_is_none() {
4876 let score = Score::default();
4877 assert!(score.extract_part(99).is_none());
4878 }
4879
4880 #[test]
4881 fn extract_and_merge_remap_typed_spanner_part_addresses() {
4882 let mut left = Score::template(ScoreTemplate::StringQuartet);
4883 let address = NoteAddr {
4884 part: 1,
4885 staff: 0,
4886 measure: 0,
4887 voice: 0,
4888 note: 0,
4889 };
4890 left.spanners.push(NotationSpanner {
4891 id: "left-span".to_string(),
4892 kind: NotationSpannerKind::Slur,
4893 start: address.clone(),
4894 end: address,
4895 number: Some(1),
4896 line_type: None,
4897 text: None,
4898 placement: None,
4899 ottava_size: None,
4900 ottava_type: None,
4901 });
4902 let extracted = left.extract_part_checked(1).expect("valid extracted part");
4903 assert_eq!(extracted.spanners[0].start.part, 0);
4904 assert_eq!(extracted.spanners[0].end.part, 0);
4905
4906 let mut right = Score::new("R", 120, 4, 4, 0, 1);
4907 let right_address = NoteAddr {
4908 part: 0,
4909 staff: 0,
4910 measure: 0,
4911 voice: 0,
4912 note: 0,
4913 };
4914 right.spanners.push(NotationSpanner {
4915 id: "right-span".to_string(),
4916 kind: NotationSpannerKind::Pedal,
4917 start: right_address.clone(),
4918 end: right_address,
4919 number: Some(1),
4920 line_type: None,
4921 text: None,
4922 placement: None,
4923 ottava_size: None,
4924 ottava_type: None,
4925 });
4926 let merged = left.merge_checked(&right).expect("valid merged score");
4927 let right_span = merged
4928 .spanners
4929 .iter()
4930 .find(|spanner| spanner.id == "right-span")
4931 .expect("merged right span");
4932 assert_eq!(right_span.start.part, left.parts.len());
4933 assert_eq!(right_span.end.part, left.parts.len());
4934 }
4935
4936 #[test]
4939 fn transpose_zero_is_clone() {
4940 let score = Score::new("T", 120, 4, 4, 0, 1);
4941 let t = transpose(&score, 0);
4942 assert_eq!(t.settings.key_signature.fifths, 0);
4943 }
4944
4945 #[test]
4946 fn transpose_staff_region_rewrites_only_selected_written_measures() {
4947 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4948 for measure in &mut score.parts[0].staves[0].measures {
4949 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4950 }
4951 let transformed = transpose_staff_region_checked(
4952 &score,
4953 0,
4954 0,
4955 1,
4956 2,
4957 2,
4958 RegionalTranspositionTarget::Written,
4959 )
4960 .expect("region should transpose");
4961 assert_eq!(
4962 transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
4963 Pitch::new(Step::C, 4)
4964 );
4965 assert_eq!(
4966 transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4967 Pitch::new(Step::D, 4)
4968 );
4969 assert_eq!(
4970 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4971 Pitch::new(Step::C, 4)
4972 );
4973 }
4974
4975 #[test]
4976 fn regional_concert_transposition_requires_full_staff() {
4977 let score = Score::new("T", 120, 4, 4, 0, 2);
4978 assert!(
4979 transpose_staff_region_checked(
4980 &score,
4981 0,
4982 0,
4983 1,
4984 2,
4985 -2,
4986 RegionalTranspositionTarget::Concert,
4987 )
4988 .is_err()
4989 );
4990 }
4991
4992 #[test]
4993 fn transpose_c_major_up_2_to_d_major() {
4994 let score = Score::new("T", 120, 4, 4, 0, 1);
4995 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
4996 }
4997
4998 #[test]
4999 fn transpose_d_major_up_5_to_g_major() {
5000 let score = Score::new("T", 120, 4, 4, 2, 1);
5001 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
5002 }
5003
5004 #[test]
5005 fn transpose_c4_up_1_to_csharp4() {
5006 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
5007 assert_eq!(p.to_midi(), 61);
5008 assert_eq!(p.step, Step::C);
5009 assert_eq!(p.alter, 1);
5010 }
5011
5012 #[test]
5013 fn transpose_c4_down_1_to_b3() {
5014 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
5015 assert_eq!(p.to_midi(), 59);
5016 assert_eq!(p.step, Step::B);
5017 assert_eq!(p.alter, 0);
5018 }
5019
5020 #[test]
5021 fn transpose_up_octave_keeps_step() {
5022 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
5023 assert_eq!(p.to_midi(), 81);
5024 assert_eq!(p.step, Step::A);
5025 assert_eq!(p.octave, 5);
5026 }
5027
5028 #[test]
5029 fn statistics_with_repeat_doubles_duration() {
5030 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5032 score.parts[0].staves[0].measures[0].barline_left =
5033 crate::model::notation::Barline::RepeatStart;
5034 score.parts[0].staves[0].measures[1].barline_right =
5035 crate::model::notation::Barline::RepeatEnd;
5036 let s = score.statistics();
5037 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
5039 }
5040
5041 #[test]
5042 fn transpose_octave_boundary_b4_to_c5() {
5043 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
5045 assert_eq!(p.to_midi(), 72);
5046 assert_eq!(p.step, Step::C);
5047 assert_eq!(p.octave, 5);
5048 }
5049
5050 #[test]
5051 fn transpose_clamp_at_midi_127() {
5052 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
5054 assert_eq!(p.to_midi(), 127);
5055 }
5056
5057 #[test]
5060 fn merge_combines_parts() {
5061 let mut a = Score::new("A", 120, 4, 4, 0, 2);
5062 let b = Score::new("B", 120, 4, 4, 0, 2);
5063 let mut p2 = Part::new("Violin", "Vln.");
5065 p2.staves.push(Staff::new(Clef::Treble));
5066 for i in 0..2usize {
5067 let mut m = Measure::empty(4, 4);
5068 m.number = i as u32 + 1;
5069 p2.staves[0].measures.push(m);
5070 }
5071 a.parts.push(p2);
5072 let merged = a.merge(&b);
5073 assert_eq!(merged.parts.len(), 3);
5075 }
5076
5077 #[test]
5078 fn merge_pads_shorter_score() {
5079 let a = Score::new("A", 120, 4, 4, 0, 4);
5080 let b = Score::new("B", 120, 4, 4, 0, 2);
5081 let merged = a.merge(&b);
5082 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
5084 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
5085 }
5086
5087 #[test]
5088 fn merge_uses_self_metadata() {
5089 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
5090 a.metadata.composer = "Composer A".to_string();
5091 let b = Score::new("Title B", 120, 4, 4, 0, 2);
5092 let merged = a.merge(&b);
5093 assert_eq!(merged.metadata.title, "Title A");
5094 assert_eq!(merged.metadata.composer, "Composer A");
5095 }
5096
5097 #[test]
5098 fn merge_new_id_differs_from_both() {
5099 let a = Score::new("A", 120, 4, 4, 0, 2);
5100 let b = Score::new("B", 120, 4, 4, 0, 2);
5101 let merged = a.merge(&b);
5102 assert_ne!(merged.id, a.id);
5103 assert_ne!(merged.id, b.id);
5104 }
5105
5106 #[test]
5109 fn staff_default_transpose_is_zero() {
5110 let s = Staff::new(Clef::Treble);
5111 assert_eq!(s.transpose_semitones, 0);
5112 }
5113
5114 #[test]
5115 fn staff_presentation_defaults_and_tracks_percussion_clef() {
5116 let standard = Staff::new(Clef::Treble);
5117 assert_eq!(standard.presentation, StaffPresentation::default());
5118
5119 let percussion = Staff::new(Clef::Percussion);
5120 assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
5121 assert_eq!(percussion.presentation.lines, 5);
5122 assert!(percussion.presentation.visible);
5123 }
5124
5125 #[test]
5128 fn score_default_has_schema_version_1() {
5129 let score = Score::default();
5130 assert_eq!(score.schema_version, 1);
5131 }
5132
5133 #[test]
5134 fn score_new_has_schema_version_1() {
5135 let score = Score::new("T", 120, 4, 4, 0, 4);
5136 assert_eq!(score.schema_version, 1);
5137 }
5138
5139 #[test]
5140 fn score_without_schema_version_deserializes_to_zero() {
5141 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":[]}"#;
5142 let score: Score = serde_json::from_str(json).unwrap();
5143 assert_eq!(score.schema_version, 0);
5144 }
5145
5146 #[test]
5147 fn legacy_score_json_defaults_typed_spanners() {
5148 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5149 let mut value = serde_json::to_value(score).expect("score serializes");
5150 value
5151 .as_object_mut()
5152 .expect("score is an object")
5153 .remove("spanners");
5154 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5155 assert!(restored.spanners.is_empty());
5156 }
5157
5158 #[test]
5159 fn legacy_staff_json_defaults_presentation() {
5160 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5161 let mut value = serde_json::to_value(score).expect("score serializes");
5162 value["parts"][0]["staves"][0]
5163 .as_object_mut()
5164 .expect("staff is an object")
5165 .remove("presentation");
5166
5167 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5168 assert_eq!(
5169 restored.parts[0].staves[0].presentation,
5170 StaffPresentation::default()
5171 );
5172 }
5173
5174 #[test]
5175 fn legacy_part_json_defaults_instrument_definition() {
5176 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5177 let mut value = serde_json::to_value(score).expect("score serializes");
5178 value["parts"][0]
5179 .as_object_mut()
5180 .expect("part is an object")
5181 .remove("instrument");
5182
5183 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5184 assert!(restored.parts[0].instrument.is_none());
5185 }
5186
5187 #[test]
5188 fn resolve_view_projects_linked_part_without_mutating_source_score() {
5189 let mut score = Score::template(ScoreTemplate::StringQuartet);
5190 let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
5191 view.layout.measures_per_row = Some(2);
5192 score.views.push(view);
5193
5194 let projected = score.resolve_view("violin-2").expect("view resolves");
5195 assert_eq!(projected.parts.len(), 1);
5196 assert_eq!(projected.parts[0].name, "Violin II");
5197 assert_eq!(projected.views.len(), 1);
5198 assert_eq!(projected.views[0].parts, vec![0]);
5199 assert_eq!(score.parts.len(), 4);
5200 assert_eq!(score.views[0].parts, vec![1]);
5201 }
5202
5203 #[test]
5204 fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
5205 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5206 score.parts[0].staves[0].tablature = Some(TablatureConfig {
5207 lines: 6,
5208 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5209 capo: 0,
5210 });
5211 score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
5212 score.views.push(ScoreView::linked_tablature_staff(
5213 "guitar-tab",
5214 "Guitar Tab",
5215 0,
5216 0,
5217 ));
5218
5219 let projected = score.resolve_view("guitar-tab").expect("view resolves");
5220 assert_eq!(
5221 projected.parts[0].staves[0].presentation.kind,
5222 StaffKind::Tablature
5223 );
5224 assert_eq!(
5225 projected.views[0].staff_kind_overrides[0].staff,
5226 ViewStaffRef { part: 0, staff: 0 }
5227 );
5228 assert_eq!(
5229 score.parts[0].staves[0].presentation.kind,
5230 StaffKind::Standard
5231 );
5232 }
5233
5234 #[test]
5235 fn legacy_score_view_json_defaults_staff_kind_overrides() {
5236 let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
5237 score.views.push(ScoreView::linked_part("part", "Part", 0));
5238 let mut value = serde_json::to_value(score).expect("score serializes");
5239 value["views"][0]
5240 .as_object_mut()
5241 .expect("view is an object")
5242 .remove("staff_kind_overrides");
5243
5244 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5245 assert!(restored.views[0].staff_kind_overrides.is_empty());
5246 }
5247
5248 #[test]
5249 fn typed_view_style_overrides_are_ordered_and_json_compatible() {
5250 let mut view = ScoreView::linked_part("part", "Part", 0);
5251 view.layout.typed_style_overrides = vec![
5252 ViewStyleOverride {
5253 property: ViewStyleProperty::TextScale,
5254 value: 0.9,
5255 },
5256 ViewStyleOverride {
5257 property: ViewStyleProperty::TextScale,
5258 value: 1.1,
5259 },
5260 ];
5261 assert_eq!(
5262 view.layout.style_value(ViewStyleProperty::TextScale),
5263 Some(1.1)
5264 );
5265 let restored: ScoreView =
5266 serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
5267 assert_eq!(
5268 restored.layout.typed_style_overrides,
5269 view.layout.typed_style_overrides
5270 );
5271 assert_eq!(view.layout.resolved_style().text_scale, 1.1);
5272 assert_eq!(view.layout.resolved_style().system_gap, 2.0);
5273 }
5274
5275 #[test]
5276 fn score_style_defaults_are_inherited_then_overridden_by_view() {
5277 let mut score = Score::new("Style", 120, 4, 4, 0, 1);
5278 score.style_overrides = vec![
5279 ViewStyleOverride {
5280 property: ViewStyleProperty::StaffSpace,
5281 value: 1.2,
5282 },
5283 ViewStyleOverride {
5284 property: ViewStyleProperty::TextScale,
5285 value: 0.9,
5286 },
5287 ];
5288 let mut view = ScoreView::linked_part("part", "Part", 0);
5289 view.layout.typed_style_overrides.push(ViewStyleOverride {
5290 property: ViewStyleProperty::TextScale,
5291 value: 1.1,
5292 });
5293 let style = score.resolved_view_style(&view.layout);
5294 assert_eq!(style.staff_space, 1.2);
5295 assert_eq!(style.text_scale, 1.1);
5296 let restored: Score =
5297 serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
5298 assert_eq!(restored.style_overrides, score.style_overrides);
5299 }
5300
5301 #[test]
5302 fn legacy_measure_json_defaults_source_voice_numbers() {
5303 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5304 let mut value = serde_json::to_value(&score).expect("score serializes");
5305 value["parts"][0]["staves"][0]["measures"][0]
5306 .as_object_mut()
5307 .expect("measure is an object")
5308 .remove("source_voice_numbers");
5309
5310 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5311 assert_eq!(
5312 restored.parts[0].staves[0].measures[0].source_voice_numbers,
5313 [None; 4]
5314 );
5315 }
5316
5317 #[test]
5318 fn legacy_measure_json_defaults_tempo_ramp() {
5319 let measure: Measure = serde_json::from_str(
5320 r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
5321 )
5322 .expect("legacy measure deserializes");
5323 assert_eq!(measure.tempo_ramp_to, None);
5324 }
5325
5326 #[test]
5327 fn note_without_new_percussion_fields_uses_serde_defaults() {
5328 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5329 let mut value = serde_json::to_value(note).unwrap();
5330 let object = value.as_object_mut().unwrap();
5331 object.remove("is_unpitched");
5332 object.remove("instrument_id");
5333 let restored: Note = serde_json::from_value(value).unwrap();
5334 assert!(!restored.is_unpitched);
5335 assert_eq!(restored.instrument_id, None);
5336 }
5337
5338 #[test]
5339 fn percussion_instrument_resolution_prefers_id_then_display_key() {
5340 let mut part = Part::new("Drums", "Dr.");
5341 part.percussion_instruments = vec![
5342 PercussionInstrument {
5343 id: "snare".to_string(),
5344 name: Some("Acoustic Snare".to_string()),
5345 midi_unpitched: Some(38),
5346 staff_position: None,
5347 notehead: None,
5348 preferred_voice: None,
5349 techniques: Vec::new(),
5350 },
5351 PercussionInstrument {
5352 id: "rim".to_string(),
5353 name: Some("Side Stick".to_string()),
5354 midi_unpitched: Some(37),
5355 staff_position: None,
5356 notehead: None,
5357 preferred_voice: None,
5358 techniques: Vec::new(),
5359 },
5360 ];
5361 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
5362 note.is_unpitched = true;
5363 assert_eq!(
5364 part.percussion_instrument_for_note(¬e)
5365 .map(|instrument| instrument.id.as_str()),
5366 Some("snare")
5367 );
5368 note.instrument_id = Some("rim".to_string());
5369 assert_eq!(
5370 part.percussion_instrument_for_note(¬e)
5371 .map(|instrument| instrument.id.as_str()),
5372 Some("rim")
5373 );
5374 note.instrument_id = Some("missing".to_string());
5375 assert!(part.percussion_instrument_for_note(¬e).is_none());
5376 note.instrument_id = None;
5377 note.is_unpitched = false;
5378 assert!(part.percussion_instrument_for_note(¬e).is_none());
5379 }
5380
5381 #[test]
5382 fn percussion_kit_extensions_are_json_backward_compatible() {
5383 let mut instrument = PercussionInstrument {
5384 id: "snare".to_string(),
5385 name: Some("Acoustic Snare".to_string()),
5386 midi_unpitched: Some(38),
5387 staff_position: Some(0),
5388 notehead: Some(NoteHead::Cross),
5389 preferred_voice: Some(1),
5390 techniques: vec!["rim-shot".to_string()],
5391 };
5392 let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
5393 let object = value
5394 .as_object_mut()
5395 .expect("percussion instrument is an object");
5396 object.remove("staff_position");
5397 object.remove("notehead");
5398 object.remove("preferred_voice");
5399 object.remove("techniques");
5400 instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
5401 assert_eq!(instrument.staff_position, None);
5402 assert_eq!(instrument.notehead, None);
5403 assert_eq!(instrument.preferred_voice, None);
5404 assert!(instrument.techniques.is_empty());
5405 }
5406
5407 #[test]
5410 fn score_template_solo_has_one_part_treble() {
5411 let score = Score::template(ScoreTemplate::Solo);
5412 assert_eq!(score.parts.len(), 1);
5413 assert_eq!(score.parts[0].staves.len(), 1);
5414 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5415 assert_eq!(score.parts[0].midi_program, 0);
5416 }
5417
5418 #[test]
5419 fn score_template_piano_has_two_staves() {
5420 let score = Score::template(ScoreTemplate::Piano);
5421 assert_eq!(score.parts.len(), 1);
5422 assert_eq!(score.parts[0].staves.len(), 2);
5423 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5424 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
5425 }
5426
5427 #[test]
5428 fn score_template_string_quartet_has_four_parts() {
5429 let score = Score::template(ScoreTemplate::StringQuartet);
5430 assert_eq!(score.parts.len(), 4);
5431 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);
5434 assert_eq!(score.parts[3].midi_program, 42);
5435 }
5436
5437 #[test]
5438 fn score_template_string_orchestra_has_five_parts() {
5439 let score = Score::template(ScoreTemplate::StringOrchestra);
5440 assert_eq!(score.parts.len(), 5);
5441 assert_eq!(score.parts[4].midi_program, 43); }
5443
5444 #[test]
5445 fn score_template_brass_quintet_has_five_parts() {
5446 let score = Score::template(ScoreTemplate::BrassQuintet);
5447 assert_eq!(score.parts.len(), 5);
5448 assert_eq!(score.parts[2].midi_program, 60); }
5450
5451 #[test]
5452 fn score_template_default_measures_are_four() {
5453 let score = Score::template(ScoreTemplate::StringQuartet);
5454 for part in &score.parts {
5455 for staff in &part.staves {
5456 assert_eq!(staff.measures.len(), 4);
5457 }
5458 }
5459 }
5460
5461 #[test]
5464 fn measure_empty_has_no_breaks() {
5465 let m = Measure::empty(4, 4);
5466 assert!(!m.system_break);
5467 assert!(!m.page_break);
5468 assert!(!m.section_break);
5469 }
5470
5471 #[test]
5472 fn section_range_uses_semantic_boundaries_not_layout_breaks() {
5473 let mut score = Score::new("sections", 120, 4, 4, 0, 6);
5474 score.parts[0].staves[0].measures[0].section_break = true;
5475 score.parts[0].staves[0].measures[2].section_break = true;
5476 score.parts[0].staves[0].measures[4].section_break = true;
5477 score.parts[0].staves[0].measures[5].section_break = true;
5478 score.parts[0].staves[0].measures[1].system_break = true;
5479 score.parts[0].staves[0].measures[3].page_break = true;
5480 assert_eq!(score.section_range(0).unwrap(), 0..=1);
5481 assert_eq!(score.section_range(3).unwrap(), 2..=3);
5482 assert_eq!(score.section_range(4).unwrap(), 4..=4);
5483 assert_eq!(score.section_range(5).unwrap(), 5..=5);
5484 assert!(score.section_range(6).is_err());
5485 }
5486
5487 #[test]
5488 fn system_break_survives_json_roundtrip() {
5489 let mut m = Measure::empty(4, 4);
5490 m.system_break = true;
5491 let json = serde_json::to_string(&m).unwrap();
5492 let m2: Measure = serde_json::from_str(&json).unwrap();
5493 assert!(m2.system_break);
5494 assert!(!m2.page_break);
5495 }
5496
5497 #[test]
5500 fn diff_identical_scores_is_empty() {
5501 let s = Score::new("T", 120, 4, 4, 0, 2);
5502 assert!(diff(&s, &s).is_empty());
5503 }
5504
5505 #[test]
5506 fn score_patch_covers_measure_semantics_and_note_insert_index() {
5507 let mut a = Score::new("T", 120, 4, 4, 0, 1);
5508 a.parts[0].staves[0].measures[0].voices[0].clear();
5509 let mut b = a.clone();
5510 let measure = &mut b.parts[0].staves[0].measures[0];
5511 measure.key_sig = Some(KeySignature {
5512 fifths: -2,
5513 mode: "major".to_string(),
5514 });
5515 measure.time_sig = Some(TimeSignature {
5516 numerator: 3,
5517 denominator: 4,
5518 });
5519 measure.barline_left = Barline::RepeatStart;
5520 measure.barline_right = Barline::RepeatEnd;
5521 measure.rehearsal = Some("A".to_string());
5522 measure.volta = Some(VoltaBracket {
5523 number: 1,
5524 kind: "begin_end".to_string(),
5525 });
5526 measure.texts.push(StyledText {
5527 style: TextStyle::RehearsalMark,
5528 text: "A".to_string(),
5529 placement: None,
5530 offset_x: None,
5531 offset_y: None,
5532 relative_x: None,
5533 relative_y: None,
5534 });
5535 measure.figured_bass.push(FiguredBassFigure {
5536 number: "6".to_string(),
5537 alter: None,
5538 prefix: None,
5539 suffix: None,
5540 extender: false,
5541 });
5542 measure.harp_pedal_diagrams.push(HarpPedalDiagram {
5543 positions: [
5544 HarpPedalPosition::Flat,
5545 HarpPedalPosition::Natural,
5546 HarpPedalPosition::Sharp,
5547 HarpPedalPosition::Natural,
5548 HarpPedalPosition::Flat,
5549 HarpPedalPosition::Sharp,
5550 HarpPedalPosition::Natural,
5551 ],
5552 placement: Some("above".to_string()),
5553 });
5554 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
5555 let expected = b.parts[0].staves[0].measures[0].clone();
5556
5557 let patches = score_patch(&a, &b);
5558 assert!(
5559 patches
5560 .iter()
5561 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
5562 );
5563 assert!(
5564 patches
5565 .iter()
5566 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
5567 );
5568 assert!(
5569 patches
5570 .iter()
5571 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
5572 );
5573 assert!(
5574 patches
5575 .iter()
5576 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
5577 );
5578 assert!(
5579 patches
5580 .iter()
5581 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
5582 );
5583 assert!(
5584 patches
5585 .iter()
5586 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
5587 );
5588 assert!(
5589 patches
5590 .iter()
5591 .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
5592 );
5593 let result = apply_patch(&a, &patches).expect("patch application failed");
5594 let result_measure = &result.parts[0].staves[0].measures[0];
5595 assert_eq!(result_measure.key_sig, expected.key_sig);
5596 assert_eq!(result_measure.time_sig, expected.time_sig);
5597 assert_eq!(result_measure.barline_left, expected.barline_left);
5598 assert_eq!(result_measure.barline_right, expected.barline_right);
5599 assert_eq!(result_measure.rehearsal, expected.rehearsal);
5600 assert_eq!(result_measure.volta, expected.volta);
5601 assert_eq!(result_measure.texts, expected.texts);
5602 assert_eq!(result_measure.figured_bass, expected.figured_bass);
5603 assert_eq!(
5604 result_measure.harp_pedal_diagrams,
5605 expected.harp_pedal_diagrams
5606 );
5607 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
5608 }
5609
5610 #[test]
5611 fn diff_and_patch_preserve_score_level_texts() {
5612 let a = Score::new("T", 120, 4, 4, 0, 1);
5613 let mut b = a.clone();
5614 b.texts.push(StyledText {
5615 style: TextStyle::Expression,
5616 text: "Prelude".to_string(),
5617 placement: Some("above".to_string()),
5618 offset_x: Some(12.0),
5619 offset_y: Some(-8.0),
5620 relative_x: None,
5621 relative_y: None,
5622 });
5623
5624 let changes = diff(&a, &b);
5625 assert!(changes.iter().any(|change| matches!(
5626 change,
5627 ScoreChange::ScoreTextChanged { old, new }
5628 if old.is_empty() && new == &b.texts
5629 )));
5630
5631 let patches = score_patch(&a, &b);
5632 assert!(patches.iter().any(|patch| matches!(
5633 patch,
5634 ScorePatch::SetScoreTexts { value } if value == &b.texts
5635 )));
5636 let result = apply_patch(&a, &patches).expect("score text patch failed");
5637 assert_eq!(result.texts, b.texts);
5638 }
5639
5640 #[test]
5641 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
5642 let a = Score::new("T", 120, 4, 4, 0, 1);
5643 let mut b = a.clone();
5644 let measure = &mut b.parts[0].staves[0].measures[0];
5645 measure.number = 8;
5646 measure.clef = Some(Clef::Bass);
5647 measure.tempo_text = Some("Allegro".to_string());
5648 measure.navigation = Some("D.S.".to_string());
5649 measure.expression_text = Some("espressivo".to_string());
5650 measure.multi_rest_count = Some(3);
5651 measure.system_break = true;
5652 measure.page_break = true;
5653
5654 let changes = diff(&a, &b);
5655 assert!(changes.iter().any(|change| matches!(
5656 change,
5657 ScoreChange::MeasurePresentationChanged {
5658 part: 0,
5659 staff: 0,
5660 measure: 0,
5661 old_number: 1,
5662 new_number: 8,
5663 old_clef: None,
5664 new_clef: Some(Clef::Bass),
5665 new_tempo_text: Some(text),
5666 new_navigation: Some(navigation),
5667 new_expression_text: Some(expression),
5668 new_multi_rest_count: Some(3),
5669 old_system_break: false,
5670 new_system_break: true,
5671 old_page_break: false,
5672 new_page_break: true,
5673 ..
5674 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5675 )));
5676 assert!(!changes.iter().any(|change| matches!(
5677 change,
5678 ScoreChange::UnrepresentedFieldChanged { path }
5679 if path == "parts[0].staves[0].measures[0].number"
5680 )));
5681
5682 let patches = score_patch(&a, &b);
5683 assert!(patches.iter().any(|patch| matches!(
5684 patch,
5685 ScorePatch::SetMeasurePresentation {
5686 part: 0,
5687 staff: 0,
5688 measure: 0,
5689 number: 8,
5690 clef: Some(Clef::Bass),
5691 tempo_text: Some(text),
5692 navigation: Some(navigation),
5693 expression_text: Some(expression),
5694 multi_rest_count: Some(3),
5695 system_break: true,
5696 page_break: true,
5697 section_break: false,
5698 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5699 )));
5700 assert!(
5701 !patches
5702 .iter()
5703 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5704 );
5705 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
5706 let decoded: Vec<ScorePatch> =
5707 serde_json::from_str(&json).expect("measure presentation patch should decode");
5708 let mut legacy_json: serde_json::Value =
5709 serde_json::from_str(&json).expect("patch JSON value");
5710 for patch in legacy_json.as_array_mut().expect("patch JSON array") {
5711 patch
5712 .as_object_mut()
5713 .expect("patch JSON object")
5714 .remove("section_break");
5715 }
5716 let legacy: Vec<ScorePatch> =
5717 serde_json::from_value(legacy_json).expect("legacy patch should decode");
5718 assert!(matches!(
5719 legacy.first(),
5720 Some(ScorePatch::SetMeasurePresentation {
5721 section_break: false,
5722 ..
5723 })
5724 ));
5725 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
5726 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
5727 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
5728 assert_eq!(
5729 serde_json::to_value(&result).expect("patched score JSON"),
5730 serde_json::to_value(&b).expect("expected score JSON")
5731 );
5732 }
5733
5734 #[test]
5735 fn chord_definition_changes_use_typed_diff_and_local_patch() {
5736 let a = Score::new("T", 120, 4, 4, 0, 1);
5737 let mut b = a.clone();
5738 b.chord_definitions.push(ChordDefinition {
5739 id: Some("c-major".to_string()),
5740 label: Some("C".to_string()),
5741 kind: Some("major".to_string()),
5742 fret_position: Some(0),
5743 tab_strings: Some("x32010".to_string()),
5744 tab_courses: None,
5745 members: Vec::new(),
5746 barres: Vec::new(),
5747 });
5748
5749 let changes = diff(&a, &b);
5750 assert!(changes.iter().any(|change| matches!(
5751 change,
5752 ScoreChange::ChordDefinitionsChanged { old, new }
5753 if old.is_empty() && new == &b.chord_definitions
5754 )));
5755 assert!(!changes.iter().any(|change| matches!(
5756 change,
5757 ScoreChange::UnrepresentedFieldChanged { path }
5758 if path == "chord_definitions"
5759 )));
5760
5761 let patches = score_patch(&a, &b);
5762 assert!(patches.iter().any(|patch| matches!(
5763 patch,
5764 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
5765 )));
5766 assert!(
5767 !patches
5768 .iter()
5769 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5770 );
5771 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
5772 let decoded: Vec<ScorePatch> =
5773 serde_json::from_str(&json).expect("chord definition patch should decode");
5774 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
5775 assert_eq!(result.chord_definitions, b.chord_definitions);
5776 }
5777
5778 #[test]
5779 fn part_name_changes_use_typed_diff_and_local_patch() {
5780 let a = Score::new("T", 120, 4, 4, 0, 1);
5781 let mut b = a.clone();
5782 b.parts[0].name = "Violin".to_string();
5783 b.parts[0].short_name = "Vln.".to_string();
5784 b.parts[0].midi_channel = 4;
5785 b.parts[0].midi_program = 40;
5786 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
5787 tick: 120,
5788 channel: 4,
5789 value: 2048,
5790 });
5791 b.parts[0].midi_control_changes.push(MidiControlChange {
5792 tick: 240,
5793 channel: 4,
5794 controller: 64,
5795 value: 127,
5796 });
5797 b.parts[0].midi_program_changes.push(MidiProgramChange {
5798 tick: 0,
5799 channel: 4,
5800 program: 40,
5801 });
5802 b.parts[0].midi_aftertouch.push(MidiAftertouch {
5803 tick: 360,
5804 channel: 4,
5805 key: Some(64),
5806 value: 80,
5807 });
5808 b.parts[0].staves[0].clef = Clef::Bass;
5809 b.parts[0].staves[0].transpose_semitones = -2;
5810
5811 let changes = diff(&a, &b);
5812 assert!(changes.iter().any(|change| matches!(
5813 change,
5814 ScoreChange::PartNamesChanged {
5815 part: 0,
5816 old_name,
5817 new_name,
5818 old_short_name,
5819 new_short_name,
5820 } if old_name == "Piano"
5821 && new_name == "Violin"
5822 && old_short_name == "Pno."
5823 && new_short_name == "Vln."
5824 )));
5825 assert!(changes.iter().any(|change| matches!(
5826 change,
5827 ScoreChange::PartMidiChanged {
5828 part: 0,
5829 old_channel: 0,
5830 new_channel: 4,
5831 old_program: 0,
5832 new_program: 40,
5833 }
5834 )));
5835 assert!(changes.iter().any(|change| matches!(
5836 change,
5837 ScoreChange::PartMidiAutomationChanged {
5838 part: 0,
5839 new_pitch_bends,
5840 new_control_changes,
5841 new_program_changes,
5842 new_aftertouch,
5843 ..
5844 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
5845 && new_control_changes == &b.parts[0].midi_control_changes
5846 && new_program_changes == &b.parts[0].midi_program_changes
5847 && new_aftertouch == &b.parts[0].midi_aftertouch
5848 )));
5849 assert!(changes.iter().any(|change| matches!(
5850 change,
5851 ScoreChange::StaffConfigurationChanged {
5852 part: 0,
5853 staff: 0,
5854 old_clef: Clef::Treble,
5855 new_clef: Clef::Bass,
5856 old_transpose_semitones: 0,
5857 new_transpose_semitones: -2,
5858 }
5859 )));
5860 assert!(!changes.iter().any(|change| matches!(
5861 change,
5862 ScoreChange::UnrepresentedFieldChanged { path }
5863 if path == "parts[0].name"
5864 )));
5865
5866 let patches = score_patch(&a, &b);
5867 assert!(patches.iter().any(|patch| matches!(
5868 patch,
5869 ScorePatch::SetPartNames {
5870 part: 0,
5871 name,
5872 short_name,
5873 } if name == "Violin" && short_name == "Vln."
5874 )));
5875 assert!(
5876 !patches
5877 .iter()
5878 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5879 );
5880 assert!(patches.iter().any(|patch| matches!(
5881 patch,
5882 ScorePatch::SetPartMidi {
5883 part: 0,
5884 channel: 4,
5885 program: 40,
5886 }
5887 )));
5888 assert!(patches.iter().any(|patch| matches!(
5889 patch,
5890 ScorePatch::SetPartMidiAutomation {
5891 part: 0,
5892 pitch_bends,
5893 control_changes,
5894 program_changes,
5895 aftertouch,
5896 } if pitch_bends == &b.parts[0].midi_pitch_bends
5897 && control_changes == &b.parts[0].midi_control_changes
5898 && program_changes == &b.parts[0].midi_program_changes
5899 && aftertouch == &b.parts[0].midi_aftertouch
5900 )));
5901 assert!(patches.iter().any(|patch| matches!(
5902 patch,
5903 ScorePatch::SetStaffConfiguration {
5904 part: 0,
5905 staff: 0,
5906 clef: Clef::Bass,
5907 transpose_semitones: -2,
5908 }
5909 )));
5910 let result = apply_patch(&a, &patches).expect("part name patch failed");
5911 assert_eq!(result.parts[0].name, b.parts[0].name);
5912 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
5913 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
5914 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
5915 assert_eq!(
5916 result.parts[0].midi_pitch_bends,
5917 b.parts[0].midi_pitch_bends
5918 );
5919 assert_eq!(
5920 result.parts[0].midi_control_changes,
5921 b.parts[0].midi_control_changes
5922 );
5923 assert_eq!(
5924 result.parts[0].midi_program_changes,
5925 b.parts[0].midi_program_changes
5926 );
5927 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
5928 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
5929 assert_eq!(
5930 result.parts[0].staves[0].transpose_semitones,
5931 b.parts[0].staves[0].transpose_semitones
5932 );
5933 }
5934
5935 #[test]
5936 fn diff_reports_measure_text_and_figured_bass_changes() {
5937 let a = Score::new("T", 120, 4, 4, 0, 1);
5938 let mut b = a.clone();
5939 let measure = &mut b.parts[0].staves[0].measures[0];
5940 measure.texts.push(StyledText {
5941 style: TextStyle::Lyrics,
5942 text: "la".to_string(),
5943 placement: None,
5944 offset_x: None,
5945 offset_y: None,
5946 relative_x: None,
5947 relative_y: None,
5948 });
5949 measure.figured_bass.push(FiguredBassFigure {
5950 number: "6".to_string(),
5951 alter: None,
5952 prefix: None,
5953 suffix: None,
5954 extender: false,
5955 });
5956
5957 let changes = diff(&a, &b);
5958 assert!(changes.iter().any(|change| matches!(
5959 change,
5960 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
5961 if old.is_empty() && new.len() == 1
5962 )));
5963 assert!(changes.iter().any(|change| matches!(
5964 change,
5965 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
5966 if old.is_empty() && new.len() == 1
5967 )));
5968 }
5969
5970 #[test]
5971 fn diff_reports_tablature_changes_and_patches_them_locally() {
5972 let a = Score::new("T", 120, 4, 4, 0, 1);
5973 let mut b = a.clone();
5974 b.parts[0].staves[0].tablature = Some(TablatureConfig {
5975 lines: 6,
5976 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5977 capo: 2,
5978 });
5979
5980 let changes = diff(&a, &b);
5981 assert!(!changes.iter().any(|change| matches!(
5982 change,
5983 ScoreChange::UnrepresentedFieldChanged { path }
5984 if path == "parts[0].staves[0].tablature"
5985 )));
5986 assert!(changes.iter().any(|change| matches!(
5987 change,
5988 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
5989 if config.lines == 6 && config.capo == 2
5990 )));
5991 let patches = score_patch(&a, &b);
5992 assert!(patches.iter().any(|patch| matches!(
5993 patch,
5994 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
5995 if config.lines == 6 && config.capo == 2
5996 )));
5997 assert_eq!(
5998 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
5999 b.parts[0].staves[0].tablature
6000 );
6001 }
6002
6003 #[test]
6004 fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
6005 let mut a = Score::new("T", 120, 4, 4, 0, 2);
6006 a.parts[0].staves[0].tablature = Some(TablatureConfig {
6007 lines: 6,
6008 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6009 capo: 0,
6010 });
6011 let mut b = a.clone();
6012 b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
6013 lines: 6,
6014 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6015 capo: 3,
6016 });
6017
6018 let changes = diff(&a, &b);
6019 assert!(changes.iter().any(|change| matches!(
6020 change,
6021 ScoreChange::TablatureChangeChanged {
6022 part: 0,
6023 staff: 0,
6024 measure: 1,
6025 old: None,
6026 new: Some(config),
6027 } if config.capo == 3
6028 )));
6029 let patches = score_patch(&a, &b);
6030 assert!(patches.iter().any(|patch| matches!(
6031 patch,
6032 ScorePatch::SetMeasureTablatureChange {
6033 part: 0,
6034 staff: 0,
6035 measure: 1,
6036 value: Some(config),
6037 } if config.capo == 3
6038 )));
6039 assert_eq!(
6040 apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
6041 b.parts[0].staves[0].measures[1].tablature_change
6042 );
6043 }
6044
6045 #[test]
6046 fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
6047 let a = Score::new("Ramp", 120, 4, 4, 0, 2);
6048 let mut b = a.clone();
6049 b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
6050
6051 assert!(diff(&a, &b).iter().any(|change| matches!(
6052 change,
6053 ScoreChange::MeasureTempoRampChanged {
6054 part: 0,
6055 staff: 0,
6056 measure: 1,
6057 old: None,
6058 new: Some(72),
6059 }
6060 )));
6061 let patches = score_patch(&a, &b);
6062 assert!(patches.iter().any(|patch| matches!(
6063 patch,
6064 ScorePatch::SetMeasureTempoRamp {
6065 part: 0,
6066 staff: 0,
6067 measure: 1,
6068 value: Some(72),
6069 }
6070 )));
6071 assert_eq!(
6072 apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
6073 .tempo_ramp_to,
6074 b.parts[0].staves[0].measures[1].tempo_ramp_to
6075 );
6076 }
6077
6078 #[test]
6079 fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
6080 let a = Score::new("T", 120, 4, 4, 0, 1);
6081 let mut b = a.clone();
6082 b.parts[0].staves[0].presentation = StaffPresentation {
6083 kind: StaffKind::Percussion,
6084 lines: 1,
6085 line_distance: 1.5,
6086 small: true,
6087 cutaway: true,
6088 visible: false,
6089 notehead_scheme: StaffNoteheadScheme::PitchNames,
6090 tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
6091 tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
6092 };
6093
6094 let changes = diff(&a, &b);
6095 assert!(changes.iter().any(|change| matches!(
6096 change,
6097 ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
6098 if new == &b.parts[0].staves[0].presentation
6099 )));
6100 let patches = score_patch(&a, &b);
6101 assert!(patches.iter().any(|patch| matches!(
6102 patch,
6103 ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
6104 if value == &b.parts[0].staves[0].presentation
6105 )));
6106 assert!(
6107 !patches
6108 .iter()
6109 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6110 );
6111 assert_eq!(
6112 apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
6113 b.parts[0].staves[0].presentation
6114 );
6115 }
6116
6117 #[test]
6118 fn score_patch_uses_local_presentation_patch_for_display_fields() {
6119 let a = Score::new("T", 120, 4, 4, 0, 1);
6120 let mut b = a.clone();
6121 b.parts[0].name = "Piano".to_string();
6122 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
6123 let patches = score_patch(&a, &b);
6124 assert!(patches.iter().any(|patch| matches!(
6125 patch,
6126 ScorePatch::SetMeasurePresentation {
6127 part: 0,
6128 staff: 0,
6129 measure: 0,
6130 expression_text: Some(text),
6131 ..
6132 } if text == "dolce"
6133 )));
6134 assert!(
6135 !patches
6136 .iter()
6137 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6138 );
6139 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
6140 assert_eq!(result.parts[0].name, "Piano");
6141 assert_eq!(
6142 result.parts[0].staves[0].measures[0].expression_text,
6143 Some("dolce".to_string())
6144 );
6145 }
6146
6147 #[test]
6148 fn diff_detects_tempo_change() {
6149 let a = Score::new("T", 120, 4, 4, 0, 1);
6150 let mut b = a.clone();
6151 b.settings.tempo_bpm = 90;
6152 let changes = diff(&a, &b);
6153 assert_eq!(changes.len(), 1);
6154 assert!(matches!(
6155 changes[0],
6156 ScoreChange::TempoChanged { old: 120, new: 90 }
6157 ));
6158 }
6159
6160 #[test]
6161 fn diff_detects_title_change() {
6162 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
6163 let mut b = a.clone();
6164 b.metadata.title = "New Title".to_string();
6165 let changes = diff(&a, &b);
6166 assert!(
6167 changes.iter().any(
6168 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
6169 )
6170 );
6171 }
6172
6173 #[test]
6174 fn diff_detects_note_modification() {
6175 let mut a = Score::new("T", 120, 4, 4, 0, 1);
6176 a.parts[0].staves[0].measures[0].voices[0] =
6177 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6178 let mut b = a.clone();
6179 b.parts[0].staves[0].measures[0].voices[0][0] =
6180 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
6181 let changes = diff(&a, &b);
6182 assert!(
6183 changes
6184 .iter()
6185 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
6186 );
6187 }
6188
6189 #[test]
6190 fn diff_detects_part_added() {
6191 let a = Score::new("T", 120, 4, 4, 0, 1);
6192 let mut b = a.clone();
6193 let mut p = Part::new("Violin", "Vln.");
6194 p.staves.push(Staff::new(Clef::Treble));
6195 b.parts.push(p);
6196 let changes = diff(&a, &b);
6197 assert!(
6198 changes
6199 .iter()
6200 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
6201 );
6202 }
6203
6204 #[test]
6205 fn diff_detects_measure_tempo_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[1].tempo = Some(60);
6209 let changes = diff(&a, &b);
6210 assert!(changes.iter().any(|c| matches!(
6211 c,
6212 ScoreChange::MeasureTempoChanged {
6213 measure: 1,
6214 old: None,
6215 new: Some(60),
6216 ..
6217 }
6218 )));
6219 }
6220
6221 #[test]
6222 fn diff_detects_barline_change() {
6223 use crate::model::notation::Barline;
6224 let a = Score::new("T", 120, 4, 4, 0, 2);
6225 let mut b = a.clone();
6226 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
6227 let changes = diff(&a, &b);
6228 assert!(
6229 changes
6230 .iter()
6231 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
6232 );
6233 }
6234
6235 #[test]
6236 fn diff_detects_rehearsal_change() {
6237 let a = Score::new("T", 120, 4, 4, 0, 2);
6238 let mut b = a.clone();
6239 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
6240 let changes = diff(&a, &b);
6241 assert!(
6242 changes
6243 .iter()
6244 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
6245 );
6246 }
6247
6248 #[test]
6249 fn diff_detects_volta_change() {
6250 use super::VoltaBracket;
6251 let a = Score::new("T", 120, 4, 4, 0, 2);
6252 let mut b = a.clone();
6253 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
6254 number: 1,
6255 kind: "begin_end".into(),
6256 });
6257 let changes = diff(&a, &b);
6258 assert!(
6259 changes
6260 .iter()
6261 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
6262 );
6263 }
6264
6265 #[test]
6266 fn diff_detects_key_signature_change() {
6267 let a = Score::new("T", 120, 4, 4, 0, 1);
6268 let mut b = a.clone();
6269 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
6271 assert!(
6272 changes
6273 .iter()
6274 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
6275 );
6276 }
6277
6278 #[test]
6279 fn measure_key_signature_uses_typed_local_patch() {
6280 let a = Score::new("T", 120, 4, 4, 0, 1);
6281 let mut b = a.clone();
6282 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
6283 fifths: 2,
6284 mode: "major".to_string(),
6285 });
6286 let changes = diff(&a, &b);
6287 assert!(!changes.iter().any(|change| matches!(
6288 change,
6289 ScoreChange::UnrepresentedFieldChanged { path }
6290 if path == "parts[0].staves[0].measures[0].key_sig"
6291 )));
6292 let patches = score_patch(&a, &b);
6293 assert!(patches.iter().any(|patch| matches!(
6294 patch,
6295 ScorePatch::SetKeySignature {
6296 part: 0,
6297 staff: 0,
6298 measure: 0,
6299 value: Some(KeySignature { fifths: 2, .. }),
6300 }
6301 )));
6302 assert!(
6303 !patches
6304 .iter()
6305 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6306 );
6307 }
6308
6309 #[test]
6310 fn diff_same_key_signature_no_change() {
6311 let a = Score::new("T", 120, 4, 4, 2, 1);
6312 let changes = diff(&a, &a);
6313 assert!(changes.is_empty());
6314 }
6315
6316 #[test]
6317 fn score_duration_secs_region_partial() {
6318 use super::score_duration_secs_region;
6319 let score = Score::new("T", 120, 4, 4, 0, 4);
6321 let secs = score_duration_secs_region(&score, (1, 2));
6322 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
6323 }
6324
6325 #[test]
6326 fn score_duration_secs_region_single_measure() {
6327 use super::score_duration_secs_region;
6328 let score = Score::new("T", 120, 4, 4, 0, 4);
6330 let secs = score_duration_secs_region(&score, (0, 0));
6331 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
6332 }
6333
6334 #[test]
6337 fn measure_beats_remaining_empty_voice_returns_full() {
6338 use super::measure_beats_remaining;
6339 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6340 score.parts[0].staves[0].measures[0].voices[0].clear();
6341 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6342 assert!(
6343 (rem - 4.0).abs() < 1e-9,
6344 "expected 4.0 remaining, got {rem}"
6345 );
6346 }
6347
6348 #[test]
6349 fn measure_beats_remaining_half_full_returns_half() {
6350 use super::measure_beats_remaining;
6351 use crate::model::pitch::Step;
6352 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6353 score.parts[0].staves[0].measures[0].voices[0] = vec![
6354 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6355 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
6356 ];
6357 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6358 assert!(
6359 (rem - 2.0).abs() < 1e-9,
6360 "expected 2.0 remaining, got {rem}"
6361 );
6362 }
6363
6364 #[test]
6365 fn measure_beats_remaining_full_voice_returns_zero() {
6366 use super::measure_beats_remaining;
6367 use crate::model::pitch::Step;
6368 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6369 score.parts[0].staves[0].measures[0].voices[0] =
6370 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
6371 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6372 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
6373 }
6374
6375 #[test]
6376 fn measure_beats_remaining_tuplet_accounting() {
6377 use super::measure_beats_remaining;
6378 use crate::model::notation::TupletInfo;
6379 use crate::model::pitch::Step;
6380 let mut score = Score::new("T", 120, 4, 4, 0, 1);
6382 let tuplet = TupletInfo {
6383 actual_notes: 3,
6384 normal_notes: 2,
6385 };
6386 let mk = |step| {
6387 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
6388 n.tuplet = Some(tuplet.clone());
6389 n
6390 };
6391 score.parts[0].staves[0].measures[0].voices[0] =
6392 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
6393 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6394 assert!(
6395 (rem - 2.0).abs() < 1e-9,
6396 "expected 2.0 remaining (triplets used 2.0), got {rem}"
6397 );
6398 }
6399
6400 #[test]
6401 fn measure_beats_remaining_out_of_range_returns_err() {
6402 use super::measure_beats_remaining;
6403 let score = Score::new("T", 120, 4, 4, 0, 1);
6404 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
6405 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
6406 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
6407 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
6408 }
6409
6410 #[test]
6411 fn note_content_eq_ignores_id() {
6412 use crate::model::pitch::Step;
6413 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
6414 let mut b = a.clone();
6415 b.id = "different-id".to_string();
6416 assert!(note_content_eq(&a, &b));
6417 b.pitches[0] = Pitch::new(Step::D, 4);
6419 assert!(!note_content_eq(&a, &b));
6420 let mut c = a.clone();
6422 a.stem_up = Some(true);
6423 c.stem_up = Some(false);
6424 assert!(!note_content_eq(&a, &c));
6425 }
6426
6427 #[test]
6428 fn suggested_stem_up_below_middle() {
6429 use crate::model::notation::Clef;
6430 let pitches = vec![Pitch::new(Step::C, 4)];
6432 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6433 }
6434
6435 #[test]
6436 fn suggested_stem_up_above_middle() {
6437 use crate::model::notation::Clef;
6438 let pitches = vec![Pitch::new(Step::G, 5)];
6440 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6441 }
6442
6443 #[test]
6444 fn suggested_stem_up_at_middle_line() {
6445 use crate::model::notation::Clef;
6446 let pitches = vec![Pitch::new(Step::B, 4)];
6448 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6449 }
6450
6451 #[test]
6452 fn suggested_stem_up_chord() {
6453 use crate::model::notation::Clef;
6454 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
6456 assert!(suggested_stem_up(&pitches, &Clef::Treble));
6457 }
6458
6459 #[test]
6460 fn suggested_stem_up_bass_clef() {
6461 use crate::model::notation::Clef;
6462 let pitches = vec![Pitch::new(Step::D, 3)];
6464 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
6465 let pitches2 = vec![Pitch::new(Step::C, 3)];
6467 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
6468 }
6469
6470 #[test]
6471 fn suggested_stem_up_empty_pitches() {
6472 use crate::model::notation::Clef;
6473 assert!(suggested_stem_up(&[], &Clef::Treble));
6474 }
6475
6476 fn eighth(pitch: Pitch) -> Note {
6477 Note::new(pitch, Duration::Eighth)
6478 }
6479 fn quarter(pitch: Pitch) -> Note {
6480 Note::new(pitch, Duration::Quarter)
6481 }
6482 fn rest_eighth() -> Note {
6483 Note::rest(Duration::Eighth)
6484 }
6485
6486 #[test]
6487 fn compute_beams_4_4_four_eighths() {
6488 use crate::model::notation::{Clef, TimeSignature};
6489 let _ = Clef::Treble; let ts = TimeSignature {
6491 numerator: 4,
6492 denominator: 4,
6493 };
6494 let c4 = Pitch::new(Step::C, 4);
6495 let notes = vec![
6496 eighth(c4.clone()),
6497 eighth(c4.clone()),
6498 eighth(c4.clone()),
6499 eighth(c4.clone()),
6500 ];
6501 let beams = compute_beams(¬es, &ts);
6502 assert_eq!(beams[0], BeamState::Begin);
6504 assert_eq!(beams[1], BeamState::End);
6505 assert_eq!(beams[2], BeamState::Begin);
6506 assert_eq!(beams[3], BeamState::End);
6507 }
6508
6509 #[test]
6510 fn compute_beams_4_4_all_eighth_one_group() {
6511 use crate::model::notation::TimeSignature;
6512 let ts = TimeSignature {
6513 numerator: 4,
6514 denominator: 4,
6515 };
6516 let c4 = Pitch::new(Step::C, 4);
6517 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
6519 let beams = compute_beams(¬es, &ts);
6520 assert_eq!(beams[0], BeamState::Begin);
6521 assert_eq!(beams[1], BeamState::End);
6522 }
6523
6524 #[test]
6525 fn compute_beams_quarter_not_beamed() {
6526 use crate::model::notation::TimeSignature;
6527 let ts = TimeSignature {
6528 numerator: 4,
6529 denominator: 4,
6530 };
6531 let c4 = Pitch::new(Step::C, 4);
6532 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
6533 let beams = compute_beams(¬es, &ts);
6534 assert_eq!(beams[0], BeamState::None);
6535 assert_eq!(beams[1], BeamState::None);
6536 }
6537
6538 #[test]
6539 fn compute_beams_rest_breaks_beam() {
6540 use crate::model::notation::TimeSignature;
6541 let ts = TimeSignature {
6542 numerator: 4,
6543 denominator: 4,
6544 };
6545 let c4 = Pitch::new(Step::C, 4);
6546 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
6547 let beams = compute_beams(¬es, &ts);
6548 assert_eq!(beams[0], BeamState::None);
6550 assert_eq!(beams[1], BeamState::None);
6551 assert_eq!(beams[2], BeamState::None);
6552 }
6553
6554 #[test]
6555 fn compute_beams_6_8_compound() {
6556 use crate::model::notation::TimeSignature;
6557 let ts = TimeSignature {
6558 numerator: 6,
6559 denominator: 8,
6560 };
6561 let c4 = Pitch::new(Step::C, 4);
6562 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
6564 let beams = compute_beams(¬es, &ts);
6565 assert_eq!(beams[0], BeamState::Begin);
6566 assert_eq!(beams[1], BeamState::Continue);
6567 assert_eq!(beams[2], BeamState::End);
6568 assert_eq!(beams[3], BeamState::Begin);
6569 assert_eq!(beams[4], BeamState::Continue);
6570 assert_eq!(beams[5], BeamState::End);
6571 }
6572
6573 #[test]
6574 fn compute_beams_single_eighth() {
6575 use crate::model::notation::TimeSignature;
6576 let ts = TimeSignature {
6577 numerator: 4,
6578 denominator: 4,
6579 };
6580 let c4 = Pitch::new(Step::C, 4);
6581 let notes = vec![eighth(c4.clone())];
6582 let beams = compute_beams(¬es, &ts);
6583 assert_eq!(beams[0], BeamState::None);
6584 }
6585}
6586
6587#[cfg(test)]
6588mod mid_clef_tests {
6589 use super::*;
6590 use crate::{Command, JoinMeasuresCmd, ScoreEngine, SetMidMeasureClefsCmd, SplitMeasureCmd};
6591
6592 fn quarter_notes() -> Vec<Note> {
6593 [Step::C, Step::D, Step::E, Step::F]
6594 .into_iter()
6595 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter))
6596 .collect()
6597 }
6598
6599 fn change(beats: f64, clef: Clef) -> MidMeasureClef {
6600 MidMeasureClef {
6601 offset: MeasureLength::from_beats(beats).unwrap(),
6602 clef,
6603 }
6604 }
6605
6606 #[test]
6607 fn measure_length_from_beats_is_exact_for_tuplet_subdivisions() {
6608 let third = MeasureLength::from_beats(1.0 / 3.0).unwrap();
6609 assert_eq!((third.numerator, third.denominator), (1, 12));
6610 let two = MeasureLength::from_beats(2.0).unwrap();
6611 assert_eq!((two.numerator, two.denominator), (1, 2));
6612 assert!(MeasureLength::from_beats(0.0).is_none());
6613 }
6614
6615 #[test]
6616 fn clef_at_follows_mid_bar_changes_and_json_omits_an_empty_list() {
6617 let mut measure = Measure::empty(4, 4);
6618 let json = serde_json::to_string(&measure).unwrap();
6619 assert!(!json.contains("mid_clefs"));
6620 measure.mid_clefs = vec![change(2.0, Clef::Bass)];
6621 assert_eq!(measure.clef_at(&Clef::Treble, 1.0), Clef::Treble);
6622 assert_eq!(measure.clef_at(&Clef::Treble, 2.0), Clef::Bass);
6623 let restored: Measure =
6624 serde_json::from_str(&serde_json::to_string(&measure).unwrap()).unwrap();
6625 assert_eq!(restored.mid_clefs, measure.mid_clefs);
6626 }
6627
6628 #[test]
6629 fn validation_rejects_mid_bar_clefs_outside_the_bar_or_out_of_order() {
6630 let mut score = Score::new("clefs", 120, 4, 4, 0, 1);
6631 score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6632 score.parts[0].staves[0].measures[0].mid_clefs =
6633 vec![change(2.0, Clef::Bass), change(1.0, Clef::Treble)];
6634 let errors = crate::validate(&score).errors;
6635 assert!(errors.iter().any(|error| matches!(
6636 error,
6637 crate::ValidationError::InvalidMidMeasureClef { index: 1, .. }
6638 )));
6639 score.parts[0].staves[0].measures[0].mid_clefs = vec![change(4.0, Clef::Bass)];
6640 assert!(!crate::validate(&score).errors.is_empty());
6641 }
6642
6643 #[test]
6644 fn command_split_join_diff_and_patch_keep_mid_bar_clefs() {
6645 let mut engine = ScoreEngine::new();
6646 engine.score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6647 engine
6648 .apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6649 part_index: 0,
6650 staff_index: 0,
6651 measure_index: 0,
6652 clefs: vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)],
6653 }))
6654 .unwrap();
6655 let with_clefs = engine.score.clone();
6656 let plain = {
6657 let mut score = with_clefs.clone();
6658 score.parts[0].staves[0].measures[0].mid_clefs.clear();
6659 score
6660 };
6661 assert!(diff(&plain, &with_clefs).iter().any(|change| matches!(
6662 change,
6663 ScoreChange::MidMeasureClefsChanged { measure: 0, .. }
6664 )));
6665 let patched = apply_patch(&plain, &score_patch(&plain, &with_clefs)).unwrap();
6666 assert_eq!(
6667 patched.parts[0].staves[0].measures[0].mid_clefs,
6668 with_clefs.parts[0].staves[0].measures[0].mid_clefs
6669 );
6670
6671 engine
6672 .apply(Command::SplitMeasure(SplitMeasureCmd {
6673 measure_index: 0,
6674 split_at_beats: 1.0,
6675 }))
6676 .unwrap();
6677 let measures = &engine.score.parts[0].staves[0].measures;
6678 assert!(measures[0].mid_clefs.is_empty());
6679 assert_eq!(measures[1].clef, Some(Clef::Bass));
6680 assert_eq!(measures[1].mid_clefs, vec![change(2.0, Clef::Alto)]);
6681
6682 engine
6683 .apply(Command::JoinMeasures(JoinMeasuresCmd { measure_index: 0 }))
6684 .unwrap();
6685 let measure = &engine.score.parts[0].staves[0].measures[0];
6686 assert_eq!(
6687 measure.mid_clefs,
6688 vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)]
6689 );
6690
6691 let invalid = engine.apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6692 part_index: 0,
6693 staff_index: 0,
6694 measure_index: 0,
6695 clefs: vec![change(0.0 + 5.0, Clef::Bass)],
6696 }));
6697 assert!(invalid.is_err());
6698 }
6699}
6700
6701#[cfg(test)]
6702mod percussion_key_tests {
6703 use super::*;
6704
6705 #[test]
6706 fn unpitched_chord_members_sound_their_kit_keys_by_display_position() {
6707 let mut part = Part::new("Drums", "Dr.");
6708 part.midi_channel = 9;
6709 let kit = |id: &str, key: u8, display: Pitch| PercussionInstrument {
6710 id: id.into(),
6711 name: None,
6712 midi_unpitched: Some(key),
6713 staff_position: Some(PercussionInstrument::display_position(&display)),
6714 notehead: None,
6715 preferred_voice: None,
6716 techniques: Vec::new(),
6717 };
6718 part.percussion_instruments = vec![
6719 kit("kick", 35, Pitch::new(Step::F, 4)),
6720 kit("snare", 38, Pitch::new(Step::C, 5)),
6721 ];
6722 let mut note = Note::new(Pitch::new(Step::C, 5), Duration::Quarter);
6723 note.pitches.push(Pitch::new(Step::F, 4));
6724 note.is_unpitched = true;
6725 note.instrument_id = Some("snare".into());
6726 assert_eq!(part.percussion_key(¬e, 0), Some(38));
6727 assert_eq!(part.percussion_key(¬e, 1), Some(35));
6728 note.is_unpitched = false;
6729 assert_eq!(part.percussion_key(¬e, 0), None);
6730 assert_eq!(
6731 PercussionInstrument::display_position(&Pitch::new(Step::B, 4)),
6732 0
6733 );
6734 }
6735}