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
295impl Default for TimeSignature {
296    fn default() -> Self {
297        Self {
298            numerator: 4,
299            denominator: 4,
300        }
301    }
302}
303
304impl TimeSignature {
305    pub fn beats_per_measure(&self) -> f64 {
306        self.numerator as f64
307    }
308
309    pub fn beat_unit_beats(&self) -> f64 {
310        4.0 / self.denominator as f64
311    }
312
313    pub fn total_beats(&self) -> f64 {
314        self.beats_per_measure() * self.beat_unit_beats()
315    }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
319pub enum Dynamic {
320    Pppp,
321    Ppp,
322    Pp,
323    P,
324    Mp,
325    Mf,
326    F,
327    Ff,
328    Fff,
329    Ffff,
330    Sfz,
331    Rfz,
332    Fz,
333    Sf,
334    /// Forte-piano: loud attack, then piano.
335    Fp,
336    /// Sforzando-piano: accented attack, then piano.
337    Sfp,
338    /// Sforzando-pianissimo: accented attack, then pianissimo.
339    Sfpp,
340    /// Piano-forte: soft attack, then forte.
341    Pf,
342    /// Sforzatissimo: a stronger sforzando.
343    Sffz,
344    /// Sforzato-piano: sforzato attack, then piano.
345    Sfzp,
346    /// Niente: fading to nothing.
347    N,
348}
349
350impl Dynamic {
351    /// Every dynamic, for exhaustive mapping tables.
352    pub const ALL: [Dynamic; 21] = [
353        Dynamic::Pppp,
354        Dynamic::Ppp,
355        Dynamic::Pp,
356        Dynamic::P,
357        Dynamic::Mp,
358        Dynamic::Mf,
359        Dynamic::F,
360        Dynamic::Ff,
361        Dynamic::Fff,
362        Dynamic::Ffff,
363        Dynamic::Sfz,
364        Dynamic::Rfz,
365        Dynamic::Fz,
366        Dynamic::Sf,
367        Dynamic::Fp,
368        Dynamic::Sfp,
369        Dynamic::Sfpp,
370        Dynamic::Pf,
371        Dynamic::Sffz,
372        Dynamic::Sfzp,
373        Dynamic::N,
374    ];
375
376    /// Parse a MusicXML dynamics element name (also MEI `<dynam>` and MuseScore dynamic
377    /// text), folding the extreme `ppppp`/`fffff` levels into the nearest supported one.
378    pub fn from_musicxml_str(name: &str) -> Option<Dynamic> {
379        Some(match name {
380            "pppppp" | "ppppp" => Dynamic::Pppp,
381            "ffffff" | "fffff" => Dynamic::Ffff,
382            "rf" => Dynamic::Rfz,
383            other => *Self::ALL
384                .iter()
385                .find(|dynamic| dynamic.to_musicxml_str() == other)?,
386        })
387    }
388
389    /// The level that continues after this marking, until the next one: itself for a level
390    /// (p, f, …), the second level for a compound (fp → p, pf → f), and `None` for an
391    /// accent on one note (sf, sfz, fz, rfz), after which the previous level resumes.
392    pub fn sustained_level(&self) -> Option<Dynamic> {
393        match self {
394            Dynamic::Sfz | Dynamic::Rfz | Dynamic::Fz | Dynamic::Sf | Dynamic::Sffz => None,
395            Dynamic::Fp | Dynamic::Sfp | Dynamic::Sfzp => Some(Dynamic::P),
396            Dynamic::Sfpp => Some(Dynamic::Pp),
397            Dynamic::Pf => Some(Dynamic::F),
398            level => Some(*level),
399        }
400    }
401
402    pub fn to_musicxml_str(&self) -> &'static str {
403        match self {
404            Dynamic::Pppp => "pppp",
405            Dynamic::Ppp => "ppp",
406            Dynamic::Pp => "pp",
407            Dynamic::P => "p",
408            Dynamic::Mp => "mp",
409            Dynamic::Mf => "mf",
410            Dynamic::F => "f",
411            Dynamic::Ff => "ff",
412            Dynamic::Fff => "fff",
413            Dynamic::Ffff => "ffff",
414            Dynamic::Sfz => "sfz",
415            Dynamic::Rfz => "rfz",
416            Dynamic::Fz => "fz",
417            Dynamic::Sf => "sf",
418            Dynamic::Fp => "fp",
419            Dynamic::Sfp => "sfp",
420            Dynamic::Sfpp => "sfpp",
421            Dynamic::Pf => "pf",
422            Dynamic::Sffz => "sffz",
423            Dynamic::Sfzp => "sfzp",
424            Dynamic::N => "n",
425        }
426    }
427
428    pub fn to_velocity(&self) -> u8 {
429        match self {
430            Dynamic::Pppp => 16,
431            Dynamic::Ppp => 24,
432            Dynamic::Pp => 36,
433            Dynamic::P => 48,
434            Dynamic::Mp => 60,
435            Dynamic::Mf => 72,
436            Dynamic::F => 84,
437            Dynamic::Ff => 96,
438            Dynamic::Fff => 108,
439            Dynamic::Ffff => 120,
440            Dynamic::Sfz => 112,
441            Dynamic::Rfz => 104,
442            Dynamic::Fz => 100,
443            Dynamic::Sf => 96,
444            Dynamic::Fp => 84,
445            Dynamic::Sfp | Dynamic::Sfpp => 96,
446            Dynamic::Pf => 48,
447            Dynamic::Sffz => 120,
448            Dynamic::Sfzp => 112,
449            Dynamic::N => 8,
450        }
451    }
452}
453
454#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
455pub enum Articulation {
456    Staccato,
457    Staccatissimo,
458    Accent,
459    Tenuto,
460    Marcato,
461    Fermata,
462    Trill,
463    Mordent,
464    InvertedMordent,
465    Turn,
466    InvertedTurn,
467    Shake,
468    Tremolo(u8),
469    BreathMark,
470    Caesura,
471    /// String up-bow (MusicXML `<up-bow/>`).
472    UpBow,
473    /// String down-bow (MusicXML `<down-bow/>`).
474    DownBow,
475    /// Harmonic circle (MusicXML `<harmonic/>`).
476    Harmonic,
477    /// Open string / open mute circle (MusicXML `<open-string/>`).
478    OpenString,
479    /// Stopped note or closed mute "+" (MusicXML `<stopped/>`).
480    Stopped,
481    /// Snap (Bartók) pizzicato (MusicXML `<snap-pizzicato/>`).
482    SnapPizzicato,
483    /// Right-hand tapping, drawn "T" (MusicXML `<tap/>`, MEI `tap`, Guitar Pro tapping).
484    Tap,
485    /// Left-hand tapping (MusicXML `<tap hand="left"/>`, Guitar Pro left-hand tapping).
486    LeftHandTap,
487    /// Vibrato on one note, a short wavy line above it (Guitar Pro note vibrato).
488    Vibrato,
489    /// A slide into the note from below (MusicXML `<scoop/>`, Guitar Pro slide in from below).
490    Scoop,
491    /// A slide into the note from above (MusicXML `<plop/>`, Guitar Pro slide in from above).
492    Plop,
493    /// A slide up out of the note (MusicXML `<doit/>`, Guitar Pro slide out upwards).
494    Doit,
495    /// A slide down out of the note (MusicXML `<falloff/>`, Guitar Pro slide out downwards).
496    Falloff,
497}
498
499impl Articulation {
500    /// String and brass techniques that MusicXML writes inside `<technical>` rather than
501    /// `<articulations>`.
502    pub fn is_technical_mark(&self) -> bool {
503        matches!(
504            self,
505            Self::UpBow
506                | Self::DownBow
507                | Self::Harmonic
508                | Self::OpenString
509                | Self::Stopped
510                | Self::SnapPizzicato
511                | Self::Tap
512                | Self::LeftHandTap
513                | Self::Vibrato
514        )
515    }
516}
517
518/// Guitar-specific playing technique attached to a note.
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520#[serde(rename_all = "kebab-case")]
521pub enum GuitarTechnique {
522    Bend,
523    Slide,
524    HammerOn,
525    PullOff,
526}
527
528/// Deterministic policy for selecting one candidate from an ordered fingering list.
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
530pub enum FingeringSelectionPolicy {
531    /// Preserve the source/application's first candidate.
532    #[default]
533    SourceOrder,
534    /// Select the numerically smallest candidate.
535    LowestNumber,
536    /// Select the numerically largest candidate.
537    HighestNumber,
538}
539
540#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
541pub enum Barline {
542    #[default]
543    Normal,
544    Double,
545    Final,
546    RepeatStart,
547    RepeatEnd,
548    RepeatBoth,
549    Dashed,
550    Dotted,
551    Invisible,
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
555pub enum HairpinKind {
556    Crescendo,
557    Decrescendo,
558}
559
560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
561pub struct TupletInfo {
562    /// Notes in the tuplet group (e.g. 3 for triplet).
563    pub actual_notes: u8,
564    /// Normal notes displaced (e.g. 2 for triplet = 3-in-2).
565    pub normal_notes: u8,
566}
567
568/// Explicit written tuplet boundary. Timing remains defined by [`TupletInfo`].
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
570#[serde(rename_all = "kebab-case")]
571pub enum TupletBoundary {
572    Start,
573    Stop,
574    StartStop,
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
578pub enum BeamState {
579    #[default]
580    None,
581    Begin,
582    Continue,
583    End,
584    BeginEnd,
585    BackwardHook,
586    ForwardHook,
587}
588
589/// Ottava (octave transposition bracket).
590#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
591pub enum OttavaKind {
592    /// 8va — sounds one octave higher than written.
593    Va8,
594    /// 8vb — sounds one octave lower than written.
595    Vb8,
596    /// 15ma — sounds two octaves higher than written.
597    Ma15,
598    /// 15mb — sounds two octaves lower than written.
599    Mb15,
600}
601
602impl OttavaKind {
603    /// MusicXML `octave-shift@type`: the direction the notes are *displayed* shifted from
604    /// their (sounding) pitch, so an 8va — sounding higher than written — is `down`.
605    pub fn musicxml_type(&self) -> &'static str {
606        match self {
607            OttavaKind::Va8 | OttavaKind::Ma15 => "down",
608            OttavaKind::Vb8 | OttavaKind::Mb15 => "up",
609        }
610    }
611
612    /// Diatonic steps by which notes under this mark are drawn from their sounding pitch.
613    pub fn display_shift_steps(&self) -> i32 {
614        match self {
615            OttavaKind::Va8 => -7,
616            OttavaKind::Ma15 => -14,
617            OttavaKind::Vb8 => 7,
618            OttavaKind::Mb15 => 14,
619        }
620    }
621
622    pub fn musicxml_size(&self) -> u8 {
623        match self {
624            OttavaKind::Va8 | OttavaKind::Vb8 => 8,
625            OttavaKind::Ma15 | OttavaKind::Mb15 => 15,
626        }
627    }
628}
629
630/// How a pitch's accidental is shown when the notation rules alone would not decide it: a
631/// cautionary (courtesy) accidental the source asked for, one in parentheses or brackets, or
632/// an editorial one. `Auto` (the default) leaves the accidental to the key and the bar.
633#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
634pub enum AccidentalDisplay {
635    #[default]
636    Auto,
637    /// Always shown, without parentheses (MusicXML `cautionary="yes"`, MEI `@func="caution"`).
638    Cautionary,
639    /// Always shown, in parentheses (MusicXML `parentheses="yes"`/`bracket="yes"`, MEI
640    /// `@enclose`, MuseScore accidental brackets).
641    Parenthesized,
642    /// An editor's addition (MusicXML `editorial="yes"`, MEI `@func="edit"`), always shown.
643    Editorial,
644}
645
646/// Note head shape.
647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
648pub enum NoteHead {
649    #[default]
650    Normal,
651    Diamond,  // natural harmonics
652    X,        // muted / dead note
653    Slash,    // ghost note
654    Cross,    // percussion special
655    Triangle, // tap harmonics
656}
657
658/// Lyric syllable attached to a note.
659#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
660pub struct Lyric {
661    /// The syllable text.
662    pub text: String,
663    /// Syllabic position: "single" | "begin" | "middle" | "end"
664    pub syllabic: String,
665    /// A melisma extender line follows the syllable, under the notes up to (not including)
666    /// the next note with a lyric or the next rest (MusicXML `<extend>`, MEI `con="u"`).
667    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
668    pub extend: bool,
669}
670
671/// A lyric syllable for verse 2 or later. Verse 1 stays in `Note.lyric`.
672#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
673pub struct VerseLyric {
674    /// Verse number, from 2 to [`VerseLyric::MAX_VERSE`].
675    pub verse: u8,
676    pub lyric: Lyric,
677}
678
679impl VerseLyric {
680    /// Highest supported verse number.
681    pub const MAX_VERSE: u8 = 32;
682}
683
684/// A typed score text annotation. The text itself is kept separate from its
685/// presentation role so consumers do not need to infer semantics from prose.
686#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
687pub struct StyledText {
688    pub style: TextStyle,
689    pub text: String,
690    /// Optional vertical placement hint from interchange formats.
691    #[serde(default)]
692    pub placement: Option<String>,
693    /// MusicXML default horizontal offset in tenths, when attached to a direction text.
694    #[serde(default)]
695    pub offset_x: Option<f64>,
696    /// MusicXML default vertical offset in tenths, when attached to a direction text.
697    #[serde(default)]
698    pub offset_y: Option<f64>,
699    /// MusicXML relative horizontal offset in tenths, retaining its relative-coordinate meaning.
700    #[serde(default)]
701    pub relative_x: Option<f64>,
702    /// MusicXML relative vertical offset in tenths, retaining its relative-coordinate meaning.
703    #[serde(default)]
704    pub relative_y: Option<f64>,
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
708pub enum TextStyle {
709    Expression,
710    Technique,
711    Lyrics,
712    ChordSymbol,
713    FiguredBass,
714    RehearsalMark,
715    Generic,
716}
717
718/// Cross-staff placement metadata. The note remains in its source staff, so
719/// its stable playback address continues to identify the original note.
720#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
721pub struct CrossStaff {
722    pub target_staff: usize,
723    #[serde(default)]
724    pub target_voice: Option<usize>,
725}
726
727/// A tablature string/fret position attached to a pitched note.
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
729pub struct TabPosition {
730    /// One-based string number, matching MusicXML `<string>`.
731    pub string: u8,
732    pub fret: u8,
733}
734
735/// Tablature staff metadata. Tuning MIDI values are ordered by string number.
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
737pub struct TablatureConfig {
738    pub lines: u8,
739    pub tuning_midi: Vec<i16>,
740    #[serde(default)]
741    pub capo: u8,
742}
743
744/// Structured chord symbol attached to a note.
745#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
746pub struct ChordSymbol {
747    /// Root note name: "C", "F#", "Bb", etc.
748    pub root: String,
749    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
750    pub kind: String,
751    /// Slash-chord bass note.
752    pub bass: Option<String>,
753    /// Optional vertical placement hint from interchange formats (for example `above`/`below`).
754    #[serde(default)]
755    pub placement: Option<String>,
756    /// Whether the source harmony carries a continuation/extender line.
757    #[serde(default)]
758    pub extender: bool,
759    /// MEI harmonic-analysis scale degree (Humdrum **deg syntax), when supplied.
760    #[serde(default)]
761    pub harmonic_degree: Option<String>,
762    /// MEI harmonic function token (for example `T`, `PD`, or `D`), when supplied.
763    #[serde(default)]
764    pub harmony_function: Option<String>,
765    /// MEI `harm@type` classification token(s), when supplied.
766    #[serde(default)]
767    pub harmony_type: Option<String>,
768    /// MEI `harm@chordref` URI, retained without resolving an external chord definition.
769    #[serde(default)]
770    pub chord_ref: Option<String>,
771    /// Optional note address where a harmony range ends.
772    ///
773    /// This is primarily used by MEI `harm@tstamp2`/`endid`.  The field is
774    /// optional so older score JSON and formats without harmony ranges remain
775    /// fully compatible.
776    #[serde(default)]
777    pub range_end: Option<NoteAddr>,
778    /// Structured chord extensions such as add9, alter5, or omit3.
779    #[serde(default)]
780    pub degrees: Vec<ChordDegree>,
781}
782
783/// A reusable MEI chord/tablature definition referenced by `harm@chordref`.
784///
785/// The fields intentionally retain the source spelling for deprecated MEI tuning
786/// attributes.  Consumers may interpret the member positions without requiring
787/// the canonical score to invent an instrument catalog.
788#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
789pub struct ChordDefinition {
790    #[serde(default)]
791    pub id: Option<String>,
792    #[serde(default)]
793    pub label: Option<String>,
794    #[serde(default)]
795    pub kind: Option<String>,
796    #[serde(default)]
797    pub fret_position: Option<u32>,
798    #[serde(default)]
799    pub tab_strings: Option<String>,
800    #[serde(default)]
801    pub tab_courses: Option<String>,
802    #[serde(default)]
803    pub members: Vec<ChordDefinitionMember>,
804    #[serde(default)]
805    pub barres: Vec<ChordBarre>,
806}
807
808/// One pitch and/or tablature position in a [`ChordDefinition`].
809#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
810pub struct ChordDefinitionMember {
811    #[serde(default)]
812    pub id: Option<String>,
813    #[serde(default)]
814    pub pitch: Option<Pitch>,
815    #[serde(default)]
816    pub tab_string: Option<u8>,
817    #[serde(default)]
818    pub tab_course: Option<u8>,
819    #[serde(default)]
820    pub tab_fret: Option<u16>,
821    #[serde(default)]
822    pub fingering: Option<u8>,
823}
824
825/// A barre range inside a [`ChordDefinition`] fretboard diagram.
826#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
827pub struct ChordBarre {
828    #[serde(default)]
829    pub start_member: Option<String>,
830    #[serde(default)]
831    pub end_member: Option<String>,
832    #[serde(default)]
833    pub fret: Option<u16>,
834    #[serde(default)]
835    pub label: Option<String>,
836    #[serde(default)]
837    pub kind: Option<String>,
838}
839
840/// One structured MusicXML chord degree.
841#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
842pub struct ChordDegree {
843    /// Scale degree number (normally 1 through 13).
844    pub value: u8,
845    /// Semitone alteration relative to the diatonic degree.
846    pub alter: i8,
847    /// MusicXML degree type, such as `add`, `alter`, or `subtract`.
848    pub kind: String,
849}
850
851/// One structured figure in a figured-bass annotation.
852#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
853pub struct FiguredBassFigure {
854    /// Source figure number, retained as text because MusicXML permits non-numeric figures.
855    pub number: String,
856    /// Raw source alteration value, such as `-1`, `0`, or `1`.
857    #[serde(default)]
858    pub alter: Option<String>,
859    /// Optional source prefix decoration.
860    #[serde(default)]
861    pub prefix: Option<String>,
862    /// Optional source suffix decoration.
863    #[serde(default)]
864    pub suffix: Option<String>,
865    /// Whether MEI marks this figure with a horizontal extender.
866    #[serde(default)]
867    pub extender: bool,
868}
869
870impl ChordDegree {
871    /// Render the degree using the compact chord-label convention.
872    pub fn display_text(&self) -> String {
873        let accidental = match self.alter {
874            -2 => "bb",
875            -1 => "b",
876            1 => "#",
877            2 => "##",
878            _ => "",
879        };
880        match self.kind.as_str() {
881            "subtract" => format!("no{}{}", accidental, self.value),
882            "alter" => format!("{}{}", accidental, self.value),
883            _ => format!("add{}{}", accidental, self.value),
884        }
885    }
886}
887
888/// Chord kinds (MusicXML `<kind>` values, plus aliases other formats use) and the compact
889/// suffix a chord label writes after its root. The first entry for a suffix is the kind a label
890/// reads back as.
891pub const CHORD_KIND_SUFFIXES: &[(&str, &str)] = &[
892    ("major", ""),
893    ("minor", "m"),
894    ("augmented", "aug"),
895    ("diminished", "dim"),
896    ("dominant", "7"),
897    ("major-seventh", "maj7"),
898    ("minor-seventh", "m7"),
899    ("diminished-seventh", "dim7"),
900    ("augmented-seventh", "aug7"),
901    ("half-diminished", "m7b5"),
902    ("major-minor", "mMaj7"),
903    ("minor-major-seventh", "mMaj7"),
904    ("minor-major", "mMaj7"),
905    ("major-sixth", "6"),
906    ("minor-sixth", "m6"),
907    ("dominant-ninth", "9"),
908    ("major-ninth", "maj9"),
909    ("minor-ninth", "m9"),
910    ("dominant-11th", "11"),
911    ("major-11th", "maj11"),
912    ("minor-11th", "m11"),
913    ("dominant-13th", "13"),
914    ("major-13th", "maj13"),
915    ("minor-13th", "m13"),
916    ("suspended-second", "sus2"),
917    ("suspended-fourth", "sus4"),
918    ("power", "5"),
919    ("major-add9", "add9"),
920    ("minor-add9", "madd9"),
921    ("dominant-flat-five", "7b5"),
922    ("dominant-sharp-five", "7#5"),
923];
924
925impl ChordSymbol {
926    /// The chord kind a compact label suffix (`"m7"`, `"aug7"`, `"13"`) stands for.
927    pub fn kind_for_suffix(suffix: &str) -> Option<&'static str> {
928        CHORD_KIND_SUFFIXES
929            .iter()
930            .find(|(_, candidate)| *candidate == suffix)
931            .map(|(kind, _)| *kind)
932    }
933
934    pub fn display_text(&self) -> String {
935        let kind_str = CHORD_KIND_SUFFIXES
936            .iter()
937            .find(|(kind, _)| *kind == self.kind)
938            .map_or(self.kind.as_str(), |(_, suffix)| *suffix);
939        let bass_str = match &self.bass {
940            Some(b) => format!("/{}", b),
941            None => String::new(),
942        };
943        let degree_str = self
944            .degrees
945            .iter()
946            .map(ChordDegree::display_text)
947            .collect::<String>();
948        format!("{}{}{}{}", self.root, kind_str, degree_str, bass_str)
949    }
950}
951
952#[cfg(test)]
953mod tests {
954    use super::*;
955
956    #[test]
957    fn key_alter_c_major_all_natural() {
958        let key = KeySignature {
959            fifths: 0,
960            mode: "major".into(),
961        };
962        for step in [
963            Step::C,
964            Step::D,
965            Step::E,
966            Step::F,
967            Step::G,
968            Step::A,
969            Step::B,
970        ] {
971            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
972        }
973    }
974
975    #[test]
976    fn key_alter_g_major_fsharp() {
977        let key = KeySignature {
978            fifths: 1,
979            mode: "major".into(),
980        };
981        assert_eq!(key.alter_for_step(&Step::F), 1);
982        assert_eq!(key.alter_for_step(&Step::G), 0);
983    }
984
985    #[test]
986    fn key_alter_f_major_bflat() {
987        let key = KeySignature {
988            fifths: -1,
989            mode: "major".into(),
990        };
991        assert_eq!(key.alter_for_step(&Step::B), -1);
992        assert_eq!(key.alter_for_step(&Step::C), 0);
993    }
994
995    #[test]
996    fn key_alter_bb_major() {
997        let key = KeySignature {
998            fifths: -2,
999            mode: "major".into(),
1000        };
1001        assert_eq!(key.alter_for_step(&Step::B), -1);
1002        assert_eq!(key.alter_for_step(&Step::E), -1);
1003        assert_eq!(key.alter_for_step(&Step::A), 0);
1004    }
1005
1006    #[test]
1007    fn key_contains_pitch_g_major() {
1008        let key = KeySignature {
1009            fifths: 1,
1010            mode: "major".into(),
1011        };
1012        // In-key: G, A, B, C, D, E, F# (alter=1)
1013        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
1014        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
1015        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
1016        // Out-of-key: F natural
1017        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
1018    }
1019
1020    #[test]
1021    fn key_display_name_c_major() {
1022        let key = KeySignature {
1023            fifths: 0,
1024            mode: "major".into(),
1025        };
1026        assert_eq!(key.display_name(), "C major");
1027    }
1028
1029    #[test]
1030    fn key_display_name_bb_major() {
1031        let key = KeySignature {
1032            fifths: -2,
1033            mode: "major".into(),
1034        };
1035        assert_eq!(key.display_name(), "Bb major");
1036    }
1037
1038    #[test]
1039    fn key_display_name_fsharp_minor() {
1040        let key = KeySignature {
1041            fifths: 3,
1042            mode: "minor".into(),
1043        };
1044        assert_eq!(key.display_name(), "F# minor");
1045    }
1046
1047    #[test]
1048    fn key_tonic_d_major() {
1049        let key = KeySignature {
1050            fifths: 2,
1051            mode: "major".into(),
1052        };
1053        let (step, alter) = key.tonic();
1054        assert_eq!(step, Step::D);
1055        assert_eq!(alter, 0);
1056    }
1057
1058    #[test]
1059    fn key_tonic_a_minor() {
1060        // A minor = relative minor of C major (fifths=0)
1061        let key = KeySignature {
1062            fifths: 0,
1063            mode: "minor".into(),
1064        };
1065        let (step, alter) = key.tonic();
1066        assert_eq!(step, Step::A);
1067        assert_eq!(alter, 0);
1068    }
1069
1070    #[test]
1071    fn chord_display_major() {
1072        let c = ChordSymbol {
1073            root: "C".into(),
1074            kind: "major".into(),
1075            bass: None,
1076            placement: None,
1077            extender: false,
1078            harmonic_degree: None,
1079            harmony_function: None,
1080            harmony_type: None,
1081            chord_ref: None,
1082            range_end: None,
1083            degrees: Vec::new(),
1084        };
1085        assert_eq!(c.display_text(), "C");
1086    }
1087
1088    #[test]
1089    fn chord_display_minor_seventh_slash() {
1090        let c = ChordSymbol {
1091            root: "D".into(),
1092            kind: "minor-seventh".into(),
1093            bass: Some("F".into()),
1094            placement: None,
1095            extender: false,
1096            harmonic_degree: None,
1097            harmony_function: None,
1098            harmony_type: None,
1099            chord_ref: None,
1100            range_end: None,
1101            degrees: Vec::new(),
1102        };
1103        assert_eq!(c.display_text(), "Dm7/F");
1104    }
1105
1106    #[test]
1107    fn chord_display_structured_degrees() {
1108        let c = ChordSymbol {
1109            root: "C".into(),
1110            kind: "dominant".into(),
1111            bass: None,
1112            placement: None,
1113            extender: false,
1114            harmonic_degree: None,
1115            harmony_function: None,
1116            harmony_type: None,
1117            chord_ref: None,
1118            range_end: None,
1119            degrees: vec![
1120                ChordDegree {
1121                    value: 9,
1122                    alter: 1,
1123                    kind: "add".into(),
1124                },
1125                ChordDegree {
1126                    value: 5,
1127                    alter: -1,
1128                    kind: "alter".into(),
1129                },
1130                ChordDegree {
1131                    value: 3,
1132                    alter: 0,
1133                    kind: "subtract".into(),
1134                },
1135            ],
1136        };
1137        assert_eq!(c.display_text(), "C7add#9b5no3");
1138    }
1139
1140    #[test]
1141    fn chord_symbol_legacy_json_defaults_degrees() {
1142        let chord: ChordSymbol =
1143            serde_json::from_str(r#"{"root":"C","kind":"major","bass":null,"placement":null}"#)
1144                .expect("legacy chord symbol JSON deserializes");
1145        assert!(chord.degrees.is_empty());
1146        assert!(!chord.extender);
1147        assert!(chord.harmonic_degree.is_none());
1148        assert!(chord.harmony_function.is_none());
1149        assert!(chord.harmony_type.is_none());
1150    }
1151
1152    #[test]
1153    fn time_sig_total_beats_three_four() {
1154        let ts = TimeSignature {
1155            numerator: 3,
1156            denominator: 4,
1157        };
1158        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1159    }
1160
1161    #[test]
1162    fn time_sig_total_beats_six_eight() {
1163        let ts = TimeSignature {
1164            numerator: 6,
1165            denominator: 8,
1166        };
1167        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1168    }
1169
1170    #[test]
1171    fn clef_treble_middle_b4() {
1172        assert_eq!(Clef::Treble.middle_line_midi(), 71);
1173    }
1174
1175    #[test]
1176    fn clef_bass_middle_d3() {
1177        assert_eq!(Clef::Bass.middle_line_midi(), 50);
1178    }
1179
1180    #[test]
1181    fn clef_alto_middle_c4() {
1182        assert_eq!(Clef::Alto.middle_line_midi(), 60);
1183    }
1184
1185    #[test]
1186    fn clef_tenor_middle_a3() {
1187        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
1188    }
1189}