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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
281pub enum Barline {
282    #[default]
283    Normal,
284    Double,
285    Final,
286    RepeatStart,
287    RepeatEnd,
288    RepeatBoth,
289    Dashed,
290    Dotted,
291    Invisible,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
295pub enum HairpinKind {
296    Crescendo,
297    Decrescendo,
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301pub struct TupletInfo {
302    /// Notes in the tuplet group (e.g. 3 for triplet).
303    pub actual_notes: u8,
304    /// Normal notes displaced (e.g. 2 for triplet = 3-in-2).
305    pub normal_notes: u8,
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
309pub enum BeamState {
310    #[default]
311    None,
312    Begin,
313    Continue,
314    End,
315    BeginEnd,
316    BackwardHook,
317    ForwardHook,
318}
319
320/// Ottava (octave transposition bracket).
321#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
322pub enum OttavaKind {
323    /// 8va — sounds one octave higher than written.
324    Va8,
325    /// 8vb — sounds one octave lower than written.
326    Vb8,
327    /// 15ma — sounds two octaves higher than written.
328    Ma15,
329    /// 15mb — sounds two octaves lower than written.
330    Mb15,
331}
332
333impl OttavaKind {
334    pub fn musicxml_type(&self) -> &'static str {
335        match self {
336            OttavaKind::Va8 | OttavaKind::Ma15 => "up",
337            OttavaKind::Vb8 | OttavaKind::Mb15 => "down",
338        }
339    }
340
341    pub fn musicxml_size(&self) -> u8 {
342        match self {
343            OttavaKind::Va8 | OttavaKind::Vb8 => 8,
344            OttavaKind::Ma15 | OttavaKind::Mb15 => 15,
345        }
346    }
347}
348
349/// Note head shape.
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
351pub enum NoteHead {
352    #[default]
353    Normal,
354    Diamond,  // natural harmonics
355    X,        // muted / dead note
356    Slash,    // ghost note
357    Cross,    // percussion special
358    Triangle, // tap harmonics
359}
360
361/// Lyric syllable attached to a note.
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363pub struct Lyric {
364    /// The syllable text.
365    pub text: String,
366    /// Syllabic position: "single" | "begin" | "middle" | "end"
367    pub syllabic: String,
368}
369
370/// Structured chord symbol attached to a note.
371#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
372pub struct ChordSymbol {
373    /// Root note name: "C", "F#", "Bb", etc.
374    pub root: String,
375    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
376    pub kind: String,
377    /// Slash-chord bass note.
378    pub bass: Option<String>,
379}
380
381impl ChordSymbol {
382    pub fn display_text(&self) -> String {
383        let kind_str = match self.kind.as_str() {
384            "major" | "" => "",
385            "minor" => "m",
386            "dominant" => "7",
387            "major-seventh" => "maj7",
388            "minor-seventh" => "m7",
389            "diminished" => "dim",
390            "diminished-seventh" => "dim7",
391            "augmented" => "aug",
392            "suspended-second" => "sus2",
393            "suspended-fourth" => "sus4",
394            "half-diminished" => "m7b5",
395            "major-sixth" => "6",
396            "minor-sixth" => "m6",
397            other => other,
398        };
399        let bass_str = match &self.bass {
400            Some(b) => format!("/{}", b),
401            None => String::new(),
402        };
403        format!("{}{}{}", self.root, kind_str, bass_str)
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn key_alter_c_major_all_natural() {
413        let key = KeySignature {
414            fifths: 0,
415            mode: "major".into(),
416        };
417        for step in [
418            Step::C,
419            Step::D,
420            Step::E,
421            Step::F,
422            Step::G,
423            Step::A,
424            Step::B,
425        ] {
426            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
427        }
428    }
429
430    #[test]
431    fn key_alter_g_major_fsharp() {
432        let key = KeySignature {
433            fifths: 1,
434            mode: "major".into(),
435        };
436        assert_eq!(key.alter_for_step(&Step::F), 1);
437        assert_eq!(key.alter_for_step(&Step::G), 0);
438    }
439
440    #[test]
441    fn key_alter_f_major_bflat() {
442        let key = KeySignature {
443            fifths: -1,
444            mode: "major".into(),
445        };
446        assert_eq!(key.alter_for_step(&Step::B), -1);
447        assert_eq!(key.alter_for_step(&Step::C), 0);
448    }
449
450    #[test]
451    fn key_alter_bb_major() {
452        let key = KeySignature {
453            fifths: -2,
454            mode: "major".into(),
455        };
456        assert_eq!(key.alter_for_step(&Step::B), -1);
457        assert_eq!(key.alter_for_step(&Step::E), -1);
458        assert_eq!(key.alter_for_step(&Step::A), 0);
459    }
460
461    #[test]
462    fn key_contains_pitch_g_major() {
463        let key = KeySignature {
464            fifths: 1,
465            mode: "major".into(),
466        };
467        // In-key: G, A, B, C, D, E, F# (alter=1)
468        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
469        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
470        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
471        // Out-of-key: F natural
472        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
473    }
474
475    #[test]
476    fn key_display_name_c_major() {
477        let key = KeySignature {
478            fifths: 0,
479            mode: "major".into(),
480        };
481        assert_eq!(key.display_name(), "C major");
482    }
483
484    #[test]
485    fn key_display_name_bb_major() {
486        let key = KeySignature {
487            fifths: -2,
488            mode: "major".into(),
489        };
490        assert_eq!(key.display_name(), "Bb major");
491    }
492
493    #[test]
494    fn key_display_name_fsharp_minor() {
495        let key = KeySignature {
496            fifths: 3,
497            mode: "minor".into(),
498        };
499        assert_eq!(key.display_name(), "F# minor");
500    }
501
502    #[test]
503    fn key_tonic_d_major() {
504        let key = KeySignature {
505            fifths: 2,
506            mode: "major".into(),
507        };
508        let (step, alter) = key.tonic();
509        assert_eq!(step, Step::D);
510        assert_eq!(alter, 0);
511    }
512
513    #[test]
514    fn key_tonic_a_minor() {
515        // A minor = relative minor of C major (fifths=0)
516        let key = KeySignature {
517            fifths: 0,
518            mode: "minor".into(),
519        };
520        let (step, alter) = key.tonic();
521        assert_eq!(step, Step::A);
522        assert_eq!(alter, 0);
523    }
524
525    #[test]
526    fn chord_display_major() {
527        let c = ChordSymbol {
528            root: "C".into(),
529            kind: "major".into(),
530            bass: None,
531        };
532        assert_eq!(c.display_text(), "C");
533    }
534
535    #[test]
536    fn chord_display_minor_seventh_slash() {
537        let c = ChordSymbol {
538            root: "D".into(),
539            kind: "minor-seventh".into(),
540            bass: Some("F".into()),
541        };
542        assert_eq!(c.display_text(), "Dm7/F");
543    }
544
545    #[test]
546    fn time_sig_total_beats_three_four() {
547        let ts = TimeSignature {
548            numerator: 3,
549            denominator: 4,
550        };
551        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
552    }
553
554    #[test]
555    fn time_sig_total_beats_six_eight() {
556        let ts = TimeSignature {
557            numerator: 6,
558            denominator: 8,
559        };
560        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
561    }
562
563    #[test]
564    fn clef_treble_middle_b4() {
565        assert_eq!(Clef::Treble.middle_line_midi(), 71);
566    }
567
568    #[test]
569    fn clef_bass_middle_d3() {
570        assert_eq!(Clef::Bass.middle_line_midi(), 50);
571    }
572
573    #[test]
574    fn clef_alto_middle_c4() {
575        assert_eq!(Clef::Alto.middle_line_midi(), 60);
576    }
577
578    #[test]
579    fn clef_tenor_middle_a3() {
580        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
581    }
582}