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