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
883impl ChordDegree {
884    /// Render the degree using the compact chord-label convention.
885    pub fn display_text(&self) -> String {
886        let accidental = match self.alter {
887            -2 => "bb",
888            -1 => "b",
889            1 => "#",
890            2 => "##",
891            _ => "",
892        };
893        match self.kind.as_str() {
894            "subtract" => format!("no{}{}", accidental, self.value),
895            "alter" => format!("{}{}", accidental, self.value),
896            _ => format!("add{}{}", accidental, self.value),
897        }
898    }
899}
900
901/// Chord kinds (MusicXML `<kind>` values, plus aliases other formats use) and the compact
902/// suffix a chord label writes after its root. The first entry for a suffix is the kind a label
903/// reads back as.
904pub const CHORD_KIND_SUFFIXES: &[(&str, &str)] = &[
905    ("major", ""),
906    ("minor", "m"),
907    ("augmented", "aug"),
908    ("diminished", "dim"),
909    ("dominant", "7"),
910    ("major-seventh", "maj7"),
911    ("minor-seventh", "m7"),
912    ("diminished-seventh", "dim7"),
913    ("augmented-seventh", "aug7"),
914    ("half-diminished", "m7b5"),
915    ("major-minor", "mMaj7"),
916    ("minor-major-seventh", "mMaj7"),
917    ("minor-major", "mMaj7"),
918    ("major-sixth", "6"),
919    ("minor-sixth", "m6"),
920    ("dominant-ninth", "9"),
921    ("major-ninth", "maj9"),
922    ("minor-ninth", "m9"),
923    ("dominant-11th", "11"),
924    ("major-11th", "maj11"),
925    ("minor-11th", "m11"),
926    ("dominant-13th", "13"),
927    ("major-13th", "maj13"),
928    ("minor-13th", "m13"),
929    ("suspended-second", "sus2"),
930    ("suspended-fourth", "sus4"),
931    ("power", "5"),
932    ("major-add9", "add9"),
933    ("minor-add9", "madd9"),
934    ("dominant-flat-five", "7b5"),
935    ("dominant-sharp-five", "7#5"),
936];
937
938impl ChordSymbol {
939    /// The chord kind a compact label suffix (`"m7"`, `"aug7"`, `"13"`) stands for.
940    pub fn kind_for_suffix(suffix: &str) -> Option<&'static str> {
941        CHORD_KIND_SUFFIXES
942            .iter()
943            .find(|(_, candidate)| *candidate == suffix)
944            .map(|(kind, _)| *kind)
945    }
946
947    pub fn display_text(&self) -> String {
948        let kind_str = CHORD_KIND_SUFFIXES
949            .iter()
950            .find(|(kind, _)| *kind == self.kind)
951            .map_or(self.kind.as_str(), |(_, suffix)| *suffix);
952        let bass_str = match &self.bass {
953            Some(b) => format!("/{}", b),
954            None => String::new(),
955        };
956        let degree_str = self
957            .degrees
958            .iter()
959            .map(ChordDegree::display_text)
960            .collect::<String>();
961        format!("{}{}{}{}", self.root, kind_str, degree_str, bass_str)
962    }
963}
964
965#[cfg(test)]
966mod tests {
967    use super::*;
968
969    #[test]
970    fn key_alter_c_major_all_natural() {
971        let key = KeySignature {
972            fifths: 0,
973            mode: "major".into(),
974        };
975        for step in [
976            Step::C,
977            Step::D,
978            Step::E,
979            Step::F,
980            Step::G,
981            Step::A,
982            Step::B,
983        ] {
984            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
985        }
986    }
987
988    #[test]
989    fn key_alter_g_major_fsharp() {
990        let key = KeySignature {
991            fifths: 1,
992            mode: "major".into(),
993        };
994        assert_eq!(key.alter_for_step(&Step::F), 1);
995        assert_eq!(key.alter_for_step(&Step::G), 0);
996    }
997
998    #[test]
999    fn key_alter_f_major_bflat() {
1000        let key = KeySignature {
1001            fifths: -1,
1002            mode: "major".into(),
1003        };
1004        assert_eq!(key.alter_for_step(&Step::B), -1);
1005        assert_eq!(key.alter_for_step(&Step::C), 0);
1006    }
1007
1008    #[test]
1009    fn key_alter_bb_major() {
1010        let key = KeySignature {
1011            fifths: -2,
1012            mode: "major".into(),
1013        };
1014        assert_eq!(key.alter_for_step(&Step::B), -1);
1015        assert_eq!(key.alter_for_step(&Step::E), -1);
1016        assert_eq!(key.alter_for_step(&Step::A), 0);
1017    }
1018
1019    #[test]
1020    fn key_contains_pitch_g_major() {
1021        let key = KeySignature {
1022            fifths: 1,
1023            mode: "major".into(),
1024        };
1025        // In-key: G, A, B, C, D, E, F# (alter=1)
1026        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
1027        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
1028        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
1029        // Out-of-key: F natural
1030        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
1031    }
1032
1033    #[test]
1034    fn key_display_name_c_major() {
1035        let key = KeySignature {
1036            fifths: 0,
1037            mode: "major".into(),
1038        };
1039        assert_eq!(key.display_name(), "C major");
1040    }
1041
1042    #[test]
1043    fn key_display_name_bb_major() {
1044        let key = KeySignature {
1045            fifths: -2,
1046            mode: "major".into(),
1047        };
1048        assert_eq!(key.display_name(), "Bb major");
1049    }
1050
1051    #[test]
1052    fn key_display_name_fsharp_minor() {
1053        let key = KeySignature {
1054            fifths: 3,
1055            mode: "minor".into(),
1056        };
1057        assert_eq!(key.display_name(), "F# minor");
1058    }
1059
1060    #[test]
1061    fn key_tonic_d_major() {
1062        let key = KeySignature {
1063            fifths: 2,
1064            mode: "major".into(),
1065        };
1066        let (step, alter) = key.tonic();
1067        assert_eq!(step, Step::D);
1068        assert_eq!(alter, 0);
1069    }
1070
1071    #[test]
1072    fn key_tonic_a_minor() {
1073        // A minor = relative minor of C major (fifths=0)
1074        let key = KeySignature {
1075            fifths: 0,
1076            mode: "minor".into(),
1077        };
1078        let (step, alter) = key.tonic();
1079        assert_eq!(step, Step::A);
1080        assert_eq!(alter, 0);
1081    }
1082
1083    #[test]
1084    fn chord_display_major() {
1085        let c = ChordSymbol {
1086            root: "C".into(),
1087            kind: "major".into(),
1088            bass: None,
1089            placement: None,
1090            extender: false,
1091            harmonic_degree: None,
1092            harmony_function: None,
1093            harmony_type: None,
1094            chord_ref: None,
1095            range_end: None,
1096            degrees: Vec::new(),
1097        };
1098        assert_eq!(c.display_text(), "C");
1099    }
1100
1101    #[test]
1102    fn chord_display_minor_seventh_slash() {
1103        let c = ChordSymbol {
1104            root: "D".into(),
1105            kind: "minor-seventh".into(),
1106            bass: Some("F".into()),
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(), "Dm7/F");
1117    }
1118
1119    #[test]
1120    fn chord_display_structured_degrees() {
1121        let c = ChordSymbol {
1122            root: "C".into(),
1123            kind: "dominant".into(),
1124            bass: None,
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![
1133                ChordDegree {
1134                    value: 9,
1135                    alter: 1,
1136                    kind: "add".into(),
1137                },
1138                ChordDegree {
1139                    value: 5,
1140                    alter: -1,
1141                    kind: "alter".into(),
1142                },
1143                ChordDegree {
1144                    value: 3,
1145                    alter: 0,
1146                    kind: "subtract".into(),
1147                },
1148            ],
1149        };
1150        assert_eq!(c.display_text(), "C7add#9b5no3");
1151    }
1152
1153    #[test]
1154    fn chord_symbol_legacy_json_defaults_degrees() {
1155        let chord: ChordSymbol =
1156            serde_json::from_str(r#"{"root":"C","kind":"major","bass":null,"placement":null}"#)
1157                .expect("legacy chord symbol JSON deserializes");
1158        assert!(chord.degrees.is_empty());
1159        assert!(!chord.extender);
1160        assert!(chord.harmonic_degree.is_none());
1161        assert!(chord.harmony_function.is_none());
1162        assert!(chord.harmony_type.is_none());
1163    }
1164
1165    #[test]
1166    fn time_sig_total_beats_three_four() {
1167        let ts = TimeSignature {
1168            numerator: 3,
1169            denominator: 4,
1170        };
1171        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1172    }
1173
1174    #[test]
1175    fn time_sig_total_beats_six_eight() {
1176        let ts = TimeSignature {
1177            numerator: 6,
1178            denominator: 8,
1179        };
1180        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
1181    }
1182
1183    #[test]
1184    fn clef_treble_middle_b4() {
1185        assert_eq!(Clef::Treble.middle_line_midi(), 71);
1186    }
1187
1188    #[test]
1189    fn clef_bass_middle_d3() {
1190        assert_eq!(Clef::Bass.middle_line_midi(), 50);
1191    }
1192
1193    #[test]
1194    fn clef_alto_middle_c4() {
1195        assert_eq!(Clef::Alto.middle_line_midi(), 60);
1196    }
1197
1198    #[test]
1199    fn clef_tenor_middle_a3() {
1200        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
1201    }
1202}