1use super::{
2 duration::Duration,
3 notation::{
4 Articulation, Barline, BeamState, ChordDefinition, ChordSymbol, Clef, CrossStaff, Dynamic,
5 FiguredBassFigure, GuitarTechnique, HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind,
6 StyledText, TabPosition, TablatureConfig, TimeSignature, TupletInfo,
7 },
8 pitch::Pitch,
9};
10use crate::Error;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ScoreMetadata {
16 pub title: String,
17 pub composer: String,
18 pub lyricist: String,
19 pub copyright: String,
20 pub work_number: String,
21 pub movement_title: String,
22}
23
24impl Default for ScoreMetadata {
25 fn default() -> Self {
26 Self {
27 title: "Untitled Score".to_string(),
28 composer: String::new(),
29 lyricist: String::new(),
30 copyright: String::new(),
31 work_number: String::new(),
32 movement_title: String::new(),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScoreSettings {
39 pub tempo_bpm: u16,
40 pub time_signature: TimeSignature,
41 pub key_signature: KeySignature,
42}
43
44impl Default for ScoreSettings {
45 fn default() -> Self {
46 Self {
47 tempo_bpm: 120,
48 time_signature: TimeSignature::default(),
49 key_signature: KeySignature::default(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum PartGroupSymbol {
57 Bracket, Brace, Line, }
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PartGroup {
65 pub first_part: usize,
67 pub last_part: usize,
69 pub symbol: PartGroupSymbol,
70 #[serde(default)]
72 pub barlines_connect: bool,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct StaffGroup {
82 pub first_staff: usize,
84 pub last_staff: usize,
86 pub symbol: PartGroupSymbol,
87 #[serde(default)]
89 pub barlines_connect: bool,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Score {
94 pub id: String,
95 #[serde(default)]
97 pub schema_version: u32,
98 pub metadata: ScoreMetadata,
99 pub settings: ScoreSettings,
100 pub parts: Vec<Part>,
101 #[serde(default)]
102 pub part_groups: Vec<PartGroup>,
103 #[serde(default)]
105 pub texts: Vec<StyledText>,
106 #[serde(default)]
108 pub chord_definitions: Vec<ChordDefinition>,
109 #[serde(default)]
111 pub spanners: Vec<NotationSpanner>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
116pub enum NotationSpannerKind {
117 Slur,
118 Glissando,
119 TrillLine,
120 Pedal,
121 Ottava,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct NotationSpanner {
127 pub id: String,
128 pub kind: NotationSpannerKind,
129 pub start: NoteAddr,
130 pub end: NoteAddr,
131 #[serde(default)]
132 pub number: Option<u16>,
133 #[serde(default)]
134 pub line_type: Option<String>,
135 #[serde(default)]
136 pub text: Option<String>,
137 #[serde(default)]
138 pub placement: Option<String>,
139 #[serde(default)]
140 pub ottava_size: Option<u8>,
141 #[serde(default)]
143 pub ottava_type: Option<String>,
144}
145
146impl Default for Score {
147 fn default() -> Self {
148 let mut part = Part::new("Piano", "Pno.");
149 part.staves.push(Staff::new(Clef::Treble));
150 for _ in 0..4 {
151 part.staves[0].measures.push(Measure::empty(4, 4));
152 }
153 Self {
154 id: Uuid::new_v4().to_string(),
155 schema_version: 1,
156 metadata: ScoreMetadata::default(),
157 settings: ScoreSettings::default(),
158 parts: vec![part],
159 part_groups: Vec::new(),
160 texts: Vec::new(),
161 chord_definitions: Vec::new(),
162 spanners: Vec::new(),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169pub enum ScoreTemplate {
170 Solo,
172 Piano,
174 StringQuartet,
176 StringOrchestra,
178 BrassQuintet,
180}
181
182impl Score {
183 pub fn new(
184 title: &str,
185 tempo_bpm: u16,
186 numerator: u8,
187 denominator: u8,
188 fifths: i8,
189 measure_count: u32,
190 ) -> Self {
191 let mut score = Score::default();
192 score.metadata.title = title.to_string();
193 score.settings.tempo_bpm = tempo_bpm;
194 score.settings.time_signature = TimeSignature {
195 numerator,
196 denominator,
197 };
198 score.settings.key_signature = KeySignature {
199 fifths,
200 mode: "major".to_string(),
201 };
202
203 score.parts[0].staves[0].measures.clear();
204 for i in 0..measure_count {
205 let mut m = Measure::empty(numerator, denominator);
206 m.number = i + 1;
207 score.parts[0].staves[0].measures.push(m);
208 }
209 score
210 }
211
212 pub fn template(kind: ScoreTemplate) -> Self {
217 fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
218 (0..count)
219 .map(|i| {
220 let mut m = Measure::empty(num, den);
221 m.number = i + 1;
222 m
223 })
224 .collect()
225 }
226 fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
227 let mut p = Part::new(name, short);
228 p.midi_program = program;
229 let mut s = Staff::new(clef);
230 s.measures = measures(4, 4, 4);
231 p.staves.push(s);
232 p
233 }
234
235 let mut score = Score {
236 id: uuid::Uuid::new_v4().to_string(),
237 schema_version: 1,
238 metadata: ScoreMetadata::default(),
239 settings: ScoreSettings::default(),
240 parts: Vec::new(),
241 part_groups: Vec::new(),
242 texts: Vec::new(),
243 chord_definitions: Vec::new(),
244 spanners: Vec::new(),
245 };
246
247 match kind {
248 ScoreTemplate::Solo => {
249 score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
250 }
251 ScoreTemplate::Piano => {
252 let mut p = Part::new("Piano", "Pno.");
253 p.midi_program = 0;
254 let mut treble = Staff::new(Clef::Treble);
255 treble.measures = measures(4, 4, 4);
256 let mut bass = Staff::new(Clef::Bass);
257 bass.measures = measures(4, 4, 4);
258 p.staves.push(treble);
259 p.staves.push(bass);
260 score.parts.push(p);
261 }
262 ScoreTemplate::StringQuartet => {
263 score
264 .parts
265 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
266 score
267 .parts
268 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
269 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
270 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
271 }
272 ScoreTemplate::StringOrchestra => {
273 score
274 .parts
275 .push(part("Violin I", "Vn. I", Clef::Treble, 40));
276 score
277 .parts
278 .push(part("Violin II", "Vn. II", Clef::Treble, 40));
279 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
280 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
281 score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
282 }
283 ScoreTemplate::BrassQuintet => {
284 score
285 .parts
286 .push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
287 score
288 .parts
289 .push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
290 score
291 .parts
292 .push(part("French Horn", "Hn.", Clef::Treble, 60));
293 score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
294 score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
295 }
296 }
297 score
298 }
299
300 pub fn measure_count(&self) -> usize {
301 self.parts
302 .first()
303 .and_then(|p| p.staves.first())
304 .map(|s| s.measures.len())
305 .unwrap_or(0)
306 }
307
308 pub fn statistics(&self) -> ScoreStats {
310 let measure_count = self.measure_count();
311 let part_count = self.parts.len();
312
313 let seq = measure_sequence(self);
315 let total_beats: f64 = self
316 .parts
317 .first()
318 .and_then(|p| p.staves.first())
319 .map(|s| {
320 seq.iter()
321 .filter_map(|&idx| s.measures.get(idx))
322 .flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
323 .map(|n| n.beats())
324 .sum()
325 })
326 .unwrap_or(0.0);
327
328 let mut note_count = 0usize;
329 let mut rest_count = 0usize;
330 for part in &self.parts {
331 for staff in &part.staves {
332 for measure in &staff.measures {
333 for voice in &measure.voices {
334 for note in voice {
335 if note.is_rest {
336 rest_count += 1;
337 } else {
338 note_count += 1;
339 }
340 }
341 }
342 }
343 }
344 }
345
346 let bpm = self.settings.tempo_bpm as f64;
347 let estimated_duration_secs = if bpm > 0.0 {
348 total_beats / bpm * 60.0
349 } else {
350 0.0
351 };
352
353 ScoreStats {
354 measure_count,
355 note_count,
356 rest_count,
357 part_count,
358 estimated_duration_secs,
359 }
360 }
361
362 pub fn extract_part(&self, part_index: usize) -> Option<Score> {
365 let part = self.parts.get(part_index)?.clone();
366 let spanners = self
367 .spanners
368 .iter()
369 .filter(|spanner| spanner.start.part == part_index && spanner.end.part == part_index)
370 .cloned()
371 .map(|mut spanner| {
372 spanner.start.part = 0;
373 spanner.end.part = 0;
374 spanner
375 })
376 .collect();
377 Some(Score {
378 id: Uuid::new_v4().to_string(),
379 schema_version: 1,
380 metadata: self.metadata.clone(),
381 settings: self.settings.clone(),
382 parts: vec![part],
383 part_groups: Vec::new(),
384 texts: self.texts.clone(),
385 chord_definitions: self.chord_definitions.clone(),
386 spanners,
387 })
388 }
389
390 pub fn extract_part_checked(&self, part_index: usize) -> Result<Score, Error> {
392 if !super::validate::validate(self).is_valid() {
393 return Err(Error::InvalidScore);
394 }
395 let extracted = self
396 .extract_part(part_index)
397 .ok_or(Error::PartNotFound(part_index))?;
398 if !super::validate::validate(&extracted).is_valid() {
399 return Err(Error::InvalidScore);
400 }
401 Ok(extracted)
402 }
403
404 pub fn merge(&self, other: &Score) -> Score {
408 let self_count = self.measure_count();
409 let other_count = other.measure_count();
410 let max_count = self_count.max(other_count);
411 let ts = self.settings.time_signature.clone();
412
413 let pad = |mut part: Part, from: usize| -> Part {
414 for staff in &mut part.staves {
415 for i in from..max_count {
416 let mut m = Measure::empty(ts.numerator, ts.denominator);
417 m.number = i as u32 + 1;
418 staff.measures.push(m);
419 }
420 }
421 part
422 };
423
424 let mut parts: Vec<Part> = self
425 .parts
426 .iter()
427 .cloned()
428 .map(|p| pad(p, self_count))
429 .collect();
430 for p in &other.parts {
431 parts.push(pad(p.clone(), other_count));
432 }
433 let self_part_count = self.parts.len();
434 let mut spanners = self.spanners.clone();
435 spanners.extend(other.spanners.iter().cloned().map(|mut spanner| {
436 spanner.start.part += self_part_count;
437 spanner.end.part += self_part_count;
438 spanner
439 }));
440
441 Score {
442 id: Uuid::new_v4().to_string(),
443 schema_version: 1,
444 metadata: self.metadata.clone(),
445 settings: self.settings.clone(),
446 parts,
447 part_groups: Vec::new(),
448 texts: self.texts.clone(),
449 chord_definitions: self.chord_definitions.clone(),
450 spanners,
451 }
452 }
453
454 pub fn merge_checked(&self, other: &Score) -> Result<Score, Error> {
456 if !super::validate::validate(self).is_valid()
457 || !super::validate::validate(other).is_valid()
458 {
459 return Err(Error::InvalidScore);
460 }
461 let merged = self.merge(other);
462 if !super::validate::validate(&merged).is_valid() {
463 return Err(Error::InvalidScore);
464 }
465 Ok(merged)
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
471pub struct ScoreStats {
472 pub measure_count: usize,
473 pub note_count: usize,
475 pub rest_count: usize,
476 pub part_count: usize,
477 pub estimated_duration_secs: f64,
479}
480
481use super::pitch::Step;
484use super::repeat::measure_sequence;
485
486pub fn assign_tablature_positions(score: &mut Score) -> usize {
498 const MAX_FRET: i16 = 24;
499 let mut assigned = 0;
500
501 for part in &mut score.parts {
502 for staff in &mut part.staves {
503 let Some(tab) = &staff.tablature else {
504 continue;
505 };
506 let tuning = tab.tuning_midi.clone();
507 let lines = tab.lines as usize;
508 let capo = i16::from(tab.capo);
509
510 for measure in &mut staff.measures {
511 for voice in &mut measure.voices {
512 for note in voice.iter_mut() {
513 if note.is_rest
514 || note.tab_position.is_some()
515 || !note.tab_positions.is_empty()
516 || note.pitches.is_empty()
517 {
518 continue;
519 }
520 let pitches: Vec<i16> = note.pitches.iter().map(Pitch::to_midi).collect();
521 let Some(positions) =
522 best_tablature_assignment(&pitches, &tuning, lines, capo, MAX_FRET)
523 else {
524 continue;
525 };
526
527 note.tab_position = positions.first().cloned();
528 note.tab_positions = positions;
529 note.string_number = note.tab_position.as_ref().map(|p| p.string);
530 assigned += 1;
531 }
532 }
533 }
534 }
535 }
536
537 assigned
538}
539
540pub fn optimize_tablature_positions(score: &mut Score) -> usize {
547 const MAX_FRET: i16 = 24;
548 let mut assigned = 0;
549
550 for part in &mut score.parts {
551 for staff in &mut part.staves {
552 let Some(tab) = &staff.tablature else {
553 continue;
554 };
555 let tuning = tab.tuning_midi.clone();
556 let lines = tab.lines as usize;
557 let capo = i16::from(tab.capo);
558 for voice in 0..4 {
559 let mut locations = Vec::new();
561 for (measure_index, measure) in staff.measures.iter().enumerate() {
562 for (note_index, note) in measure.voices[voice].iter().enumerate() {
563 if !note.is_rest && !note.pitches.is_empty() {
564 locations.push((measure_index, note_index, note.clone()));
565 }
566 }
567 }
568 let candidates: Vec<Vec<Vec<TabPosition>>> = locations
569 .iter()
570 .map(|(_, _, note)| {
571 if let Some(positions) = if !note.tab_positions.is_empty() {
572 Some(note.tab_positions.clone())
573 } else {
574 note.tab_position.clone().map(|position| vec![position])
575 } {
576 vec![positions]
577 } else {
578 tablature_assignments(
579 ¬e.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
580 &tuning,
581 lines,
582 capo,
583 MAX_FRET,
584 )
585 }
586 })
587 .collect();
588 if candidates.iter().any(Vec::is_empty) {
589 continue;
590 }
591
592 let mut costs: Vec<Vec<(u32, Option<usize>)>> = candidates
593 .iter()
594 .map(|events| vec![(u32::MAX, None); events.len()])
595 .collect();
596 for (candidate_index, candidate) in candidates[0].iter().enumerate() {
597 costs[0][candidate_index] = (tablature_load(candidate), None);
598 }
599 for event_index in 1..candidates.len() {
600 for (candidate_index, candidate) in candidates[event_index].iter().enumerate() {
601 let load = tablature_load(candidate);
602 for (previous_index, previous) in
603 candidates[event_index - 1].iter().enumerate()
604 {
605 let previous_cost = costs[event_index - 1][previous_index].0;
606 let cost = previous_cost
607 .saturating_add(load)
608 .saturating_add(tablature_movement(previous, candidate));
609 if cost < costs[event_index][candidate_index].0 {
610 costs[event_index][candidate_index] = (cost, Some(previous_index));
611 }
612 }
613 }
614 }
615 let mut selected = vec![0; candidates.len()];
616 if let Some((last, _)) = costs.last().and_then(|row| {
617 row.iter()
618 .enumerate()
619 .min_by_key(|(index, (cost, _))| (*cost, *index))
620 }) {
621 selected[candidates.len() - 1] = last;
622 for event_index in (1..candidates.len()).rev() {
623 selected[event_index - 1] =
624 costs[event_index][selected[event_index]].1.unwrap_or(0);
625 }
626 }
627
628 for (((measure_index, note_index, original), event_candidates), selected_index) in
629 locations.into_iter().zip(candidates).zip(selected)
630 {
631 if original.tab_position.is_none() && original.tab_positions.is_empty() {
632 let note = &mut staff.measures[measure_index].voices[voice][note_index];
633 let positions = event_candidates[selected_index].clone();
634 note.tab_position = positions.first().cloned();
635 note.tab_positions = positions;
636 note.string_number = note.tab_position.as_ref().map(|p| p.string);
637 assigned += 1;
638 }
639 }
640 }
641 }
642 }
643
644 assigned
645}
646
647fn tablature_load(positions: &[TabPosition]) -> u32 {
648 let sum: u32 = positions
649 .iter()
650 .map(|position| u32::from(position.fret))
651 .sum();
652 let min = positions
653 .iter()
654 .map(|position| position.fret)
655 .min()
656 .unwrap_or(0);
657 let max = positions
658 .iter()
659 .map(|position| position.fret)
660 .max()
661 .unwrap_or(0);
662 let span = max - min;
663 let stretch_penalty = span.saturating_sub(4) as u32 * 12;
666 sum + u32::from(span) * 2 + stretch_penalty
667}
668
669fn tablature_movement(previous: &[TabPosition], current: &[TabPosition]) -> u32 {
670 previous
671 .iter()
672 .zip(current)
673 .map(|(a, b)| u32::from(a.fret.abs_diff(b.fret)) + u32::from(a.string.abs_diff(b.string)))
674 .sum()
675}
676
677fn tablature_assignments(
678 pitches: &[i16],
679 tuning: &[i16],
680 lines: usize,
681 capo: i16,
682 max_fret: i16,
683) -> Vec<Vec<TabPosition>> {
684 #[allow(clippy::too_many_arguments)]
685 fn visit(
686 pitches: &[i16],
687 tuning: &[i16],
688 lines: usize,
689 capo: i16,
690 max_fret: i16,
691 index: usize,
692 used: &mut [bool],
693 current: &mut Vec<TabPosition>,
694 output: &mut Vec<Vec<TabPosition>>,
695 ) {
696 if index == pitches.len() {
697 output.push(current.clone());
698 return;
699 }
700 for (string, open) in tuning.iter().enumerate().take(lines) {
701 if used[string] {
702 continue;
703 }
704 let fret = pitches[index] - *open - capo;
705 if !(0..=max_fret).contains(&fret) {
706 continue;
707 }
708 used[string] = true;
709 current.push(TabPosition {
710 string: (string + 1) as u8,
711 fret: fret as u8,
712 });
713 visit(
714 pitches,
715 tuning,
716 lines,
717 capo,
718 max_fret,
719 index + 1,
720 used,
721 current,
722 output,
723 );
724 current.pop();
725 used[string] = false;
726 }
727 }
728
729 if pitches.is_empty() || pitches.len() > lines {
730 return Vec::new();
731 }
732 let mut output = Vec::new();
733 visit(
734 pitches,
735 tuning,
736 lines,
737 capo,
738 max_fret,
739 0,
740 &mut vec![false; lines],
741 &mut Vec::new(),
742 &mut output,
743 );
744 output
745}
746
747fn best_tablature_assignment(
748 pitches: &[i16],
749 tuning: &[i16],
750 lines: usize,
751 capo: i16,
752 max_fret: i16,
753) -> Option<Vec<TabPosition>> {
754 type Assignment = (i16, i16, i16, Vec<u8>, Vec<TabPosition>);
755
756 #[allow(clippy::too_many_arguments)]
757 fn search(
758 pitches: &[i16],
759 tuning: &[i16],
760 lines: usize,
761 capo: i16,
762 max_fret: i16,
763 index: usize,
764 used: &mut [bool],
765 current: &mut Vec<TabPosition>,
766 best: &mut Option<Assignment>,
767 ) {
768 if index == pitches.len() {
769 let sum: i16 = current.iter().map(|p| i16::from(p.fret)).sum();
770 let min = current.iter().map(|p| p.fret).min().unwrap_or(0);
771 let max = current.iter().map(|p| p.fret).max().unwrap_or(0);
772 let strings: Vec<u8> = current.iter().map(|p| p.string).collect();
773 let candidate = (
774 sum,
775 i16::from(max) - i16::from(min),
776 i16::from(max),
777 strings,
778 current.clone(),
779 );
780 if best.as_ref().is_none_or(|existing| {
781 (candidate.0, candidate.1, candidate.2, &candidate.3)
782 < (existing.0, existing.1, existing.2, &existing.3)
783 }) {
784 *best = Some(candidate);
785 }
786 return;
787 }
788
789 for (string, open) in tuning.iter().enumerate().take(lines) {
790 if used[string] {
791 continue;
792 }
793 let fret = pitches[index] - *open - capo;
794 if !(0..=max_fret).contains(&fret) {
795 continue;
796 }
797 used[string] = true;
798 current.push(TabPosition {
799 string: (string + 1) as u8,
800 fret: fret as u8,
801 });
802 search(
803 pitches,
804 tuning,
805 lines,
806 capo,
807 max_fret,
808 index + 1,
809 used,
810 current,
811 best,
812 );
813 current.pop();
814 used[string] = false;
815 }
816 }
817
818 if pitches.len() > lines {
819 return None;
820 }
821 let mut used = vec![false; lines];
822 let mut current = Vec::with_capacity(pitches.len());
823 let mut best = None;
824 search(
825 pitches,
826 tuning,
827 lines,
828 capo,
829 max_fret,
830 0,
831 &mut used,
832 &mut current,
833 &mut best,
834 );
835 best.map(|(_, _, _, _, positions)| positions)
836}
837
838pub fn transpose(score: &Score, semitones: i8) -> Score {
842 if semitones == 0 {
843 return score.clone();
844 }
845 let mut out = score.clone();
846 out.settings.key_signature.fifths = transpose_fifths(
847 score.settings.key_signature.fifths,
848 &score.settings.key_signature.mode,
849 semitones,
850 );
851 for part in &mut out.parts {
852 for staff in &mut part.staves {
853 for measure in &mut staff.measures {
854 if let Some(ref mut ks) = measure.key_sig {
855 ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
856 }
857 for voice in &mut measure.voices {
858 for note in voice.iter_mut() {
859 for pitch in note.pitches.iter_mut() {
860 *pitch = transpose_pitch(pitch, semitones);
861 }
862 }
863 }
864 }
865 }
866 }
867 out
868}
869
870pub fn transpose_checked(score: &Score, semitones: i8) -> Result<Score, Error> {
872 if !super::validate::validate(score).is_valid() {
873 return Err(Error::InvalidScore);
874 }
875 let transposed = transpose(score, semitones);
876 if !super::validate::validate(&transposed).is_valid() {
877 return Err(Error::InvalidScore);
878 }
879 Ok(transposed)
880}
881
882fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
883 let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
884 let pc = new_midi % 12;
885 let oct = (new_midi / 12) as i8 - 1;
886 let (step, alter): (Step, i8) = if semitones >= 0 {
887 match pc {
888 0 => (Step::C, 0),
889 1 => (Step::C, 1),
890 2 => (Step::D, 0),
891 3 => (Step::D, 1),
892 4 => (Step::E, 0),
893 5 => (Step::F, 0),
894 6 => (Step::F, 1),
895 7 => (Step::G, 0),
896 8 => (Step::G, 1),
897 9 => (Step::A, 0),
898 10 => (Step::A, 1),
899 11 => (Step::B, 0),
900 _ => (Step::C, 0),
901 }
902 } else {
903 match pc {
904 0 => (Step::C, 0),
905 1 => (Step::D, -1),
906 2 => (Step::D, 0),
907 3 => (Step::E, -1),
908 4 => (Step::E, 0),
909 5 => (Step::F, 0),
910 6 => (Step::G, -1),
911 7 => (Step::G, 0),
912 8 => (Step::A, -1),
913 9 => (Step::A, 0),
914 10 => (Step::B, -1),
915 11 => (Step::B, 0),
916 _ => (Step::C, 0),
917 }
918 };
919 Pitch::with_microtone(step, oct, alter, pitch.microtone_cents)
920}
921
922fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
928 let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
929 let tonic_pc = if mode == "minor" {
930 ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
931 } else {
932 tonic_major_pc
933 };
934 let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
935 let major_tonic = if mode == "minor" {
936 ((new_tonic as i32 + 3).rem_euclid(12)) as u8
937 } else {
938 new_tonic
939 };
940 let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
941 if raw > 6 { raw - 12 } else { raw }
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize)]
945pub struct Part {
946 pub id: String,
947 pub name: String,
948 pub short_name: String,
949 pub staves: Vec<Staff>,
950 #[serde(default)]
952 pub midi_channel: u8,
953 #[serde(default)]
955 pub midi_program: u8,
956 #[serde(default)]
958 pub midi_pitch_bends: Vec<MidiPitchBend>,
959 #[serde(default)]
961 pub midi_control_changes: Vec<MidiControlChange>,
962 #[serde(default)]
964 pub midi_program_changes: Vec<MidiProgramChange>,
965 #[serde(default)]
967 pub midi_aftertouch: Vec<MidiAftertouch>,
968 #[serde(default)]
970 pub percussion_instruments: Vec<PercussionInstrument>,
971 #[serde(default)]
973 pub staff_groups: Vec<StaffGroup>,
974}
975
976#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
977pub struct MidiPitchBend {
978 pub tick: u64,
979 pub channel: u8,
980 pub value: i16,
982}
983
984#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
985pub struct MidiControlChange {
986 pub tick: u64,
987 pub channel: u8,
988 pub controller: u8,
990 pub value: u8,
992}
993
994#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
995pub struct MidiProgramChange {
996 pub tick: u64,
997 pub channel: u8,
998 pub program: u8,
1000}
1001
1002#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1003pub struct MidiAftertouch {
1004 pub tick: u64,
1005 pub channel: u8,
1006 pub key: Option<u8>,
1008 pub value: u8,
1010}
1011
1012#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1013pub struct PercussionInstrument {
1014 pub id: String,
1015 #[serde(default)]
1016 pub name: Option<String>,
1017 #[serde(default)]
1018 pub midi_unpitched: Option<u8>,
1019}
1020
1021impl Part {
1022 pub fn new(name: &str, short_name: &str) -> Self {
1023 Self {
1024 id: Uuid::new_v4().to_string(),
1025 name: name.to_string(),
1026 short_name: short_name.to_string(),
1027 staves: Vec::new(),
1028 midi_channel: 0,
1029 midi_program: 0,
1030 midi_pitch_bends: Vec::new(),
1031 midi_control_changes: Vec::new(),
1032 midi_program_changes: Vec::new(),
1033 midi_aftertouch: Vec::new(),
1034 percussion_instruments: Vec::new(),
1035 staff_groups: Vec::new(),
1036 }
1037 }
1038
1039 pub fn percussion_instrument_for_note(&self, note: &Note) -> Option<&PercussionInstrument> {
1047 if !note.is_unpitched {
1048 return None;
1049 }
1050 if let Some(instrument_id) = note.instrument_id.as_deref() {
1051 return self
1052 .percussion_instruments
1053 .iter()
1054 .find(|instrument| instrument.id == instrument_id);
1055 }
1056 let midi_key = u8::try_from(note.pitches.first()?.to_midi()).ok()?;
1057 self.percussion_instruments
1058 .iter()
1059 .find(|instrument| instrument.midi_unpitched == Some(midi_key))
1060 }
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1064pub struct Staff {
1065 pub clef: Clef,
1066 pub measures: Vec<Measure>,
1067 #[serde(default)]
1070 pub transpose_semitones: i8,
1071 #[serde(default)]
1072 pub tablature: Option<TablatureConfig>,
1073}
1074
1075impl Staff {
1076 pub fn new(clef: Clef) -> Self {
1077 Self {
1078 clef,
1079 measures: Vec::new(),
1080 transpose_semitones: 0,
1081 tablature: None,
1082 }
1083 }
1084}
1085
1086#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1087pub struct VoltaBracket {
1088 pub number: u8,
1090 pub kind: String,
1092}
1093
1094#[derive(Debug, Clone, Serialize, Deserialize)]
1095pub struct Measure {
1096 pub number: u32,
1097 pub time_sig: Option<TimeSignature>,
1098 pub key_sig: Option<KeySignature>,
1099 pub clef: Option<Clef>,
1100 pub tempo: Option<u16>,
1101 pub barline_left: Barline,
1102 pub barline_right: Barline,
1103 #[serde(default)]
1104 pub volta: Option<VoltaBracket>,
1105 #[serde(default)]
1106 pub tempo_text: Option<String>,
1107 #[serde(default)]
1108 pub rehearsal: Option<String>,
1109 #[serde(default)]
1112 pub navigation: Option<String>,
1113 #[serde(default)]
1115 pub expression_text: Option<String>,
1116 #[serde(default)]
1117 pub texts: Vec<StyledText>,
1118 #[serde(default)]
1120 pub figured_bass: Vec<FiguredBassFigure>,
1121 #[serde(default)]
1123 pub multi_rest_count: Option<u8>,
1124 #[serde(default)]
1126 pub system_break: bool,
1127 #[serde(default)]
1129 pub page_break: bool,
1130 pub voices: [Vec<Note>; 4],
1132 #[serde(default)]
1138 pub source_voice_numbers: [Option<u32>; 4],
1139}
1140
1141impl Measure {
1142 pub fn empty(numerator: u8, denominator: u8) -> Self {
1143 let total_beats = TimeSignature {
1144 numerator,
1145 denominator,
1146 }
1147 .total_beats();
1148 let mut voice0: Vec<Note> = Vec::new();
1149 let mut remaining = total_beats;
1150 while remaining > 1e-9 {
1151 let dur = Duration::whole_filling_beats(remaining);
1152 remaining -= dur.beats(0);
1153 voice0.push(Note::rest(dur));
1154 }
1155 Self {
1156 number: 0,
1157 time_sig: None,
1158 key_sig: None,
1159 clef: None,
1160 tempo: None,
1161 barline_left: Barline::Normal,
1162 barline_right: Barline::Normal,
1163 volta: None,
1164 tempo_text: None,
1165 rehearsal: None,
1166 navigation: None,
1167 expression_text: None,
1168 texts: Vec::new(),
1169 figured_bass: Vec::new(),
1170 multi_rest_count: None,
1171 system_break: false,
1172 page_break: false,
1173 voices: [voice0, vec![], vec![], vec![]],
1174 source_voice_numbers: [None; 4],
1175 }
1176 }
1177
1178 pub fn renumber(&mut self, n: u32) {
1179 self.number = n;
1180 }
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1184pub struct Note {
1185 pub id: String,
1186 pub is_rest: bool,
1187 #[serde(default)]
1189 pub is_unpitched: bool,
1190 #[serde(default)]
1192 pub instrument_id: Option<String>,
1193 #[serde(default)]
1195 pub offset_x: Option<f64>,
1196 #[serde(default)]
1198 pub offset_y: Option<f64>,
1199 #[serde(default)]
1201 pub relative_x: Option<f64>,
1202 #[serde(default)]
1204 pub relative_y: Option<f64>,
1205 pub pitches: Vec<Pitch>,
1207 #[serde(default)]
1208 pub tab_position: Option<super::notation::TabPosition>,
1209 #[serde(default)]
1212 pub tab_positions: Vec<super::notation::TabPosition>,
1213 pub duration: Duration,
1214 pub dot_count: u8,
1215 pub tie_start: bool,
1216 pub tie_end: bool,
1217 pub beam: BeamState,
1218 pub articulations: Vec<Articulation>,
1219 pub dynamic: Option<Dynamic>,
1220 pub stem_up: Option<bool>,
1221 #[serde(default)]
1222 pub hairpin_start: Option<HairpinKind>,
1223 #[serde(default)]
1224 pub hairpin_end: bool,
1225 #[serde(default)]
1226 pub tuplet: Option<TupletInfo>,
1227 #[serde(default)]
1228 pub chord_symbol: Option<ChordSymbol>,
1229 #[serde(default)]
1230 pub is_grace: bool,
1231 #[serde(default)]
1233 pub grace_slash: bool,
1234 #[serde(default)]
1235 pub ottava_start: Option<OttavaKind>,
1236 #[serde(default)]
1237 pub ottava_end: bool,
1238 #[serde(default)]
1239 pub lyric: Option<Lyric>,
1240 #[serde(default)]
1241 pub pedal_start: bool,
1242 #[serde(default)]
1243 pub pedal_end: bool,
1244 #[serde(default)]
1245 pub slur_start: bool,
1246 #[serde(default)]
1247 pub slur_end: bool,
1248 #[serde(default)]
1250 pub arpeggiate: Option<bool>,
1251 #[serde(default)]
1253 pub technique_text: Option<String>,
1254 #[serde(default)]
1255 pub glissando_start: bool,
1256 #[serde(default)]
1257 pub glissando_end: bool,
1258 #[serde(default)]
1259 pub cross_staff: Option<CrossStaff>,
1260 #[serde(default)]
1262 pub fingering: Option<u8>,
1263 #[serde(default)]
1266 pub fingerings: Vec<u8>,
1267 #[serde(default)]
1269 pub string_number: Option<u8>,
1270 #[serde(default)]
1271 pub note_head: NoteHead,
1272 #[serde(default)]
1274 pub is_cue: bool,
1275 #[serde(default)]
1277 pub trill_line_start: bool,
1278 #[serde(default)]
1280 pub trill_line_end: bool,
1281 #[serde(default)]
1283 pub guitar_technique: Option<GuitarTechnique>,
1284 #[serde(default)]
1286 pub guitar_bend_alter_cents: Option<i16>,
1287}
1288
1289impl Note {
1290 pub fn select_fingering(
1292 &self,
1293 policy: super::notation::FingeringSelectionPolicy,
1294 ) -> Option<u8> {
1295 let candidates = if self.fingerings.is_empty() {
1296 self.fingering.into_iter().collect::<Vec<_>>()
1297 } else {
1298 self.fingerings.clone()
1299 };
1300 match policy {
1301 super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
1302 super::notation::FingeringSelectionPolicy::LowestNumber => {
1303 candidates.iter().copied().min()
1304 }
1305 super::notation::FingeringSelectionPolicy::HighestNumber => {
1306 candidates.iter().copied().max()
1307 }
1308 }
1309 }
1310
1311 pub fn new(pitch: Pitch, duration: Duration) -> Self {
1312 Self {
1313 id: Uuid::new_v4().to_string(),
1314 is_rest: false,
1315 is_unpitched: false,
1316 instrument_id: None,
1317 offset_x: None,
1318 offset_y: None,
1319 relative_x: None,
1320 relative_y: None,
1321 pitches: vec![pitch],
1322 tab_position: None,
1323 tab_positions: Vec::new(),
1324 duration,
1325 dot_count: 0,
1326 tie_start: false,
1327 tie_end: false,
1328 beam: BeamState::None,
1329 articulations: Vec::new(),
1330 dynamic: None,
1331 stem_up: None,
1332 hairpin_start: None,
1333 hairpin_end: false,
1334 tuplet: None,
1335 chord_symbol: None,
1336 is_grace: false,
1337 grace_slash: false,
1338 ottava_start: None,
1339 ottava_end: false,
1340 lyric: None,
1341 pedal_start: false,
1342 pedal_end: false,
1343 slur_start: false,
1344 slur_end: false,
1345 arpeggiate: None,
1346 technique_text: None,
1347 glissando_start: false,
1348 glissando_end: false,
1349 cross_staff: None,
1350 fingering: None,
1351 fingerings: Vec::new(),
1352 string_number: None,
1353 note_head: NoteHead::Normal,
1354 is_cue: false,
1355 trill_line_start: false,
1356 trill_line_end: false,
1357 guitar_technique: None,
1358 guitar_bend_alter_cents: None,
1359 }
1360 }
1361
1362 pub fn rest(duration: Duration) -> Self {
1363 Self {
1364 id: Uuid::new_v4().to_string(),
1365 is_rest: true,
1366 is_unpitched: false,
1367 instrument_id: None,
1368 offset_x: None,
1369 offset_y: None,
1370 relative_x: None,
1371 relative_y: None,
1372 pitches: Vec::new(),
1373 tab_position: None,
1374 tab_positions: Vec::new(),
1375 duration,
1376 dot_count: 0,
1377 tie_start: false,
1378 tie_end: false,
1379 beam: BeamState::None,
1380 articulations: Vec::new(),
1381 dynamic: None,
1382 stem_up: None,
1383 hairpin_start: None,
1384 hairpin_end: false,
1385 tuplet: None,
1386 chord_symbol: None,
1387 is_grace: false,
1388 grace_slash: false,
1389 ottava_start: None,
1390 ottava_end: false,
1391 lyric: None,
1392 pedal_start: false,
1393 pedal_end: false,
1394 slur_start: false,
1395 slur_end: false,
1396 arpeggiate: None,
1397 technique_text: None,
1398 glissando_start: false,
1399 glissando_end: false,
1400 cross_staff: None,
1401 fingering: None,
1402 fingerings: Vec::new(),
1403 string_number: None,
1404 note_head: NoteHead::Normal,
1405 is_cue: false,
1406 trill_line_start: false,
1407 trill_line_end: false,
1408 guitar_technique: None,
1409 guitar_bend_alter_cents: None,
1410 }
1411 }
1412
1413 pub fn beats(&self) -> f64 {
1414 if self.is_grace || self.is_cue {
1415 return 0.0;
1416 }
1417 let base = self.duration.beats(self.dot_count);
1418 if let Some(ref t) = self.tuplet {
1419 base * (t.normal_notes as f64) / (t.actual_notes as f64)
1420 } else {
1421 base
1422 }
1423 }
1424}
1425
1426impl Duration {
1427 pub fn whole_filling_beats(beats: f64) -> Duration {
1429 if beats >= 4.0 {
1430 Duration::Whole
1431 } else if beats >= 2.0 {
1432 Duration::Half
1433 } else if beats >= 1.0 {
1434 Duration::Quarter
1435 } else if beats >= 0.5 {
1436 Duration::Eighth
1437 } else if beats >= 0.25 {
1438 Duration::Sixteenth
1439 } else if beats >= 0.125 {
1440 Duration::ThirtySecond
1441 } else {
1442 Duration::SixtyFourth
1443 }
1444 }
1445}
1446
1447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1451pub struct NoteAddr {
1452 pub part: usize,
1453 pub staff: usize,
1454 pub measure: usize,
1455 pub voice: usize,
1456 pub note: usize,
1457}
1458
1459#[derive(Debug, Clone, Serialize, Deserialize)]
1463pub enum ScoreChange {
1464 ScoreTextChanged {
1465 old: Vec<StyledText>,
1466 new: Vec<StyledText>,
1467 },
1468 MeasureTextChanged {
1469 part: usize,
1470 staff: usize,
1471 measure: usize,
1472 old: Vec<StyledText>,
1473 new: Vec<StyledText>,
1474 },
1475 FiguredBassChanged {
1476 part: usize,
1477 staff: usize,
1478 measure: usize,
1479 old: Vec<FiguredBassFigure>,
1480 new: Vec<FiguredBassFigure>,
1481 },
1482 ChordDefinitionsChanged {
1483 old: Vec<ChordDefinition>,
1484 new: Vec<ChordDefinition>,
1485 },
1486 PartNamesChanged {
1487 part: usize,
1488 old_name: String,
1489 new_name: String,
1490 old_short_name: String,
1491 new_short_name: String,
1492 },
1493 PartMidiChanged {
1494 part: usize,
1495 old_channel: u8,
1496 new_channel: u8,
1497 old_program: u8,
1498 new_program: u8,
1499 },
1500 PartMidiAutomationChanged {
1501 part: usize,
1502 old_pitch_bends: Vec<MidiPitchBend>,
1503 new_pitch_bends: Vec<MidiPitchBend>,
1504 old_control_changes: Vec<MidiControlChange>,
1505 new_control_changes: Vec<MidiControlChange>,
1506 old_program_changes: Vec<MidiProgramChange>,
1507 new_program_changes: Vec<MidiProgramChange>,
1508 old_aftertouch: Vec<MidiAftertouch>,
1509 new_aftertouch: Vec<MidiAftertouch>,
1510 },
1511 StaffConfigurationChanged {
1512 part: usize,
1513 staff: usize,
1514 old_clef: Clef,
1515 new_clef: Clef,
1516 old_transpose_semitones: i8,
1517 new_transpose_semitones: i8,
1518 },
1519 MeasurePresentationChanged {
1520 part: usize,
1521 staff: usize,
1522 measure: usize,
1523 old_number: u32,
1524 new_number: u32,
1525 old_clef: Option<Clef>,
1526 new_clef: Option<Clef>,
1527 old_tempo_text: Option<String>,
1528 new_tempo_text: Option<String>,
1529 old_navigation: Option<String>,
1530 new_navigation: Option<String>,
1531 old_expression_text: Option<String>,
1532 new_expression_text: Option<String>,
1533 old_multi_rest_count: Option<u8>,
1534 new_multi_rest_count: Option<u8>,
1535 old_system_break: bool,
1536 new_system_break: bool,
1537 old_page_break: bool,
1538 new_page_break: bool,
1539 },
1540 TablatureConfigChanged {
1541 part: usize,
1542 staff: usize,
1543 old: Option<TablatureConfig>,
1544 new: Option<TablatureConfig>,
1545 },
1546 UnrepresentedFieldChanged {
1551 path: String,
1552 },
1553 MetadataChanged {
1554 field: String,
1555 old: String,
1556 new: String,
1557 },
1558 TempoChanged {
1559 old: u16,
1560 new: u16,
1561 },
1562 KeySignatureChanged {
1563 old: KeySignature,
1564 new: KeySignature,
1565 },
1566 PartAdded {
1567 part_index: usize,
1568 },
1569 PartRemoved {
1570 part_index: usize,
1571 name: String,
1572 },
1573 NoteAdded {
1574 part: usize,
1575 staff: usize,
1576 measure: usize,
1577 voice: usize,
1578 note_index: usize,
1579 },
1580 NoteRemoved {
1581 part: usize,
1582 staff: usize,
1583 measure: usize,
1584 voice: usize,
1585 note: Box<Note>,
1586 },
1587 NoteModified {
1588 part: usize,
1589 staff: usize,
1590 measure: usize,
1591 voice: usize,
1592 note_index: usize,
1593 old: Box<Note>,
1594 new: Box<Note>,
1595 },
1596 TimeSigChanged {
1597 part: usize,
1598 staff: usize,
1599 measure: usize,
1600 old: Option<TimeSignature>,
1601 new: Option<TimeSignature>,
1602 },
1603 MeasureTempoChanged {
1604 part: usize,
1605 staff: usize,
1606 measure: usize,
1607 old: Option<u16>,
1608 new: Option<u16>,
1609 },
1610 BarlineChanged {
1611 part: usize,
1612 staff: usize,
1613 measure: usize,
1614 },
1615 RehearsalMarkChanged {
1616 part: usize,
1617 staff: usize,
1618 measure: usize,
1619 old: Option<String>,
1620 new: Option<String>,
1621 },
1622 VoltaChanged {
1623 part: usize,
1624 staff: usize,
1625 measure: usize,
1626 },
1627}
1628
1629pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
1635 let mut changes: Vec<ScoreChange> = Vec::new();
1636
1637 if a.texts != b.texts {
1638 changes.push(ScoreChange::ScoreTextChanged {
1639 old: a.texts.clone(),
1640 new: b.texts.clone(),
1641 });
1642 }
1643 if a.chord_definitions != b.chord_definitions {
1644 changes.push(ScoreChange::ChordDefinitionsChanged {
1645 old: a.chord_definitions.clone(),
1646 new: b.chord_definitions.clone(),
1647 });
1648 }
1649 if let Some(path) = first_unrepresented_field_change(a, b) {
1650 changes.push(ScoreChange::UnrepresentedFieldChanged { path });
1651 }
1652
1653 macro_rules! meta {
1654 ($field:ident, $name:literal) => {
1655 if a.metadata.$field != b.metadata.$field {
1656 changes.push(ScoreChange::MetadataChanged {
1657 field: $name.to_string(),
1658 old: a.metadata.$field.clone(),
1659 new: b.metadata.$field.clone(),
1660 });
1661 }
1662 };
1663 }
1664 meta!(title, "title");
1665 meta!(composer, "composer");
1666 meta!(lyricist, "lyricist");
1667 meta!(copyright, "copyright");
1668 meta!(work_number, "work_number");
1669 meta!(movement_title, "movement_title");
1670
1671 if a.settings.tempo_bpm != b.settings.tempo_bpm {
1672 changes.push(ScoreChange::TempoChanged {
1673 old: a.settings.tempo_bpm,
1674 new: b.settings.tempo_bpm,
1675 });
1676 }
1677 if a.settings.key_signature != b.settings.key_signature {
1678 changes.push(ScoreChange::KeySignatureChanged {
1679 old: a.settings.key_signature.clone(),
1680 new: b.settings.key_signature.clone(),
1681 });
1682 }
1683
1684 let a_len = a.parts.len();
1685 let b_len = b.parts.len();
1686 for i in b_len..a_len {
1687 changes.push(ScoreChange::PartRemoved {
1688 part_index: i,
1689 name: a.parts[i].name.clone(),
1690 });
1691 }
1692 for i in a_len..b_len {
1693 changes.push(ScoreChange::PartAdded { part_index: i });
1694 }
1695
1696 for pi in 0..a_len.min(b_len) {
1697 let ap = &a.parts[pi];
1698 let bp = &b.parts[pi];
1699 if ap.name != bp.name || ap.short_name != bp.short_name {
1700 changes.push(ScoreChange::PartNamesChanged {
1701 part: pi,
1702 old_name: ap.name.clone(),
1703 new_name: bp.name.clone(),
1704 old_short_name: ap.short_name.clone(),
1705 new_short_name: bp.short_name.clone(),
1706 });
1707 }
1708 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
1709 changes.push(ScoreChange::PartMidiChanged {
1710 part: pi,
1711 old_channel: ap.midi_channel,
1712 new_channel: bp.midi_channel,
1713 old_program: ap.midi_program,
1714 new_program: bp.midi_program,
1715 });
1716 }
1717 if ap.midi_pitch_bends != bp.midi_pitch_bends
1718 || ap.midi_control_changes != bp.midi_control_changes
1719 || ap.midi_program_changes != bp.midi_program_changes
1720 || ap.midi_aftertouch != bp.midi_aftertouch
1721 {
1722 changes.push(ScoreChange::PartMidiAutomationChanged {
1723 part: pi,
1724 old_pitch_bends: ap.midi_pitch_bends.clone(),
1725 new_pitch_bends: bp.midi_pitch_bends.clone(),
1726 old_control_changes: ap.midi_control_changes.clone(),
1727 new_control_changes: bp.midi_control_changes.clone(),
1728 old_program_changes: ap.midi_program_changes.clone(),
1729 new_program_changes: bp.midi_program_changes.clone(),
1730 old_aftertouch: ap.midi_aftertouch.clone(),
1731 new_aftertouch: bp.midi_aftertouch.clone(),
1732 });
1733 }
1734 for si in 0..ap.staves.len().min(bp.staves.len()) {
1735 let a_staff = &ap.staves[si];
1736 let b_staff = &bp.staves[si];
1737 if a_staff.clef != b_staff.clef
1738 || a_staff.transpose_semitones != b_staff.transpose_semitones
1739 {
1740 changes.push(ScoreChange::StaffConfigurationChanged {
1741 part: pi,
1742 staff: si,
1743 old_clef: a_staff.clef.clone(),
1744 new_clef: b_staff.clef.clone(),
1745 old_transpose_semitones: a_staff.transpose_semitones,
1746 new_transpose_semitones: b_staff.transpose_semitones,
1747 });
1748 }
1749 if a_staff.tablature != b_staff.tablature {
1750 changes.push(ScoreChange::TablatureConfigChanged {
1751 part: pi,
1752 staff: si,
1753 old: a_staff.tablature.clone(),
1754 new: b_staff.tablature.clone(),
1755 });
1756 }
1757 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
1758 let am = &a_staff.measures[mi];
1759 let bm = &b_staff.measures[mi];
1760 if am.number != bm.number
1761 || am.clef != bm.clef
1762 || am.tempo_text != bm.tempo_text
1763 || am.navigation != bm.navigation
1764 || am.expression_text != bm.expression_text
1765 || am.multi_rest_count != bm.multi_rest_count
1766 || am.system_break != bm.system_break
1767 || am.page_break != bm.page_break
1768 {
1769 changes.push(ScoreChange::MeasurePresentationChanged {
1770 part: pi,
1771 staff: si,
1772 measure: mi,
1773 old_number: am.number,
1774 new_number: bm.number,
1775 old_clef: am.clef.clone(),
1776 new_clef: bm.clef.clone(),
1777 old_tempo_text: am.tempo_text.clone(),
1778 new_tempo_text: bm.tempo_text.clone(),
1779 old_navigation: am.navigation.clone(),
1780 new_navigation: bm.navigation.clone(),
1781 old_expression_text: am.expression_text.clone(),
1782 new_expression_text: bm.expression_text.clone(),
1783 old_multi_rest_count: am.multi_rest_count,
1784 new_multi_rest_count: bm.multi_rest_count,
1785 old_system_break: am.system_break,
1786 new_system_break: bm.system_break,
1787 old_page_break: am.page_break,
1788 new_page_break: bm.page_break,
1789 });
1790 }
1791 for vi in 0..4usize {
1792 let av = &am.voices[vi];
1793 let bv = &bm.voices[vi];
1794 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
1795 if !note_content_eq(a_note, b_note) {
1796 changes.push(ScoreChange::NoteModified {
1797 part: pi,
1798 staff: si,
1799 measure: mi,
1800 voice: vi,
1801 note_index: ni,
1802 old: Box::new(a_note.clone()),
1803 new: Box::new(b_note.clone()),
1804 });
1805 }
1806 }
1807 for note in av.iter().skip(bv.len()) {
1808 changes.push(ScoreChange::NoteRemoved {
1809 part: pi,
1810 staff: si,
1811 measure: mi,
1812 voice: vi,
1813 note: Box::new(note.clone()),
1814 });
1815 }
1816 for ni in av.len()..bv.len() {
1817 changes.push(ScoreChange::NoteAdded {
1818 part: pi,
1819 staff: si,
1820 measure: mi,
1821 voice: vi,
1822 note_index: ni,
1823 });
1824 }
1825 }
1826 if am.time_sig != bm.time_sig {
1827 changes.push(ScoreChange::TimeSigChanged {
1828 part: pi,
1829 staff: si,
1830 measure: mi,
1831 old: am.time_sig.clone(),
1832 new: bm.time_sig.clone(),
1833 });
1834 }
1835 if am.tempo != bm.tempo {
1836 changes.push(ScoreChange::MeasureTempoChanged {
1837 part: pi,
1838 staff: si,
1839 measure: mi,
1840 old: am.tempo,
1841 new: bm.tempo,
1842 });
1843 }
1844 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
1845 changes.push(ScoreChange::BarlineChanged {
1846 part: pi,
1847 staff: si,
1848 measure: mi,
1849 });
1850 }
1851 if am.rehearsal != bm.rehearsal {
1852 changes.push(ScoreChange::RehearsalMarkChanged {
1853 part: pi,
1854 staff: si,
1855 measure: mi,
1856 old: am.rehearsal.clone(),
1857 new: bm.rehearsal.clone(),
1858 });
1859 }
1860 if am.volta != bm.volta {
1861 changes.push(ScoreChange::VoltaChanged {
1862 part: pi,
1863 staff: si,
1864 measure: mi,
1865 });
1866 }
1867 if am.texts != bm.texts {
1868 changes.push(ScoreChange::MeasureTextChanged {
1869 part: pi,
1870 staff: si,
1871 measure: mi,
1872 old: am.texts.clone(),
1873 new: bm.texts.clone(),
1874 });
1875 }
1876 if am.figured_bass != bm.figured_bass {
1877 changes.push(ScoreChange::FiguredBassChanged {
1878 part: pi,
1879 staff: si,
1880 measure: mi,
1881 old: am.figured_bass.clone(),
1882 new: bm.figured_bass.clone(),
1883 });
1884 }
1885 }
1886 }
1887 }
1888
1889 changes
1890}
1891
1892fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
1893 if a.settings.time_signature != b.settings.time_signature {
1894 return Some("settings.time_signature".to_string());
1895 }
1896 if a.part_groups.len() != b.part_groups.len()
1897 || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
1898 x.first_part != y.first_part
1899 || x.last_part != y.last_part
1900 || x.symbol != y.symbol
1901 || x.barlines_connect != y.barlines_connect
1902 })
1903 {
1904 return Some("part_groups".to_string());
1905 }
1906 a.parts
1907 .iter()
1908 .zip(&b.parts)
1909 .enumerate()
1910 .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
1911}
1912
1913fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
1914 let prefix = format!("parts[{part_index}]");
1915 if a.percussion_instruments != b.percussion_instruments {
1916 return Some(format!("{prefix}.percussion_instruments"));
1917 }
1918 if a.staff_groups != b.staff_groups {
1919 return Some(format!("{prefix}.staff_groups"));
1920 }
1921 if a.staves.len() != b.staves.len() {
1922 return Some(format!("{prefix}.staves"));
1923 }
1924 a.staves
1925 .iter()
1926 .zip(&b.staves)
1927 .enumerate()
1928 .find_map(|(staff_index, (a, b))| {
1929 first_unrepresented_staff_change(&prefix, staff_index, a, b)
1930 })
1931}
1932
1933fn first_unrepresented_staff_change(
1934 part_prefix: &str,
1935 staff_index: usize,
1936 a: &Staff,
1937 b: &Staff,
1938) -> Option<String> {
1939 let prefix = format!("{part_prefix}.staves[{staff_index}]");
1940 if a.measures.len() != b.measures.len() {
1941 return Some(format!("{prefix}.measures"));
1942 }
1943 a.measures
1944 .iter()
1945 .zip(&b.measures)
1946 .enumerate()
1947 .find_map(|(measure_index, (a, b))| {
1948 first_unrepresented_measure_change(&prefix, measure_index, a, b)
1949 })
1950}
1951
1952fn first_unrepresented_measure_change(
1953 _staff_prefix: &str,
1954 _measure_index: usize,
1955 _a: &Measure,
1956 _b: &Measure,
1957) -> Option<String> {
1958 None
1959}
1960
1961#[derive(Debug, Clone, Serialize, Deserialize)]
1968pub enum ScorePatch {
1969 SetScoreTexts {
1970 value: Vec<StyledText>,
1971 },
1972 SetMeasureTexts {
1973 part: usize,
1974 staff: usize,
1975 measure: usize,
1976 value: Vec<StyledText>,
1977 },
1978 SetFiguredBass {
1979 part: usize,
1980 staff: usize,
1981 measure: usize,
1982 value: Vec<FiguredBassFigure>,
1983 },
1984 SetChordDefinitions {
1985 value: Vec<ChordDefinition>,
1986 },
1987 SetPartNames {
1988 part: usize,
1989 name: String,
1990 short_name: String,
1991 },
1992 SetPartMidi {
1993 part: usize,
1994 channel: u8,
1995 program: u8,
1996 },
1997 SetPartMidiAutomation {
1998 part: usize,
1999 pitch_bends: Vec<MidiPitchBend>,
2000 control_changes: Vec<MidiControlChange>,
2001 program_changes: Vec<MidiProgramChange>,
2002 aftertouch: Vec<MidiAftertouch>,
2003 },
2004 SetStaffConfiguration {
2005 part: usize,
2006 staff: usize,
2007 clef: Clef,
2008 transpose_semitones: i8,
2009 },
2010 SetMeasurePresentation {
2011 part: usize,
2012 staff: usize,
2013 measure: usize,
2014 number: u32,
2015 clef: Option<Clef>,
2016 tempo_text: Option<String>,
2017 navigation: Option<String>,
2018 expression_text: Option<String>,
2019 multi_rest_count: Option<u8>,
2020 system_break: bool,
2021 page_break: bool,
2022 },
2023 SetTablatureConfig {
2024 part: usize,
2025 staff: usize,
2026 value: Option<TablatureConfig>,
2027 },
2028 SetMetadata {
2029 field: String,
2030 value: String,
2031 },
2032 SetTempo {
2033 value: u16,
2034 },
2035 SetKeySignature {
2036 part: usize,
2037 staff: usize,
2038 measure: usize,
2039 value: Option<KeySignature>,
2040 },
2041 SetTimeSignature {
2042 part: usize,
2043 staff: usize,
2044 measure: usize,
2045 value: Option<TimeSignature>,
2046 },
2047 SetBarlines {
2048 part: usize,
2049 staff: usize,
2050 measure: usize,
2051 left: Barline,
2052 right: Barline,
2053 },
2054 SetRehearsal {
2055 part: usize,
2056 staff: usize,
2057 measure: usize,
2058 value: Option<String>,
2059 },
2060 SetVolta {
2061 part: usize,
2062 staff: usize,
2063 measure: usize,
2064 value: Option<VoltaBracket>,
2065 },
2066 AddNote {
2068 part: usize,
2069 staff: usize,
2070 measure: usize,
2071 voice: usize,
2072 #[serde(default = "legacy_append_index")]
2074 note_index: usize,
2075 note: Box<Note>,
2076 },
2077 RemoveNote {
2078 part: usize,
2079 staff: usize,
2080 measure: usize,
2081 voice: usize,
2082 note_index: usize,
2083 },
2084 ReplaceNote {
2086 part: usize,
2087 staff: usize,
2088 measure: usize,
2089 voice: usize,
2090 note_index: usize,
2091 note: Box<Note>,
2092 },
2093 SetMeasureTempo {
2094 part: usize,
2095 staff: usize,
2096 measure: usize,
2097 value: Option<u16>,
2098 },
2099 ReplaceScore {
2102 score: Box<Score>,
2103 },
2104}
2105
2106fn legacy_append_index() -> usize {
2107 usize::MAX
2108}
2109
2110fn patch_requires_replace(a: &Score, b: &Score) -> bool {
2114 if a.settings.time_signature != b.settings.time_signature
2115 || a.settings.key_signature != b.settings.key_signature
2116 || a.parts.len() != b.parts.len()
2117 || a.part_groups.len() != b.part_groups.len()
2118 {
2119 return true;
2120 }
2121 if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
2122 x.first_part != y.first_part
2123 || x.last_part != y.last_part
2124 || x.symbol != y.symbol
2125 || x.barlines_connect != y.barlines_connect
2126 }) {
2127 return true;
2128 }
2129 for (ap, bp) in a.parts.iter().zip(&b.parts) {
2130 if ap.percussion_instruments != bp.percussion_instruments
2131 || ap.staff_groups != bp.staff_groups
2132 || ap.staves.len() != bp.staves.len()
2133 {
2134 return true;
2135 }
2136 for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
2137 if as_.measures.len() != bs.measures.len() {
2138 return true;
2139 }
2140 }
2141 }
2142 false
2143}
2144
2145pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
2150 let mut patches: Vec<ScorePatch> = Vec::new();
2151
2152 if patch_requires_replace(a, b) {
2153 return vec![ScorePatch::ReplaceScore {
2154 score: Box::new(b.clone()),
2155 }];
2156 }
2157
2158 if a.texts != b.texts {
2159 patches.push(ScorePatch::SetScoreTexts {
2160 value: b.texts.clone(),
2161 });
2162 }
2163 if a.chord_definitions != b.chord_definitions {
2164 patches.push(ScorePatch::SetChordDefinitions {
2165 value: b.chord_definitions.clone(),
2166 });
2167 }
2168
2169 macro_rules! meta {
2170 ($field:ident, $name:literal) => {
2171 if a.metadata.$field != b.metadata.$field {
2172 patches.push(ScorePatch::SetMetadata {
2173 field: $name.to_string(),
2174 value: b.metadata.$field.clone(),
2175 });
2176 }
2177 };
2178 }
2179 meta!(title, "title");
2180 meta!(composer, "composer");
2181 meta!(lyricist, "lyricist");
2182 meta!(copyright, "copyright");
2183 meta!(work_number, "work_number");
2184 meta!(movement_title, "movement_title");
2185
2186 if a.settings.tempo_bpm != b.settings.tempo_bpm {
2187 patches.push(ScorePatch::SetTempo {
2188 value: b.settings.tempo_bpm,
2189 });
2190 }
2191
2192 for pi in 0..a.parts.len().min(b.parts.len()) {
2193 let ap = &a.parts[pi];
2194 let bp = &b.parts[pi];
2195 if ap.name != bp.name || ap.short_name != bp.short_name {
2196 patches.push(ScorePatch::SetPartNames {
2197 part: pi,
2198 name: bp.name.clone(),
2199 short_name: bp.short_name.clone(),
2200 });
2201 }
2202 if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
2203 patches.push(ScorePatch::SetPartMidi {
2204 part: pi,
2205 channel: bp.midi_channel,
2206 program: bp.midi_program,
2207 });
2208 }
2209 if ap.midi_pitch_bends != bp.midi_pitch_bends
2210 || ap.midi_control_changes != bp.midi_control_changes
2211 || ap.midi_program_changes != bp.midi_program_changes
2212 || ap.midi_aftertouch != bp.midi_aftertouch
2213 {
2214 patches.push(ScorePatch::SetPartMidiAutomation {
2215 part: pi,
2216 pitch_bends: bp.midi_pitch_bends.clone(),
2217 control_changes: bp.midi_control_changes.clone(),
2218 program_changes: bp.midi_program_changes.clone(),
2219 aftertouch: bp.midi_aftertouch.clone(),
2220 });
2221 }
2222 for si in 0..ap.staves.len().min(bp.staves.len()) {
2223 let a_staff = &ap.staves[si];
2224 let b_staff = &bp.staves[si];
2225 if a_staff.clef != b_staff.clef
2226 || a_staff.transpose_semitones != b_staff.transpose_semitones
2227 {
2228 patches.push(ScorePatch::SetStaffConfiguration {
2229 part: pi,
2230 staff: si,
2231 clef: b_staff.clef.clone(),
2232 transpose_semitones: b_staff.transpose_semitones,
2233 });
2234 }
2235 if a_staff.tablature != b_staff.tablature {
2236 patches.push(ScorePatch::SetTablatureConfig {
2237 part: pi,
2238 staff: si,
2239 value: b_staff.tablature.clone(),
2240 });
2241 }
2242 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
2243 let am = &a_staff.measures[mi];
2244 let bm = &b_staff.measures[mi];
2245
2246 if am.number != bm.number
2247 || am.clef != bm.clef
2248 || am.tempo_text != bm.tempo_text
2249 || am.navigation != bm.navigation
2250 || am.expression_text != bm.expression_text
2251 || am.multi_rest_count != bm.multi_rest_count
2252 || am.system_break != bm.system_break
2253 || am.page_break != bm.page_break
2254 {
2255 patches.push(ScorePatch::SetMeasurePresentation {
2256 part: pi,
2257 staff: si,
2258 measure: mi,
2259 number: bm.number,
2260 clef: bm.clef.clone(),
2261 tempo_text: bm.tempo_text.clone(),
2262 navigation: bm.navigation.clone(),
2263 expression_text: bm.expression_text.clone(),
2264 multi_rest_count: bm.multi_rest_count,
2265 system_break: bm.system_break,
2266 page_break: bm.page_break,
2267 });
2268 }
2269 if am.key_sig != bm.key_sig {
2270 patches.push(ScorePatch::SetKeySignature {
2271 part: pi,
2272 staff: si,
2273 measure: mi,
2274 value: bm.key_sig.clone(),
2275 });
2276 }
2277 if am.time_sig != bm.time_sig {
2278 patches.push(ScorePatch::SetTimeSignature {
2279 part: pi,
2280 staff: si,
2281 measure: mi,
2282 value: bm.time_sig.clone(),
2283 });
2284 }
2285 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
2286 patches.push(ScorePatch::SetBarlines {
2287 part: pi,
2288 staff: si,
2289 measure: mi,
2290 left: bm.barline_left.clone(),
2291 right: bm.barline_right.clone(),
2292 });
2293 }
2294 if am.rehearsal != bm.rehearsal {
2295 patches.push(ScorePatch::SetRehearsal {
2296 part: pi,
2297 staff: si,
2298 measure: mi,
2299 value: bm.rehearsal.clone(),
2300 });
2301 }
2302 if am.volta != bm.volta {
2303 patches.push(ScorePatch::SetVolta {
2304 part: pi,
2305 staff: si,
2306 measure: mi,
2307 value: bm.volta.clone(),
2308 });
2309 }
2310 if am.tempo != bm.tempo {
2311 patches.push(ScorePatch::SetMeasureTempo {
2312 part: pi,
2313 staff: si,
2314 measure: mi,
2315 value: bm.tempo,
2316 });
2317 }
2318 if am.texts != bm.texts {
2319 patches.push(ScorePatch::SetMeasureTexts {
2320 part: pi,
2321 staff: si,
2322 measure: mi,
2323 value: bm.texts.clone(),
2324 });
2325 }
2326 if am.figured_bass != bm.figured_bass {
2327 patches.push(ScorePatch::SetFiguredBass {
2328 part: pi,
2329 staff: si,
2330 measure: mi,
2331 value: bm.figured_bass.clone(),
2332 });
2333 }
2334
2335 for vi in 0..4usize {
2336 let av = &am.voices[vi];
2337 let bv = &bm.voices[vi];
2338 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
2339 if !note_content_eq(a_note, b_note) {
2340 patches.push(ScorePatch::ReplaceNote {
2341 part: pi,
2342 staff: si,
2343 measure: mi,
2344 voice: vi,
2345 note_index: ni,
2346 note: Box::new(b_note.clone()),
2347 });
2348 }
2349 }
2350 for ni in (bv.len()..av.len()).rev() {
2352 patches.push(ScorePatch::RemoveNote {
2353 part: pi,
2354 staff: si,
2355 measure: mi,
2356 voice: vi,
2357 note_index: ni,
2358 });
2359 }
2360 for (offset, note) in bv.iter().skip(av.len()).enumerate() {
2362 patches.push(ScorePatch::AddNote {
2363 part: pi,
2364 staff: si,
2365 measure: mi,
2366 voice: vi,
2367 note_index: av.len() + offset,
2368 note: Box::new(note.clone()),
2369 });
2370 }
2371 }
2372 }
2373 }
2374 }
2375
2376 patches
2377}
2378
2379pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
2384 let mut s = score.clone();
2385 for patch in patches {
2386 match patch {
2387 ScorePatch::ReplaceScore { score } => {
2388 s = (**score).clone();
2389 }
2390 ScorePatch::SetScoreTexts { value } => {
2391 s.texts = value.clone();
2392 }
2393 ScorePatch::SetMeasureTexts {
2394 part,
2395 staff,
2396 measure,
2397 value,
2398 } => {
2399 s.parts
2400 .get_mut(*part)
2401 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2402 .staves
2403 .get_mut(*staff)
2404 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2405 .measures
2406 .get_mut(*measure)
2407 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2408 .texts = value.clone();
2409 }
2410 ScorePatch::SetFiguredBass {
2411 part,
2412 staff,
2413 measure,
2414 value,
2415 } => {
2416 s.parts
2417 .get_mut(*part)
2418 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2419 .staves
2420 .get_mut(*staff)
2421 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2422 .measures
2423 .get_mut(*measure)
2424 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2425 .figured_bass = value.clone();
2426 }
2427 ScorePatch::SetChordDefinitions { value } => {
2428 s.chord_definitions = value.clone();
2429 }
2430 ScorePatch::SetPartNames {
2431 part,
2432 name,
2433 short_name,
2434 } => {
2435 let target = s
2436 .parts
2437 .get_mut(*part)
2438 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
2439 target.name = name.clone();
2440 target.short_name = short_name.clone();
2441 }
2442 ScorePatch::SetPartMidi {
2443 part,
2444 channel,
2445 program,
2446 } => {
2447 let target = s
2448 .parts
2449 .get_mut(*part)
2450 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
2451 target.midi_channel = *channel;
2452 target.midi_program = *program;
2453 }
2454 ScorePatch::SetPartMidiAutomation {
2455 part,
2456 pitch_bends,
2457 control_changes,
2458 program_changes,
2459 aftertouch,
2460 } => {
2461 let target = s
2462 .parts
2463 .get_mut(*part)
2464 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
2465 target.midi_pitch_bends = pitch_bends.clone();
2466 target.midi_control_changes = control_changes.clone();
2467 target.midi_program_changes = program_changes.clone();
2468 target.midi_aftertouch = aftertouch.clone();
2469 }
2470 ScorePatch::SetStaffConfiguration {
2471 part,
2472 staff,
2473 clef,
2474 transpose_semitones,
2475 } => {
2476 let target = s
2477 .parts
2478 .get_mut(*part)
2479 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2480 .staves
2481 .get_mut(*staff)
2482 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
2483 target.clef = clef.clone();
2484 target.transpose_semitones = *transpose_semitones;
2485 }
2486 ScorePatch::SetMeasurePresentation {
2487 part,
2488 staff,
2489 measure,
2490 number,
2491 clef,
2492 tempo_text,
2493 navigation,
2494 expression_text,
2495 multi_rest_count,
2496 system_break,
2497 page_break,
2498 } => {
2499 let target = s
2500 .parts
2501 .get_mut(*part)
2502 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2503 .staves
2504 .get_mut(*staff)
2505 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2506 .measures
2507 .get_mut(*measure)
2508 .ok_or_else(|| {
2509 Error::InvalidPatch(format!("measure {measure} out of range"))
2510 })?;
2511 target.number = *number;
2512 target.clef = clef.clone();
2513 target.tempo_text = tempo_text.clone();
2514 target.navigation = navigation.clone();
2515 target.expression_text = expression_text.clone();
2516 target.multi_rest_count = *multi_rest_count;
2517 target.system_break = *system_break;
2518 target.page_break = *page_break;
2519 }
2520 ScorePatch::SetTablatureConfig { part, staff, value } => {
2521 s.parts
2522 .get_mut(*part)
2523 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2524 .staves
2525 .get_mut(*staff)
2526 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2527 .tablature = value.clone();
2528 }
2529 ScorePatch::SetMetadata { field, value } => match field.as_str() {
2530 "title" => s.metadata.title = value.clone(),
2531 "composer" => s.metadata.composer = value.clone(),
2532 "lyricist" => s.metadata.lyricist = value.clone(),
2533 "copyright" => s.metadata.copyright = value.clone(),
2534 "work_number" => s.metadata.work_number = value.clone(),
2535 "movement_title" => s.metadata.movement_title = value.clone(),
2536 other => {
2537 return Err(Error::InvalidPatch(format!(
2538 "unknown metadata field: {other}"
2539 )));
2540 }
2541 },
2542 ScorePatch::SetTempo { value } => {
2543 s.settings.tempo_bpm = *value;
2544 }
2545 ScorePatch::SetKeySignature {
2546 part,
2547 staff,
2548 measure,
2549 value,
2550 } => {
2551 s.parts
2552 .get_mut(*part)
2553 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2554 .staves
2555 .get_mut(*staff)
2556 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2557 .measures
2558 .get_mut(*measure)
2559 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2560 .key_sig = value.clone();
2561 }
2562 ScorePatch::SetTimeSignature {
2563 part,
2564 staff,
2565 measure,
2566 value,
2567 } => {
2568 s.parts
2569 .get_mut(*part)
2570 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2571 .staves
2572 .get_mut(*staff)
2573 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2574 .measures
2575 .get_mut(*measure)
2576 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2577 .time_sig = value.clone();
2578 }
2579 ScorePatch::SetBarlines {
2580 part,
2581 staff,
2582 measure,
2583 left,
2584 right,
2585 } => {
2586 let m = s
2587 .parts
2588 .get_mut(*part)
2589 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2590 .staves
2591 .get_mut(*staff)
2592 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2593 .measures
2594 .get_mut(*measure)
2595 .ok_or_else(|| {
2596 Error::InvalidPatch(format!("measure {measure} out of range"))
2597 })?;
2598 m.barline_left = left.clone();
2599 m.barline_right = right.clone();
2600 }
2601 ScorePatch::SetRehearsal {
2602 part,
2603 staff,
2604 measure,
2605 value,
2606 } => {
2607 s.parts
2608 .get_mut(*part)
2609 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2610 .staves
2611 .get_mut(*staff)
2612 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2613 .measures
2614 .get_mut(*measure)
2615 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2616 .rehearsal = value.clone();
2617 }
2618 ScorePatch::SetVolta {
2619 part,
2620 staff,
2621 measure,
2622 value,
2623 } => {
2624 s.parts
2625 .get_mut(*part)
2626 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2627 .staves
2628 .get_mut(*staff)
2629 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2630 .measures
2631 .get_mut(*measure)
2632 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2633 .volta = value.clone();
2634 }
2635 ScorePatch::AddNote {
2636 part,
2637 staff,
2638 measure,
2639 voice,
2640 note_index,
2641 note,
2642 } => {
2643 let v = s
2644 .parts
2645 .get_mut(*part)
2646 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2647 .staves
2648 .get_mut(*staff)
2649 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2650 .measures
2651 .get_mut(*measure)
2652 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2653 .voices
2654 .get_mut(*voice)
2655 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
2656 let insert_at = if *note_index == usize::MAX {
2657 v.len()
2658 } else {
2659 *note_index
2660 };
2661 if insert_at > v.len() {
2662 return Err(Error::InvalidPatch(format!(
2663 "note_index {note_index} out of range"
2664 )));
2665 }
2666 v.insert(insert_at, *note.clone());
2667 }
2668 ScorePatch::RemoveNote {
2669 part,
2670 staff,
2671 measure,
2672 voice,
2673 note_index,
2674 } => {
2675 let v = s
2676 .parts
2677 .get_mut(*part)
2678 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2679 .staves
2680 .get_mut(*staff)
2681 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2682 .measures
2683 .get_mut(*measure)
2684 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2685 .voices
2686 .get_mut(*voice)
2687 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
2688 if *note_index >= v.len() {
2689 return Err(Error::InvalidPatch(format!(
2690 "note_index {note_index} out of range"
2691 )));
2692 }
2693 v.remove(*note_index);
2694 }
2695 ScorePatch::ReplaceNote {
2696 part,
2697 staff,
2698 measure,
2699 voice,
2700 note_index,
2701 note,
2702 } => {
2703 let v = s
2704 .parts
2705 .get_mut(*part)
2706 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2707 .staves
2708 .get_mut(*staff)
2709 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2710 .measures
2711 .get_mut(*measure)
2712 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2713 .voices
2714 .get_mut(*voice)
2715 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
2716 if *note_index >= v.len() {
2717 return Err(Error::InvalidPatch(format!(
2718 "note_index {note_index} out of range"
2719 )));
2720 }
2721 v[*note_index] = *note.clone();
2722 }
2723 ScorePatch::SetMeasureTempo {
2724 part,
2725 staff,
2726 measure,
2727 value,
2728 } => {
2729 s.parts
2730 .get_mut(*part)
2731 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
2732 .staves
2733 .get_mut(*staff)
2734 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
2735 .measures
2736 .get_mut(*measure)
2737 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
2738 .tempo = *value;
2739 }
2740 }
2741 }
2742 if !super::validate::validate(&s).is_valid() {
2743 return Err(Error::InvalidScore);
2744 }
2745 Ok(s)
2746}
2747
2748pub fn respell_score(score: &mut Score, prefer_flat: bool) {
2752 for part in &mut score.parts {
2753 for staff in &mut part.staves {
2754 for measure in &mut staff.measures {
2755 for voice in &mut measure.voices {
2756 for note in voice.iter_mut() {
2757 for pitch in &mut note.pitches {
2758 *pitch = pitch.respell(prefer_flat);
2759 }
2760 }
2761 }
2762 }
2763 }
2764 }
2765}
2766
2767pub fn respell_score_to_key(score: &mut Score) {
2771 let prefer_flat = score.settings.key_signature.fifths < 0;
2772 respell_score(score, prefer_flat);
2773}
2774
2775pub fn score_duration_secs(score: &Score) -> f64 {
2780 if score.settings.tempo_bpm == 0 {
2781 return 0.0;
2782 }
2783 let seq = measure_sequence(score);
2784 let mut total_secs = 0.0f64;
2785 let mut current_bpm = score.settings.tempo_bpm as f64;
2786 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
2787 for &idx in &seq {
2788 if let Some(m) = staff.measures.get(idx) {
2789 if let Some(b) = m.tempo {
2790 current_bpm = b as f64;
2791 }
2792 if current_bpm == 0.0 {
2793 continue;
2794 }
2795 let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
2796 total_secs += beats / current_bpm * 60.0;
2797 }
2798 }
2799 }
2800 total_secs
2801}
2802
2803pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
2808 if score.settings.tempo_bpm == 0 {
2809 return 0.0;
2810 }
2811 let seq: Vec<usize> = measure_sequence(score)
2812 .into_iter()
2813 .filter(|&idx| idx >= region.0 && idx <= region.1)
2814 .collect();
2815 let mut total_secs = 0.0f64;
2816 let mut current_bpm = score.settings.tempo_bpm as f64;
2817 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
2818 for &idx in &seq {
2819 if let Some(m) = staff.measures.get(idx) {
2820 if let Some(b) = m.tempo {
2821 current_bpm = b as f64;
2822 }
2823 if current_bpm == 0.0 {
2824 continue;
2825 }
2826 let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
2827 total_secs += beats / current_bpm * 60.0;
2828 }
2829 }
2830 }
2831 total_secs
2832}
2833
2834pub fn measure_beats_remaining(
2839 score: &Score,
2840 part_index: usize,
2841 staff_index: usize,
2842 measure_index: usize,
2843 voice_index: usize,
2844) -> Result<f64, Error> {
2845 let part = score
2846 .parts
2847 .get(part_index)
2848 .ok_or(Error::PartNotFound(part_index))?;
2849 let staff = part
2850 .staves
2851 .get(staff_index)
2852 .ok_or(Error::StaffNotFound(staff_index))?;
2853 let measure = staff
2854 .measures
2855 .get(measure_index)
2856 .ok_or(Error::MeasureNotFound(measure_index))?;
2857 let voice = measure
2858 .voices
2859 .get(voice_index)
2860 .ok_or(Error::VoiceOutOfRange(voice_index))?;
2861 let ts = measure
2862 .time_sig
2863 .as_ref()
2864 .unwrap_or(&score.settings.time_signature);
2865 let used: f64 = voice.iter().map(|n| n.beats()).sum();
2866 Ok((ts.total_beats() - used).max(0.0))
2867}
2868
2869pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
2875 if pitches.is_empty() {
2876 return true;
2877 }
2878 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
2879 avg < clef.middle_line_midi() as f64
2880}
2881
2882fn beam_beat_size(ts: &TimeSignature) -> f64 {
2883 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
2884 3.0 * 4.0 / ts.denominator as f64
2885 } else {
2886 4.0 / ts.denominator as f64
2887 }
2888}
2889
2890pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
2895 let beat_size = beam_beat_size(time_sig);
2896 let n = notes.len();
2897 let mut result = vec![BeamState::None; n];
2898
2899 let is_beamable = |note: &Note| -> bool {
2900 !note.is_rest
2901 && matches!(
2902 note.duration,
2903 Duration::Eighth
2904 | Duration::Sixteenth
2905 | Duration::ThirtySecond
2906 | Duration::SixtyFourth
2907 )
2908 };
2909
2910 let mut starts = Vec::with_capacity(n);
2912 let mut pos = 0.0f64;
2913 for note in notes {
2914 starts.push(pos);
2915 pos += note.beats();
2916 }
2917
2918 let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
2920
2921 let mut i = 0;
2922 while i < n {
2923 if !is_beamable(¬es[i]) {
2924 i += 1;
2925 continue;
2926 }
2927 let g = group_id(i);
2928 let mut j = i;
2930 while j < n && is_beamable(¬es[j]) && group_id(j) == g {
2931 j += 1;
2932 }
2933 let run = j - i;
2934 if run == 1 {
2935 result[i] = BeamState::None;
2936 } else {
2937 result[i] = BeamState::Begin;
2938 result[i + 1..j - 1].fill(BeamState::Continue);
2939 result[j - 1] = BeamState::End;
2940 }
2941 i = j;
2942 }
2943 result
2944}
2945
2946fn note_content_eq(a: &Note, b: &Note) -> bool {
2947 a.is_rest == b.is_rest
2948 && a.is_unpitched == b.is_unpitched
2949 && a.instrument_id == b.instrument_id
2950 && a.offset_x == b.offset_x
2951 && a.offset_y == b.offset_y
2952 && a.relative_x == b.relative_x
2953 && a.relative_y == b.relative_y
2954 && a.pitches == b.pitches
2955 && a.duration == b.duration
2956 && a.dot_count == b.dot_count
2957 && a.tie_start == b.tie_start
2958 && a.tie_end == b.tie_end
2959 && a.beam == b.beam
2960 && a.articulations == b.articulations
2961 && a.dynamic == b.dynamic
2962 && a.stem_up == b.stem_up
2963 && a.hairpin_start == b.hairpin_start
2964 && a.hairpin_end == b.hairpin_end
2965 && a.tuplet == b.tuplet
2966 && a.chord_symbol == b.chord_symbol
2967 && a.is_grace == b.is_grace
2968 && a.grace_slash == b.grace_slash
2969 && a.ottava_start == b.ottava_start
2970 && a.ottava_end == b.ottava_end
2971 && a.lyric == b.lyric
2972 && a.pedal_start == b.pedal_start
2973 && a.pedal_end == b.pedal_end
2974 && a.slur_start == b.slur_start
2975 && a.slur_end == b.slur_end
2976 && a.arpeggiate == b.arpeggiate
2977 && a.tab_position == b.tab_position
2978 && a.tab_positions == b.tab_positions
2979 && a.guitar_technique == b.guitar_technique
2980 && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
2981}
2982
2983#[cfg(test)]
2984mod tests {
2985 use super::*;
2986 use crate::model::{
2987 notation::{FingeringSelectionPolicy, TextStyle},
2988 pitch::Step,
2989 };
2990
2991 #[test]
2992 fn fingering_selection_policy_is_deterministic_and_non_mutating() {
2993 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
2994 note.fingerings = vec![3, 1, 4];
2995 note.fingering = Some(3);
2996 assert_eq!(
2997 note.select_fingering(FingeringSelectionPolicy::SourceOrder),
2998 Some(3)
2999 );
3000 assert_eq!(
3001 note.select_fingering(FingeringSelectionPolicy::LowestNumber),
3002 Some(1)
3003 );
3004 assert_eq!(
3005 note.select_fingering(FingeringSelectionPolicy::HighestNumber),
3006 Some(4)
3007 );
3008 assert_eq!(note.fingerings, vec![3, 1, 4]);
3009 assert_eq!(note.fingering, Some(3));
3010 }
3011
3012 #[test]
3013 fn default_score_has_one_part_four_measures() {
3014 let score = Score::default();
3015 assert_eq!(score.parts.len(), 1);
3016 assert_eq!(score.parts[0].staves.len(), 1);
3017 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
3018 }
3019
3020 #[test]
3021 fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
3022 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
3023 score.parts[0].staves[0].tablature = Some(TablatureConfig {
3024 lines: 6,
3025 tuning_midi: vec![64, 59, 55, 50, 45, 40],
3026 capo: 2,
3027 });
3028 score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
3029 Pitch::with_alter(Step::F, 4, 1),
3030 Duration::Quarter,
3031 ));
3032 score.parts[0].staves[0].measures[0].voices[0]
3033 .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
3034 score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
3035 Some(TabPosition { string: 6, fret: 7 });
3036
3037 assert_eq!(assign_tablature_positions(&mut score), 1);
3038 let notes = &score.parts[0].staves[0].measures[0].voices[0];
3039 assert_eq!(
3040 notes[1].tab_position,
3041 Some(TabPosition { string: 1, fret: 0 })
3042 );
3043 assert_eq!(notes[1].string_number, Some(1));
3044 assert_eq!(
3045 notes[2].tab_position,
3046 Some(TabPosition { string: 6, fret: 7 })
3047 );
3048 }
3049
3050 #[test]
3051 fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
3052 let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
3053 score.parts[0].staves[0].tablature = Some(TablatureConfig {
3054 lines: 6,
3055 tuning_midi: vec![64, 59, 55, 50, 45, 40],
3056 capo: 0,
3057 });
3058 let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
3059 chord.pitches.push(Pitch::new(Step::G, 4));
3060 score.parts[0].staves[0].measures[0].voices[0].push(chord);
3061
3062 assert_eq!(assign_tablature_positions(&mut score), 1);
3063 let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
3064 assert_eq!(
3065 positions,
3066 &vec![
3067 TabPosition { string: 2, fret: 5 },
3068 TabPosition { string: 1, fret: 3 },
3069 ]
3070 );
3071 }
3072
3073 #[test]
3074 fn new_score_measure_count() {
3075 let score = Score::new("Test", 120, 4, 4, 0, 8);
3076 assert_eq!(score.measure_count(), 8);
3077 }
3078
3079 #[test]
3080 fn note_beats_quarter() {
3081 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3082 assert!((note.beats() - 1.0).abs() < 1e-9);
3083 }
3084
3085 #[test]
3086 fn note_beats_dotted_quarter() {
3087 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3088 note.dot_count = 1;
3089 assert!((note.beats() - 1.5).abs() < 1e-9);
3090 }
3091
3092 #[test]
3093 fn grace_note_beats_zero() {
3094 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
3095 note.is_grace = true;
3096 assert_eq!(note.beats(), 0.0);
3097 }
3098
3099 #[test]
3100 fn measure_empty_4_4_fills_four_beats() {
3101 let m = Measure::empty(4, 4);
3102 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
3103 assert!((total - 4.0).abs() < 1e-9);
3104 }
3105
3106 #[test]
3107 fn measure_empty_3_4_fills_three_beats() {
3108 let m = Measure::empty(3, 4);
3109 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
3110 assert!((total - 3.0).abs() < 1e-9);
3111 }
3112
3113 #[test]
3114 fn whole_filling_beats() {
3115 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
3116 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
3117 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
3118 }
3119
3120 #[test]
3123 fn statistics_default_score_all_rests() {
3124 let score = Score::default();
3125 let s = score.statistics();
3126 assert_eq!(s.part_count, 1);
3127 assert_eq!(s.measure_count, 4);
3128 assert_eq!(s.note_count, 0);
3129 assert!(s.rest_count > 0);
3130 }
3131
3132 #[test]
3133 fn statistics_duration_estimate() {
3134 let score = Score::new("T", 120, 4, 4, 0, 1);
3136 let s = score.statistics();
3137 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
3138 }
3139
3140 #[test]
3141 fn score_duration_secs_matches_statistics() {
3142 use super::score_duration_secs;
3143 let score = Score::new("T", 120, 4, 4, 0, 4);
3144 let secs = score_duration_secs(&score);
3145 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
3147 }
3148
3149 #[test]
3150 fn score_duration_secs_zero_bpm_returns_zero() {
3151 use super::score_duration_secs;
3152 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3153 score.settings.tempo_bpm = 0;
3154 assert_eq!(score_duration_secs(&score), 0.0);
3155 }
3156
3157 #[test]
3158 fn score_duration_secs_per_measure_tempo() {
3159 use super::score_duration_secs;
3160 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3162 score.parts[0].staves[0].measures[1].tempo = Some(60);
3163 let secs = score_duration_secs(&score);
3164 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
3165 }
3166
3167 #[test]
3170 fn extract_part_returns_single_part_score() {
3171 let mut score = Score::default();
3172 let mut p2 = Part::new("Violin", "Vln.");
3173 p2.staves.push(Staff::new(Clef::Treble));
3174 score.parts.push(p2);
3175 let ex = score.extract_part(0).unwrap();
3176 assert_eq!(ex.parts.len(), 1);
3177 assert_ne!(ex.id, score.id);
3178 assert_eq!(ex.metadata.title, score.metadata.title);
3179 }
3180
3181 #[test]
3182 fn extract_part_out_of_range_is_none() {
3183 let score = Score::default();
3184 assert!(score.extract_part(99).is_none());
3185 }
3186
3187 #[test]
3188 fn extract_and_merge_remap_typed_spanner_part_addresses() {
3189 let mut left = Score::template(ScoreTemplate::StringQuartet);
3190 let address = NoteAddr {
3191 part: 1,
3192 staff: 0,
3193 measure: 0,
3194 voice: 0,
3195 note: 0,
3196 };
3197 left.spanners.push(NotationSpanner {
3198 id: "left-span".to_string(),
3199 kind: NotationSpannerKind::Slur,
3200 start: address.clone(),
3201 end: address,
3202 number: Some(1),
3203 line_type: None,
3204 text: None,
3205 placement: None,
3206 ottava_size: None,
3207 ottava_type: None,
3208 });
3209 let extracted = left.extract_part_checked(1).expect("valid extracted part");
3210 assert_eq!(extracted.spanners[0].start.part, 0);
3211 assert_eq!(extracted.spanners[0].end.part, 0);
3212
3213 let mut right = Score::new("R", 120, 4, 4, 0, 1);
3214 let right_address = NoteAddr {
3215 part: 0,
3216 staff: 0,
3217 measure: 0,
3218 voice: 0,
3219 note: 0,
3220 };
3221 right.spanners.push(NotationSpanner {
3222 id: "right-span".to_string(),
3223 kind: NotationSpannerKind::Pedal,
3224 start: right_address.clone(),
3225 end: right_address,
3226 number: Some(1),
3227 line_type: None,
3228 text: None,
3229 placement: None,
3230 ottava_size: None,
3231 ottava_type: None,
3232 });
3233 let merged = left.merge_checked(&right).expect("valid merged score");
3234 let right_span = merged
3235 .spanners
3236 .iter()
3237 .find(|spanner| spanner.id == "right-span")
3238 .expect("merged right span");
3239 assert_eq!(right_span.start.part, left.parts.len());
3240 assert_eq!(right_span.end.part, left.parts.len());
3241 }
3242
3243 #[test]
3246 fn transpose_zero_is_clone() {
3247 let score = Score::new("T", 120, 4, 4, 0, 1);
3248 let t = transpose(&score, 0);
3249 assert_eq!(t.settings.key_signature.fifths, 0);
3250 }
3251
3252 #[test]
3253 fn transpose_c_major_up_2_to_d_major() {
3254 let score = Score::new("T", 120, 4, 4, 0, 1);
3255 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
3256 }
3257
3258 #[test]
3259 fn transpose_d_major_up_5_to_g_major() {
3260 let score = Score::new("T", 120, 4, 4, 2, 1);
3261 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
3262 }
3263
3264 #[test]
3265 fn transpose_c4_up_1_to_csharp4() {
3266 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
3267 assert_eq!(p.to_midi(), 61);
3268 assert_eq!(p.step, Step::C);
3269 assert_eq!(p.alter, 1);
3270 }
3271
3272 #[test]
3273 fn transpose_c4_down_1_to_b3() {
3274 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
3275 assert_eq!(p.to_midi(), 59);
3276 assert_eq!(p.step, Step::B);
3277 assert_eq!(p.alter, 0);
3278 }
3279
3280 #[test]
3281 fn transpose_up_octave_keeps_step() {
3282 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
3283 assert_eq!(p.to_midi(), 81);
3284 assert_eq!(p.step, Step::A);
3285 assert_eq!(p.octave, 5);
3286 }
3287
3288 #[test]
3289 fn statistics_with_repeat_doubles_duration() {
3290 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3292 score.parts[0].staves[0].measures[0].barline_left =
3293 crate::model::notation::Barline::RepeatStart;
3294 score.parts[0].staves[0].measures[1].barline_right =
3295 crate::model::notation::Barline::RepeatEnd;
3296 let s = score.statistics();
3297 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
3299 }
3300
3301 #[test]
3302 fn transpose_octave_boundary_b4_to_c5() {
3303 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
3305 assert_eq!(p.to_midi(), 72);
3306 assert_eq!(p.step, Step::C);
3307 assert_eq!(p.octave, 5);
3308 }
3309
3310 #[test]
3311 fn transpose_clamp_at_midi_127() {
3312 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
3314 assert_eq!(p.to_midi(), 127);
3315 }
3316
3317 #[test]
3320 fn merge_combines_parts() {
3321 let mut a = Score::new("A", 120, 4, 4, 0, 2);
3322 let b = Score::new("B", 120, 4, 4, 0, 2);
3323 let mut p2 = Part::new("Violin", "Vln.");
3325 p2.staves.push(Staff::new(Clef::Treble));
3326 for i in 0..2usize {
3327 let mut m = Measure::empty(4, 4);
3328 m.number = i as u32 + 1;
3329 p2.staves[0].measures.push(m);
3330 }
3331 a.parts.push(p2);
3332 let merged = a.merge(&b);
3333 assert_eq!(merged.parts.len(), 3);
3335 }
3336
3337 #[test]
3338 fn merge_pads_shorter_score() {
3339 let a = Score::new("A", 120, 4, 4, 0, 4);
3340 let b = Score::new("B", 120, 4, 4, 0, 2);
3341 let merged = a.merge(&b);
3342 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
3344 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
3345 }
3346
3347 #[test]
3348 fn merge_uses_self_metadata() {
3349 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
3350 a.metadata.composer = "Composer A".to_string();
3351 let b = Score::new("Title B", 120, 4, 4, 0, 2);
3352 let merged = a.merge(&b);
3353 assert_eq!(merged.metadata.title, "Title A");
3354 assert_eq!(merged.metadata.composer, "Composer A");
3355 }
3356
3357 #[test]
3358 fn merge_new_id_differs_from_both() {
3359 let a = Score::new("A", 120, 4, 4, 0, 2);
3360 let b = Score::new("B", 120, 4, 4, 0, 2);
3361 let merged = a.merge(&b);
3362 assert_ne!(merged.id, a.id);
3363 assert_ne!(merged.id, b.id);
3364 }
3365
3366 #[test]
3369 fn staff_default_transpose_is_zero() {
3370 let s = Staff::new(Clef::Treble);
3371 assert_eq!(s.transpose_semitones, 0);
3372 }
3373
3374 #[test]
3377 fn score_default_has_schema_version_1() {
3378 let score = Score::default();
3379 assert_eq!(score.schema_version, 1);
3380 }
3381
3382 #[test]
3383 fn score_new_has_schema_version_1() {
3384 let score = Score::new("T", 120, 4, 4, 0, 4);
3385 assert_eq!(score.schema_version, 1);
3386 }
3387
3388 #[test]
3389 fn score_without_schema_version_deserializes_to_zero() {
3390 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":[]}"#;
3391 let score: Score = serde_json::from_str(json).unwrap();
3392 assert_eq!(score.schema_version, 0);
3393 }
3394
3395 #[test]
3396 fn legacy_score_json_defaults_typed_spanners() {
3397 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
3398 let mut value = serde_json::to_value(score).expect("score serializes");
3399 value
3400 .as_object_mut()
3401 .expect("score is an object")
3402 .remove("spanners");
3403 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
3404 assert!(restored.spanners.is_empty());
3405 }
3406
3407 #[test]
3408 fn legacy_measure_json_defaults_source_voice_numbers() {
3409 let score = Score::new("Legacy", 120, 4, 4, 0, 1);
3410 let mut value = serde_json::to_value(&score).expect("score serializes");
3411 value["parts"][0]["staves"][0]["measures"][0]
3412 .as_object_mut()
3413 .expect("measure is an object")
3414 .remove("source_voice_numbers");
3415
3416 let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
3417 assert_eq!(
3418 restored.parts[0].staves[0].measures[0].source_voice_numbers,
3419 [None; 4]
3420 );
3421 }
3422
3423 #[test]
3424 fn note_without_new_percussion_fields_uses_serde_defaults() {
3425 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3426 let mut value = serde_json::to_value(note).unwrap();
3427 let object = value.as_object_mut().unwrap();
3428 object.remove("is_unpitched");
3429 object.remove("instrument_id");
3430 let restored: Note = serde_json::from_value(value).unwrap();
3431 assert!(!restored.is_unpitched);
3432 assert_eq!(restored.instrument_id, None);
3433 }
3434
3435 #[test]
3436 fn percussion_instrument_resolution_prefers_id_then_display_key() {
3437 let mut part = Part::new("Drums", "Dr.");
3438 part.percussion_instruments = vec![
3439 PercussionInstrument {
3440 id: "snare".to_string(),
3441 name: Some("Acoustic Snare".to_string()),
3442 midi_unpitched: Some(38),
3443 },
3444 PercussionInstrument {
3445 id: "rim".to_string(),
3446 name: Some("Side Stick".to_string()),
3447 midi_unpitched: Some(37),
3448 },
3449 ];
3450 let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
3451 note.is_unpitched = true;
3452 assert_eq!(
3453 part.percussion_instrument_for_note(¬e)
3454 .map(|instrument| instrument.id.as_str()),
3455 Some("snare")
3456 );
3457 note.instrument_id = Some("rim".to_string());
3458 assert_eq!(
3459 part.percussion_instrument_for_note(¬e)
3460 .map(|instrument| instrument.id.as_str()),
3461 Some("rim")
3462 );
3463 note.instrument_id = Some("missing".to_string());
3464 assert!(part.percussion_instrument_for_note(¬e).is_none());
3465 note.instrument_id = None;
3466 note.is_unpitched = false;
3467 assert!(part.percussion_instrument_for_note(¬e).is_none());
3468 }
3469
3470 #[test]
3473 fn score_template_solo_has_one_part_treble() {
3474 let score = Score::template(ScoreTemplate::Solo);
3475 assert_eq!(score.parts.len(), 1);
3476 assert_eq!(score.parts[0].staves.len(), 1);
3477 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
3478 assert_eq!(score.parts[0].midi_program, 0);
3479 }
3480
3481 #[test]
3482 fn score_template_piano_has_two_staves() {
3483 let score = Score::template(ScoreTemplate::Piano);
3484 assert_eq!(score.parts.len(), 1);
3485 assert_eq!(score.parts[0].staves.len(), 2);
3486 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
3487 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
3488 }
3489
3490 #[test]
3491 fn score_template_string_quartet_has_four_parts() {
3492 let score = Score::template(ScoreTemplate::StringQuartet);
3493 assert_eq!(score.parts.len(), 4);
3494 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);
3497 assert_eq!(score.parts[3].midi_program, 42);
3498 }
3499
3500 #[test]
3501 fn score_template_string_orchestra_has_five_parts() {
3502 let score = Score::template(ScoreTemplate::StringOrchestra);
3503 assert_eq!(score.parts.len(), 5);
3504 assert_eq!(score.parts[4].midi_program, 43); }
3506
3507 #[test]
3508 fn score_template_brass_quintet_has_five_parts() {
3509 let score = Score::template(ScoreTemplate::BrassQuintet);
3510 assert_eq!(score.parts.len(), 5);
3511 assert_eq!(score.parts[2].midi_program, 60); }
3513
3514 #[test]
3515 fn score_template_default_measures_are_four() {
3516 let score = Score::template(ScoreTemplate::StringQuartet);
3517 for part in &score.parts {
3518 for staff in &part.staves {
3519 assert_eq!(staff.measures.len(), 4);
3520 }
3521 }
3522 }
3523
3524 #[test]
3527 fn measure_empty_has_no_breaks() {
3528 let m = Measure::empty(4, 4);
3529 assert!(!m.system_break);
3530 assert!(!m.page_break);
3531 }
3532
3533 #[test]
3534 fn system_break_survives_json_roundtrip() {
3535 let mut m = Measure::empty(4, 4);
3536 m.system_break = true;
3537 let json = serde_json::to_string(&m).unwrap();
3538 let m2: Measure = serde_json::from_str(&json).unwrap();
3539 assert!(m2.system_break);
3540 assert!(!m2.page_break);
3541 }
3542
3543 #[test]
3546 fn diff_identical_scores_is_empty() {
3547 let s = Score::new("T", 120, 4, 4, 0, 2);
3548 assert!(diff(&s, &s).is_empty());
3549 }
3550
3551 #[test]
3552 fn score_patch_covers_measure_semantics_and_note_insert_index() {
3553 let mut a = Score::new("T", 120, 4, 4, 0, 1);
3554 a.parts[0].staves[0].measures[0].voices[0].clear();
3555 let mut b = a.clone();
3556 let measure = &mut b.parts[0].staves[0].measures[0];
3557 measure.key_sig = Some(KeySignature {
3558 fifths: -2,
3559 mode: "major".to_string(),
3560 });
3561 measure.time_sig = Some(TimeSignature {
3562 numerator: 3,
3563 denominator: 4,
3564 });
3565 measure.barline_left = Barline::RepeatStart;
3566 measure.barline_right = Barline::RepeatEnd;
3567 measure.rehearsal = Some("A".to_string());
3568 measure.volta = Some(VoltaBracket {
3569 number: 1,
3570 kind: "begin_end".to_string(),
3571 });
3572 measure.texts.push(StyledText {
3573 style: TextStyle::RehearsalMark,
3574 text: "A".to_string(),
3575 placement: None,
3576 offset_x: None,
3577 offset_y: None,
3578 relative_x: None,
3579 relative_y: None,
3580 });
3581 measure.figured_bass.push(FiguredBassFigure {
3582 number: "6".to_string(),
3583 alter: None,
3584 prefix: None,
3585 suffix: None,
3586 extender: false,
3587 });
3588 measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
3589 let expected = b.parts[0].staves[0].measures[0].clone();
3590
3591 let patches = score_patch(&a, &b);
3592 assert!(
3593 patches
3594 .iter()
3595 .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
3596 );
3597 assert!(
3598 patches
3599 .iter()
3600 .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
3601 );
3602 assert!(
3603 patches
3604 .iter()
3605 .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
3606 );
3607 assert!(
3608 patches
3609 .iter()
3610 .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
3611 );
3612 assert!(
3613 patches
3614 .iter()
3615 .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
3616 );
3617 assert!(
3618 patches
3619 .iter()
3620 .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
3621 );
3622 let result = apply_patch(&a, &patches).expect("patch application failed");
3623 let result_measure = &result.parts[0].staves[0].measures[0];
3624 assert_eq!(result_measure.key_sig, expected.key_sig);
3625 assert_eq!(result_measure.time_sig, expected.time_sig);
3626 assert_eq!(result_measure.barline_left, expected.barline_left);
3627 assert_eq!(result_measure.barline_right, expected.barline_right);
3628 assert_eq!(result_measure.rehearsal, expected.rehearsal);
3629 assert_eq!(result_measure.volta, expected.volta);
3630 assert_eq!(result_measure.texts, expected.texts);
3631 assert_eq!(result_measure.figured_bass, expected.figured_bass);
3632 assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
3633 }
3634
3635 #[test]
3636 fn diff_and_patch_preserve_score_level_texts() {
3637 let a = Score::new("T", 120, 4, 4, 0, 1);
3638 let mut b = a.clone();
3639 b.texts.push(StyledText {
3640 style: TextStyle::Expression,
3641 text: "Prelude".to_string(),
3642 placement: Some("above".to_string()),
3643 offset_x: Some(12.0),
3644 offset_y: Some(-8.0),
3645 relative_x: None,
3646 relative_y: None,
3647 });
3648
3649 let changes = diff(&a, &b);
3650 assert!(changes.iter().any(|change| matches!(
3651 change,
3652 ScoreChange::ScoreTextChanged { old, new }
3653 if old.is_empty() && new == &b.texts
3654 )));
3655
3656 let patches = score_patch(&a, &b);
3657 assert!(patches.iter().any(|patch| matches!(
3658 patch,
3659 ScorePatch::SetScoreTexts { value } if value == &b.texts
3660 )));
3661 let result = apply_patch(&a, &patches).expect("score text patch failed");
3662 assert_eq!(result.texts, b.texts);
3663 }
3664
3665 #[test]
3666 fn measure_presentation_changes_use_typed_diff_and_local_patch() {
3667 let a = Score::new("T", 120, 4, 4, 0, 1);
3668 let mut b = a.clone();
3669 let measure = &mut b.parts[0].staves[0].measures[0];
3670 measure.number = 8;
3671 measure.clef = Some(Clef::Bass);
3672 measure.tempo_text = Some("Allegro".to_string());
3673 measure.navigation = Some("D.S.".to_string());
3674 measure.expression_text = Some("espressivo".to_string());
3675 measure.multi_rest_count = Some(3);
3676 measure.system_break = true;
3677 measure.page_break = true;
3678
3679 let changes = diff(&a, &b);
3680 assert!(changes.iter().any(|change| matches!(
3681 change,
3682 ScoreChange::MeasurePresentationChanged {
3683 part: 0,
3684 staff: 0,
3685 measure: 0,
3686 old_number: 1,
3687 new_number: 8,
3688 old_clef: None,
3689 new_clef: Some(Clef::Bass),
3690 new_tempo_text: Some(text),
3691 new_navigation: Some(navigation),
3692 new_expression_text: Some(expression),
3693 new_multi_rest_count: Some(3),
3694 old_system_break: false,
3695 new_system_break: true,
3696 old_page_break: false,
3697 new_page_break: true,
3698 ..
3699 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
3700 )));
3701 assert!(!changes.iter().any(|change| matches!(
3702 change,
3703 ScoreChange::UnrepresentedFieldChanged { path }
3704 if path == "parts[0].staves[0].measures[0].number"
3705 )));
3706
3707 let patches = score_patch(&a, &b);
3708 assert!(patches.iter().any(|patch| matches!(
3709 patch,
3710 ScorePatch::SetMeasurePresentation {
3711 part: 0,
3712 staff: 0,
3713 measure: 0,
3714 number: 8,
3715 clef: Some(Clef::Bass),
3716 tempo_text: Some(text),
3717 navigation: Some(navigation),
3718 expression_text: Some(expression),
3719 multi_rest_count: Some(3),
3720 system_break: true,
3721 page_break: true,
3722 } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
3723 )));
3724 assert!(
3725 !patches
3726 .iter()
3727 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
3728 );
3729 let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
3730 let decoded: Vec<ScorePatch> =
3731 serde_json::from_str(&json).expect("measure presentation patch should decode");
3732 let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
3733 assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
3734 assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
3735 assert_eq!(
3736 serde_json::to_value(&result).expect("patched score JSON"),
3737 serde_json::to_value(&b).expect("expected score JSON")
3738 );
3739 }
3740
3741 #[test]
3742 fn chord_definition_changes_use_typed_diff_and_local_patch() {
3743 let a = Score::new("T", 120, 4, 4, 0, 1);
3744 let mut b = a.clone();
3745 b.chord_definitions.push(ChordDefinition {
3746 id: Some("c-major".to_string()),
3747 label: Some("C".to_string()),
3748 kind: Some("major".to_string()),
3749 fret_position: Some(0),
3750 tab_strings: Some("x32010".to_string()),
3751 tab_courses: None,
3752 members: Vec::new(),
3753 barres: Vec::new(),
3754 });
3755
3756 let changes = diff(&a, &b);
3757 assert!(changes.iter().any(|change| matches!(
3758 change,
3759 ScoreChange::ChordDefinitionsChanged { old, new }
3760 if old.is_empty() && new == &b.chord_definitions
3761 )));
3762 assert!(!changes.iter().any(|change| matches!(
3763 change,
3764 ScoreChange::UnrepresentedFieldChanged { path }
3765 if path == "chord_definitions"
3766 )));
3767
3768 let patches = score_patch(&a, &b);
3769 assert!(patches.iter().any(|patch| matches!(
3770 patch,
3771 ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
3772 )));
3773 assert!(
3774 !patches
3775 .iter()
3776 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
3777 );
3778 let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
3779 let decoded: Vec<ScorePatch> =
3780 serde_json::from_str(&json).expect("chord definition patch should decode");
3781 let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
3782 assert_eq!(result.chord_definitions, b.chord_definitions);
3783 }
3784
3785 #[test]
3786 fn part_name_changes_use_typed_diff_and_local_patch() {
3787 let a = Score::new("T", 120, 4, 4, 0, 1);
3788 let mut b = a.clone();
3789 b.parts[0].name = "Violin".to_string();
3790 b.parts[0].short_name = "Vln.".to_string();
3791 b.parts[0].midi_channel = 4;
3792 b.parts[0].midi_program = 40;
3793 b.parts[0].midi_pitch_bends.push(MidiPitchBend {
3794 tick: 120,
3795 channel: 4,
3796 value: 2048,
3797 });
3798 b.parts[0].midi_control_changes.push(MidiControlChange {
3799 tick: 240,
3800 channel: 4,
3801 controller: 64,
3802 value: 127,
3803 });
3804 b.parts[0].midi_program_changes.push(MidiProgramChange {
3805 tick: 0,
3806 channel: 4,
3807 program: 40,
3808 });
3809 b.parts[0].midi_aftertouch.push(MidiAftertouch {
3810 tick: 360,
3811 channel: 4,
3812 key: Some(64),
3813 value: 80,
3814 });
3815 b.parts[0].staves[0].clef = Clef::Bass;
3816 b.parts[0].staves[0].transpose_semitones = -2;
3817
3818 let changes = diff(&a, &b);
3819 assert!(changes.iter().any(|change| matches!(
3820 change,
3821 ScoreChange::PartNamesChanged {
3822 part: 0,
3823 old_name,
3824 new_name,
3825 old_short_name,
3826 new_short_name,
3827 } if old_name == "Piano"
3828 && new_name == "Violin"
3829 && old_short_name == "Pno."
3830 && new_short_name == "Vln."
3831 )));
3832 assert!(changes.iter().any(|change| matches!(
3833 change,
3834 ScoreChange::PartMidiChanged {
3835 part: 0,
3836 old_channel: 0,
3837 new_channel: 4,
3838 old_program: 0,
3839 new_program: 40,
3840 }
3841 )));
3842 assert!(changes.iter().any(|change| matches!(
3843 change,
3844 ScoreChange::PartMidiAutomationChanged {
3845 part: 0,
3846 new_pitch_bends,
3847 new_control_changes,
3848 new_program_changes,
3849 new_aftertouch,
3850 ..
3851 } if new_pitch_bends == &b.parts[0].midi_pitch_bends
3852 && new_control_changes == &b.parts[0].midi_control_changes
3853 && new_program_changes == &b.parts[0].midi_program_changes
3854 && new_aftertouch == &b.parts[0].midi_aftertouch
3855 )));
3856 assert!(changes.iter().any(|change| matches!(
3857 change,
3858 ScoreChange::StaffConfigurationChanged {
3859 part: 0,
3860 staff: 0,
3861 old_clef: Clef::Treble,
3862 new_clef: Clef::Bass,
3863 old_transpose_semitones: 0,
3864 new_transpose_semitones: -2,
3865 }
3866 )));
3867 assert!(!changes.iter().any(|change| matches!(
3868 change,
3869 ScoreChange::UnrepresentedFieldChanged { path }
3870 if path == "parts[0].name"
3871 )));
3872
3873 let patches = score_patch(&a, &b);
3874 assert!(patches.iter().any(|patch| matches!(
3875 patch,
3876 ScorePatch::SetPartNames {
3877 part: 0,
3878 name,
3879 short_name,
3880 } if name == "Violin" && short_name == "Vln."
3881 )));
3882 assert!(
3883 !patches
3884 .iter()
3885 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
3886 );
3887 assert!(patches.iter().any(|patch| matches!(
3888 patch,
3889 ScorePatch::SetPartMidi {
3890 part: 0,
3891 channel: 4,
3892 program: 40,
3893 }
3894 )));
3895 assert!(patches.iter().any(|patch| matches!(
3896 patch,
3897 ScorePatch::SetPartMidiAutomation {
3898 part: 0,
3899 pitch_bends,
3900 control_changes,
3901 program_changes,
3902 aftertouch,
3903 } if pitch_bends == &b.parts[0].midi_pitch_bends
3904 && control_changes == &b.parts[0].midi_control_changes
3905 && program_changes == &b.parts[0].midi_program_changes
3906 && aftertouch == &b.parts[0].midi_aftertouch
3907 )));
3908 assert!(patches.iter().any(|patch| matches!(
3909 patch,
3910 ScorePatch::SetStaffConfiguration {
3911 part: 0,
3912 staff: 0,
3913 clef: Clef::Bass,
3914 transpose_semitones: -2,
3915 }
3916 )));
3917 let result = apply_patch(&a, &patches).expect("part name patch failed");
3918 assert_eq!(result.parts[0].name, b.parts[0].name);
3919 assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
3920 assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
3921 assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
3922 assert_eq!(
3923 result.parts[0].midi_pitch_bends,
3924 b.parts[0].midi_pitch_bends
3925 );
3926 assert_eq!(
3927 result.parts[0].midi_control_changes,
3928 b.parts[0].midi_control_changes
3929 );
3930 assert_eq!(
3931 result.parts[0].midi_program_changes,
3932 b.parts[0].midi_program_changes
3933 );
3934 assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
3935 assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
3936 assert_eq!(
3937 result.parts[0].staves[0].transpose_semitones,
3938 b.parts[0].staves[0].transpose_semitones
3939 );
3940 }
3941
3942 #[test]
3943 fn diff_reports_measure_text_and_figured_bass_changes() {
3944 let a = Score::new("T", 120, 4, 4, 0, 1);
3945 let mut b = a.clone();
3946 let measure = &mut b.parts[0].staves[0].measures[0];
3947 measure.texts.push(StyledText {
3948 style: TextStyle::Lyrics,
3949 text: "la".to_string(),
3950 placement: None,
3951 offset_x: None,
3952 offset_y: None,
3953 relative_x: None,
3954 relative_y: None,
3955 });
3956 measure.figured_bass.push(FiguredBassFigure {
3957 number: "6".to_string(),
3958 alter: None,
3959 prefix: None,
3960 suffix: None,
3961 extender: false,
3962 });
3963
3964 let changes = diff(&a, &b);
3965 assert!(changes.iter().any(|change| matches!(
3966 change,
3967 ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
3968 if old.is_empty() && new.len() == 1
3969 )));
3970 assert!(changes.iter().any(|change| matches!(
3971 change,
3972 ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
3973 if old.is_empty() && new.len() == 1
3974 )));
3975 }
3976
3977 #[test]
3978 fn diff_reports_tablature_changes_and_patches_them_locally() {
3979 let a = Score::new("T", 120, 4, 4, 0, 1);
3980 let mut b = a.clone();
3981 b.parts[0].staves[0].tablature = Some(TablatureConfig {
3982 lines: 6,
3983 tuning_midi: vec![40, 45, 50, 55, 59, 64],
3984 capo: 2,
3985 });
3986
3987 let changes = diff(&a, &b);
3988 assert!(!changes.iter().any(|change| matches!(
3989 change,
3990 ScoreChange::UnrepresentedFieldChanged { path }
3991 if path == "parts[0].staves[0].tablature"
3992 )));
3993 assert!(changes.iter().any(|change| matches!(
3994 change,
3995 ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
3996 if config.lines == 6 && config.capo == 2
3997 )));
3998 let patches = score_patch(&a, &b);
3999 assert!(patches.iter().any(|patch| matches!(
4000 patch,
4001 ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
4002 if config.lines == 6 && config.capo == 2
4003 )));
4004 assert_eq!(
4005 apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
4006 b.parts[0].staves[0].tablature
4007 );
4008 }
4009
4010 #[test]
4011 fn score_patch_uses_local_presentation_patch_for_display_fields() {
4012 let a = Score::new("T", 120, 4, 4, 0, 1);
4013 let mut b = a.clone();
4014 b.parts[0].name = "Piano".to_string();
4015 b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
4016 let patches = score_patch(&a, &b);
4017 assert!(patches.iter().any(|patch| matches!(
4018 patch,
4019 ScorePatch::SetMeasurePresentation {
4020 part: 0,
4021 staff: 0,
4022 measure: 0,
4023 expression_text: Some(text),
4024 ..
4025 } if text == "dolce"
4026 )));
4027 assert!(
4028 !patches
4029 .iter()
4030 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
4031 );
4032 let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
4033 assert_eq!(result.parts[0].name, "Piano");
4034 assert_eq!(
4035 result.parts[0].staves[0].measures[0].expression_text,
4036 Some("dolce".to_string())
4037 );
4038 }
4039
4040 #[test]
4041 fn diff_detects_tempo_change() {
4042 let a = Score::new("T", 120, 4, 4, 0, 1);
4043 let mut b = a.clone();
4044 b.settings.tempo_bpm = 90;
4045 let changes = diff(&a, &b);
4046 assert_eq!(changes.len(), 1);
4047 assert!(matches!(
4048 changes[0],
4049 ScoreChange::TempoChanged { old: 120, new: 90 }
4050 ));
4051 }
4052
4053 #[test]
4054 fn diff_detects_title_change() {
4055 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
4056 let mut b = a.clone();
4057 b.metadata.title = "New Title".to_string();
4058 let changes = diff(&a, &b);
4059 assert!(
4060 changes.iter().any(
4061 |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
4062 )
4063 );
4064 }
4065
4066 #[test]
4067 fn diff_detects_note_modification() {
4068 let mut a = Score::new("T", 120, 4, 4, 0, 1);
4069 a.parts[0].staves[0].measures[0].voices[0] =
4070 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4071 let mut b = a.clone();
4072 b.parts[0].staves[0].measures[0].voices[0][0] =
4073 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
4074 let changes = diff(&a, &b);
4075 assert!(
4076 changes
4077 .iter()
4078 .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
4079 );
4080 }
4081
4082 #[test]
4083 fn diff_detects_part_added() {
4084 let a = Score::new("T", 120, 4, 4, 0, 1);
4085 let mut b = a.clone();
4086 let mut p = Part::new("Violin", "Vln.");
4087 p.staves.push(Staff::new(Clef::Treble));
4088 b.parts.push(p);
4089 let changes = diff(&a, &b);
4090 assert!(
4091 changes
4092 .iter()
4093 .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
4094 );
4095 }
4096
4097 #[test]
4098 fn diff_detects_measure_tempo_change() {
4099 let a = Score::new("T", 120, 4, 4, 0, 2);
4100 let mut b = a.clone();
4101 b.parts[0].staves[0].measures[1].tempo = Some(60);
4102 let changes = diff(&a, &b);
4103 assert!(changes.iter().any(|c| matches!(
4104 c,
4105 ScoreChange::MeasureTempoChanged {
4106 measure: 1,
4107 old: None,
4108 new: Some(60),
4109 ..
4110 }
4111 )));
4112 }
4113
4114 #[test]
4115 fn diff_detects_barline_change() {
4116 use crate::model::notation::Barline;
4117 let a = Score::new("T", 120, 4, 4, 0, 2);
4118 let mut b = a.clone();
4119 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
4120 let changes = diff(&a, &b);
4121 assert!(
4122 changes
4123 .iter()
4124 .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
4125 );
4126 }
4127
4128 #[test]
4129 fn diff_detects_rehearsal_change() {
4130 let a = Score::new("T", 120, 4, 4, 0, 2);
4131 let mut b = a.clone();
4132 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
4133 let changes = diff(&a, &b);
4134 assert!(
4135 changes
4136 .iter()
4137 .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
4138 );
4139 }
4140
4141 #[test]
4142 fn diff_detects_volta_change() {
4143 use super::VoltaBracket;
4144 let a = Score::new("T", 120, 4, 4, 0, 2);
4145 let mut b = a.clone();
4146 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
4147 number: 1,
4148 kind: "begin_end".into(),
4149 });
4150 let changes = diff(&a, &b);
4151 assert!(
4152 changes
4153 .iter()
4154 .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
4155 );
4156 }
4157
4158 #[test]
4159 fn diff_detects_key_signature_change() {
4160 let a = Score::new("T", 120, 4, 4, 0, 1);
4161 let mut b = a.clone();
4162 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
4164 assert!(
4165 changes
4166 .iter()
4167 .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
4168 );
4169 }
4170
4171 #[test]
4172 fn measure_key_signature_uses_typed_local_patch() {
4173 let a = Score::new("T", 120, 4, 4, 0, 1);
4174 let mut b = a.clone();
4175 b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
4176 fifths: 2,
4177 mode: "major".to_string(),
4178 });
4179 let changes = diff(&a, &b);
4180 assert!(!changes.iter().any(|change| matches!(
4181 change,
4182 ScoreChange::UnrepresentedFieldChanged { path }
4183 if path == "parts[0].staves[0].measures[0].key_sig"
4184 )));
4185 let patches = score_patch(&a, &b);
4186 assert!(patches.iter().any(|patch| matches!(
4187 patch,
4188 ScorePatch::SetKeySignature {
4189 part: 0,
4190 staff: 0,
4191 measure: 0,
4192 value: Some(KeySignature { fifths: 2, .. }),
4193 }
4194 )));
4195 assert!(
4196 !patches
4197 .iter()
4198 .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
4199 );
4200 }
4201
4202 #[test]
4203 fn diff_same_key_signature_no_change() {
4204 let a = Score::new("T", 120, 4, 4, 2, 1);
4205 let changes = diff(&a, &a);
4206 assert!(changes.is_empty());
4207 }
4208
4209 #[test]
4210 fn score_duration_secs_region_partial() {
4211 use super::score_duration_secs_region;
4212 let score = Score::new("T", 120, 4, 4, 0, 4);
4214 let secs = score_duration_secs_region(&score, (1, 2));
4215 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
4216 }
4217
4218 #[test]
4219 fn score_duration_secs_region_single_measure() {
4220 use super::score_duration_secs_region;
4221 let score = Score::new("T", 120, 4, 4, 0, 4);
4223 let secs = score_duration_secs_region(&score, (0, 0));
4224 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
4225 }
4226
4227 #[test]
4230 fn measure_beats_remaining_empty_voice_returns_full() {
4231 use super::measure_beats_remaining;
4232 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4233 score.parts[0].staves[0].measures[0].voices[0].clear();
4234 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
4235 assert!(
4236 (rem - 4.0).abs() < 1e-9,
4237 "expected 4.0 remaining, got {rem}"
4238 );
4239 }
4240
4241 #[test]
4242 fn measure_beats_remaining_half_full_returns_half() {
4243 use super::measure_beats_remaining;
4244 use crate::model::pitch::Step;
4245 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4246 score.parts[0].staves[0].measures[0].voices[0] = vec![
4247 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4248 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4249 ];
4250 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
4251 assert!(
4252 (rem - 2.0).abs() < 1e-9,
4253 "expected 2.0 remaining, got {rem}"
4254 );
4255 }
4256
4257 #[test]
4258 fn measure_beats_remaining_full_voice_returns_zero() {
4259 use super::measure_beats_remaining;
4260 use crate::model::pitch::Step;
4261 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4262 score.parts[0].staves[0].measures[0].voices[0] =
4263 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4264 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
4265 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
4266 }
4267
4268 #[test]
4269 fn measure_beats_remaining_tuplet_accounting() {
4270 use super::measure_beats_remaining;
4271 use crate::model::notation::TupletInfo;
4272 use crate::model::pitch::Step;
4273 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4275 let tuplet = TupletInfo {
4276 actual_notes: 3,
4277 normal_notes: 2,
4278 };
4279 let mk = |step| {
4280 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
4281 n.tuplet = Some(tuplet.clone());
4282 n
4283 };
4284 score.parts[0].staves[0].measures[0].voices[0] =
4285 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
4286 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
4287 assert!(
4288 (rem - 2.0).abs() < 1e-9,
4289 "expected 2.0 remaining (triplets used 2.0), got {rem}"
4290 );
4291 }
4292
4293 #[test]
4294 fn measure_beats_remaining_out_of_range_returns_err() {
4295 use super::measure_beats_remaining;
4296 let score = Score::new("T", 120, 4, 4, 0, 1);
4297 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
4298 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
4299 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
4300 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
4301 }
4302
4303 #[test]
4304 fn note_content_eq_ignores_id() {
4305 use crate::model::pitch::Step;
4306 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4307 let mut b = a.clone();
4308 b.id = "different-id".to_string();
4309 assert!(note_content_eq(&a, &b));
4310 b.pitches[0] = Pitch::new(Step::D, 4);
4312 assert!(!note_content_eq(&a, &b));
4313 let mut c = a.clone();
4315 a.stem_up = Some(true);
4316 c.stem_up = Some(false);
4317 assert!(!note_content_eq(&a, &c));
4318 }
4319
4320 #[test]
4321 fn suggested_stem_up_below_middle() {
4322 use crate::model::notation::Clef;
4323 let pitches = vec![Pitch::new(Step::C, 4)];
4325 assert!(suggested_stem_up(&pitches, &Clef::Treble));
4326 }
4327
4328 #[test]
4329 fn suggested_stem_up_above_middle() {
4330 use crate::model::notation::Clef;
4331 let pitches = vec![Pitch::new(Step::G, 5)];
4333 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
4334 }
4335
4336 #[test]
4337 fn suggested_stem_up_at_middle_line() {
4338 use crate::model::notation::Clef;
4339 let pitches = vec![Pitch::new(Step::B, 4)];
4341 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
4342 }
4343
4344 #[test]
4345 fn suggested_stem_up_chord() {
4346 use crate::model::notation::Clef;
4347 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
4349 assert!(suggested_stem_up(&pitches, &Clef::Treble));
4350 }
4351
4352 #[test]
4353 fn suggested_stem_up_bass_clef() {
4354 use crate::model::notation::Clef;
4355 let pitches = vec![Pitch::new(Step::D, 3)];
4357 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
4358 let pitches2 = vec![Pitch::new(Step::C, 3)];
4360 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
4361 }
4362
4363 #[test]
4364 fn suggested_stem_up_empty_pitches() {
4365 use crate::model::notation::Clef;
4366 assert!(suggested_stem_up(&[], &Clef::Treble));
4367 }
4368
4369 fn eighth(pitch: Pitch) -> Note {
4370 Note::new(pitch, Duration::Eighth)
4371 }
4372 fn quarter(pitch: Pitch) -> Note {
4373 Note::new(pitch, Duration::Quarter)
4374 }
4375 fn rest_eighth() -> Note {
4376 Note::rest(Duration::Eighth)
4377 }
4378
4379 #[test]
4380 fn compute_beams_4_4_four_eighths() {
4381 use crate::model::notation::{Clef, TimeSignature};
4382 let _ = Clef::Treble; let ts = TimeSignature {
4384 numerator: 4,
4385 denominator: 4,
4386 };
4387 let c4 = Pitch::new(Step::C, 4);
4388 let notes = vec![
4389 eighth(c4.clone()),
4390 eighth(c4.clone()),
4391 eighth(c4.clone()),
4392 eighth(c4.clone()),
4393 ];
4394 let beams = compute_beams(¬es, &ts);
4395 assert_eq!(beams[0], BeamState::Begin);
4397 assert_eq!(beams[1], BeamState::End);
4398 assert_eq!(beams[2], BeamState::Begin);
4399 assert_eq!(beams[3], BeamState::End);
4400 }
4401
4402 #[test]
4403 fn compute_beams_4_4_all_eighth_one_group() {
4404 use crate::model::notation::TimeSignature;
4405 let ts = TimeSignature {
4406 numerator: 4,
4407 denominator: 4,
4408 };
4409 let c4 = Pitch::new(Step::C, 4);
4410 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
4412 let beams = compute_beams(¬es, &ts);
4413 assert_eq!(beams[0], BeamState::Begin);
4414 assert_eq!(beams[1], BeamState::End);
4415 }
4416
4417 #[test]
4418 fn compute_beams_quarter_not_beamed() {
4419 use crate::model::notation::TimeSignature;
4420 let ts = TimeSignature {
4421 numerator: 4,
4422 denominator: 4,
4423 };
4424 let c4 = Pitch::new(Step::C, 4);
4425 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
4426 let beams = compute_beams(¬es, &ts);
4427 assert_eq!(beams[0], BeamState::None);
4428 assert_eq!(beams[1], BeamState::None);
4429 }
4430
4431 #[test]
4432 fn compute_beams_rest_breaks_beam() {
4433 use crate::model::notation::TimeSignature;
4434 let ts = TimeSignature {
4435 numerator: 4,
4436 denominator: 4,
4437 };
4438 let c4 = Pitch::new(Step::C, 4);
4439 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
4440 let beams = compute_beams(¬es, &ts);
4441 assert_eq!(beams[0], BeamState::None);
4443 assert_eq!(beams[1], BeamState::None);
4444 assert_eq!(beams[2], BeamState::None);
4445 }
4446
4447 #[test]
4448 fn compute_beams_6_8_compound() {
4449 use crate::model::notation::TimeSignature;
4450 let ts = TimeSignature {
4451 numerator: 6,
4452 denominator: 8,
4453 };
4454 let c4 = Pitch::new(Step::C, 4);
4455 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
4457 let beams = compute_beams(¬es, &ts);
4458 assert_eq!(beams[0], BeamState::Begin);
4459 assert_eq!(beams[1], BeamState::Continue);
4460 assert_eq!(beams[2], BeamState::End);
4461 assert_eq!(beams[3], BeamState::Begin);
4462 assert_eq!(beams[4], BeamState::Continue);
4463 assert_eq!(beams[5], BeamState::End);
4464 }
4465
4466 #[test]
4467 fn compute_beams_single_eighth() {
4468 use crate::model::notation::TimeSignature;
4469 let ts = TimeSignature {
4470 numerator: 4,
4471 denominator: 4,
4472 };
4473 let c4 = Pitch::new(Step::C, 4);
4474 let notes = vec![eighth(c4.clone())];
4475 let beams = compute_beams(¬es, &ts);
4476 assert_eq!(beams[0], BeamState::None);
4477 }
4478}