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