Skip to main content

acorde_core/model/
score.rs

1use super::{
2    duration::Duration,
3    notation::{
4        Articulation, Barline, BeamState, ChordDefinition, ChordSymbol, Clef, CrossStaff, Dynamic,
5        FiguredBassFigure, GuitarTechnique, HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind,
6        StyledText, TabPosition, TablatureConfig, TimeSignature, TupletInfo, VerseLyric,
7    },
8    pitch::Pitch,
9};
10use crate::Error;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ScoreMetadata {
16    pub title: String,
17    pub composer: String,
18    pub lyricist: String,
19    pub copyright: String,
20    pub work_number: String,
21    pub movement_title: String,
22}
23
24impl Default for ScoreMetadata {
25    fn default() -> Self {
26        Self {
27            title: "Untitled Score".to_string(),
28            composer: String::new(),
29            lyricist: String::new(),
30            copyright: String::new(),
31            work_number: String::new(),
32            movement_title: String::new(),
33        }
34    }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScoreSettings {
39    pub tempo_bpm: u16,
40    pub time_signature: TimeSignature,
41    pub key_signature: KeySignature,
42}
43
44impl Default for ScoreSettings {
45    fn default() -> Self {
46        Self {
47            tempo_bpm: 120,
48            time_signature: TimeSignature::default(),
49            key_signature: KeySignature::default(),
50        }
51    }
52}
53
54/// Visual connector symbol for a group of adjacent parts.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum PartGroupSymbol {
57    Bracket, // square bracket — orchestral strings, woodwinds
58    Brace,   // curly brace — piano grand staff
59    Line,    // thin vertical line
60}
61
62/// Groups a range of adjacent parts with a bracket or brace for rendering.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PartGroup {
65    /// Index of the first part in the group (inclusive).
66    pub first_part: usize,
67    /// Index of the last part in the group (inclusive).
68    pub last_part: usize,
69    pub symbol: PartGroupSymbol,
70    /// Whether barlines are connected across all staves in the group.
71    #[serde(default)]
72    pub barlines_connect: bool,
73}
74
75/// Groups a range of adjacent staves within one part.
76///
77/// This is distinct from [`PartGroup`], which groups separate parts.  The
78/// distinction matters for MEI and MuseScore sources where a piano-like part
79/// can contain several staves and nested staff groups.
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct StaffGroup {
82    /// Index of the first staff in the group (inclusive).
83    pub first_staff: usize,
84    /// Index of the last staff in the group (inclusive).
85    pub last_staff: usize,
86    pub symbol: PartGroupSymbol,
87    /// Whether barlines are connected across all staves in the group.
88    #[serde(default)]
89    pub barlines_connect: bool,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Score {
94    pub id: String,
95    /// JSON schema version. 0 when deserialized from files that predate this field.
96    #[serde(default)]
97    pub schema_version: u32,
98    pub metadata: ScoreMetadata,
99    pub settings: ScoreSettings,
100    pub parts: Vec<Part>,
101    #[serde(default)]
102    pub part_groups: Vec<PartGroup>,
103    /// Typed score-level text annotations retained independently of legacy text fields.
104    #[serde(default)]
105    pub texts: Vec<StyledText>,
106    /// Typed score-wide style defaults. A linked view may override each property locally.
107    #[serde(default)]
108    pub style_overrides: Vec<ViewStyleOverride>,
109    /// Typed presentation overrides attached to stable score objects rather than a renderer key.
110    #[serde(default)]
111    pub object_style_overrides: Vec<ObjectStyleOverride>,
112    /// Reusable chord/tablature definitions imported from interchange formats.
113    #[serde(default)]
114    pub chord_definitions: Vec<ChordDefinition>,
115    /// Typed notation spans. Legacy note-level boolean endpoints remain supported during migration.
116    #[serde(default)]
117    pub spanners: Vec<NotationSpanner>,
118    /// Named non-destructive projections used for linked parts and alternate layouts.
119    #[serde(default)]
120    pub views: Vec<ScoreView>,
121}
122
123/// A stable part/staff address used by a [`ScoreView`].
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub struct ViewStaffRef {
126    pub part: usize,
127    pub staff: usize,
128}
129
130/// Whether a view presents written notation or concert pitch.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
132#[serde(rename_all = "snake_case")]
133pub enum ViewTranspositionPolicy {
134    #[default]
135    Written,
136    Concert,
137}
138
139/// Layout choices which may differ for a linked part without changing musical content.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
141pub struct ScoreViewLayoutOverrides {
142    #[serde(default)]
143    pub measures_per_row: Option<usize>,
144    #[serde(default)]
145    pub hidden_staves: Vec<ViewStaffRef>,
146    #[serde(default)]
147    pub system_breaks: Vec<usize>,
148    #[serde(default)]
149    pub page_breaks: Vec<usize>,
150    /// Deterministic key/value style overrides interpreted by a renderer or host.
151    #[serde(default)]
152    pub style_overrides: Vec<(String, String)>,
153    /// Typed, bounded styling values preferred over the legacy string bridge.
154    #[serde(default)]
155    pub typed_style_overrides: Vec<ViewStyleOverride>,
156}
157
158/// A stable view-level style property with renderer-independent units.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum ViewStyleProperty {
162    StaffSpace,
163    TextScale,
164    AnnotationGap,
165    SystemGap,
166}
167
168/// One typed style override. `StaffSpace` is a renderer scale multiplier; the remaining
169/// spacing values use staff-space units, while `TextScale` is dimensionless.
170#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
171pub struct ViewStyleOverride {
172    pub property: ViewStyleProperty,
173    pub value: f32,
174}
175
176/// A stable score object to which a typed presentation override applies.
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case", tag = "kind")]
179pub enum ObjectStyleTarget {
180    ScoreText {
181        text_index: usize,
182    },
183    MeasureText {
184        part: usize,
185        staff: usize,
186        measure: usize,
187        text_index: usize,
188    },
189    Note {
190        address: NoteAddr,
191    },
192}
193
194/// Bounded source information retained when an object style originated in interchange.
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct StyleImportProvenance {
197    pub format: String,
198    pub source_location: String,
199}
200
201/// A renderer-neutral typed style override attached to a score object.
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct ObjectStyleOverride {
204    pub target: ObjectStyleTarget,
205    pub property: ViewStyleProperty,
206    pub value: f32,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub provenance: Option<StyleImportProvenance>,
209}
210
211impl ScoreViewLayoutOverrides {
212    /// Return the last authored override for a property, preserving deterministic source order.
213    pub fn style_value(&self, property: ViewStyleProperty) -> Option<f32> {
214        self.typed_style_overrides
215            .iter()
216            .rev()
217            .find(|override_| override_.property == property)
218            .map(|override_| override_.value)
219    }
220
221    /// Resolve this view's typed style overrides onto the stable default style.
222    pub fn resolved_style(&self) -> ViewStyle {
223        let mut style = ViewStyle::default();
224        apply_style_overrides(&mut style, &self.typed_style_overrides);
225        style
226    }
227}
228
229fn apply_style_overrides(style: &mut ViewStyle, overrides: &[ViewStyleOverride]) {
230    for override_ in overrides {
231        match override_.property {
232            ViewStyleProperty::StaffSpace => style.staff_space = override_.value,
233            ViewStyleProperty::TextScale => style.text_scale = override_.value,
234            ViewStyleProperty::AnnotationGap => style.annotation_gap = override_.value,
235            ViewStyleProperty::SystemGap => style.system_gap = override_.value,
236        }
237    }
238}
239
240/// Renderer-independent effective style for a linked view.
241///
242/// `staff_space` and `text_scale` are dimensionless multipliers. Annotation and system gaps
243/// use staff-space units.
244#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
245pub struct ViewStyle {
246    pub staff_space: f32,
247    pub text_scale: f32,
248    pub annotation_gap: f32,
249    pub system_gap: f32,
250}
251
252impl Default for ViewStyle {
253    fn default() -> Self {
254        Self {
255            staff_space: 1.0,
256            text_scale: 1.0,
257            annotation_gap: 1.0,
258            system_gap: 2.0,
259        }
260    }
261}
262
263/// A view-local staff-kind override which never changes the source score.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265pub struct ViewStaffKindOverride {
266    pub staff: ViewStaffRef,
267    pub kind: StaffKind,
268}
269
270/// A linked, non-destructive score projection.
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272pub struct ScoreView {
273    pub id: String,
274    pub name: String,
275    #[serde(default)]
276    pub parts: Vec<usize>,
277    #[serde(default)]
278    pub transposition_policy: ViewTranspositionPolicy,
279    /// Presentation kinds applied only when this view is resolved.
280    #[serde(default)]
281    pub staff_kind_overrides: Vec<ViewStaffKindOverride>,
282    #[serde(default)]
283    pub layout: ScoreViewLayoutOverrides,
284}
285
286impl ScoreView {
287    /// Construct a linked part view for one source part.
288    pub fn linked_part(id: impl Into<String>, name: impl Into<String>, part: usize) -> Self {
289        Self {
290            id: id.into(),
291            name: name.into(),
292            parts: vec![part],
293            transposition_policy: ViewTranspositionPolicy::Written,
294            staff_kind_overrides: Vec::new(),
295            layout: ScoreViewLayoutOverrides::default(),
296        }
297    }
298
299    /// Construct a linked part view that presents one selected staff as tablature.
300    pub fn linked_tablature_staff(
301        id: impl Into<String>,
302        name: impl Into<String>,
303        part: usize,
304        staff: usize,
305    ) -> Self {
306        let mut view = Self::linked_part(id, name, part);
307        view.staff_kind_overrides.push(ViewStaffKindOverride {
308            staff: ViewStaffRef { part, staff },
309            kind: StaffKind::Tablature,
310        });
311        view
312    }
313
314    /// Construct a linked part view that presents one selected staff as standard notation.
315    pub fn linked_standard_staff(
316        id: impl Into<String>,
317        name: impl Into<String>,
318        part: usize,
319        staff: usize,
320    ) -> Self {
321        let mut view = Self::linked_part(id, name, part);
322        view.staff_kind_overrides.push(ViewStaffKindOverride {
323            staff: ViewStaffRef { part, staff },
324            kind: StaffKind::Standard,
325        });
326        view
327    }
328}
329
330/// Bounded notation span kinds with stable source identity.
331#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
332pub enum NotationSpannerKind {
333    Slur,
334    Glissando,
335    TrillLine,
336    Pedal,
337    Ottava,
338}
339
340/// A typed, potentially cross-staff notation span between two canonical note addresses.
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct NotationSpanner {
343    pub id: String,
344    pub kind: NotationSpannerKind,
345    pub start: NoteAddr,
346    pub end: NoteAddr,
347    #[serde(default)]
348    pub number: Option<u16>,
349    #[serde(default)]
350    pub line_type: Option<String>,
351    #[serde(default)]
352    pub text: Option<String>,
353    #[serde(default)]
354    pub placement: Option<String>,
355    #[serde(default)]
356    pub ottava_size: Option<u8>,
357    /// MusicXML octave-shift direction (`up` or `down`) when this is an ottava span.
358    #[serde(default)]
359    pub ottava_type: Option<String>,
360}
361
362impl Default for Score {
363    fn default() -> Self {
364        let mut part = Part::new("Piano", "Pno.");
365        part.staves.push(Staff::new(Clef::Treble));
366        for _ in 0..4 {
367            part.staves[0].measures.push(Measure::empty(4, 4));
368        }
369        Self {
370            id: Uuid::new_v4().to_string(),
371            schema_version: 1,
372            metadata: ScoreMetadata::default(),
373            settings: ScoreSettings::default(),
374            parts: vec![part],
375            part_groups: Vec::new(),
376            texts: Vec::new(),
377            style_overrides: Vec::new(),
378            object_style_overrides: Vec::new(),
379            chord_definitions: Vec::new(),
380            spanners: Vec::new(),
381            views: Vec::new(),
382        }
383    }
384}
385
386/// Score template presets for common ensemble configurations.
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
388pub enum ScoreTemplate {
389    /// Single treble-clef part (piano by default).
390    Solo,
391    /// One piano part with treble + bass grand staff.
392    Piano,
393    /// Violin I, Violin II, Viola, Cello.
394    StringQuartet,
395    /// Violin I, Violin II, Viola, Cello, Contrabass.
396    StringOrchestra,
397    /// Two trumpets, French horn, trombone, tuba.
398    BrassQuintet,
399}
400
401impl Score {
402    /// Resolve score-wide defaults followed by the supplied view's local overrides.
403    pub fn resolved_view_style(&self, layout: &ScoreViewLayoutOverrides) -> ViewStyle {
404        let mut style = ViewStyle::default();
405        apply_style_overrides(&mut style, &self.style_overrides);
406        apply_style_overrides(&mut style, &layout.typed_style_overrides);
407        style
408    }
409
410    pub fn new(
411        title: &str,
412        tempo_bpm: u16,
413        numerator: u8,
414        denominator: u8,
415        fifths: i8,
416        measure_count: u32,
417    ) -> Self {
418        let mut score = Score::default();
419        score.metadata.title = title.to_string();
420        score.settings.tempo_bpm = tempo_bpm;
421        score.settings.time_signature = TimeSignature {
422            numerator,
423            denominator,
424        };
425        score.settings.key_signature = KeySignature {
426            fifths,
427            mode: "major".to_string(),
428        };
429
430        score.parts[0].staves[0].measures.clear();
431        for i in 0..measure_count {
432            let mut m = Measure::empty(numerator, denominator);
433            m.number = i + 1;
434            score.parts[0].staves[0].measures.push(m);
435        }
436        score
437    }
438
439    /// Create a score pre-populated with parts for the given ensemble template.
440    ///
441    /// Defaults: 120 BPM, 4/4, C major, 4 empty measures.
442    /// Use [`NewScoreCmd`](crate::model::commands::NewScoreCmd) to override those after creation.
443    pub fn template(kind: ScoreTemplate) -> Self {
444        fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
445            (0..count)
446                .map(|i| {
447                    let mut m = Measure::empty(num, den);
448                    m.number = i + 1;
449                    m
450                })
451                .collect()
452        }
453        fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
454            let mut p = Part::new(name, short);
455            p.midi_program = program;
456            let mut s = Staff::new(clef);
457            s.measures = measures(4, 4, 4);
458            p.staves.push(s);
459            p
460        }
461
462        let mut score = Score {
463            id: uuid::Uuid::new_v4().to_string(),
464            schema_version: 1,
465            metadata: ScoreMetadata::default(),
466            settings: ScoreSettings::default(),
467            parts: Vec::new(),
468            part_groups: Vec::new(),
469            texts: Vec::new(),
470            style_overrides: Vec::new(),
471            object_style_overrides: Vec::new(),
472            chord_definitions: Vec::new(),
473            spanners: Vec::new(),
474            views: Vec::new(),
475        };
476
477        match kind {
478            ScoreTemplate::Solo => {
479                score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
480            }
481            ScoreTemplate::Piano => {
482                let mut p = Part::new("Piano", "Pno.");
483                p.midi_program = 0;
484                let mut treble = Staff::new(Clef::Treble);
485                treble.measures = measures(4, 4, 4);
486                let mut bass = Staff::new(Clef::Bass);
487                bass.measures = measures(4, 4, 4);
488                p.staves.push(treble);
489                p.staves.push(bass);
490                score.parts.push(p);
491            }
492            ScoreTemplate::StringQuartet => {
493                score
494                    .parts
495                    .push(part("Violin I", "Vn. I", Clef::Treble, 40));
496                score
497                    .parts
498                    .push(part("Violin II", "Vn. II", Clef::Treble, 40));
499                score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
500                score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
501            }
502            ScoreTemplate::StringOrchestra => {
503                score
504                    .parts
505                    .push(part("Violin I", "Vn. I", Clef::Treble, 40));
506                score
507                    .parts
508                    .push(part("Violin II", "Vn. II", Clef::Treble, 40));
509                score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
510                score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
511                score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
512            }
513            ScoreTemplate::BrassQuintet => {
514                score
515                    .parts
516                    .push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
517                score
518                    .parts
519                    .push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
520                score
521                    .parts
522                    .push(part("French Horn", "Hn.", Clef::Treble, 60));
523                score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
524                score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
525            }
526        }
527        score
528    }
529
530    pub fn measure_count(&self) -> usize {
531        self.parts
532            .first()
533            .and_then(|p| p.staves.first())
534            .map(|s| s.measures.len())
535            .unwrap_or(0)
536    }
537
538    /// Resolve the inclusive section containing `measure_index`.
539    ///
540    /// A section boundary belongs to the measure where it starts. Layout breaks are
541    /// intentionally ignored: they have no editor-range semantics.
542    pub fn section_range(
543        &self,
544        measure_index: usize,
545    ) -> Result<std::ops::RangeInclusive<usize>, Error> {
546        let measures = self
547            .parts
548            .first()
549            .and_then(|part| part.staves.first())
550            .map(|staff| &staff.measures)
551            .ok_or(Error::MeasureNotFound(measure_index))?;
552        if measure_index >= measures.len() {
553            return Err(Error::MeasureNotFound(measure_index));
554        }
555        let start = (0..=measure_index)
556            .rev()
557            .find(|&index| measures[index].section_break)
558            .unwrap_or(0);
559        let end = ((measure_index + 1)..measures.len())
560            .find(|&index| measures[index].section_break)
561            .map(|index| index - 1)
562            .unwrap_or(measures.len() - 1);
563        Ok(start..=end)
564    }
565
566    /// Aggregate statistics about the score.
567    pub fn statistics(&self) -> ScoreStats {
568        let measure_count = self.measure_count();
569        let part_count = self.parts.len();
570
571        // Beat accumulation via measure_sequence so repeats are counted correctly.
572        let seq = measure_sequence(self);
573        let total_beats: f64 = self
574            .parts
575            .first()
576            .and_then(|p| p.staves.first())
577            .map(|s| {
578                seq.iter()
579                    .filter_map(|&idx| s.measures.get(idx))
580                    .flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
581                    .map(|n| n.beats())
582                    .sum()
583            })
584            .unwrap_or(0.0);
585
586        let mut note_count = 0usize;
587        let mut rest_count = 0usize;
588        for part in &self.parts {
589            for staff in &part.staves {
590                for measure in &staff.measures {
591                    for voice in &measure.voices {
592                        for note in voice {
593                            if note.is_rest {
594                                rest_count += 1;
595                            } else {
596                                note_count += 1;
597                            }
598                        }
599                    }
600                }
601            }
602        }
603
604        let bpm = self.settings.tempo_bpm as f64;
605        let estimated_duration_secs = if bpm > 0.0 {
606            total_beats / bpm * 60.0
607        } else {
608            0.0
609        };
610
611        ScoreStats {
612            measure_count,
613            note_count,
614            rest_count,
615            part_count,
616            estimated_duration_secs,
617        }
618    }
619
620    /// Resolve a linked view to an independent score snapshot without mutating this score.
621    ///
622    /// The snapshot contains only the selected parts and notation spans whose endpoints remain
623    /// inside the projection. View-only layout settings are deliberately retained in the returned
624    /// view list so a layout consumer can apply them without changing the source score.
625    pub fn resolve_view(&self, view_id: &str) -> Result<Score, Error> {
626        let view = self
627            .views
628            .iter()
629            .find(|view| view.id == view_id)
630            .ok_or_else(|| {
631                Error::InvalidCommand(format!("score view '{view_id}' does not exist"))
632            })?;
633        let mut source_to_target = vec![None; self.parts.len()];
634        let mut parts = Vec::with_capacity(view.parts.len());
635        for &source_index in &view.parts {
636            let source = self
637                .parts
638                .get(source_index)
639                .ok_or(Error::PartNotFound(source_index))?;
640            if source_to_target[source_index].is_some() {
641                return Err(Error::InvalidCommand(format!(
642                    "score view '{}' selects part {} more than once",
643                    view.id, source_index
644                )));
645            }
646            source_to_target[source_index] = Some(parts.len());
647            parts.push(source.clone());
648        }
649        for override_ in &view.staff_kind_overrides {
650            let target_part = source_to_target
651                .get(override_.staff.part)
652                .copied()
653                .flatten()
654                .ok_or_else(|| {
655                    Error::InvalidCommand(
656                        "view overrides a staff outside its selected parts".into(),
657                    )
658                })?;
659            let target_staff = parts[target_part]
660                .staves
661                .get_mut(override_.staff.staff)
662                .ok_or_else(|| {
663                    Error::InvalidCommand(format!(
664                        "view overrides staff {} outside part {}",
665                        override_.staff.staff, override_.staff.part
666                    ))
667                })?;
668            if override_.kind == StaffKind::Tablature && target_staff.tablature.is_none() {
669                return Err(Error::InvalidCommand(
670                    "tablature view requires a tablature configuration on its source staff".into(),
671                ));
672            }
673            target_staff.presentation.kind = override_.kind;
674        }
675        let spanners = self
676            .spanners
677            .iter()
678            .filter_map(|spanner| {
679                let start = source_to_target
680                    .get(spanner.start.part)
681                    .copied()
682                    .flatten()?;
683                let end = source_to_target.get(spanner.end.part).copied().flatten()?;
684                let mut projected = spanner.clone();
685                projected.start.part = start;
686                projected.end.part = end;
687                Some(projected)
688            })
689            .collect();
690        let part_groups = self
691            .part_groups
692            .iter()
693            .filter_map(|group| {
694                let first = source_to_target.get(group.first_part).copied().flatten()?;
695                let last = source_to_target.get(group.last_part).copied().flatten()?;
696                Some(PartGroup {
697                    first_part: first,
698                    last_part: last,
699                    symbol: group.symbol.clone(),
700                    barlines_connect: group.barlines_connect,
701                })
702            })
703            .collect();
704        let mut projected_view = view.clone();
705        projected_view.parts = (0..parts.len()).collect();
706        for reference in &mut projected_view.layout.hidden_staves {
707            reference.part = source_to_target
708                .get(reference.part)
709                .copied()
710                .flatten()
711                .ok_or_else(|| {
712                    Error::InvalidCommand("view hides a part outside its selection".into())
713                })?;
714        }
715        for override_ in &mut projected_view.staff_kind_overrides {
716            override_.staff.part = source_to_target
717                .get(override_.staff.part)
718                .copied()
719                .flatten()
720                .ok_or_else(|| {
721                    Error::InvalidCommand("view overrides a staff outside its selection".into())
722                })?;
723        }
724        Ok(Score {
725            id: Uuid::new_v4().to_string(),
726            schema_version: self.schema_version,
727            metadata: self.metadata.clone(),
728            settings: self.settings.clone(),
729            parts,
730            part_groups,
731            texts: self.texts.clone(),
732            style_overrides: self.style_overrides.clone(),
733            object_style_overrides: self.object_style_overrides.clone(),
734            chord_definitions: self.chord_definitions.clone(),
735            spanners,
736            views: vec![projected_view],
737        })
738    }
739
740    /// Return a new `Score` containing only the given part.
741    /// Returns `None` if `part_index` is out of range.
742    pub fn extract_part(&self, part_index: usize) -> Option<Score> {
743        let part = self.parts.get(part_index)?.clone();
744        let spanners = self
745            .spanners
746            .iter()
747            .filter(|spanner| spanner.start.part == part_index && spanner.end.part == part_index)
748            .cloned()
749            .map(|mut spanner| {
750                spanner.start.part = 0;
751                spanner.end.part = 0;
752                spanner
753            })
754            .collect();
755        Some(Score {
756            id: Uuid::new_v4().to_string(),
757            schema_version: 1,
758            metadata: self.metadata.clone(),
759            settings: self.settings.clone(),
760            parts: vec![part],
761            part_groups: Vec::new(),
762            texts: self.texts.clone(),
763            style_overrides: self.style_overrides.clone(),
764            object_style_overrides: self.object_style_overrides.clone(),
765            chord_definitions: self.chord_definitions.clone(),
766            spanners,
767            views: Vec::new(),
768        })
769    }
770
771    /// Extract a part through the validated transformation boundary.
772    pub fn extract_part_checked(&self, part_index: usize) -> Result<Score, Error> {
773        if !super::validate::validate(self).is_valid() {
774            return Err(Error::InvalidScore);
775        }
776        let extracted = self
777            .extract_part(part_index)
778            .ok_or(Error::PartNotFound(part_index))?;
779        if !super::validate::validate(&extracted).is_valid() {
780            return Err(Error::InvalidScore);
781        }
782        Ok(extracted)
783    }
784
785    /// Merge two scores by appending `other`'s parts to `self`'s parts.
786    /// Shorter scores are padded with empty measures to match the longer one.
787    /// Metadata and settings are taken from `self`.
788    pub fn merge(&self, other: &Score) -> Score {
789        let self_count = self.measure_count();
790        let other_count = other.measure_count();
791        let max_count = self_count.max(other_count);
792        let ts = self.settings.time_signature.clone();
793
794        let pad = |mut part: Part, from: usize| -> Part {
795            for staff in &mut part.staves {
796                for i in from..max_count {
797                    let mut m = Measure::empty(ts.numerator, ts.denominator);
798                    m.number = i as u32 + 1;
799                    staff.measures.push(m);
800                }
801            }
802            part
803        };
804
805        let mut parts: Vec<Part> = self
806            .parts
807            .iter()
808            .cloned()
809            .map(|p| pad(p, self_count))
810            .collect();
811        for p in &other.parts {
812            parts.push(pad(p.clone(), other_count));
813        }
814        let self_part_count = self.parts.len();
815        let mut spanners = self.spanners.clone();
816        spanners.extend(other.spanners.iter().cloned().map(|mut spanner| {
817            spanner.start.part += self_part_count;
818            spanner.end.part += self_part_count;
819            spanner
820        }));
821
822        Score {
823            id: Uuid::new_v4().to_string(),
824            schema_version: 1,
825            metadata: self.metadata.clone(),
826            settings: self.settings.clone(),
827            parts,
828            part_groups: Vec::new(),
829            texts: self.texts.clone(),
830            style_overrides: self.style_overrides.clone(),
831            object_style_overrides: self.object_style_overrides.clone(),
832            chord_definitions: self.chord_definitions.clone(),
833            spanners,
834            views: Vec::new(),
835        }
836    }
837
838    /// Merge scores through the validated transformation boundary.
839    pub fn merge_checked(&self, other: &Score) -> Result<Score, Error> {
840        if !super::validate::validate(self).is_valid()
841            || !super::validate::validate(other).is_valid()
842        {
843            return Err(Error::InvalidScore);
844        }
845        let merged = self.merge(other);
846        if !super::validate::validate(&merged).is_valid() {
847            return Err(Error::InvalidScore);
848        }
849        Ok(merged)
850    }
851}
852
853/// Aggregate statistics returned by [`Score::statistics`].
854#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
855pub struct ScoreStats {
856    pub measure_count: usize,
857    /// Number of non-rest notes across all parts.
858    pub note_count: usize,
859    pub rest_count: usize,
860    pub part_count: usize,
861    /// Rough estimate: `total_beats(first part) / tempo_bpm * 60`.
862    pub estimated_duration_secs: f64,
863}
864
865// ── transpose ─────────────────────────────────────────────────────────────────
866
867use super::pitch::Step;
868use super::repeat::measure_sequence;
869
870/// Assign deterministic guitar tablature positions to eligible notes and chords.
871///
872/// Existing positions are preserved. For each note, the lowest reachable fret is
873/// selected, breaking ties toward the highest string (the lowest string number). For
874/// chords, strings are unique and the assignment minimizes total fret, then fret
875/// span, then highest fret. This is a bounded deterministic fingering heuristic,
876/// not a claim of instrument-specific playability.
877/// The staff tuning is interpreted as open-string MIDI pitches before capo, and
878/// assignments are limited to frets 0 through 24. Rests, chords, and notes with
879/// no reachable position are left unchanged. Returns the number of notes/chords
880/// assigned.
881pub fn assign_tablature_positions(score: &mut Score) -> usize {
882    const MAX_FRET: i16 = 24;
883    let mut assigned = 0;
884
885    for part in &mut score.parts {
886        for staff in &mut part.staves {
887            if staff.tablature.is_none() {
888                continue;
889            }
890            let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
891                .map(|measure_index| staff.tablature_at(measure_index))
892                .collect();
893
894            for (measure_index, measure) in staff.measures.iter_mut().enumerate() {
895                let Some(tab) = configurations[measure_index].as_ref() else {
896                    continue;
897                };
898                for voice in &mut measure.voices {
899                    for note in voice.iter_mut() {
900                        if note.is_rest
901                            || note.tab_position.is_some()
902                            || !note.tab_positions.is_empty()
903                            || note.pitches.is_empty()
904                        {
905                            continue;
906                        }
907                        let pitches: Vec<i16> = note.pitches.iter().map(Pitch::to_midi).collect();
908                        let Some(positions) = best_tablature_assignment(
909                            &pitches,
910                            &tab.tuning_midi,
911                            tab.lines as usize,
912                            i16::from(tab.capo),
913                            MAX_FRET,
914                        ) else {
915                            continue;
916                        };
917
918                        note.tab_position = positions.first().cloned();
919                        note.tab_positions = positions;
920                        note.string_number = note.tab_position.as_ref().map(|p| p.string);
921                        assigned += 1;
922                    }
923                }
924            }
925        }
926    }
927
928    assigned
929}
930
931/// Optimize tablature positions across each voice using bounded position movement.
932///
933/// Explicit positions are treated as fixed anchors. Unassigned notes and chords
934/// receive positions that minimize fret load plus movement from the preceding
935/// event; ties are resolved deterministically. Returns the number of notes/chords
936/// newly assigned.
937pub fn optimize_tablature_positions(score: &mut Score) -> usize {
938    const MAX_FRET: i16 = 24;
939    let mut assigned = 0;
940
941    for part in &mut score.parts {
942        for staff in &mut part.staves {
943            if staff.tablature.is_none() {
944                continue;
945            }
946            let configurations: Vec<Option<TablatureConfig>> = (0..staff.measures.len())
947                .map(|measure_index| staff.tablature_at(measure_index))
948                .collect();
949            for voice in 0..4 {
950                // Work per measure/voice location so repeated note indices remain distinct.
951                let mut locations = Vec::new();
952                for (measure_index, measure) in staff.measures.iter().enumerate() {
953                    for (note_index, note) in measure.voices[voice].iter().enumerate() {
954                        if !note.is_rest && !note.pitches.is_empty() {
955                            locations.push((measure_index, note_index, note.clone()));
956                        }
957                    }
958                }
959                let candidates: Vec<Vec<Vec<TabPosition>>> = locations
960                    .iter()
961                    .map(|(measure_index, _, note)| {
962                        if let Some(positions) = if !note.tab_positions.is_empty() {
963                            Some(note.tab_positions.clone())
964                        } else {
965                            note.tab_position.clone().map(|position| vec![position])
966                        } {
967                            vec![positions]
968                        } else {
969                            let Some(tab) = configurations[*measure_index].as_ref() else {
970                                return Vec::new();
971                            };
972                            tablature_assignments(
973                                &note.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
974                                &tab.tuning_midi,
975                                tab.lines as usize,
976                                i16::from(tab.capo),
977                                MAX_FRET,
978                            )
979                        }
980                    })
981                    .collect();
982                if candidates.iter().any(Vec::is_empty) {
983                    continue;
984                }
985
986                let mut costs: Vec<Vec<(u32, Option<usize>)>> = candidates
987                    .iter()
988                    .map(|events| vec![(u32::MAX, None); events.len()])
989                    .collect();
990                for (candidate_index, candidate) in candidates[0].iter().enumerate() {
991                    costs[0][candidate_index] = (tablature_load(candidate), None);
992                }
993                for event_index in 1..candidates.len() {
994                    for (candidate_index, candidate) in candidates[event_index].iter().enumerate() {
995                        let load = tablature_load(candidate);
996                        for (previous_index, previous) in
997                            candidates[event_index - 1].iter().enumerate()
998                        {
999                            let previous_cost = costs[event_index - 1][previous_index].0;
1000                            let cost = previous_cost
1001                                .saturating_add(load)
1002                                .saturating_add(tablature_movement(previous, candidate));
1003                            if cost < costs[event_index][candidate_index].0 {
1004                                costs[event_index][candidate_index] = (cost, Some(previous_index));
1005                            }
1006                        }
1007                    }
1008                }
1009                let mut selected = vec![0; candidates.len()];
1010                if let Some((last, _)) = costs.last().and_then(|row| {
1011                    row.iter()
1012                        .enumerate()
1013                        .min_by_key(|(index, (cost, _))| (*cost, *index))
1014                }) {
1015                    selected[candidates.len() - 1] = last;
1016                    for event_index in (1..candidates.len()).rev() {
1017                        selected[event_index - 1] =
1018                            costs[event_index][selected[event_index]].1.unwrap_or(0);
1019                    }
1020                }
1021
1022                for (((measure_index, note_index, original), event_candidates), selected_index) in
1023                    locations.into_iter().zip(candidates).zip(selected)
1024                {
1025                    if original.tab_position.is_none() && original.tab_positions.is_empty() {
1026                        let note = &mut staff.measures[measure_index].voices[voice][note_index];
1027                        let positions = event_candidates[selected_index].clone();
1028                        note.tab_position = positions.first().cloned();
1029                        note.tab_positions = positions;
1030                        note.string_number = note.tab_position.as_ref().map(|p| p.string);
1031                        assigned += 1;
1032                    }
1033                }
1034            }
1035        }
1036    }
1037
1038    assigned
1039}
1040
1041fn tablature_load(positions: &[TabPosition]) -> u32 {
1042    let sum: u32 = positions
1043        .iter()
1044        .map(|position| u32::from(position.fret))
1045        .sum();
1046    let min = positions
1047        .iter()
1048        .map(|position| position.fret)
1049        .min()
1050        .unwrap_or(0);
1051    let max = positions
1052        .iter()
1053        .map(|position| position.fret)
1054        .max()
1055        .unwrap_or(0);
1056    let span = max - min;
1057    // A four-fret hand position is a practical baseline; wide chords receive
1058    // a strong penalty so a higher but compact voicing wins when available.
1059    let stretch_penalty = span.saturating_sub(4) as u32 * 12;
1060    sum + u32::from(span) * 2 + stretch_penalty
1061}
1062
1063fn tablature_movement(previous: &[TabPosition], current: &[TabPosition]) -> u32 {
1064    previous
1065        .iter()
1066        .zip(current)
1067        .map(|(a, b)| u32::from(a.fret.abs_diff(b.fret)) + u32::from(a.string.abs_diff(b.string)))
1068        .sum()
1069}
1070
1071fn tablature_assignments(
1072    pitches: &[i16],
1073    tuning: &[i16],
1074    lines: usize,
1075    capo: i16,
1076    max_fret: i16,
1077) -> Vec<Vec<TabPosition>> {
1078    #[allow(clippy::too_many_arguments)]
1079    fn visit(
1080        pitches: &[i16],
1081        tuning: &[i16],
1082        lines: usize,
1083        capo: i16,
1084        max_fret: i16,
1085        index: usize,
1086        used: &mut [bool],
1087        current: &mut Vec<TabPosition>,
1088        output: &mut Vec<Vec<TabPosition>>,
1089    ) {
1090        if index == pitches.len() {
1091            output.push(current.clone());
1092            return;
1093        }
1094        for (string, open) in tuning.iter().enumerate().take(lines) {
1095            if used[string] {
1096                continue;
1097            }
1098            let fret = pitches[index] - *open - capo;
1099            if !(0..=max_fret).contains(&fret) {
1100                continue;
1101            }
1102            used[string] = true;
1103            current.push(TabPosition {
1104                string: (string + 1) as u8,
1105                fret: fret as u8,
1106            });
1107            visit(
1108                pitches,
1109                tuning,
1110                lines,
1111                capo,
1112                max_fret,
1113                index + 1,
1114                used,
1115                current,
1116                output,
1117            );
1118            current.pop();
1119            used[string] = false;
1120        }
1121    }
1122
1123    if pitches.is_empty() || pitches.len() > lines {
1124        return Vec::new();
1125    }
1126    let mut output = Vec::new();
1127    visit(
1128        pitches,
1129        tuning,
1130        lines,
1131        capo,
1132        max_fret,
1133        0,
1134        &mut vec![false; lines],
1135        &mut Vec::new(),
1136        &mut output,
1137    );
1138    output
1139}
1140
1141fn best_tablature_assignment(
1142    pitches: &[i16],
1143    tuning: &[i16],
1144    lines: usize,
1145    capo: i16,
1146    max_fret: i16,
1147) -> Option<Vec<TabPosition>> {
1148    type Assignment = (i16, i16, i16, Vec<u8>, Vec<TabPosition>);
1149
1150    #[allow(clippy::too_many_arguments)]
1151    fn search(
1152        pitches: &[i16],
1153        tuning: &[i16],
1154        lines: usize,
1155        capo: i16,
1156        max_fret: i16,
1157        index: usize,
1158        used: &mut [bool],
1159        current: &mut Vec<TabPosition>,
1160        best: &mut Option<Assignment>,
1161    ) {
1162        if index == pitches.len() {
1163            let sum: i16 = current.iter().map(|p| i16::from(p.fret)).sum();
1164            let min = current.iter().map(|p| p.fret).min().unwrap_or(0);
1165            let max = current.iter().map(|p| p.fret).max().unwrap_or(0);
1166            let strings: Vec<u8> = current.iter().map(|p| p.string).collect();
1167            let candidate = (
1168                sum,
1169                i16::from(max) - i16::from(min),
1170                i16::from(max),
1171                strings,
1172                current.clone(),
1173            );
1174            if best.as_ref().is_none_or(|existing| {
1175                (candidate.0, candidate.1, candidate.2, &candidate.3)
1176                    < (existing.0, existing.1, existing.2, &existing.3)
1177            }) {
1178                *best = Some(candidate);
1179            }
1180            return;
1181        }
1182
1183        for (string, open) in tuning.iter().enumerate().take(lines) {
1184            if used[string] {
1185                continue;
1186            }
1187            let fret = pitches[index] - *open - capo;
1188            if !(0..=max_fret).contains(&fret) {
1189                continue;
1190            }
1191            used[string] = true;
1192            current.push(TabPosition {
1193                string: (string + 1) as u8,
1194                fret: fret as u8,
1195            });
1196            search(
1197                pitches,
1198                tuning,
1199                lines,
1200                capo,
1201                max_fret,
1202                index + 1,
1203                used,
1204                current,
1205                best,
1206            );
1207            current.pop();
1208            used[string] = false;
1209        }
1210    }
1211
1212    if pitches.len() > lines {
1213        return None;
1214    }
1215    let mut used = vec![false; lines];
1216    let mut current = Vec::with_capacity(pitches.len());
1217    let mut best = None;
1218    search(
1219        pitches,
1220        tuning,
1221        lines,
1222        capo,
1223        max_fret,
1224        0,
1225        &mut used,
1226        &mut current,
1227        &mut best,
1228    );
1229    best.map(|(_, _, _, _, positions)| positions)
1230}
1231
1232/// Return a new `Score` with all pitches shifted by `semitones`.
1233/// Key signatures (global and per-measure) are updated accordingly.
1234/// If `semitones == 0` the score is cloned unchanged.
1235pub fn transpose(score: &Score, semitones: i8) -> Score {
1236    if semitones == 0 {
1237        return score.clone();
1238    }
1239    let mut out = score.clone();
1240    out.settings.key_signature.fifths = transpose_fifths(
1241        score.settings.key_signature.fifths,
1242        &score.settings.key_signature.mode,
1243        semitones,
1244    );
1245    for part in &mut out.parts {
1246        for staff in &mut part.staves {
1247            for measure in &mut staff.measures {
1248                if let Some(ref mut ks) = measure.key_sig {
1249                    ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
1250                }
1251                for voice in &mut measure.voices {
1252                    for note in voice.iter_mut() {
1253                        for pitch in note.pitches.iter_mut() {
1254                            *pitch = transpose_pitch(pitch, semitones);
1255                        }
1256                    }
1257                }
1258            }
1259        }
1260    }
1261    out
1262}
1263
1264/// Transpose a score through the validated transformation boundary.
1265pub fn transpose_checked(score: &Score, semitones: i8) -> Result<Score, Error> {
1266    if !super::validate::validate(score).is_valid() {
1267        return Err(Error::InvalidScore);
1268    }
1269    let transposed = transpose(score, semitones);
1270    if !super::validate::validate(&transposed).is_valid() {
1271        return Err(Error::InvalidScore);
1272    }
1273    Ok(transposed)
1274}
1275
1276/// Choose whether a regional transposition rewrites notation or concert sounding pitch.
1277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1278#[serde(rename_all = "snake_case")]
1279pub enum RegionalTranspositionTarget {
1280    /// Rewrite written pitches and local key signatures in the selected measures.
1281    Written,
1282    /// Keep written notation unchanged and adjust the staff's concert-pitch offset.
1283    Concert,
1284}
1285
1286/// Transpose one inclusive-start, exclusive-end staff measure range through the validated
1287/// transformation boundary. The source score is never mutated.
1288pub fn transpose_staff_region_checked(
1289    score: &Score,
1290    part_index: usize,
1291    staff_index: usize,
1292    start_measure: usize,
1293    end_measure: usize,
1294    semitones: i8,
1295    target: RegionalTranspositionTarget,
1296) -> Result<Score, Error> {
1297    if !super::validate::validate(score).is_valid() {
1298        return Err(Error::InvalidScore);
1299    }
1300    let source_staff = score
1301        .parts
1302        .get(part_index)
1303        .ok_or(Error::PartNotFound(part_index))?
1304        .staves
1305        .get(staff_index)
1306        .ok_or(Error::StaffNotFound(staff_index))?;
1307    if start_measure >= end_measure || end_measure > source_staff.measures.len() {
1308        return Err(Error::InvalidCommand(format!(
1309            "invalid measure range {start_measure}..{end_measure}"
1310        )));
1311    }
1312    if target == RegionalTranspositionTarget::Concert
1313        && (start_measure != 0 || end_measure != source_staff.measures.len())
1314    {
1315        return Err(Error::InvalidCommand(
1316            "concert-pitch staff transposition requires the full staff range".into(),
1317        ));
1318    }
1319    let mut transformed = score.clone();
1320    let staff = &mut transformed.parts[part_index].staves[staff_index];
1321    match target {
1322        RegionalTranspositionTarget::Written => {
1323            for measure in &mut staff.measures[start_measure..end_measure] {
1324                if let Some(key) = &mut measure.key_sig {
1325                    key.fifths = transpose_fifths(key.fifths, &key.mode, semitones);
1326                }
1327                for voice in &mut measure.voices {
1328                    for note in voice {
1329                        for pitch in &mut note.pitches {
1330                            *pitch = transpose_pitch(pitch, semitones);
1331                        }
1332                    }
1333                }
1334            }
1335        }
1336        RegionalTranspositionTarget::Concert => {
1337            staff.transpose_semitones = staff.transpose_semitones.saturating_add(semitones);
1338        }
1339    }
1340    if !super::validate::validate(&transformed).is_valid() {
1341        return Err(Error::InvalidScore);
1342    }
1343    Ok(transformed)
1344}
1345
1346fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
1347    let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
1348    let pc = new_midi % 12;
1349    let oct = (new_midi / 12) as i8 - 1;
1350    let (step, alter): (Step, i8) = if semitones >= 0 {
1351        match pc {
1352            0 => (Step::C, 0),
1353            1 => (Step::C, 1),
1354            2 => (Step::D, 0),
1355            3 => (Step::D, 1),
1356            4 => (Step::E, 0),
1357            5 => (Step::F, 0),
1358            6 => (Step::F, 1),
1359            7 => (Step::G, 0),
1360            8 => (Step::G, 1),
1361            9 => (Step::A, 0),
1362            10 => (Step::A, 1),
1363            11 => (Step::B, 0),
1364            _ => (Step::C, 0),
1365        }
1366    } else {
1367        match pc {
1368            0 => (Step::C, 0),
1369            1 => (Step::D, -1),
1370            2 => (Step::D, 0),
1371            3 => (Step::E, -1),
1372            4 => (Step::E, 0),
1373            5 => (Step::F, 0),
1374            6 => (Step::G, -1),
1375            7 => (Step::G, 0),
1376            8 => (Step::A, -1),
1377            9 => (Step::A, 0),
1378            10 => (Step::B, -1),
1379            11 => (Step::B, 0),
1380            _ => (Step::C, 0),
1381        }
1382    };
1383    Pitch::with_microtone(step, oct, alter, pitch.microtone_cents)
1384}
1385
1386/// Shift a key signature's fifths value by `semitones`.
1387///
1388/// Uses the circle-of-fifths arithmetic:
1389/// - `tonic_pc = (fifths * 7) mod 12`  (for major; minor adds 9 to get relative major tonic)
1390/// - `new_fifths = (new_tonic_pc * 7) mod 12`, adjusted to `[-7, 7]`
1391fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
1392    let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
1393    let tonic_pc = if mode == "minor" {
1394        ((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
1395    } else {
1396        tonic_major_pc
1397    };
1398    let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
1399    let major_tonic = if mode == "minor" {
1400        ((new_tonic as i32 + 3).rem_euclid(12)) as u8
1401    } else {
1402        new_tonic
1403    };
1404    let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
1405    if raw > 6 { raw - 12 } else { raw }
1406}
1407
1408/// Inclusive MIDI note-number range declared by an instrument definition.
1409#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1410pub struct InstrumentRange {
1411    pub lowest: u8,
1412    pub highest: u8,
1413}
1414
1415/// Stable, renderer-independent instrument semantics for a part.
1416///
1417/// This is deliberately separate from a part's current MIDI state: the latter
1418/// records imported/performance values, while this definition supplies the
1419/// instrument identity, notation defaults, and practical written/sounding
1420/// ranges used by editors and layout clients.
1421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1422pub struct InstrumentDefinition {
1423    /// Stable source or application-defined identifier. It must be non-empty.
1424    pub id: String,
1425    #[serde(default)]
1426    pub name: String,
1427    #[serde(default)]
1428    pub short_name: String,
1429    #[serde(default)]
1430    pub family: Option<String>,
1431    #[serde(default)]
1432    pub transpose_semitones: i8,
1433    #[serde(default)]
1434    pub written_range: Option<InstrumentRange>,
1435    #[serde(default)]
1436    pub sounding_range: Option<InstrumentRange>,
1437    #[serde(default)]
1438    pub default_clefs: Vec<Clef>,
1439    #[serde(default = "default_instrument_staff_count")]
1440    pub staff_count: u8,
1441    #[serde(default)]
1442    pub staff_kind: StaffKind,
1443    #[serde(default)]
1444    pub midi_channel: u8,
1445    #[serde(default)]
1446    pub midi_program: u8,
1447    #[serde(default)]
1448    pub percussion_map_id: Option<String>,
1449}
1450
1451const fn default_instrument_staff_count() -> u8 {
1452    1
1453}
1454
1455impl InstrumentDefinition {
1456    /// Construct a minimal single-staff instrument declaration.
1457    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
1458        Self {
1459            id: id.into(),
1460            name: name.into(),
1461            short_name: String::new(),
1462            family: None,
1463            transpose_semitones: 0,
1464            written_range: None,
1465            sounding_range: None,
1466            default_clefs: Vec::new(),
1467            staff_count: 1,
1468            staff_kind: StaffKind::Standard,
1469            midi_channel: 0,
1470            midi_program: 0,
1471            percussion_map_id: None,
1472        }
1473    }
1474}
1475
1476#[derive(Debug, Clone, Serialize, Deserialize)]
1477pub struct Part {
1478    pub id: String,
1479    pub name: String,
1480    pub short_name: String,
1481    pub staves: Vec<Staff>,
1482    /// MIDI channel (0–15). Channel 9 is conventionally used for percussion.
1483    #[serde(default)]
1484    pub midi_channel: u8,
1485    /// General MIDI program number (0–127). Default 0 = Acoustic Grand Piano.
1486    #[serde(default)]
1487    pub midi_program: u8,
1488    /// MIDI pitch-bend events preserved from interchange input, in canonical 480 PPQ ticks.
1489    #[serde(default)]
1490    pub midi_pitch_bends: Vec<MidiPitchBend>,
1491    /// MIDI Control Change events preserved from interchange input, in canonical 480 PPQ ticks.
1492    #[serde(default)]
1493    pub midi_control_changes: Vec<MidiControlChange>,
1494    /// MIDI Program Change events preserved from interchange input, in canonical 480 PPQ ticks.
1495    #[serde(default)]
1496    pub midi_program_changes: Vec<MidiProgramChange>,
1497    /// MIDI key/channel aftertouch events preserved from interchange input, in canonical 480 PPQ ticks.
1498    #[serde(default)]
1499    pub midi_aftertouch: Vec<MidiAftertouch>,
1500    /// MusicXML score-instrument definitions for note-level instrument IDs.
1501    #[serde(default)]
1502    pub percussion_instruments: Vec<PercussionInstrument>,
1503    /// MEI/MuseScore staff-group structure within this part.
1504    #[serde(default)]
1505    pub staff_groups: Vec<StaffGroup>,
1506    /// Optional named instrument semantics. Omitted in older score JSON.
1507    #[serde(default)]
1508    pub instrument: Option<InstrumentDefinition>,
1509}
1510
1511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1512pub struct MidiPitchBend {
1513    pub tick: u64,
1514    pub channel: u8,
1515    /// Signed 14-bit MIDI bend value in the range -8192..=8191.
1516    pub value: i16,
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1520pub struct MidiControlChange {
1521    pub tick: u64,
1522    pub channel: u8,
1523    /// MIDI controller number (0–127).
1524    pub controller: u8,
1525    /// Seven-bit controller value (0–127).
1526    pub value: u8,
1527}
1528
1529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1530pub struct MidiProgramChange {
1531    pub tick: u64,
1532    pub channel: u8,
1533    /// General MIDI program number (0–127).
1534    pub program: u8,
1535}
1536
1537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1538pub struct MidiAftertouch {
1539    pub tick: u64,
1540    pub channel: u8,
1541    /// Key number for key pressure, or `None` for channel pressure.
1542    pub key: Option<u8>,
1543    /// Seven-bit pressure value (0–127).
1544    pub value: u8,
1545}
1546
1547/// An editable percussion-kit mapping associated with a MusicXML score-instrument identity.
1548///
1549/// The MIDI key remains the playback fallback, while the remaining fields
1550/// describe renderer-independent notation defaults. Individual notes may still
1551/// override these defaults.
1552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1553pub struct PercussionInstrument {
1554    /// Stable MusicXML score-instrument identity.
1555    pub id: String,
1556    #[serde(default)]
1557    pub name: Option<String>,
1558    /// General MIDI unpitched key used as the playback fallback.
1559    #[serde(default)]
1560    pub midi_unpitched: Option<u8>,
1561    /// Diatonic staff position relative to the middle line.
1562    #[serde(default)]
1563    pub staff_position: Option<i8>,
1564    /// Default glyph when a note does not supply a renderer-specific override.
1565    #[serde(default)]
1566    pub notehead: Option<NoteHead>,
1567    /// Preferred one-based score voice for newly entered kit notes.
1568    #[serde(default)]
1569    pub preferred_voice: Option<u8>,
1570    /// Named kit-specific variants such as rim-shot or open.
1571    #[serde(default)]
1572    pub techniques: Vec<String>,
1573}
1574
1575impl Part {
1576    pub fn new(name: &str, short_name: &str) -> Self {
1577        Self {
1578            id: Uuid::new_v4().to_string(),
1579            name: name.to_string(),
1580            short_name: short_name.to_string(),
1581            staves: Vec::new(),
1582            midi_channel: 0,
1583            midi_program: 0,
1584            midi_pitch_bends: Vec::new(),
1585            midi_control_changes: Vec::new(),
1586            midi_program_changes: Vec::new(),
1587            midi_aftertouch: Vec::new(),
1588            percussion_instruments: Vec::new(),
1589            staff_groups: Vec::new(),
1590            instrument: None,
1591        }
1592    }
1593
1594    /// Resolve the declared percussion instrument for an unpitched note.
1595    ///
1596    /// An explicit MusicXML `instrument@id` always takes precedence.  When no
1597    /// identifier is attached, the retained display-key MIDI value is matched
1598    /// against the part's declared `midi_unpitched` entries.  This deliberately
1599    /// does not invent a sound identity for an unpitched note with no matching
1600    /// declaration.
1601    pub fn percussion_instrument_for_note(&self, note: &Note) -> Option<&PercussionInstrument> {
1602        if !note.is_unpitched {
1603            return None;
1604        }
1605        if let Some(instrument_id) = note.instrument_id.as_deref() {
1606            return self
1607                .percussion_instruments
1608                .iter()
1609                .find(|instrument| instrument.id == instrument_id);
1610        }
1611        let midi_key = u8::try_from(note.pitches.first()?.to_midi()).ok()?;
1612        self.percussion_instruments
1613            .iter()
1614            .find(|instrument| instrument.midi_unpitched == Some(midi_key))
1615    }
1616}
1617
1618/// The semantic notation mode of a staff, independent of renderer-specific styling.
1619#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1620#[serde(rename_all = "snake_case")]
1621pub enum StaffKind {
1622    #[default]
1623    Standard,
1624    Tablature,
1625    Percussion,
1626}
1627
1628/// A score-level notehead convention selected for an entire staff.
1629#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1630#[serde(rename_all = "snake_case")]
1631pub enum StaffNoteheadScheme {
1632    #[default]
1633    Standard,
1634    PitchNames,
1635    ShapeNotes,
1636}
1637
1638/// How a tablature staff exposes rhythmic duration in addition to fret numbers.
1639#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1640#[serde(rename_all = "snake_case")]
1641pub enum TablatureRhythmDisplay {
1642    /// Fret numbers only; duration remains available in the score model.
1643    #[default]
1644    FretOnly,
1645    /// Add stems and flags to the tablature staff's fret numbers.
1646    Stems,
1647}
1648
1649/// Renderer-independent staff appearance and semantic settings.
1650///
1651/// Values are expressed in staff-space units, never pixels. Existing tablature
1652/// tuning data remains in [`Staff::tablature`]; `kind` records how a renderer
1653/// should interpret the staff.
1654#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1655pub struct StaffPresentation {
1656    #[serde(default)]
1657    pub kind: StaffKind,
1658    #[serde(default = "default_staff_line_count")]
1659    pub lines: u8,
1660    #[serde(default = "default_staff_line_distance")]
1661    pub line_distance: f32,
1662    #[serde(default)]
1663    pub small: bool,
1664    #[serde(default)]
1665    pub cutaway: bool,
1666    #[serde(default = "default_staff_visible")]
1667    pub visible: bool,
1668    #[serde(default)]
1669    pub notehead_scheme: StaffNoteheadScheme,
1670    /// Tablature-only duration presentation. Ignored on non-tablature staves.
1671    #[serde(default)]
1672    pub tablature_rhythm_display: TablatureRhythmDisplay,
1673    /// Tablature fret-label convention. Ignored on non-tablature staves.
1674    #[serde(default)]
1675    pub tablature_fret_mark_style: TablatureFretMarkStyle,
1676}
1677
1678const fn default_staff_line_count() -> u8 {
1679    5
1680}
1681
1682const fn default_staff_line_distance() -> f32 {
1683    1.0
1684}
1685
1686const fn default_staff_visible() -> bool {
1687    true
1688}
1689
1690impl Default for StaffPresentation {
1691    fn default() -> Self {
1692        Self {
1693            kind: StaffKind::Standard,
1694            lines: default_staff_line_count(),
1695            line_distance: default_staff_line_distance(),
1696            small: false,
1697            cutaway: false,
1698            visible: true,
1699            notehead_scheme: StaffNoteheadScheme::Standard,
1700            tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
1701            tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
1702        }
1703    }
1704}
1705
1706/// Glyph convention used to label tablature frets.
1707#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1708#[serde(rename_all = "snake_case")]
1709pub enum TablatureFretMarkStyle {
1710    #[default]
1711    Arabic,
1712    RomanUpper,
1713    RomanLower,
1714}
1715
1716impl StaffPresentation {
1717    fn for_clef(clef: &Clef) -> Self {
1718        let mut presentation = Self::default();
1719        if matches!(clef, Clef::Percussion) {
1720            presentation.kind = StaffKind::Percussion;
1721        }
1722        presentation
1723    }
1724}
1725
1726#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub struct Staff {
1728    pub clef: Clef,
1729    pub measures: Vec<Measure>,
1730    /// Semitones to add to written pitch for concert pitch / MIDI output.
1731    /// -2 = Bb instrument (clarinet, trumpet), -9 = Eb instrument (alto sax), etc.
1732    #[serde(default)]
1733    pub transpose_semitones: i8,
1734    #[serde(default)]
1735    pub tablature: Option<TablatureConfig>,
1736    #[serde(default)]
1737    pub presentation: StaffPresentation,
1738}
1739
1740impl Staff {
1741    pub fn new(clef: Clef) -> Self {
1742        Self {
1743            presentation: StaffPresentation::for_clef(&clef),
1744            clef,
1745            measures: Vec::new(),
1746            transpose_semitones: 0,
1747            tablature: None,
1748        }
1749    }
1750
1751    /// Resolve the tablature tuning and capo active at a physical measure.
1752    ///
1753    /// A measure-local change starts at its own boundary and continues until
1754    /// replaced or cleared. Line-count changes are rejected by validation so
1755    /// the staff geometry remains stable within a system.
1756    pub fn tablature_at(&self, measure_index: usize) -> Option<TablatureConfig> {
1757        let mut active = self.tablature.clone();
1758        for measure in self.measures.iter().take(measure_index.saturating_add(1)) {
1759            if let Some(change) = &measure.tablature_change {
1760                active = Some(change.clone());
1761            }
1762        }
1763        active
1764    }
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1768pub struct VoltaBracket {
1769    /// Ending number (1, 2, …)
1770    pub number: u8,
1771    /// "begin" | "mid" | "end" | "begin_end"
1772    pub kind: String,
1773}
1774
1775/// One of the three mechanically available positions of a concert-harp pedal.
1776#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1777#[serde(rename_all = "kebab-case")]
1778pub enum HarpPedalPosition {
1779    Flat,
1780    #[default]
1781    Natural,
1782    Sharp,
1783}
1784
1785/// A MusicXML-compatible harp-pedal diagram attached to one measure.
1786///
1787/// Positions are always ordered D, C, B, E, F, G, A, which is the conventional two-row
1788/// pedal-diagram order. The optional placement is retained without inventing pixel geometry.
1789#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1790pub struct HarpPedalDiagram {
1791    #[serde(default)]
1792    pub positions: [HarpPedalPosition; 7],
1793    #[serde(default, skip_serializing_if = "Option::is_none")]
1794    pub placement: Option<String>,
1795}
1796
1797impl Default for HarpPedalDiagram {
1798    fn default() -> Self {
1799        Self {
1800            positions: [HarpPedalPosition::Natural; 7],
1801            placement: None,
1802        }
1803    }
1804}
1805
1806/// Authored length of a measure that differs from its time signature: a pickup (anacrusis), an
1807/// incomplete final bar, or any irregular bar, as MuseScore's measure `len="1/4"`. The value is a
1808/// fraction of a whole note, so `1/4` lasts one quarter-note beat.
1809#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1810pub struct MeasureLength {
1811    pub numerator: u32,
1812    pub denominator: u32,
1813}
1814
1815impl MeasureLength {
1816    /// Largest supported measure length, in quarter-note beats.
1817    pub const MAX_BEATS: f64 = 256.0;
1818
1819    /// Length in quarter-note beats, or `None` for a zero, non-finite, or oversized fraction.
1820    pub fn beats(&self) -> Option<f64> {
1821        if self.numerator == 0 || self.denominator == 0 {
1822            return None;
1823        }
1824        let beats = 4.0 * f64::from(self.numerator) / f64::from(self.denominator);
1825        (beats.is_finite() && beats <= Self::MAX_BEATS).then_some(beats)
1826    }
1827
1828    /// Fraction for a length in quarter-note beats, exact for binary, triplet, quintuplet
1829    /// and septuplet subdivisions; `None` for a zero, negative, or oversized length.
1830    pub fn from_beats(beats: f64) -> Option<Self> {
1831        const DIVISIONS: u32 = 3 * 5 * 7 * 64;
1832        if !beats.is_finite() || beats <= 0.0 || beats > Self::MAX_BEATS {
1833            return None;
1834        }
1835        let ticks = (beats * f64::from(DIVISIONS)).round();
1836        (ticks >= 1.0).then(|| Self::from_ticks(ticks as u32, DIVISIONS))?
1837    }
1838
1839    /// Exact fraction for `ticks` at `divisions` ticks per quarter note, in lowest terms.
1840    pub fn from_ticks(ticks: u32, divisions: u32) -> Option<Self> {
1841        let numerator = u64::from(ticks);
1842        let denominator = u64::from(divisions).checked_mul(4)?;
1843        if numerator == 0 || denominator == 0 {
1844            return None;
1845        }
1846        let gcd = {
1847            let (mut a, mut b) = (numerator, denominator);
1848            while b != 0 {
1849                (a, b) = (b, a % b);
1850            }
1851            a
1852        };
1853        let length = Self {
1854            numerator: u32::try_from(numerator / gcd).ok()?,
1855            denominator: u32::try_from(denominator / gcd).ok()?,
1856        };
1857        length.beats().map(|_| length)
1858    }
1859}
1860
1861/// A clef change partway through a staff's measure. `offset` is the time from the start of
1862/// the bar at which the new clef takes effect, as a fraction of a whole note (`1/4` = one
1863/// quarter-note beat). A change at the barline itself is [`Measure::clef`] instead.
1864#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1865pub struct MidMeasureClef {
1866    pub offset: MeasureLength,
1867    pub clef: Clef,
1868}
1869
1870#[derive(Debug, Clone, Serialize, Deserialize)]
1871pub struct Measure {
1872    pub number: u32,
1873    pub time_sig: Option<TimeSignature>,
1874    pub key_sig: Option<KeySignature>,
1875    pub clef: Option<Clef>,
1876    /// Clef changes inside the bar, in increasing `offset` order, each strictly after the
1877    /// bar's start and before its end. Notes from an offset on are read in that clef.
1878    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1879    pub mid_clefs: Vec<MidMeasureClef>,
1880    pub tempo: Option<u16>,
1881    /// Optional target BPM at this measure's end. The playback contract linearly interpolates
1882    /// BPM across the measure; `None` keeps a constant tempo until the next change.
1883    #[serde(default)]
1884    pub tempo_ramp_to: Option<u16>,
1885    /// Instrument semantics beginning at this staff-local measure boundary.
1886    #[serde(default)]
1887    pub instrument_change: Option<InstrumentDefinition>,
1888    /// Tablature tuning/capo beginning at this staff-local measure boundary.
1889    #[serde(default)]
1890    pub tablature_change: Option<TablatureConfig>,
1891    pub barline_left: Barline,
1892    pub barline_right: Barline,
1893    #[serde(default)]
1894    pub volta: Option<VoltaBracket>,
1895    #[serde(default)]
1896    pub tempo_text: Option<String>,
1897    #[serde(default)]
1898    pub rehearsal: Option<String>,
1899    /// Navigation mark: "Segno" | "Coda" | "Fine" | "DaCapo" | "DaCapoAlFine" |
1900    /// "DaCapoAlCoda" | "DalSegno" | "DalSegnoAlFine" | "DalSegnoAlCoda" | "ToCoda"
1901    #[serde(default)]
1902    pub navigation: Option<String>,
1903    /// Expression / performance text ("dolce", "espressivo", "con fuoco", etc.).
1904    #[serde(default)]
1905    pub expression_text: Option<String>,
1906    #[serde(default)]
1907    pub texts: Vec<StyledText>,
1908    /// Structured MusicXML figured-bass figures in source order.
1909    #[serde(default)]
1910    pub figured_bass: Vec<FiguredBassFigure>,
1911    /// Concert-harp pedal diagrams declared by MusicXML `<harp-pedals>` directions.
1912    #[serde(default)]
1913    pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
1914    /// When ≥ 2, this measure is displayed as a multi-measure rest spanning N measures.
1915    #[serde(default)]
1916    pub multi_rest_count: Option<u8>,
1917    /// Force a new system (row) after this measure.
1918    #[serde(default)]
1919    pub system_break: bool,
1920    /// Force a new page after this measure.
1921    #[serde(default)]
1922    pub page_break: bool,
1923    /// Semantic section boundary beginning at this measure. Unlike system and page breaks,
1924    /// this is an editor/navigation marker and has no layout implication.
1925    #[serde(default)]
1926    pub section_break: bool,
1927    /// Up to 4 voices; voice 0 is the primary voice.
1928    pub voices: [Vec<Note>; 4],
1929    /// Original positive MusicXML voice numbers associated with the four editable slots.
1930    ///
1931    /// `None` retains the legacy convention that slot `n` serializes as voice `n + 1`.
1932    /// This lets import preserve sparse source identifiers such as voices 1 and 5 without
1933    /// changing the established fixed-slot editing API.
1934    #[serde(default)]
1935    pub source_voice_numbers: [Option<u32>; 4],
1936    /// Authored length when it differs from the time signature (pickup or irregular bar).
1937    /// `None` means the measure lasts exactly one bar of its time signature.
1938    #[serde(default, skip_serializing_if = "Option::is_none")]
1939    pub actual_length: Option<MeasureLength>,
1940    /// Display this measure as a repeat of the previous `n` measures (MuseScore measure repeat,
1941    /// MusicXML `<measure-repeat>`). Its voices hold a copy of the repeated content, so playback,
1942    /// validation, and editing see real notes; renderers draw the repeat sign instead.
1943    #[serde(default, skip_serializing_if = "Option::is_none")]
1944    pub measure_repeat: Option<u8>,
1945}
1946
1947impl Measure {
1948    /// The clef in effect `beats` quarter-note beats into this bar, given the clef in effect
1949    /// at its start (after [`Measure::clef`]): the latest mid-bar change at or before `beats`.
1950    pub fn clef_at(&self, start_clef: &Clef, beats: f64) -> Clef {
1951        self.mid_clefs
1952            .iter()
1953            .take_while(|change| {
1954                change
1955                    .offset
1956                    .beats()
1957                    .is_some_and(|offset| offset <= beats + 1e-9)
1958            })
1959            .last()
1960            .map_or_else(|| start_clef.clone(), |change| change.clef.clone())
1961    }
1962
1963    pub fn empty(numerator: u8, denominator: u8) -> Self {
1964        let total_beats = TimeSignature {
1965            numerator,
1966            denominator,
1967        }
1968        .total_beats();
1969        let mut voice0: Vec<Note> = Vec::new();
1970        let mut remaining = total_beats;
1971        while remaining > 1e-9 {
1972            let dur = Duration::whole_filling_beats(remaining);
1973            remaining -= dur.beats(0);
1974            voice0.push(Note::rest(dur));
1975        }
1976        Self {
1977            number: 0,
1978            time_sig: None,
1979            key_sig: None,
1980            clef: None,
1981            mid_clefs: Vec::new(),
1982            tempo: None,
1983            tempo_ramp_to: None,
1984            instrument_change: None,
1985            tablature_change: None,
1986            barline_left: Barline::Normal,
1987            barline_right: Barline::Normal,
1988            volta: None,
1989            tempo_text: None,
1990            rehearsal: None,
1991            navigation: None,
1992            expression_text: None,
1993            texts: Vec::new(),
1994            figured_bass: Vec::new(),
1995            harp_pedal_diagrams: Vec::new(),
1996            multi_rest_count: None,
1997            system_break: false,
1998            page_break: false,
1999            section_break: false,
2000            voices: [voice0, vec![], vec![], vec![]],
2001            source_voice_numbers: [None; 4],
2002            actual_length: None,
2003            measure_repeat: None,
2004        }
2005    }
2006
2007    /// Replace this measure's voices with a playable copy of `source`, as a measure repeat
2008    /// stores its repeated content; see [`Note::repeated_copy`].
2009    pub fn repeat_content_from(&mut self, source: &Measure) {
2010        for (target, voice) in self.voices.iter_mut().zip(&source.voices) {
2011            *target = voice.iter().map(Note::repeated_copy).collect();
2012        }
2013        self.source_voice_numbers = source.source_voice_numbers;
2014    }
2015
2016    /// Whether this measure's voices sound like `other`'s: the same rests, pitches, durations,
2017    /// dots, and tuplets in every voice.
2018    pub fn same_sounding_content(&self, other: &Measure) -> bool {
2019        self.voices.iter().zip(&other.voices).all(|(left, right)| {
2020            left.len() == right.len()
2021                && left.iter().zip(right).all(|(a, b)| {
2022                    a.is_rest == b.is_rest
2023                        && a.pitches == b.pitches
2024                        && a.duration == b.duration
2025                        && a.dot_count == b.dot_count
2026                        && a.tuplet == b.tuplet
2027                        && a.is_grace == b.is_grace
2028                })
2029        })
2030    }
2031
2032    /// Beats this measure lasts: its [`actual_length`](Self::actual_length) when present and
2033    /// valid, otherwise its own time signature, otherwise `time_signature` (the one in effect).
2034    pub fn duration_beats(&self, time_signature: &TimeSignature) -> f64 {
2035        self.actual_length
2036            .and_then(|length| length.beats())
2037            .unwrap_or_else(|| {
2038                self.time_sig
2039                    .as_ref()
2040                    .unwrap_or(time_signature)
2041                    .total_beats()
2042            })
2043    }
2044
2045    pub fn renumber(&mut self, n: u32) {
2046        self.number = n;
2047    }
2048}
2049
2050#[derive(Debug, Clone, Serialize, Deserialize)]
2051pub struct Note {
2052    pub id: String,
2053    pub is_rest: bool,
2054    /// MusicXML unpitched note; `pitches` then stores display placement only.
2055    #[serde(default)]
2056    pub is_unpitched: bool,
2057    /// Optional source instrument identifier (for example MusicXML note-level `instrument@id`).
2058    #[serde(default)]
2059    pub instrument_id: Option<String>,
2060    /// Horizontal nudge in tenths from the note's computed (rhythmic) position, set by hosts.
2061    /// MusicXML import does not fill it from `default-x`, which is an absolute position in the
2062    /// source engraver's layout; MusicXML export writes it as part of `relative-x`.
2063    #[serde(default)]
2064    pub offset_x: Option<f64>,
2065    /// Vertical nudge in tenths from the note's staff position; see `offset_x`.
2066    #[serde(default)]
2067    pub offset_y: Option<f64>,
2068    /// MusicXML note-level horizontal adjustment in tenths.
2069    #[serde(default)]
2070    pub relative_x: Option<f64>,
2071    /// MusicXML note-level vertical adjustment in tenths.
2072    #[serde(default)]
2073    pub relative_y: Option<f64>,
2074    /// Single note: one pitch. Chord: multiple pitches (same duration).
2075    pub pitches: Vec<Pitch>,
2076    #[serde(default)]
2077    pub tab_position: Option<super::notation::TabPosition>,
2078    /// One tablature position per pitch; the first entry mirrors `tab_position`.
2079    /// This is populated for chords so each pitch can occupy a distinct string.
2080    #[serde(default)]
2081    pub tab_positions: Vec<super::notation::TabPosition>,
2082    pub duration: Duration,
2083    pub dot_count: u8,
2084    pub tie_start: bool,
2085    pub tie_end: bool,
2086    /// Per-pitch tie starts, parallel to `pitches`, for a chord whose notes are not all tied.
2087    /// Empty (the usual case) means `tie_start` applies to every pitch. A vector whose length
2088    /// differs from `pitches` is ignored in favour of `tie_start`; see [`Note::pitch_tie_start`].
2089    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2090    pub pitch_tie_starts: Vec<bool>,
2091    /// Per-pitch tie ends, parallel to `pitches`; empty means `tie_end` applies to every pitch.
2092    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2093    pub pitch_tie_ends: Vec<bool>,
2094    pub beam: BeamState,
2095    pub articulations: Vec<Articulation>,
2096    pub dynamic: Option<Dynamic>,
2097    pub stem_up: Option<bool>,
2098    #[serde(default)]
2099    pub hairpin_start: Option<HairpinKind>,
2100    #[serde(default)]
2101    pub hairpin_end: bool,
2102    #[serde(default)]
2103    pub tuplet: Option<TupletInfo>,
2104    #[serde(default)]
2105    pub chord_symbol: Option<ChordSymbol>,
2106    #[serde(default)]
2107    pub is_grace: bool,
2108    /// Acciaccatura: true (slash through stem). Appoggiatura: false.
2109    #[serde(default)]
2110    pub grace_slash: bool,
2111    #[serde(default)]
2112    pub ottava_start: Option<OttavaKind>,
2113    #[serde(default)]
2114    pub ottava_end: bool,
2115    #[serde(default)]
2116    pub lyric: Option<Lyric>,
2117    /// Lyrics for verse 2 and later, in ascending verse order; verse 1 is [`lyric`](Self::lyric).
2118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2119    pub additional_lyrics: Vec<VerseLyric>,
2120    #[serde(default)]
2121    pub pedal_start: bool,
2122    #[serde(default)]
2123    pub pedal_end: bool,
2124    #[serde(default)]
2125    pub slur_start: bool,
2126    #[serde(default)]
2127    pub slur_end: bool,
2128    /// Arpeggiate direction: `Some(true)` = up, `Some(false)` = down, `None` = none.
2129    #[serde(default)]
2130    pub arpeggiate: Option<bool>,
2131    /// Technique/style instruction attached to this note ("pizz.", "arco", "con sord.", etc.).
2132    #[serde(default)]
2133    pub technique_text: Option<String>,
2134    #[serde(default)]
2135    pub glissando_start: bool,
2136    #[serde(default)]
2137    pub glissando_end: bool,
2138    #[serde(default)]
2139    pub cross_staff: Option<CrossStaff>,
2140    /// Left-hand fingering number (0 = open / thumb, 1–5 = fingers).
2141    #[serde(default)]
2142    pub fingering: Option<u8>,
2143    /// Alternate left-hand fingering candidates, in source order. The first
2144    /// entry mirrors `fingering` when present.
2145    #[serde(default)]
2146    pub fingerings: Vec<u8>,
2147    /// String number for plucked/bowed string instruments (1 = highest string).
2148    #[serde(default)]
2149    pub string_number: Option<u8>,
2150    #[serde(default)]
2151    pub note_head: NoteHead,
2152    /// Cue note (small-sized, does not count toward beat total).
2153    #[serde(default)]
2154    pub is_cue: bool,
2155    /// Start of a multi-note trill line span.
2156    #[serde(default)]
2157    pub trill_line_start: bool,
2158    /// End of a multi-note trill line span.
2159    #[serde(default)]
2160    pub trill_line_end: bool,
2161    /// Guitar-specific playing technique (bend, slide, hammer-on, pull-off).
2162    #[serde(default)]
2163    pub guitar_technique: Option<GuitarTechnique>,
2164    /// MusicXML bend amount in cents when supplied by the source.
2165    #[serde(default)]
2166    pub guitar_bend_alter_cents: Option<i16>,
2167    /// Ordered bend curve points. Position is relative note time in per-mille (0..=1000).
2168    #[serde(default)]
2169    pub guitar_bend_curve: Vec<GuitarBendPoint>,
2170}
2171
2172/// One point of an authored guitar bend curve.
2173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2174pub struct GuitarBendPoint {
2175    pub position_per_mille: u16,
2176    pub alter_cents: i16,
2177}
2178
2179impl Note {
2180    /// Select one authored fingering candidate without changing the score.
2181    pub fn select_fingering(
2182        &self,
2183        policy: super::notation::FingeringSelectionPolicy,
2184    ) -> Option<u8> {
2185        let candidates = if self.fingerings.is_empty() {
2186            self.fingering.into_iter().collect::<Vec<_>>()
2187        } else {
2188            self.fingerings.clone()
2189        };
2190        match policy {
2191            super::notation::FingeringSelectionPolicy::SourceOrder => candidates.first().copied(),
2192            super::notation::FingeringSelectionPolicy::LowestNumber => {
2193                candidates.iter().copied().min()
2194            }
2195            super::notation::FingeringSelectionPolicy::HighestNumber => {
2196                candidates.iter().copied().max()
2197            }
2198        }
2199    }
2200
2201    pub fn new(pitch: Pitch, duration: Duration) -> Self {
2202        Self {
2203            id: Uuid::new_v4().to_string(),
2204            is_rest: false,
2205            is_unpitched: false,
2206            instrument_id: None,
2207            offset_x: None,
2208            offset_y: None,
2209            relative_x: None,
2210            relative_y: None,
2211            pitches: vec![pitch],
2212            tab_position: None,
2213            tab_positions: Vec::new(),
2214            duration,
2215            dot_count: 0,
2216            tie_start: false,
2217            tie_end: false,
2218            pitch_tie_starts: Vec::new(),
2219            pitch_tie_ends: Vec::new(),
2220            beam: BeamState::None,
2221            articulations: Vec::new(),
2222            dynamic: None,
2223            stem_up: None,
2224            hairpin_start: None,
2225            hairpin_end: false,
2226            tuplet: None,
2227            chord_symbol: None,
2228            is_grace: false,
2229            grace_slash: false,
2230            ottava_start: None,
2231            ottava_end: false,
2232            lyric: None,
2233            additional_lyrics: Vec::new(),
2234            pedal_start: false,
2235            pedal_end: false,
2236            slur_start: false,
2237            slur_end: false,
2238            arpeggiate: None,
2239            technique_text: None,
2240            glissando_start: false,
2241            glissando_end: false,
2242            cross_staff: None,
2243            fingering: None,
2244            fingerings: Vec::new(),
2245            string_number: None,
2246            note_head: NoteHead::Normal,
2247            is_cue: false,
2248            trill_line_start: false,
2249            trill_line_end: false,
2250            guitar_technique: None,
2251            guitar_bend_alter_cents: None,
2252            guitar_bend_curve: Vec::new(),
2253        }
2254    }
2255
2256    pub fn rest(duration: Duration) -> Self {
2257        Self {
2258            id: Uuid::new_v4().to_string(),
2259            is_rest: true,
2260            is_unpitched: false,
2261            instrument_id: None,
2262            offset_x: None,
2263            offset_y: None,
2264            relative_x: None,
2265            relative_y: None,
2266            pitches: Vec::new(),
2267            tab_position: None,
2268            tab_positions: Vec::new(),
2269            duration,
2270            dot_count: 0,
2271            tie_start: false,
2272            tie_end: false,
2273            pitch_tie_starts: Vec::new(),
2274            pitch_tie_ends: Vec::new(),
2275            beam: BeamState::None,
2276            articulations: Vec::new(),
2277            dynamic: None,
2278            stem_up: None,
2279            hairpin_start: None,
2280            hairpin_end: false,
2281            tuplet: None,
2282            chord_symbol: None,
2283            is_grace: false,
2284            grace_slash: false,
2285            ottava_start: None,
2286            ottava_end: false,
2287            lyric: None,
2288            additional_lyrics: Vec::new(),
2289            pedal_start: false,
2290            pedal_end: false,
2291            slur_start: false,
2292            slur_end: false,
2293            arpeggiate: None,
2294            technique_text: None,
2295            glissando_start: false,
2296            glissando_end: false,
2297            cross_staff: None,
2298            fingering: None,
2299            fingerings: Vec::new(),
2300            string_number: None,
2301            note_head: NoteHead::Normal,
2302            is_cue: false,
2303            trill_line_start: false,
2304            trill_line_end: false,
2305            guitar_technique: None,
2306            guitar_bend_alter_cents: None,
2307            guitar_bend_curve: Vec::new(),
2308        }
2309    }
2310
2311    /// Copy for a repeated measure: a fresh id and the same sounding content, without ties,
2312    /// slurs, lines, lyrics, chord symbol, or dynamic, which belong to the written original.
2313    pub fn repeated_copy(&self) -> Note {
2314        let mut copy = self.clone();
2315        copy.id = Uuid::new_v4().to_string();
2316        copy.tie_start = false;
2317        copy.tie_end = false;
2318        copy.slur_start = false;
2319        copy.slur_end = false;
2320        copy.hairpin_start = None;
2321        copy.hairpin_end = false;
2322        copy.pedal_start = false;
2323        copy.pedal_end = false;
2324        copy.ottava_start = None;
2325        copy.ottava_end = false;
2326        copy.glissando_start = false;
2327        copy.glissando_end = false;
2328        copy.trill_line_start = false;
2329        copy.trill_line_end = false;
2330        copy.lyric = None;
2331        copy.additional_lyrics.clear();
2332        copy.chord_symbol = None;
2333        copy.dynamic = None;
2334        copy
2335    }
2336
2337    /// Whether the pitch at `index` starts a tie: its own flag when per-pitch ties are recorded,
2338    /// otherwise the chord-level `tie_start`.
2339    pub fn pitch_tie_start(&self, index: usize) -> bool {
2340        per_pitch_flag(
2341            &self.pitch_tie_starts,
2342            self.pitches.len(),
2343            index,
2344            self.tie_start,
2345        )
2346    }
2347
2348    /// Whether the pitch at `index` ends a tie; see [`Note::pitch_tie_start`].
2349    pub fn pitch_tie_end(&self, index: usize) -> bool {
2350        per_pitch_flag(
2351            &self.pitch_tie_ends,
2352            self.pitches.len(),
2353            index,
2354            self.tie_end,
2355        )
2356    }
2357
2358    /// Record which pitches of this note start (`starts`) and end (`ends`) a tie. The chord-level
2359    /// flags become "any pitch", and per-pitch vectors are kept only when the pitches differ, so a
2360    /// uniformly tied chord stays in the plain form. Slices whose length is not the pitch count
2361    /// are ignored.
2362    pub fn set_pitch_ties(&mut self, starts: &[bool], ends: &[bool]) {
2363        let count = self.pitches.len();
2364        if starts.len() == count {
2365            self.tie_start = starts.iter().any(|tied| *tied);
2366            self.pitch_tie_starts = if starts.iter().all(|tied| *tied == self.tie_start) {
2367                Vec::new()
2368            } else {
2369                starts.to_vec()
2370            };
2371        }
2372        if ends.len() == count {
2373            self.tie_end = ends.iter().any(|tied| *tied);
2374            self.pitch_tie_ends = if ends.iter().all(|tied| *tied == self.tie_end) {
2375                Vec::new()
2376            } else {
2377                ends.to_vec()
2378            };
2379        }
2380    }
2381
2382    /// Per-pitch tie starts as a full vector (chord-level flag repeated when not recorded).
2383    pub fn pitch_tie_starts_or_uniform(&self) -> Vec<bool> {
2384        (0..self.pitches.len())
2385            .map(|index| self.pitch_tie_start(index))
2386            .collect()
2387    }
2388
2389    /// Per-pitch tie ends as a full vector (chord-level flag repeated when not recorded).
2390    pub fn pitch_tie_ends_or_uniform(&self) -> Vec<bool> {
2391        (0..self.pitches.len())
2392            .map(|index| self.pitch_tie_end(index))
2393            .collect()
2394    }
2395
2396    /// A plain whole rest, the form MusicXML `<rest measure="yes"/>` and MuseScore
2397    /// `durationType=measure` import to. Alone in a voice it is a measure rest; see
2398    /// [`voice_duration_beats`].
2399    pub fn is_plain_whole_rest(&self) -> bool {
2400        self.is_rest
2401            && matches!(self.duration, Duration::Whole)
2402            && self.dot_count == 0
2403            && self.tuplet.is_none()
2404            && !self.is_grace
2405            && !self.is_cue
2406    }
2407
2408    pub fn beats(&self) -> f64 {
2409        if self.is_grace || self.is_cue {
2410            return 0.0;
2411        }
2412        let base = self.duration.beats(self.dot_count);
2413        if let Some(ref t) = self.tuplet {
2414            base * (t.normal_notes as f64) / (t.actual_notes as f64)
2415        } else {
2416            base
2417        }
2418    }
2419}
2420
2421impl Duration {
2422    /// Returns the largest single duration that fills the given number of beats.
2423    pub fn whole_filling_beats(beats: f64) -> Duration {
2424        if beats >= 4.0 {
2425            Duration::Whole
2426        } else if beats >= 2.0 {
2427            Duration::Half
2428        } else if beats >= 1.0 {
2429            Duration::Quarter
2430        } else if beats >= 0.5 {
2431            Duration::Eighth
2432        } else if beats >= 0.25 {
2433            Duration::Sixteenth
2434        } else if beats >= 0.125 {
2435            Duration::ThirtySecond
2436        } else {
2437            Duration::SixtyFourth
2438        }
2439    }
2440}
2441
2442// ── NoteAddr ──────────────────────────────────────────────────────────────────
2443
2444/// Physical address of a note within a score.
2445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2446pub struct NoteAddr {
2447    pub part: usize,
2448    pub staff: usize,
2449    pub measure: usize,
2450    pub voice: usize,
2451    pub note: usize,
2452}
2453
2454// ── diff ──────────────────────────────────────────────────────────────────────
2455
2456/// A single change between two [`Score`] values as reported by [`diff`].
2457#[derive(Debug, Clone, Serialize, Deserialize)]
2458pub enum ScoreChange {
2459    ScoreViewsChanged {
2460        old: Vec<ScoreView>,
2461        new: Vec<ScoreView>,
2462    },
2463    ScoreTextChanged {
2464        old: Vec<StyledText>,
2465        new: Vec<StyledText>,
2466    },
2467    MeasureTextChanged {
2468        part: usize,
2469        staff: usize,
2470        measure: usize,
2471        old: Vec<StyledText>,
2472        new: Vec<StyledText>,
2473    },
2474    FiguredBassChanged {
2475        part: usize,
2476        staff: usize,
2477        measure: usize,
2478        old: Vec<FiguredBassFigure>,
2479        new: Vec<FiguredBassFigure>,
2480    },
2481    HarpPedalDiagramsChanged {
2482        part: usize,
2483        staff: usize,
2484        measure: usize,
2485        old: Vec<HarpPedalDiagram>,
2486        new: Vec<HarpPedalDiagram>,
2487    },
2488    ChordDefinitionsChanged {
2489        old: Vec<ChordDefinition>,
2490        new: Vec<ChordDefinition>,
2491    },
2492    PartNamesChanged {
2493        part: usize,
2494        old_name: String,
2495        new_name: String,
2496        old_short_name: String,
2497        new_short_name: String,
2498    },
2499    PartMidiChanged {
2500        part: usize,
2501        old_channel: u8,
2502        new_channel: u8,
2503        old_program: u8,
2504        new_program: u8,
2505    },
2506    InstrumentDefinitionChanged {
2507        part: usize,
2508        old: Option<InstrumentDefinition>,
2509        new: Option<InstrumentDefinition>,
2510    },
2511    PartMidiAutomationChanged {
2512        part: usize,
2513        old_pitch_bends: Vec<MidiPitchBend>,
2514        new_pitch_bends: Vec<MidiPitchBend>,
2515        old_control_changes: Vec<MidiControlChange>,
2516        new_control_changes: Vec<MidiControlChange>,
2517        old_program_changes: Vec<MidiProgramChange>,
2518        new_program_changes: Vec<MidiProgramChange>,
2519        old_aftertouch: Vec<MidiAftertouch>,
2520        new_aftertouch: Vec<MidiAftertouch>,
2521    },
2522    StaffConfigurationChanged {
2523        part: usize,
2524        staff: usize,
2525        old_clef: Clef,
2526        new_clef: Clef,
2527        old_transpose_semitones: i8,
2528        new_transpose_semitones: i8,
2529    },
2530    StaffPresentationChanged {
2531        part: usize,
2532        staff: usize,
2533        old: StaffPresentation,
2534        new: StaffPresentation,
2535    },
2536    MeasurePresentationChanged {
2537        part: usize,
2538        staff: usize,
2539        measure: usize,
2540        old_number: u32,
2541        new_number: u32,
2542        old_clef: Option<Clef>,
2543        new_clef: Option<Clef>,
2544        old_tempo_text: Option<String>,
2545        new_tempo_text: Option<String>,
2546        old_navigation: Option<String>,
2547        new_navigation: Option<String>,
2548        old_expression_text: Option<String>,
2549        new_expression_text: Option<String>,
2550        old_multi_rest_count: Option<u8>,
2551        new_multi_rest_count: Option<u8>,
2552        old_system_break: bool,
2553        new_system_break: bool,
2554        old_page_break: bool,
2555        new_page_break: bool,
2556        old_section_break: bool,
2557        new_section_break: bool,
2558    },
2559    TablatureConfigChanged {
2560        part: usize,
2561        staff: usize,
2562        old: Option<TablatureConfig>,
2563        new: Option<TablatureConfig>,
2564    },
2565    TablatureChangeChanged {
2566        part: usize,
2567        staff: usize,
2568        measure: usize,
2569        old: Option<TablatureConfig>,
2570        new: Option<TablatureConfig>,
2571    },
2572    MidMeasureClefsChanged {
2573        part: usize,
2574        staff: usize,
2575        measure: usize,
2576        old: Vec<MidMeasureClef>,
2577        new: Vec<MidMeasureClef>,
2578    },
2579    /// A semantic field changed without a dedicated positional diff variant.
2580    ///
2581    /// The stable path keeps compatibility reports honest while the complete score remains
2582    /// available through [`ScorePatch::ReplaceScore`].
2583    UnrepresentedFieldChanged {
2584        path: String,
2585    },
2586    MetadataChanged {
2587        field: String,
2588        old: String,
2589        new: String,
2590    },
2591    TempoChanged {
2592        old: u16,
2593        new: u16,
2594    },
2595    KeySignatureChanged {
2596        old: KeySignature,
2597        new: KeySignature,
2598    },
2599    PartAdded {
2600        part_index: usize,
2601    },
2602    PartRemoved {
2603        part_index: usize,
2604        name: String,
2605    },
2606    NoteAdded {
2607        part: usize,
2608        staff: usize,
2609        measure: usize,
2610        voice: usize,
2611        note_index: usize,
2612    },
2613    NoteRemoved {
2614        part: usize,
2615        staff: usize,
2616        measure: usize,
2617        voice: usize,
2618        note: Box<Note>,
2619    },
2620    NoteModified {
2621        part: usize,
2622        staff: usize,
2623        measure: usize,
2624        voice: usize,
2625        note_index: usize,
2626        old: Box<Note>,
2627        new: Box<Note>,
2628    },
2629    TimeSigChanged {
2630        part: usize,
2631        staff: usize,
2632        measure: usize,
2633        old: Option<TimeSignature>,
2634        new: Option<TimeSignature>,
2635    },
2636    MeasureTempoChanged {
2637        part: usize,
2638        staff: usize,
2639        measure: usize,
2640        old: Option<u16>,
2641        new: Option<u16>,
2642    },
2643    MeasureTempoRampChanged {
2644        part: usize,
2645        staff: usize,
2646        measure: usize,
2647        old: Option<u16>,
2648        new: Option<u16>,
2649    },
2650    BarlineChanged {
2651        part: usize,
2652        staff: usize,
2653        measure: usize,
2654    },
2655    RehearsalMarkChanged {
2656        part: usize,
2657        staff: usize,
2658        measure: usize,
2659        old: Option<String>,
2660        new: Option<String>,
2661    },
2662    VoltaChanged {
2663        part: usize,
2664        staff: usize,
2665        measure: usize,
2666    },
2667}
2668
2669/// Compare two scores and return a list of differences.
2670///
2671/// Parts, staves, measures, and voices are compared by position. Notes are compared by
2672/// position within each voice, ignoring their `id` field. Metadata fields are compared
2673/// individually.
2674pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
2675    let mut changes: Vec<ScoreChange> = Vec::new();
2676
2677    if a.views != b.views {
2678        changes.push(ScoreChange::ScoreViewsChanged {
2679            old: a.views.clone(),
2680            new: b.views.clone(),
2681        });
2682    }
2683
2684    if a.texts != b.texts {
2685        changes.push(ScoreChange::ScoreTextChanged {
2686            old: a.texts.clone(),
2687            new: b.texts.clone(),
2688        });
2689    }
2690    if a.chord_definitions != b.chord_definitions {
2691        changes.push(ScoreChange::ChordDefinitionsChanged {
2692            old: a.chord_definitions.clone(),
2693            new: b.chord_definitions.clone(),
2694        });
2695    }
2696    if let Some(path) = first_unrepresented_field_change(a, b) {
2697        changes.push(ScoreChange::UnrepresentedFieldChanged { path });
2698    }
2699
2700    macro_rules! meta {
2701        ($field:ident, $name:literal) => {
2702            if a.metadata.$field != b.metadata.$field {
2703                changes.push(ScoreChange::MetadataChanged {
2704                    field: $name.to_string(),
2705                    old: a.metadata.$field.clone(),
2706                    new: b.metadata.$field.clone(),
2707                });
2708            }
2709        };
2710    }
2711    meta!(title, "title");
2712    meta!(composer, "composer");
2713    meta!(lyricist, "lyricist");
2714    meta!(copyright, "copyright");
2715    meta!(work_number, "work_number");
2716    meta!(movement_title, "movement_title");
2717
2718    if a.settings.tempo_bpm != b.settings.tempo_bpm {
2719        changes.push(ScoreChange::TempoChanged {
2720            old: a.settings.tempo_bpm,
2721            new: b.settings.tempo_bpm,
2722        });
2723    }
2724    if a.settings.key_signature != b.settings.key_signature {
2725        changes.push(ScoreChange::KeySignatureChanged {
2726            old: a.settings.key_signature.clone(),
2727            new: b.settings.key_signature.clone(),
2728        });
2729    }
2730
2731    let a_len = a.parts.len();
2732    let b_len = b.parts.len();
2733    for i in b_len..a_len {
2734        changes.push(ScoreChange::PartRemoved {
2735            part_index: i,
2736            name: a.parts[i].name.clone(),
2737        });
2738    }
2739    for i in a_len..b_len {
2740        changes.push(ScoreChange::PartAdded { part_index: i });
2741    }
2742
2743    for pi in 0..a_len.min(b_len) {
2744        let ap = &a.parts[pi];
2745        let bp = &b.parts[pi];
2746        if ap.name != bp.name || ap.short_name != bp.short_name {
2747            changes.push(ScoreChange::PartNamesChanged {
2748                part: pi,
2749                old_name: ap.name.clone(),
2750                new_name: bp.name.clone(),
2751                old_short_name: ap.short_name.clone(),
2752                new_short_name: bp.short_name.clone(),
2753            });
2754        }
2755        if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
2756            changes.push(ScoreChange::PartMidiChanged {
2757                part: pi,
2758                old_channel: ap.midi_channel,
2759                new_channel: bp.midi_channel,
2760                old_program: ap.midi_program,
2761                new_program: bp.midi_program,
2762            });
2763        }
2764        if ap.instrument != bp.instrument {
2765            changes.push(ScoreChange::InstrumentDefinitionChanged {
2766                part: pi,
2767                old: ap.instrument.clone(),
2768                new: bp.instrument.clone(),
2769            });
2770        }
2771        if ap.midi_pitch_bends != bp.midi_pitch_bends
2772            || ap.midi_control_changes != bp.midi_control_changes
2773            || ap.midi_program_changes != bp.midi_program_changes
2774            || ap.midi_aftertouch != bp.midi_aftertouch
2775        {
2776            changes.push(ScoreChange::PartMidiAutomationChanged {
2777                part: pi,
2778                old_pitch_bends: ap.midi_pitch_bends.clone(),
2779                new_pitch_bends: bp.midi_pitch_bends.clone(),
2780                old_control_changes: ap.midi_control_changes.clone(),
2781                new_control_changes: bp.midi_control_changes.clone(),
2782                old_program_changes: ap.midi_program_changes.clone(),
2783                new_program_changes: bp.midi_program_changes.clone(),
2784                old_aftertouch: ap.midi_aftertouch.clone(),
2785                new_aftertouch: bp.midi_aftertouch.clone(),
2786            });
2787        }
2788        for si in 0..ap.staves.len().min(bp.staves.len()) {
2789            let a_staff = &ap.staves[si];
2790            let b_staff = &bp.staves[si];
2791            if a_staff.clef != b_staff.clef
2792                || a_staff.transpose_semitones != b_staff.transpose_semitones
2793            {
2794                changes.push(ScoreChange::StaffConfigurationChanged {
2795                    part: pi,
2796                    staff: si,
2797                    old_clef: a_staff.clef.clone(),
2798                    new_clef: b_staff.clef.clone(),
2799                    old_transpose_semitones: a_staff.transpose_semitones,
2800                    new_transpose_semitones: b_staff.transpose_semitones,
2801                });
2802            }
2803            if a_staff.tablature != b_staff.tablature {
2804                changes.push(ScoreChange::TablatureConfigChanged {
2805                    part: pi,
2806                    staff: si,
2807                    old: a_staff.tablature.clone(),
2808                    new: b_staff.tablature.clone(),
2809                });
2810            }
2811            if a_staff.presentation != b_staff.presentation {
2812                changes.push(ScoreChange::StaffPresentationChanged {
2813                    part: pi,
2814                    staff: si,
2815                    old: a_staff.presentation.clone(),
2816                    new: b_staff.presentation.clone(),
2817                });
2818            }
2819            for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
2820                let am = &a_staff.measures[mi];
2821                let bm = &b_staff.measures[mi];
2822                if am.tablature_change != bm.tablature_change {
2823                    changes.push(ScoreChange::TablatureChangeChanged {
2824                        part: pi,
2825                        staff: si,
2826                        measure: mi,
2827                        old: am.tablature_change.clone(),
2828                        new: bm.tablature_change.clone(),
2829                    });
2830                }
2831                if am.mid_clefs != bm.mid_clefs {
2832                    changes.push(ScoreChange::MidMeasureClefsChanged {
2833                        part: pi,
2834                        staff: si,
2835                        measure: mi,
2836                        old: am.mid_clefs.clone(),
2837                        new: bm.mid_clefs.clone(),
2838                    });
2839                }
2840                if am.number != bm.number
2841                    || am.clef != bm.clef
2842                    || am.tempo_text != bm.tempo_text
2843                    || am.navigation != bm.navigation
2844                    || am.expression_text != bm.expression_text
2845                    || am.multi_rest_count != bm.multi_rest_count
2846                    || am.system_break != bm.system_break
2847                    || am.page_break != bm.page_break
2848                    || am.section_break != bm.section_break
2849                {
2850                    changes.push(ScoreChange::MeasurePresentationChanged {
2851                        part: pi,
2852                        staff: si,
2853                        measure: mi,
2854                        old_number: am.number,
2855                        new_number: bm.number,
2856                        old_clef: am.clef.clone(),
2857                        new_clef: bm.clef.clone(),
2858                        old_tempo_text: am.tempo_text.clone(),
2859                        new_tempo_text: bm.tempo_text.clone(),
2860                        old_navigation: am.navigation.clone(),
2861                        new_navigation: bm.navigation.clone(),
2862                        old_expression_text: am.expression_text.clone(),
2863                        new_expression_text: bm.expression_text.clone(),
2864                        old_multi_rest_count: am.multi_rest_count,
2865                        new_multi_rest_count: bm.multi_rest_count,
2866                        old_system_break: am.system_break,
2867                        new_system_break: bm.system_break,
2868                        old_page_break: am.page_break,
2869                        new_page_break: bm.page_break,
2870                        old_section_break: am.section_break,
2871                        new_section_break: bm.section_break,
2872                    });
2873                }
2874                for vi in 0..4usize {
2875                    let av = &am.voices[vi];
2876                    let bv = &bm.voices[vi];
2877                    for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
2878                        if !note_content_eq(a_note, b_note) {
2879                            changes.push(ScoreChange::NoteModified {
2880                                part: pi,
2881                                staff: si,
2882                                measure: mi,
2883                                voice: vi,
2884                                note_index: ni,
2885                                old: Box::new(a_note.clone()),
2886                                new: Box::new(b_note.clone()),
2887                            });
2888                        }
2889                    }
2890                    for note in av.iter().skip(bv.len()) {
2891                        changes.push(ScoreChange::NoteRemoved {
2892                            part: pi,
2893                            staff: si,
2894                            measure: mi,
2895                            voice: vi,
2896                            note: Box::new(note.clone()),
2897                        });
2898                    }
2899                    for ni in av.len()..bv.len() {
2900                        changes.push(ScoreChange::NoteAdded {
2901                            part: pi,
2902                            staff: si,
2903                            measure: mi,
2904                            voice: vi,
2905                            note_index: ni,
2906                        });
2907                    }
2908                }
2909                if am.time_sig != bm.time_sig {
2910                    changes.push(ScoreChange::TimeSigChanged {
2911                        part: pi,
2912                        staff: si,
2913                        measure: mi,
2914                        old: am.time_sig.clone(),
2915                        new: bm.time_sig.clone(),
2916                    });
2917                }
2918                if am.tempo != bm.tempo {
2919                    changes.push(ScoreChange::MeasureTempoChanged {
2920                        part: pi,
2921                        staff: si,
2922                        measure: mi,
2923                        old: am.tempo,
2924                        new: bm.tempo,
2925                    });
2926                }
2927                if am.tempo_ramp_to != bm.tempo_ramp_to {
2928                    changes.push(ScoreChange::MeasureTempoRampChanged {
2929                        part: pi,
2930                        staff: si,
2931                        measure: mi,
2932                        old: am.tempo_ramp_to,
2933                        new: bm.tempo_ramp_to,
2934                    });
2935                }
2936                if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
2937                    changes.push(ScoreChange::BarlineChanged {
2938                        part: pi,
2939                        staff: si,
2940                        measure: mi,
2941                    });
2942                }
2943                if am.rehearsal != bm.rehearsal {
2944                    changes.push(ScoreChange::RehearsalMarkChanged {
2945                        part: pi,
2946                        staff: si,
2947                        measure: mi,
2948                        old: am.rehearsal.clone(),
2949                        new: bm.rehearsal.clone(),
2950                    });
2951                }
2952                if am.volta != bm.volta {
2953                    changes.push(ScoreChange::VoltaChanged {
2954                        part: pi,
2955                        staff: si,
2956                        measure: mi,
2957                    });
2958                }
2959                if am.texts != bm.texts {
2960                    changes.push(ScoreChange::MeasureTextChanged {
2961                        part: pi,
2962                        staff: si,
2963                        measure: mi,
2964                        old: am.texts.clone(),
2965                        new: bm.texts.clone(),
2966                    });
2967                }
2968                if am.figured_bass != bm.figured_bass {
2969                    changes.push(ScoreChange::FiguredBassChanged {
2970                        part: pi,
2971                        staff: si,
2972                        measure: mi,
2973                        old: am.figured_bass.clone(),
2974                        new: bm.figured_bass.clone(),
2975                    });
2976                }
2977                if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
2978                    changes.push(ScoreChange::HarpPedalDiagramsChanged {
2979                        part: pi,
2980                        staff: si,
2981                        measure: mi,
2982                        old: am.harp_pedal_diagrams.clone(),
2983                        new: bm.harp_pedal_diagrams.clone(),
2984                    });
2985                }
2986            }
2987        }
2988    }
2989
2990    changes
2991}
2992
2993fn first_unrepresented_field_change(a: &Score, b: &Score) -> Option<String> {
2994    if a.settings.time_signature != b.settings.time_signature {
2995        return Some("settings.time_signature".to_string());
2996    }
2997    if a.part_groups.len() != b.part_groups.len()
2998        || a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
2999            x.first_part != y.first_part
3000                || x.last_part != y.last_part
3001                || x.symbol != y.symbol
3002                || x.barlines_connect != y.barlines_connect
3003        })
3004    {
3005        return Some("part_groups".to_string());
3006    }
3007    a.parts
3008        .iter()
3009        .zip(&b.parts)
3010        .enumerate()
3011        .find_map(|(part_index, (ap, bp))| first_unrepresented_part_change(part_index, ap, bp))
3012}
3013
3014fn first_unrepresented_part_change(part_index: usize, a: &Part, b: &Part) -> Option<String> {
3015    let prefix = format!("parts[{part_index}]");
3016    if a.percussion_instruments != b.percussion_instruments {
3017        return Some(format!("{prefix}.percussion_instruments"));
3018    }
3019    if a.staff_groups != b.staff_groups {
3020        return Some(format!("{prefix}.staff_groups"));
3021    }
3022    if a.staves.len() != b.staves.len() {
3023        return Some(format!("{prefix}.staves"));
3024    }
3025    a.staves
3026        .iter()
3027        .zip(&b.staves)
3028        .enumerate()
3029        .find_map(|(staff_index, (a, b))| {
3030            first_unrepresented_staff_change(&prefix, staff_index, a, b)
3031        })
3032}
3033
3034fn first_unrepresented_staff_change(
3035    part_prefix: &str,
3036    staff_index: usize,
3037    a: &Staff,
3038    b: &Staff,
3039) -> Option<String> {
3040    let prefix = format!("{part_prefix}.staves[{staff_index}]");
3041    if a.measures.len() != b.measures.len() {
3042        return Some(format!("{prefix}.measures"));
3043    }
3044    a.measures
3045        .iter()
3046        .zip(&b.measures)
3047        .enumerate()
3048        .find_map(|(measure_index, (a, b))| {
3049            first_unrepresented_measure_change(&prefix, measure_index, a, b)
3050        })
3051}
3052
3053fn first_unrepresented_measure_change(
3054    _staff_prefix: &str,
3055    _measure_index: usize,
3056    _a: &Measure,
3057    _b: &Measure,
3058) -> Option<String> {
3059    None
3060}
3061
3062// ── ScorePatch ────────────────────────────────────────────────────────────────
3063
3064/// An individually applicable patch operation produced by [`score_patch`].
3065///
3066/// Unlike [`ScoreChange`], every variant carries enough data to apply the change to a
3067/// [`Score`] without needing the original score. Use [`apply_patch`] to apply a list.
3068#[derive(Debug, Clone, Serialize, Deserialize)]
3069pub enum ScorePatch {
3070    SetScoreViews {
3071        value: Vec<ScoreView>,
3072    },
3073    SetScoreTexts {
3074        value: Vec<StyledText>,
3075    },
3076    SetMeasureTexts {
3077        part: usize,
3078        staff: usize,
3079        measure: usize,
3080        value: Vec<StyledText>,
3081    },
3082    SetFiguredBass {
3083        part: usize,
3084        staff: usize,
3085        measure: usize,
3086        value: Vec<FiguredBassFigure>,
3087    },
3088    SetHarpPedalDiagrams {
3089        part: usize,
3090        staff: usize,
3091        measure: usize,
3092        value: Vec<HarpPedalDiagram>,
3093    },
3094    SetChordDefinitions {
3095        value: Vec<ChordDefinition>,
3096    },
3097    SetPartNames {
3098        part: usize,
3099        name: String,
3100        short_name: String,
3101    },
3102    SetPartMidi {
3103        part: usize,
3104        channel: u8,
3105        program: u8,
3106    },
3107    SetInstrumentDefinition {
3108        part: usize,
3109        value: Option<InstrumentDefinition>,
3110    },
3111    SetPartMidiAutomation {
3112        part: usize,
3113        pitch_bends: Vec<MidiPitchBend>,
3114        control_changes: Vec<MidiControlChange>,
3115        program_changes: Vec<MidiProgramChange>,
3116        aftertouch: Vec<MidiAftertouch>,
3117    },
3118    SetStaffConfiguration {
3119        part: usize,
3120        staff: usize,
3121        clef: Clef,
3122        transpose_semitones: i8,
3123    },
3124    SetStaffPresentation {
3125        part: usize,
3126        staff: usize,
3127        value: StaffPresentation,
3128    },
3129    SetMeasurePresentation {
3130        part: usize,
3131        staff: usize,
3132        measure: usize,
3133        number: u32,
3134        clef: Option<Clef>,
3135        tempo_text: Option<String>,
3136        navigation: Option<String>,
3137        expression_text: Option<String>,
3138        multi_rest_count: Option<u8>,
3139        system_break: bool,
3140        page_break: bool,
3141        #[serde(default)]
3142        section_break: bool,
3143    },
3144    SetTablatureConfig {
3145        part: usize,
3146        staff: usize,
3147        value: Option<TablatureConfig>,
3148    },
3149    SetMeasureTablatureChange {
3150        part: usize,
3151        staff: usize,
3152        measure: usize,
3153        value: Option<TablatureConfig>,
3154    },
3155    SetMeasureMidClefs {
3156        part: usize,
3157        staff: usize,
3158        measure: usize,
3159        value: Vec<MidMeasureClef>,
3160    },
3161    SetMetadata {
3162        field: String,
3163        value: String,
3164    },
3165    SetTempo {
3166        value: u16,
3167    },
3168    SetKeySignature {
3169        part: usize,
3170        staff: usize,
3171        measure: usize,
3172        value: Option<KeySignature>,
3173    },
3174    SetTimeSignature {
3175        part: usize,
3176        staff: usize,
3177        measure: usize,
3178        value: Option<TimeSignature>,
3179    },
3180    SetBarlines {
3181        part: usize,
3182        staff: usize,
3183        measure: usize,
3184        left: Barline,
3185        right: Barline,
3186    },
3187    SetRehearsal {
3188        part: usize,
3189        staff: usize,
3190        measure: usize,
3191        value: Option<String>,
3192    },
3193    SetVolta {
3194        part: usize,
3195        staff: usize,
3196        measure: usize,
3197        value: Option<VoltaBracket>,
3198    },
3199    /// Insert `note` at `note_index` in the given voice (existing notes shift right).
3200    AddNote {
3201        part: usize,
3202        staff: usize,
3203        measure: usize,
3204        voice: usize,
3205        /// Position for insertion. `usize::MAX` is the legacy append sentinel.
3206        #[serde(default = "legacy_append_index")]
3207        note_index: usize,
3208        note: Box<Note>,
3209    },
3210    RemoveNote {
3211        part: usize,
3212        staff: usize,
3213        measure: usize,
3214        voice: usize,
3215        note_index: usize,
3216    },
3217    /// Replace the note at `note_index` with `note`.
3218    ReplaceNote {
3219        part: usize,
3220        staff: usize,
3221        measure: usize,
3222        voice: usize,
3223        note_index: usize,
3224        note: Box<Note>,
3225    },
3226    SetMeasureTempo {
3227        part: usize,
3228        staff: usize,
3229        measure: usize,
3230        value: Option<u16>,
3231    },
3232    SetMeasureTempoRamp {
3233        part: usize,
3234        staff: usize,
3235        measure: usize,
3236        value: Option<u16>,
3237    },
3238    /// Replace the complete score when a change cannot be represented safely by
3239    /// positional operations (for example, a part or measure was added).
3240    ReplaceScore {
3241        score: Box<Score>,
3242    },
3243}
3244
3245fn legacy_append_index() -> usize {
3246    usize::MAX
3247}
3248
3249/// Return whether positional patches would lose score data. The patch format deliberately
3250/// keeps the common editing operations small; fields without a dedicated operation use the
3251/// complete-score fallback so an interchange round-trip never silently drops notation.
3252fn patch_requires_replace(a: &Score, b: &Score) -> bool {
3253    if a.settings.time_signature != b.settings.time_signature
3254        || a.settings.key_signature != b.settings.key_signature
3255        || a.parts.len() != b.parts.len()
3256        || a.part_groups.len() != b.part_groups.len()
3257    {
3258        return true;
3259    }
3260    if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
3261        x.first_part != y.first_part
3262            || x.last_part != y.last_part
3263            || x.symbol != y.symbol
3264            || x.barlines_connect != y.barlines_connect
3265    }) {
3266        return true;
3267    }
3268    for (ap, bp) in a.parts.iter().zip(&b.parts) {
3269        if ap.percussion_instruments != bp.percussion_instruments
3270            || ap.staff_groups != bp.staff_groups
3271            || ap.staves.len() != bp.staves.len()
3272        {
3273            return true;
3274        }
3275        for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
3276            if as_.measures.len() != bs.measures.len() {
3277                return true;
3278            }
3279        }
3280    }
3281    false
3282}
3283
3284/// Compare two scores and return a list of [`ScorePatch`] operations.
3285///
3286/// Applying the patches to `a` via [`apply_patch`] produces a score structurally
3287/// equivalent to `b` (same parts, staves, measures, and note content).
3288pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
3289    let mut patches: Vec<ScorePatch> = Vec::new();
3290
3291    if patch_requires_replace(a, b) {
3292        return vec![ScorePatch::ReplaceScore {
3293            score: Box::new(b.clone()),
3294        }];
3295    }
3296
3297    if a.views != b.views {
3298        patches.push(ScorePatch::SetScoreViews {
3299            value: b.views.clone(),
3300        });
3301    }
3302
3303    if a.texts != b.texts {
3304        patches.push(ScorePatch::SetScoreTexts {
3305            value: b.texts.clone(),
3306        });
3307    }
3308    if a.chord_definitions != b.chord_definitions {
3309        patches.push(ScorePatch::SetChordDefinitions {
3310            value: b.chord_definitions.clone(),
3311        });
3312    }
3313
3314    macro_rules! meta {
3315        ($field:ident, $name:literal) => {
3316            if a.metadata.$field != b.metadata.$field {
3317                patches.push(ScorePatch::SetMetadata {
3318                    field: $name.to_string(),
3319                    value: b.metadata.$field.clone(),
3320                });
3321            }
3322        };
3323    }
3324    meta!(title, "title");
3325    meta!(composer, "composer");
3326    meta!(lyricist, "lyricist");
3327    meta!(copyright, "copyright");
3328    meta!(work_number, "work_number");
3329    meta!(movement_title, "movement_title");
3330
3331    if a.settings.tempo_bpm != b.settings.tempo_bpm {
3332        patches.push(ScorePatch::SetTempo {
3333            value: b.settings.tempo_bpm,
3334        });
3335    }
3336
3337    for pi in 0..a.parts.len().min(b.parts.len()) {
3338        let ap = &a.parts[pi];
3339        let bp = &b.parts[pi];
3340        if ap.name != bp.name || ap.short_name != bp.short_name {
3341            patches.push(ScorePatch::SetPartNames {
3342                part: pi,
3343                name: bp.name.clone(),
3344                short_name: bp.short_name.clone(),
3345            });
3346        }
3347        if ap.midi_channel != bp.midi_channel || ap.midi_program != bp.midi_program {
3348            patches.push(ScorePatch::SetPartMidi {
3349                part: pi,
3350                channel: bp.midi_channel,
3351                program: bp.midi_program,
3352            });
3353        }
3354        if ap.instrument != bp.instrument {
3355            patches.push(ScorePatch::SetInstrumentDefinition {
3356                part: pi,
3357                value: bp.instrument.clone(),
3358            });
3359        }
3360        if ap.midi_pitch_bends != bp.midi_pitch_bends
3361            || ap.midi_control_changes != bp.midi_control_changes
3362            || ap.midi_program_changes != bp.midi_program_changes
3363            || ap.midi_aftertouch != bp.midi_aftertouch
3364        {
3365            patches.push(ScorePatch::SetPartMidiAutomation {
3366                part: pi,
3367                pitch_bends: bp.midi_pitch_bends.clone(),
3368                control_changes: bp.midi_control_changes.clone(),
3369                program_changes: bp.midi_program_changes.clone(),
3370                aftertouch: bp.midi_aftertouch.clone(),
3371            });
3372        }
3373        for si in 0..ap.staves.len().min(bp.staves.len()) {
3374            let a_staff = &ap.staves[si];
3375            let b_staff = &bp.staves[si];
3376            if a_staff.clef != b_staff.clef
3377                || a_staff.transpose_semitones != b_staff.transpose_semitones
3378            {
3379                patches.push(ScorePatch::SetStaffConfiguration {
3380                    part: pi,
3381                    staff: si,
3382                    clef: b_staff.clef.clone(),
3383                    transpose_semitones: b_staff.transpose_semitones,
3384                });
3385            }
3386            if a_staff.tablature != b_staff.tablature {
3387                patches.push(ScorePatch::SetTablatureConfig {
3388                    part: pi,
3389                    staff: si,
3390                    value: b_staff.tablature.clone(),
3391                });
3392            }
3393            if a_staff.presentation != b_staff.presentation {
3394                patches.push(ScorePatch::SetStaffPresentation {
3395                    part: pi,
3396                    staff: si,
3397                    value: b_staff.presentation.clone(),
3398                });
3399            }
3400            for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
3401                let am = &a_staff.measures[mi];
3402                let bm = &b_staff.measures[mi];
3403
3404                if am.tablature_change != bm.tablature_change {
3405                    patches.push(ScorePatch::SetMeasureTablatureChange {
3406                        part: pi,
3407                        staff: si,
3408                        measure: mi,
3409                        value: bm.tablature_change.clone(),
3410                    });
3411                }
3412
3413                if am.mid_clefs != bm.mid_clefs {
3414                    patches.push(ScorePatch::SetMeasureMidClefs {
3415                        part: pi,
3416                        staff: si,
3417                        measure: mi,
3418                        value: bm.mid_clefs.clone(),
3419                    });
3420                }
3421
3422                if am.number != bm.number
3423                    || am.clef != bm.clef
3424                    || am.tempo_text != bm.tempo_text
3425                    || am.navigation != bm.navigation
3426                    || am.expression_text != bm.expression_text
3427                    || am.multi_rest_count != bm.multi_rest_count
3428                    || am.system_break != bm.system_break
3429                    || am.page_break != bm.page_break
3430                    || am.section_break != bm.section_break
3431                {
3432                    patches.push(ScorePatch::SetMeasurePresentation {
3433                        part: pi,
3434                        staff: si,
3435                        measure: mi,
3436                        number: bm.number,
3437                        clef: bm.clef.clone(),
3438                        tempo_text: bm.tempo_text.clone(),
3439                        navigation: bm.navigation.clone(),
3440                        expression_text: bm.expression_text.clone(),
3441                        multi_rest_count: bm.multi_rest_count,
3442                        system_break: bm.system_break,
3443                        page_break: bm.page_break,
3444                        section_break: bm.section_break,
3445                    });
3446                }
3447                if am.key_sig != bm.key_sig {
3448                    patches.push(ScorePatch::SetKeySignature {
3449                        part: pi,
3450                        staff: si,
3451                        measure: mi,
3452                        value: bm.key_sig.clone(),
3453                    });
3454                }
3455                if am.time_sig != bm.time_sig {
3456                    patches.push(ScorePatch::SetTimeSignature {
3457                        part: pi,
3458                        staff: si,
3459                        measure: mi,
3460                        value: bm.time_sig.clone(),
3461                    });
3462                }
3463                if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
3464                    patches.push(ScorePatch::SetBarlines {
3465                        part: pi,
3466                        staff: si,
3467                        measure: mi,
3468                        left: bm.barline_left.clone(),
3469                        right: bm.barline_right.clone(),
3470                    });
3471                }
3472                if am.rehearsal != bm.rehearsal {
3473                    patches.push(ScorePatch::SetRehearsal {
3474                        part: pi,
3475                        staff: si,
3476                        measure: mi,
3477                        value: bm.rehearsal.clone(),
3478                    });
3479                }
3480                if am.volta != bm.volta {
3481                    patches.push(ScorePatch::SetVolta {
3482                        part: pi,
3483                        staff: si,
3484                        measure: mi,
3485                        value: bm.volta.clone(),
3486                    });
3487                }
3488                if am.tempo != bm.tempo {
3489                    patches.push(ScorePatch::SetMeasureTempo {
3490                        part: pi,
3491                        staff: si,
3492                        measure: mi,
3493                        value: bm.tempo,
3494                    });
3495                }
3496                if am.tempo_ramp_to != bm.tempo_ramp_to {
3497                    patches.push(ScorePatch::SetMeasureTempoRamp {
3498                        part: pi,
3499                        staff: si,
3500                        measure: mi,
3501                        value: bm.tempo_ramp_to,
3502                    });
3503                }
3504                if am.texts != bm.texts {
3505                    patches.push(ScorePatch::SetMeasureTexts {
3506                        part: pi,
3507                        staff: si,
3508                        measure: mi,
3509                        value: bm.texts.clone(),
3510                    });
3511                }
3512                if am.figured_bass != bm.figured_bass {
3513                    patches.push(ScorePatch::SetFiguredBass {
3514                        part: pi,
3515                        staff: si,
3516                        measure: mi,
3517                        value: bm.figured_bass.clone(),
3518                    });
3519                }
3520                if am.harp_pedal_diagrams != bm.harp_pedal_diagrams {
3521                    patches.push(ScorePatch::SetHarpPedalDiagrams {
3522                        part: pi,
3523                        staff: si,
3524                        measure: mi,
3525                        value: bm.harp_pedal_diagrams.clone(),
3526                    });
3527                }
3528
3529                for vi in 0..4usize {
3530                    let av = &am.voices[vi];
3531                    let bv = &bm.voices[vi];
3532                    for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
3533                        if !note_content_eq(a_note, b_note) {
3534                            patches.push(ScorePatch::ReplaceNote {
3535                                part: pi,
3536                                staff: si,
3537                                measure: mi,
3538                                voice: vi,
3539                                note_index: ni,
3540                                note: Box::new(b_note.clone()),
3541                            });
3542                        }
3543                    }
3544                    // Notes in `a` beyond `b` — remove in reverse order to preserve indices.
3545                    for ni in (bv.len()..av.len()).rev() {
3546                        patches.push(ScorePatch::RemoveNote {
3547                            part: pi,
3548                            staff: si,
3549                            measure: mi,
3550                            voice: vi,
3551                            note_index: ni,
3552                        });
3553                    }
3554                    // Notes in `b` beyond `a` — append.
3555                    for (offset, note) in bv.iter().skip(av.len()).enumerate() {
3556                        patches.push(ScorePatch::AddNote {
3557                            part: pi,
3558                            staff: si,
3559                            measure: mi,
3560                            voice: vi,
3561                            note_index: av.len() + offset,
3562                            note: Box::new(note.clone()),
3563                        });
3564                    }
3565                }
3566            }
3567        }
3568    }
3569
3570    patches
3571}
3572
3573/// Apply a list of [`ScorePatch`] operations to a cloned copy of `score`.
3574///
3575/// Returns `Err(Error::InvalidPatch)` if any patch references an out-of-bounds index.
3576/// The returned score is an independent clone — `score` is not modified.
3577pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
3578    let mut s = score.clone();
3579    for patch in patches {
3580        match patch {
3581            ScorePatch::ReplaceScore { score } => {
3582                s = (**score).clone();
3583            }
3584            ScorePatch::SetScoreViews { value } => {
3585                s.views = value.clone();
3586            }
3587            ScorePatch::SetScoreTexts { value } => {
3588                s.texts = value.clone();
3589            }
3590            ScorePatch::SetMeasureTexts {
3591                part,
3592                staff,
3593                measure,
3594                value,
3595            } => {
3596                s.parts
3597                    .get_mut(*part)
3598                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3599                    .staves
3600                    .get_mut(*staff)
3601                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3602                    .measures
3603                    .get_mut(*measure)
3604                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3605                    .texts = value.clone();
3606            }
3607            ScorePatch::SetFiguredBass {
3608                part,
3609                staff,
3610                measure,
3611                value,
3612            } => {
3613                s.parts
3614                    .get_mut(*part)
3615                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3616                    .staves
3617                    .get_mut(*staff)
3618                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3619                    .measures
3620                    .get_mut(*measure)
3621                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3622                    .figured_bass = value.clone();
3623            }
3624            ScorePatch::SetHarpPedalDiagrams {
3625                part,
3626                staff,
3627                measure,
3628                value,
3629            } => {
3630                s.parts
3631                    .get_mut(*part)
3632                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3633                    .staves
3634                    .get_mut(*staff)
3635                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3636                    .measures
3637                    .get_mut(*measure)
3638                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3639                    .harp_pedal_diagrams = value.clone();
3640            }
3641            ScorePatch::SetChordDefinitions { value } => {
3642                s.chord_definitions = value.clone();
3643            }
3644            ScorePatch::SetPartNames {
3645                part,
3646                name,
3647                short_name,
3648            } => {
3649                let target = s
3650                    .parts
3651                    .get_mut(*part)
3652                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3653                target.name = name.clone();
3654                target.short_name = short_name.clone();
3655            }
3656            ScorePatch::SetPartMidi {
3657                part,
3658                channel,
3659                program,
3660            } => {
3661                let target = s
3662                    .parts
3663                    .get_mut(*part)
3664                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3665                target.midi_channel = *channel;
3666                target.midi_program = *program;
3667            }
3668            ScorePatch::SetInstrumentDefinition { part, value } => {
3669                s.parts
3670                    .get_mut(*part)
3671                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3672                    .instrument = value.clone();
3673            }
3674            ScorePatch::SetPartMidiAutomation {
3675                part,
3676                pitch_bends,
3677                control_changes,
3678                program_changes,
3679                aftertouch,
3680            } => {
3681                let target = s
3682                    .parts
3683                    .get_mut(*part)
3684                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?;
3685                target.midi_pitch_bends = pitch_bends.clone();
3686                target.midi_control_changes = control_changes.clone();
3687                target.midi_program_changes = program_changes.clone();
3688                target.midi_aftertouch = aftertouch.clone();
3689            }
3690            ScorePatch::SetStaffConfiguration {
3691                part,
3692                staff,
3693                clef,
3694                transpose_semitones,
3695            } => {
3696                let target = s
3697                    .parts
3698                    .get_mut(*part)
3699                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3700                    .staves
3701                    .get_mut(*staff)
3702                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3703                target.clef = clef.clone();
3704                target.transpose_semitones = *transpose_semitones;
3705            }
3706            ScorePatch::SetStaffPresentation { part, staff, value } => {
3707                let target = s
3708                    .parts
3709                    .get_mut(*part)
3710                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3711                    .staves
3712                    .get_mut(*staff)
3713                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?;
3714                target.presentation = value.clone();
3715            }
3716            ScorePatch::SetMeasurePresentation {
3717                part,
3718                staff,
3719                measure,
3720                number,
3721                clef,
3722                tempo_text,
3723                navigation,
3724                expression_text,
3725                multi_rest_count,
3726                system_break,
3727                page_break,
3728                section_break,
3729            } => {
3730                let target = s
3731                    .parts
3732                    .get_mut(*part)
3733                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3734                    .staves
3735                    .get_mut(*staff)
3736                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3737                    .measures
3738                    .get_mut(*measure)
3739                    .ok_or_else(|| {
3740                        Error::InvalidPatch(format!("measure {measure} out of range"))
3741                    })?;
3742                target.number = *number;
3743                target.clef = clef.clone();
3744                target.tempo_text = tempo_text.clone();
3745                target.navigation = navigation.clone();
3746                target.expression_text = expression_text.clone();
3747                target.multi_rest_count = *multi_rest_count;
3748                target.system_break = *system_break;
3749                target.page_break = *page_break;
3750                target.section_break = *section_break;
3751            }
3752            ScorePatch::SetTablatureConfig { part, staff, value } => {
3753                s.parts
3754                    .get_mut(*part)
3755                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3756                    .staves
3757                    .get_mut(*staff)
3758                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3759                    .tablature = value.clone();
3760            }
3761            ScorePatch::SetMeasureTablatureChange {
3762                part,
3763                staff,
3764                measure,
3765                value,
3766            } => {
3767                s.parts
3768                    .get_mut(*part)
3769                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3770                    .staves
3771                    .get_mut(*staff)
3772                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3773                    .measures
3774                    .get_mut(*measure)
3775                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3776                    .tablature_change = value.clone();
3777            }
3778            ScorePatch::SetMeasureMidClefs {
3779                part,
3780                staff,
3781                measure,
3782                value,
3783            } => {
3784                s.parts
3785                    .get_mut(*part)
3786                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3787                    .staves
3788                    .get_mut(*staff)
3789                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3790                    .measures
3791                    .get_mut(*measure)
3792                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3793                    .mid_clefs = value.clone();
3794            }
3795            ScorePatch::SetMetadata { field, value } => match field.as_str() {
3796                "title" => s.metadata.title = value.clone(),
3797                "composer" => s.metadata.composer = value.clone(),
3798                "lyricist" => s.metadata.lyricist = value.clone(),
3799                "copyright" => s.metadata.copyright = value.clone(),
3800                "work_number" => s.metadata.work_number = value.clone(),
3801                "movement_title" => s.metadata.movement_title = value.clone(),
3802                other => {
3803                    return Err(Error::InvalidPatch(format!(
3804                        "unknown metadata field: {other}"
3805                    )));
3806                }
3807            },
3808            ScorePatch::SetTempo { value } => {
3809                s.settings.tempo_bpm = *value;
3810            }
3811            ScorePatch::SetKeySignature {
3812                part,
3813                staff,
3814                measure,
3815                value,
3816            } => {
3817                s.parts
3818                    .get_mut(*part)
3819                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3820                    .staves
3821                    .get_mut(*staff)
3822                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3823                    .measures
3824                    .get_mut(*measure)
3825                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3826                    .key_sig = value.clone();
3827            }
3828            ScorePatch::SetTimeSignature {
3829                part,
3830                staff,
3831                measure,
3832                value,
3833            } => {
3834                s.parts
3835                    .get_mut(*part)
3836                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3837                    .staves
3838                    .get_mut(*staff)
3839                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3840                    .measures
3841                    .get_mut(*measure)
3842                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3843                    .time_sig = value.clone();
3844            }
3845            ScorePatch::SetBarlines {
3846                part,
3847                staff,
3848                measure,
3849                left,
3850                right,
3851            } => {
3852                let m = s
3853                    .parts
3854                    .get_mut(*part)
3855                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3856                    .staves
3857                    .get_mut(*staff)
3858                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3859                    .measures
3860                    .get_mut(*measure)
3861                    .ok_or_else(|| {
3862                        Error::InvalidPatch(format!("measure {measure} out of range"))
3863                    })?;
3864                m.barline_left = left.clone();
3865                m.barline_right = right.clone();
3866            }
3867            ScorePatch::SetRehearsal {
3868                part,
3869                staff,
3870                measure,
3871                value,
3872            } => {
3873                s.parts
3874                    .get_mut(*part)
3875                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3876                    .staves
3877                    .get_mut(*staff)
3878                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3879                    .measures
3880                    .get_mut(*measure)
3881                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3882                    .rehearsal = value.clone();
3883            }
3884            ScorePatch::SetVolta {
3885                part,
3886                staff,
3887                measure,
3888                value,
3889            } => {
3890                s.parts
3891                    .get_mut(*part)
3892                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3893                    .staves
3894                    .get_mut(*staff)
3895                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3896                    .measures
3897                    .get_mut(*measure)
3898                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3899                    .volta = value.clone();
3900            }
3901            ScorePatch::AddNote {
3902                part,
3903                staff,
3904                measure,
3905                voice,
3906                note_index,
3907                note,
3908            } => {
3909                let v = s
3910                    .parts
3911                    .get_mut(*part)
3912                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3913                    .staves
3914                    .get_mut(*staff)
3915                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3916                    .measures
3917                    .get_mut(*measure)
3918                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3919                    .voices
3920                    .get_mut(*voice)
3921                    .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3922                let insert_at = if *note_index == usize::MAX {
3923                    v.len()
3924                } else {
3925                    *note_index
3926                };
3927                if insert_at > v.len() {
3928                    return Err(Error::InvalidPatch(format!(
3929                        "note_index {note_index} out of range"
3930                    )));
3931                }
3932                v.insert(insert_at, *note.clone());
3933            }
3934            ScorePatch::RemoveNote {
3935                part,
3936                staff,
3937                measure,
3938                voice,
3939                note_index,
3940            } => {
3941                let v = s
3942                    .parts
3943                    .get_mut(*part)
3944                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3945                    .staves
3946                    .get_mut(*staff)
3947                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3948                    .measures
3949                    .get_mut(*measure)
3950                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3951                    .voices
3952                    .get_mut(*voice)
3953                    .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3954                if *note_index >= v.len() {
3955                    return Err(Error::InvalidPatch(format!(
3956                        "note_index {note_index} out of range"
3957                    )));
3958                }
3959                v.remove(*note_index);
3960            }
3961            ScorePatch::ReplaceNote {
3962                part,
3963                staff,
3964                measure,
3965                voice,
3966                note_index,
3967                note,
3968            } => {
3969                let v = s
3970                    .parts
3971                    .get_mut(*part)
3972                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3973                    .staves
3974                    .get_mut(*staff)
3975                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
3976                    .measures
3977                    .get_mut(*measure)
3978                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
3979                    .voices
3980                    .get_mut(*voice)
3981                    .ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
3982                if *note_index >= v.len() {
3983                    return Err(Error::InvalidPatch(format!(
3984                        "note_index {note_index} out of range"
3985                    )));
3986                }
3987                v[*note_index] = *note.clone();
3988            }
3989            ScorePatch::SetMeasureTempo {
3990                part,
3991                staff,
3992                measure,
3993                value,
3994            } => {
3995                s.parts
3996                    .get_mut(*part)
3997                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
3998                    .staves
3999                    .get_mut(*staff)
4000                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4001                    .measures
4002                    .get_mut(*measure)
4003                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4004                    .tempo = *value;
4005            }
4006            ScorePatch::SetMeasureTempoRamp {
4007                part,
4008                staff,
4009                measure,
4010                value,
4011            } => {
4012                s.parts
4013                    .get_mut(*part)
4014                    .ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
4015                    .staves
4016                    .get_mut(*staff)
4017                    .ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
4018                    .measures
4019                    .get_mut(*measure)
4020                    .ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
4021                    .tempo_ramp_to = *value;
4022            }
4023        }
4024    }
4025    if !super::validate::validate(&s).is_valid() {
4026        return Err(Error::InvalidScore);
4027    }
4028    Ok(s)
4029}
4030
4031/// Beats a voice occupies in a measure lasting `measure_beats`. A voice holding only one plain
4032/// whole rest is a measure rest and lasts exactly the measure in any time signature (as in
4033/// MusicXML and MuseScore); otherwise the voice lasts the sum of its notes.
4034pub fn voice_duration_beats(voice: &[Note], measure_beats: f64) -> f64 {
4035    match voice {
4036        [only] if only.is_plain_whole_rest() => measure_beats,
4037        _ => voice.iter().map(Note::beats).sum(),
4038    }
4039}
4040
4041/// Respell all pitches in the score to prefer flats or sharps.
4042///
4043/// Applies [`Pitch::respell`] to every note in every part, staff, measure, and voice.
4044pub fn respell_score(score: &mut Score, prefer_flat: bool) {
4045    for part in &mut score.parts {
4046        for staff in &mut part.staves {
4047            for measure in &mut staff.measures {
4048                for voice in &mut measure.voices {
4049                    for note in voice.iter_mut() {
4050                        for pitch in &mut note.pitches {
4051                            *pitch = pitch.respell(prefer_flat);
4052                        }
4053                    }
4054                }
4055            }
4056        }
4057    }
4058}
4059
4060/// Spelling policy for [`respell_staff_region`].
4061#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4062#[serde(rename_all = "snake_case")]
4063pub enum RespellPolicy {
4064    /// Spell black-key pitches with flats.
4065    Flat,
4066    /// Spell black-key pitches with sharps.
4067    Sharp,
4068    /// Follow the key signature in effect at each measure: flat keys use flats, others sharps.
4069    Key,
4070}
4071
4072/// Respell the pitched notes of one staff in the measure range `start_measure..end_measure`
4073/// (end exclusive), the selection form of [`respell_score`].
4074///
4075/// With [`RespellPolicy::Key`], each measure uses its own key change, then the part's first-staff
4076/// key change, then the score key. Rests and unpitched notes are left unchanged, because an
4077/// unpitched note's pitch is its staff position. Sounding pitch and microtones never change.
4078///
4079/// A tie never joins two spellings: a tied chain takes the spelling of its first note inside the
4080/// range, including chain members before or after the range. Returns the inclusive measure span
4081/// that changed, which is wider than the range when a tie crosses its boundary.
4082pub fn respell_staff_region(
4083    score: &mut Score,
4084    part_index: usize,
4085    staff_index: usize,
4086    start_measure: usize,
4087    end_measure: usize,
4088    policy: RespellPolicy,
4089) -> Result<(usize, usize), Error> {
4090    let score_key_flat = score.settings.key_signature.fifths < 0;
4091    let part = score
4092        .parts
4093        .get_mut(part_index)
4094        .ok_or(Error::PartNotFound(part_index))?;
4095    let staff_count = part.staves.len();
4096    if staff_index >= staff_count {
4097        return Err(Error::StaffNotFound(staff_index));
4098    }
4099    let measure_count = part.staves[staff_index].measures.len();
4100    if start_measure >= end_measure || end_measure > measure_count {
4101        return Err(Error::InvalidCommand(format!(
4102            "invalid measure range {start_measure}..{end_measure}"
4103        )));
4104    }
4105    let mut prefer_flat = Vec::with_capacity(end_measure);
4106    let mut running_flat = score_key_flat;
4107    for index in 0..end_measure {
4108        let key = part.staves[staff_index].measures[index]
4109            .key_sig
4110            .as_ref()
4111            .or_else(|| {
4112                part.staves[0]
4113                    .measures
4114                    .get(index)
4115                    .and_then(|measure| measure.key_sig.as_ref())
4116            });
4117        if let Some(key) = key {
4118            running_flat = key.fifths < 0;
4119        }
4120        prefer_flat.push(match policy {
4121            RespellPolicy::Flat => true,
4122            RespellPolicy::Sharp => false,
4123            RespellPolicy::Key => running_flat,
4124        });
4125    }
4126
4127    let measures = &mut part.staves[staff_index].measures;
4128    for (index, measure) in measures
4129        .iter_mut()
4130        .enumerate()
4131        .take(end_measure)
4132        .skip(start_measure)
4133    {
4134        for voice in &mut measure.voices {
4135            for note in voice
4136                .iter_mut()
4137                .filter(|note| !note.is_rest && !note.is_unpitched)
4138            {
4139                for pitch in &mut note.pitches {
4140                    *pitch = pitch.respell(prefer_flat[index]);
4141                }
4142            }
4143        }
4144    }
4145
4146    let mut changed = (start_measure, end_measure - 1);
4147    for voice in 0..4 {
4148        for measure in start_measure..end_measure {
4149            for note in 0..measures[measure].voices[voice].len() {
4150                if measures[measure].voices[voice][note].tie_start {
4151                    let last = propagate_tied_spelling(measures, voice, (measure, note), true);
4152                    changed.1 = changed.1.max(last);
4153                }
4154            }
4155        }
4156        if !measures[start_measure].voices[voice].is_empty() {
4157            let first = propagate_tied_spelling(measures, voice, (start_measure, 0), false);
4158            changed.0 = changed.0.min(first);
4159        }
4160    }
4161    Ok(changed)
4162}
4163
4164/// Copy one note's spelling along its tie chain (forward through `tie_start`, or backward through
4165/// `tie_end`), matching chord members by sounding pitch. Returns the last measure reached.
4166fn propagate_tied_spelling(
4167    measures: &mut [Measure],
4168    voice: usize,
4169    from: (usize, usize),
4170    forward: bool,
4171) -> usize {
4172    let mut current = from;
4173    loop {
4174        let note = &measures[current.0].voices[voice][current.1];
4175        if !(if forward {
4176            note.tie_start
4177        } else {
4178            note.tie_end
4179        }) {
4180            return current.0;
4181        }
4182        let neighbor = if forward {
4183            if current.1 + 1 < measures[current.0].voices[voice].len() {
4184                Some((current.0, current.1 + 1))
4185            } else {
4186                measures
4187                    .get(current.0 + 1)
4188                    .filter(|measure| !measure.voices[voice].is_empty())
4189                    .map(|_| (current.0 + 1, 0))
4190            }
4191        } else if current.1 > 0 {
4192            Some((current.0, current.1 - 1))
4193        } else {
4194            current.0.checked_sub(1).and_then(|previous| {
4195                measures[previous].voices[voice]
4196                    .len()
4197                    .checked_sub(1)
4198                    .map(|last| (previous, last))
4199            })
4200        };
4201        let Some(next) = neighbor else {
4202            return current.0;
4203        };
4204        let next_note = &measures[next.0].voices[voice][next.1];
4205        if !(if forward {
4206            next_note.tie_end
4207        } else {
4208            next_note.tie_start
4209        }) {
4210            return current.0;
4211        }
4212        let source = measures[current.0].voices[voice][current.1].pitches.clone();
4213        for pitch in &mut measures[next.0].voices[voice][next.1].pitches {
4214            if let Some(spelled) = source.iter().find(|candidate| {
4215                candidate.to_midi() == pitch.to_midi()
4216                    && candidate.microtone_cents == pitch.microtone_cents
4217            }) {
4218                *pitch = spelled.clone();
4219            }
4220        }
4221        current = next;
4222    }
4223}
4224
4225/// Respell all pitches to match the score's key signature spelling convention.
4226///
4227/// Flat-key signatures (fifths < 0) use flat spellings; sharp-key and C major use sharps.
4228pub fn respell_score_to_key(score: &mut Score) {
4229    let prefer_flat = score.settings.key_signature.fifths < 0;
4230    respell_score(score, prefer_flat);
4231}
4232
4233/// Compute total playback duration in seconds.
4234///
4235/// Uses `measure_sequence` for correct repeat handling. Lighter than generating
4236/// full playback events — suitable for progress bars and UI display.
4237pub fn score_duration_secs(score: &Score) -> f64 {
4238    if score.settings.tempo_bpm == 0 {
4239        return 0.0;
4240    }
4241    let seq = measure_sequence(score);
4242    let mut total_secs = 0.0f64;
4243    let mut current_bpm = score.settings.tempo_bpm as f64;
4244    if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4245        for &idx in &seq {
4246            if let Some(m) = staff.measures.get(idx) {
4247                if let Some(b) = m.tempo {
4248                    current_bpm = b as f64;
4249                }
4250                if current_bpm == 0.0 {
4251                    continue;
4252                }
4253                let beats = voice_duration_beats(
4254                    &m.voices[0],
4255                    m.duration_beats(&score.settings.time_signature),
4256                );
4257                total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4258                if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4259                    current_bpm = f64::from(target);
4260                }
4261            }
4262        }
4263    }
4264    total_secs
4265}
4266
4267/// Compute playback duration in seconds for a specific measure range (inclusive).
4268///
4269/// `region` is `(start_measure, end_measure)`, both 0-based. Measures outside the range
4270/// are excluded. Uses `measure_sequence` for correct repeat handling.
4271pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
4272    if score.settings.tempo_bpm == 0 {
4273        return 0.0;
4274    }
4275    let seq: Vec<usize> = measure_sequence(score)
4276        .into_iter()
4277        .filter(|&idx| idx >= region.0 && idx <= region.1)
4278        .collect();
4279    let mut total_secs = 0.0f64;
4280    let mut current_bpm = score.settings.tempo_bpm as f64;
4281    if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
4282        for &idx in &seq {
4283            if let Some(m) = staff.measures.get(idx) {
4284                if let Some(b) = m.tempo {
4285                    current_bpm = b as f64;
4286                }
4287                if current_bpm == 0.0 {
4288                    continue;
4289                }
4290                let beats = voice_duration_beats(
4291                    &m.voices[0],
4292                    m.duration_beats(&score.settings.time_signature),
4293                );
4294                total_secs += tempo_ramp_duration_secs(current_bpm, m.tempo_ramp_to, beats);
4295                if let Some(target) = m.tempo_ramp_to.filter(|target| *target > 0) {
4296                    current_bpm = f64::from(target);
4297                }
4298            }
4299        }
4300    }
4301    total_secs
4302}
4303
4304fn tempo_ramp_duration_secs(start_bpm: f64, target_bpm: Option<u16>, beats: f64) -> f64 {
4305    let Some(target_bpm) = target_bpm.filter(|target| *target > 0) else {
4306        return beats / start_bpm * 60.0;
4307    };
4308    let end_bpm = f64::from(target_bpm);
4309    let delta = end_bpm - start_bpm;
4310    if delta.abs() < f64::EPSILON {
4311        return beats / start_bpm * 60.0;
4312    }
4313    60.0 * beats / delta * (end_bpm / start_bpm).ln()
4314}
4315
4316/// Return the number of beats available in a voice before it is full.
4317///
4318/// Uses [`Note::beats`] which correctly handles tuplet scaling.
4319/// Returns `Ok(0.0)` when the voice is already full or over-full.
4320pub fn measure_beats_remaining(
4321    score: &Score,
4322    part_index: usize,
4323    staff_index: usize,
4324    measure_index: usize,
4325    voice_index: usize,
4326) -> Result<f64, Error> {
4327    let part = score
4328        .parts
4329        .get(part_index)
4330        .ok_or(Error::PartNotFound(part_index))?;
4331    let staff = part
4332        .staves
4333        .get(staff_index)
4334        .ok_or(Error::StaffNotFound(staff_index))?;
4335    let measure = staff
4336        .measures
4337        .get(measure_index)
4338        .ok_or(Error::MeasureNotFound(measure_index))?;
4339    let voice = measure
4340        .voices
4341        .get(voice_index)
4342        .ok_or(Error::VoiceOutOfRange(voice_index))?;
4343    let capacity = measure.duration_beats(&score.settings.time_signature);
4344    Ok((capacity - voice_duration_beats(voice, capacity)).max(0.0))
4345}
4346
4347/// Suggest whether the stem should point up for the given pitches and clef.
4348///
4349/// Conventional rule: if the average MIDI pitch of the chord is below the staff
4350/// middle line, the stem points up; at or above, it points down.
4351/// For empty pitch lists (rests), returns `true` by convention.
4352pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
4353    if pitches.is_empty() {
4354        return true;
4355    }
4356    let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
4357    avg < clef.middle_line_midi() as f64
4358}
4359
4360fn beam_beat_size(ts: &TimeSignature) -> f64 {
4361    if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
4362        3.0 * 4.0 / ts.denominator as f64
4363    } else {
4364        4.0 / ts.denominator as f64
4365    }
4366}
4367
4368/// Compute recommended [`BeamState`] values for a voice's notes.
4369///
4370/// Groups beamable notes (eighth or shorter, non-rest) within beat boundaries.
4371/// Returns a `Vec` the same length as `notes`.
4372pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
4373    let beat_size = beam_beat_size(time_sig);
4374    let n = notes.len();
4375    let mut result = vec![BeamState::None; n];
4376
4377    let is_beamable = |note: &Note| -> bool {
4378        !note.is_rest
4379            && matches!(
4380                note.duration,
4381                Duration::Eighth
4382                    | Duration::Sixteenth
4383                    | Duration::ThirtySecond
4384                    | Duration::SixtyFourth
4385            )
4386    };
4387
4388    // Compute beat start positions
4389    let mut starts = Vec::with_capacity(n);
4390    let mut pos = 0.0f64;
4391    for note in notes {
4392        starts.push(pos);
4393        pos += note.beats();
4394    }
4395
4396    // Assign beam group ids based on beat boundary
4397    let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
4398
4399    let mut i = 0;
4400    while i < n {
4401        if !is_beamable(&notes[i]) {
4402            i += 1;
4403            continue;
4404        }
4405        let g = group_id(i);
4406        // Find the run of beamable notes in the same beat group
4407        let mut j = i;
4408        while j < n && is_beamable(&notes[j]) && group_id(j) == g {
4409            j += 1;
4410        }
4411        let run = j - i;
4412        if run == 1 {
4413            result[i] = BeamState::None;
4414        } else {
4415            result[i] = BeamState::Begin;
4416            result[i + 1..j - 1].fill(BeamState::Continue);
4417            result[j - 1] = BeamState::End;
4418        }
4419        i = j;
4420    }
4421    result
4422}
4423
4424fn note_content_eq(a: &Note, b: &Note) -> bool {
4425    a.is_rest == b.is_rest
4426        && a.is_unpitched == b.is_unpitched
4427        && a.instrument_id == b.instrument_id
4428        && a.offset_x == b.offset_x
4429        && a.offset_y == b.offset_y
4430        && a.relative_x == b.relative_x
4431        && a.relative_y == b.relative_y
4432        && a.pitches == b.pitches
4433        && a.duration == b.duration
4434        && a.dot_count == b.dot_count
4435        && a.tie_start == b.tie_start
4436        && a.tie_end == b.tie_end
4437        && a.beam == b.beam
4438        && a.articulations == b.articulations
4439        && a.dynamic == b.dynamic
4440        && a.stem_up == b.stem_up
4441        && a.hairpin_start == b.hairpin_start
4442        && a.hairpin_end == b.hairpin_end
4443        && a.tuplet == b.tuplet
4444        && a.chord_symbol == b.chord_symbol
4445        && a.is_grace == b.is_grace
4446        && a.grace_slash == b.grace_slash
4447        && a.ottava_start == b.ottava_start
4448        && a.ottava_end == b.ottava_end
4449        && a.lyric == b.lyric
4450        && a.additional_lyrics == b.additional_lyrics
4451        && a.pedal_start == b.pedal_start
4452        && a.pedal_end == b.pedal_end
4453        && a.slur_start == b.slur_start
4454        && a.slur_end == b.slur_end
4455        && a.arpeggiate == b.arpeggiate
4456        && a.tab_position == b.tab_position
4457        && a.tab_positions == b.tab_positions
4458        && a.guitar_technique == b.guitar_technique
4459        && a.guitar_bend_alter_cents == b.guitar_bend_alter_cents
4460        && a.guitar_bend_curve == b.guitar_bend_curve
4461}
4462
4463fn per_pitch_flag(flags: &[bool], pitch_count: usize, index: usize, chord: bool) -> bool {
4464    if flags.len() == pitch_count {
4465        flags.get(index).copied().unwrap_or(chord)
4466    } else {
4467        chord
4468    }
4469}
4470
4471#[cfg(test)]
4472mod tests {
4473    use super::*;
4474    use crate::model::{
4475        notation::{FingeringSelectionPolicy, TextStyle},
4476        pitch::Step,
4477    };
4478
4479    #[test]
4480    fn fingering_selection_policy_is_deterministic_and_non_mutating() {
4481        let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4482        note.fingerings = vec![3, 1, 4];
4483        note.fingering = Some(3);
4484        assert_eq!(
4485            note.select_fingering(FingeringSelectionPolicy::SourceOrder),
4486            Some(3)
4487        );
4488        assert_eq!(
4489            note.select_fingering(FingeringSelectionPolicy::LowestNumber),
4490            Some(1)
4491        );
4492        assert_eq!(
4493            note.select_fingering(FingeringSelectionPolicy::HighestNumber),
4494            Some(4)
4495        );
4496        assert_eq!(note.fingerings, vec![3, 1, 4]);
4497        assert_eq!(note.fingering, Some(3));
4498    }
4499
4500    #[test]
4501    fn default_score_has_one_part_four_measures() {
4502        let score = Score::default();
4503        assert_eq!(score.parts.len(), 1);
4504        assert_eq!(score.parts[0].staves.len(), 1);
4505        assert_eq!(score.parts[0].staves[0].measures.len(), 4);
4506    }
4507
4508    #[test]
4509    fn assign_tablature_positions_is_capo_aware_and_preserves_explicit_positions() {
4510        let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4511        score.parts[0].staves[0].tablature = Some(TablatureConfig {
4512            lines: 6,
4513            tuning_midi: vec![64, 59, 55, 50, 45, 40],
4514            capo: 2,
4515        });
4516        score.parts[0].staves[0].measures[0].voices[0].push(Note::new(
4517            Pitch::with_alter(Step::F, 4, 1),
4518            Duration::Quarter,
4519        ));
4520        score.parts[0].staves[0].measures[0].voices[0]
4521            .push(Note::new(Pitch::new(Step::G, 3), Duration::Quarter));
4522        score.parts[0].staves[0].measures[0].voices[0][2].tab_position =
4523            Some(TabPosition { string: 6, fret: 7 });
4524
4525        assert_eq!(assign_tablature_positions(&mut score), 1);
4526        let notes = &score.parts[0].staves[0].measures[0].voices[0];
4527        assert_eq!(
4528            notes[1].tab_position,
4529            Some(TabPosition { string: 1, fret: 0 })
4530        );
4531        assert_eq!(notes[1].string_number, Some(1));
4532        assert_eq!(
4533            notes[2].tab_position,
4534            Some(TabPosition { string: 6, fret: 7 })
4535        );
4536    }
4537
4538    #[test]
4539    fn assign_tablature_positions_uses_measure_local_capo_change() {
4540        let mut score = Score::new("Guitar", 120, 4, 4, 0, 2);
4541        score.parts[0].staves[0].tablature = Some(TablatureConfig {
4542            lines: 6,
4543            tuning_midi: vec![40, 45, 50, 55, 59, 64],
4544            capo: 0,
4545        });
4546        score.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
4547            lines: 6,
4548            tuning_midi: vec![40, 45, 50, 55, 59, 64],
4549            capo: 2,
4550        });
4551        score.parts[0].staves[0].measures[1].voices[0]
4552            .push(Note::new(Pitch::new(Step::E, 4), Duration::Quarter));
4553
4554        assert_eq!(assign_tablature_positions(&mut score), 1);
4555        assert_eq!(
4556            score.parts[0].staves[0].measures[1].voices[0][1].tab_position,
4557            Some(TabPosition { string: 5, fret: 3 })
4558        );
4559    }
4560
4561    #[test]
4562    fn assign_tablature_positions_optimizes_chord_strings_and_fret_span() {
4563        let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
4564        score.parts[0].staves[0].tablature = Some(TablatureConfig {
4565            lines: 6,
4566            tuning_midi: vec![64, 59, 55, 50, 45, 40],
4567            capo: 0,
4568        });
4569        let mut chord = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4570        chord.pitches.push(Pitch::new(Step::G, 4));
4571        score.parts[0].staves[0].measures[0].voices[0].push(chord);
4572
4573        assert_eq!(assign_tablature_positions(&mut score), 1);
4574        let positions = &score.parts[0].staves[0].measures[0].voices[0][1].tab_positions;
4575        assert_eq!(
4576            positions,
4577            &vec![
4578                TabPosition { string: 2, fret: 5 },
4579                TabPosition { string: 1, fret: 3 },
4580            ]
4581        );
4582    }
4583
4584    #[test]
4585    fn new_score_measure_count() {
4586        let score = Score::new("Test", 120, 4, 4, 0, 8);
4587        assert_eq!(score.measure_count(), 8);
4588    }
4589
4590    #[test]
4591    fn note_beats_quarter() {
4592        let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4593        assert!((note.beats() - 1.0).abs() < 1e-9);
4594    }
4595
4596    #[test]
4597    fn note_beats_dotted_quarter() {
4598        let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4599        note.dot_count = 1;
4600        assert!((note.beats() - 1.5).abs() < 1e-9);
4601    }
4602
4603    #[test]
4604    fn grace_note_beats_zero() {
4605        let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
4606        note.is_grace = true;
4607        assert_eq!(note.beats(), 0.0);
4608    }
4609
4610    #[test]
4611    fn measure_empty_4_4_fills_four_beats() {
4612        let m = Measure::empty(4, 4);
4613        let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4614        assert!((total - 4.0).abs() < 1e-9);
4615    }
4616
4617    #[test]
4618    fn measure_empty_3_4_fills_three_beats() {
4619        let m = Measure::empty(3, 4);
4620        let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
4621        assert!((total - 3.0).abs() < 1e-9);
4622    }
4623
4624    #[test]
4625    fn whole_filling_beats() {
4626        assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
4627        assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
4628        assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
4629    }
4630
4631    // ── ScoreStats ────────────────────────────────────────────────────────────
4632
4633    #[test]
4634    fn statistics_default_score_all_rests() {
4635        let score = Score::default();
4636        let s = score.statistics();
4637        assert_eq!(s.part_count, 1);
4638        assert_eq!(s.measure_count, 4);
4639        assert_eq!(s.note_count, 0);
4640        assert!(s.rest_count > 0);
4641    }
4642
4643    #[test]
4644    fn statistics_duration_estimate() {
4645        // 4/4, 120 BPM, 1 measure → 4 beats → 2.0 s
4646        let score = Score::new("T", 120, 4, 4, 0, 1);
4647        let s = score.statistics();
4648        assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
4649    }
4650
4651    #[test]
4652    fn score_duration_secs_matches_statistics() {
4653        use super::score_duration_secs;
4654        let score = Score::new("T", 120, 4, 4, 0, 4);
4655        let secs = score_duration_secs(&score);
4656        // 4/4, 120 BPM, 4 measures → 16 beats → 8.0 s
4657        assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
4658    }
4659
4660    #[test]
4661    fn score_duration_secs_integrates_measure_tempo_ramp() {
4662        use super::score_duration_secs;
4663        let mut score = Score::new("Ramp", 120, 4, 4, 0, 1);
4664        score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4665        let expected = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
4666        assert!((score_duration_secs(&score) - expected).abs() < 1e-9);
4667    }
4668
4669    #[test]
4670    fn score_duration_secs_zero_bpm_returns_zero() {
4671        use super::score_duration_secs;
4672        let mut score = Score::new("T", 120, 4, 4, 0, 1);
4673        score.settings.tempo_bpm = 0;
4674        assert_eq!(score_duration_secs(&score), 0.0);
4675    }
4676
4677    #[test]
4678    fn score_duration_secs_per_measure_tempo() {
4679        use super::score_duration_secs;
4680        // 2 measures: measure 0 at 120 BPM (2.0 s), measure 1 at 60 BPM (4.0 s)
4681        let mut score = Score::new("T", 120, 4, 4, 0, 2);
4682        score.parts[0].staves[0].measures[1].tempo = Some(60);
4683        let secs = score_duration_secs(&score);
4684        assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
4685    }
4686
4687    // ── extract_part ──────────────────────────────────────────────────────────
4688
4689    #[test]
4690    fn extract_part_returns_single_part_score() {
4691        let mut score = Score::default();
4692        let mut p2 = Part::new("Violin", "Vln.");
4693        p2.staves.push(Staff::new(Clef::Treble));
4694        score.parts.push(p2);
4695        let ex = score.extract_part(0).unwrap();
4696        assert_eq!(ex.parts.len(), 1);
4697        assert_ne!(ex.id, score.id);
4698        assert_eq!(ex.metadata.title, score.metadata.title);
4699    }
4700
4701    #[test]
4702    fn extract_part_out_of_range_is_none() {
4703        let score = Score::default();
4704        assert!(score.extract_part(99).is_none());
4705    }
4706
4707    #[test]
4708    fn extract_and_merge_remap_typed_spanner_part_addresses() {
4709        let mut left = Score::template(ScoreTemplate::StringQuartet);
4710        let address = NoteAddr {
4711            part: 1,
4712            staff: 0,
4713            measure: 0,
4714            voice: 0,
4715            note: 0,
4716        };
4717        left.spanners.push(NotationSpanner {
4718            id: "left-span".to_string(),
4719            kind: NotationSpannerKind::Slur,
4720            start: address.clone(),
4721            end: address,
4722            number: Some(1),
4723            line_type: None,
4724            text: None,
4725            placement: None,
4726            ottava_size: None,
4727            ottava_type: None,
4728        });
4729        let extracted = left.extract_part_checked(1).expect("valid extracted part");
4730        assert_eq!(extracted.spanners[0].start.part, 0);
4731        assert_eq!(extracted.spanners[0].end.part, 0);
4732
4733        let mut right = Score::new("R", 120, 4, 4, 0, 1);
4734        let right_address = NoteAddr {
4735            part: 0,
4736            staff: 0,
4737            measure: 0,
4738            voice: 0,
4739            note: 0,
4740        };
4741        right.spanners.push(NotationSpanner {
4742            id: "right-span".to_string(),
4743            kind: NotationSpannerKind::Pedal,
4744            start: right_address.clone(),
4745            end: right_address,
4746            number: Some(1),
4747            line_type: None,
4748            text: None,
4749            placement: None,
4750            ottava_size: None,
4751            ottava_type: None,
4752        });
4753        let merged = left.merge_checked(&right).expect("valid merged score");
4754        let right_span = merged
4755            .spanners
4756            .iter()
4757            .find(|spanner| spanner.id == "right-span")
4758            .expect("merged right span");
4759        assert_eq!(right_span.start.part, left.parts.len());
4760        assert_eq!(right_span.end.part, left.parts.len());
4761    }
4762
4763    // ── transpose ─────────────────────────────────────────────────────────────
4764
4765    #[test]
4766    fn transpose_zero_is_clone() {
4767        let score = Score::new("T", 120, 4, 4, 0, 1);
4768        let t = transpose(&score, 0);
4769        assert_eq!(t.settings.key_signature.fifths, 0);
4770    }
4771
4772    #[test]
4773    fn transpose_staff_region_rewrites_only_selected_written_measures() {
4774        let mut score = Score::new("T", 120, 4, 4, 0, 2);
4775        for measure in &mut score.parts[0].staves[0].measures {
4776            measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4777        }
4778        let transformed = transpose_staff_region_checked(
4779            &score,
4780            0,
4781            0,
4782            1,
4783            2,
4784            2,
4785            RegionalTranspositionTarget::Written,
4786        )
4787        .expect("region should transpose");
4788        assert_eq!(
4789            transformed.parts[0].staves[0].measures[0].voices[0][0].pitches[0],
4790            Pitch::new(Step::C, 4)
4791        );
4792        assert_eq!(
4793            transformed.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4794            Pitch::new(Step::D, 4)
4795        );
4796        assert_eq!(
4797            score.parts[0].staves[0].measures[1].voices[0][0].pitches[0],
4798            Pitch::new(Step::C, 4)
4799        );
4800    }
4801
4802    #[test]
4803    fn regional_concert_transposition_requires_full_staff() {
4804        let score = Score::new("T", 120, 4, 4, 0, 2);
4805        assert!(
4806            transpose_staff_region_checked(
4807                &score,
4808                0,
4809                0,
4810                1,
4811                2,
4812                -2,
4813                RegionalTranspositionTarget::Concert,
4814            )
4815            .is_err()
4816        );
4817    }
4818
4819    #[test]
4820    fn transpose_c_major_up_2_to_d_major() {
4821        let score = Score::new("T", 120, 4, 4, 0, 1);
4822        assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
4823    }
4824
4825    #[test]
4826    fn transpose_d_major_up_5_to_g_major() {
4827        let score = Score::new("T", 120, 4, 4, 2, 1);
4828        assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
4829    }
4830
4831    #[test]
4832    fn transpose_c4_up_1_to_csharp4() {
4833        let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
4834        assert_eq!(p.to_midi(), 61);
4835        assert_eq!(p.step, Step::C);
4836        assert_eq!(p.alter, 1);
4837    }
4838
4839    #[test]
4840    fn transpose_c4_down_1_to_b3() {
4841        let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
4842        assert_eq!(p.to_midi(), 59);
4843        assert_eq!(p.step, Step::B);
4844        assert_eq!(p.alter, 0);
4845    }
4846
4847    #[test]
4848    fn transpose_up_octave_keeps_step() {
4849        let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
4850        assert_eq!(p.to_midi(), 81);
4851        assert_eq!(p.step, Step::A);
4852        assert_eq!(p.octave, 5);
4853    }
4854
4855    #[test]
4856    fn statistics_with_repeat_doubles_duration() {
4857        // 4/4, 120 BPM, 2 measures with RepeatStart+RepeatEnd → plays twice → 4 measures worth
4858        let mut score = Score::new("T", 120, 4, 4, 0, 2);
4859        score.parts[0].staves[0].measures[0].barline_left =
4860            crate::model::notation::Barline::RepeatStart;
4861        score.parts[0].staves[0].measures[1].barline_right =
4862            crate::model::notation::Barline::RepeatEnd;
4863        let s = score.statistics();
4864        // 4 beats × 4 measures (2 physical × 2 passes) ÷ 120 BPM × 60 = 8.0 s
4865        assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
4866    }
4867
4868    #[test]
4869    fn transpose_octave_boundary_b4_to_c5() {
4870        // B4 (midi=71) + 1 semitone = C5 (midi=72)
4871        let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
4872        assert_eq!(p.to_midi(), 72);
4873        assert_eq!(p.step, Step::C);
4874        assert_eq!(p.octave, 5);
4875    }
4876
4877    #[test]
4878    fn transpose_clamp_at_midi_127() {
4879        // G9 (midi=127) + 3 semitones → clamped to 127
4880        let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
4881        assert_eq!(p.to_midi(), 127);
4882    }
4883
4884    // ── merge ─────────────────────────────────────────────────────────────────
4885
4886    #[test]
4887    fn merge_combines_parts() {
4888        let mut a = Score::new("A", 120, 4, 4, 0, 2);
4889        let b = Score::new("B", 120, 4, 4, 0, 2);
4890        // Add a second part to score a
4891        let mut p2 = Part::new("Violin", "Vln.");
4892        p2.staves.push(Staff::new(Clef::Treble));
4893        for i in 0..2usize {
4894            let mut m = Measure::empty(4, 4);
4895            m.number = i as u32 + 1;
4896            p2.staves[0].measures.push(m);
4897        }
4898        a.parts.push(p2);
4899        let merged = a.merge(&b);
4900        // a has 2 parts, b has 1 part → merged has 3 parts
4901        assert_eq!(merged.parts.len(), 3);
4902    }
4903
4904    #[test]
4905    fn merge_pads_shorter_score() {
4906        let a = Score::new("A", 120, 4, 4, 0, 4);
4907        let b = Score::new("B", 120, 4, 4, 0, 2);
4908        let merged = a.merge(&b);
4909        // Both parts should have 4 measures
4910        assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
4911        assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
4912    }
4913
4914    #[test]
4915    fn merge_uses_self_metadata() {
4916        let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
4917        a.metadata.composer = "Composer A".to_string();
4918        let b = Score::new("Title B", 120, 4, 4, 0, 2);
4919        let merged = a.merge(&b);
4920        assert_eq!(merged.metadata.title, "Title A");
4921        assert_eq!(merged.metadata.composer, "Composer A");
4922    }
4923
4924    #[test]
4925    fn merge_new_id_differs_from_both() {
4926        let a = Score::new("A", 120, 4, 4, 0, 2);
4927        let b = Score::new("B", 120, 4, 4, 0, 2);
4928        let merged = a.merge(&b);
4929        assert_ne!(merged.id, a.id);
4930        assert_ne!(merged.id, b.id);
4931    }
4932
4933    // ── Staff.transpose_semitones ─────────────────────────────────────────────
4934
4935    #[test]
4936    fn staff_default_transpose_is_zero() {
4937        let s = Staff::new(Clef::Treble);
4938        assert_eq!(s.transpose_semitones, 0);
4939    }
4940
4941    #[test]
4942    fn staff_presentation_defaults_and_tracks_percussion_clef() {
4943        let standard = Staff::new(Clef::Treble);
4944        assert_eq!(standard.presentation, StaffPresentation::default());
4945
4946        let percussion = Staff::new(Clef::Percussion);
4947        assert_eq!(percussion.presentation.kind, StaffKind::Percussion);
4948        assert_eq!(percussion.presentation.lines, 5);
4949        assert!(percussion.presentation.visible);
4950    }
4951
4952    // ── schema_version ────────────────────────────────────────────────────────
4953
4954    #[test]
4955    fn score_default_has_schema_version_1() {
4956        let score = Score::default();
4957        assert_eq!(score.schema_version, 1);
4958    }
4959
4960    #[test]
4961    fn score_new_has_schema_version_1() {
4962        let score = Score::new("T", 120, 4, 4, 0, 4);
4963        assert_eq!(score.schema_version, 1);
4964    }
4965
4966    #[test]
4967    fn score_without_schema_version_deserializes_to_zero() {
4968        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":[]}"#;
4969        let score: Score = serde_json::from_str(json).unwrap();
4970        assert_eq!(score.schema_version, 0);
4971    }
4972
4973    #[test]
4974    fn legacy_score_json_defaults_typed_spanners() {
4975        let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4976        let mut value = serde_json::to_value(score).expect("score serializes");
4977        value
4978            .as_object_mut()
4979            .expect("score is an object")
4980            .remove("spanners");
4981        let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4982        assert!(restored.spanners.is_empty());
4983    }
4984
4985    #[test]
4986    fn legacy_staff_json_defaults_presentation() {
4987        let score = Score::new("Legacy", 120, 4, 4, 0, 1);
4988        let mut value = serde_json::to_value(score).expect("score serializes");
4989        value["parts"][0]["staves"][0]
4990            .as_object_mut()
4991            .expect("staff is an object")
4992            .remove("presentation");
4993
4994        let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
4995        assert_eq!(
4996            restored.parts[0].staves[0].presentation,
4997            StaffPresentation::default()
4998        );
4999    }
5000
5001    #[test]
5002    fn legacy_part_json_defaults_instrument_definition() {
5003        let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5004        let mut value = serde_json::to_value(score).expect("score serializes");
5005        value["parts"][0]
5006            .as_object_mut()
5007            .expect("part is an object")
5008            .remove("instrument");
5009
5010        let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5011        assert!(restored.parts[0].instrument.is_none());
5012    }
5013
5014    #[test]
5015    fn resolve_view_projects_linked_part_without_mutating_source_score() {
5016        let mut score = Score::template(ScoreTemplate::StringQuartet);
5017        let mut view = ScoreView::linked_part("violin-2", "Violin II", 1);
5018        view.layout.measures_per_row = Some(2);
5019        score.views.push(view);
5020
5021        let projected = score.resolve_view("violin-2").expect("view resolves");
5022        assert_eq!(projected.parts.len(), 1);
5023        assert_eq!(projected.parts[0].name, "Violin II");
5024        assert_eq!(projected.views.len(), 1);
5025        assert_eq!(projected.views[0].parts, vec![0]);
5026        assert_eq!(score.parts.len(), 4);
5027        assert_eq!(score.views[0].parts, vec![1]);
5028    }
5029
5030    #[test]
5031    fn resolve_view_applies_linked_standard_and_tablature_presentations_non_destructively() {
5032        let mut score = Score::new("Guitar", 120, 4, 4, 0, 1);
5033        score.parts[0].staves[0].tablature = Some(TablatureConfig {
5034            lines: 6,
5035            tuning_midi: vec![40, 45, 50, 55, 59, 64],
5036            capo: 0,
5037        });
5038        score.parts[0].staves[0].presentation.kind = StaffKind::Standard;
5039        score.views.push(ScoreView::linked_tablature_staff(
5040            "guitar-tab",
5041            "Guitar Tab",
5042            0,
5043            0,
5044        ));
5045
5046        let projected = score.resolve_view("guitar-tab").expect("view resolves");
5047        assert_eq!(
5048            projected.parts[0].staves[0].presentation.kind,
5049            StaffKind::Tablature
5050        );
5051        assert_eq!(
5052            projected.views[0].staff_kind_overrides[0].staff,
5053            ViewStaffRef { part: 0, staff: 0 }
5054        );
5055        assert_eq!(
5056            score.parts[0].staves[0].presentation.kind,
5057            StaffKind::Standard
5058        );
5059    }
5060
5061    #[test]
5062    fn legacy_score_view_json_defaults_staff_kind_overrides() {
5063        let mut score = Score::new("Legacy", 120, 4, 4, 0, 1);
5064        score.views.push(ScoreView::linked_part("part", "Part", 0));
5065        let mut value = serde_json::to_value(score).expect("score serializes");
5066        value["views"][0]
5067            .as_object_mut()
5068            .expect("view is an object")
5069            .remove("staff_kind_overrides");
5070
5071        let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5072        assert!(restored.views[0].staff_kind_overrides.is_empty());
5073    }
5074
5075    #[test]
5076    fn typed_view_style_overrides_are_ordered_and_json_compatible() {
5077        let mut view = ScoreView::linked_part("part", "Part", 0);
5078        view.layout.typed_style_overrides = vec![
5079            ViewStyleOverride {
5080                property: ViewStyleProperty::TextScale,
5081                value: 0.9,
5082            },
5083            ViewStyleOverride {
5084                property: ViewStyleProperty::TextScale,
5085                value: 1.1,
5086            },
5087        ];
5088        assert_eq!(
5089            view.layout.style_value(ViewStyleProperty::TextScale),
5090            Some(1.1)
5091        );
5092        let restored: ScoreView =
5093            serde_json::from_str(&serde_json::to_string(&view).unwrap()).unwrap();
5094        assert_eq!(
5095            restored.layout.typed_style_overrides,
5096            view.layout.typed_style_overrides
5097        );
5098        assert_eq!(view.layout.resolved_style().text_scale, 1.1);
5099        assert_eq!(view.layout.resolved_style().system_gap, 2.0);
5100    }
5101
5102    #[test]
5103    fn score_style_defaults_are_inherited_then_overridden_by_view() {
5104        let mut score = Score::new("Style", 120, 4, 4, 0, 1);
5105        score.style_overrides = vec![
5106            ViewStyleOverride {
5107                property: ViewStyleProperty::StaffSpace,
5108                value: 1.2,
5109            },
5110            ViewStyleOverride {
5111                property: ViewStyleProperty::TextScale,
5112                value: 0.9,
5113            },
5114        ];
5115        let mut view = ScoreView::linked_part("part", "Part", 0);
5116        view.layout.typed_style_overrides.push(ViewStyleOverride {
5117            property: ViewStyleProperty::TextScale,
5118            value: 1.1,
5119        });
5120        let style = score.resolved_view_style(&view.layout);
5121        assert_eq!(style.staff_space, 1.2);
5122        assert_eq!(style.text_scale, 1.1);
5123        let restored: Score =
5124            serde_json::from_str(&serde_json::to_string(&score).unwrap()).unwrap();
5125        assert_eq!(restored.style_overrides, score.style_overrides);
5126    }
5127
5128    #[test]
5129    fn legacy_measure_json_defaults_source_voice_numbers() {
5130        let score = Score::new("Legacy", 120, 4, 4, 0, 1);
5131        let mut value = serde_json::to_value(&score).expect("score serializes");
5132        value["parts"][0]["staves"][0]["measures"][0]
5133            .as_object_mut()
5134            .expect("measure is an object")
5135            .remove("source_voice_numbers");
5136
5137        let restored: Score = serde_json::from_value(value).expect("legacy score deserializes");
5138        assert_eq!(
5139            restored.parts[0].staves[0].measures[0].source_voice_numbers,
5140            [None; 4]
5141        );
5142    }
5143
5144    #[test]
5145    fn legacy_measure_json_defaults_tempo_ramp() {
5146        let measure: Measure = serde_json::from_str(
5147            r#"{"number":1,"time_sig":null,"key_sig":null,"clef":null,"tempo":120,"barline_left":"Normal","barline_right":"Normal","voices":[[],[],[],[]]}"#,
5148        )
5149        .expect("legacy measure deserializes");
5150        assert_eq!(measure.tempo_ramp_to, None);
5151    }
5152
5153    #[test]
5154    fn note_without_new_percussion_fields_uses_serde_defaults() {
5155        let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5156        let mut value = serde_json::to_value(note).unwrap();
5157        let object = value.as_object_mut().unwrap();
5158        object.remove("is_unpitched");
5159        object.remove("instrument_id");
5160        let restored: Note = serde_json::from_value(value).unwrap();
5161        assert!(!restored.is_unpitched);
5162        assert_eq!(restored.instrument_id, None);
5163    }
5164
5165    #[test]
5166    fn percussion_instrument_resolution_prefers_id_then_display_key() {
5167        let mut part = Part::new("Drums", "Dr.");
5168        part.percussion_instruments = vec![
5169            PercussionInstrument {
5170                id: "snare".to_string(),
5171                name: Some("Acoustic Snare".to_string()),
5172                midi_unpitched: Some(38),
5173                staff_position: None,
5174                notehead: None,
5175                preferred_voice: None,
5176                techniques: Vec::new(),
5177            },
5178            PercussionInstrument {
5179                id: "rim".to_string(),
5180                name: Some("Side Stick".to_string()),
5181                midi_unpitched: Some(37),
5182                staff_position: None,
5183                notehead: None,
5184                preferred_voice: None,
5185                techniques: Vec::new(),
5186            },
5187        ];
5188        let mut note = Note::new(Pitch::from_midi(38, false), Duration::Quarter);
5189        note.is_unpitched = true;
5190        assert_eq!(
5191            part.percussion_instrument_for_note(&note)
5192                .map(|instrument| instrument.id.as_str()),
5193            Some("snare")
5194        );
5195        note.instrument_id = Some("rim".to_string());
5196        assert_eq!(
5197            part.percussion_instrument_for_note(&note)
5198                .map(|instrument| instrument.id.as_str()),
5199            Some("rim")
5200        );
5201        note.instrument_id = Some("missing".to_string());
5202        assert!(part.percussion_instrument_for_note(&note).is_none());
5203        note.instrument_id = None;
5204        note.is_unpitched = false;
5205        assert!(part.percussion_instrument_for_note(&note).is_none());
5206    }
5207
5208    #[test]
5209    fn percussion_kit_extensions_are_json_backward_compatible() {
5210        let mut instrument = PercussionInstrument {
5211            id: "snare".to_string(),
5212            name: Some("Acoustic Snare".to_string()),
5213            midi_unpitched: Some(38),
5214            staff_position: Some(0),
5215            notehead: Some(NoteHead::Cross),
5216            preferred_voice: Some(1),
5217            techniques: vec!["rim-shot".to_string()],
5218        };
5219        let mut value = serde_json::to_value(&instrument).expect("instrument serializes");
5220        let object = value
5221            .as_object_mut()
5222            .expect("percussion instrument is an object");
5223        object.remove("staff_position");
5224        object.remove("notehead");
5225        object.remove("preferred_voice");
5226        object.remove("techniques");
5227        instrument = serde_json::from_value(value).expect("legacy instrument deserializes");
5228        assert_eq!(instrument.staff_position, None);
5229        assert_eq!(instrument.notehead, None);
5230        assert_eq!(instrument.preferred_voice, None);
5231        assert!(instrument.techniques.is_empty());
5232    }
5233
5234    // ── ScoreTemplate ─────────────────────────────────────────────────────────
5235
5236    #[test]
5237    fn score_template_solo_has_one_part_treble() {
5238        let score = Score::template(ScoreTemplate::Solo);
5239        assert_eq!(score.parts.len(), 1);
5240        assert_eq!(score.parts[0].staves.len(), 1);
5241        assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5242        assert_eq!(score.parts[0].midi_program, 0);
5243    }
5244
5245    #[test]
5246    fn score_template_piano_has_two_staves() {
5247        let score = Score::template(ScoreTemplate::Piano);
5248        assert_eq!(score.parts.len(), 1);
5249        assert_eq!(score.parts[0].staves.len(), 2);
5250        assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
5251        assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
5252    }
5253
5254    #[test]
5255    fn score_template_string_quartet_has_four_parts() {
5256        let score = Score::template(ScoreTemplate::StringQuartet);
5257        assert_eq!(score.parts.len(), 4);
5258        assert_eq!(score.parts[2].staves[0].clef, Clef::Alto); // Viola
5259        assert_eq!(score.parts[3].staves[0].clef, Clef::Bass); // Cello
5260        assert_eq!(score.parts[0].midi_program, 40);
5261        assert_eq!(score.parts[3].midi_program, 42);
5262    }
5263
5264    #[test]
5265    fn score_template_string_orchestra_has_five_parts() {
5266        let score = Score::template(ScoreTemplate::StringOrchestra);
5267        assert_eq!(score.parts.len(), 5);
5268        assert_eq!(score.parts[4].midi_program, 43); // Contrabass
5269    }
5270
5271    #[test]
5272    fn score_template_brass_quintet_has_five_parts() {
5273        let score = Score::template(ScoreTemplate::BrassQuintet);
5274        assert_eq!(score.parts.len(), 5);
5275        assert_eq!(score.parts[2].midi_program, 60); // French Horn
5276    }
5277
5278    #[test]
5279    fn score_template_default_measures_are_four() {
5280        let score = Score::template(ScoreTemplate::StringQuartet);
5281        for part in &score.parts {
5282            for staff in &part.staves {
5283                assert_eq!(staff.measures.len(), 4);
5284            }
5285        }
5286    }
5287
5288    // ── system_break / page_break ─────────────────────────────────────────────
5289
5290    #[test]
5291    fn measure_empty_has_no_breaks() {
5292        let m = Measure::empty(4, 4);
5293        assert!(!m.system_break);
5294        assert!(!m.page_break);
5295        assert!(!m.section_break);
5296    }
5297
5298    #[test]
5299    fn section_range_uses_semantic_boundaries_not_layout_breaks() {
5300        let mut score = Score::new("sections", 120, 4, 4, 0, 6);
5301        score.parts[0].staves[0].measures[0].section_break = true;
5302        score.parts[0].staves[0].measures[2].section_break = true;
5303        score.parts[0].staves[0].measures[4].section_break = true;
5304        score.parts[0].staves[0].measures[5].section_break = true;
5305        score.parts[0].staves[0].measures[1].system_break = true;
5306        score.parts[0].staves[0].measures[3].page_break = true;
5307        assert_eq!(score.section_range(0).unwrap(), 0..=1);
5308        assert_eq!(score.section_range(3).unwrap(), 2..=3);
5309        assert_eq!(score.section_range(4).unwrap(), 4..=4);
5310        assert_eq!(score.section_range(5).unwrap(), 5..=5);
5311        assert!(score.section_range(6).is_err());
5312    }
5313
5314    #[test]
5315    fn system_break_survives_json_roundtrip() {
5316        let mut m = Measure::empty(4, 4);
5317        m.system_break = true;
5318        let json = serde_json::to_string(&m).unwrap();
5319        let m2: Measure = serde_json::from_str(&json).unwrap();
5320        assert!(m2.system_break);
5321        assert!(!m2.page_break);
5322    }
5323
5324    // ── diff ──────────────────────────────────────────────────────────────────
5325
5326    #[test]
5327    fn diff_identical_scores_is_empty() {
5328        let s = Score::new("T", 120, 4, 4, 0, 2);
5329        assert!(diff(&s, &s).is_empty());
5330    }
5331
5332    #[test]
5333    fn score_patch_covers_measure_semantics_and_note_insert_index() {
5334        let mut a = Score::new("T", 120, 4, 4, 0, 1);
5335        a.parts[0].staves[0].measures[0].voices[0].clear();
5336        let mut b = a.clone();
5337        let measure = &mut b.parts[0].staves[0].measures[0];
5338        measure.key_sig = Some(KeySignature {
5339            fifths: -2,
5340            mode: "major".to_string(),
5341        });
5342        measure.time_sig = Some(TimeSignature {
5343            numerator: 3,
5344            denominator: 4,
5345        });
5346        measure.barline_left = Barline::RepeatStart;
5347        measure.barline_right = Barline::RepeatEnd;
5348        measure.rehearsal = Some("A".to_string());
5349        measure.volta = Some(VoltaBracket {
5350            number: 1,
5351            kind: "begin_end".to_string(),
5352        });
5353        measure.texts.push(StyledText {
5354            style: TextStyle::RehearsalMark,
5355            text: "A".to_string(),
5356            placement: None,
5357            offset_x: None,
5358            offset_y: None,
5359            relative_x: None,
5360            relative_y: None,
5361        });
5362        measure.figured_bass.push(FiguredBassFigure {
5363            number: "6".to_string(),
5364            alter: None,
5365            prefix: None,
5366            suffix: None,
5367            extender: false,
5368        });
5369        measure.harp_pedal_diagrams.push(HarpPedalDiagram {
5370            positions: [
5371                HarpPedalPosition::Flat,
5372                HarpPedalPosition::Natural,
5373                HarpPedalPosition::Sharp,
5374                HarpPedalPosition::Natural,
5375                HarpPedalPosition::Flat,
5376                HarpPedalPosition::Sharp,
5377                HarpPedalPosition::Natural,
5378            ],
5379            placement: Some("above".to_string()),
5380        });
5381        measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
5382        let expected = b.parts[0].staves[0].measures[0].clone();
5383
5384        let patches = score_patch(&a, &b);
5385        assert!(
5386            patches
5387                .iter()
5388                .any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
5389        );
5390        assert!(
5391            patches
5392                .iter()
5393                .any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
5394        );
5395        assert!(
5396            patches
5397                .iter()
5398                .any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
5399        );
5400        assert!(
5401            patches
5402                .iter()
5403                .any(|p| matches!(p, ScorePatch::SetVolta { .. }))
5404        );
5405        assert!(
5406            patches
5407                .iter()
5408                .any(|p| matches!(p, ScorePatch::SetMeasureTexts { .. }))
5409        );
5410        assert!(
5411            patches
5412                .iter()
5413                .any(|p| matches!(p, ScorePatch::SetFiguredBass { .. }))
5414        );
5415        assert!(
5416            patches
5417                .iter()
5418                .any(|p| matches!(p, ScorePatch::SetHarpPedalDiagrams { .. }))
5419        );
5420        let result = apply_patch(&a, &patches).expect("patch application failed");
5421        let result_measure = &result.parts[0].staves[0].measures[0];
5422        assert_eq!(result_measure.key_sig, expected.key_sig);
5423        assert_eq!(result_measure.time_sig, expected.time_sig);
5424        assert_eq!(result_measure.barline_left, expected.barline_left);
5425        assert_eq!(result_measure.barline_right, expected.barline_right);
5426        assert_eq!(result_measure.rehearsal, expected.rehearsal);
5427        assert_eq!(result_measure.volta, expected.volta);
5428        assert_eq!(result_measure.texts, expected.texts);
5429        assert_eq!(result_measure.figured_bass, expected.figured_bass);
5430        assert_eq!(
5431            result_measure.harp_pedal_diagrams,
5432            expected.harp_pedal_diagrams
5433        );
5434        assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
5435    }
5436
5437    #[test]
5438    fn diff_and_patch_preserve_score_level_texts() {
5439        let a = Score::new("T", 120, 4, 4, 0, 1);
5440        let mut b = a.clone();
5441        b.texts.push(StyledText {
5442            style: TextStyle::Expression,
5443            text: "Prelude".to_string(),
5444            placement: Some("above".to_string()),
5445            offset_x: Some(12.0),
5446            offset_y: Some(-8.0),
5447            relative_x: None,
5448            relative_y: None,
5449        });
5450
5451        let changes = diff(&a, &b);
5452        assert!(changes.iter().any(|change| matches!(
5453            change,
5454            ScoreChange::ScoreTextChanged { old, new }
5455                if old.is_empty() && new == &b.texts
5456        )));
5457
5458        let patches = score_patch(&a, &b);
5459        assert!(patches.iter().any(|patch| matches!(
5460            patch,
5461            ScorePatch::SetScoreTexts { value } if value == &b.texts
5462        )));
5463        let result = apply_patch(&a, &patches).expect("score text patch failed");
5464        assert_eq!(result.texts, b.texts);
5465    }
5466
5467    #[test]
5468    fn measure_presentation_changes_use_typed_diff_and_local_patch() {
5469        let a = Score::new("T", 120, 4, 4, 0, 1);
5470        let mut b = a.clone();
5471        let measure = &mut b.parts[0].staves[0].measures[0];
5472        measure.number = 8;
5473        measure.clef = Some(Clef::Bass);
5474        measure.tempo_text = Some("Allegro".to_string());
5475        measure.navigation = Some("D.S.".to_string());
5476        measure.expression_text = Some("espressivo".to_string());
5477        measure.multi_rest_count = Some(3);
5478        measure.system_break = true;
5479        measure.page_break = true;
5480
5481        let changes = diff(&a, &b);
5482        assert!(changes.iter().any(|change| matches!(
5483            change,
5484            ScoreChange::MeasurePresentationChanged {
5485                part: 0,
5486                staff: 0,
5487                measure: 0,
5488                old_number: 1,
5489                new_number: 8,
5490                old_clef: None,
5491                new_clef: Some(Clef::Bass),
5492                new_tempo_text: Some(text),
5493                new_navigation: Some(navigation),
5494                new_expression_text: Some(expression),
5495                new_multi_rest_count: Some(3),
5496                old_system_break: false,
5497                new_system_break: true,
5498                old_page_break: false,
5499                new_page_break: true,
5500                ..
5501            } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5502        )));
5503        assert!(!changes.iter().any(|change| matches!(
5504            change,
5505            ScoreChange::UnrepresentedFieldChanged { path }
5506                if path == "parts[0].staves[0].measures[0].number"
5507        )));
5508
5509        let patches = score_patch(&a, &b);
5510        assert!(patches.iter().any(|patch| matches!(
5511            patch,
5512            ScorePatch::SetMeasurePresentation {
5513                part: 0,
5514                staff: 0,
5515                measure: 0,
5516                number: 8,
5517                clef: Some(Clef::Bass),
5518                tempo_text: Some(text),
5519                navigation: Some(navigation),
5520                expression_text: Some(expression),
5521                multi_rest_count: Some(3),
5522                system_break: true,
5523                page_break: true,
5524                section_break: false,
5525            } if text == "Allegro" && navigation == "D.S." && expression == "espressivo"
5526        )));
5527        assert!(
5528            !patches
5529                .iter()
5530                .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5531        );
5532        let json = serde_json::to_string(&patches).expect("measure presentation patch JSON");
5533        let decoded: Vec<ScorePatch> =
5534            serde_json::from_str(&json).expect("measure presentation patch should decode");
5535        let mut legacy_json: serde_json::Value =
5536            serde_json::from_str(&json).expect("patch JSON value");
5537        for patch in legacy_json.as_array_mut().expect("patch JSON array") {
5538            patch
5539                .as_object_mut()
5540                .expect("patch JSON object")
5541                .remove("section_break");
5542        }
5543        let legacy: Vec<ScorePatch> =
5544            serde_json::from_value(legacy_json).expect("legacy patch should decode");
5545        assert!(matches!(
5546            legacy.first(),
5547            Some(ScorePatch::SetMeasurePresentation {
5548                section_break: false,
5549                ..
5550            })
5551        ));
5552        let result = apply_patch(&a, &decoded).expect("measure presentation patch failed");
5553        assert_eq!(result.parts[0].staves[0].measures[0].number, 8);
5554        assert_eq!(result.parts[0].staves[0].measures[0].clef, Some(Clef::Bass));
5555        assert_eq!(
5556            serde_json::to_value(&result).expect("patched score JSON"),
5557            serde_json::to_value(&b).expect("expected score JSON")
5558        );
5559    }
5560
5561    #[test]
5562    fn chord_definition_changes_use_typed_diff_and_local_patch() {
5563        let a = Score::new("T", 120, 4, 4, 0, 1);
5564        let mut b = a.clone();
5565        b.chord_definitions.push(ChordDefinition {
5566            id: Some("c-major".to_string()),
5567            label: Some("C".to_string()),
5568            kind: Some("major".to_string()),
5569            fret_position: Some(0),
5570            tab_strings: Some("x32010".to_string()),
5571            tab_courses: None,
5572            members: Vec::new(),
5573            barres: Vec::new(),
5574        });
5575
5576        let changes = diff(&a, &b);
5577        assert!(changes.iter().any(|change| matches!(
5578            change,
5579            ScoreChange::ChordDefinitionsChanged { old, new }
5580                if old.is_empty() && new == &b.chord_definitions
5581        )));
5582        assert!(!changes.iter().any(|change| matches!(
5583            change,
5584            ScoreChange::UnrepresentedFieldChanged { path }
5585                if path == "chord_definitions"
5586        )));
5587
5588        let patches = score_patch(&a, &b);
5589        assert!(patches.iter().any(|patch| matches!(
5590            patch,
5591            ScorePatch::SetChordDefinitions { value } if value == &b.chord_definitions
5592        )));
5593        assert!(
5594            !patches
5595                .iter()
5596                .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5597        );
5598        let json = serde_json::to_string(&patches).expect("chord definition patch JSON");
5599        let decoded: Vec<ScorePatch> =
5600            serde_json::from_str(&json).expect("chord definition patch should decode");
5601        let result = apply_patch(&a, &decoded).expect("chord definition patch failed");
5602        assert_eq!(result.chord_definitions, b.chord_definitions);
5603    }
5604
5605    #[test]
5606    fn part_name_changes_use_typed_diff_and_local_patch() {
5607        let a = Score::new("T", 120, 4, 4, 0, 1);
5608        let mut b = a.clone();
5609        b.parts[0].name = "Violin".to_string();
5610        b.parts[0].short_name = "Vln.".to_string();
5611        b.parts[0].midi_channel = 4;
5612        b.parts[0].midi_program = 40;
5613        b.parts[0].midi_pitch_bends.push(MidiPitchBend {
5614            tick: 120,
5615            channel: 4,
5616            value: 2048,
5617        });
5618        b.parts[0].midi_control_changes.push(MidiControlChange {
5619            tick: 240,
5620            channel: 4,
5621            controller: 64,
5622            value: 127,
5623        });
5624        b.parts[0].midi_program_changes.push(MidiProgramChange {
5625            tick: 0,
5626            channel: 4,
5627            program: 40,
5628        });
5629        b.parts[0].midi_aftertouch.push(MidiAftertouch {
5630            tick: 360,
5631            channel: 4,
5632            key: Some(64),
5633            value: 80,
5634        });
5635        b.parts[0].staves[0].clef = Clef::Bass;
5636        b.parts[0].staves[0].transpose_semitones = -2;
5637
5638        let changes = diff(&a, &b);
5639        assert!(changes.iter().any(|change| matches!(
5640            change,
5641            ScoreChange::PartNamesChanged {
5642                part: 0,
5643                old_name,
5644                new_name,
5645                old_short_name,
5646                new_short_name,
5647            } if old_name == "Piano"
5648                && new_name == "Violin"
5649                && old_short_name == "Pno."
5650                && new_short_name == "Vln."
5651        )));
5652        assert!(changes.iter().any(|change| matches!(
5653            change,
5654            ScoreChange::PartMidiChanged {
5655                part: 0,
5656                old_channel: 0,
5657                new_channel: 4,
5658                old_program: 0,
5659                new_program: 40,
5660            }
5661        )));
5662        assert!(changes.iter().any(|change| matches!(
5663            change,
5664            ScoreChange::PartMidiAutomationChanged {
5665                part: 0,
5666                new_pitch_bends,
5667                new_control_changes,
5668                new_program_changes,
5669                new_aftertouch,
5670                ..
5671            } if new_pitch_bends == &b.parts[0].midi_pitch_bends
5672                && new_control_changes == &b.parts[0].midi_control_changes
5673                && new_program_changes == &b.parts[0].midi_program_changes
5674                && new_aftertouch == &b.parts[0].midi_aftertouch
5675        )));
5676        assert!(changes.iter().any(|change| matches!(
5677            change,
5678            ScoreChange::StaffConfigurationChanged {
5679                part: 0,
5680                staff: 0,
5681                old_clef: Clef::Treble,
5682                new_clef: Clef::Bass,
5683                old_transpose_semitones: 0,
5684                new_transpose_semitones: -2,
5685            }
5686        )));
5687        assert!(!changes.iter().any(|change| matches!(
5688            change,
5689            ScoreChange::UnrepresentedFieldChanged { path }
5690                if path == "parts[0].name"
5691        )));
5692
5693        let patches = score_patch(&a, &b);
5694        assert!(patches.iter().any(|patch| matches!(
5695            patch,
5696            ScorePatch::SetPartNames {
5697                part: 0,
5698                name,
5699                short_name,
5700            } if name == "Violin" && short_name == "Vln."
5701        )));
5702        assert!(
5703            !patches
5704                .iter()
5705                .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5706        );
5707        assert!(patches.iter().any(|patch| matches!(
5708            patch,
5709            ScorePatch::SetPartMidi {
5710                part: 0,
5711                channel: 4,
5712                program: 40,
5713            }
5714        )));
5715        assert!(patches.iter().any(|patch| matches!(
5716            patch,
5717            ScorePatch::SetPartMidiAutomation {
5718                part: 0,
5719                pitch_bends,
5720                control_changes,
5721                program_changes,
5722                aftertouch,
5723            } if pitch_bends == &b.parts[0].midi_pitch_bends
5724                && control_changes == &b.parts[0].midi_control_changes
5725                && program_changes == &b.parts[0].midi_program_changes
5726                && aftertouch == &b.parts[0].midi_aftertouch
5727        )));
5728        assert!(patches.iter().any(|patch| matches!(
5729            patch,
5730            ScorePatch::SetStaffConfiguration {
5731                part: 0,
5732                staff: 0,
5733                clef: Clef::Bass,
5734                transpose_semitones: -2,
5735            }
5736        )));
5737        let result = apply_patch(&a, &patches).expect("part name patch failed");
5738        assert_eq!(result.parts[0].name, b.parts[0].name);
5739        assert_eq!(result.parts[0].short_name, b.parts[0].short_name);
5740        assert_eq!(result.parts[0].midi_channel, b.parts[0].midi_channel);
5741        assert_eq!(result.parts[0].midi_program, b.parts[0].midi_program);
5742        assert_eq!(
5743            result.parts[0].midi_pitch_bends,
5744            b.parts[0].midi_pitch_bends
5745        );
5746        assert_eq!(
5747            result.parts[0].midi_control_changes,
5748            b.parts[0].midi_control_changes
5749        );
5750        assert_eq!(
5751            result.parts[0].midi_program_changes,
5752            b.parts[0].midi_program_changes
5753        );
5754        assert_eq!(result.parts[0].midi_aftertouch, b.parts[0].midi_aftertouch);
5755        assert_eq!(result.parts[0].staves[0].clef, b.parts[0].staves[0].clef);
5756        assert_eq!(
5757            result.parts[0].staves[0].transpose_semitones,
5758            b.parts[0].staves[0].transpose_semitones
5759        );
5760    }
5761
5762    #[test]
5763    fn diff_reports_measure_text_and_figured_bass_changes() {
5764        let a = Score::new("T", 120, 4, 4, 0, 1);
5765        let mut b = a.clone();
5766        let measure = &mut b.parts[0].staves[0].measures[0];
5767        measure.texts.push(StyledText {
5768            style: TextStyle::Lyrics,
5769            text: "la".to_string(),
5770            placement: None,
5771            offset_x: None,
5772            offset_y: None,
5773            relative_x: None,
5774            relative_y: None,
5775        });
5776        measure.figured_bass.push(FiguredBassFigure {
5777            number: "6".to_string(),
5778            alter: None,
5779            prefix: None,
5780            suffix: None,
5781            extender: false,
5782        });
5783
5784        let changes = diff(&a, &b);
5785        assert!(changes.iter().any(|change| matches!(
5786            change,
5787            ScoreChange::MeasureTextChanged { part: 0, staff: 0, measure: 0, old, new }
5788                if old.is_empty() && new.len() == 1
5789        )));
5790        assert!(changes.iter().any(|change| matches!(
5791            change,
5792            ScoreChange::FiguredBassChanged { part: 0, staff: 0, measure: 0, old, new }
5793                if old.is_empty() && new.len() == 1
5794        )));
5795    }
5796
5797    #[test]
5798    fn diff_reports_tablature_changes_and_patches_them_locally() {
5799        let a = Score::new("T", 120, 4, 4, 0, 1);
5800        let mut b = a.clone();
5801        b.parts[0].staves[0].tablature = Some(TablatureConfig {
5802            lines: 6,
5803            tuning_midi: vec![40, 45, 50, 55, 59, 64],
5804            capo: 2,
5805        });
5806
5807        let changes = diff(&a, &b);
5808        assert!(!changes.iter().any(|change| matches!(
5809            change,
5810            ScoreChange::UnrepresentedFieldChanged { path }
5811                if path == "parts[0].staves[0].tablature"
5812        )));
5813        assert!(changes.iter().any(|change| matches!(
5814            change,
5815            ScoreChange::TablatureConfigChanged { part: 0, staff: 0, old: None, new: Some(config) }
5816                if config.lines == 6 && config.capo == 2
5817        )));
5818        let patches = score_patch(&a, &b);
5819        assert!(patches.iter().any(|patch| matches!(
5820            patch,
5821            ScorePatch::SetTablatureConfig { part: 0, staff: 0, value: Some(config) }
5822                if config.lines == 6 && config.capo == 2
5823        )));
5824        assert_eq!(
5825            apply_patch(&a, &patches).unwrap().parts[0].staves[0].tablature,
5826            b.parts[0].staves[0].tablature
5827        );
5828    }
5829
5830    #[test]
5831    fn diff_reports_measure_tablature_changes_and_patches_them_locally() {
5832        let mut a = Score::new("T", 120, 4, 4, 0, 2);
5833        a.parts[0].staves[0].tablature = Some(TablatureConfig {
5834            lines: 6,
5835            tuning_midi: vec![40, 45, 50, 55, 59, 64],
5836            capo: 0,
5837        });
5838        let mut b = a.clone();
5839        b.parts[0].staves[0].measures[1].tablature_change = Some(TablatureConfig {
5840            lines: 6,
5841            tuning_midi: vec![40, 45, 50, 55, 59, 64],
5842            capo: 3,
5843        });
5844
5845        let changes = diff(&a, &b);
5846        assert!(changes.iter().any(|change| matches!(
5847            change,
5848            ScoreChange::TablatureChangeChanged {
5849                part: 0,
5850                staff: 0,
5851                measure: 1,
5852                old: None,
5853                new: Some(config),
5854            } if config.capo == 3
5855        )));
5856        let patches = score_patch(&a, &b);
5857        assert!(patches.iter().any(|patch| matches!(
5858            patch,
5859            ScorePatch::SetMeasureTablatureChange {
5860                part: 0,
5861                staff: 0,
5862                measure: 1,
5863                value: Some(config),
5864            } if config.capo == 3
5865        )));
5866        assert_eq!(
5867            apply_patch(&a, &patches).unwrap().parts[0].staves[0].measures[1].tablature_change,
5868            b.parts[0].staves[0].measures[1].tablature_change
5869        );
5870    }
5871
5872    #[test]
5873    fn diff_reports_measure_tempo_ramps_and_patches_them_locally() {
5874        let a = Score::new("Ramp", 120, 4, 4, 0, 2);
5875        let mut b = a.clone();
5876        b.parts[0].staves[0].measures[1].tempo_ramp_to = Some(72);
5877
5878        assert!(diff(&a, &b).iter().any(|change| matches!(
5879            change,
5880            ScoreChange::MeasureTempoRampChanged {
5881                part: 0,
5882                staff: 0,
5883                measure: 1,
5884                old: None,
5885                new: Some(72),
5886            }
5887        )));
5888        let patches = score_patch(&a, &b);
5889        assert!(patches.iter().any(|patch| matches!(
5890            patch,
5891            ScorePatch::SetMeasureTempoRamp {
5892                part: 0,
5893                staff: 0,
5894                measure: 1,
5895                value: Some(72),
5896            }
5897        )));
5898        assert_eq!(
5899            apply_patch(&a, &patches).expect("patch applies").parts[0].staves[0].measures[1]
5900                .tempo_ramp_to,
5901            b.parts[0].staves[0].measures[1].tempo_ramp_to
5902        );
5903    }
5904
5905    #[test]
5906    fn diff_reports_staff_presentation_changes_and_patches_them_locally() {
5907        let a = Score::new("T", 120, 4, 4, 0, 1);
5908        let mut b = a.clone();
5909        b.parts[0].staves[0].presentation = StaffPresentation {
5910            kind: StaffKind::Percussion,
5911            lines: 1,
5912            line_distance: 1.5,
5913            small: true,
5914            cutaway: true,
5915            visible: false,
5916            notehead_scheme: StaffNoteheadScheme::PitchNames,
5917            tablature_rhythm_display: TablatureRhythmDisplay::FretOnly,
5918            tablature_fret_mark_style: TablatureFretMarkStyle::Arabic,
5919        };
5920
5921        let changes = diff(&a, &b);
5922        assert!(changes.iter().any(|change| matches!(
5923            change,
5924            ScoreChange::StaffPresentationChanged { part: 0, staff: 0, new, .. }
5925                if new == &b.parts[0].staves[0].presentation
5926        )));
5927        let patches = score_patch(&a, &b);
5928        assert!(patches.iter().any(|patch| matches!(
5929            patch,
5930            ScorePatch::SetStaffPresentation { part: 0, staff: 0, value }
5931                if value == &b.parts[0].staves[0].presentation
5932        )));
5933        assert!(
5934            !patches
5935                .iter()
5936                .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5937        );
5938        assert_eq!(
5939            apply_patch(&a, &patches).unwrap().parts[0].staves[0].presentation,
5940            b.parts[0].staves[0].presentation
5941        );
5942    }
5943
5944    #[test]
5945    fn score_patch_uses_local_presentation_patch_for_display_fields() {
5946        let a = Score::new("T", 120, 4, 4, 0, 1);
5947        let mut b = a.clone();
5948        b.parts[0].name = "Piano".to_string();
5949        b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
5950        let patches = score_patch(&a, &b);
5951        assert!(patches.iter().any(|patch| matches!(
5952            patch,
5953            ScorePatch::SetMeasurePresentation {
5954                part: 0,
5955                staff: 0,
5956                measure: 0,
5957                expression_text: Some(text),
5958                ..
5959            } if text == "dolce"
5960        )));
5961        assert!(
5962            !patches
5963                .iter()
5964                .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
5965        );
5966        let result = apply_patch(&a, &patches).expect("measure presentation patch failed");
5967        assert_eq!(result.parts[0].name, "Piano");
5968        assert_eq!(
5969            result.parts[0].staves[0].measures[0].expression_text,
5970            Some("dolce".to_string())
5971        );
5972    }
5973
5974    #[test]
5975    fn diff_detects_tempo_change() {
5976        let a = Score::new("T", 120, 4, 4, 0, 1);
5977        let mut b = a.clone();
5978        b.settings.tempo_bpm = 90;
5979        let changes = diff(&a, &b);
5980        assert_eq!(changes.len(), 1);
5981        assert!(matches!(
5982            changes[0],
5983            ScoreChange::TempoChanged { old: 120, new: 90 }
5984        ));
5985    }
5986
5987    #[test]
5988    fn diff_detects_title_change() {
5989        let a = Score::new("Old Title", 120, 4, 4, 0, 1);
5990        let mut b = a.clone();
5991        b.metadata.title = "New Title".to_string();
5992        let changes = diff(&a, &b);
5993        assert!(
5994            changes.iter().any(
5995                |c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
5996            )
5997        );
5998    }
5999
6000    #[test]
6001    fn diff_detects_note_modification() {
6002        let mut a = Score::new("T", 120, 4, 4, 0, 1);
6003        a.parts[0].staves[0].measures[0].voices[0] =
6004            vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6005        let mut b = a.clone();
6006        b.parts[0].staves[0].measures[0].voices[0][0] =
6007            Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
6008        let changes = diff(&a, &b);
6009        assert!(
6010            changes
6011                .iter()
6012                .any(|c| matches!(c, ScoreChange::NoteModified { .. }))
6013        );
6014    }
6015
6016    #[test]
6017    fn diff_detects_part_added() {
6018        let a = Score::new("T", 120, 4, 4, 0, 1);
6019        let mut b = a.clone();
6020        let mut p = Part::new("Violin", "Vln.");
6021        p.staves.push(Staff::new(Clef::Treble));
6022        b.parts.push(p);
6023        let changes = diff(&a, &b);
6024        assert!(
6025            changes
6026                .iter()
6027                .any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
6028        );
6029    }
6030
6031    #[test]
6032    fn diff_detects_measure_tempo_change() {
6033        let a = Score::new("T", 120, 4, 4, 0, 2);
6034        let mut b = a.clone();
6035        b.parts[0].staves[0].measures[1].tempo = Some(60);
6036        let changes = diff(&a, &b);
6037        assert!(changes.iter().any(|c| matches!(
6038            c,
6039            ScoreChange::MeasureTempoChanged {
6040                measure: 1,
6041                old: None,
6042                new: Some(60),
6043                ..
6044            }
6045        )));
6046    }
6047
6048    #[test]
6049    fn diff_detects_barline_change() {
6050        use crate::model::notation::Barline;
6051        let a = Score::new("T", 120, 4, 4, 0, 2);
6052        let mut b = a.clone();
6053        b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
6054        let changes = diff(&a, &b);
6055        assert!(
6056            changes
6057                .iter()
6058                .any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
6059        );
6060    }
6061
6062    #[test]
6063    fn diff_detects_rehearsal_change() {
6064        let a = Score::new("T", 120, 4, 4, 0, 2);
6065        let mut b = a.clone();
6066        b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
6067        let changes = diff(&a, &b);
6068        assert!(
6069            changes
6070                .iter()
6071                .any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
6072        );
6073    }
6074
6075    #[test]
6076    fn diff_detects_volta_change() {
6077        use super::VoltaBracket;
6078        let a = Score::new("T", 120, 4, 4, 0, 2);
6079        let mut b = a.clone();
6080        b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
6081            number: 1,
6082            kind: "begin_end".into(),
6083        });
6084        let changes = diff(&a, &b);
6085        assert!(
6086            changes
6087                .iter()
6088                .any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
6089        );
6090    }
6091
6092    #[test]
6093    fn diff_detects_key_signature_change() {
6094        let a = Score::new("T", 120, 4, 4, 0, 1);
6095        let mut b = a.clone();
6096        b.settings.key_signature.fifths = 2; // C major → D major
6097        let changes = diff(&a, &b);
6098        assert!(
6099            changes
6100                .iter()
6101                .any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
6102        );
6103    }
6104
6105    #[test]
6106    fn measure_key_signature_uses_typed_local_patch() {
6107        let a = Score::new("T", 120, 4, 4, 0, 1);
6108        let mut b = a.clone();
6109        b.parts[0].staves[0].measures[0].key_sig = Some(KeySignature {
6110            fifths: 2,
6111            mode: "major".to_string(),
6112        });
6113        let changes = diff(&a, &b);
6114        assert!(!changes.iter().any(|change| matches!(
6115            change,
6116            ScoreChange::UnrepresentedFieldChanged { path }
6117                if path == "parts[0].staves[0].measures[0].key_sig"
6118        )));
6119        let patches = score_patch(&a, &b);
6120        assert!(patches.iter().any(|patch| matches!(
6121            patch,
6122            ScorePatch::SetKeySignature {
6123                part: 0,
6124                staff: 0,
6125                measure: 0,
6126                value: Some(KeySignature { fifths: 2, .. }),
6127            }
6128        )));
6129        assert!(
6130            !patches
6131                .iter()
6132                .any(|patch| matches!(patch, ScorePatch::ReplaceScore { .. }))
6133        );
6134    }
6135
6136    #[test]
6137    fn diff_same_key_signature_no_change() {
6138        let a = Score::new("T", 120, 4, 4, 2, 1);
6139        let changes = diff(&a, &a);
6140        assert!(changes.is_empty());
6141    }
6142
6143    #[test]
6144    fn score_duration_secs_region_partial() {
6145        use super::score_duration_secs_region;
6146        // 4/4, 120 BPM, 4 measures → each measure = 2.0 s; region [1,2] = 4.0 s
6147        let score = Score::new("T", 120, 4, 4, 0, 4);
6148        let secs = score_duration_secs_region(&score, (1, 2));
6149        assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
6150    }
6151
6152    #[test]
6153    fn score_duration_secs_region_single_measure() {
6154        use super::score_duration_secs_region;
6155        // 4/4, 120 BPM → 1 measure = 2.0 s
6156        let score = Score::new("T", 120, 4, 4, 0, 4);
6157        let secs = score_duration_secs_region(&score, (0, 0));
6158        assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
6159    }
6160
6161    // ── measure_beats_remaining ───────────────────────────────────────────────
6162
6163    #[test]
6164    fn measure_beats_remaining_empty_voice_returns_full() {
6165        use super::measure_beats_remaining;
6166        let mut score = Score::new("T", 120, 4, 4, 0, 1);
6167        score.parts[0].staves[0].measures[0].voices[0].clear();
6168        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6169        assert!(
6170            (rem - 4.0).abs() < 1e-9,
6171            "expected 4.0 remaining, got {rem}"
6172        );
6173    }
6174
6175    #[test]
6176    fn measure_beats_remaining_half_full_returns_half() {
6177        use super::measure_beats_remaining;
6178        use crate::model::pitch::Step;
6179        let mut score = Score::new("T", 120, 4, 4, 0, 1);
6180        score.parts[0].staves[0].measures[0].voices[0] = vec![
6181            Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6182            Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
6183        ];
6184        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6185        assert!(
6186            (rem - 2.0).abs() < 1e-9,
6187            "expected 2.0 remaining, got {rem}"
6188        );
6189    }
6190
6191    #[test]
6192    fn measure_beats_remaining_full_voice_returns_zero() {
6193        use super::measure_beats_remaining;
6194        use crate::model::pitch::Step;
6195        let mut score = Score::new("T", 120, 4, 4, 0, 1);
6196        score.parts[0].staves[0].measures[0].voices[0] =
6197            vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
6198        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6199        assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
6200    }
6201
6202    #[test]
6203    fn measure_beats_remaining_tuplet_accounting() {
6204        use super::measure_beats_remaining;
6205        use crate::model::notation::TupletInfo;
6206        use crate::model::pitch::Step;
6207        // 3 quarter-note triplets each take 2/3 of a beat → total 2.0 beats used → 2.0 remaining
6208        let mut score = Score::new("T", 120, 4, 4, 0, 1);
6209        let tuplet = TupletInfo {
6210            actual_notes: 3,
6211            normal_notes: 2,
6212        };
6213        let mk = |step| {
6214            let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
6215            n.tuplet = Some(tuplet.clone());
6216            n
6217        };
6218        score.parts[0].staves[0].measures[0].voices[0] =
6219            vec![mk(Step::C), mk(Step::D), mk(Step::E)];
6220        let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
6221        assert!(
6222            (rem - 2.0).abs() < 1e-9,
6223            "expected 2.0 remaining (triplets used 2.0), got {rem}"
6224        );
6225    }
6226
6227    #[test]
6228    fn measure_beats_remaining_out_of_range_returns_err() {
6229        use super::measure_beats_remaining;
6230        let score = Score::new("T", 120, 4, 4, 0, 1);
6231        assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
6232        assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
6233        assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
6234        assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
6235    }
6236
6237    #[test]
6238    fn note_content_eq_ignores_id() {
6239        use crate::model::pitch::Step;
6240        let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
6241        let mut b = a.clone();
6242        b.id = "different-id".to_string();
6243        assert!(note_content_eq(&a, &b));
6244        // Actual pitch change should differ
6245        b.pitches[0] = Pitch::new(Step::D, 4);
6246        assert!(!note_content_eq(&a, &b));
6247        // stem_up difference
6248        let mut c = a.clone();
6249        a.stem_up = Some(true);
6250        c.stem_up = Some(false);
6251        assert!(!note_content_eq(&a, &c));
6252    }
6253
6254    #[test]
6255    fn suggested_stem_up_below_middle() {
6256        use crate::model::notation::Clef;
6257        // C4 = MIDI 60, Treble middle = B4 = 71 → stem up
6258        let pitches = vec![Pitch::new(Step::C, 4)];
6259        assert!(suggested_stem_up(&pitches, &Clef::Treble));
6260    }
6261
6262    #[test]
6263    fn suggested_stem_up_above_middle() {
6264        use crate::model::notation::Clef;
6265        // G5 = MIDI 79, Treble middle = 71 → stem down
6266        let pitches = vec![Pitch::new(Step::G, 5)];
6267        assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6268    }
6269
6270    #[test]
6271    fn suggested_stem_up_at_middle_line() {
6272        use crate::model::notation::Clef;
6273        // B4 = MIDI 71, Treble middle = 71 → stem down (avg >= middle)
6274        let pitches = vec![Pitch::new(Step::B, 4)];
6275        assert!(!suggested_stem_up(&pitches, &Clef::Treble));
6276    }
6277
6278    #[test]
6279    fn suggested_stem_up_chord() {
6280        use crate::model::notation::Clef;
6281        // [C4=60, G4=67] avg=63.5 < 71 → stem up
6282        let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
6283        assert!(suggested_stem_up(&pitches, &Clef::Treble));
6284    }
6285
6286    #[test]
6287    fn suggested_stem_up_bass_clef() {
6288        use crate::model::notation::Clef;
6289        // D3=50 is exactly at Bass middle line → stem down
6290        let pitches = vec![Pitch::new(Step::D, 3)];
6291        assert!(!suggested_stem_up(&pitches, &Clef::Bass));
6292        // C3=48 < 50 → stem up
6293        let pitches2 = vec![Pitch::new(Step::C, 3)];
6294        assert!(suggested_stem_up(&pitches2, &Clef::Bass));
6295    }
6296
6297    #[test]
6298    fn suggested_stem_up_empty_pitches() {
6299        use crate::model::notation::Clef;
6300        assert!(suggested_stem_up(&[], &Clef::Treble));
6301    }
6302
6303    fn eighth(pitch: Pitch) -> Note {
6304        Note::new(pitch, Duration::Eighth)
6305    }
6306    fn quarter(pitch: Pitch) -> Note {
6307        Note::new(pitch, Duration::Quarter)
6308    }
6309    fn rest_eighth() -> Note {
6310        Note::rest(Duration::Eighth)
6311    }
6312
6313    #[test]
6314    fn compute_beams_4_4_four_eighths() {
6315        use crate::model::notation::{Clef, TimeSignature};
6316        let _ = Clef::Treble; // suppress unused import warning
6317        let ts = TimeSignature {
6318            numerator: 4,
6319            denominator: 4,
6320        };
6321        let c4 = Pitch::new(Step::C, 4);
6322        let notes = vec![
6323            eighth(c4.clone()),
6324            eighth(c4.clone()),
6325            eighth(c4.clone()),
6326            eighth(c4.clone()),
6327        ];
6328        let beams = compute_beams(&notes, &ts);
6329        // 4 eighths in 4/4: beat size=1.0, two groups of 2 each
6330        assert_eq!(beams[0], BeamState::Begin);
6331        assert_eq!(beams[1], BeamState::End);
6332        assert_eq!(beams[2], BeamState::Begin);
6333        assert_eq!(beams[3], BeamState::End);
6334    }
6335
6336    #[test]
6337    fn compute_beams_4_4_all_eighth_one_group() {
6338        use crate::model::notation::TimeSignature;
6339        let ts = TimeSignature {
6340            numerator: 4,
6341            denominator: 4,
6342        };
6343        let c4 = Pitch::new(Step::C, 4);
6344        // 2 eighths in a beat → group of 2
6345        let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
6346        let beams = compute_beams(&notes, &ts);
6347        assert_eq!(beams[0], BeamState::Begin);
6348        assert_eq!(beams[1], BeamState::End);
6349    }
6350
6351    #[test]
6352    fn compute_beams_quarter_not_beamed() {
6353        use crate::model::notation::TimeSignature;
6354        let ts = TimeSignature {
6355            numerator: 4,
6356            denominator: 4,
6357        };
6358        let c4 = Pitch::new(Step::C, 4);
6359        let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
6360        let beams = compute_beams(&notes, &ts);
6361        assert_eq!(beams[0], BeamState::None);
6362        assert_eq!(beams[1], BeamState::None);
6363    }
6364
6365    #[test]
6366    fn compute_beams_rest_breaks_beam() {
6367        use crate::model::notation::TimeSignature;
6368        let ts = TimeSignature {
6369            numerator: 4,
6370            denominator: 4,
6371        };
6372        let c4 = Pitch::new(Step::C, 4);
6373        let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
6374        let beams = compute_beams(&notes, &ts);
6375        // rest breaks beam group
6376        assert_eq!(beams[0], BeamState::None);
6377        assert_eq!(beams[1], BeamState::None);
6378        assert_eq!(beams[2], BeamState::None);
6379    }
6380
6381    #[test]
6382    fn compute_beams_6_8_compound() {
6383        use crate::model::notation::TimeSignature;
6384        let ts = TimeSignature {
6385            numerator: 6,
6386            denominator: 8,
6387        };
6388        let c4 = Pitch::new(Step::C, 4);
6389        // 6 eighths in 6/8 compound → two groups of 3 (beam size=1.5 beats)
6390        let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
6391        let beams = compute_beams(&notes, &ts);
6392        assert_eq!(beams[0], BeamState::Begin);
6393        assert_eq!(beams[1], BeamState::Continue);
6394        assert_eq!(beams[2], BeamState::End);
6395        assert_eq!(beams[3], BeamState::Begin);
6396        assert_eq!(beams[4], BeamState::Continue);
6397        assert_eq!(beams[5], BeamState::End);
6398    }
6399
6400    #[test]
6401    fn compute_beams_single_eighth() {
6402        use crate::model::notation::TimeSignature;
6403        let ts = TimeSignature {
6404            numerator: 4,
6405            denominator: 4,
6406        };
6407        let c4 = Pitch::new(Step::C, 4);
6408        let notes = vec![eighth(c4.clone())];
6409        let beams = compute_beams(&notes, &ts);
6410        assert_eq!(beams[0], BeamState::None);
6411    }
6412}
6413
6414#[cfg(test)]
6415mod mid_clef_tests {
6416    use super::*;
6417    use crate::{Command, JoinMeasuresCmd, ScoreEngine, SetMidMeasureClefsCmd, SplitMeasureCmd};
6418
6419    fn quarter_notes() -> Vec<Note> {
6420        [Step::C, Step::D, Step::E, Step::F]
6421            .into_iter()
6422            .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter))
6423            .collect()
6424    }
6425
6426    fn change(beats: f64, clef: Clef) -> MidMeasureClef {
6427        MidMeasureClef {
6428            offset: MeasureLength::from_beats(beats).unwrap(),
6429            clef,
6430        }
6431    }
6432
6433    #[test]
6434    fn measure_length_from_beats_is_exact_for_tuplet_subdivisions() {
6435        let third = MeasureLength::from_beats(1.0 / 3.0).unwrap();
6436        assert_eq!((third.numerator, third.denominator), (1, 12));
6437        let two = MeasureLength::from_beats(2.0).unwrap();
6438        assert_eq!((two.numerator, two.denominator), (1, 2));
6439        assert!(MeasureLength::from_beats(0.0).is_none());
6440    }
6441
6442    #[test]
6443    fn clef_at_follows_mid_bar_changes_and_json_omits_an_empty_list() {
6444        let mut measure = Measure::empty(4, 4);
6445        let json = serde_json::to_string(&measure).unwrap();
6446        assert!(!json.contains("mid_clefs"));
6447        measure.mid_clefs = vec![change(2.0, Clef::Bass)];
6448        assert_eq!(measure.clef_at(&Clef::Treble, 1.0), Clef::Treble);
6449        assert_eq!(measure.clef_at(&Clef::Treble, 2.0), Clef::Bass);
6450        let restored: Measure =
6451            serde_json::from_str(&serde_json::to_string(&measure).unwrap()).unwrap();
6452        assert_eq!(restored.mid_clefs, measure.mid_clefs);
6453    }
6454
6455    #[test]
6456    fn validation_rejects_mid_bar_clefs_outside_the_bar_or_out_of_order() {
6457        let mut score = Score::new("clefs", 120, 4, 4, 0, 1);
6458        score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6459        score.parts[0].staves[0].measures[0].mid_clefs =
6460            vec![change(2.0, Clef::Bass), change(1.0, Clef::Treble)];
6461        let errors = crate::validate(&score).errors;
6462        assert!(errors.iter().any(|error| matches!(
6463            error,
6464            crate::ValidationError::InvalidMidMeasureClef { index: 1, .. }
6465        )));
6466        score.parts[0].staves[0].measures[0].mid_clefs = vec![change(4.0, Clef::Bass)];
6467        assert!(!crate::validate(&score).errors.is_empty());
6468    }
6469
6470    #[test]
6471    fn command_split_join_diff_and_patch_keep_mid_bar_clefs() {
6472        let mut engine = ScoreEngine::new();
6473        engine.score.parts[0].staves[0].measures[0].voices[0] = quarter_notes();
6474        engine
6475            .apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6476                part_index: 0,
6477                staff_index: 0,
6478                measure_index: 0,
6479                clefs: vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)],
6480            }))
6481            .unwrap();
6482        let with_clefs = engine.score.clone();
6483        let plain = {
6484            let mut score = with_clefs.clone();
6485            score.parts[0].staves[0].measures[0].mid_clefs.clear();
6486            score
6487        };
6488        assert!(diff(&plain, &with_clefs).iter().any(|change| matches!(
6489            change,
6490            ScoreChange::MidMeasureClefsChanged { measure: 0, .. }
6491        )));
6492        let patched = apply_patch(&plain, &score_patch(&plain, &with_clefs)).unwrap();
6493        assert_eq!(
6494            patched.parts[0].staves[0].measures[0].mid_clefs,
6495            with_clefs.parts[0].staves[0].measures[0].mid_clefs
6496        );
6497
6498        engine
6499            .apply(Command::SplitMeasure(SplitMeasureCmd {
6500                measure_index: 0,
6501                split_at_beats: 1.0,
6502            }))
6503            .unwrap();
6504        let measures = &engine.score.parts[0].staves[0].measures;
6505        assert!(measures[0].mid_clefs.is_empty());
6506        assert_eq!(measures[1].clef, Some(Clef::Bass));
6507        assert_eq!(measures[1].mid_clefs, vec![change(2.0, Clef::Alto)]);
6508
6509        engine
6510            .apply(Command::JoinMeasures(JoinMeasuresCmd { measure_index: 0 }))
6511            .unwrap();
6512        let measure = &engine.score.parts[0].staves[0].measures[0];
6513        assert_eq!(
6514            measure.mid_clefs,
6515            vec![change(1.0, Clef::Bass), change(3.0, Clef::Alto)]
6516        );
6517
6518        let invalid = engine.apply(Command::SetMidMeasureClefs(SetMidMeasureClefsCmd {
6519            part_index: 0,
6520            staff_index: 0,
6521            measure_index: 0,
6522            clefs: vec![change(0.0 + 5.0, Clef::Bass)],
6523        }));
6524        assert!(invalid.is_err());
6525    }
6526}