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