Skip to main content

acorde_core/model/
score.rs

1use super::{
2    duration::Duration,
3    notation::{
4        Articulation, Barline, BeamState, ChordSymbol, Clef, CrossStaff, Dynamic, GuitarTechnique,
5        HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind, StyledText, TimeSignature,
6        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/// Visual connector symbol for a group of adjacent parts.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum PartGroupSymbol {
57    Bracket, // square bracket — orchestral strings, woodwinds
58    Brace,   // curly brace — piano grand staff
59    Line,    // thin vertical line
60}
61
62/// Groups a range of adjacent parts with a bracket or brace for rendering.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PartGroup {
65    /// Index of the first part in the group (inclusive).
66    pub first_part: usize,
67    /// Index of the last part in the group (inclusive).
68    pub last_part: usize,
69    pub symbol: PartGroupSymbol,
70    /// Whether barlines are connected across all staves in the group.
71    #[serde(default)]
72    pub barlines_connect: bool,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Score {
77    pub id: String,
78    /// JSON schema version. 0 when deserialized from files that predate this field.
79    #[serde(default)]
80    pub schema_version: u32,
81    pub metadata: ScoreMetadata,
82    pub settings: ScoreSettings,
83    pub parts: Vec<Part>,
84    #[serde(default)]
85    pub part_groups: Vec<PartGroup>,
86    /// Typed score-level text annotations retained independently of legacy text fields.
87    #[serde(default)]
88    pub texts: Vec<StyledText>,
89}
90
91impl Default for Score {
92    fn default() -> Self {
93        let mut part = Part::new("Piano", "Pno.");
94        part.staves.push(Staff::new(Clef::Treble));
95        for _ in 0..4 {
96            part.staves[0].measures.push(Measure::empty(4, 4));
97        }
98        Self {
99            id: Uuid::new_v4().to_string(),
100            schema_version: 1,
101            metadata: ScoreMetadata::default(),
102            settings: ScoreSettings::default(),
103            parts: vec![part],
104            part_groups: Vec::new(),
105            texts: Vec::new(),
106        }
107    }
108}
109
110/// Score template presets for common ensemble configurations.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112pub enum ScoreTemplate {
113    /// Single treble-clef part (piano by default).
114    Solo,
115    /// One piano part with treble + bass grand staff.
116    Piano,
117    /// Violin I, Violin II, Viola, Cello.
118    StringQuartet,
119    /// Violin I, Violin II, Viola, Cello, Contrabass.
120    StringOrchestra,
121    /// Two trumpets, French horn, trombone, tuba.
122    BrassQuintet,
123}
124
125impl Score {
126    pub fn new(
127        title: &str,
128        tempo_bpm: u16,
129        numerator: u8,
130        denominator: u8,
131        fifths: i8,
132        measure_count: u32,
133    ) -> Self {
134        let mut score = Score::default();
135        score.metadata.title = title.to_string();
136        score.settings.tempo_bpm = tempo_bpm;
137        score.settings.time_signature = TimeSignature {
138            numerator,
139            denominator,
140        };
141        score.settings.key_signature = KeySignature {
142            fifths,
143            mode: "major".to_string(),
144        };
145
146        score.parts[0].staves[0].measures.clear();
147        for i in 0..measure_count {
148            let mut m = Measure::empty(numerator, denominator);
149            m.number = i + 1;
150            score.parts[0].staves[0].measures.push(m);
151        }
152        score
153    }
154
155    /// Create a score pre-populated with parts for the given ensemble template.
156    ///
157    /// Defaults: 120 BPM, 4/4, C major, 4 empty measures.
158    /// Use [`NewScoreCmd`](crate::model::commands::NewScoreCmd) to override those after creation.
159    pub fn template(kind: ScoreTemplate) -> Self {
160        fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
161            (0..count)
162                .map(|i| {
163                    let mut m = Measure::empty(num, den);
164                    m.number = i + 1;
165                    m
166                })
167                .collect()
168        }
169        fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
170            let mut p = Part::new(name, short);
171            p.midi_program = program;
172            let mut s = Staff::new(clef);
173            s.measures = measures(4, 4, 4);
174            p.staves.push(s);
175            p
176        }
177
178        let mut score = Score {
179            id: uuid::Uuid::new_v4().to_string(),
180            schema_version: 1,
181            metadata: ScoreMetadata::default(),
182            settings: ScoreSettings::default(),
183            parts: Vec::new(),
184            part_groups: Vec::new(),
185            texts: Vec::new(),
186        };
187
188        match kind {
189            ScoreTemplate::Solo => {
190                score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
191            }
192            ScoreTemplate::Piano => {
193                let mut p = Part::new("Piano", "Pno.");
194                p.midi_program = 0;
195                let mut treble = Staff::new(Clef::Treble);
196                treble.measures = measures(4, 4, 4);
197                let mut bass = Staff::new(Clef::Bass);
198                bass.measures = measures(4, 4, 4);
199                p.staves.push(treble);
200                p.staves.push(bass);
201                score.parts.push(p);
202            }
203            ScoreTemplate::StringQuartet => {
204                score
205                    .parts
206                    .push(part("Violin I", "Vn. I", Clef::Treble, 40));
207                score
208                    .parts
209                    .push(part("Violin II", "Vn. II", Clef::Treble, 40));
210                score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
211                score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
212            }
213            ScoreTemplate::StringOrchestra => {
214                score
215                    .parts
216                    .push(part("Violin I", "Vn. I", Clef::Treble, 40));
217                score
218                    .parts
219                    .push(part("Violin II", "Vn. II", Clef::Treble, 40));
220                score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
221                score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
222                score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
223            }
224            ScoreTemplate::BrassQuintet => {
225                score
226                    .parts
227                    .push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
228                score
229                    .parts
230                    .push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
231                score
232                    .parts
233                    .push(part("French Horn", "Hn.", Clef::Treble, 60));
234                score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
235                score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
236            }
237        }
238        score
239    }
240
241    pub fn measure_count(&self) -> usize {
242        self.parts
243            .first()
244            .and_then(|p| p.staves.first())
245            .map(|s| s.measures.len())
246            .unwrap_or(0)
247    }
248
249    /// Aggregate statistics about the score.
250    pub fn statistics(&self) -> ScoreStats {
251        let measure_count = self.measure_count();
252        let part_count = self.parts.len();
253
254        // Beat accumulation via measure_sequence so repeats are counted correctly.
255        let seq = measure_sequence(self);
256        let total_beats: f64 = self
257            .parts
258            .first()
259            .and_then(|p| p.staves.first())
260            .map(|s| {
261                seq.iter()
262                    .filter_map(|&idx| s.measures.get(idx))
263                    .flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
264                    .map(|n| n.beats())
265                    .sum()
266            })
267            .unwrap_or(0.0);
268
269        let mut note_count = 0usize;
270        let mut rest_count = 0usize;
271        for part in &self.parts {
272            for staff in &part.staves {
273                for measure in &staff.measures {
274                    for voice in &measure.voices {
275                        for note in voice {
276                            if note.is_rest {
277                                rest_count += 1;
278                            } else {
279                                note_count += 1;
280                            }
281                        }
282                    }
283                }
284            }
285        }
286
287        let bpm = self.settings.tempo_bpm as f64;
288        let estimated_duration_secs = if bpm > 0.0 {
289            total_beats / bpm * 60.0
290        } else {
291            0.0
292        };
293
294        ScoreStats {
295            measure_count,
296            note_count,
297            rest_count,
298            part_count,
299            estimated_duration_secs,
300        }
301    }
302
303    /// Return a new `Score` containing only the given part.
304    /// Returns `None` if `part_index` is out of range.
305    pub fn extract_part(&self, part_index: usize) -> Option<Score> {
306        let part = self.parts.get(part_index)?.clone();
307        Some(Score {
308            id: Uuid::new_v4().to_string(),
309            schema_version: 1,
310            metadata: self.metadata.clone(),
311            settings: self.settings.clone(),
312            parts: vec![part],
313            part_groups: Vec::new(),
314            texts: self.texts.clone(),
315        })
316    }
317
318    /// Merge two scores by appending `other`'s parts to `self`'s parts.
319    /// Shorter scores are padded with empty measures to match the longer one.
320    /// Metadata and settings are taken from `self`.
321    pub fn merge(&self, other: &Score) -> Score {
322        let self_count = self.measure_count();
323        let other_count = other.measure_count();
324        let max_count = self_count.max(other_count);
325        let ts = self.settings.time_signature.clone();
326
327        let pad = |mut part: Part, from: usize| -> Part {
328            for staff in &mut part.staves {
329                for i in from..max_count {
330                    let mut m = Measure::empty(ts.numerator, ts.denominator);
331                    m.number = i as u32 + 1;
332                    staff.measures.push(m);
333                }
334            }
335            part
336        };
337
338        let mut parts: Vec<Part> = self
339            .parts
340            .iter()
341            .cloned()
342            .map(|p| pad(p, self_count))
343            .collect();
344        for p in &other.parts {
345            parts.push(pad(p.clone(), other_count));
346        }
347
348        Score {
349            id: Uuid::new_v4().to_string(),
350            schema_version: 1,
351            metadata: self.metadata.clone(),
352            settings: self.settings.clone(),
353            parts,
354            part_groups: Vec::new(),
355            texts: self.texts.clone(),
356        }
357    }
358}
359
360/// Aggregate statistics returned by [`Score::statistics`].
361#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
362pub struct ScoreStats {
363    pub measure_count: usize,
364    /// Number of non-rest notes across all parts.
365    pub note_count: usize,
366    pub rest_count: usize,
367    pub part_count: usize,
368    /// Rough estimate: `total_beats(first part) / tempo_bpm * 60`.
369    pub estimated_duration_secs: f64,
370}
371
372// ── transpose ─────────────────────────────────────────────────────────────────
373
374use super::pitch::Step;
375use super::repeat::measure_sequence;
376
377/// Return a new `Score` with all pitches shifted by `semitones`.
378/// Key signatures (global and per-measure) are updated accordingly.
379/// If `semitones == 0` the score is cloned unchanged.
380pub fn transpose(score: &Score, semitones: i8) -> Score {
381    if semitones == 0 {
382        return score.clone();
383    }
384    let mut out = score.clone();
385    out.settings.key_signature.fifths = transpose_fifths(
386        score.settings.key_signature.fifths,
387        &score.settings.key_signature.mode,
388        semitones,
389    );
390    for part in &mut out.parts {
391        for staff in &mut part.staves {
392            for measure in &mut staff.measures {
393                if let Some(ref mut ks) = measure.key_sig {
394                    ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
395                }
396                for voice in &mut measure.voices {
397                    for note in voice.iter_mut() {
398                        for pitch in note.pitches.iter_mut() {
399                            *pitch = transpose_pitch(pitch, semitones);
400                        }
401                    }
402                }
403            }
404        }
405    }
406    out
407}
408
409fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
410    let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
411    let pc = new_midi % 12;
412    let oct = (new_midi / 12) as i8 - 1;
413    let (step, alter): (Step, i8) = if semitones >= 0 {
414        match pc {
415            0 => (Step::C, 0),
416            1 => (Step::C, 1),
417            2 => (Step::D, 0),
418            3 => (Step::D, 1),
419            4 => (Step::E, 0),
420            5 => (Step::F, 0),
421            6 => (Step::F, 1),
422            7 => (Step::G, 0),
423            8 => (Step::G, 1),
424            9 => (Step::A, 0),
425            10 => (Step::A, 1),
426            11 => (Step::B, 0),
427            _ => (Step::C, 0),
428        }
429    } else {
430        match pc {
431            0 => (Step::C, 0),
432            1 => (Step::D, -1),
433            2 => (Step::D, 0),
434            3 => (Step::E, -1),
435            4 => (Step::E, 0),
436            5 => (Step::F, 0),
437            6 => (Step::G, -1),
438            7 => (Step::G, 0),
439            8 => (Step::A, -1),
440            9 => (Step::A, 0),
441            10 => (Step::B, -1),
442            11 => (Step::B, 0),
443            _ => (Step::C, 0),
444        }
445    };
446    Pitch::with_alter(step, oct, alter)
447}
448
449/// Shift a key signature's fifths value by `semitones`.
450///
451/// Uses the circle-of-fifths arithmetic:
452/// - `tonic_pc = (fifths * 7) mod 12`  (for major; minor adds 9 to get relative major tonic)
453/// - `new_fifths = (new_tonic_pc * 7) mod 12`, adjusted to `[-7, 7]`
454fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
455    let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
456    let tonic_pc = if mode == "minor" {
457        ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
458    } else {
459        tonic_major_pc
460    };
461    let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
462    let major_tonic = if mode == "minor" {
463        ((new_tonic as i32 + 3).rem_euclid(12)) as u8
464    } else {
465        new_tonic
466    };
467    let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
468    if raw > 6 { raw - 12 } else { raw }
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct Part {
473    pub id: String,
474    pub name: String,
475    pub short_name: String,
476    pub staves: Vec<Staff>,
477    /// MIDI channel (0–15). Channel 9 is conventionally used for percussion.
478    #[serde(default)]
479    pub midi_channel: u8,
480    /// General MIDI program number (0–127). Default 0 = Acoustic Grand Piano.
481    #[serde(default)]
482    pub midi_program: u8,
483}
484
485impl Part {
486    pub fn new(name: &str, short_name: &str) -> Self {
487        Self {
488            id: Uuid::new_v4().to_string(),
489            name: name.to_string(),
490            short_name: short_name.to_string(),
491            staves: Vec::new(),
492            midi_channel: 0,
493            midi_program: 0,
494        }
495    }
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct Staff {
500    pub clef: Clef,
501    pub measures: Vec<Measure>,
502    /// Semitones to add to written pitch for concert pitch / MIDI output.
503    /// -2 = Bb instrument (clarinet, trumpet), -9 = Eb instrument (alto sax), etc.
504    #[serde(default)]
505    pub transpose_semitones: i8,
506}
507
508impl Staff {
509    pub fn new(clef: Clef) -> Self {
510        Self {
511            clef,
512            measures: Vec::new(),
513            transpose_semitones: 0,
514        }
515    }
516}
517
518#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
519pub struct VoltaBracket {
520    /// Ending number (1, 2, …)
521    pub number: u8,
522    /// "begin" | "mid" | "end" | "begin_end"
523    pub kind: String,
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct Measure {
528    pub number: u32,
529    pub time_sig: Option<TimeSignature>,
530    pub key_sig: Option<KeySignature>,
531    pub clef: Option<Clef>,
532    pub tempo: Option<u16>,
533    pub barline_left: Barline,
534    pub barline_right: Barline,
535    #[serde(default)]
536    pub volta: Option<VoltaBracket>,
537    #[serde(default)]
538    pub tempo_text: Option<String>,
539    #[serde(default)]
540    pub rehearsal: Option<String>,
541    /// Navigation mark: "Segno" | "Coda" | "Fine" | "DaCapo" | "DaCapoAlFine" |
542    /// "DaCapoAlCoda" | "DalSegno" | "DalSegnoAlFine" | "DalSegnoAlCoda" | "ToCoda"
543    #[serde(default)]
544    pub navigation: Option<String>,
545    /// Expression / performance text ("dolce", "espressivo", "con fuoco", etc.).
546    #[serde(default)]
547    pub expression_text: Option<String>,
548    #[serde(default)]
549    pub texts: Vec<StyledText>,
550    /// When ≥ 2, this measure is displayed as a multi-measure rest spanning N measures.
551    #[serde(default)]
552    pub multi_rest_count: Option<u8>,
553    /// Force a new system (row) after this measure.
554    #[serde(default)]
555    pub system_break: bool,
556    /// Force a new page after this measure.
557    #[serde(default)]
558    pub page_break: bool,
559    /// Up to 4 voices; voice 0 is the primary voice.
560    pub voices: [Vec<Note>; 4],
561}
562
563impl Measure {
564    pub fn empty(numerator: u8, denominator: u8) -> Self {
565        let total_beats = TimeSignature {
566            numerator,
567            denominator,
568        }
569        .total_beats();
570        let mut voice0: Vec<Note> = Vec::new();
571        let mut remaining = total_beats;
572        while remaining > 1e-9 {
573            let dur = Duration::whole_filling_beats(remaining);
574            remaining -= dur.beats(0);
575            voice0.push(Note::rest(dur));
576        }
577        Self {
578            number: 0,
579            time_sig: None,
580            key_sig: None,
581            clef: None,
582            tempo: None,
583            barline_left: Barline::Normal,
584            barline_right: Barline::Normal,
585            volta: None,
586            tempo_text: None,
587            rehearsal: None,
588            navigation: None,
589            expression_text: None,
590            texts: Vec::new(),
591            multi_rest_count: None,
592            system_break: false,
593            page_break: false,
594            voices: [voice0, vec![], vec![], vec![]],
595        }
596    }
597
598    pub fn renumber(&mut self, n: u32) {
599        self.number = n;
600    }
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct Note {
605    pub id: String,
606    pub is_rest: bool,
607    /// Single note: one pitch. Chord: multiple pitches (same duration).
608    pub pitches: Vec<Pitch>,
609    pub duration: Duration,
610    pub dot_count: u8,
611    pub tie_start: bool,
612    pub tie_end: bool,
613    pub beam: BeamState,
614    pub articulations: Vec<Articulation>,
615    pub dynamic: Option<Dynamic>,
616    pub stem_up: Option<bool>,
617    #[serde(default)]
618    pub hairpin_start: Option<HairpinKind>,
619    #[serde(default)]
620    pub hairpin_end: bool,
621    #[serde(default)]
622    pub tuplet: Option<TupletInfo>,
623    #[serde(default)]
624    pub chord_symbol: Option<ChordSymbol>,
625    #[serde(default)]
626    pub is_grace: bool,
627    /// Acciaccatura: true (slash through stem). Appoggiatura: false.
628    #[serde(default)]
629    pub grace_slash: bool,
630    #[serde(default)]
631    pub ottava_start: Option<OttavaKind>,
632    #[serde(default)]
633    pub ottava_end: bool,
634    #[serde(default)]
635    pub lyric: Option<Lyric>,
636    #[serde(default)]
637    pub pedal_start: bool,
638    #[serde(default)]
639    pub pedal_end: bool,
640    #[serde(default)]
641    pub slur_start: bool,
642    #[serde(default)]
643    pub slur_end: bool,
644    /// Arpeggiate direction: `Some(true)` = up, `Some(false)` = down, `None` = none.
645    #[serde(default)]
646    pub arpeggiate: Option<bool>,
647    /// Technique/style instruction attached to this note ("pizz.", "arco", "con sord.", etc.).
648    #[serde(default)]
649    pub technique_text: Option<String>,
650    #[serde(default)]
651    pub glissando_start: bool,
652    #[serde(default)]
653    pub glissando_end: bool,
654    #[serde(default)]
655    pub cross_staff: Option<CrossStaff>,
656    /// Left-hand fingering number (0 = open / thumb, 1–5 = fingers).
657    #[serde(default)]
658    pub fingering: Option<u8>,
659    /// String number for plucked/bowed string instruments (1 = highest string).
660    #[serde(default)]
661    pub string_number: Option<u8>,
662    #[serde(default)]
663    pub note_head: NoteHead,
664    /// Cue note (small-sized, does not count toward beat total).
665    #[serde(default)]
666    pub is_cue: bool,
667    /// Start of a multi-note trill line span.
668    #[serde(default)]
669    pub trill_line_start: bool,
670    /// End of a multi-note trill line span.
671    #[serde(default)]
672    pub trill_line_end: bool,
673    /// Guitar-specific playing technique (bend, slide, hammer-on, pull-off).
674    #[serde(default)]
675    pub guitar_technique: Option<GuitarTechnique>,
676}
677
678impl Note {
679    pub fn new(pitch: Pitch, duration: Duration) -> Self {
680        Self {
681            id: Uuid::new_v4().to_string(),
682            is_rest: false,
683            pitches: vec![pitch],
684            duration,
685            dot_count: 0,
686            tie_start: false,
687            tie_end: false,
688            beam: BeamState::None,
689            articulations: Vec::new(),
690            dynamic: None,
691            stem_up: None,
692            hairpin_start: None,
693            hairpin_end: false,
694            tuplet: None,
695            chord_symbol: None,
696            is_grace: false,
697            grace_slash: false,
698            ottava_start: None,
699            ottava_end: false,
700            lyric: None,
701            pedal_start: false,
702            pedal_end: false,
703            slur_start: false,
704            slur_end: false,
705            arpeggiate: None,
706            technique_text: None,
707            glissando_start: false,
708            glissando_end: false,
709            cross_staff: None,
710            fingering: None,
711            string_number: None,
712            note_head: NoteHead::Normal,
713            is_cue: false,
714            trill_line_start: false,
715            trill_line_end: false,
716            guitar_technique: None,
717        }
718    }
719
720    pub fn rest(duration: Duration) -> Self {
721        Self {
722            id: Uuid::new_v4().to_string(),
723            is_rest: true,
724            pitches: Vec::new(),
725            duration,
726            dot_count: 0,
727            tie_start: false,
728            tie_end: false,
729            beam: BeamState::None,
730            articulations: Vec::new(),
731            dynamic: None,
732            stem_up: None,
733            hairpin_start: None,
734            hairpin_end: false,
735            tuplet: None,
736            chord_symbol: None,
737            is_grace: false,
738            grace_slash: false,
739            ottava_start: None,
740            ottava_end: false,
741            lyric: None,
742            pedal_start: false,
743            pedal_end: false,
744            slur_start: false,
745            slur_end: false,
746            arpeggiate: None,
747            technique_text: None,
748            glissando_start: false,
749            glissando_end: false,
750            cross_staff: None,
751            fingering: None,
752            string_number: None,
753            note_head: NoteHead::Normal,
754            is_cue: false,
755            trill_line_start: false,
756            trill_line_end: false,
757            guitar_technique: None,
758        }
759    }
760
761    pub fn beats(&self) -> f64 {
762        if self.is_grace || self.is_cue {
763            return 0.0;
764        }
765        let base = self.duration.beats(self.dot_count);
766        if let Some(ref t) = self.tuplet {
767            base * (t.normal_notes as f64) / (t.actual_notes as f64)
768        } else {
769            base
770        }
771    }
772}
773
774impl Duration {
775    /// Returns the largest single duration that fills the given number of beats.
776    pub fn whole_filling_beats(beats: f64) -> Duration {
777        if beats >= 4.0 {
778            Duration::Whole
779        } else if beats >= 2.0 {
780            Duration::Half
781        } else if beats >= 1.0 {
782            Duration::Quarter
783        } else if beats >= 0.5 {
784            Duration::Eighth
785        } else if beats >= 0.25 {
786            Duration::Sixteenth
787        } else if beats >= 0.125 {
788            Duration::ThirtySecond
789        } else {
790            Duration::SixtyFourth
791        }
792    }
793}
794
795// ── NoteAddr ──────────────────────────────────────────────────────────────────
796
797/// Physical address of a note within a score.
798#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
799pub struct NoteAddr {
800    pub part: usize,
801    pub staff: usize,
802    pub measure: usize,
803    pub voice: usize,
804    pub note: usize,
805}
806
807// ── diff ──────────────────────────────────────────────────────────────────────
808
809/// A single change between two [`Score`] values as reported by [`diff`].
810#[derive(Debug, Clone, Serialize, Deserialize)]
811pub enum ScoreChange {
812    MetadataChanged {
813        field: String,
814        old: String,
815        new: String,
816    },
817    TempoChanged {
818        old: u16,
819        new: u16,
820    },
821    KeySignatureChanged {
822        old: KeySignature,
823        new: KeySignature,
824    },
825    PartAdded {
826        part_index: usize,
827    },
828    PartRemoved {
829        part_index: usize,
830        name: String,
831    },
832    NoteAdded {
833        part: usize,
834        staff: usize,
835        measure: usize,
836        voice: usize,
837        note_index: usize,
838    },
839    NoteRemoved {
840        part: usize,
841        staff: usize,
842        measure: usize,
843        voice: usize,
844        note: Box<Note>,
845    },
846    NoteModified {
847        part: usize,
848        staff: usize,
849        measure: usize,
850        voice: usize,
851        note_index: usize,
852        old: Box<Note>,
853        new: Box<Note>,
854    },
855    TimeSigChanged {
856        part: usize,
857        staff: usize,
858        measure: usize,
859        old: Option<TimeSignature>,
860        new: Option<TimeSignature>,
861    },
862    MeasureTempoChanged {
863        part: usize,
864        staff: usize,
865        measure: usize,
866        old: Option<u16>,
867        new: Option<u16>,
868    },
869    BarlineChanged {
870        part: usize,
871        staff: usize,
872        measure: usize,
873    },
874    RehearsalMarkChanged {
875        part: usize,
876        staff: usize,
877        measure: usize,
878        old: Option<String>,
879        new: Option<String>,
880    },
881    VoltaChanged {
882        part: usize,
883        staff: usize,
884        measure: usize,
885    },
886}
887
888/// Compare two scores and return a list of differences.
889///
890/// Parts, staves, measures, and voices are compared by position. Notes are compared by
891/// position within each voice, ignoring their `id` field. Metadata fields are compared
892/// individually.
893pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
894    let mut changes: Vec<ScoreChange> = Vec::new();
895
896    macro_rules! meta {
897        ($field:ident, $name:literal) => {
898            if a.metadata.$field != b.metadata.$field {
899                changes.push(ScoreChange::MetadataChanged {
900                    field: $name.to_string(),
901                    old: a.metadata.$field.clone(),
902                    new: b.metadata.$field.clone(),
903                });
904            }
905        };
906    }
907    meta!(title, "title");
908    meta!(composer, "composer");
909    meta!(lyricist, "lyricist");
910    meta!(copyright, "copyright");
911    meta!(work_number, "work_number");
912    meta!(movement_title, "movement_title");
913
914    if a.settings.tempo_bpm != b.settings.tempo_bpm {
915        changes.push(ScoreChange::TempoChanged {
916            old: a.settings.tempo_bpm,
917            new: b.settings.tempo_bpm,
918        });
919    }
920    if a.settings.key_signature != b.settings.key_signature {
921        changes.push(ScoreChange::KeySignatureChanged {
922            old: a.settings.key_signature.clone(),
923            new: b.settings.key_signature.clone(),
924        });
925    }
926
927    let a_len = a.parts.len();
928    let b_len = b.parts.len();
929    for i in b_len..a_len {
930        changes.push(ScoreChange::PartRemoved {
931            part_index: i,
932            name: a.parts[i].name.clone(),
933        });
934    }
935    for i in a_len..b_len {
936        changes.push(ScoreChange::PartAdded { part_index: i });
937    }
938
939    for pi in 0..a_len.min(b_len) {
940        let ap = &a.parts[pi];
941        let bp = &b.parts[pi];
942        for si in 0..ap.staves.len().min(bp.staves.len()) {
943            let a_staff = &ap.staves[si];
944            let b_staff = &bp.staves[si];
945            for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
946                let am = &a_staff.measures[mi];
947                let bm = &b_staff.measures[mi];
948                for vi in 0..4usize {
949                    let av = &am.voices[vi];
950                    let bv = &bm.voices[vi];
951                    for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
952                        if !note_content_eq(a_note, b_note) {
953                            changes.push(ScoreChange::NoteModified {
954                                part: pi,
955                                staff: si,
956                                measure: mi,
957                                voice: vi,
958                                note_index: ni,
959                                old: Box::new(a_note.clone()),
960                                new: Box::new(b_note.clone()),
961                            });
962                        }
963                    }
964                    for note in av.iter().skip(bv.len()) {
965                        changes.push(ScoreChange::NoteRemoved {
966                            part: pi,
967                            staff: si,
968                            measure: mi,
969                            voice: vi,
970                            note: Box::new(note.clone()),
971                        });
972                    }
973                    for ni in av.len()..bv.len() {
974                        changes.push(ScoreChange::NoteAdded {
975                            part: pi,
976                            staff: si,
977                            measure: mi,
978                            voice: vi,
979                            note_index: ni,
980                        });
981                    }
982                }
983                if am.time_sig != bm.time_sig {
984                    changes.push(ScoreChange::TimeSigChanged {
985                        part: pi,
986                        staff: si,
987                        measure: mi,
988                        old: am.time_sig.clone(),
989                        new: bm.time_sig.clone(),
990                    });
991                }
992                if am.tempo != bm.tempo {
993                    changes.push(ScoreChange::MeasureTempoChanged {
994                        part: pi,
995                        staff: si,
996                        measure: mi,
997                        old: am.tempo,
998                        new: bm.tempo,
999                    });
1000                }
1001                if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
1002                    changes.push(ScoreChange::BarlineChanged {
1003                        part: pi,
1004                        staff: si,
1005                        measure: mi,
1006                    });
1007                }
1008                if am.rehearsal != bm.rehearsal {
1009                    changes.push(ScoreChange::RehearsalMarkChanged {
1010                        part: pi,
1011                        staff: si,
1012                        measure: mi,
1013                        old: am.rehearsal.clone(),
1014                        new: bm.rehearsal.clone(),
1015                    });
1016                }
1017                if am.volta != bm.volta {
1018                    changes.push(ScoreChange::VoltaChanged {
1019                        part: pi,
1020                        staff: si,
1021                        measure: mi,
1022                    });
1023                }
1024            }
1025        }
1026    }
1027
1028    changes
1029}
1030
1031// ── ScorePatch ────────────────────────────────────────────────────────────────
1032
1033/// An individually applicable patch operation produced by [`score_patch`].
1034///
1035/// Unlike [`ScoreChange`], every variant carries enough data to apply the change to a
1036/// [`Score`] without needing the original score. Use [`apply_patch`] to apply a list.
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1038pub enum ScorePatch {
1039    SetMetadata {
1040        field: String,
1041        value: String,
1042    },
1043    SetTempo {
1044        value: u16,
1045    },
1046    SetKeySignature {
1047        part: usize,
1048        staff: usize,
1049        measure: usize,
1050        value: Option<KeySignature>,
1051    },
1052    SetTimeSignature {
1053        part: usize,
1054        staff: usize,
1055        measure: usize,
1056        value: Option<TimeSignature>,
1057    },
1058    SetBarlines {
1059        part: usize,
1060        staff: usize,
1061        measure: usize,
1062        left: Barline,
1063        right: Barline,
1064    },
1065    SetRehearsal {
1066        part: usize,
1067        staff: usize,
1068        measure: usize,
1069        value: Option<String>,
1070    },
1071    SetVolta {
1072        part: usize,
1073        staff: usize,
1074        measure: usize,
1075        value: Option<VoltaBracket>,
1076    },
1077    /// Insert `note` at `note_index` in the given voice (existing notes shift right).
1078    AddNote {
1079        part: usize,
1080        staff: usize,
1081        measure: usize,
1082        voice: usize,
1083        /// Position for insertion. `usize::MAX` is the legacy append sentinel.
1084        #[serde(default = "legacy_append_index")]
1085        note_index: usize,
1086        note: Box<Note>,
1087    },
1088    RemoveNote {
1089        part: usize,
1090        staff: usize,
1091        measure: usize,
1092        voice: usize,
1093        note_index: usize,
1094    },
1095    /// Replace the note at `note_index` with `note`.
1096    ReplaceNote {
1097        part: usize,
1098        staff: usize,
1099        measure: usize,
1100        voice: usize,
1101        note_index: usize,
1102        note: Box<Note>,
1103    },
1104    SetMeasureTempo {
1105        part: usize,
1106        staff: usize,
1107        measure: usize,
1108        value: Option<u16>,
1109    },
1110    /// Replace the complete score when a change cannot be represented safely by
1111    /// positional operations (for example, a part or measure was added).
1112    ReplaceScore {
1113        score: Box<Score>,
1114    },
1115}
1116
1117fn legacy_append_index() -> usize {
1118    usize::MAX
1119}
1120
1121/// Return whether positional patches would lose score data. The patch format deliberately
1122/// keeps the common editing operations small; fields without a dedicated operation use the
1123/// complete-score fallback so an interchange round-trip never silently drops notation.
1124fn patch_requires_replace(a: &Score, b: &Score) -> bool {
1125    if a.settings.time_signature != b.settings.time_signature
1126        || a.settings.key_signature != b.settings.key_signature
1127        || a.parts.len() != b.parts.len()
1128        || a.part_groups.len() != b.part_groups.len()
1129    {
1130        return true;
1131    }
1132    if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
1133        x.first_part != y.first_part
1134            || x.last_part != y.last_part
1135            || x.symbol != y.symbol
1136            || x.barlines_connect != y.barlines_connect
1137    }) {
1138        return true;
1139    }
1140    for (ap, bp) in a.parts.iter().zip(&b.parts) {
1141        if ap.name != bp.name
1142            || ap.short_name != bp.short_name
1143            || ap.midi_channel != bp.midi_channel
1144            || ap.midi_program != bp.midi_program
1145            || ap.staves.len() != bp.staves.len()
1146        {
1147            return true;
1148        }
1149        for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
1150            if as_.clef != bs.clef
1151                || as_.transpose_semitones != bs.transpose_semitones
1152                || as_.measures.len() != bs.measures.len()
1153            {
1154                return true;
1155            }
1156            for (am, bm) in as_.measures.iter().zip(&bs.measures) {
1157                if am.number != bm.number
1158                    || am.clef != bm.clef
1159                    || am.tempo_text != bm.tempo_text
1160                    || am.navigation != bm.navigation
1161                    || am.expression_text != bm.expression_text
1162                    || am.multi_rest_count != bm.multi_rest_count
1163                    || am.system_break != bm.system_break
1164                    || am.page_break != bm.page_break
1165                {
1166                    return true;
1167                }
1168            }
1169        }
1170    }
1171    false
1172}
1173
1174/// Compare two scores and return a list of [`ScorePatch`] operations.
1175///
1176/// Applying the patches to `a` via [`apply_patch`] produces a score structurally
1177/// equivalent to `b` (same parts, staves, measures, and note content).
1178pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
1179    let mut patches: Vec<ScorePatch> = Vec::new();
1180
1181    if patch_requires_replace(a, b) {
1182        return vec![ScorePatch::ReplaceScore {
1183            score: Box::new(b.clone()),
1184        }];
1185    }
1186
1187    macro_rules! meta {
1188        ($field:ident, $name:literal) => {
1189            if a.metadata.$field != b.metadata.$field {
1190                patches.push(ScorePatch::SetMetadata {
1191                    field: $name.to_string(),
1192                    value: b.metadata.$field.clone(),
1193                });
1194            }
1195        };
1196    }
1197    meta!(title, "title");
1198    meta!(composer, "composer");
1199    meta!(lyricist, "lyricist");
1200    meta!(copyright, "copyright");
1201    meta!(work_number, "work_number");
1202    meta!(movement_title, "movement_title");
1203
1204    if a.settings.tempo_bpm != b.settings.tempo_bpm {
1205        patches.push(ScorePatch::SetTempo {
1206            value: b.settings.tempo_bpm,
1207        });
1208    }
1209
1210    for pi in 0..a.parts.len().min(b.parts.len()) {
1211        let ap = &a.parts[pi];
1212        let bp = &b.parts[pi];
1213        for si in 0..ap.staves.len().min(bp.staves.len()) {
1214            let a_staff = &ap.staves[si];
1215            let b_staff = &bp.staves[si];
1216            for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
1217                let am = &a_staff.measures[mi];
1218                let bm = &b_staff.measures[mi];
1219
1220                if am.key_sig != bm.key_sig {
1221                    patches.push(ScorePatch::SetKeySignature {
1222                        part: pi,
1223                        staff: si,
1224                        measure: mi,
1225                        value: bm.key_sig.clone(),
1226                    });
1227                }
1228                if am.time_sig != bm.time_sig {
1229                    patches.push(ScorePatch::SetTimeSignature {
1230                        part: pi,
1231                        staff: si,
1232                        measure: mi,
1233                        value: bm.time_sig.clone(),
1234                    });
1235                }
1236                if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
1237                    patches.push(ScorePatch::SetBarlines {
1238                        part: pi,
1239                        staff: si,
1240                        measure: mi,
1241                        left: bm.barline_left.clone(),
1242                        right: bm.barline_right.clone(),
1243                    });
1244                }
1245                if am.rehearsal != bm.rehearsal {
1246                    patches.push(ScorePatch::SetRehearsal {
1247                        part: pi,
1248                        staff: si,
1249                        measure: mi,
1250                        value: bm.rehearsal.clone(),
1251                    });
1252                }
1253                if am.volta != bm.volta {
1254                    patches.push(ScorePatch::SetVolta {
1255                        part: pi,
1256                        staff: si,
1257                        measure: mi,
1258                        value: bm.volta.clone(),
1259                    });
1260                }
1261                if am.tempo != bm.tempo {
1262                    patches.push(ScorePatch::SetMeasureTempo {
1263                        part: pi,
1264                        staff: si,
1265                        measure: mi,
1266                        value: bm.tempo,
1267                    });
1268                }
1269
1270                for vi in 0..4usize {
1271                    let av = &am.voices[vi];
1272                    let bv = &bm.voices[vi];
1273                    for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
1274                        if !note_content_eq(a_note, b_note) {
1275                            patches.push(ScorePatch::ReplaceNote {
1276                                part: pi,
1277                                staff: si,
1278                                measure: mi,
1279                                voice: vi,
1280                                note_index: ni,
1281                                note: Box::new(b_note.clone()),
1282                            });
1283                        }
1284                    }
1285                    // Notes in `a` beyond `b` — remove in reverse order to preserve indices.
1286                    for ni in (bv.len()..av.len()).rev() {
1287                        patches.push(ScorePatch::RemoveNote {
1288                            part: pi,
1289                            staff: si,
1290                            measure: mi,
1291                            voice: vi,
1292                            note_index: ni,
1293                        });
1294                    }
1295                    // Notes in `b` beyond `a` — append.
1296                    for (offset, note) in bv.iter().skip(av.len()).enumerate() {
1297                        patches.push(ScorePatch::AddNote {
1298                            part: pi,
1299                            staff: si,
1300                            measure: mi,
1301                            voice: vi,
1302                            note_index: av.len() + offset,
1303                            note: Box::new(note.clone()),
1304                        });
1305                    }
1306                }
1307            }
1308        }
1309    }
1310
1311    patches
1312}
1313
1314/// Apply a list of [`ScorePatch`] operations to a cloned copy of `score`.
1315///
1316/// Returns `Err(Error::InvalidPatch)` if any patch references an out-of-bounds index.
1317/// The returned score is an independent clone — `score` is not modified.
1318pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
1319    let mut s = score.clone();
1320    for patch in patches {
1321        match patch {
1322            ScorePatch::ReplaceScore { score } => {
1323                s = (**score).clone();
1324            }
1325            ScorePatch::SetMetadata { field, value } => match field.as_str() {
1326                "title" => s.metadata.title = value.clone(),
1327                "composer" => s.metadata.composer = value.clone(),
1328                "lyricist" => s.metadata.lyricist = value.clone(),
1329                "copyright" => s.metadata.copyright = value.clone(),
1330                "work_number" => s.metadata.work_number = value.clone(),
1331                "movement_title" => s.metadata.movement_title = value.clone(),
1332                other => {
1333                    return Err(Error::InvalidPatch(format!(
1334                        "unknown metadata field: {other}"
1335                    )));
1336                }
1337            },
1338            ScorePatch::SetTempo { value } => {
1339                s.settings.tempo_bpm = *value;
1340            }
1341            ScorePatch::SetKeySignature {
1342                part,
1343                staff,
1344                measure,
1345                value,
1346            } => {
1347                s.parts
1348                    .get_mut(*part)
1349                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1350                    .staves
1351                    .get_mut(*staff)
1352                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1353                    .measures
1354                    .get_mut(*measure)
1355                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1356                    .key_sig = value.clone();
1357            }
1358            ScorePatch::SetTimeSignature {
1359                part,
1360                staff,
1361                measure,
1362                value,
1363            } => {
1364                s.parts
1365                    .get_mut(*part)
1366                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1367                    .staves
1368                    .get_mut(*staff)
1369                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1370                    .measures
1371                    .get_mut(*measure)
1372                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1373                    .time_sig = value.clone();
1374            }
1375            ScorePatch::SetBarlines {
1376                part,
1377                staff,
1378                measure,
1379                left,
1380                right,
1381            } => {
1382                let m = s
1383                    .parts
1384                    .get_mut(*part)
1385                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1386                    .staves
1387                    .get_mut(*staff)
1388                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1389                    .measures
1390                    .get_mut(*measure)
1391                    .ok_or_else(|| {
1392                        Error::InvalidPatch(format!("measure {measure} out of range"))
1393                    })?;
1394                m.barline_left = left.clone();
1395                m.barline_right = right.clone();
1396            }
1397            ScorePatch::SetRehearsal {
1398                part,
1399                staff,
1400                measure,
1401                value,
1402            } => {
1403                s.parts
1404                    .get_mut(*part)
1405                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1406                    .staves
1407                    .get_mut(*staff)
1408                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1409                    .measures
1410                    .get_mut(*measure)
1411                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1412                    .rehearsal = value.clone();
1413            }
1414            ScorePatch::SetVolta {
1415                part,
1416                staff,
1417                measure,
1418                value,
1419            } => {
1420                s.parts
1421                    .get_mut(*part)
1422                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1423                    .staves
1424                    .get_mut(*staff)
1425                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1426                    .measures
1427                    .get_mut(*measure)
1428                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1429                    .volta = value.clone();
1430            }
1431            ScorePatch::AddNote {
1432                part,
1433                staff,
1434                measure,
1435                voice,
1436                note_index,
1437                note,
1438            } => {
1439                let v = s
1440                    .parts
1441                    .get_mut(*part)
1442                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1443                    .staves
1444                    .get_mut(*staff)
1445                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1446                    .measures
1447                    .get_mut(*measure)
1448                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1449                    .voices
1450                    .get_mut(*voice)
1451                    .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
1452                let insert_at = if *note_index == usize::MAX {
1453                    v.len()
1454                } else {
1455                    *note_index
1456                };
1457                if insert_at > v.len() {
1458                    return Err(Error::InvalidPatch(format!(
1459                        "note_index {note_index} out of range"
1460                    )));
1461                }
1462                v.insert(insert_at, *note.clone());
1463            }
1464            ScorePatch::RemoveNote {
1465                part,
1466                staff,
1467                measure,
1468                voice,
1469                note_index,
1470            } => {
1471                let v = s
1472                    .parts
1473                    .get_mut(*part)
1474                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1475                    .staves
1476                    .get_mut(*staff)
1477                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1478                    .measures
1479                    .get_mut(*measure)
1480                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1481                    .voices
1482                    .get_mut(*voice)
1483                    .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
1484                if *note_index >= v.len() {
1485                    return Err(Error::InvalidPatch(format!(
1486                        "note_index {note_index} out of range"
1487                    )));
1488                }
1489                v.remove(*note_index);
1490            }
1491            ScorePatch::ReplaceNote {
1492                part,
1493                staff,
1494                measure,
1495                voice,
1496                note_index,
1497                note,
1498            } => {
1499                let v = s
1500                    .parts
1501                    .get_mut(*part)
1502                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1503                    .staves
1504                    .get_mut(*staff)
1505                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1506                    .measures
1507                    .get_mut(*measure)
1508                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1509                    .voices
1510                    .get_mut(*voice)
1511                    .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
1512                if *note_index >= v.len() {
1513                    return Err(Error::InvalidPatch(format!(
1514                        "note_index {note_index} out of range"
1515                    )));
1516                }
1517                v[*note_index] = *note.clone();
1518            }
1519            ScorePatch::SetMeasureTempo {
1520                part,
1521                staff,
1522                measure,
1523                value,
1524            } => {
1525                s.parts
1526                    .get_mut(*part)
1527                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
1528                    .staves
1529                    .get_mut(*staff)
1530                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
1531                    .measures
1532                    .get_mut(*measure)
1533                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
1534                    .tempo = *value;
1535            }
1536        }
1537    }
1538    Ok(s)
1539}
1540
1541/// Respell all pitches in the score to prefer flats or sharps.
1542///
1543/// Applies [`Pitch::respell`] to every note in every part, staff, measure, and voice.
1544pub fn respell_score(score: &mut Score, prefer_flat: bool) {
1545    for part in &mut score.parts {
1546        for staff in &mut part.staves {
1547            for measure in &mut staff.measures {
1548                for voice in &mut measure.voices {
1549                    for note in voice.iter_mut() {
1550                        for pitch in &mut note.pitches {
1551                            *pitch = pitch.respell(prefer_flat);
1552                        }
1553                    }
1554                }
1555            }
1556        }
1557    }
1558}
1559
1560/// Respell all pitches to match the score's key signature spelling convention.
1561///
1562/// Flat-key signatures (fifths < 0) use flat spellings; sharp-key and C major use sharps.
1563pub fn respell_score_to_key(score: &mut Score) {
1564    let prefer_flat = score.settings.key_signature.fifths < 0;
1565    respell_score(score, prefer_flat);
1566}
1567
1568/// Compute total playback duration in seconds.
1569///
1570/// Uses `measure_sequence` for correct repeat handling. Lighter than generating
1571/// full playback events — suitable for progress bars and UI display.
1572pub fn score_duration_secs(score: &Score) -> f64 {
1573    if score.settings.tempo_bpm == 0 {
1574        return 0.0;
1575    }
1576    let seq = measure_sequence(score);
1577    let mut total_secs = 0.0f64;
1578    let mut current_bpm = score.settings.tempo_bpm as f64;
1579    if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
1580        for &idx in &seq {
1581            if let Some(m) = staff.measures.get(idx) {
1582                if let Some(b) = m.tempo {
1583                    current_bpm = b as f64;
1584                }
1585                if current_bpm == 0.0 {
1586                    continue;
1587                }
1588                let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1589                total_secs += beats / current_bpm * 60.0;
1590            }
1591        }
1592    }
1593    total_secs
1594}
1595
1596/// Compute playback duration in seconds for a specific measure range (inclusive).
1597///
1598/// `region` is `(start_measure, end_measure)`, both 0-based. Measures outside the range
1599/// are excluded. Uses `measure_sequence` for correct repeat handling.
1600pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
1601    if score.settings.tempo_bpm == 0 {
1602        return 0.0;
1603    }
1604    let seq: Vec<usize> = measure_sequence(score)
1605        .into_iter()
1606        .filter(|&idx| idx >= region.0 && idx <= region.1)
1607        .collect();
1608    let mut total_secs = 0.0f64;
1609    let mut current_bpm = score.settings.tempo_bpm as f64;
1610    if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
1611        for &idx in &seq {
1612            if let Some(m) = staff.measures.get(idx) {
1613                if let Some(b) = m.tempo {
1614                    current_bpm = b as f64;
1615                }
1616                if current_bpm == 0.0 {
1617                    continue;
1618                }
1619                let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1620                total_secs += beats / current_bpm * 60.0;
1621            }
1622        }
1623    }
1624    total_secs
1625}
1626
1627/// Return the number of beats available in a voice before it is full.
1628///
1629/// Uses [`Note::beats`] which correctly handles tuplet scaling.
1630/// Returns `Ok(0.0)` when the voice is already full or over-full.
1631pub fn measure_beats_remaining(
1632    score: &Score,
1633    part_index: usize,
1634    staff_index: usize,
1635    measure_index: usize,
1636    voice_index: usize,
1637) -> Result<f64, Error> {
1638    let part = score
1639        .parts
1640        .get(part_index)
1641        .ok_or(Error::PartNotFound(part_index))?;
1642    let staff = part
1643        .staves
1644        .get(staff_index)
1645        .ok_or(Error::StaffNotFound(staff_index))?;
1646    let measure = staff
1647        .measures
1648        .get(measure_index)
1649        .ok_or(Error::MeasureNotFound(measure_index))?;
1650    let voice = measure
1651        .voices
1652        .get(voice_index)
1653        .ok_or(Error::VoiceOutOfRange(voice_index))?;
1654    let ts = measure
1655        .time_sig
1656        .as_ref()
1657        .unwrap_or(&score.settings.time_signature);
1658    let used: f64 = voice.iter().map(|n| n.beats()).sum();
1659    Ok((ts.total_beats() - used).max(0.0))
1660}
1661
1662/// Suggest whether the stem should point up for the given pitches and clef.
1663///
1664/// Conventional rule: if the average MIDI pitch of the chord is below the staff
1665/// middle line, the stem points up; at or above, it points down.
1666/// For empty pitch lists (rests), returns `true` by convention.
1667pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
1668    if pitches.is_empty() {
1669        return true;
1670    }
1671    let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
1672    avg < clef.middle_line_midi() as f64
1673}
1674
1675fn beam_beat_size(ts: &TimeSignature) -> f64 {
1676    if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
1677        3.0 * 4.0 / ts.denominator as f64
1678    } else {
1679        4.0 / ts.denominator as f64
1680    }
1681}
1682
1683/// Compute recommended [`BeamState`] values for a voice's notes.
1684///
1685/// Groups beamable notes (eighth or shorter, non-rest) within beat boundaries.
1686/// Returns a `Vec` the same length as `notes`.
1687pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
1688    let beat_size = beam_beat_size(time_sig);
1689    let n = notes.len();
1690    let mut result = vec![BeamState::None; n];
1691
1692    let is_beamable = |note: &Note| -> bool {
1693        !note.is_rest
1694            && matches!(
1695                note.duration,
1696                Duration::Eighth
1697                    | Duration::Sixteenth
1698                    | Duration::ThirtySecond
1699                    | Duration::SixtyFourth
1700            )
1701    };
1702
1703    // Compute beat start positions
1704    let mut starts = Vec::with_capacity(n);
1705    let mut pos = 0.0f64;
1706    for note in notes {
1707        starts.push(pos);
1708        pos += note.beats();
1709    }
1710
1711    // Assign beam group ids based on beat boundary
1712    let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
1713
1714    let mut i = 0;
1715    while i < n {
1716        if !is_beamable(&notes[i]) {
1717            i += 1;
1718            continue;
1719        }
1720        let g = group_id(i);
1721        // Find the run of beamable notes in the same beat group
1722        let mut j = i;
1723        while j < n && is_beamable(&notes[j]) && group_id(j) == g {
1724            j += 1;
1725        }
1726        let run = j - i;
1727        if run == 1 {
1728            result[i] = BeamState::None;
1729        } else {
1730            result[i] = BeamState::Begin;
1731            result[i + 1..j - 1].fill(BeamState::Continue);
1732            result[j - 1] = BeamState::End;
1733        }
1734        i = j;
1735    }
1736    result
1737}
1738
1739fn note_content_eq(a: &Note, b: &Note) -> bool {
1740    a.is_rest == b.is_rest
1741        && a.pitches == b.pitches
1742        && a.duration == b.duration
1743        && a.dot_count == b.dot_count
1744        && a.tie_start == b.tie_start
1745        && a.tie_end == b.tie_end
1746        && a.beam == b.beam
1747        && a.articulations == b.articulations
1748        && a.dynamic == b.dynamic
1749        && a.stem_up == b.stem_up
1750        && a.hairpin_start == b.hairpin_start
1751        && a.hairpin_end == b.hairpin_end
1752        && a.tuplet == b.tuplet
1753        && a.chord_symbol == b.chord_symbol
1754        && a.is_grace == b.is_grace
1755        && a.grace_slash == b.grace_slash
1756        && a.ottava_start == b.ottava_start
1757        && a.ottava_end == b.ottava_end
1758        && a.lyric == b.lyric
1759        && a.pedal_start == b.pedal_start
1760        && a.pedal_end == b.pedal_end
1761        && a.slur_start == b.slur_start
1762        && a.slur_end == b.slur_end
1763        && a.arpeggiate == b.arpeggiate
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768    use super::*;
1769    use crate::model::pitch::Step;
1770
1771    #[test]
1772    fn default_score_has_one_part_four_measures() {
1773        let score = Score::default();
1774        assert_eq!(score.parts.len(), 1);
1775        assert_eq!(score.parts[0].staves.len(), 1);
1776        assert_eq!(score.parts[0].staves[0].measures.len(), 4);
1777    }
1778
1779    #[test]
1780    fn new_score_measure_count() {
1781        let score = Score::new("Test", 120, 4, 4, 0, 8);
1782        assert_eq!(score.measure_count(), 8);
1783    }
1784
1785    #[test]
1786    fn note_beats_quarter() {
1787        let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1788        assert!((note.beats() - 1.0).abs() < 1e-9);
1789    }
1790
1791    #[test]
1792    fn note_beats_dotted_quarter() {
1793        let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1794        note.dot_count = 1;
1795        assert!((note.beats() - 1.5).abs() < 1e-9);
1796    }
1797
1798    #[test]
1799    fn grace_note_beats_zero() {
1800        let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1801        note.is_grace = true;
1802        assert_eq!(note.beats(), 0.0);
1803    }
1804
1805    #[test]
1806    fn measure_empty_4_4_fills_four_beats() {
1807        let m = Measure::empty(4, 4);
1808        let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1809        assert!((total - 4.0).abs() < 1e-9);
1810    }
1811
1812    #[test]
1813    fn measure_empty_3_4_fills_three_beats() {
1814        let m = Measure::empty(3, 4);
1815        let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
1816        assert!((total - 3.0).abs() < 1e-9);
1817    }
1818
1819    #[test]
1820    fn whole_filling_beats() {
1821        assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
1822        assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
1823        assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
1824    }
1825
1826    // ── ScoreStats ────────────────────────────────────────────────────────────
1827
1828    #[test]
1829    fn statistics_default_score_all_rests() {
1830        let score = Score::default();
1831        let s = score.statistics();
1832        assert_eq!(s.part_count, 1);
1833        assert_eq!(s.measure_count, 4);
1834        assert_eq!(s.note_count, 0);
1835        assert!(s.rest_count > 0);
1836    }
1837
1838    #[test]
1839    fn statistics_duration_estimate() {
1840        // 4/4, 120 BPM, 1 measure → 4 beats → 2.0 s
1841        let score = Score::new("T", 120, 4, 4, 0, 1);
1842        let s = score.statistics();
1843        assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
1844    }
1845
1846    #[test]
1847    fn score_duration_secs_matches_statistics() {
1848        use super::score_duration_secs;
1849        let score = Score::new("T", 120, 4, 4, 0, 4);
1850        let secs = score_duration_secs(&score);
1851        // 4/4, 120 BPM, 4 measures → 16 beats → 8.0 s
1852        assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
1853    }
1854
1855    #[test]
1856    fn score_duration_secs_zero_bpm_returns_zero() {
1857        use super::score_duration_secs;
1858        let mut score = Score::new("T", 120, 4, 4, 0, 1);
1859        score.settings.tempo_bpm = 0;
1860        assert_eq!(score_duration_secs(&score), 0.0);
1861    }
1862
1863    #[test]
1864    fn score_duration_secs_per_measure_tempo() {
1865        use super::score_duration_secs;
1866        // 2 measures: measure 0 at 120 BPM (2.0 s), measure 1 at 60 BPM (4.0 s)
1867        let mut score = Score::new("T", 120, 4, 4, 0, 2);
1868        score.parts[0].staves[0].measures[1].tempo = Some(60);
1869        let secs = score_duration_secs(&score);
1870        assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
1871    }
1872
1873    // ── extract_part ──────────────────────────────────────────────────────────
1874
1875    #[test]
1876    fn extract_part_returns_single_part_score() {
1877        let mut score = Score::default();
1878        let mut p2 = Part::new("Violin", "Vln.");
1879        p2.staves.push(Staff::new(Clef::Treble));
1880        score.parts.push(p2);
1881        let ex = score.extract_part(0).unwrap();
1882        assert_eq!(ex.parts.len(), 1);
1883        assert_ne!(ex.id, score.id);
1884        assert_eq!(ex.metadata.title, score.metadata.title);
1885    }
1886
1887    #[test]
1888    fn extract_part_out_of_range_is_none() {
1889        let score = Score::default();
1890        assert!(score.extract_part(99).is_none());
1891    }
1892
1893    // ── transpose ─────────────────────────────────────────────────────────────
1894
1895    #[test]
1896    fn transpose_zero_is_clone() {
1897        let score = Score::new("T", 120, 4, 4, 0, 1);
1898        let t = transpose(&score, 0);
1899        assert_eq!(t.settings.key_signature.fifths, 0);
1900    }
1901
1902    #[test]
1903    fn transpose_c_major_up_2_to_d_major() {
1904        let score = Score::new("T", 120, 4, 4, 0, 1);
1905        assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
1906    }
1907
1908    #[test]
1909    fn transpose_d_major_up_5_to_g_major() {
1910        let score = Score::new("T", 120, 4, 4, 2, 1);
1911        assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
1912    }
1913
1914    #[test]
1915    fn transpose_c4_up_1_to_csharp4() {
1916        let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
1917        assert_eq!(p.to_midi(), 61);
1918        assert_eq!(p.step, Step::C);
1919        assert_eq!(p.alter, 1);
1920    }
1921
1922    #[test]
1923    fn transpose_c4_down_1_to_b3() {
1924        let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
1925        assert_eq!(p.to_midi(), 59);
1926        assert_eq!(p.step, Step::B);
1927        assert_eq!(p.alter, 0);
1928    }
1929
1930    #[test]
1931    fn transpose_up_octave_keeps_step() {
1932        let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
1933        assert_eq!(p.to_midi(), 81);
1934        assert_eq!(p.step, Step::A);
1935        assert_eq!(p.octave, 5);
1936    }
1937
1938    #[test]
1939    fn statistics_with_repeat_doubles_duration() {
1940        // 4/4, 120 BPM, 2 measures with RepeatStart+RepeatEnd → plays twice → 4 measures worth
1941        let mut score = Score::new("T", 120, 4, 4, 0, 2);
1942        score.parts[0].staves[0].measures[0].barline_left =
1943            crate::model::notation::Barline::RepeatStart;
1944        score.parts[0].staves[0].measures[1].barline_right =
1945            crate::model::notation::Barline::RepeatEnd;
1946        let s = score.statistics();
1947        // 4 beats × 4 measures (2 physical × 2 passes) ÷ 120 BPM × 60 = 8.0 s
1948        assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
1949    }
1950
1951    #[test]
1952    fn transpose_octave_boundary_b4_to_c5() {
1953        // B4 (midi=71) + 1 semitone = C5 (midi=72)
1954        let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
1955        assert_eq!(p.to_midi(), 72);
1956        assert_eq!(p.step, Step::C);
1957        assert_eq!(p.octave, 5);
1958    }
1959
1960    #[test]
1961    fn transpose_clamp_at_midi_127() {
1962        // G9 (midi=127) + 3 semitones → clamped to 127
1963        let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
1964        assert_eq!(p.to_midi(), 127);
1965    }
1966
1967    // ── merge ─────────────────────────────────────────────────────────────────
1968
1969    #[test]
1970    fn merge_combines_parts() {
1971        let mut a = Score::new("A", 120, 4, 4, 0, 2);
1972        let b = Score::new("B", 120, 4, 4, 0, 2);
1973        // Add a second part to score a
1974        let mut p2 = Part::new("Violin", "Vln.");
1975        p2.staves.push(Staff::new(Clef::Treble));
1976        for i in 0..2usize {
1977            let mut m = Measure::empty(4, 4);
1978            m.number = i as u32 + 1;
1979            p2.staves[0].measures.push(m);
1980        }
1981        a.parts.push(p2);
1982        let merged = a.merge(&b);
1983        // a has 2 parts, b has 1 part → merged has 3 parts
1984        assert_eq!(merged.parts.len(), 3);
1985    }
1986
1987    #[test]
1988    fn merge_pads_shorter_score() {
1989        let a = Score::new("A", 120, 4, 4, 0, 4);
1990        let b = Score::new("B", 120, 4, 4, 0, 2);
1991        let merged = a.merge(&b);
1992        // Both parts should have 4 measures
1993        assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
1994        assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
1995    }
1996
1997    #[test]
1998    fn merge_uses_self_metadata() {
1999        let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
2000        a.metadata.composer = "Composer A".to_string();
2001        let b = Score::new("Title B", 120, 4, 4, 0, 2);
2002        let merged = a.merge(&b);
2003        assert_eq!(merged.metadata.title, "Title A");
2004        assert_eq!(merged.metadata.composer, "Composer A");
2005    }
2006
2007    #[test]
2008    fn merge_new_id_differs_from_both() {
2009        let a = Score::new("A", 120, 4, 4, 0, 2);
2010        let b = Score::new("B", 120, 4, 4, 0, 2);
2011        let merged = a.merge(&b);
2012        assert_ne!(merged.id, a.id);
2013        assert_ne!(merged.id, b.id);
2014    }
2015
2016    // ── Staff.transpose_semitones ─────────────────────────────────────────────
2017
2018    #[test]
2019    fn staff_default_transpose_is_zero() {
2020        let s = Staff::new(Clef::Treble);
2021        assert_eq!(s.transpose_semitones, 0);
2022    }
2023
2024    // ── schema_version ────────────────────────────────────────────────────────
2025
2026    #[test]
2027    fn score_default_has_schema_version_1() {
2028        let score = Score::default();
2029        assert_eq!(score.schema_version, 1);
2030    }
2031
2032    #[test]
2033    fn score_new_has_schema_version_1() {
2034        let score = Score::new("T", 120, 4, 4, 0, 4);
2035        assert_eq!(score.schema_version, 1);
2036    }
2037
2038    #[test]
2039    fn score_without_schema_version_deserializes_to_zero() {
2040        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":[]}"#;
2041        let score: Score = serde_json::from_str(json).unwrap();
2042        assert_eq!(score.schema_version, 0);
2043    }
2044
2045    // ── ScoreTemplate ─────────────────────────────────────────────────────────
2046
2047    #[test]
2048    fn score_template_solo_has_one_part_treble() {
2049        let score = Score::template(ScoreTemplate::Solo);
2050        assert_eq!(score.parts.len(), 1);
2051        assert_eq!(score.parts[0].staves.len(), 1);
2052        assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
2053        assert_eq!(score.parts[0].midi_program, 0);
2054    }
2055
2056    #[test]
2057    fn score_template_piano_has_two_staves() {
2058        let score = Score::template(ScoreTemplate::Piano);
2059        assert_eq!(score.parts.len(), 1);
2060        assert_eq!(score.parts[0].staves.len(), 2);
2061        assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
2062        assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
2063    }
2064
2065    #[test]
2066    fn score_template_string_quartet_has_four_parts() {
2067        let score = Score::template(ScoreTemplate::StringQuartet);
2068        assert_eq!(score.parts.len(), 4);
2069        assert_eq!(score.parts[2].staves[0].clef, Clef::Alto); // Viola
2070        assert_eq!(score.parts[3].staves[0].clef, Clef::Bass); // Cello
2071        assert_eq!(score.parts[0].midi_program, 40);
2072        assert_eq!(score.parts[3].midi_program, 42);
2073    }
2074
2075    #[test]
2076    fn score_template_string_orchestra_has_five_parts() {
2077        let score = Score::template(ScoreTemplate::StringOrchestra);
2078        assert_eq!(score.parts.len(), 5);
2079        assert_eq!(score.parts[4].midi_program, 43); // Contrabass
2080    }
2081
2082    #[test]
2083    fn score_template_brass_quintet_has_five_parts() {
2084        let score = Score::template(ScoreTemplate::BrassQuintet);
2085        assert_eq!(score.parts.len(), 5);
2086        assert_eq!(score.parts[2].midi_program, 60); // French Horn
2087    }
2088
2089    #[test]
2090    fn score_template_default_measures_are_four() {
2091        let score = Score::template(ScoreTemplate::StringQuartet);
2092        for part in &score.parts {
2093            for staff in &part.staves {
2094                assert_eq!(staff.measures.len(), 4);
2095            }
2096        }
2097    }
2098
2099    // ── system_break / page_break ─────────────────────────────────────────────
2100
2101    #[test]
2102    fn measure_empty_has_no_breaks() {
2103        let m = Measure::empty(4, 4);
2104        assert!(!m.system_break);
2105        assert!(!m.page_break);
2106    }
2107
2108    #[test]
2109    fn system_break_survives_json_roundtrip() {
2110        let mut m = Measure::empty(4, 4);
2111        m.system_break = true;
2112        let json = serde_json::to_string(&m).unwrap();
2113        let m2: Measure = serde_json::from_str(&json).unwrap();
2114        assert!(m2.system_break);
2115        assert!(!m2.page_break);
2116    }
2117
2118    // ── diff ──────────────────────────────────────────────────────────────────
2119
2120    #[test]
2121    fn diff_identical_scores_is_empty() {
2122        let s = Score::new("T", 120, 4, 4, 0, 2);
2123        assert!(diff(&s, &s).is_empty());
2124    }
2125
2126    #[test]
2127    fn score_patch_covers_measure_semantics_and_note_insert_index() {
2128        let a = Score::new("T", 120, 4, 4, 0, 1);
2129        let mut b = a.clone();
2130        let measure = &mut b.parts[0].staves[0].measures[0];
2131        measure.key_sig = Some(KeySignature {
2132            fifths: -2,
2133            mode: "major".to_string(),
2134        });
2135        measure.time_sig = Some(TimeSignature {
2136            numerator: 3,
2137            denominator: 4,
2138        });
2139        measure.barline_left = Barline::RepeatStart;
2140        measure.barline_right = Barline::RepeatEnd;
2141        measure.rehearsal = Some("A".to_string());
2142        measure.volta = Some(VoltaBracket {
2143            number: 1,
2144            kind: "begin_end".to_string(),
2145        });
2146        measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
2147        let expected = b.parts[0].staves[0].measures[0].clone();
2148
2149        let patches = score_patch(&a, &b);
2150        assert!(
2151            patches
2152                .iter()
2153                .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
2154        );
2155        assert!(
2156            patches
2157                .iter()
2158                .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
2159        );
2160        assert!(
2161            patches
2162                .iter()
2163                .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
2164        );
2165        assert!(
2166            patches
2167                .iter()
2168                .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
2169        );
2170        let result = apply_patch(&a, &patches).expect("patch application failed");
2171        let result_measure = &result.parts[0].staves[0].measures[0];
2172        assert_eq!(result_measure.key_sig, expected.key_sig);
2173        assert_eq!(result_measure.time_sig, expected.time_sig);
2174        assert_eq!(result_measure.barline_left, expected.barline_left);
2175        assert_eq!(result_measure.barline_right, expected.barline_right);
2176        assert_eq!(result_measure.rehearsal, expected.rehearsal);
2177        assert_eq!(result_measure.volta, expected.volta);
2178        assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
2179    }
2180
2181    #[test]
2182    fn score_patch_replaces_when_structure_or_uncovered_fields_change() {
2183        let a = Score::new("T", 120, 4, 4, 0, 1);
2184        let mut b = a.clone();
2185        b.parts[0].name = "Piano".to_string();
2186        b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
2187        let patches = score_patch(&a, &b);
2188        assert!(matches!(
2189            patches.as_slice(),
2190            [ScorePatch::ReplaceScore { .. }]
2191        ));
2192        let result = apply_patch(&a, &patches).expect("replacement failed");
2193        assert_eq!(result.parts[0].name, "Piano");
2194        assert_eq!(
2195            result.parts[0].staves[0].measures[0].expression_text,
2196            Some("dolce".to_string())
2197        );
2198    }
2199
2200    #[test]
2201    fn diff_detects_tempo_change() {
2202        let a = Score::new("T", 120, 4, 4, 0, 1);
2203        let mut b = a.clone();
2204        b.settings.tempo_bpm = 90;
2205        let changes = diff(&a, &b);
2206        assert_eq!(changes.len(), 1);
2207        assert!(matches!(
2208            changes[0],
2209            ScoreChange::TempoChanged { old: 120, new: 90 }
2210        ));
2211    }
2212
2213    #[test]
2214    fn diff_detects_title_change() {
2215        let a = Score::new("Old Title", 120, 4, 4, 0, 1);
2216        let mut b = a.clone();
2217        b.metadata.title = "New Title".to_string();
2218        let changes = diff(&a, &b);
2219        assert!(
2220            changes.iter().any(
2221                |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
2222            )
2223        );
2224    }
2225
2226    #[test]
2227    fn diff_detects_note_modification() {
2228        let mut a = Score::new("T", 120, 4, 4, 0, 1);
2229        a.parts[0].staves[0].measures[0].voices[0] =
2230            vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
2231        let mut b = a.clone();
2232        b.parts[0].staves[0].measures[0].voices[0][0] =
2233            Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
2234        let changes = diff(&a, &b);
2235        assert!(
2236            changes
2237                .iter()
2238                .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
2239        );
2240    }
2241
2242    #[test]
2243    fn diff_detects_part_added() {
2244        let a = Score::new("T", 120, 4, 4, 0, 1);
2245        let mut b = a.clone();
2246        let mut p = Part::new("Violin", "Vln.");
2247        p.staves.push(Staff::new(Clef::Treble));
2248        b.parts.push(p);
2249        let changes = diff(&a, &b);
2250        assert!(
2251            changes
2252                .iter()
2253                .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
2254        );
2255    }
2256
2257    #[test]
2258    fn diff_detects_measure_tempo_change() {
2259        let a = Score::new("T", 120, 4, 4, 0, 2);
2260        let mut b = a.clone();
2261        b.parts[0].staves[0].measures[1].tempo = Some(60);
2262        let changes = diff(&a, &b);
2263        assert!(changes.iter().any(|c| matches!(
2264            c,
2265            ScoreChange::MeasureTempoChanged {
2266                measure: 1,
2267                old: None,
2268                new: Some(60),
2269                ..
2270            }
2271        )));
2272    }
2273
2274    #[test]
2275    fn diff_detects_barline_change() {
2276        use crate::model::notation::Barline;
2277        let a = Score::new("T", 120, 4, 4, 0, 2);
2278        let mut b = a.clone();
2279        b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
2280        let changes = diff(&a, &b);
2281        assert!(
2282            changes
2283                .iter()
2284                .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
2285        );
2286    }
2287
2288    #[test]
2289    fn diff_detects_rehearsal_change() {
2290        let a = Score::new("T", 120, 4, 4, 0, 2);
2291        let mut b = a.clone();
2292        b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
2293        let changes = diff(&a, &b);
2294        assert!(
2295            changes
2296                .iter()
2297                .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
2298        );
2299    }
2300
2301    #[test]
2302    fn diff_detects_volta_change() {
2303        use super::VoltaBracket;
2304        let a = Score::new("T", 120, 4, 4, 0, 2);
2305        let mut b = a.clone();
2306        b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
2307            number: 1,
2308            kind: "begin_end".into(),
2309        });
2310        let changes = diff(&a, &b);
2311        assert!(
2312            changes
2313                .iter()
2314                .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
2315        );
2316    }
2317
2318    #[test]
2319    fn diff_detects_key_signature_change() {
2320        let a = Score::new("T", 120, 4, 4, 0, 1);
2321        let mut b = a.clone();
2322        b.settings.key_signature.fifths = 2; // C major → D major
2323        let changes = diff(&a, &b);
2324        assert!(
2325            changes
2326                .iter()
2327                .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
2328        );
2329    }
2330
2331    #[test]
2332    fn diff_same_key_signature_no_change() {
2333        let a = Score::new("T", 120, 4, 4, 2, 1);
2334        let changes = diff(&a, &a);
2335        assert!(changes.is_empty());
2336    }
2337
2338    #[test]
2339    fn score_duration_secs_region_partial() {
2340        use super::score_duration_secs_region;
2341        // 4/4, 120 BPM, 4 measures → each measure = 2.0 s; region [1,2] = 4.0 s
2342        let score = Score::new("T", 120, 4, 4, 0, 4);
2343        let secs = score_duration_secs_region(&score, (1, 2));
2344        assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
2345    }
2346
2347    #[test]
2348    fn score_duration_secs_region_single_measure() {
2349        use super::score_duration_secs_region;
2350        // 4/4, 120 BPM → 1 measure = 2.0 s
2351        let score = Score::new("T", 120, 4, 4, 0, 4);
2352        let secs = score_duration_secs_region(&score, (0, 0));
2353        assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
2354    }
2355
2356    // ── measure_beats_remaining ───────────────────────────────────────────────
2357
2358    #[test]
2359    fn measure_beats_remaining_empty_voice_returns_full() {
2360        use super::measure_beats_remaining;
2361        let mut score = Score::new("T", 120, 4, 4, 0, 1);
2362        score.parts[0].staves[0].measures[0].voices[0].clear();
2363        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
2364        assert!(
2365            (rem - 4.0).abs() < 1e-9,
2366            "expected 4.0 remaining, got {rem}"
2367        );
2368    }
2369
2370    #[test]
2371    fn measure_beats_remaining_half_full_returns_half() {
2372        use super::measure_beats_remaining;
2373        use crate::model::pitch::Step;
2374        let mut score = Score::new("T", 120, 4, 4, 0, 1);
2375        score.parts[0].staves[0].measures[0].voices[0] = vec![
2376            Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
2377            Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
2378        ];
2379        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
2380        assert!(
2381            (rem - 2.0).abs() < 1e-9,
2382            "expected 2.0 remaining, got {rem}"
2383        );
2384    }
2385
2386    #[test]
2387    fn measure_beats_remaining_full_voice_returns_zero() {
2388        use super::measure_beats_remaining;
2389        use crate::model::pitch::Step;
2390        let mut score = Score::new("T", 120, 4, 4, 0, 1);
2391        score.parts[0].staves[0].measures[0].voices[0] =
2392            vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
2393        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
2394        assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
2395    }
2396
2397    #[test]
2398    fn measure_beats_remaining_tuplet_accounting() {
2399        use super::measure_beats_remaining;
2400        use crate::model::notation::TupletInfo;
2401        use crate::model::pitch::Step;
2402        // 3 quarter-note triplets each take 2/3 of a beat → total 2.0 beats used → 2.0 remaining
2403        let mut score = Score::new("T", 120, 4, 4, 0, 1);
2404        let tuplet = TupletInfo {
2405            actual_notes: 3,
2406            normal_notes: 2,
2407        };
2408        let mk = |step| {
2409            let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
2410            n.tuplet = Some(tuplet.clone());
2411            n
2412        };
2413        score.parts[0].staves[0].measures[0].voices[0] =
2414            vec![mk(Step::C), mk(Step::D), mk(Step::E)];
2415        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
2416        assert!(
2417            (rem - 2.0).abs() < 1e-9,
2418            "expected 2.0 remaining (triplets used 2.0), got {rem}"
2419        );
2420    }
2421
2422    #[test]
2423    fn measure_beats_remaining_out_of_range_returns_err() {
2424        use super::measure_beats_remaining;
2425        let score = Score::new("T", 120, 4, 4, 0, 1);
2426        assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
2427        assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
2428        assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
2429        assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
2430    }
2431
2432    #[test]
2433    fn note_content_eq_ignores_id() {
2434        use crate::model::pitch::Step;
2435        let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
2436        let mut b = a.clone();
2437        b.id = "different-id".to_string();
2438        assert!(note_content_eq(&a, &b));
2439        // Actual pitch change should differ
2440        b.pitches[0] = Pitch::new(Step::D, 4);
2441        assert!(!note_content_eq(&a, &b));
2442        // stem_up difference
2443        let mut c = a.clone();
2444        a.stem_up = Some(true);
2445        c.stem_up = Some(false);
2446        assert!(!note_content_eq(&a, &c));
2447    }
2448
2449    #[test]
2450    fn suggested_stem_up_below_middle() {
2451        use crate::model::notation::Clef;
2452        // C4 = MIDI 60, Treble middle = B4 = 71 → stem up
2453        let pitches = vec![Pitch::new(Step::C, 4)];
2454        assert!(suggested_stem_up(&pitches, &Clef::Treble));
2455    }
2456
2457    #[test]
2458    fn suggested_stem_up_above_middle() {
2459        use crate::model::notation::Clef;
2460        // G5 = MIDI 79, Treble middle = 71 → stem down
2461        let pitches = vec![Pitch::new(Step::G, 5)];
2462        assert!(!suggested_stem_up(&pitches, &Clef::Treble));
2463    }
2464
2465    #[test]
2466    fn suggested_stem_up_at_middle_line() {
2467        use crate::model::notation::Clef;
2468        // B4 = MIDI 71, Treble middle = 71 → stem down (avg >= middle)
2469        let pitches = vec![Pitch::new(Step::B, 4)];
2470        assert!(!suggested_stem_up(&pitches, &Clef::Treble));
2471    }
2472
2473    #[test]
2474    fn suggested_stem_up_chord() {
2475        use crate::model::notation::Clef;
2476        // [C4=60, G4=67] avg=63.5 < 71 → stem up
2477        let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
2478        assert!(suggested_stem_up(&pitches, &Clef::Treble));
2479    }
2480
2481    #[test]
2482    fn suggested_stem_up_bass_clef() {
2483        use crate::model::notation::Clef;
2484        // D3=50 is exactly at Bass middle line → stem down
2485        let pitches = vec![Pitch::new(Step::D, 3)];
2486        assert!(!suggested_stem_up(&pitches, &Clef::Bass));
2487        // C3=48 < 50 → stem up
2488        let pitches2 = vec![Pitch::new(Step::C, 3)];
2489        assert!(suggested_stem_up(&pitches2, &Clef::Bass));
2490    }
2491
2492    #[test]
2493    fn suggested_stem_up_empty_pitches() {
2494        use crate::model::notation::Clef;
2495        assert!(suggested_stem_up(&[], &Clef::Treble));
2496    }
2497
2498    fn eighth(pitch: Pitch) -> Note {
2499        Note::new(pitch, Duration::Eighth)
2500    }
2501    fn quarter(pitch: Pitch) -> Note {
2502        Note::new(pitch, Duration::Quarter)
2503    }
2504    fn rest_eighth() -> Note {
2505        Note::rest(Duration::Eighth)
2506    }
2507
2508    #[test]
2509    fn compute_beams_4_4_four_eighths() {
2510        use crate::model::notation::{Clef, TimeSignature};
2511        let _ = Clef::Treble; // suppress unused import warning
2512        let ts = TimeSignature {
2513            numerator: 4,
2514            denominator: 4,
2515        };
2516        let c4 = Pitch::new(Step::C, 4);
2517        let notes = vec![
2518            eighth(c4.clone()),
2519            eighth(c4.clone()),
2520            eighth(c4.clone()),
2521            eighth(c4.clone()),
2522        ];
2523        let beams = compute_beams(&notes, &ts);
2524        // 4 eighths in 4/4: beat size=1.0, two groups of 2 each
2525        assert_eq!(beams[0], BeamState::Begin);
2526        assert_eq!(beams[1], BeamState::End);
2527        assert_eq!(beams[2], BeamState::Begin);
2528        assert_eq!(beams[3], BeamState::End);
2529    }
2530
2531    #[test]
2532    fn compute_beams_4_4_all_eighth_one_group() {
2533        use crate::model::notation::TimeSignature;
2534        let ts = TimeSignature {
2535            numerator: 4,
2536            denominator: 4,
2537        };
2538        let c4 = Pitch::new(Step::C, 4);
2539        // 2 eighths in a beat → group of 2
2540        let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
2541        let beams = compute_beams(&notes, &ts);
2542        assert_eq!(beams[0], BeamState::Begin);
2543        assert_eq!(beams[1], BeamState::End);
2544    }
2545
2546    #[test]
2547    fn compute_beams_quarter_not_beamed() {
2548        use crate::model::notation::TimeSignature;
2549        let ts = TimeSignature {
2550            numerator: 4,
2551            denominator: 4,
2552        };
2553        let c4 = Pitch::new(Step::C, 4);
2554        let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
2555        let beams = compute_beams(&notes, &ts);
2556        assert_eq!(beams[0], BeamState::None);
2557        assert_eq!(beams[1], BeamState::None);
2558    }
2559
2560    #[test]
2561    fn compute_beams_rest_breaks_beam() {
2562        use crate::model::notation::TimeSignature;
2563        let ts = TimeSignature {
2564            numerator: 4,
2565            denominator: 4,
2566        };
2567        let c4 = Pitch::new(Step::C, 4);
2568        let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
2569        let beams = compute_beams(&notes, &ts);
2570        // rest breaks beam group
2571        assert_eq!(beams[0], BeamState::None);
2572        assert_eq!(beams[1], BeamState::None);
2573        assert_eq!(beams[2], BeamState::None);
2574    }
2575
2576    #[test]
2577    fn compute_beams_6_8_compound() {
2578        use crate::model::notation::TimeSignature;
2579        let ts = TimeSignature {
2580            numerator: 6,
2581            denominator: 8,
2582        };
2583        let c4 = Pitch::new(Step::C, 4);
2584        // 6 eighths in 6/8 compound → two groups of 3 (beam size=1.5 beats)
2585        let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
2586        let beams = compute_beams(&notes, &ts);
2587        assert_eq!(beams[0], BeamState::Begin);
2588        assert_eq!(beams[1], BeamState::Continue);
2589        assert_eq!(beams[2], BeamState::End);
2590        assert_eq!(beams[3], BeamState::Begin);
2591        assert_eq!(beams[4], BeamState::Continue);
2592        assert_eq!(beams[5], BeamState::End);
2593    }
2594
2595    #[test]
2596    fn compute_beams_single_eighth() {
2597        use crate::model::notation::TimeSignature;
2598        let ts = TimeSignature {
2599            numerator: 4,
2600            denominator: 4,
2601        };
2602        let c4 = Pitch::new(Step::C, 4);
2603        let notes = vec![eighth(c4.clone())];
2604        let beams = compute_beams(&notes, &ts);
2605        assert_eq!(beams[0], BeamState::None);
2606    }
2607}