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    /// Optional vertical placement hint from interchange formats.
389    #[serde(default)]
390    pub placement: Option<String>,
391    /// MusicXML default horizontal offset in tenths, when attached to a direction text.
392    #[serde(default)]
393    pub offset_x: Option<f64>,
394    /// MusicXML default vertical offset in tenths, when attached to a direction text.
395    #[serde(default)]
396    pub offset_y: Option<f64>,
397    /// MusicXML relative horizontal offset in tenths, retaining its relative-coordinate meaning.
398    #[serde(default)]
399    pub relative_x: Option<f64>,
400    /// MusicXML relative vertical offset in tenths, retaining its relative-coordinate meaning.
401    #[serde(default)]
402    pub relative_y: Option<f64>,
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
406pub enum TextStyle {
407    Expression,
408    Technique,
409    Lyrics,
410    ChordSymbol,
411    FiguredBass,
412    RehearsalMark,
413    Generic,
414}
415
416/// Cross-staff placement metadata. The note remains in its source staff, so
417/// its stable playback address continues to identify the original note.
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct CrossStaff {
420    pub target_staff: usize,
421    #[serde(default)]
422    pub target_voice: Option<usize>,
423}
424
425/// A tablature string/fret position attached to a pitched note.
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct TabPosition {
428    /// One-based string number, matching MusicXML `<string>`.
429    pub string: u8,
430    pub fret: u8,
431}
432
433/// Tablature staff metadata. Tuning MIDI values are ordered by string number.
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct TablatureConfig {
436    pub lines: u8,
437    pub tuning_midi: Vec<i16>,
438    #[serde(default)]
439    pub capo: u8,
440}
441
442/// Structured chord symbol attached to a note.
443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
444pub struct ChordSymbol {
445    /// Root note name: "C", "F#", "Bb", etc.
446    pub root: String,
447    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
448    pub kind: String,
449    /// Slash-chord bass note.
450    pub bass: Option<String>,
451    /// Optional vertical placement hint from interchange formats (for example `above`/`below`).
452    #[serde(default)]
453    pub placement: Option<String>,
454    /// Whether the source harmony carries a continuation/extender line.
455    #[serde(default)]
456    pub extender: bool,
457    /// MEI harmonic-analysis scale degree (Humdrum **deg syntax), when supplied.
458    #[serde(default)]
459    pub harmonic_degree: Option<String>,
460    /// MEI harmonic function token (for example `T`, `PD`, or `D`), when supplied.
461    #[serde(default)]
462    pub harmony_function: Option<String>,
463    /// MEI `harm@type` classification token(s), when supplied.
464    #[serde(default)]
465    pub harmony_type: Option<String>,
466    /// MEI `harm@chordref` URI, retained without resolving an external chord definition.
467    #[serde(default)]
468    pub chord_ref: Option<String>,
469    /// Structured chord extensions such as add9, alter5, or omit3.
470    #[serde(default)]
471    pub degrees: Vec<ChordDegree>,
472}
473
474/// A reusable MEI chord/tablature definition referenced by `harm@chordref`.
475///
476/// The fields intentionally retain the source spelling for deprecated MEI tuning
477/// attributes.  Consumers may interpret the member positions without requiring
478/// the canonical score to invent an instrument catalog.
479#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
480pub struct ChordDefinition {
481    #[serde(default)]
482    pub id: Option<String>,
483    #[serde(default)]
484    pub label: Option<String>,
485    #[serde(default)]
486    pub kind: Option<String>,
487    #[serde(default)]
488    pub fret_position: Option<u32>,
489    #[serde(default)]
490    pub tab_strings: Option<String>,
491    #[serde(default)]
492    pub tab_courses: Option<String>,
493    #[serde(default)]
494    pub members: Vec<ChordDefinitionMember>,
495    #[serde(default)]
496    pub barres: Vec<ChordBarre>,
497}
498
499/// One pitch and/or tablature position in a [`ChordDefinition`].
500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
501pub struct ChordDefinitionMember {
502    #[serde(default)]
503    pub id: Option<String>,
504    #[serde(default)]
505    pub pitch: Option<Pitch>,
506    #[serde(default)]
507    pub tab_string: Option<u8>,
508    #[serde(default)]
509    pub tab_course: Option<u8>,
510    #[serde(default)]
511    pub tab_fret: Option<u16>,
512    #[serde(default)]
513    pub fingering: Option<u8>,
514}
515
516/// A barre range inside a [`ChordDefinition`] fretboard diagram.
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
518pub struct ChordBarre {
519    #[serde(default)]
520    pub start_member: Option<String>,
521    #[serde(default)]
522    pub end_member: Option<String>,
523    #[serde(default)]
524    pub fret: Option<u16>,
525    #[serde(default)]
526    pub label: Option<String>,
527    #[serde(default)]
528    pub kind: Option<String>,
529}
530
531/// One structured MusicXML chord degree.
532#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
533pub struct ChordDegree {
534    /// Scale degree number (normally 1 through 13).
535    pub value: u8,
536    /// Semitone alteration relative to the diatonic degree.
537    pub alter: i8,
538    /// MusicXML degree type, such as `add`, `alter`, or `subtract`.
539    pub kind: String,
540}
541
542/// One structured figure in a figured-bass annotation.
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544pub struct FiguredBassFigure {
545    /// Source figure number, retained as text because MusicXML permits non-numeric figures.
546    pub number: String,
547    /// Raw source alteration value, such as `-1`, `0`, or `1`.
548    #[serde(default)]
549    pub alter: Option<String>,
550    /// Optional source prefix decoration.
551    #[serde(default)]
552    pub prefix: Option<String>,
553    /// Optional source suffix decoration.
554    #[serde(default)]
555    pub suffix: Option<String>,
556    /// Whether MEI marks this figure with a horizontal extender.
557    #[serde(default)]
558    pub extender: bool,
559}
560
561impl ChordDegree {
562    /// Render the degree using the compact chord-label convention.
563    pub fn display_text(&self) -> String {
564        let accidental = match self.alter {
565            -2 => "bb",
566            -1 => "b",
567            1 => "#",
568            2 => "##",
569            _ => "",
570        };
571        match self.kind.as_str() {
572            "subtract" => format!("no{}{}", accidental, self.value),
573            "alter" => format!("{}{}", accidental, self.value),
574            _ => format!("add{}{}", accidental, self.value),
575        }
576    }
577}
578
579impl ChordSymbol {
580    pub fn display_text(&self) -> String {
581        let kind_str = match self.kind.as_str() {
582            "major" | "" => "",
583            "minor" => "m",
584            "dominant" => "7",
585            "major-seventh" => "maj7",
586            "minor-seventh" => "m7",
587            "diminished" => "dim",
588            "diminished-seventh" => "dim7",
589            "augmented" => "aug",
590            "suspended-second" => "sus2",
591            "suspended-fourth" => "sus4",
592            "half-diminished" => "m7b5",
593            "major-sixth" => "6",
594            "minor-sixth" => "m6",
595            other => other,
596        };
597        let bass_str = match &self.bass {
598            Some(b) => format!("/{}", b),
599            None => String::new(),
600        };
601        let degree_str = self
602            .degrees
603            .iter()
604            .map(ChordDegree::display_text)
605            .collect::<String>();
606        format!("{}{}{}{}", self.root, kind_str, degree_str, bass_str)
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn key_alter_c_major_all_natural() {
616        let key = KeySignature {
617            fifths: 0,
618            mode: "major".into(),
619        };
620        for step in [
621            Step::C,
622            Step::D,
623            Step::E,
624            Step::F,
625            Step::G,
626            Step::A,
627            Step::B,
628        ] {
629            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
630        }
631    }
632
633    #[test]
634    fn key_alter_g_major_fsharp() {
635        let key = KeySignature {
636            fifths: 1,
637            mode: "major".into(),
638        };
639        assert_eq!(key.alter_for_step(&Step::F), 1);
640        assert_eq!(key.alter_for_step(&Step::G), 0);
641    }
642
643    #[test]
644    fn key_alter_f_major_bflat() {
645        let key = KeySignature {
646            fifths: -1,
647            mode: "major".into(),
648        };
649        assert_eq!(key.alter_for_step(&Step::B), -1);
650        assert_eq!(key.alter_for_step(&Step::C), 0);
651    }
652
653    #[test]
654    fn key_alter_bb_major() {
655        let key = KeySignature {
656            fifths: -2,
657            mode: "major".into(),
658        };
659        assert_eq!(key.alter_for_step(&Step::B), -1);
660        assert_eq!(key.alter_for_step(&Step::E), -1);
661        assert_eq!(key.alter_for_step(&Step::A), 0);
662    }
663
664    #[test]
665    fn key_contains_pitch_g_major() {
666        let key = KeySignature {
667            fifths: 1,
668            mode: "major".into(),
669        };
670        // In-key: G, A, B, C, D, E, F# (alter=1)
671        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
672        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
673        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
674        // Out-of-key: F natural
675        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
676    }
677
678    #[test]
679    fn key_display_name_c_major() {
680        let key = KeySignature {
681            fifths: 0,
682            mode: "major".into(),
683        };
684        assert_eq!(key.display_name(), "C major");
685    }
686
687    #[test]
688    fn key_display_name_bb_major() {
689        let key = KeySignature {
690            fifths: -2,
691            mode: "major".into(),
692        };
693        assert_eq!(key.display_name(), "Bb major");
694    }
695
696    #[test]
697    fn key_display_name_fsharp_minor() {
698        let key = KeySignature {
699            fifths: 3,
700            mode: "minor".into(),
701        };
702        assert_eq!(key.display_name(), "F# minor");
703    }
704
705    #[test]
706    fn key_tonic_d_major() {
707        let key = KeySignature {
708            fifths: 2,
709            mode: "major".into(),
710        };
711        let (step, alter) = key.tonic();
712        assert_eq!(step, Step::D);
713        assert_eq!(alter, 0);
714    }
715
716    #[test]
717    fn key_tonic_a_minor() {
718        // A minor = relative minor of C major (fifths=0)
719        let key = KeySignature {
720            fifths: 0,
721            mode: "minor".into(),
722        };
723        let (step, alter) = key.tonic();
724        assert_eq!(step, Step::A);
725        assert_eq!(alter, 0);
726    }
727
728    #[test]
729    fn chord_display_major() {
730        let c = ChordSymbol {
731            root: "C".into(),
732            kind: "major".into(),
733            bass: None,
734            placement: None,
735            extender: false,
736            harmonic_degree: None,
737            harmony_function: None,
738            harmony_type: None,
739            chord_ref: None,
740            degrees: Vec::new(),
741        };
742        assert_eq!(c.display_text(), "C");
743    }
744
745    #[test]
746    fn chord_display_minor_seventh_slash() {
747        let c = ChordSymbol {
748            root: "D".into(),
749            kind: "minor-seventh".into(),
750            bass: Some("F".into()),
751            placement: None,
752            extender: false,
753            harmonic_degree: None,
754            harmony_function: None,
755            harmony_type: None,
756            chord_ref: None,
757            degrees: Vec::new(),
758        };
759        assert_eq!(c.display_text(), "Dm7/F");
760    }
761
762    #[test]
763    fn chord_display_structured_degrees() {
764        let c = ChordSymbol {
765            root: "C".into(),
766            kind: "dominant".into(),
767            bass: None,
768            placement: None,
769            extender: false,
770            harmonic_degree: None,
771            harmony_function: None,
772            harmony_type: None,
773            chord_ref: None,
774            degrees: vec![
775                ChordDegree {
776                    value: 9,
777                    alter: 1,
778                    kind: "add".into(),
779                },
780                ChordDegree {
781                    value: 5,
782                    alter: -1,
783                    kind: "alter".into(),
784                },
785                ChordDegree {
786                    value: 3,
787                    alter: 0,
788                    kind: "subtract".into(),
789                },
790            ],
791        };
792        assert_eq!(c.display_text(), "C7add#9b5no3");
793    }
794
795    #[test]
796    fn chord_symbol_legacy_json_defaults_degrees() {
797        let chord: ChordSymbol =
798            serde_json::from_str(r#"{"root":"C","kind":"major","bass":null,"placement":null}"#)
799                .expect("legacy chord symbol JSON deserializes");
800        assert!(chord.degrees.is_empty());
801        assert!(!chord.extender);
802        assert!(chord.harmonic_degree.is_none());
803        assert!(chord.harmony_function.is_none());
804        assert!(chord.harmony_type.is_none());
805    }
806
807    #[test]
808    fn time_sig_total_beats_three_four() {
809        let ts = TimeSignature {
810            numerator: 3,
811            denominator: 4,
812        };
813        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
814    }
815
816    #[test]
817    fn time_sig_total_beats_six_eight() {
818        let ts = TimeSignature {
819            numerator: 6,
820            denominator: 8,
821        };
822        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
823    }
824
825    #[test]
826    fn clef_treble_middle_b4() {
827        assert_eq!(Clef::Treble.middle_line_midi(), 71);
828    }
829
830    #[test]
831    fn clef_bass_middle_d3() {
832        assert_eq!(Clef::Bass.middle_line_midi(), 50);
833    }
834
835    #[test]
836    fn clef_alto_middle_c4() {
837        assert_eq!(Clef::Alto.middle_line_midi(), 60);
838    }
839
840    #[test]
841    fn clef_tenor_middle_a3() {
842        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
843    }
844}