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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
569pub enum BeamState {
570    #[default]
571    None,
572    Begin,
573    Continue,
574    End,
575    BeginEnd,
576    BackwardHook,
577    ForwardHook,
578}
579
580/// Ottava (octave transposition bracket).
581#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
582pub enum OttavaKind {
583    /// 8va — sounds one octave higher than written.
584    Va8,
585    /// 8vb — sounds one octave lower than written.
586    Vb8,
587    /// 15ma — sounds two octaves higher than written.
588    Ma15,
589    /// 15mb — sounds two octaves lower than written.
590    Mb15,
591}
592
593impl OttavaKind {
594    /// MusicXML `octave-shift@type`: the direction the notes are *displayed* shifted from
595    /// their (sounding) pitch, so an 8va — sounding higher than written — is `down`.
596    pub fn musicxml_type(&self) -> &'static str {
597        match self {
598            OttavaKind::Va8 | OttavaKind::Ma15 => "down",
599            OttavaKind::Vb8 | OttavaKind::Mb15 => "up",
600        }
601    }
602
603    /// Diatonic steps by which notes under this mark are drawn from their sounding pitch.
604    pub fn display_shift_steps(&self) -> i32 {
605        match self {
606            OttavaKind::Va8 => -7,
607            OttavaKind::Ma15 => -14,
608            OttavaKind::Vb8 => 7,
609            OttavaKind::Mb15 => 14,
610        }
611    }
612
613    pub fn musicxml_size(&self) -> u8 {
614        match self {
615            OttavaKind::Va8 | OttavaKind::Vb8 => 8,
616            OttavaKind::Ma15 | OttavaKind::Mb15 => 15,
617        }
618    }
619}
620
621/// How a pitch's accidental is shown when the notation rules alone would not decide it: a
622/// cautionary (courtesy) accidental the source asked for, one in parentheses or brackets, or
623/// an editorial one. `Auto` (the default) leaves the accidental to the key and the bar.
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
625pub enum AccidentalDisplay {
626    #[default]
627    Auto,
628    /// Always shown, without parentheses (MusicXML `cautionary="yes"`, MEI `@func="caution"`).
629    Cautionary,
630    /// Always shown, in parentheses (MusicXML `parentheses="yes"`/`bracket="yes"`, MEI
631    /// `@enclose`, MuseScore accidental brackets).
632    Parenthesized,
633    /// An editor's addition (MusicXML `editorial="yes"`, MEI `@func="edit"`), always shown.
634    Editorial,
635}
636
637/// Note head shape.
638#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
639pub enum NoteHead {
640    #[default]
641    Normal,
642    Diamond,  // natural harmonics
643    X,        // muted / dead note
644    Slash,    // ghost note
645    Cross,    // percussion special
646    Triangle, // tap harmonics
647}
648
649/// Lyric syllable attached to a note.
650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
651pub struct Lyric {
652    /// The syllable text.
653    pub text: String,
654    /// Syllabic position: "single" | "begin" | "middle" | "end"
655    pub syllabic: String,
656    /// A melisma extender line follows the syllable, under the notes up to (not including)
657    /// the next note with a lyric or the next rest (MusicXML `<extend>`, MEI `con="u"`).
658    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
659    pub extend: bool,
660}
661
662/// A lyric syllable for verse 2 or later. Verse 1 stays in `Note.lyric`.
663#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
664pub struct VerseLyric {
665    /// Verse number, from 2 to [`VerseLyric::MAX_VERSE`].
666    pub verse: u8,
667    pub lyric: Lyric,
668}
669
670impl VerseLyric {
671    /// Highest supported verse number.
672    pub const MAX_VERSE: u8 = 32;
673}
674
675/// A typed score text annotation. The text itself is kept separate from its
676/// presentation role so consumers do not need to infer semantics from prose.
677#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
678pub struct StyledText {
679    pub style: TextStyle,
680    pub text: String,
681    /// Optional vertical placement hint from interchange formats.
682    #[serde(default)]
683    pub placement: Option<String>,
684    /// MusicXML default horizontal offset in tenths, when attached to a direction text.
685    #[serde(default)]
686    pub offset_x: Option<f64>,
687    /// MusicXML default vertical offset in tenths, when attached to a direction text.
688    #[serde(default)]
689    pub offset_y: Option<f64>,
690    /// MusicXML relative horizontal offset in tenths, retaining its relative-coordinate meaning.
691    #[serde(default)]
692    pub relative_x: Option<f64>,
693    /// MusicXML relative vertical offset in tenths, retaining its relative-coordinate meaning.
694    #[serde(default)]
695    pub relative_y: Option<f64>,
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
699pub enum TextStyle {
700    Expression,
701    Technique,
702    Lyrics,
703    ChordSymbol,
704    FiguredBass,
705    RehearsalMark,
706    Generic,
707}
708
709/// Cross-staff placement metadata. The note remains in its source staff, so
710/// its stable playback address continues to identify the original note.
711#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
712pub struct CrossStaff {
713    pub target_staff: usize,
714    #[serde(default)]
715    pub target_voice: Option<usize>,
716}
717
718/// A tablature string/fret position attached to a pitched note.
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720pub struct TabPosition {
721    /// One-based string number, matching MusicXML `<string>`.
722    pub string: u8,
723    pub fret: u8,
724}
725
726/// Tablature staff metadata. Tuning MIDI values are ordered by string number.
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
728pub struct TablatureConfig {
729    pub lines: u8,
730    pub tuning_midi: Vec<i16>,
731    #[serde(default)]
732    pub capo: u8,
733}
734
735/// Structured chord symbol attached to a note.
736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
737pub struct ChordSymbol {
738    /// Root note name: "C", "F#", "Bb", etc.
739    pub root: String,
740    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
741    pub kind: String,
742    /// Slash-chord bass note.
743    pub bass: Option<String>,
744    /// Optional vertical placement hint from interchange formats (for example `above`/`below`).
745    #[serde(default)]
746    pub placement: Option<String>,
747    /// Whether the source harmony carries a continuation/extender line.
748    #[serde(default)]
749    pub extender: bool,
750    /// MEI harmonic-analysis scale degree (Humdrum **deg syntax), when supplied.
751    #[serde(default)]
752    pub harmonic_degree: Option<String>,
753    /// MEI harmonic function token (for example `T`, `PD`, or `D`), when supplied.
754    #[serde(default)]
755    pub harmony_function: Option<String>,
756    /// MEI `harm@type` classification token(s), when supplied.
757    #[serde(default)]
758    pub harmony_type: Option<String>,
759    /// MEI `harm@chordref` URI, retained without resolving an external chord definition.
760    #[serde(default)]
761    pub chord_ref: Option<String>,
762    /// Optional note address where a harmony range ends.
763    ///
764    /// This is primarily used by MEI `harm@tstamp2`/`endid`.  The field is
765    /// optional so older score JSON and formats without harmony ranges remain
766    /// fully compatible.
767    #[serde(default)]
768    pub range_end: Option<NoteAddr>,
769    /// Structured chord extensions such as add9, alter5, or omit3.
770    #[serde(default)]
771    pub degrees: Vec<ChordDegree>,
772}
773
774/// A reusable MEI chord/tablature definition referenced by `harm@chordref`.
775///
776/// The fields intentionally retain the source spelling for deprecated MEI tuning
777/// attributes.  Consumers may interpret the member positions without requiring
778/// the canonical score to invent an instrument catalog.
779#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
780pub struct ChordDefinition {
781    #[serde(default)]
782    pub id: Option<String>,
783    #[serde(default)]
784    pub label: Option<String>,
785    #[serde(default)]
786    pub kind: Option<String>,
787    #[serde(default)]
788    pub fret_position: Option<u32>,
789    #[serde(default)]
790    pub tab_strings: Option<String>,
791    #[serde(default)]
792    pub tab_courses: Option<String>,
793    #[serde(default)]
794    pub members: Vec<ChordDefinitionMember>,
795    #[serde(default)]
796    pub barres: Vec<ChordBarre>,
797}
798
799/// One pitch and/or tablature position in a [`ChordDefinition`].
800#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
801pub struct ChordDefinitionMember {
802    #[serde(default)]
803    pub id: Option<String>,
804    #[serde(default)]
805    pub pitch: Option<Pitch>,
806    #[serde(default)]
807    pub tab_string: Option<u8>,
808    #[serde(default)]
809    pub tab_course: Option<u8>,
810    #[serde(default)]
811    pub tab_fret: Option<u16>,
812    #[serde(default)]
813    pub fingering: Option<u8>,
814}
815
816/// A barre range inside a [`ChordDefinition`] fretboard diagram.
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818pub struct ChordBarre {
819    #[serde(default)]
820    pub start_member: Option<String>,
821    #[serde(default)]
822    pub end_member: Option<String>,
823    #[serde(default)]
824    pub fret: Option<u16>,
825    #[serde(default)]
826    pub label: Option<String>,
827    #[serde(default)]
828    pub kind: Option<String>,
829}
830
831/// One structured MusicXML chord degree.
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833pub struct ChordDegree {
834    /// Scale degree number (normally 1 through 13).
835    pub value: u8,
836    /// Semitone alteration relative to the diatonic degree.
837    pub alter: i8,
838    /// MusicXML degree type, such as `add`, `alter`, or `subtract`.
839    pub kind: String,
840}
841
842/// One structured figure in a figured-bass annotation.
843#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
844pub struct FiguredBassFigure {
845    /// Source figure number, retained as text because MusicXML permits non-numeric figures.
846    pub number: String,
847    /// Raw source alteration value, such as `-1`, `0`, or `1`.
848    #[serde(default)]
849    pub alter: Option<String>,
850    /// Optional source prefix decoration.
851    #[serde(default)]
852    pub prefix: Option<String>,
853    /// Optional source suffix decoration.
854    #[serde(default)]
855    pub suffix: Option<String>,
856    /// Whether MEI marks this figure with a horizontal extender.
857    #[serde(default)]
858    pub extender: bool,
859}
860
861impl ChordDegree {
862    /// Render the degree using the compact chord-label convention.
863    pub fn display_text(&self) -> String {
864        let accidental = match self.alter {
865            -2 => "bb",
866            -1 => "b",
867            1 => "#",
868            2 => "##",
869            _ => "",
870        };
871        match self.kind.as_str() {
872            "subtract" => format!("no{}{}", accidental, self.value),
873            "alter" => format!("{}{}", accidental, self.value),
874            _ => format!("add{}{}", accidental, self.value),
875        }
876    }
877}
878
879/// Chord kinds (MusicXML `<kind>` values, plus aliases other formats use) and the compact
880/// suffix a chord label writes after its root. The first entry for a suffix is the kind a label
881/// reads back as.
882pub const CHORD_KIND_SUFFIXES: &[(&str, &str)] = &[
883    ("major", ""),
884    ("minor", "m"),
885    ("augmented", "aug"),
886    ("diminished", "dim"),
887    ("dominant", "7"),
888    ("major-seventh", "maj7"),
889    ("minor-seventh", "m7"),
890    ("diminished-seventh", "dim7"),
891    ("augmented-seventh", "aug7"),
892    ("half-diminished", "m7b5"),
893    ("major-minor", "mMaj7"),
894    ("minor-major-seventh", "mMaj7"),
895    ("minor-major", "mMaj7"),
896    ("major-sixth", "6"),
897    ("minor-sixth", "m6"),
898    ("dominant-ninth", "9"),
899    ("major-ninth", "maj9"),
900    ("minor-ninth", "m9"),
901    ("dominant-11th", "11"),
902    ("major-11th", "maj11"),
903    ("minor-11th", "m11"),
904    ("dominant-13th", "13"),
905    ("major-13th", "maj13"),
906    ("minor-13th", "m13"),
907    ("suspended-second", "sus2"),
908    ("suspended-fourth", "sus4"),
909    ("power", "5"),
910    ("major-add9", "add9"),
911    ("minor-add9", "madd9"),
912    ("dominant-flat-five", "7b5"),
913    ("dominant-sharp-five", "7#5"),
914];
915
916impl ChordSymbol {
917    /// The chord kind a compact label suffix (`"m7"`, `"aug7"`, `"13"`) stands for.
918    pub fn kind_for_suffix(suffix: &str) -> Option<&'static str> {
919        CHORD_KIND_SUFFIXES
920            .iter()
921            .find(|(_, candidate)| *candidate == suffix)
922            .map(|(kind, _)| *kind)
923    }
924
925    pub fn display_text(&self) -> String {
926        let kind_str = CHORD_KIND_SUFFIXES
927            .iter()
928            .find(|(kind, _)| *kind == self.kind)
929            .map_or(self.kind.as_str(), |(_, suffix)| *suffix);
930        let bass_str = match &self.bass {
931            Some(b) => format!("/{}", b),
932            None => String::new(),
933        };
934        let degree_str = self
935            .degrees
936            .iter()
937            .map(ChordDegree::display_text)
938            .collect::<String>();
939        format!("{}{}{}{}", self.root, kind_str, degree_str, bass_str)
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946
947    #[test]
948    fn key_alter_c_major_all_natural() {
949        let key = KeySignature {
950            fifths: 0,
951            mode: "major".into(),
952        };
953        for step in [
954            Step::C,
955            Step::D,
956            Step::E,
957            Step::F,
958            Step::G,
959            Step::A,
960            Step::B,
961        ] {
962            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
963        }
964    }
965
966    #[test]
967    fn key_alter_g_major_fsharp() {
968        let key = KeySignature {
969            fifths: 1,
970            mode: "major".into(),
971        };
972        assert_eq!(key.alter_for_step(&Step::F), 1);
973        assert_eq!(key.alter_for_step(&Step::G), 0);
974    }
975
976    #[test]
977    fn key_alter_f_major_bflat() {
978        let key = KeySignature {
979            fifths: -1,
980            mode: "major".into(),
981        };
982        assert_eq!(key.alter_for_step(&Step::B), -1);
983        assert_eq!(key.alter_for_step(&Step::C), 0);
984    }
985
986    #[test]
987    fn key_alter_bb_major() {
988        let key = KeySignature {
989            fifths: -2,
990            mode: "major".into(),
991        };
992        assert_eq!(key.alter_for_step(&Step::B), -1);
993        assert_eq!(key.alter_for_step(&Step::E), -1);
994        assert_eq!(key.alter_for_step(&Step::A), 0);
995    }
996
997    #[test]
998    fn key_contains_pitch_g_major() {
999        let key = KeySignature {
1000            fifths: 1,
1001            mode: "major".into(),
1002        };
1003        // In-key: G, A, B, C, D, E, F# (alter=1)
1004        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
1005        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
1006        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
1007        // Out-of-key: F natural
1008        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
1009    }
1010
1011    #[test]
1012    fn key_display_name_c_major() {
1013        let key = KeySignature {
1014            fifths: 0,
1015            mode: "major".into(),
1016        };
1017        assert_eq!(key.display_name(), "C major");
1018    }
1019
1020    #[test]
1021    fn key_display_name_bb_major() {
1022        let key = KeySignature {
1023            fifths: -2,
1024            mode: "major".into(),
1025        };
1026        assert_eq!(key.display_name(), "Bb major");
1027    }
1028
1029    #[test]
1030    fn key_display_name_fsharp_minor() {
1031        let key = KeySignature {
1032            fifths: 3,
1033            mode: "minor".into(),
1034        };
1035        assert_eq!(key.display_name(), "F# minor");
1036    }
1037
1038    #[test]
1039    fn key_tonic_d_major() {
1040        let key = KeySignature {
1041            fifths: 2,
1042            mode: "major".into(),
1043        };
1044        let (step, alter) = key.tonic();
1045        assert_eq!(step, Step::D);
1046        assert_eq!(alter, 0);
1047    }
1048
1049    #[test]
1050    fn key_tonic_a_minor() {
1051        // A minor = relative minor of C major (fifths=0)
1052        let key = KeySignature {
1053            fifths: 0,
1054            mode: "minor".into(),
1055        };
1056        let (step, alter) = key.tonic();
1057        assert_eq!(step, Step::A);
1058        assert_eq!(alter, 0);
1059    }
1060
1061    #[test]
1062    fn chord_display_major() {
1063        let c = ChordSymbol {
1064            root: "C".into(),
1065            kind: "major".into(),
1066            bass: None,
1067            placement: None,
1068            extender: false,
1069            harmonic_degree: None,
1070            harmony_function: None,
1071            harmony_type: None,
1072            chord_ref: None,
1073            range_end: None,
1074            degrees: Vec::new(),
1075        };
1076        assert_eq!(c.display_text(), "C");
1077    }
1078
1079    #[test]
1080    fn chord_display_minor_seventh_slash() {
1081        let c = ChordSymbol {
1082            root: "D".into(),
1083            kind: "minor-seventh".into(),
1084            bass: Some("F".into()),
1085            placement: None,
1086            extender: false,
1087            harmonic_degree: None,
1088            harmony_function: None,
1089            harmony_type: None,
1090            chord_ref: None,
1091            range_end: None,
1092            degrees: Vec::new(),
1093        };
1094        assert_eq!(c.display_text(), "Dm7/F");
1095    }
1096
1097    #[test]
1098    fn chord_display_structured_degrees() {
1099        let c = ChordSymbol {
1100            root: "C".into(),
1101            kind: "dominant".into(),
1102            bass: None,
1103            placement: None,
1104            extender: false,
1105            harmonic_degree: None,
1106            harmony_function: None,
1107            harmony_type: None,
1108            chord_ref: None,
1109            range_end: None,
1110            degrees: vec![
1111                ChordDegree {
1112                    value: 9,
1113                    alter: 1,
1114                    kind: "add".into(),
1115                },
1116                ChordDegree {
1117                    value: 5,
1118                    alter: -1,
1119                    kind: "alter".into(),
1120                },
1121                ChordDegree {
1122                    value: 3,
1123                    alter: 0,
1124                    kind: "subtract".into(),
1125                },
1126            ],
1127        };
1128        assert_eq!(c.display_text(), "C7add#9b5no3");
1129    }
1130
1131    #[test]
1132    fn chord_symbol_legacy_json_defaults_degrees() {
1133        let chord: ChordSymbol =
1134            serde_json::from_str(r#"{"root":"C","kind":"major","bass":null,"placement":null}"#)
1135                .expect("legacy chord symbol JSON deserializes");
1136        assert!(chord.degrees.is_empty());
1137        assert!(!chord.extender);
1138        assert!(chord.harmonic_degree.is_none());
1139        assert!(chord.harmony_function.is_none());
1140        assert!(chord.harmony_type.is_none());
1141    }
1142
1143    #[test]
1144    fn time_sig_total_beats_three_four() {
1145        let ts = TimeSignature {
1146            numerator: 3,
1147            denominator: 4,
1148        };
1149        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1150    }
1151
1152    #[test]
1153    fn time_sig_total_beats_six_eight() {
1154        let ts = TimeSignature {
1155            numerator: 6,
1156            denominator: 8,
1157        };
1158        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1159    }
1160
1161    #[test]
1162    fn clef_treble_middle_b4() {
1163        assert_eq!(Clef::Treble.middle_line_midi(), 71);
1164    }
1165
1166    #[test]
1167    fn clef_bass_middle_d3() {
1168        assert_eq!(Clef::Bass.middle_line_midi(), 50);
1169    }
1170
1171    #[test]
1172    fn clef_alto_middle_c4() {
1173        assert_eq!(Clef::Alto.middle_line_midi(), 60);
1174    }
1175
1176    #[test]
1177    fn clef_tenor_middle_a3() {
1178        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
1179    }
1180}