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, PartialEq, 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}
211
212impl Dynamic {
213    pub fn to_musicxml_str(&self) -> &'static str {
214        match self {
215            Dynamic::Pppp => "pppp",
216            Dynamic::Ppp => "ppp",
217            Dynamic::Pp => "pp",
218            Dynamic::P => "p",
219            Dynamic::Mp => "mp",
220            Dynamic::Mf => "mf",
221            Dynamic::F => "f",
222            Dynamic::Ff => "ff",
223            Dynamic::Fff => "fff",
224            Dynamic::Ffff => "ffff",
225            Dynamic::Sfz => "sfz",
226            Dynamic::Rfz => "rfz",
227            Dynamic::Fz => "fz",
228            Dynamic::Sf => "sf",
229        }
230    }
231
232    pub fn to_velocity(&self) -> u8 {
233        match self {
234            Dynamic::Pppp => 16,
235            Dynamic::Ppp => 24,
236            Dynamic::Pp => 36,
237            Dynamic::P => 48,
238            Dynamic::Mp => 60,
239            Dynamic::Mf => 72,
240            Dynamic::F => 84,
241            Dynamic::Ff => 96,
242            Dynamic::Fff => 108,
243            Dynamic::Ffff => 120,
244            Dynamic::Sfz => 112,
245            Dynamic::Rfz => 104,
246            Dynamic::Fz => 100,
247            Dynamic::Sf => 96,
248        }
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253pub enum Articulation {
254    Staccato,
255    Staccatissimo,
256    Accent,
257    Tenuto,
258    Marcato,
259    Fermata,
260    Trill,
261    Mordent,
262    InvertedMordent,
263    Turn,
264    InvertedTurn,
265    Shake,
266    Tremolo(u8),
267    BreathMark,
268    Caesura,
269    /// String up-bow (MusicXML `<up-bow/>`).
270    UpBow,
271    /// String down-bow (MusicXML `<down-bow/>`).
272    DownBow,
273    /// Harmonic circle (MusicXML `<harmonic/>`).
274    Harmonic,
275    /// Open string / open mute circle (MusicXML `<open-string/>`).
276    OpenString,
277    /// Stopped note or closed mute "+" (MusicXML `<stopped/>`).
278    Stopped,
279    /// Snap (Bartók) pizzicato (MusicXML `<snap-pizzicato/>`).
280    SnapPizzicato,
281}
282
283impl Articulation {
284    /// String and brass techniques that MusicXML writes inside `<technical>` rather than
285    /// `<articulations>`.
286    pub fn is_technical_mark(&self) -> bool {
287        matches!(
288            self,
289            Self::UpBow
290                | Self::DownBow
291                | Self::Harmonic
292                | Self::OpenString
293                | Self::Stopped
294                | Self::SnapPizzicato
295        )
296    }
297}
298
299/// Guitar-specific playing technique attached to a note.
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301#[serde(rename_all = "kebab-case")]
302pub enum GuitarTechnique {
303    Bend,
304    Slide,
305    HammerOn,
306    PullOff,
307}
308
309/// Deterministic policy for selecting one candidate from an ordered fingering list.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
311pub enum FingeringSelectionPolicy {
312    /// Preserve the source/application's first candidate.
313    #[default]
314    SourceOrder,
315    /// Select the numerically smallest candidate.
316    LowestNumber,
317    /// Select the numerically largest candidate.
318    HighestNumber,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
322pub enum Barline {
323    #[default]
324    Normal,
325    Double,
326    Final,
327    RepeatStart,
328    RepeatEnd,
329    RepeatBoth,
330    Dashed,
331    Dotted,
332    Invisible,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
336pub enum HairpinKind {
337    Crescendo,
338    Decrescendo,
339}
340
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342pub struct TupletInfo {
343    /// Notes in the tuplet group (e.g. 3 for triplet).
344    pub actual_notes: u8,
345    /// Normal notes displaced (e.g. 2 for triplet = 3-in-2).
346    pub normal_notes: u8,
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
350pub enum BeamState {
351    #[default]
352    None,
353    Begin,
354    Continue,
355    End,
356    BeginEnd,
357    BackwardHook,
358    ForwardHook,
359}
360
361/// Ottava (octave transposition bracket).
362#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
363pub enum OttavaKind {
364    /// 8va — sounds one octave higher than written.
365    Va8,
366    /// 8vb — sounds one octave lower than written.
367    Vb8,
368    /// 15ma — sounds two octaves higher than written.
369    Ma15,
370    /// 15mb — sounds two octaves lower than written.
371    Mb15,
372}
373
374impl OttavaKind {
375    pub fn musicxml_type(&self) -> &'static str {
376        match self {
377            OttavaKind::Va8 | OttavaKind::Ma15 => "up",
378            OttavaKind::Vb8 | OttavaKind::Mb15 => "down",
379        }
380    }
381
382    pub fn musicxml_size(&self) -> u8 {
383        match self {
384            OttavaKind::Va8 | OttavaKind::Vb8 => 8,
385            OttavaKind::Ma15 | OttavaKind::Mb15 => 15,
386        }
387    }
388}
389
390/// Note head shape.
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
392pub enum NoteHead {
393    #[default]
394    Normal,
395    Diamond,  // natural harmonics
396    X,        // muted / dead note
397    Slash,    // ghost note
398    Cross,    // percussion special
399    Triangle, // tap harmonics
400}
401
402/// Lyric syllable attached to a note.
403#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
404pub struct Lyric {
405    /// The syllable text.
406    pub text: String,
407    /// Syllabic position: "single" | "begin" | "middle" | "end"
408    pub syllabic: String,
409}
410
411/// A lyric syllable for verse 2 or later. Verse 1 stays in `Note.lyric`.
412#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413pub struct VerseLyric {
414    /// Verse number, from 2 to [`VerseLyric::MAX_VERSE`].
415    pub verse: u8,
416    pub lyric: Lyric,
417}
418
419impl VerseLyric {
420    /// Highest supported verse number.
421    pub const MAX_VERSE: u8 = 32;
422}
423
424/// A typed score text annotation. The text itself is kept separate from its
425/// presentation role so consumers do not need to infer semantics from prose.
426#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
427pub struct StyledText {
428    pub style: TextStyle,
429    pub text: String,
430    /// Optional vertical placement hint from interchange formats.
431    #[serde(default)]
432    pub placement: Option<String>,
433    /// MusicXML default horizontal offset in tenths, when attached to a direction text.
434    #[serde(default)]
435    pub offset_x: Option<f64>,
436    /// MusicXML default vertical offset in tenths, when attached to a direction text.
437    #[serde(default)]
438    pub offset_y: Option<f64>,
439    /// MusicXML relative horizontal offset in tenths, retaining its relative-coordinate meaning.
440    #[serde(default)]
441    pub relative_x: Option<f64>,
442    /// MusicXML relative vertical offset in tenths, retaining its relative-coordinate meaning.
443    #[serde(default)]
444    pub relative_y: Option<f64>,
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
448pub enum TextStyle {
449    Expression,
450    Technique,
451    Lyrics,
452    ChordSymbol,
453    FiguredBass,
454    RehearsalMark,
455    Generic,
456}
457
458/// Cross-staff placement metadata. The note remains in its source staff, so
459/// its stable playback address continues to identify the original note.
460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
461pub struct CrossStaff {
462    pub target_staff: usize,
463    #[serde(default)]
464    pub target_voice: Option<usize>,
465}
466
467/// A tablature string/fret position attached to a pitched note.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct TabPosition {
470    /// One-based string number, matching MusicXML `<string>`.
471    pub string: u8,
472    pub fret: u8,
473}
474
475/// Tablature staff metadata. Tuning MIDI values are ordered by string number.
476#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
477pub struct TablatureConfig {
478    pub lines: u8,
479    pub tuning_midi: Vec<i16>,
480    #[serde(default)]
481    pub capo: u8,
482}
483
484/// Structured chord symbol attached to a note.
485#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
486pub struct ChordSymbol {
487    /// Root note name: "C", "F#", "Bb", etc.
488    pub root: String,
489    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
490    pub kind: String,
491    /// Slash-chord bass note.
492    pub bass: Option<String>,
493    /// Optional vertical placement hint from interchange formats (for example `above`/`below`).
494    #[serde(default)]
495    pub placement: Option<String>,
496    /// Whether the source harmony carries a continuation/extender line.
497    #[serde(default)]
498    pub extender: bool,
499    /// MEI harmonic-analysis scale degree (Humdrum **deg syntax), when supplied.
500    #[serde(default)]
501    pub harmonic_degree: Option<String>,
502    /// MEI harmonic function token (for example `T`, `PD`, or `D`), when supplied.
503    #[serde(default)]
504    pub harmony_function: Option<String>,
505    /// MEI `harm@type` classification token(s), when supplied.
506    #[serde(default)]
507    pub harmony_type: Option<String>,
508    /// MEI `harm@chordref` URI, retained without resolving an external chord definition.
509    #[serde(default)]
510    pub chord_ref: Option<String>,
511    /// Optional note address where a harmony range ends.
512    ///
513    /// This is primarily used by MEI `harm@tstamp2`/`endid`.  The field is
514    /// optional so older score JSON and formats without harmony ranges remain
515    /// fully compatible.
516    #[serde(default)]
517    pub range_end: Option<NoteAddr>,
518    /// Structured chord extensions such as add9, alter5, or omit3.
519    #[serde(default)]
520    pub degrees: Vec<ChordDegree>,
521}
522
523/// A reusable MEI chord/tablature definition referenced by `harm@chordref`.
524///
525/// The fields intentionally retain the source spelling for deprecated MEI tuning
526/// attributes.  Consumers may interpret the member positions without requiring
527/// the canonical score to invent an instrument catalog.
528#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
529pub struct ChordDefinition {
530    #[serde(default)]
531    pub id: Option<String>,
532    #[serde(default)]
533    pub label: Option<String>,
534    #[serde(default)]
535    pub kind: Option<String>,
536    #[serde(default)]
537    pub fret_position: Option<u32>,
538    #[serde(default)]
539    pub tab_strings: Option<String>,
540    #[serde(default)]
541    pub tab_courses: Option<String>,
542    #[serde(default)]
543    pub members: Vec<ChordDefinitionMember>,
544    #[serde(default)]
545    pub barres: Vec<ChordBarre>,
546}
547
548/// One pitch and/or tablature position in a [`ChordDefinition`].
549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
550pub struct ChordDefinitionMember {
551    #[serde(default)]
552    pub id: Option<String>,
553    #[serde(default)]
554    pub pitch: Option<Pitch>,
555    #[serde(default)]
556    pub tab_string: Option<u8>,
557    #[serde(default)]
558    pub tab_course: Option<u8>,
559    #[serde(default)]
560    pub tab_fret: Option<u16>,
561    #[serde(default)]
562    pub fingering: Option<u8>,
563}
564
565/// A barre range inside a [`ChordDefinition`] fretboard diagram.
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567pub struct ChordBarre {
568    #[serde(default)]
569    pub start_member: Option<String>,
570    #[serde(default)]
571    pub end_member: Option<String>,
572    #[serde(default)]
573    pub fret: Option<u16>,
574    #[serde(default)]
575    pub label: Option<String>,
576    #[serde(default)]
577    pub kind: Option<String>,
578}
579
580/// One structured MusicXML chord degree.
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub struct ChordDegree {
583    /// Scale degree number (normally 1 through 13).
584    pub value: u8,
585    /// Semitone alteration relative to the diatonic degree.
586    pub alter: i8,
587    /// MusicXML degree type, such as `add`, `alter`, or `subtract`.
588    pub kind: String,
589}
590
591/// One structured figure in a figured-bass annotation.
592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
593pub struct FiguredBassFigure {
594    /// Source figure number, retained as text because MusicXML permits non-numeric figures.
595    pub number: String,
596    /// Raw source alteration value, such as `-1`, `0`, or `1`.
597    #[serde(default)]
598    pub alter: Option<String>,
599    /// Optional source prefix decoration.
600    #[serde(default)]
601    pub prefix: Option<String>,
602    /// Optional source suffix decoration.
603    #[serde(default)]
604    pub suffix: Option<String>,
605    /// Whether MEI marks this figure with a horizontal extender.
606    #[serde(default)]
607    pub extender: bool,
608}
609
610impl ChordDegree {
611    /// Render the degree using the compact chord-label convention.
612    pub fn display_text(&self) -> String {
613        let accidental = match self.alter {
614            -2 => "bb",
615            -1 => "b",
616            1 => "#",
617            2 => "##",
618            _ => "",
619        };
620        match self.kind.as_str() {
621            "subtract" => format!("no{}{}", accidental, self.value),
622            "alter" => format!("{}{}", accidental, self.value),
623            _ => format!("add{}{}", accidental, self.value),
624        }
625    }
626}
627
628impl ChordSymbol {
629    pub fn display_text(&self) -> String {
630        let kind_str = match self.kind.as_str() {
631            "major" | "" => "",
632            "minor" => "m",
633            "dominant" => "7",
634            "major-seventh" => "maj7",
635            "minor-seventh" => "m7",
636            "diminished" => "dim",
637            "diminished-seventh" => "dim7",
638            "augmented" => "aug",
639            "suspended-second" => "sus2",
640            "suspended-fourth" => "sus4",
641            "half-diminished" => "m7b5",
642            "major-sixth" => "6",
643            "minor-sixth" => "m6",
644            "power" => "5",
645            "major-add9" => "add9",
646            "minor-add9" => "madd9",
647            "minor-major-seventh" => "mMaj7",
648            "dominant-flat-five" => "7b5",
649            "dominant-sharp-five" => "7#5",
650            "dominant-ninth" => "9",
651            "major-ninth" => "maj9",
652            "minor-ninth" => "m9",
653            other => other,
654        };
655        let bass_str = match &self.bass {
656            Some(b) => format!("/{}", b),
657            None => String::new(),
658        };
659        let degree_str = self
660            .degrees
661            .iter()
662            .map(ChordDegree::display_text)
663            .collect::<String>();
664        format!("{}{}{}{}", self.root, kind_str, degree_str, bass_str)
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn key_alter_c_major_all_natural() {
674        let key = KeySignature {
675            fifths: 0,
676            mode: "major".into(),
677        };
678        for step in [
679            Step::C,
680            Step::D,
681            Step::E,
682            Step::F,
683            Step::G,
684            Step::A,
685            Step::B,
686        ] {
687            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
688        }
689    }
690
691    #[test]
692    fn key_alter_g_major_fsharp() {
693        let key = KeySignature {
694            fifths: 1,
695            mode: "major".into(),
696        };
697        assert_eq!(key.alter_for_step(&Step::F), 1);
698        assert_eq!(key.alter_for_step(&Step::G), 0);
699    }
700
701    #[test]
702    fn key_alter_f_major_bflat() {
703        let key = KeySignature {
704            fifths: -1,
705            mode: "major".into(),
706        };
707        assert_eq!(key.alter_for_step(&Step::B), -1);
708        assert_eq!(key.alter_for_step(&Step::C), 0);
709    }
710
711    #[test]
712    fn key_alter_bb_major() {
713        let key = KeySignature {
714            fifths: -2,
715            mode: "major".into(),
716        };
717        assert_eq!(key.alter_for_step(&Step::B), -1);
718        assert_eq!(key.alter_for_step(&Step::E), -1);
719        assert_eq!(key.alter_for_step(&Step::A), 0);
720    }
721
722    #[test]
723    fn key_contains_pitch_g_major() {
724        let key = KeySignature {
725            fifths: 1,
726            mode: "major".into(),
727        };
728        // In-key: G, A, B, C, D, E, F# (alter=1)
729        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
730        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
731        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
732        // Out-of-key: F natural
733        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
734    }
735
736    #[test]
737    fn key_display_name_c_major() {
738        let key = KeySignature {
739            fifths: 0,
740            mode: "major".into(),
741        };
742        assert_eq!(key.display_name(), "C major");
743    }
744
745    #[test]
746    fn key_display_name_bb_major() {
747        let key = KeySignature {
748            fifths: -2,
749            mode: "major".into(),
750        };
751        assert_eq!(key.display_name(), "Bb major");
752    }
753
754    #[test]
755    fn key_display_name_fsharp_minor() {
756        let key = KeySignature {
757            fifths: 3,
758            mode: "minor".into(),
759        };
760        assert_eq!(key.display_name(), "F# minor");
761    }
762
763    #[test]
764    fn key_tonic_d_major() {
765        let key = KeySignature {
766            fifths: 2,
767            mode: "major".into(),
768        };
769        let (step, alter) = key.tonic();
770        assert_eq!(step, Step::D);
771        assert_eq!(alter, 0);
772    }
773
774    #[test]
775    fn key_tonic_a_minor() {
776        // A minor = relative minor of C major (fifths=0)
777        let key = KeySignature {
778            fifths: 0,
779            mode: "minor".into(),
780        };
781        let (step, alter) = key.tonic();
782        assert_eq!(step, Step::A);
783        assert_eq!(alter, 0);
784    }
785
786    #[test]
787    fn chord_display_major() {
788        let c = ChordSymbol {
789            root: "C".into(),
790            kind: "major".into(),
791            bass: None,
792            placement: None,
793            extender: false,
794            harmonic_degree: None,
795            harmony_function: None,
796            harmony_type: None,
797            chord_ref: None,
798            range_end: None,
799            degrees: Vec::new(),
800        };
801        assert_eq!(c.display_text(), "C");
802    }
803
804    #[test]
805    fn chord_display_minor_seventh_slash() {
806        let c = ChordSymbol {
807            root: "D".into(),
808            kind: "minor-seventh".into(),
809            bass: Some("F".into()),
810            placement: None,
811            extender: false,
812            harmonic_degree: None,
813            harmony_function: None,
814            harmony_type: None,
815            chord_ref: None,
816            range_end: None,
817            degrees: Vec::new(),
818        };
819        assert_eq!(c.display_text(), "Dm7/F");
820    }
821
822    #[test]
823    fn chord_display_structured_degrees() {
824        let c = ChordSymbol {
825            root: "C".into(),
826            kind: "dominant".into(),
827            bass: None,
828            placement: None,
829            extender: false,
830            harmonic_degree: None,
831            harmony_function: None,
832            harmony_type: None,
833            chord_ref: None,
834            range_end: None,
835            degrees: vec![
836                ChordDegree {
837                    value: 9,
838                    alter: 1,
839                    kind: "add".into(),
840                },
841                ChordDegree {
842                    value: 5,
843                    alter: -1,
844                    kind: "alter".into(),
845                },
846                ChordDegree {
847                    value: 3,
848                    alter: 0,
849                    kind: "subtract".into(),
850                },
851            ],
852        };
853        assert_eq!(c.display_text(), "C7add#9b5no3");
854    }
855
856    #[test]
857    fn chord_symbol_legacy_json_defaults_degrees() {
858        let chord: ChordSymbol =
859            serde_json::from_str(r#"{"root":"C","kind":"major","bass":null,"placement":null}"#)
860                .expect("legacy chord symbol JSON deserializes");
861        assert!(chord.degrees.is_empty());
862        assert!(!chord.extender);
863        assert!(chord.harmonic_degree.is_none());
864        assert!(chord.harmony_function.is_none());
865        assert!(chord.harmony_type.is_none());
866    }
867
868    #[test]
869    fn time_sig_total_beats_three_four() {
870        let ts = TimeSignature {
871            numerator: 3,
872            denominator: 4,
873        };
874        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
875    }
876
877    #[test]
878    fn time_sig_total_beats_six_eight() {
879        let ts = TimeSignature {
880            numerator: 6,
881            denominator: 8,
882        };
883        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
884    }
885
886    #[test]
887    fn clef_treble_middle_b4() {
888        assert_eq!(Clef::Treble.middle_line_midi(), 71);
889    }
890
891    #[test]
892    fn clef_bass_middle_d3() {
893        assert_eq!(Clef::Bass.middle_line_midi(), 50);
894    }
895
896    #[test]
897    fn clef_alto_middle_c4() {
898        assert_eq!(Clef::Alto.middle_line_midi(), 60);
899    }
900
901    #[test]
902    fn clef_tenor_middle_a3() {
903        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
904    }
905}