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