Skip to main content

acorde_core/model/
notation.rs

1use super::score::NoteAddr;
2use serde::{Deserialize, Serialize};
3
4use super::pitch::{Pitch, Step};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub enum Clef {
8    Treble,
9    Bass,
10    Alto,
11    Tenor,
12    Percussion,
13    /// Treble clef with an 8 below: sounds an octave lower (tenor voice, guitar).
14    Treble8vb,
15    /// Treble clef with an 8 above: sounds an octave higher.
16    Treble8va,
17    /// Bass clef with an 8 below: sounds an octave lower (contrabass, bass guitar).
18    Bass8vb,
19    /// Bass clef with an 8 above: sounds an octave higher.
20    Bass8va,
21    /// C clef on the bottom line.
22    Soprano,
23    /// C clef on the second line.
24    MezzoSoprano,
25    /// C clef on the top line.
26    Baritone,
27    /// An ABC 2.1 clef whose middle staff line is explicitly mapped to a written pitch.
28    ///
29    /// ABC's `middle=` modifier changes staff placement only. It does not transpose the
30    /// written pitch or playback, so the original ABC pitch remains intact.
31    AbcMiddle {
32        /// The conventional clef glyph to draw.
33        base: Box<Clef>,
34        /// Written pitch placed on the middle staff line.
35        middle: Pitch,
36    },
37}
38
39impl Clef {
40    pub fn to_musicxml_sign(&self) -> &'static str {
41        match self {
42            Clef::Treble | Clef::Treble8vb | Clef::Treble8va => "G",
43            Clef::Bass | Clef::Bass8vb | Clef::Bass8va => "F",
44            Clef::Alto | Clef::Tenor | Clef::Soprano | Clef::MezzoSoprano | Clef::Baritone => "C",
45            Clef::Percussion => "percussion",
46            Clef::AbcMiddle { base, .. } => base.to_musicxml_sign(),
47        }
48    }
49
50    pub fn musicxml_line(&self) -> u8 {
51        match self {
52            Clef::Treble | Clef::Treble8vb | Clef::Treble8va => 2,
53            Clef::Bass | Clef::Bass8vb | Clef::Bass8va => 4,
54            Clef::Alto => 3,
55            Clef::Tenor => 4,
56            Clef::Percussion => 2,
57            Clef::Soprano => 1,
58            Clef::MezzoSoprano => 2,
59            Clef::Baritone => 5,
60            Clef::AbcMiddle { base, .. } => base.musicxml_line(),
61        }
62    }
63
64    /// Octaves the music sounds above (+) or below (−) the written notes: the small 8 on the
65    /// clef (MusicXML `<clef-octave-change>`, MEI `@clef.dis`/`@dis.place`).
66    pub fn octave_change(&self) -> i8 {
67        match self {
68            Clef::Treble8vb | Clef::Bass8vb => -1,
69            Clef::Treble8va | Clef::Bass8va => 1,
70            Clef::AbcMiddle { base, .. } => base.octave_change(),
71            _ => 0,
72        }
73    }
74
75    /// The clef without its octave mark.
76    pub fn without_octave(&self) -> Clef {
77        match self {
78            Clef::Treble8vb | Clef::Treble8va => Clef::Treble,
79            Clef::Bass8vb | Clef::Bass8va => Clef::Bass,
80            Clef::AbcMiddle { base, middle } => Clef::AbcMiddle {
81                base: Box::new(base.without_octave()),
82                middle: middle.clone(),
83            },
84            other => other.clone(),
85        }
86    }
87
88    /// Attach ABC 2.1's visual `middle=` placement to a conventional clef.
89    ///
90    /// Nested custom placements and percussion clefs have no unambiguous ABC meaning and are
91    /// rejected by returning `None`.
92    pub fn with_abc_middle(base: Clef, middle: Pitch) -> Option<Clef> {
93        (!matches!(base, Clef::AbcMiddle { .. } | Clef::Percussion)).then_some(Clef::AbcMiddle {
94            base: Box::new(base),
95            middle,
96        })
97    }
98
99    /// The pitch explicitly placed on the middle line by ABC's `middle=` modifier.
100    pub fn abc_middle(&self) -> Option<&Pitch> {
101        match self {
102            Clef::AbcMiddle { middle, .. } => Some(middle),
103            _ => None,
104        }
105    }
106
107    /// The clef for a sign (`G`, `F`, `C`, `percussion`), a staff line (1 = bottom) and an
108    /// octave change (±1), as MusicXML, MEI and MuseScore describe clefs. `None` for
109    /// combinations the model does not have (French violin clef, a 15ma clef, sub-bass clef).
110    pub fn from_sign(sign: &str, line: Option<u8>, octave_change: i8) -> Option<Clef> {
111        let clef = match (sign.trim().to_ascii_uppercase().as_str(), line) {
112            ("G", None | Some(2)) => Clef::Treble,
113            ("F", None | Some(4)) => Clef::Bass,
114            ("C", Some(1)) => Clef::Soprano,
115            ("C", Some(2)) => Clef::MezzoSoprano,
116            ("C", None | Some(3)) => Clef::Alto,
117            ("C", Some(4)) => Clef::Tenor,
118            ("C", Some(5)) | ("F", Some(3)) => Clef::Baritone,
119            ("PERCUSSION", _) => Clef::Percussion,
120            _ => return None,
121        };
122        Some(match (clef, octave_change) {
123            (clef, 0) => clef,
124            (Clef::Treble, -1) => Clef::Treble8vb,
125            (Clef::Treble, 1) => Clef::Treble8va,
126            (Clef::Bass, -1) => Clef::Bass8vb,
127            (Clef::Bass, 1) => Clef::Bass8va,
128            (Clef::Percussion, _) => Clef::Percussion,
129            _ => return None,
130        })
131    }
132
133    /// The clef named as in score JSON (`"Treble"`, `"Treble8vb"`, `"MezzoSoprano"`, …).
134    pub fn from_name(name: &str) -> Option<Clef> {
135        Some(match name {
136            "Treble" => Clef::Treble,
137            "Bass" => Clef::Bass,
138            "Alto" => Clef::Alto,
139            "Tenor" => Clef::Tenor,
140            "Percussion" => Clef::Percussion,
141            "Treble8vb" => Clef::Treble8vb,
142            "Treble8va" => Clef::Treble8va,
143            "Bass8vb" => Clef::Bass8vb,
144            "Bass8va" => Clef::Bass8va,
145            "Soprano" => Clef::Soprano,
146            "MezzoSoprano" => Clef::MezzoSoprano,
147            "Baritone" => Clef::Baritone,
148            _ => return None,
149        })
150    }
151
152    /// MIDI note number of the middle staff line (used for stem direction heuristics).
153    pub fn middle_line_midi(&self) -> u8 {
154        match self {
155            Clef::Treble => 71,       // B4
156            Clef::Bass => 50,         // D3
157            Clef::Alto => 60,         // C4
158            Clef::Tenor => 57,        // A3
159            Clef::Percussion => 71,   // B4 (same as Treble)
160            Clef::Treble8vb => 59,    // B3
161            Clef::Treble8va => 83,    // B5
162            Clef::Bass8vb => 38,      // D2
163            Clef::Bass8va => 62,      // D4
164            Clef::Soprano => 67,      // G4
165            Clef::MezzoSoprano => 64, // E4
166            Clef::Baritone => 53,     // F3
167            Clef::AbcMiddle { middle, .. } => middle.to_midi().clamp(0, 127) as u8,
168        }
169    }
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct KeySignature {
174    /// -7 (7 flats) to +7 (7 sharps), 0 = C major / A minor.
175    pub fifths: i8,
176    /// "major" or "minor"
177    pub mode: String,
178}
179
180impl Default for KeySignature {
181    fn default() -> Self {
182        Self {
183            fifths: 0,
184            mode: "major".to_string(),
185        }
186    }
187}
188
189impl KeySignature {
190    // Order of sharps added one by one as fifths increases: F C G D A E B
191    const SHARP_ORDER: [Step; 7] = [
192        Step::F,
193        Step::C,
194        Step::G,
195        Step::D,
196        Step::A,
197        Step::E,
198        Step::B,
199    ];
200    // Order of flats: B E A D G C F
201    const FLAT_ORDER: [Step; 7] = [
202        Step::B,
203        Step::E,
204        Step::A,
205        Step::D,
206        Step::G,
207        Step::C,
208        Step::F,
209    ];
210
211    /// Accidental alter (`-1`, `0`, or `+1`) applied to `step` in this key signature.
212    pub fn alter_for_step(&self, step: &Step) -> i8 {
213        if self.fifths > 0 {
214            let count = self.fifths.min(7) as usize;
215            if Self::SHARP_ORDER[..count].contains(step) {
216                1
217            } else {
218                0
219            }
220        } else if self.fifths < 0 {
221            let count = (-self.fifths).min(7) as usize;
222            if Self::FLAT_ORDER[..count].contains(step) {
223                -1
224            } else {
225                0
226            }
227        } else {
228            0
229        }
230    }
231
232    /// True if `pitch` is diatonic to this key (octave-independent, checks step + alter).
233    pub fn contains_pitch(&self, pitch: &Pitch) -> bool {
234        pitch.alter == self.alter_for_step(&pitch.step)
235    }
236
237    /// Tonic step and alter for this key.
238    ///
239    /// Examples: G major → `(Step::G, 0)`, Bb major → `(Step::B, -1)`, F# minor → `(Step::F, 1)`.
240    pub fn tonic(&self) -> (Step, i8) {
241        if self.mode == "minor" {
242            let (maj_step, maj_alter) = Self::major_tonic_from_fifths(self.fifths);
243            // Relative minor tonic = major tonic - 3 semitones.
244            let major_midi = Pitch::with_alter(maj_step, 4, maj_alter).to_midi();
245            let minor_midi = (major_midi - 3).clamp(0, 127) as u8;
246            let p = Pitch::from_midi(minor_midi, self.fifths < 0);
247            (p.step, p.alter)
248        } else {
249            Self::major_tonic_from_fifths(self.fifths)
250        }
251    }
252
253    /// Human-readable key name: `"C major"`, `"G major"`, `"F# minor"`, `"Bb major"` etc.
254    pub fn display_name(&self) -> String {
255        let (step, alter) = self.tonic();
256        let acc = match alter {
257            1 => "#",
258            -1 => "b",
259            _ => "",
260        };
261        format!("{}{} {}", step.to_char(), acc, self.mode)
262    }
263
264    fn major_tonic_from_fifths(fifths: i8) -> (Step, i8) {
265        // Index = fifths + 7 (range 0..=14).
266        // -7=Cb, -6=Gb, -5=Db, -4=Ab, -3=Eb, -2=Bb, -1=F, 0=C, +1=G, +2=D, +3=A, +4=E, +5=B, +6=F#, +7=C#
267        const TONICS: [(Step, i8); 15] = [
268            (Step::C, -1), // -7: Cb
269            (Step::G, -1), // -6: Gb
270            (Step::D, -1), // -5: Db
271            (Step::A, -1), // -4: Ab
272            (Step::E, -1), // -3: Eb
273            (Step::B, -1), // -2: Bb
274            (Step::F, 0),  // -1: F
275            (Step::C, 0),  //  0: C
276            (Step::G, 0),  // +1: G
277            (Step::D, 0),  // +2: D
278            (Step::A, 0),  // +3: A
279            (Step::E, 0),  // +4: E
280            (Step::B, 0),  // +5: B
281            (Step::F, 1),  // +6: F#
282            (Step::C, 1),  // +7: C#
283        ];
284        let idx = (fifths.clamp(-7, 7) + 7) as usize;
285        TONICS[idx].clone()
286    }
287}
288
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290pub struct TimeSignature {
291    pub numerator: u8,
292    pub denominator: u8,
293}
294
295/// Written display of a 4/4 or 2/2 meter, separate from its rhythmic value.
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297pub enum TimeSymbol {
298    Common,
299    Cut,
300}
301
302impl Default for TimeSignature {
303    fn default() -> Self {
304        Self {
305            numerator: 4,
306            denominator: 4,
307        }
308    }
309}
310
311impl TimeSignature {
312    pub fn beats_per_measure(&self) -> f64 {
313        self.numerator as f64
314    }
315
316    pub fn beat_unit_beats(&self) -> f64 {
317        4.0 / self.denominator as f64
318    }
319
320    pub fn total_beats(&self) -> f64 {
321        self.beats_per_measure() * self.beat_unit_beats()
322    }
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
326pub enum Dynamic {
327    Pppp,
328    Ppp,
329    Pp,
330    P,
331    Mp,
332    Mf,
333    F,
334    Ff,
335    Fff,
336    Ffff,
337    Sfz,
338    Rfz,
339    Fz,
340    Sf,
341    /// Forte-piano: loud attack, then piano.
342    Fp,
343    /// Sforzando-piano: accented attack, then piano.
344    Sfp,
345    /// Sforzando-pianissimo: accented attack, then pianissimo.
346    Sfpp,
347    /// Piano-forte: soft attack, then forte.
348    Pf,
349    /// Sforzatissimo: a stronger sforzando.
350    Sffz,
351    /// Sforzato-piano: sforzato attack, then piano.
352    Sfzp,
353    /// Niente: fading to nothing.
354    N,
355}
356
357impl Dynamic {
358    /// Every dynamic, for exhaustive mapping tables.
359    pub const ALL: [Dynamic; 21] = [
360        Dynamic::Pppp,
361        Dynamic::Ppp,
362        Dynamic::Pp,
363        Dynamic::P,
364        Dynamic::Mp,
365        Dynamic::Mf,
366        Dynamic::F,
367        Dynamic::Ff,
368        Dynamic::Fff,
369        Dynamic::Ffff,
370        Dynamic::Sfz,
371        Dynamic::Rfz,
372        Dynamic::Fz,
373        Dynamic::Sf,
374        Dynamic::Fp,
375        Dynamic::Sfp,
376        Dynamic::Sfpp,
377        Dynamic::Pf,
378        Dynamic::Sffz,
379        Dynamic::Sfzp,
380        Dynamic::N,
381    ];
382
383    /// Parse a MusicXML dynamics element name (also MEI `<dynam>` and MuseScore dynamic
384    /// text), folding the extreme `ppppp`/`fffff` levels into the nearest supported one.
385    pub fn from_musicxml_str(name: &str) -> Option<Dynamic> {
386        Some(match name {
387            "pppppp" | "ppppp" => Dynamic::Pppp,
388            "ffffff" | "fffff" => Dynamic::Ffff,
389            "rf" => Dynamic::Rfz,
390            other => *Self::ALL
391                .iter()
392                .find(|dynamic| dynamic.to_musicxml_str() == other)?,
393        })
394    }
395
396    /// The level that continues after this marking, until the next one: itself for a level
397    /// (p, f, …), the second level for a compound (fp → p, pf → f), and `None` for an
398    /// accent on one note (sf, sfz, fz, rfz), after which the previous level resumes.
399    pub fn sustained_level(&self) -> Option<Dynamic> {
400        match self {
401            Dynamic::Sfz | Dynamic::Rfz | Dynamic::Fz | Dynamic::Sf | Dynamic::Sffz => None,
402            Dynamic::Fp | Dynamic::Sfp | Dynamic::Sfzp => Some(Dynamic::P),
403            Dynamic::Sfpp => Some(Dynamic::Pp),
404            Dynamic::Pf => Some(Dynamic::F),
405            level => Some(*level),
406        }
407    }
408
409    pub fn to_musicxml_str(&self) -> &'static str {
410        match self {
411            Dynamic::Pppp => "pppp",
412            Dynamic::Ppp => "ppp",
413            Dynamic::Pp => "pp",
414            Dynamic::P => "p",
415            Dynamic::Mp => "mp",
416            Dynamic::Mf => "mf",
417            Dynamic::F => "f",
418            Dynamic::Ff => "ff",
419            Dynamic::Fff => "fff",
420            Dynamic::Ffff => "ffff",
421            Dynamic::Sfz => "sfz",
422            Dynamic::Rfz => "rfz",
423            Dynamic::Fz => "fz",
424            Dynamic::Sf => "sf",
425            Dynamic::Fp => "fp",
426            Dynamic::Sfp => "sfp",
427            Dynamic::Sfpp => "sfpp",
428            Dynamic::Pf => "pf",
429            Dynamic::Sffz => "sffz",
430            Dynamic::Sfzp => "sfzp",
431            Dynamic::N => "n",
432        }
433    }
434
435    pub fn to_velocity(&self) -> u8 {
436        match self {
437            Dynamic::Pppp => 16,
438            Dynamic::Ppp => 24,
439            Dynamic::Pp => 36,
440            Dynamic::P => 48,
441            Dynamic::Mp => 60,
442            Dynamic::Mf => 72,
443            Dynamic::F => 84,
444            Dynamic::Ff => 96,
445            Dynamic::Fff => 108,
446            Dynamic::Ffff => 120,
447            Dynamic::Sfz => 112,
448            Dynamic::Rfz => 104,
449            Dynamic::Fz => 100,
450            Dynamic::Sf => 96,
451            Dynamic::Fp => 84,
452            Dynamic::Sfp | Dynamic::Sfpp => 96,
453            Dynamic::Pf => 48,
454            Dynamic::Sffz => 120,
455            Dynamic::Sfzp => 112,
456            Dynamic::N => 8,
457        }
458    }
459}
460
461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
462pub enum Articulation {
463    Staccato,
464    Staccatissimo,
465    Accent,
466    Tenuto,
467    Marcato,
468    Fermata,
469    Trill,
470    Mordent,
471    InvertedMordent,
472    Turn,
473    InvertedTurn,
474    Shake,
475    Tremolo(u8),
476    BreathMark,
477    Caesura,
478    /// String up-bow (MusicXML `<up-bow/>`).
479    UpBow,
480    /// String down-bow (MusicXML `<down-bow/>`).
481    DownBow,
482    /// Harmonic circle (MusicXML `<harmonic/>`).
483    Harmonic,
484    /// Open string / open mute circle (MusicXML `<open-string/>`).
485    OpenString,
486    /// Stopped note or closed mute "+" (MusicXML `<stopped/>`).
487    Stopped,
488    /// Snap (Bartók) pizzicato (MusicXML `<snap-pizzicato/>`).
489    SnapPizzicato,
490    /// Right-hand tapping, drawn "T" (MusicXML `<tap/>`, MEI `tap`, Guitar Pro tapping).
491    Tap,
492    /// Left-hand tapping (MusicXML `<tap hand="left"/>`, Guitar Pro left-hand tapping).
493    LeftHandTap,
494    /// Vibrato on one note, a short wavy line above it (Guitar Pro note vibrato).
495    Vibrato,
496    /// A slide into the note from below (MusicXML `<scoop/>`, Guitar Pro slide in from below).
497    Scoop,
498    /// A slide into the note from above (MusicXML `<plop/>`, Guitar Pro slide in from above).
499    Plop,
500    /// A slide up out of the note (MusicXML `<doit/>`, Guitar Pro slide out upwards).
501    Doit,
502    /// A slide down out of the note (MusicXML `<falloff/>`, Guitar Pro slide out downwards).
503    Falloff,
504}
505
506impl Articulation {
507    /// String and brass techniques that MusicXML writes inside `<technical>` rather than
508    /// `<articulations>`.
509    pub fn is_technical_mark(&self) -> bool {
510        matches!(
511            self,
512            Self::UpBow
513                | Self::DownBow
514                | Self::Harmonic
515                | Self::OpenString
516                | Self::Stopped
517                | Self::SnapPizzicato
518                | Self::Tap
519                | Self::LeftHandTap
520                | Self::Vibrato
521        )
522    }
523}
524
525/// Guitar-specific playing technique attached to a note.
526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
527#[serde(rename_all = "kebab-case")]
528pub enum GuitarTechnique {
529    Bend,
530    Slide,
531    HammerOn,
532    PullOff,
533}
534
535/// Deterministic policy for selecting one candidate from an ordered fingering list.
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
537pub enum FingeringSelectionPolicy {
538    /// Preserve the source/application's first candidate.
539    #[default]
540    SourceOrder,
541    /// Select the numerically smallest candidate.
542    LowestNumber,
543    /// Select the numerically largest candidate.
544    HighestNumber,
545}
546
547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
548pub enum Barline {
549    #[default]
550    Normal,
551    Double,
552    Final,
553    RepeatStart,
554    RepeatEnd,
555    RepeatBoth,
556    Dashed,
557    Dotted,
558    Invisible,
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
562pub enum HairpinKind {
563    Crescendo,
564    Decrescendo,
565}
566
567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
568pub struct TupletInfo {
569    /// Notes in the tuplet group (e.g. 3 for triplet).
570    pub actual_notes: u8,
571    /// Normal notes displaced (e.g. 2 for triplet = 3-in-2).
572    pub normal_notes: u8,
573}
574
575/// Explicit written tuplet boundary. Timing remains defined by [`TupletInfo`].
576#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
577#[serde(rename_all = "kebab-case")]
578pub enum TupletBoundary {
579    Start,
580    Stop,
581    StartStop,
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
585pub enum BeamState {
586    #[default]
587    None,
588    Begin,
589    Continue,
590    End,
591    BeginEnd,
592    BackwardHook,
593    ForwardHook,
594}
595
596/// Ottava (octave transposition bracket).
597#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
598pub enum OttavaKind {
599    /// 8va — sounds one octave higher than written.
600    Va8,
601    /// 8vb — sounds one octave lower than written.
602    Vb8,
603    /// 15ma — sounds two octaves higher than written.
604    Ma15,
605    /// 15mb — sounds two octaves lower than written.
606    Mb15,
607}
608
609impl OttavaKind {
610    /// MusicXML `octave-shift@type`: the direction the notes are *displayed* shifted from
611    /// their (sounding) pitch, so an 8va — sounding higher than written — is `down`.
612    pub fn musicxml_type(&self) -> &'static str {
613        match self {
614            OttavaKind::Va8 | OttavaKind::Ma15 => "down",
615            OttavaKind::Vb8 | OttavaKind::Mb15 => "up",
616        }
617    }
618
619    /// Diatonic steps by which notes under this mark are drawn from their sounding pitch.
620    pub fn display_shift_steps(&self) -> i32 {
621        match self {
622            OttavaKind::Va8 => -7,
623            OttavaKind::Ma15 => -14,
624            OttavaKind::Vb8 => 7,
625            OttavaKind::Mb15 => 14,
626        }
627    }
628
629    pub fn musicxml_size(&self) -> u8 {
630        match self {
631            OttavaKind::Va8 | OttavaKind::Vb8 => 8,
632            OttavaKind::Ma15 | OttavaKind::Mb15 => 15,
633        }
634    }
635}
636
637/// How a pitch's accidental is shown when the notation rules alone would not decide it: a
638/// cautionary (courtesy) accidental the source asked for, one in parentheses or brackets, or
639/// an editorial one. `Auto` (the default) leaves the accidental to the key and the bar.
640#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
641pub enum AccidentalDisplay {
642    #[default]
643    Auto,
644    /// Explicitly written without a cautionary or editorial role.
645    Explicit,
646    /// Always shown, without parentheses (MusicXML `cautionary="yes"`, MEI `@func="caution"`).
647    Cautionary,
648    /// Always shown, in parentheses (MusicXML `parentheses="yes"`/`bracket="yes"`, MEI
649    /// `@enclose`, MuseScore accidental brackets).
650    Parenthesized,
651    /// An editor's addition (MusicXML `editorial="yes"`, MEI `@func="edit"`), always shown.
652    Editorial,
653}
654
655/// Note head shape.
656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
657pub enum NoteHead {
658    #[default]
659    Normal,
660    Diamond,  // natural harmonics
661    X,        // muted / dead note
662    Slash,    // ghost note
663    Cross,    // percussion special
664    Triangle, // tap harmonics
665}
666
667/// Lyric syllable attached to a note.
668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
669pub struct Lyric {
670    /// The syllable text.
671    pub text: String,
672    /// Syllabic position: "single" | "begin" | "middle" | "end"
673    pub syllabic: String,
674    /// A melisma extender line follows the syllable, under the notes up to (not including)
675    /// the next note with a lyric or the next rest (MusicXML `<extend>`, MEI `con="u"`).
676    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
677    pub extend: bool,
678}
679
680/// A lyric syllable for verse 2 or later. Verse 1 stays in `Note.lyric`.
681#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
682pub struct VerseLyric {
683    /// Verse number, from 2 to [`VerseLyric::MAX_VERSE`].
684    pub verse: u8,
685    pub lyric: Lyric,
686}
687
688impl VerseLyric {
689    /// Highest supported verse number.
690    pub const MAX_VERSE: u8 = 32;
691}
692
693/// A typed score text annotation. The text itself is kept separate from its
694/// presentation role so consumers do not need to infer semantics from prose.
695#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
696pub struct StyledText {
697    pub style: TextStyle,
698    pub text: String,
699    /// Position in quarter-note beats from the start of the measure. This is a
700    /// semantic direction anchor; it is independent of engraved x offsets.
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub position_beats: Option<f64>,
703    /// Optional vertical placement hint from interchange formats.
704    #[serde(default)]
705    pub placement: Option<String>,
706    /// MusicXML default horizontal offset in tenths, when attached to a direction text.
707    #[serde(default)]
708    pub offset_x: Option<f64>,
709    /// MusicXML default vertical offset in tenths, when attached to a direction text.
710    #[serde(default)]
711    pub offset_y: Option<f64>,
712    /// MusicXML relative horizontal offset in tenths, retaining its relative-coordinate meaning.
713    #[serde(default)]
714    pub relative_x: Option<f64>,
715    /// MusicXML relative vertical offset in tenths, retaining its relative-coordinate meaning.
716    #[serde(default)]
717    pub relative_y: Option<f64>,
718}
719
720#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
721pub enum TextStyle {
722    Expression,
723    Technique,
724    Lyrics,
725    ChordSymbol,
726    FiguredBass,
727    RehearsalMark,
728    Generic,
729}
730
731/// Cross-staff placement metadata. The note remains in its source staff, so
732/// its stable playback address continues to identify the original note.
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734pub struct CrossStaff {
735    pub target_staff: usize,
736    #[serde(default)]
737    pub target_voice: Option<usize>,
738}
739
740/// A tablature string/fret position attached to a pitched note.
741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
742pub struct TabPosition {
743    /// One-based string number, matching MusicXML `<string>`.
744    pub string: u8,
745    pub fret: u8,
746}
747
748/// Tablature staff metadata. Tuning MIDI values are ordered by string number.
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct TablatureConfig {
751    pub lines: u8,
752    pub tuning_midi: Vec<i16>,
753    #[serde(default)]
754    pub capo: u8,
755}
756
757/// Structured chord symbol attached to a note.
758#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
759pub struct ChordSymbol {
760    /// Root note name: "C", "F#", "Bb", etc.
761    pub root: String,
762    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
763    pub kind: String,
764    /// Slash-chord bass note.
765    pub bass: Option<String>,
766    /// Optional vertical placement hint from interchange formats (for example `above`/`below`).
767    #[serde(default)]
768    pub placement: Option<String>,
769    /// Whether the source harmony carries a continuation/extender line.
770    #[serde(default)]
771    pub extender: bool,
772    /// MEI harmonic-analysis scale degree (Humdrum **deg syntax), when supplied.
773    #[serde(default)]
774    pub harmonic_degree: Option<String>,
775    /// MEI harmonic function token (for example `T`, `PD`, or `D`), when supplied.
776    #[serde(default)]
777    pub harmony_function: Option<String>,
778    /// MEI `harm@type` classification token(s), when supplied.
779    #[serde(default)]
780    pub harmony_type: Option<String>,
781    /// MEI `harm@chordref` URI, retained without resolving an external chord definition.
782    #[serde(default)]
783    pub chord_ref: Option<String>,
784    /// Optional note address where a harmony range ends.
785    ///
786    /// This is primarily used by MEI `harm@tstamp2`/`endid`.  The field is
787    /// optional so older score JSON and formats without harmony ranges remain
788    /// fully compatible.
789    #[serde(default)]
790    pub range_end: Option<NoteAddr>,
791    /// Structured chord extensions such as add9, alter5, or omit3.
792    #[serde(default)]
793    pub degrees: Vec<ChordDegree>,
794}
795
796/// A reusable MEI chord/tablature definition referenced by `harm@chordref`.
797///
798/// The fields intentionally retain the source spelling for deprecated MEI tuning
799/// attributes.  Consumers may interpret the member positions without requiring
800/// the canonical score to invent an instrument catalog.
801#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
802pub struct ChordDefinition {
803    #[serde(default)]
804    pub id: Option<String>,
805    #[serde(default)]
806    pub label: Option<String>,
807    #[serde(default)]
808    pub kind: Option<String>,
809    #[serde(default)]
810    pub fret_position: Option<u32>,
811    #[serde(default)]
812    pub tab_strings: Option<String>,
813    #[serde(default)]
814    pub tab_courses: Option<String>,
815    #[serde(default)]
816    pub members: Vec<ChordDefinitionMember>,
817    #[serde(default)]
818    pub barres: Vec<ChordBarre>,
819}
820
821/// One pitch and/or tablature position in a [`ChordDefinition`].
822#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
823pub struct ChordDefinitionMember {
824    #[serde(default)]
825    pub id: Option<String>,
826    #[serde(default)]
827    pub pitch: Option<Pitch>,
828    #[serde(default)]
829    pub tab_string: Option<u8>,
830    #[serde(default)]
831    pub tab_course: Option<u8>,
832    #[serde(default)]
833    pub tab_fret: Option<u16>,
834    #[serde(default)]
835    pub fingering: Option<u8>,
836}
837
838/// A barre range inside a [`ChordDefinition`] fretboard diagram.
839#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
840pub struct ChordBarre {
841    #[serde(default)]
842    pub start_member: Option<String>,
843    #[serde(default)]
844    pub end_member: Option<String>,
845    #[serde(default)]
846    pub fret: Option<u16>,
847    #[serde(default)]
848    pub label: Option<String>,
849    #[serde(default)]
850    pub kind: Option<String>,
851}
852
853/// One structured MusicXML chord degree.
854#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
855pub struct ChordDegree {
856    /// Scale degree number (normally 1 through 13).
857    pub value: u8,
858    /// Semitone alteration relative to the diatonic degree.
859    pub alter: i8,
860    /// MusicXML degree type, such as `add`, `alter`, or `subtract`.
861    pub kind: String,
862}
863
864/// One structured figure in a figured-bass annotation.
865#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
866pub struct FiguredBassFigure {
867    /// Source figure number, retained as text because MusicXML permits non-numeric figures.
868    pub number: String,
869    /// Raw source alteration value, such as `-1`, `0`, or `1`.
870    #[serde(default)]
871    pub alter: Option<String>,
872    /// Optional source prefix decoration.
873    #[serde(default)]
874    pub prefix: Option<String>,
875    /// Optional source suffix decoration.
876    #[serde(default)]
877    pub suffix: Option<String>,
878    /// Whether MEI marks this figure with a horizontal extender.
879    #[serde(default)]
880    pub extender: bool,
881}
882
883/// A MusicXML figured-bass group at a score-time position in the measure.
884#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
885pub struct FiguredBassGroup {
886    /// Editable voice slot containing the following note.
887    #[serde(default)]
888    pub voice: usize,
889    /// Position in quarter-note beats from the start of the measure.
890    pub position_beats: f64,
891    /// Duration in quarter-note beats, when explicitly present in the source.
892    #[serde(default, skip_serializing_if = "Option::is_none")]
893    pub duration_beats: Option<f64>,
894    /// Figures in source order.
895    pub figures: Vec<FiguredBassFigure>,
896    /// Raw extend type for each figure; an empty string represents an untyped extend.
897    #[serde(default, skip_serializing_if = "Vec::is_empty")]
898    pub extend_types: Vec<Option<String>>,
899}
900
901impl ChordDegree {
902    /// Render the degree using the compact chord-label convention.
903    pub fn display_text(&self) -> String {
904        let accidental = match self.alter {
905            -2 => "bb",
906            -1 => "b",
907            1 => "#",
908            2 => "##",
909            _ => "",
910        };
911        match self.kind.as_str() {
912            "subtract" => format!("no{}{}", accidental, self.value),
913            "alter" => format!("{}{}", accidental, self.value),
914            _ => format!("add{}{}", accidental, self.value),
915        }
916    }
917}
918
919/// Chord kinds (MusicXML `<kind>` values, plus aliases other formats use) and the compact
920/// suffix a chord label writes after its root. The first entry for a suffix is the kind a label
921/// reads back as.
922pub const CHORD_KIND_SUFFIXES: &[(&str, &str)] = &[
923    ("major", ""),
924    ("minor", "m"),
925    ("augmented", "aug"),
926    ("diminished", "dim"),
927    ("dominant", "7"),
928    ("major-seventh", "maj7"),
929    ("minor-seventh", "m7"),
930    ("diminished-seventh", "dim7"),
931    ("augmented-seventh", "aug7"),
932    ("half-diminished", "m7b5"),
933    ("major-minor", "mMaj7"),
934    ("minor-major-seventh", "mMaj7"),
935    ("minor-major", "mMaj7"),
936    ("major-sixth", "6"),
937    ("minor-sixth", "m6"),
938    ("dominant-ninth", "9"),
939    ("major-ninth", "maj9"),
940    ("minor-ninth", "m9"),
941    ("dominant-11th", "11"),
942    ("major-11th", "maj11"),
943    ("minor-11th", "m11"),
944    ("dominant-13th", "13"),
945    ("major-13th", "maj13"),
946    ("minor-13th", "m13"),
947    ("suspended-second", "sus2"),
948    ("suspended-fourth", "sus4"),
949    ("power", "5"),
950    ("major-add9", "add9"),
951    ("minor-add9", "madd9"),
952    ("dominant-flat-five", "7b5"),
953    ("dominant-sharp-five", "7#5"),
954];
955
956impl ChordSymbol {
957    /// The chord kind a compact label suffix (`"m7"`, `"aug7"`, `"13"`) stands for.
958    pub fn kind_for_suffix(suffix: &str) -> Option<&'static str> {
959        CHORD_KIND_SUFFIXES
960            .iter()
961            .find(|(_, candidate)| *candidate == suffix)
962            .map(|(kind, _)| *kind)
963    }
964
965    pub fn display_text(&self) -> String {
966        let kind_str = CHORD_KIND_SUFFIXES
967            .iter()
968            .find(|(kind, _)| *kind == self.kind)
969            .map_or(self.kind.as_str(), |(_, suffix)| *suffix);
970        let bass_str = match &self.bass {
971            Some(b) => format!("/{}", b),
972            None => String::new(),
973        };
974        let degree_str = self
975            .degrees
976            .iter()
977            .map(ChordDegree::display_text)
978            .collect::<String>();
979        format!("{}{}{}{}", self.root, kind_str, degree_str, bass_str)
980    }
981}
982
983#[cfg(test)]
984mod tests {
985    use super::*;
986
987    #[test]
988    fn key_alter_c_major_all_natural() {
989        let key = KeySignature {
990            fifths: 0,
991            mode: "major".into(),
992        };
993        for step in [
994            Step::C,
995            Step::D,
996            Step::E,
997            Step::F,
998            Step::G,
999            Step::A,
1000            Step::B,
1001        ] {
1002            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
1003        }
1004    }
1005
1006    #[test]
1007    fn key_alter_g_major_fsharp() {
1008        let key = KeySignature {
1009            fifths: 1,
1010            mode: "major".into(),
1011        };
1012        assert_eq!(key.alter_for_step(&Step::F), 1);
1013        assert_eq!(key.alter_for_step(&Step::G), 0);
1014    }
1015
1016    #[test]
1017    fn key_alter_f_major_bflat() {
1018        let key = KeySignature {
1019            fifths: -1,
1020            mode: "major".into(),
1021        };
1022        assert_eq!(key.alter_for_step(&Step::B), -1);
1023        assert_eq!(key.alter_for_step(&Step::C), 0);
1024    }
1025
1026    #[test]
1027    fn key_alter_bb_major() {
1028        let key = KeySignature {
1029            fifths: -2,
1030            mode: "major".into(),
1031        };
1032        assert_eq!(key.alter_for_step(&Step::B), -1);
1033        assert_eq!(key.alter_for_step(&Step::E), -1);
1034        assert_eq!(key.alter_for_step(&Step::A), 0);
1035    }
1036
1037    #[test]
1038    fn key_contains_pitch_g_major() {
1039        let key = KeySignature {
1040            fifths: 1,
1041            mode: "major".into(),
1042        };
1043        // In-key: G, A, B, C, D, E, F# (alter=1)
1044        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
1045        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
1046        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
1047        // Out-of-key: F natural
1048        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
1049    }
1050
1051    #[test]
1052    fn key_display_name_c_major() {
1053        let key = KeySignature {
1054            fifths: 0,
1055            mode: "major".into(),
1056        };
1057        assert_eq!(key.display_name(), "C major");
1058    }
1059
1060    #[test]
1061    fn key_display_name_bb_major() {
1062        let key = KeySignature {
1063            fifths: -2,
1064            mode: "major".into(),
1065        };
1066        assert_eq!(key.display_name(), "Bb major");
1067    }
1068
1069    #[test]
1070    fn key_display_name_fsharp_minor() {
1071        let key = KeySignature {
1072            fifths: 3,
1073            mode: "minor".into(),
1074        };
1075        assert_eq!(key.display_name(), "F# minor");
1076    }
1077
1078    #[test]
1079    fn key_tonic_d_major() {
1080        let key = KeySignature {
1081            fifths: 2,
1082            mode: "major".into(),
1083        };
1084        let (step, alter) = key.tonic();
1085        assert_eq!(step, Step::D);
1086        assert_eq!(alter, 0);
1087    }
1088
1089    #[test]
1090    fn key_tonic_a_minor() {
1091        // A minor = relative minor of C major (fifths=0)
1092        let key = KeySignature {
1093            fifths: 0,
1094            mode: "minor".into(),
1095        };
1096        let (step, alter) = key.tonic();
1097        assert_eq!(step, Step::A);
1098        assert_eq!(alter, 0);
1099    }
1100
1101    #[test]
1102    fn chord_display_major() {
1103        let c = ChordSymbol {
1104            root: "C".into(),
1105            kind: "major".into(),
1106            bass: None,
1107            placement: None,
1108            extender: false,
1109            harmonic_degree: None,
1110            harmony_function: None,
1111            harmony_type: None,
1112            chord_ref: None,
1113            range_end: None,
1114            degrees: Vec::new(),
1115        };
1116        assert_eq!(c.display_text(), "C");
1117    }
1118
1119    #[test]
1120    fn chord_display_minor_seventh_slash() {
1121        let c = ChordSymbol {
1122            root: "D".into(),
1123            kind: "minor-seventh".into(),
1124            bass: Some("F".into()),
1125            placement: None,
1126            extender: false,
1127            harmonic_degree: None,
1128            harmony_function: None,
1129            harmony_type: None,
1130            chord_ref: None,
1131            range_end: None,
1132            degrees: Vec::new(),
1133        };
1134        assert_eq!(c.display_text(), "Dm7/F");
1135    }
1136
1137    #[test]
1138    fn chord_display_structured_degrees() {
1139        let c = ChordSymbol {
1140            root: "C".into(),
1141            kind: "dominant".into(),
1142            bass: None,
1143            placement: None,
1144            extender: false,
1145            harmonic_degree: None,
1146            harmony_function: None,
1147            harmony_type: None,
1148            chord_ref: None,
1149            range_end: None,
1150            degrees: vec![
1151                ChordDegree {
1152                    value: 9,
1153                    alter: 1,
1154                    kind: "add".into(),
1155                },
1156                ChordDegree {
1157                    value: 5,
1158                    alter: -1,
1159                    kind: "alter".into(),
1160                },
1161                ChordDegree {
1162                    value: 3,
1163                    alter: 0,
1164                    kind: "subtract".into(),
1165                },
1166            ],
1167        };
1168        assert_eq!(c.display_text(), "C7add#9b5no3");
1169    }
1170
1171    #[test]
1172    fn chord_symbol_legacy_json_defaults_degrees() {
1173        let chord: ChordSymbol =
1174            serde_json::from_str(r#"{"root":"C","kind":"major","bass":null,"placement":null}"#)
1175                .expect("legacy chord symbol JSON deserializes");
1176        assert!(chord.degrees.is_empty());
1177        assert!(!chord.extender);
1178        assert!(chord.harmonic_degree.is_none());
1179        assert!(chord.harmony_function.is_none());
1180        assert!(chord.harmony_type.is_none());
1181    }
1182
1183    #[test]
1184    fn time_sig_total_beats_three_four() {
1185        let ts = TimeSignature {
1186            numerator: 3,
1187            denominator: 4,
1188        };
1189        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1190    }
1191
1192    #[test]
1193    fn time_sig_total_beats_six_eight() {
1194        let ts = TimeSignature {
1195            numerator: 6,
1196            denominator: 8,
1197        };
1198        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1199    }
1200
1201    #[test]
1202    fn clef_treble_middle_b4() {
1203        assert_eq!(Clef::Treble.middle_line_midi(), 71);
1204    }
1205
1206    #[test]
1207    fn clef_bass_middle_d3() {
1208        assert_eq!(Clef::Bass.middle_line_midi(), 50);
1209    }
1210
1211    #[test]
1212    fn clef_alto_middle_c4() {
1213        assert_eq!(Clef::Alto.middle_line_midi(), 60);
1214    }
1215
1216    #[test]
1217    fn clef_tenor_middle_a3() {
1218        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
1219    }
1220}