1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3use crate::Error;
4use super::{
5 duration::Duration,
6 notation::{
7 Articulation, Barline, BeamState, ChordSymbol, Clef, Dynamic, GuitarTechnique,
8 HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind, TimeSignature, TupletInfo,
9 },
10 pitch::Pitch,
11};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ScoreMetadata {
15 pub title: String,
16 pub composer: String,
17 pub lyricist: String,
18 pub copyright: String,
19 pub work_number: String,
20 pub movement_title: String,
21}
22
23impl Default for ScoreMetadata {
24 fn default() -> Self {
25 Self {
26 title: "Untitled Score".to_string(),
27 composer: String::new(),
28 lyricist: String::new(),
29 copyright: String::new(),
30 work_number: String::new(),
31 movement_title: String::new(),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ScoreSettings {
38 pub tempo_bpm: u16,
39 pub time_signature: TimeSignature,
40 pub key_signature: KeySignature,
41}
42
43impl Default for ScoreSettings {
44 fn default() -> Self {
45 Self {
46 tempo_bpm: 120,
47 time_signature: TimeSignature::default(),
48 key_signature: KeySignature::default(),
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub enum PartGroupSymbol {
56 Bracket, Brace, Line, }
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PartGroup {
64 pub first_part: usize,
66 pub last_part: usize,
68 pub symbol: PartGroupSymbol,
69 #[serde(default)]
71 pub barlines_connect: bool,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Score {
76 pub id: String,
77 #[serde(default)]
79 pub schema_version: u32,
80 pub metadata: ScoreMetadata,
81 pub settings: ScoreSettings,
82 pub parts: Vec<Part>,
83 #[serde(default)]
84 pub part_groups: Vec<PartGroup>,
85}
86
87impl Default for Score {
88 fn default() -> Self {
89 let mut part = Part::new("Piano", "Pno.");
90 part.staves.push(Staff::new(Clef::Treble));
91 for _ in 0..4 {
92 part.staves[0].measures.push(Measure::empty(4, 4));
93 }
94 Self {
95 id: Uuid::new_v4().to_string(),
96 schema_version: 1,
97 metadata: ScoreMetadata::default(),
98 settings: ScoreSettings::default(),
99 parts: vec![part],
100 part_groups: Vec::new(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107pub enum ScoreTemplate {
108 Solo,
110 Piano,
112 StringQuartet,
114 StringOrchestra,
116 BrassQuintet,
118}
119
120impl Score {
121 pub fn new(
122 title: &str,
123 tempo_bpm: u16,
124 numerator: u8,
125 denominator: u8,
126 fifths: i8,
127 measure_count: u32,
128 ) -> Self {
129 let mut score = Score::default();
130 score.metadata.title = title.to_string();
131 score.settings.tempo_bpm = tempo_bpm;
132 score.settings.time_signature = TimeSignature { numerator, denominator };
133 score.settings.key_signature = KeySignature { fifths, mode: "major".to_string() };
134
135 score.parts[0].staves[0].measures.clear();
136 for i in 0..measure_count {
137 let mut m = Measure::empty(numerator, denominator);
138 m.number = i + 1;
139 score.parts[0].staves[0].measures.push(m);
140 }
141 score
142 }
143
144 pub fn template(kind: ScoreTemplate) -> Self {
149 fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
150 (0..count).map(|i| {
151 let mut m = Measure::empty(num, den);
152 m.number = i + 1;
153 m
154 }).collect()
155 }
156 fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
157 let mut p = Part::new(name, short);
158 p.midi_program = program;
159 let mut s = Staff::new(clef);
160 s.measures = measures(4, 4, 4);
161 p.staves.push(s);
162 p
163 }
164
165 let mut score = Score {
166 id: uuid::Uuid::new_v4().to_string(),
167 schema_version: 1,
168 metadata: ScoreMetadata::default(),
169 settings: ScoreSettings::default(),
170 parts: Vec::new(),
171 part_groups: Vec::new(),
172 };
173
174 match kind {
175 ScoreTemplate::Solo => {
176 score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
177 }
178 ScoreTemplate::Piano => {
179 let mut p = Part::new("Piano", "Pno.");
180 p.midi_program = 0;
181 let mut treble = Staff::new(Clef::Treble);
182 treble.measures = measures(4, 4, 4);
183 let mut bass = Staff::new(Clef::Bass);
184 bass.measures = measures(4, 4, 4);
185 p.staves.push(treble);
186 p.staves.push(bass);
187 score.parts.push(p);
188 }
189 ScoreTemplate::StringQuartet => {
190 score.parts.push(part("Violin I", "Vn. I", Clef::Treble, 40));
191 score.parts.push(part("Violin II", "Vn. II", Clef::Treble, 40));
192 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
193 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
194 }
195 ScoreTemplate::StringOrchestra => {
196 score.parts.push(part("Violin I", "Vn. I", Clef::Treble, 40));
197 score.parts.push(part("Violin II", "Vn. II", Clef::Treble, 40));
198 score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
199 score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
200 score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
201 }
202 ScoreTemplate::BrassQuintet => {
203 score.parts.push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
204 score.parts.push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
205 score.parts.push(part("French Horn", "Hn.", Clef::Treble, 60));
206 score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
207 score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
208 }
209 }
210 score
211 }
212
213 pub fn measure_count(&self) -> usize {
214 self.parts.first()
215 .and_then(|p| p.staves.first())
216 .map(|s| s.measures.len())
217 .unwrap_or(0)
218 }
219
220 pub fn statistics(&self) -> ScoreStats {
222 let measure_count = self.measure_count();
223 let part_count = self.parts.len();
224
225 let seq = measure_sequence(self);
227 let total_beats: f64 = self.parts.first()
228 .and_then(|p| p.staves.first())
229 .map(|s| {
230 seq.iter()
231 .filter_map(|&idx| s.measures.get(idx))
232 .flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
233 .map(|n| n.beats())
234 .sum()
235 })
236 .unwrap_or(0.0);
237
238 let mut note_count = 0usize;
239 let mut rest_count = 0usize;
240 for part in &self.parts {
241 for staff in &part.staves {
242 for measure in &staff.measures {
243 for voice in &measure.voices {
244 for note in voice {
245 if note.is_rest { rest_count += 1; } else { note_count += 1; }
246 }
247 }
248 }
249 }
250 }
251
252 let bpm = self.settings.tempo_bpm as f64;
253 let estimated_duration_secs = if bpm > 0.0 { total_beats / bpm * 60.0 } else { 0.0 };
254
255 ScoreStats { measure_count, note_count, rest_count, part_count, estimated_duration_secs }
256 }
257
258 pub fn extract_part(&self, part_index: usize) -> Option<Score> {
261 let part = self.parts.get(part_index)?.clone();
262 Some(Score {
263 id: Uuid::new_v4().to_string(),
264 schema_version: 1,
265 metadata: self.metadata.clone(),
266 settings: self.settings.clone(),
267 parts: vec![part],
268 part_groups: Vec::new(),
269 })
270 }
271
272 pub fn merge(&self, other: &Score) -> Score {
276 let self_count = self.measure_count();
277 let other_count = other.measure_count();
278 let max_count = self_count.max(other_count);
279 let ts = self.settings.time_signature.clone();
280
281 let pad = |mut part: Part, from: usize| -> Part {
282 for staff in &mut part.staves {
283 for i in from..max_count {
284 let mut m = Measure::empty(ts.numerator, ts.denominator);
285 m.number = i as u32 + 1;
286 staff.measures.push(m);
287 }
288 }
289 part
290 };
291
292 let mut parts: Vec<Part> = self.parts.iter().cloned()
293 .map(|p| pad(p, self_count))
294 .collect();
295 for p in &other.parts {
296 parts.push(pad(p.clone(), other_count));
297 }
298
299 Score {
300 id: Uuid::new_v4().to_string(),
301 schema_version: 1,
302 metadata: self.metadata.clone(),
303 settings: self.settings.clone(),
304 parts,
305 part_groups: Vec::new(),
306 }
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312pub struct ScoreStats {
313 pub measure_count: usize,
314 pub note_count: usize,
316 pub rest_count: usize,
317 pub part_count: usize,
318 pub estimated_duration_secs: f64,
320}
321
322use super::pitch::Step;
325use super::repeat::measure_sequence;
326
327pub fn transpose(score: &Score, semitones: i8) -> Score {
331 if semitones == 0 { return score.clone(); }
332 let mut out = score.clone();
333 out.settings.key_signature.fifths =
334 transpose_fifths(score.settings.key_signature.fifths, &score.settings.key_signature.mode, semitones);
335 for part in &mut out.parts {
336 for staff in &mut part.staves {
337 for measure in &mut staff.measures {
338 if let Some(ref mut ks) = measure.key_sig {
339 ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
340 }
341 for voice in &mut measure.voices {
342 for note in voice.iter_mut() {
343 for pitch in note.pitches.iter_mut() {
344 *pitch = transpose_pitch(pitch, semitones);
345 }
346 }
347 }
348 }
349 }
350 }
351 out
352}
353
354fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
355 let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
356 let pc = new_midi % 12;
357 let oct = (new_midi / 12) as i8 - 1;
358 let (step, alter): (Step, i8) = if semitones >= 0 {
359 match pc {
360 0 => (Step::C, 0), 1 => (Step::C, 1),
361 2 => (Step::D, 0), 3 => (Step::D, 1),
362 4 => (Step::E, 0), 5 => (Step::F, 0),
363 6 => (Step::F, 1), 7 => (Step::G, 0),
364 8 => (Step::G, 1), 9 => (Step::A, 0),
365 10 => (Step::A, 1), 11 => (Step::B, 0),
366 _ => (Step::C, 0),
367 }
368 } else {
369 match pc {
370 0 => (Step::C, 0), 1 => (Step::D, -1),
371 2 => (Step::D, 0), 3 => (Step::E, -1),
372 4 => (Step::E, 0), 5 => (Step::F, 0),
373 6 => (Step::G, -1), 7 => (Step::G, 0),
374 8 => (Step::A, -1), 9 => (Step::A, 0),
375 10 => (Step::B, -1), 11 => (Step::B, 0),
376 _ => (Step::C, 0),
377 }
378 };
379 Pitch::with_alter(step, oct, alter)
380}
381
382fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
388 let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
389 let tonic_pc = if mode == "minor" {
390 ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
391 } else {
392 tonic_major_pc
393 };
394 let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
395 let major_tonic = if mode == "minor" {
396 ((new_tonic as i32 + 3).rem_euclid(12)) as u8
397 } else {
398 new_tonic
399 };
400 let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
401 if raw > 6 { raw - 12 } else { raw }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct Part {
406 pub id: String,
407 pub name: String,
408 pub short_name: String,
409 pub staves: Vec<Staff>,
410 #[serde(default)]
412 pub midi_channel: u8,
413 #[serde(default)]
415 pub midi_program: u8,
416}
417
418impl Part {
419 pub fn new(name: &str, short_name: &str) -> Self {
420 Self {
421 id: Uuid::new_v4().to_string(),
422 name: name.to_string(),
423 short_name: short_name.to_string(),
424 staves: Vec::new(),
425 midi_channel: 0,
426 midi_program: 0,
427 }
428 }
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct Staff {
433 pub clef: Clef,
434 pub measures: Vec<Measure>,
435 #[serde(default)]
438 pub transpose_semitones: i8,
439}
440
441impl Staff {
442 pub fn new(clef: Clef) -> Self {
443 Self { clef, measures: Vec::new(), transpose_semitones: 0 }
444 }
445}
446
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448pub struct VoltaBracket {
449 pub number: u8,
451 pub kind: String,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct Measure {
457 pub number: u32,
458 pub time_sig: Option<TimeSignature>,
459 pub key_sig: Option<KeySignature>,
460 pub clef: Option<Clef>,
461 pub tempo: Option<u16>,
462 pub barline_left: Barline,
463 pub barline_right: Barline,
464 #[serde(default)]
465 pub volta: Option<VoltaBracket>,
466 #[serde(default)]
467 pub tempo_text: Option<String>,
468 #[serde(default)]
469 pub rehearsal: Option<String>,
470 #[serde(default)]
473 pub navigation: Option<String>,
474 #[serde(default)]
476 pub expression_text: Option<String>,
477 #[serde(default)]
479 pub multi_rest_count: Option<u8>,
480 #[serde(default)]
482 pub system_break: bool,
483 #[serde(default)]
485 pub page_break: bool,
486 pub voices: [Vec<Note>; 4],
488}
489
490impl Measure {
491 pub fn empty(numerator: u8, denominator: u8) -> Self {
492 let total_beats = TimeSignature { numerator, denominator }.total_beats();
493 let mut voice0: Vec<Note> = Vec::new();
494 let mut remaining = total_beats;
495 while remaining > 1e-9 {
496 let dur = Duration::whole_filling_beats(remaining);
497 remaining -= dur.beats(0);
498 voice0.push(Note::rest(dur));
499 }
500 Self {
501 number: 0,
502 time_sig: None,
503 key_sig: None,
504 clef: None,
505 tempo: None,
506 barline_left: Barline::Normal,
507 barline_right: Barline::Normal,
508 volta: None,
509 tempo_text: None,
510 rehearsal: None,
511 navigation: None,
512 expression_text: None,
513 multi_rest_count: None,
514 system_break: false,
515 page_break: false,
516 voices: [voice0, vec![], vec![], vec![]],
517 }
518 }
519
520 pub fn renumber(&mut self, n: u32) {
521 self.number = n;
522 }
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct Note {
527 pub id: String,
528 pub is_rest: bool,
529 pub pitches: Vec<Pitch>,
531 pub duration: Duration,
532 pub dot_count: u8,
533 pub tie_start: bool,
534 pub tie_end: bool,
535 pub beam: BeamState,
536 pub articulations: Vec<Articulation>,
537 pub dynamic: Option<Dynamic>,
538 pub stem_up: Option<bool>,
539 #[serde(default)]
540 pub hairpin_start: Option<HairpinKind>,
541 #[serde(default)]
542 pub hairpin_end: bool,
543 #[serde(default)]
544 pub tuplet: Option<TupletInfo>,
545 #[serde(default)]
546 pub chord_symbol: Option<ChordSymbol>,
547 #[serde(default)]
548 pub is_grace: bool,
549 #[serde(default)]
551 pub grace_slash: bool,
552 #[serde(default)]
553 pub ottava_start: Option<OttavaKind>,
554 #[serde(default)]
555 pub ottava_end: bool,
556 #[serde(default)]
557 pub lyric: Option<Lyric>,
558 #[serde(default)]
559 pub pedal_start: bool,
560 #[serde(default)]
561 pub pedal_end: bool,
562 #[serde(default)]
563 pub slur_start: bool,
564 #[serde(default)]
565 pub slur_end: bool,
566 #[serde(default)]
568 pub arpeggiate: Option<bool>,
569 #[serde(default)]
571 pub technique_text: Option<String>,
572 #[serde(default)]
574 pub fingering: Option<u8>,
575 #[serde(default)]
577 pub string_number: Option<u8>,
578 #[serde(default)]
579 pub note_head: NoteHead,
580 #[serde(default)]
582 pub is_cue: bool,
583 #[serde(default)]
585 pub trill_line_start: bool,
586 #[serde(default)]
588 pub trill_line_end: bool,
589 #[serde(default)]
591 pub guitar_technique: Option<GuitarTechnique>,
592}
593
594impl Note {
595 pub fn new(pitch: Pitch, duration: Duration) -> Self {
596 Self {
597 id: Uuid::new_v4().to_string(),
598 is_rest: false,
599 pitches: vec![pitch],
600 duration,
601 dot_count: 0,
602 tie_start: false,
603 tie_end: false,
604 beam: BeamState::None,
605 articulations: Vec::new(),
606 dynamic: None,
607 stem_up: None,
608 hairpin_start: None,
609 hairpin_end: false,
610 tuplet: None,
611 chord_symbol: None,
612 is_grace: false,
613 grace_slash: false,
614 ottava_start: None,
615 ottava_end: false,
616 lyric: None,
617 pedal_start: false,
618 pedal_end: false,
619 slur_start: false,
620 slur_end: false,
621 arpeggiate: None,
622 technique_text: None,
623 fingering: None,
624 string_number: None,
625 note_head: NoteHead::Normal,
626 is_cue: false,
627 trill_line_start: false,
628 trill_line_end: false,
629 guitar_technique: None,
630 }
631 }
632
633 pub fn rest(duration: Duration) -> Self {
634 Self {
635 id: Uuid::new_v4().to_string(),
636 is_rest: true,
637 pitches: Vec::new(),
638 duration,
639 dot_count: 0,
640 tie_start: false,
641 tie_end: false,
642 beam: BeamState::None,
643 articulations: Vec::new(),
644 dynamic: None,
645 stem_up: None,
646 hairpin_start: None,
647 hairpin_end: false,
648 tuplet: None,
649 chord_symbol: None,
650 is_grace: false,
651 grace_slash: false,
652 ottava_start: None,
653 ottava_end: false,
654 lyric: None,
655 pedal_start: false,
656 pedal_end: false,
657 slur_start: false,
658 slur_end: false,
659 arpeggiate: None,
660 technique_text: None,
661 fingering: None,
662 string_number: None,
663 note_head: NoteHead::Normal,
664 is_cue: false,
665 trill_line_start: false,
666 trill_line_end: false,
667 guitar_technique: None,
668 }
669 }
670
671 pub fn beats(&self) -> f64 {
672 if self.is_grace || self.is_cue { return 0.0; }
673 let base = self.duration.beats(self.dot_count);
674 if let Some(ref t) = self.tuplet {
675 base * (t.normal_notes as f64) / (t.actual_notes as f64)
676 } else {
677 base
678 }
679 }
680}
681
682impl Duration {
683 pub fn whole_filling_beats(beats: f64) -> Duration {
685 if beats >= 4.0 { Duration::Whole }
686 else if beats >= 2.0 { Duration::Half }
687 else if beats >= 1.0 { Duration::Quarter }
688 else if beats >= 0.5 { Duration::Eighth }
689 else if beats >= 0.25 { Duration::Sixteenth }
690 else if beats >= 0.125 { Duration::ThirtySecond }
691 else { Duration::SixtyFourth }
692 }
693}
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699pub struct NoteAddr {
700 pub part: usize,
701 pub staff: usize,
702 pub measure: usize,
703 pub voice: usize,
704 pub note: usize,
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize)]
711pub enum ScoreChange {
712 MetadataChanged { field: String, old: String, new: String },
713 TempoChanged { old: u16, new: u16 },
714 KeySignatureChanged { old: KeySignature, new: KeySignature },
715 PartAdded { part_index: usize },
716 PartRemoved { part_index: usize, name: String },
717 NoteAdded { part: usize, staff: usize, measure: usize, voice: usize, note_index: usize },
718 NoteRemoved { part: usize, staff: usize, measure: usize, voice: usize, note: Box<Note> },
719 NoteModified { part: usize, staff: usize, measure: usize, voice: usize, note_index: usize, old: Box<Note>, new: Box<Note> },
720 TimeSigChanged { part: usize, staff: usize, measure: usize, old: Option<TimeSignature>, new: Option<TimeSignature> },
721 MeasureTempoChanged { part: usize, staff: usize, measure: usize, old: Option<u16>, new: Option<u16> },
722 BarlineChanged { part: usize, staff: usize, measure: usize },
723 RehearsalMarkChanged { part: usize, staff: usize, measure: usize, old: Option<String>, new: Option<String> },
724 VoltaChanged { part: usize, staff: usize, measure: usize },
725}
726
727pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
733 let mut changes: Vec<ScoreChange> = Vec::new();
734
735 macro_rules! meta {
736 ($field:ident, $name:literal) => {
737 if a.metadata.$field != b.metadata.$field {
738 changes.push(ScoreChange::MetadataChanged {
739 field: $name.to_string(),
740 old: a.metadata.$field.clone(),
741 new: b.metadata.$field.clone(),
742 });
743 }
744 };
745 }
746 meta!(title, "title");
747 meta!(composer, "composer");
748 meta!(lyricist, "lyricist");
749 meta!(copyright, "copyright");
750 meta!(work_number, "work_number");
751 meta!(movement_title, "movement_title");
752
753 if a.settings.tempo_bpm != b.settings.tempo_bpm {
754 changes.push(ScoreChange::TempoChanged { old: a.settings.tempo_bpm, new: b.settings.tempo_bpm });
755 }
756 if a.settings.key_signature != b.settings.key_signature {
757 changes.push(ScoreChange::KeySignatureChanged {
758 old: a.settings.key_signature.clone(),
759 new: b.settings.key_signature.clone(),
760 });
761 }
762
763 let a_len = a.parts.len();
764 let b_len = b.parts.len();
765 for i in b_len..a_len {
766 changes.push(ScoreChange::PartRemoved { part_index: i, name: a.parts[i].name.clone() });
767 }
768 for i in a_len..b_len {
769 changes.push(ScoreChange::PartAdded { part_index: i });
770 }
771
772 for pi in 0..a_len.min(b_len) {
773 let ap = &a.parts[pi];
774 let bp = &b.parts[pi];
775 for si in 0..ap.staves.len().min(bp.staves.len()) {
776 let a_staff = &ap.staves[si];
777 let b_staff = &bp.staves[si];
778 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
779 let am = &a_staff.measures[mi];
780 let bm = &b_staff.measures[mi];
781 for vi in 0..4usize {
782 let av = &am.voices[vi];
783 let bv = &bm.voices[vi];
784 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
785 if !note_content_eq(a_note, b_note) {
786 changes.push(ScoreChange::NoteModified {
787 part: pi, staff: si, measure: mi, voice: vi, note_index: ni,
788 old: Box::new(a_note.clone()), new: Box::new(b_note.clone()),
789 });
790 }
791 }
792 for note in av.iter().skip(bv.len()) {
793 changes.push(ScoreChange::NoteRemoved {
794 part: pi, staff: si, measure: mi, voice: vi, note: Box::new(note.clone()),
795 });
796 }
797 for ni in av.len()..bv.len() {
798 changes.push(ScoreChange::NoteAdded {
799 part: pi, staff: si, measure: mi, voice: vi, note_index: ni,
800 });
801 }
802 }
803 if am.time_sig != bm.time_sig {
804 changes.push(ScoreChange::TimeSigChanged {
805 part: pi, staff: si, measure: mi,
806 old: am.time_sig.clone(), new: bm.time_sig.clone(),
807 });
808 }
809 if am.tempo != bm.tempo {
810 changes.push(ScoreChange::MeasureTempoChanged {
811 part: pi, staff: si, measure: mi,
812 old: am.tempo, new: bm.tempo,
813 });
814 }
815 if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
816 changes.push(ScoreChange::BarlineChanged { part: pi, staff: si, measure: mi });
817 }
818 if am.rehearsal != bm.rehearsal {
819 changes.push(ScoreChange::RehearsalMarkChanged {
820 part: pi, staff: si, measure: mi,
821 old: am.rehearsal.clone(), new: bm.rehearsal.clone(),
822 });
823 }
824 if am.volta != bm.volta {
825 changes.push(ScoreChange::VoltaChanged { part: pi, staff: si, measure: mi });
826 }
827 }
828 }
829 }
830
831 changes
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
841pub enum ScorePatch {
842 SetMetadata { field: String, value: String },
843 SetTempo { value: u16 },
844 SetKeySignature { part: usize, staff: usize, measure: usize, value: KeySignature },
845 AddNote { part: usize, staff: usize, measure: usize, voice: usize, note: Box<Note> },
847 RemoveNote { part: usize, staff: usize, measure: usize, voice: usize, note_index: usize },
848 ReplaceNote { part: usize, staff: usize, measure: usize, voice: usize, note_index: usize, note: Box<Note> },
850 SetMeasureTempo { part: usize, staff: usize, measure: usize, value: Option<u16> },
851}
852
853pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
858 let mut patches: Vec<ScorePatch> = Vec::new();
859
860 macro_rules! meta {
861 ($field:ident, $name:literal) => {
862 if a.metadata.$field != b.metadata.$field {
863 patches.push(ScorePatch::SetMetadata {
864 field: $name.to_string(),
865 value: b.metadata.$field.clone(),
866 });
867 }
868 };
869 }
870 meta!(title, "title");
871 meta!(composer, "composer");
872 meta!(lyricist, "lyricist");
873 meta!(copyright, "copyright");
874 meta!(work_number, "work_number");
875 meta!(movement_title, "movement_title");
876
877 if a.settings.tempo_bpm != b.settings.tempo_bpm {
878 patches.push(ScorePatch::SetTempo { value: b.settings.tempo_bpm });
879 }
880
881 for pi in 0..a.parts.len().min(b.parts.len()) {
882 let ap = &a.parts[pi];
883 let bp = &b.parts[pi];
884 for si in 0..ap.staves.len().min(bp.staves.len()) {
885 let a_staff = &ap.staves[si];
886 let b_staff = &bp.staves[si];
887 for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
888 let am = &a_staff.measures[mi];
889 let bm = &b_staff.measures[mi];
890
891 if am.key_sig != bm.key_sig
892 && let Some(ref ks) = bm.key_sig
893 {
894 patches.push(ScorePatch::SetKeySignature {
895 part: pi, staff: si, measure: mi, value: ks.clone(),
896 });
897 }
898 if am.tempo != bm.tempo {
899 patches.push(ScorePatch::SetMeasureTempo {
900 part: pi, staff: si, measure: mi, value: bm.tempo,
901 });
902 }
903
904 for vi in 0..4usize {
905 let av = &am.voices[vi];
906 let bv = &bm.voices[vi];
907 for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
908 if !note_content_eq(a_note, b_note) {
909 patches.push(ScorePatch::ReplaceNote {
910 part: pi, staff: si, measure: mi, voice: vi,
911 note_index: ni, note: Box::new(b_note.clone()),
912 });
913 }
914 }
915 for ni in (bv.len()..av.len()).rev() {
917 patches.push(ScorePatch::RemoveNote {
918 part: pi, staff: si, measure: mi, voice: vi, note_index: ni,
919 });
920 }
921 for note in bv.iter().skip(av.len()) {
923 patches.push(ScorePatch::AddNote {
924 part: pi, staff: si, measure: mi, voice: vi,
925 note: Box::new(note.clone()),
926 });
927 }
928 }
929 }
930 }
931 }
932
933 patches
934}
935
936pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
941 let mut s = score.clone();
942 for patch in patches {
943 match patch {
944 ScorePatch::SetMetadata { field, value } => {
945 match field.as_str() {
946 "title" => s.metadata.title = value.clone(),
947 "composer" => s.metadata.composer = value.clone(),
948 "lyricist" => s.metadata.lyricist = value.clone(),
949 "copyright" => s.metadata.copyright = value.clone(),
950 "work_number" => s.metadata.work_number = value.clone(),
951 "movement_title" => s.metadata.movement_title = value.clone(),
952 other => return Err(Error::InvalidPatch(format!("unknown metadata field: {other}"))),
953 }
954 }
955 ScorePatch::SetTempo { value } => {
956 s.settings.tempo_bpm = *value;
957 }
958 ScorePatch::SetKeySignature { part, staff, measure, value } => {
959 s.parts.get_mut(*part)
960 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
961 .staves.get_mut(*staff)
962 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
963 .measures.get_mut(*measure)
964 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
965 .key_sig = Some(value.clone());
966 }
967 ScorePatch::AddNote { part, staff, measure, voice, note } => {
968 let v = s.parts.get_mut(*part)
969 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
970 .staves.get_mut(*staff)
971 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
972 .measures.get_mut(*measure)
973 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
974 .voices.get_mut(*voice)
975 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
976 v.push(*note.clone());
977 }
978 ScorePatch::RemoveNote { part, staff, measure, voice, note_index } => {
979 let v = s.parts.get_mut(*part)
980 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
981 .staves.get_mut(*staff)
982 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
983 .measures.get_mut(*measure)
984 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
985 .voices.get_mut(*voice)
986 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
987 if *note_index >= v.len() {
988 return Err(Error::InvalidPatch(format!("note_index {note_index} out of range")));
989 }
990 v.remove(*note_index);
991 }
992 ScorePatch::ReplaceNote { part, staff, measure, voice, note_index, note } => {
993 let v = s.parts.get_mut(*part)
994 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
995 .staves.get_mut(*staff)
996 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
997 .measures.get_mut(*measure)
998 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
999 .voices.get_mut(*voice)
1000 .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
1001 if *note_index >= v.len() {
1002 return Err(Error::InvalidPatch(format!("note_index {note_index} out of range")));
1003 }
1004 v[*note_index] = *note.clone();
1005 }
1006 ScorePatch::SetMeasureTempo { part, staff, measure, value } => {
1007 s.parts.get_mut(*part)
1008 .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1009 .staves.get_mut(*staff)
1010 .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1011 .measures.get_mut(*measure)
1012 .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1013 .tempo = *value;
1014 }
1015 }
1016 }
1017 Ok(s)
1018}
1019
1020pub fn respell_score(score: &mut Score, prefer_flat: bool) {
1024 for part in &mut score.parts {
1025 for staff in &mut part.staves {
1026 for measure in &mut staff.measures {
1027 for voice in &mut measure.voices {
1028 for note in voice.iter_mut() {
1029 for pitch in &mut note.pitches {
1030 *pitch = pitch.respell(prefer_flat);
1031 }
1032 }
1033 }
1034 }
1035 }
1036 }
1037}
1038
1039pub fn respell_score_to_key(score: &mut Score) {
1043 let prefer_flat = score.settings.key_signature.fifths < 0;
1044 respell_score(score, prefer_flat);
1045}
1046
1047pub fn score_duration_secs(score: &Score) -> f64 {
1052 if score.settings.tempo_bpm == 0 { return 0.0; }
1053 let seq = measure_sequence(score);
1054 let mut total_secs = 0.0f64;
1055 let mut current_bpm = score.settings.tempo_bpm as f64;
1056 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
1057 for &idx in &seq {
1058 if let Some(m) = staff.measures.get(idx) {
1059 if let Some(b) = m.tempo { current_bpm = b as f64; }
1060 if current_bpm == 0.0 { continue; }
1061 let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1062 total_secs += beats / current_bpm * 60.0;
1063 }
1064 }
1065 }
1066 total_secs
1067}
1068
1069pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
1074 if score.settings.tempo_bpm == 0 { return 0.0; }
1075 let seq: Vec<usize> = measure_sequence(score)
1076 .into_iter()
1077 .filter(|&idx| idx >= region.0 && idx <= region.1)
1078 .collect();
1079 let mut total_secs = 0.0f64;
1080 let mut current_bpm = score.settings.tempo_bpm as f64;
1081 if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
1082 for &idx in &seq {
1083 if let Some(m) = staff.measures.get(idx) {
1084 if let Some(b) = m.tempo { current_bpm = b as f64; }
1085 if current_bpm == 0.0 { continue; }
1086 let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1087 total_secs += beats / current_bpm * 60.0;
1088 }
1089 }
1090 }
1091 total_secs
1092}
1093
1094pub fn measure_beats_remaining(
1099 score: &Score,
1100 part_index: usize,
1101 staff_index: usize,
1102 measure_index: usize,
1103 voice_index: usize,
1104) -> Result<f64, Error> {
1105 let part = score.parts.get(part_index)
1106 .ok_or(Error::PartNotFound(part_index))?;
1107 let staff = part.staves.get(staff_index)
1108 .ok_or(Error::StaffNotFound(staff_index))?;
1109 let measure = staff.measures.get(measure_index)
1110 .ok_or(Error::MeasureNotFound(measure_index))?;
1111 let voice = measure.voices.get(voice_index)
1112 .ok_or(Error::VoiceOutOfRange(voice_index))?;
1113 let ts = measure.time_sig.as_ref().unwrap_or(&score.settings.time_signature);
1114 let used: f64 = voice.iter().map(|n| n.beats()).sum();
1115 Ok((ts.total_beats() - used).max(0.0))
1116}
1117
1118pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
1124 if pitches.is_empty() { return true; }
1125 let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
1126 avg < clef.middle_line_midi() as f64
1127}
1128
1129fn beam_beat_size(ts: &TimeSignature) -> f64 {
1130 if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
1131 3.0 * 4.0 / ts.denominator as f64
1132 } else {
1133 4.0 / ts.denominator as f64
1134 }
1135}
1136
1137pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
1142 let beat_size = beam_beat_size(time_sig);
1143 let n = notes.len();
1144 let mut result = vec![BeamState::None; n];
1145
1146 let is_beamable = |note: &Note| -> bool {
1147 !note.is_rest && matches!(
1148 note.duration,
1149 Duration::Eighth | Duration::Sixteenth | Duration::ThirtySecond | Duration::SixtyFourth
1150 )
1151 };
1152
1153 let mut starts = Vec::with_capacity(n);
1155 let mut pos = 0.0f64;
1156 for note in notes {
1157 starts.push(pos);
1158 pos += note.beats();
1159 }
1160
1161 let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
1163
1164 let mut i = 0;
1165 while i < n {
1166 if !is_beamable(¬es[i]) { i += 1; continue; }
1167 let g = group_id(i);
1168 let mut j = i;
1170 while j < n && is_beamable(¬es[j]) && group_id(j) == g { j += 1; }
1171 let run = j - i;
1172 if run == 1 {
1173 result[i] = BeamState::None;
1174 } else {
1175 result[i] = BeamState::Begin;
1176 result[i+1..j-1].fill(BeamState::Continue);
1177 result[j-1] = BeamState::End;
1178 }
1179 i = j;
1180 }
1181 result
1182}
1183
1184fn note_content_eq(a: &Note, b: &Note) -> bool {
1185 a.is_rest == b.is_rest
1186 && a.pitches == b.pitches
1187 && a.duration == b.duration
1188 && a.dot_count == b.dot_count
1189 && a.tie_start == b.tie_start
1190 && a.tie_end == b.tie_end
1191 && a.beam == b.beam
1192 && a.articulations == b.articulations
1193 && a.dynamic == b.dynamic
1194 && a.stem_up == b.stem_up
1195 && a.hairpin_start == b.hairpin_start
1196 && a.hairpin_end == b.hairpin_end
1197 && a.tuplet == b.tuplet
1198 && a.chord_symbol == b.chord_symbol
1199 && a.is_grace == b.is_grace
1200 && a.grace_slash == b.grace_slash
1201 && a.ottava_start == b.ottava_start
1202 && a.ottava_end == b.ottava_end
1203 && a.lyric == b.lyric
1204 && a.pedal_start == b.pedal_start
1205 && a.pedal_end == b.pedal_end
1206 && a.slur_start == b.slur_start
1207 && a.slur_end == b.slur_end
1208 && a.arpeggiate == b.arpeggiate
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213 use super::*;
1214 use crate::model::pitch::Step;
1215
1216 #[test]
1217 fn default_score_has_one_part_four_measures() {
1218 let score = Score::default();
1219 assert_eq!(score.parts.len(), 1);
1220 assert_eq!(score.parts[0].staves.len(), 1);
1221 assert_eq!(score.parts[0].staves[0].measures.len(), 4);
1222 }
1223
1224 #[test]
1225 fn new_score_measure_count() {
1226 let score = Score::new("Test", 120, 4, 4, 0, 8);
1227 assert_eq!(score.measure_count(), 8);
1228 }
1229
1230 #[test]
1231 fn note_beats_quarter() {
1232 let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1233 assert!((note.beats() - 1.0).abs() < 1e-9);
1234 }
1235
1236 #[test]
1237 fn note_beats_dotted_quarter() {
1238 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1239 note.dot_count = 1;
1240 assert!((note.beats() - 1.5).abs() < 1e-9);
1241 }
1242
1243 #[test]
1244 fn grace_note_beats_zero() {
1245 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1246 note.is_grace = true;
1247 assert_eq!(note.beats(), 0.0);
1248 }
1249
1250 #[test]
1251 fn measure_empty_4_4_fills_four_beats() {
1252 let m = Measure::empty(4, 4);
1253 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1254 assert!((total - 4.0).abs() < 1e-9);
1255 }
1256
1257 #[test]
1258 fn measure_empty_3_4_fills_three_beats() {
1259 let m = Measure::empty(3, 4);
1260 let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1261 assert!((total - 3.0).abs() < 1e-9);
1262 }
1263
1264 #[test]
1265 fn whole_filling_beats() {
1266 assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
1267 assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
1268 assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
1269 }
1270
1271 #[test]
1274 fn statistics_default_score_all_rests() {
1275 let score = Score::default();
1276 let s = score.statistics();
1277 assert_eq!(s.part_count, 1);
1278 assert_eq!(s.measure_count, 4);
1279 assert_eq!(s.note_count, 0);
1280 assert!(s.rest_count > 0);
1281 }
1282
1283 #[test]
1284 fn statistics_duration_estimate() {
1285 let score = Score::new("T", 120, 4, 4, 0, 1);
1287 let s = score.statistics();
1288 assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
1289 }
1290
1291 #[test]
1292 fn score_duration_secs_matches_statistics() {
1293 use super::score_duration_secs;
1294 let score = Score::new("T", 120, 4, 4, 0, 4);
1295 let secs = score_duration_secs(&score);
1296 assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
1298 }
1299
1300 #[test]
1301 fn score_duration_secs_zero_bpm_returns_zero() {
1302 use super::score_duration_secs;
1303 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1304 score.settings.tempo_bpm = 0;
1305 assert_eq!(score_duration_secs(&score), 0.0);
1306 }
1307
1308 #[test]
1309 fn score_duration_secs_per_measure_tempo() {
1310 use super::score_duration_secs;
1311 let mut score = Score::new("T", 120, 4, 4, 0, 2);
1313 score.parts[0].staves[0].measures[1].tempo = Some(60);
1314 let secs = score_duration_secs(&score);
1315 assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
1316 }
1317
1318 #[test]
1321 fn extract_part_returns_single_part_score() {
1322 let mut score = Score::default();
1323 let mut p2 = Part::new("Violin", "Vln.");
1324 p2.staves.push(Staff::new(Clef::Treble));
1325 score.parts.push(p2);
1326 let ex = score.extract_part(0).unwrap();
1327 assert_eq!(ex.parts.len(), 1);
1328 assert_ne!(ex.id, score.id);
1329 assert_eq!(ex.metadata.title, score.metadata.title);
1330 }
1331
1332 #[test]
1333 fn extract_part_out_of_range_is_none() {
1334 let score = Score::default();
1335 assert!(score.extract_part(99).is_none());
1336 }
1337
1338 #[test]
1341 fn transpose_zero_is_clone() {
1342 let score = Score::new("T", 120, 4, 4, 0, 1);
1343 let t = transpose(&score, 0);
1344 assert_eq!(t.settings.key_signature.fifths, 0);
1345 }
1346
1347 #[test]
1348 fn transpose_c_major_up_2_to_d_major() {
1349 let score = Score::new("T", 120, 4, 4, 0, 1);
1350 assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
1351 }
1352
1353 #[test]
1354 fn transpose_d_major_up_5_to_g_major() {
1355 let score = Score::new("T", 120, 4, 4, 2, 1);
1356 assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
1357 }
1358
1359 #[test]
1360 fn transpose_c4_up_1_to_csharp4() {
1361 let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
1362 assert_eq!(p.to_midi(), 61);
1363 assert_eq!(p.step, Step::C);
1364 assert_eq!(p.alter, 1);
1365 }
1366
1367 #[test]
1368 fn transpose_c4_down_1_to_b3() {
1369 let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
1370 assert_eq!(p.to_midi(), 59);
1371 assert_eq!(p.step, Step::B);
1372 assert_eq!(p.alter, 0);
1373 }
1374
1375 #[test]
1376 fn transpose_up_octave_keeps_step() {
1377 let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
1378 assert_eq!(p.to_midi(), 81);
1379 assert_eq!(p.step, Step::A);
1380 assert_eq!(p.octave, 5);
1381 }
1382
1383 #[test]
1384 fn statistics_with_repeat_doubles_duration() {
1385 let mut score = Score::new("T", 120, 4, 4, 0, 2);
1387 score.parts[0].staves[0].measures[0].barline_left =
1388 crate::model::notation::Barline::RepeatStart;
1389 score.parts[0].staves[0].measures[1].barline_right =
1390 crate::model::notation::Barline::RepeatEnd;
1391 let s = score.statistics();
1392 assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
1394 }
1395
1396 #[test]
1397 fn transpose_octave_boundary_b4_to_c5() {
1398 let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
1400 assert_eq!(p.to_midi(), 72);
1401 assert_eq!(p.step, Step::C);
1402 assert_eq!(p.octave, 5);
1403 }
1404
1405 #[test]
1406 fn transpose_clamp_at_midi_127() {
1407 let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
1409 assert_eq!(p.to_midi(), 127);
1410 }
1411
1412 #[test]
1415 fn merge_combines_parts() {
1416 let mut a = Score::new("A", 120, 4, 4, 0, 2);
1417 let b = Score::new("B", 120, 4, 4, 0, 2);
1418 let mut p2 = Part::new("Violin", "Vln.");
1420 p2.staves.push(Staff::new(Clef::Treble));
1421 for i in 0..2usize {
1422 let mut m = Measure::empty(4, 4);
1423 m.number = i as u32 + 1;
1424 p2.staves[0].measures.push(m);
1425 }
1426 a.parts.push(p2);
1427 let merged = a.merge(&b);
1428 assert_eq!(merged.parts.len(), 3);
1430 }
1431
1432 #[test]
1433 fn merge_pads_shorter_score() {
1434 let a = Score::new("A", 120, 4, 4, 0, 4);
1435 let b = Score::new("B", 120, 4, 4, 0, 2);
1436 let merged = a.merge(&b);
1437 assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
1439 assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
1440 }
1441
1442 #[test]
1443 fn merge_uses_self_metadata() {
1444 let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
1445 a.metadata.composer = "Composer A".to_string();
1446 let b = Score::new("Title B", 120, 4, 4, 0, 2);
1447 let merged = a.merge(&b);
1448 assert_eq!(merged.metadata.title, "Title A");
1449 assert_eq!(merged.metadata.composer, "Composer A");
1450 }
1451
1452 #[test]
1453 fn merge_new_id_differs_from_both() {
1454 let a = Score::new("A", 120, 4, 4, 0, 2);
1455 let b = Score::new("B", 120, 4, 4, 0, 2);
1456 let merged = a.merge(&b);
1457 assert_ne!(merged.id, a.id);
1458 assert_ne!(merged.id, b.id);
1459 }
1460
1461 #[test]
1464 fn staff_default_transpose_is_zero() {
1465 let s = Staff::new(Clef::Treble);
1466 assert_eq!(s.transpose_semitones, 0);
1467 }
1468
1469 #[test]
1472 fn score_default_has_schema_version_1() {
1473 let score = Score::default();
1474 assert_eq!(score.schema_version, 1);
1475 }
1476
1477 #[test]
1478 fn score_new_has_schema_version_1() {
1479 let score = Score::new("T", 120, 4, 4, 0, 4);
1480 assert_eq!(score.schema_version, 1);
1481 }
1482
1483 #[test]
1484 fn score_without_schema_version_deserializes_to_zero() {
1485 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":[]}"#;
1486 let score: Score = serde_json::from_str(json).unwrap();
1487 assert_eq!(score.schema_version, 0);
1488 }
1489
1490 #[test]
1493 fn score_template_solo_has_one_part_treble() {
1494 let score = Score::template(ScoreTemplate::Solo);
1495 assert_eq!(score.parts.len(), 1);
1496 assert_eq!(score.parts[0].staves.len(), 1);
1497 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
1498 assert_eq!(score.parts[0].midi_program, 0);
1499 }
1500
1501 #[test]
1502 fn score_template_piano_has_two_staves() {
1503 let score = Score::template(ScoreTemplate::Piano);
1504 assert_eq!(score.parts.len(), 1);
1505 assert_eq!(score.parts[0].staves.len(), 2);
1506 assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
1507 assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
1508 }
1509
1510 #[test]
1511 fn score_template_string_quartet_has_four_parts() {
1512 let score = Score::template(ScoreTemplate::StringQuartet);
1513 assert_eq!(score.parts.len(), 4);
1514 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);
1517 assert_eq!(score.parts[3].midi_program, 42);
1518 }
1519
1520 #[test]
1521 fn score_template_string_orchestra_has_five_parts() {
1522 let score = Score::template(ScoreTemplate::StringOrchestra);
1523 assert_eq!(score.parts.len(), 5);
1524 assert_eq!(score.parts[4].midi_program, 43); }
1526
1527 #[test]
1528 fn score_template_brass_quintet_has_five_parts() {
1529 let score = Score::template(ScoreTemplate::BrassQuintet);
1530 assert_eq!(score.parts.len(), 5);
1531 assert_eq!(score.parts[2].midi_program, 60); }
1533
1534 #[test]
1535 fn score_template_default_measures_are_four() {
1536 let score = Score::template(ScoreTemplate::StringQuartet);
1537 for part in &score.parts {
1538 for staff in &part.staves {
1539 assert_eq!(staff.measures.len(), 4);
1540 }
1541 }
1542 }
1543
1544 #[test]
1547 fn measure_empty_has_no_breaks() {
1548 let m = Measure::empty(4, 4);
1549 assert!(!m.system_break);
1550 assert!(!m.page_break);
1551 }
1552
1553 #[test]
1554 fn system_break_survives_json_roundtrip() {
1555 let mut m = Measure::empty(4, 4);
1556 m.system_break = true;
1557 let json = serde_json::to_string(&m).unwrap();
1558 let m2: Measure = serde_json::from_str(&json).unwrap();
1559 assert!(m2.system_break);
1560 assert!(!m2.page_break);
1561 }
1562
1563 #[test]
1566 fn diff_identical_scores_is_empty() {
1567 let s = Score::new("T", 120, 4, 4, 0, 2);
1568 assert!(diff(&s, &s).is_empty());
1569 }
1570
1571 #[test]
1572 fn diff_detects_tempo_change() {
1573 let a = Score::new("T", 120, 4, 4, 0, 1);
1574 let mut b = a.clone();
1575 b.settings.tempo_bpm = 90;
1576 let changes = diff(&a, &b);
1577 assert_eq!(changes.len(), 1);
1578 assert!(matches!(changes[0], ScoreChange::TempoChanged { old: 120, new: 90 }));
1579 }
1580
1581 #[test]
1582 fn diff_detects_title_change() {
1583 let a = Score::new("Old Title", 120, 4, 4, 0, 1);
1584 let mut b = a.clone();
1585 b.metadata.title = "New Title".to_string();
1586 let changes = diff(&a, &b);
1587 assert!(changes.iter().any(|c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")));
1588 }
1589
1590 #[test]
1591 fn diff_detects_note_modification() {
1592 let mut a = Score::new("T", 120, 4, 4, 0, 1);
1593 a.parts[0].staves[0].measures[0].voices[0] =
1594 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
1595 let mut b = a.clone();
1596 b.parts[0].staves[0].measures[0].voices[0][0] =
1597 Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
1598 let changes = diff(&a, &b);
1599 assert!(changes.iter().any(|c| matches!(c, ScoreChange::NoteModified { .. })));
1600 }
1601
1602 #[test]
1603 fn diff_detects_part_added() {
1604 let a = Score::new("T", 120, 4, 4, 0, 1);
1605 let mut b = a.clone();
1606 let mut p = Part::new("Violin", "Vln.");
1607 p.staves.push(Staff::new(Clef::Treble));
1608 b.parts.push(p);
1609 let changes = diff(&a, &b);
1610 assert!(changes.iter().any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 })));
1611 }
1612
1613 #[test]
1614 fn diff_detects_measure_tempo_change() {
1615 let a = Score::new("T", 120, 4, 4, 0, 2);
1616 let mut b = a.clone();
1617 b.parts[0].staves[0].measures[1].tempo = Some(60);
1618 let changes = diff(&a, &b);
1619 assert!(changes.iter().any(|c| matches!(c, ScoreChange::MeasureTempoChanged {
1620 measure: 1, old: None, new: Some(60), ..
1621 })));
1622 }
1623
1624 #[test]
1625 fn diff_detects_barline_change() {
1626 use crate::model::notation::Barline;
1627 let a = Score::new("T", 120, 4, 4, 0, 2);
1628 let mut b = a.clone();
1629 b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
1630 let changes = diff(&a, &b);
1631 assert!(changes.iter().any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. })));
1632 }
1633
1634 #[test]
1635 fn diff_detects_rehearsal_change() {
1636 let a = Score::new("T", 120, 4, 4, 0, 2);
1637 let mut b = a.clone();
1638 b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
1639 let changes = diff(&a, &b);
1640 assert!(changes.iter().any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. })));
1641 }
1642
1643 #[test]
1644 fn diff_detects_volta_change() {
1645 use super::VoltaBracket;
1646 let a = Score::new("T", 120, 4, 4, 0, 2);
1647 let mut b = a.clone();
1648 b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket { number: 1, kind: "begin_end".into() });
1649 let changes = diff(&a, &b);
1650 assert!(changes.iter().any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. })));
1651 }
1652
1653 #[test]
1654 fn diff_detects_key_signature_change() {
1655 let a = Score::new("T", 120, 4, 4, 0, 1);
1656 let mut b = a.clone();
1657 b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
1659 assert!(changes.iter().any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. })));
1660 }
1661
1662 #[test]
1663 fn diff_same_key_signature_no_change() {
1664 let a = Score::new("T", 120, 4, 4, 2, 1);
1665 let changes = diff(&a, &a);
1666 assert!(changes.is_empty());
1667 }
1668
1669 #[test]
1670 fn score_duration_secs_region_partial() {
1671 use super::score_duration_secs_region;
1672 let score = Score::new("T", 120, 4, 4, 0, 4);
1674 let secs = score_duration_secs_region(&score, (1, 2));
1675 assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
1676 }
1677
1678 #[test]
1679 fn score_duration_secs_region_single_measure() {
1680 use super::score_duration_secs_region;
1681 let score = Score::new("T", 120, 4, 4, 0, 4);
1683 let secs = score_duration_secs_region(&score, (0, 0));
1684 assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
1685 }
1686
1687 #[test]
1690 fn measure_beats_remaining_empty_voice_returns_full() {
1691 use super::measure_beats_remaining;
1692 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1693 score.parts[0].staves[0].measures[0].voices[0].clear();
1694 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
1695 assert!((rem - 4.0).abs() < 1e-9, "expected 4.0 remaining, got {rem}");
1696 }
1697
1698 #[test]
1699 fn measure_beats_remaining_half_full_returns_half() {
1700 use crate::model::pitch::Step;
1701 use super::measure_beats_remaining;
1702 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1703 score.parts[0].staves[0].measures[0].voices[0] = vec![
1704 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
1705 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
1706 ];
1707 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
1708 assert!((rem - 2.0).abs() < 1e-9, "expected 2.0 remaining, got {rem}");
1709 }
1710
1711 #[test]
1712 fn measure_beats_remaining_full_voice_returns_zero() {
1713 use crate::model::pitch::Step;
1714 use super::measure_beats_remaining;
1715 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1716 score.parts[0].staves[0].measures[0].voices[0] =
1717 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
1718 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
1719 assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
1720 }
1721
1722 #[test]
1723 fn measure_beats_remaining_tuplet_accounting() {
1724 use crate::model::pitch::Step;
1725 use crate::model::notation::TupletInfo;
1726 use super::measure_beats_remaining;
1727 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1729 let tuplet = TupletInfo { actual_notes: 3, normal_notes: 2 };
1730 let mk = |step| {
1731 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
1732 n.tuplet = Some(tuplet.clone());
1733 n
1734 };
1735 score.parts[0].staves[0].measures[0].voices[0] =
1736 vec![mk(Step::C), mk(Step::D), mk(Step::E)];
1737 let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
1738 assert!((rem - 2.0).abs() < 1e-9, "expected 2.0 remaining (triplets used 2.0), got {rem}");
1739 }
1740
1741 #[test]
1742 fn measure_beats_remaining_out_of_range_returns_err() {
1743 use super::measure_beats_remaining;
1744 let score = Score::new("T", 120, 4, 4, 0, 1);
1745 assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
1746 assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
1747 assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
1748 assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
1749 }
1750
1751 #[test]
1752 fn note_content_eq_ignores_id() {
1753 use crate::model::pitch::Step;
1754 let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1755 let mut b = a.clone();
1756 b.id = "different-id".to_string();
1757 assert!(note_content_eq(&a, &b));
1758 b.pitches[0] = Pitch::new(Step::D, 4);
1760 assert!(!note_content_eq(&a, &b));
1761 let mut c = a.clone();
1763 a.stem_up = Some(true);
1764 c.stem_up = Some(false);
1765 assert!(!note_content_eq(&a, &c));
1766 }
1767
1768 #[test]
1769 fn suggested_stem_up_below_middle() {
1770 use crate::model::notation::Clef;
1771 let pitches = vec![Pitch::new(Step::C, 4)];
1773 assert!(suggested_stem_up(&pitches, &Clef::Treble));
1774 }
1775
1776 #[test]
1777 fn suggested_stem_up_above_middle() {
1778 use crate::model::notation::Clef;
1779 let pitches = vec![Pitch::new(Step::G, 5)];
1781 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
1782 }
1783
1784 #[test]
1785 fn suggested_stem_up_at_middle_line() {
1786 use crate::model::notation::Clef;
1787 let pitches = vec![Pitch::new(Step::B, 4)];
1789 assert!(!suggested_stem_up(&pitches, &Clef::Treble));
1790 }
1791
1792 #[test]
1793 fn suggested_stem_up_chord() {
1794 use crate::model::notation::Clef;
1795 let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
1797 assert!(suggested_stem_up(&pitches, &Clef::Treble));
1798 }
1799
1800 #[test]
1801 fn suggested_stem_up_bass_clef() {
1802 use crate::model::notation::Clef;
1803 let pitches = vec![Pitch::new(Step::D, 3)];
1805 assert!(!suggested_stem_up(&pitches, &Clef::Bass));
1806 let pitches2 = vec![Pitch::new(Step::C, 3)];
1808 assert!(suggested_stem_up(&pitches2, &Clef::Bass));
1809 }
1810
1811 #[test]
1812 fn suggested_stem_up_empty_pitches() {
1813 use crate::model::notation::Clef;
1814 assert!(suggested_stem_up(&[], &Clef::Treble));
1815 }
1816
1817 fn eighth(pitch: Pitch) -> Note { Note::new(pitch, Duration::Eighth) }
1818 fn quarter(pitch: Pitch) -> Note { Note::new(pitch, Duration::Quarter) }
1819 fn rest_eighth() -> Note { Note::rest(Duration::Eighth) }
1820
1821 #[test]
1822 fn compute_beams_4_4_four_eighths() {
1823 use crate::model::notation::{Clef, TimeSignature};
1824 let _ = Clef::Treble; let ts = TimeSignature { numerator: 4, denominator: 4 };
1826 let c4 = Pitch::new(Step::C, 4);
1827 let notes = vec![eighth(c4.clone()), eighth(c4.clone()), eighth(c4.clone()), eighth(c4.clone())];
1828 let beams = compute_beams(¬es, &ts);
1829 assert_eq!(beams[0], BeamState::Begin);
1831 assert_eq!(beams[1], BeamState::End);
1832 assert_eq!(beams[2], BeamState::Begin);
1833 assert_eq!(beams[3], BeamState::End);
1834 }
1835
1836 #[test]
1837 fn compute_beams_4_4_all_eighth_one_group() {
1838 use crate::model::notation::TimeSignature;
1839 let ts = TimeSignature { numerator: 4, denominator: 4 };
1840 let c4 = Pitch::new(Step::C, 4);
1841 let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
1843 let beams = compute_beams(¬es, &ts);
1844 assert_eq!(beams[0], BeamState::Begin);
1845 assert_eq!(beams[1], BeamState::End);
1846 }
1847
1848 #[test]
1849 fn compute_beams_quarter_not_beamed() {
1850 use crate::model::notation::TimeSignature;
1851 let ts = TimeSignature { numerator: 4, denominator: 4 };
1852 let c4 = Pitch::new(Step::C, 4);
1853 let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
1854 let beams = compute_beams(¬es, &ts);
1855 assert_eq!(beams[0], BeamState::None);
1856 assert_eq!(beams[1], BeamState::None);
1857 }
1858
1859 #[test]
1860 fn compute_beams_rest_breaks_beam() {
1861 use crate::model::notation::TimeSignature;
1862 let ts = TimeSignature { numerator: 4, denominator: 4 };
1863 let c4 = Pitch::new(Step::C, 4);
1864 let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
1865 let beams = compute_beams(¬es, &ts);
1866 assert_eq!(beams[0], BeamState::None);
1868 assert_eq!(beams[1], BeamState::None);
1869 assert_eq!(beams[2], BeamState::None);
1870 }
1871
1872 #[test]
1873 fn compute_beams_6_8_compound() {
1874 use crate::model::notation::TimeSignature;
1875 let ts = TimeSignature { numerator: 6, denominator: 8 };
1876 let c4 = Pitch::new(Step::C, 4);
1877 let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
1879 let beams = compute_beams(¬es, &ts);
1880 assert_eq!(beams[0], BeamState::Begin);
1881 assert_eq!(beams[1], BeamState::Continue);
1882 assert_eq!(beams[2], BeamState::End);
1883 assert_eq!(beams[3], BeamState::Begin);
1884 assert_eq!(beams[4], BeamState::Continue);
1885 assert_eq!(beams[5], BeamState::End);
1886 }
1887
1888 #[test]
1889 fn compute_beams_single_eighth() {
1890 use crate::model::notation::TimeSignature;
1891 let ts = TimeSignature { numerator: 4, denominator: 4 };
1892 let c4 = Pitch::new(Step::C, 4);
1893 let notes = vec![eighth(c4.clone())];
1894 let beams = compute_beams(¬es, &ts);
1895 assert_eq!(beams[0], BeamState::None);
1896 }
1897}