Skip to main content

acorde_core/model/
score.rs

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