Skip to main content

acorde_core/model/
notation.rs

1use serde::{Deserialize, Serialize};
2
3use super::pitch::{Pitch, Step};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub enum Clef {
7    Treble,
8    Bass,
9    Alto,
10    Tenor,
11    Percussion,
12}
13
14impl Clef {
15    pub fn to_musicxml_sign(&self) -> &'static str {
16        match self {
17            Clef::Treble => "G",
18            Clef::Bass => "F",
19            Clef::Alto => "C",
20            Clef::Tenor => "C",
21            Clef::Percussion => "percussion",
22        }
23    }
24
25    pub fn musicxml_line(&self) -> u8 {
26        match self {
27            Clef::Treble => 2,
28            Clef::Bass => 4,
29            Clef::Alto => 3,
30            Clef::Tenor => 4,
31            Clef::Percussion => 2,
32        }
33    }
34
35    /// MIDI note number of the middle staff line (used for stem direction heuristics).
36    pub fn middle_line_midi(&self) -> u8 {
37        match self {
38            Clef::Treble => 71,     // B4
39            Clef::Bass => 50,       // D3
40            Clef::Alto => 60,       // C4
41            Clef::Tenor => 57,      // A3
42            Clef::Percussion => 71, // B4 (same as Treble)
43        }
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct KeySignature {
49    /// -7 (7 flats) to +7 (7 sharps), 0 = C major / A minor.
50    pub fifths: i8,
51    /// "major" or "minor"
52    pub mode: String,
53}
54
55impl Default for KeySignature {
56    fn default() -> Self {
57        Self {
58            fifths: 0,
59            mode: "major".to_string(),
60        }
61    }
62}
63
64impl KeySignature {
65    // Order of sharps added one by one as fifths increases: F C G D A E B
66    const SHARP_ORDER: [Step; 7] = [
67        Step::F,
68        Step::C,
69        Step::G,
70        Step::D,
71        Step::A,
72        Step::E,
73        Step::B,
74    ];
75    // Order of flats: B E A D G C F
76    const FLAT_ORDER: [Step; 7] = [
77        Step::B,
78        Step::E,
79        Step::A,
80        Step::D,
81        Step::G,
82        Step::C,
83        Step::F,
84    ];
85
86    /// Accidental alter (`-1`, `0`, or `+1`) applied to `step` in this key signature.
87    pub fn alter_for_step(&self, step: &Step) -> i8 {
88        if self.fifths > 0 {
89            let count = self.fifths.min(7) as usize;
90            if Self::SHARP_ORDER[..count].contains(step) {
91                1
92            } else {
93                0
94            }
95        } else if self.fifths < 0 {
96            let count = (-self.fifths).min(7) as usize;
97            if Self::FLAT_ORDER[..count].contains(step) {
98                -1
99            } else {
100                0
101            }
102        } else {
103            0
104        }
105    }
106
107    /// True if `pitch` is diatonic to this key (octave-independent, checks step + alter).
108    pub fn contains_pitch(&self, pitch: &Pitch) -> bool {
109        pitch.alter == self.alter_for_step(&pitch.step)
110    }
111
112    /// Tonic step and alter for this key.
113    ///
114    /// Examples: G major → `(Step::G, 0)`, Bb major → `(Step::B, -1)`, F# minor → `(Step::F, 1)`.
115    pub fn tonic(&self) -> (Step, i8) {
116        if self.mode == "minor" {
117            let (maj_step, maj_alter) = Self::major_tonic_from_fifths(self.fifths);
118            // Relative minor tonic = major tonic - 3 semitones.
119            let major_midi = Pitch::with_alter(maj_step, 4, maj_alter).to_midi();
120            let minor_midi = (major_midi - 3).clamp(0, 127) as u8;
121            let p = Pitch::from_midi(minor_midi, self.fifths < 0);
122            (p.step, p.alter)
123        } else {
124            Self::major_tonic_from_fifths(self.fifths)
125        }
126    }
127
128    /// Human-readable key name: `"C major"`, `"G major"`, `"F# minor"`, `"Bb major"` etc.
129    pub fn display_name(&self) -> String {
130        let (step, alter) = self.tonic();
131        let acc = match alter {
132            1 => "#",
133            -1 => "b",
134            _ => "",
135        };
136        format!("{}{} {}", step.to_char(), acc, self.mode)
137    }
138
139    fn major_tonic_from_fifths(fifths: i8) -> (Step, i8) {
140        // Index = fifths + 7 (range 0..=14).
141        // -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#
142        const TONICS: [(Step, i8); 15] = [
143            (Step::C, -1), // -7: Cb
144            (Step::G, -1), // -6: Gb
145            (Step::D, -1), // -5: Db
146            (Step::A, -1), // -4: Ab
147            (Step::E, -1), // -3: Eb
148            (Step::B, -1), // -2: Bb
149            (Step::F, 0),  // -1: F
150            (Step::C, 0),  //  0: C
151            (Step::G, 0),  // +1: G
152            (Step::D, 0),  // +2: D
153            (Step::A, 0),  // +3: A
154            (Step::E, 0),  // +4: E
155            (Step::B, 0),  // +5: B
156            (Step::F, 1),  // +6: F#
157            (Step::C, 1),  // +7: C#
158        ];
159        let idx = (fifths.clamp(-7, 7) + 7) as usize;
160        TONICS[idx].clone()
161    }
162}
163
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct TimeSignature {
166    pub numerator: u8,
167    pub denominator: u8,
168}
169
170impl Default for TimeSignature {
171    fn default() -> Self {
172        Self {
173            numerator: 4,
174            denominator: 4,
175        }
176    }
177}
178
179impl TimeSignature {
180    pub fn beats_per_measure(&self) -> f64 {
181        self.numerator as f64
182    }
183
184    pub fn beat_unit_beats(&self) -> f64 {
185        4.0 / self.denominator as f64
186    }
187
188    pub fn total_beats(&self) -> f64 {
189        self.beats_per_measure() * self.beat_unit_beats()
190    }
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub enum Dynamic {
195    Pppp,
196    Ppp,
197    Pp,
198    P,
199    Mp,
200    Mf,
201    F,
202    Ff,
203    Fff,
204    Ffff,
205    Sfz,
206    Rfz,
207    Fz,
208    Sf,
209}
210
211impl Dynamic {
212    pub fn to_musicxml_str(&self) -> &'static str {
213        match self {
214            Dynamic::Pppp => "pppp",
215            Dynamic::Ppp => "ppp",
216            Dynamic::Pp => "pp",
217            Dynamic::P => "p",
218            Dynamic::Mp => "mp",
219            Dynamic::Mf => "mf",
220            Dynamic::F => "f",
221            Dynamic::Ff => "ff",
222            Dynamic::Fff => "fff",
223            Dynamic::Ffff => "ffff",
224            Dynamic::Sfz => "sfz",
225            Dynamic::Rfz => "rfz",
226            Dynamic::Fz => "fz",
227            Dynamic::Sf => "sf",
228        }
229    }
230
231    pub fn to_velocity(&self) -> u8 {
232        match self {
233            Dynamic::Pppp => 16,
234            Dynamic::Ppp => 24,
235            Dynamic::Pp => 36,
236            Dynamic::P => 48,
237            Dynamic::Mp => 60,
238            Dynamic::Mf => 72,
239            Dynamic::F => 84,
240            Dynamic::Ff => 96,
241            Dynamic::Fff => 108,
242            Dynamic::Ffff => 120,
243            Dynamic::Sfz => 112,
244            Dynamic::Rfz => 104,
245            Dynamic::Fz => 100,
246            Dynamic::Sf => 96,
247        }
248    }
249}
250
251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
252pub enum Articulation {
253    Staccato,
254    Staccatissimo,
255    Accent,
256    Tenuto,
257    Marcato,
258    Fermata,
259    Trill,
260    Mordent,
261    InvertedMordent,
262    Turn,
263    InvertedTurn,
264    Shake,
265    Tremolo(u8),
266    BreathMark,
267    Caesura,
268}
269
270/// Guitar-specific playing technique attached to a note.
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272#[serde(rename_all = "kebab-case")]
273pub enum GuitarTechnique {
274    Bend,
275    Slide,
276    HammerOn,
277    PullOff,
278}
279
280/// Deterministic policy for selecting one candidate from an ordered fingering list.
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
282pub enum FingeringSelectionPolicy {
283    /// Preserve the source/application's first candidate.
284    #[default]
285    SourceOrder,
286    /// Select the numerically smallest candidate.
287    LowestNumber,
288    /// Select the numerically largest candidate.
289    HighestNumber,
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
293pub enum Barline {
294    #[default]
295    Normal,
296    Double,
297    Final,
298    RepeatStart,
299    RepeatEnd,
300    RepeatBoth,
301    Dashed,
302    Dotted,
303    Invisible,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
307pub enum HairpinKind {
308    Crescendo,
309    Decrescendo,
310}
311
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
313pub struct TupletInfo {
314    /// Notes in the tuplet group (e.g. 3 for triplet).
315    pub actual_notes: u8,
316    /// Normal notes displaced (e.g. 2 for triplet = 3-in-2).
317    pub normal_notes: u8,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
321pub enum BeamState {
322    #[default]
323    None,
324    Begin,
325    Continue,
326    End,
327    BeginEnd,
328    BackwardHook,
329    ForwardHook,
330}
331
332/// Ottava (octave transposition bracket).
333#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
334pub enum OttavaKind {
335    /// 8va — sounds one octave higher than written.
336    Va8,
337    /// 8vb — sounds one octave lower than written.
338    Vb8,
339    /// 15ma — sounds two octaves higher than written.
340    Ma15,
341    /// 15mb — sounds two octaves lower than written.
342    Mb15,
343}
344
345impl OttavaKind {
346    pub fn musicxml_type(&self) -> &'static str {
347        match self {
348            OttavaKind::Va8 | OttavaKind::Ma15 => "up",
349            OttavaKind::Vb8 | OttavaKind::Mb15 => "down",
350        }
351    }
352
353    pub fn musicxml_size(&self) -> u8 {
354        match self {
355            OttavaKind::Va8 | OttavaKind::Vb8 => 8,
356            OttavaKind::Ma15 | OttavaKind::Mb15 => 15,
357        }
358    }
359}
360
361/// Note head shape.
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
363pub enum NoteHead {
364    #[default]
365    Normal,
366    Diamond,  // natural harmonics
367    X,        // muted / dead note
368    Slash,    // ghost note
369    Cross,    // percussion special
370    Triangle, // tap harmonics
371}
372
373/// Lyric syllable attached to a note.
374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
375pub struct Lyric {
376    /// The syllable text.
377    pub text: String,
378    /// Syllabic position: "single" | "begin" | "middle" | "end"
379    pub syllabic: String,
380}
381
382/// A typed score text annotation. The text itself is kept separate from its
383/// presentation role so consumers do not need to infer semantics from prose.
384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385pub struct StyledText {
386    pub style: TextStyle,
387    pub text: String,
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
391pub enum TextStyle {
392    Expression,
393    Technique,
394    Lyrics,
395    ChordSymbol,
396    FiguredBass,
397    RehearsalMark,
398    Generic,
399}
400
401/// Cross-staff placement metadata. The note remains in its source staff, so
402/// its stable playback address continues to identify the original note.
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct CrossStaff {
405    pub target_staff: usize,
406    #[serde(default)]
407    pub target_voice: Option<usize>,
408}
409
410/// A tablature string/fret position attached to a pitched note.
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412pub struct TabPosition {
413    /// One-based string number, matching MusicXML `<string>`.
414    pub string: u8,
415    pub fret: u8,
416}
417
418/// Tablature staff metadata. Tuning MIDI values are ordered by string number.
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420pub struct TablatureConfig {
421    pub lines: u8,
422    pub tuning_midi: Vec<i16>,
423    #[serde(default)]
424    pub capo: u8,
425}
426
427/// Structured chord symbol attached to a note.
428#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
429pub struct ChordSymbol {
430    /// Root note name: "C", "F#", "Bb", etc.
431    pub root: String,
432    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
433    pub kind: String,
434    /// Slash-chord bass note.
435    pub bass: Option<String>,
436    /// Optional vertical placement hint from interchange formats (for example `above`/`below`).
437    #[serde(default)]
438    pub placement: Option<String>,
439    /// Whether the source harmony carries a continuation/extender line.
440    #[serde(default)]
441    pub extender: bool,
442    /// MEI harmonic-analysis scale degree (Humdrum **deg syntax), when supplied.
443    #[serde(default)]
444    pub harmonic_degree: Option<String>,
445    /// MEI harmonic function token (for example `T`, `PD`, or `D`), when supplied.
446    #[serde(default)]
447    pub harmony_function: Option<String>,
448    /// MEI `harm@type` classification token(s), when supplied.
449    #[serde(default)]
450    pub harmony_type: Option<String>,
451    /// MEI `harm@chordref` URI, retained without resolving an external chord definition.
452    #[serde(default)]
453    pub chord_ref: Option<String>,
454    /// Structured chord extensions such as add9, alter5, or omit3.
455    #[serde(default)]
456    pub degrees: Vec<ChordDegree>,
457}
458
459/// A reusable MEI chord/tablature definition referenced by `harm@chordref`.
460///
461/// The fields intentionally retain the source spelling for deprecated MEI tuning
462/// attributes.  Consumers may interpret the member positions without requiring
463/// the canonical score to invent an instrument catalog.
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
465pub struct ChordDefinition {
466    #[serde(default)]
467    pub id: Option<String>,
468    #[serde(default)]
469    pub label: Option<String>,
470    #[serde(default)]
471    pub kind: Option<String>,
472    #[serde(default)]
473    pub fret_position: Option<u32>,
474    #[serde(default)]
475    pub tab_strings: Option<String>,
476    #[serde(default)]
477    pub tab_courses: Option<String>,
478    #[serde(default)]
479    pub members: Vec<ChordDefinitionMember>,
480    #[serde(default)]
481    pub barres: Vec<ChordBarre>,
482}
483
484/// One pitch and/or tablature position in a [`ChordDefinition`].
485#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
486pub struct ChordDefinitionMember {
487    #[serde(default)]
488    pub id: Option<String>,
489    #[serde(default)]
490    pub pitch: Option<Pitch>,
491    #[serde(default)]
492    pub tab_string: Option<u8>,
493    #[serde(default)]
494    pub tab_course: Option<u8>,
495    #[serde(default)]
496    pub tab_fret: Option<u16>,
497    #[serde(default)]
498    pub fingering: Option<u8>,
499}
500
501/// A barre range inside a [`ChordDefinition`] fretboard diagram.
502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
503pub struct ChordBarre {
504    #[serde(default)]
505    pub start_member: Option<String>,
506    #[serde(default)]
507    pub end_member: Option<String>,
508    #[serde(default)]
509    pub fret: Option<u16>,
510    #[serde(default)]
511    pub label: Option<String>,
512    #[serde(default)]
513    pub kind: Option<String>,
514}
515
516/// One structured MusicXML chord degree.
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
518pub struct ChordDegree {
519    /// Scale degree number (normally 1 through 13).
520    pub value: u8,
521    /// Semitone alteration relative to the diatonic degree.
522    pub alter: i8,
523    /// MusicXML degree type, such as `add`, `alter`, or `subtract`.
524    pub kind: String,
525}
526
527/// One structured figure in a figured-bass annotation.
528#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
529pub struct FiguredBassFigure {
530    /// Source figure number, retained as text because MusicXML permits non-numeric figures.
531    pub number: String,
532    /// Raw source alteration value, such as `-1`, `0`, or `1`.
533    #[serde(default)]
534    pub alter: Option<String>,
535    /// Optional source prefix decoration.
536    #[serde(default)]
537    pub prefix: Option<String>,
538    /// Optional source suffix decoration.
539    #[serde(default)]
540    pub suffix: Option<String>,
541    /// Whether MEI marks this figure with a horizontal extender.
542    #[serde(default)]
543    pub extender: bool,
544}
545
546impl ChordDegree {
547    /// Render the degree using the compact chord-label convention.
548    pub fn display_text(&self) -> String {
549        let accidental = match self.alter {
550            -2 => "bb",
551            -1 => "b",
552            1 => "#",
553            2 => "##",
554            _ => "",
555        };
556        match self.kind.as_str() {
557            "subtract" => format!("no{}{}", accidental, self.value),
558            "alter" => format!("{}{}", accidental, self.value),
559            _ => format!("add{}{}", accidental, self.value),
560        }
561    }
562}
563
564impl ChordSymbol {
565    pub fn display_text(&self) -> String {
566        let kind_str = match self.kind.as_str() {
567            "major" | "" => "",
568            "minor" => "m",
569            "dominant" => "7",
570            "major-seventh" => "maj7",
571            "minor-seventh" => "m7",
572            "diminished" => "dim",
573            "diminished-seventh" => "dim7",
574            "augmented" => "aug",
575            "suspended-second" => "sus2",
576            "suspended-fourth" => "sus4",
577            "half-diminished" => "m7b5",
578            "major-sixth" => "6",
579            "minor-sixth" => "m6",
580            other => other,
581        };
582        let bass_str = match &self.bass {
583            Some(b) => format!("/{}", b),
584            None => String::new(),
585        };
586        let degree_str = self
587            .degrees
588            .iter()
589            .map(ChordDegree::display_text)
590            .collect::<String>();
591        format!("{}{}{}{}", self.root, kind_str, degree_str, bass_str)
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    #[test]
600    fn key_alter_c_major_all_natural() {
601        let key = KeySignature {
602            fifths: 0,
603            mode: "major".into(),
604        };
605        for step in [
606            Step::C,
607            Step::D,
608            Step::E,
609            Step::F,
610            Step::G,
611            Step::A,
612            Step::B,
613        ] {
614            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
615        }
616    }
617
618    #[test]
619    fn key_alter_g_major_fsharp() {
620        let key = KeySignature {
621            fifths: 1,
622            mode: "major".into(),
623        };
624        assert_eq!(key.alter_for_step(&Step::F), 1);
625        assert_eq!(key.alter_for_step(&Step::G), 0);
626    }
627
628    #[test]
629    fn key_alter_f_major_bflat() {
630        let key = KeySignature {
631            fifths: -1,
632            mode: "major".into(),
633        };
634        assert_eq!(key.alter_for_step(&Step::B), -1);
635        assert_eq!(key.alter_for_step(&Step::C), 0);
636    }
637
638    #[test]
639    fn key_alter_bb_major() {
640        let key = KeySignature {
641            fifths: -2,
642            mode: "major".into(),
643        };
644        assert_eq!(key.alter_for_step(&Step::B), -1);
645        assert_eq!(key.alter_for_step(&Step::E), -1);
646        assert_eq!(key.alter_for_step(&Step::A), 0);
647    }
648
649    #[test]
650    fn key_contains_pitch_g_major() {
651        let key = KeySignature {
652            fifths: 1,
653            mode: "major".into(),
654        };
655        // In-key: G, A, B, C, D, E, F# (alter=1)
656        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
657        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
658        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
659        // Out-of-key: F natural
660        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
661    }
662
663    #[test]
664    fn key_display_name_c_major() {
665        let key = KeySignature {
666            fifths: 0,
667            mode: "major".into(),
668        };
669        assert_eq!(key.display_name(), "C major");
670    }
671
672    #[test]
673    fn key_display_name_bb_major() {
674        let key = KeySignature {
675            fifths: -2,
676            mode: "major".into(),
677        };
678        assert_eq!(key.display_name(), "Bb major");
679    }
680
681    #[test]
682    fn key_display_name_fsharp_minor() {
683        let key = KeySignature {
684            fifths: 3,
685            mode: "minor".into(),
686        };
687        assert_eq!(key.display_name(), "F# minor");
688    }
689
690    #[test]
691    fn key_tonic_d_major() {
692        let key = KeySignature {
693            fifths: 2,
694            mode: "major".into(),
695        };
696        let (step, alter) = key.tonic();
697        assert_eq!(step, Step::D);
698        assert_eq!(alter, 0);
699    }
700
701    #[test]
702    fn key_tonic_a_minor() {
703        // A minor = relative minor of C major (fifths=0)
704        let key = KeySignature {
705            fifths: 0,
706            mode: "minor".into(),
707        };
708        let (step, alter) = key.tonic();
709        assert_eq!(step, Step::A);
710        assert_eq!(alter, 0);
711    }
712
713    #[test]
714    fn chord_display_major() {
715        let c = ChordSymbol {
716            root: "C".into(),
717            kind: "major".into(),
718            bass: None,
719            placement: None,
720            extender: false,
721            harmonic_degree: None,
722            harmony_function: None,
723            harmony_type: None,
724            chord_ref: None,
725            degrees: Vec::new(),
726        };
727        assert_eq!(c.display_text(), "C");
728    }
729
730    #[test]
731    fn chord_display_minor_seventh_slash() {
732        let c = ChordSymbol {
733            root: "D".into(),
734            kind: "minor-seventh".into(),
735            bass: Some("F".into()),
736            placement: None,
737            extender: false,
738            harmonic_degree: None,
739            harmony_function: None,
740            harmony_type: None,
741            chord_ref: None,
742            degrees: Vec::new(),
743        };
744        assert_eq!(c.display_text(), "Dm7/F");
745    }
746
747    #[test]
748    fn chord_display_structured_degrees() {
749        let c = ChordSymbol {
750            root: "C".into(),
751            kind: "dominant".into(),
752            bass: None,
753            placement: None,
754            extender: false,
755            harmonic_degree: None,
756            harmony_function: None,
757            harmony_type: None,
758            chord_ref: None,
759            degrees: vec![
760                ChordDegree {
761                    value: 9,
762                    alter: 1,
763                    kind: "add".into(),
764                },
765                ChordDegree {
766                    value: 5,
767                    alter: -1,
768                    kind: "alter".into(),
769                },
770                ChordDegree {
771                    value: 3,
772                    alter: 0,
773                    kind: "subtract".into(),
774                },
775            ],
776        };
777        assert_eq!(c.display_text(), "C7add#9b5no3");
778    }
779
780    #[test]
781    fn chord_symbol_legacy_json_defaults_degrees() {
782        let chord: ChordSymbol =
783            serde_json::from_str(r#"{"root":"C","kind":"major","bass":null,"placement":null}"#)
784                .expect("legacy chord symbol JSON deserializes");
785        assert!(chord.degrees.is_empty());
786        assert!(!chord.extender);
787        assert!(chord.harmonic_degree.is_none());
788        assert!(chord.harmony_function.is_none());
789        assert!(chord.harmony_type.is_none());
790    }
791
792    #[test]
793    fn time_sig_total_beats_three_four() {
794        let ts = TimeSignature {
795            numerator: 3,
796            denominator: 4,
797        };
798        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
799    }
800
801    #[test]
802    fn time_sig_total_beats_six_eight() {
803        let ts = TimeSignature {
804            numerator: 6,
805            denominator: 8,
806        };
807        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
808    }
809
810    #[test]
811    fn clef_treble_middle_b4() {
812        assert_eq!(Clef::Treble.middle_line_midi(), 71);
813    }
814
815    #[test]
816    fn clef_bass_middle_d3() {
817        assert_eq!(Clef::Bass.middle_line_midi(), 50);
818    }
819
820    #[test]
821    fn clef_alto_middle_c4() {
822        assert_eq!(Clef::Alto.middle_line_midi(), 60);
823    }
824
825    #[test]
826    fn clef_tenor_middle_a3() {
827        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
828    }
829}