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 pub fn middle_line_midi(&self) -> u8 {
38 match self {
39 Clef::Treble => 71, Clef::Bass => 50, Clef::Alto => 60, Clef::Tenor => 57, Clef::Percussion => 71, }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct KeySignature {
50 pub fifths: i8,
52 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 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 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 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 pub fn contains_pitch(&self, pitch: &Pitch) -> bool {
110 pitch.alter == self.alter_for_step(&pitch.step)
111 }
112
113 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 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 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 const TONICS: [(Step, i8); 15] = [
144 (Step::C, -1), (Step::G, -1), (Step::D, -1), (Step::A, -1), (Step::E, -1), (Step::B, -1), (Step::F, 0), (Step::C, 0), (Step::G, 0), (Step::D, 0), (Step::A, 0), (Step::E, 0), (Step::B, 0), (Step::F, 1), (Step::C, 1), ];
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 Fp,
212 Sfp,
214 Sfpp,
216 Pf,
218 Sffz,
220 Sfzp,
222 N,
224}
225
226impl Dynamic {
227 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 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 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 UpBow,
349 DownBow,
351 Harmonic,
353 OpenString,
355 Stopped,
357 SnapPizzicato,
359}
360
361impl Articulation {
362 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
389pub enum FingeringSelectionPolicy {
390 #[default]
392 SourceOrder,
393 LowestNumber,
395 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 pub actual_notes: u8,
423 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
441pub enum OttavaKind {
442 Va8,
444 Vb8,
446 Ma15,
448 Mb15,
450}
451
452impl OttavaKind {
453 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
482pub enum NoteHead {
483 #[default]
484 Normal,
485 Diamond, X, Slash, Cross, Triangle, }
491
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
494pub struct Lyric {
495 pub text: String,
497 pub syllabic: String,
499 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
502 pub extend: bool,
503}
504
505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
507pub struct VerseLyric {
508 pub verse: u8,
510 pub lyric: Lyric,
511}
512
513impl VerseLyric {
514 pub const MAX_VERSE: u8 = 32;
516}
517
518#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
521pub struct StyledText {
522 pub style: TextStyle,
523 pub text: String,
524 #[serde(default)]
526 pub placement: Option<String>,
527 #[serde(default)]
529 pub offset_x: Option<f64>,
530 #[serde(default)]
532 pub offset_y: Option<f64>,
533 #[serde(default)]
535 pub relative_x: Option<f64>,
536 #[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
563pub struct TabPosition {
564 pub string: u8,
566 pub fret: u8,
567}
568
569#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
580pub struct ChordSymbol {
581 pub root: String,
583 pub kind: String,
585 pub bass: Option<String>,
587 #[serde(default)]
589 pub placement: Option<String>,
590 #[serde(default)]
592 pub extender: bool,
593 #[serde(default)]
595 pub harmonic_degree: Option<String>,
596 #[serde(default)]
598 pub harmony_function: Option<String>,
599 #[serde(default)]
601 pub harmony_type: Option<String>,
602 #[serde(default)]
604 pub chord_ref: Option<String>,
605 #[serde(default)]
611 pub range_end: Option<NoteAddr>,
612 #[serde(default)]
614 pub degrees: Vec<ChordDegree>,
615}
616
617#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676pub struct ChordDegree {
677 pub value: u8,
679 pub alter: i8,
681 pub kind: String,
683}
684
685#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
687pub struct FiguredBassFigure {
688 pub number: String,
690 #[serde(default)]
692 pub alter: Option<String>,
693 #[serde(default)]
695 pub prefix: Option<String>,
696 #[serde(default)]
698 pub suffix: Option<String>,
699 #[serde(default)]
701 pub extender: bool,
702}
703
704impl ChordDegree {
705 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
722pub 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 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 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))); 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 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}