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/// A typed score text annotation. The text itself is kept separate from its
371/// presentation role so consumers do not need to infer semantics from prose.
372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
373pub struct StyledText {
374    pub style: TextStyle,
375    pub text: String,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
379pub enum TextStyle {
380    Expression,
381    Technique,
382    Lyrics,
383    ChordSymbol,
384    RehearsalMark,
385    Generic,
386}
387
388/// Cross-staff placement metadata. The note remains in its source staff, so
389/// its stable playback address continues to identify the original note.
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391pub struct CrossStaff {
392    pub target_staff: usize,
393    #[serde(default)]
394    pub target_voice: Option<usize>,
395}
396
397/// Structured chord symbol attached to a note.
398#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
399pub struct ChordSymbol {
400    /// Root note name: "C", "F#", "Bb", etc.
401    pub root: String,
402    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
403    pub kind: String,
404    /// Slash-chord bass note.
405    pub bass: Option<String>,
406}
407
408impl ChordSymbol {
409    pub fn display_text(&self) -> String {
410        let kind_str = match self.kind.as_str() {
411            "major" | "" => "",
412            "minor" => "m",
413            "dominant" => "7",
414            "major-seventh" => "maj7",
415            "minor-seventh" => "m7",
416            "diminished" => "dim",
417            "diminished-seventh" => "dim7",
418            "augmented" => "aug",
419            "suspended-second" => "sus2",
420            "suspended-fourth" => "sus4",
421            "half-diminished" => "m7b5",
422            "major-sixth" => "6",
423            "minor-sixth" => "m6",
424            other => other,
425        };
426        let bass_str = match &self.bass {
427            Some(b) => format!("/{}", b),
428            None => String::new(),
429        };
430        format!("{}{}{}", self.root, kind_str, bass_str)
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn key_alter_c_major_all_natural() {
440        let key = KeySignature {
441            fifths: 0,
442            mode: "major".into(),
443        };
444        for step in [
445            Step::C,
446            Step::D,
447            Step::E,
448            Step::F,
449            Step::G,
450            Step::A,
451            Step::B,
452        ] {
453            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
454        }
455    }
456
457    #[test]
458    fn key_alter_g_major_fsharp() {
459        let key = KeySignature {
460            fifths: 1,
461            mode: "major".into(),
462        };
463        assert_eq!(key.alter_for_step(&Step::F), 1);
464        assert_eq!(key.alter_for_step(&Step::G), 0);
465    }
466
467    #[test]
468    fn key_alter_f_major_bflat() {
469        let key = KeySignature {
470            fifths: -1,
471            mode: "major".into(),
472        };
473        assert_eq!(key.alter_for_step(&Step::B), -1);
474        assert_eq!(key.alter_for_step(&Step::C), 0);
475    }
476
477    #[test]
478    fn key_alter_bb_major() {
479        let key = KeySignature {
480            fifths: -2,
481            mode: "major".into(),
482        };
483        assert_eq!(key.alter_for_step(&Step::B), -1);
484        assert_eq!(key.alter_for_step(&Step::E), -1);
485        assert_eq!(key.alter_for_step(&Step::A), 0);
486    }
487
488    #[test]
489    fn key_contains_pitch_g_major() {
490        let key = KeySignature {
491            fifths: 1,
492            mode: "major".into(),
493        };
494        // In-key: G, A, B, C, D, E, F# (alter=1)
495        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
496        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
497        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
498        // Out-of-key: F natural
499        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
500    }
501
502    #[test]
503    fn key_display_name_c_major() {
504        let key = KeySignature {
505            fifths: 0,
506            mode: "major".into(),
507        };
508        assert_eq!(key.display_name(), "C major");
509    }
510
511    #[test]
512    fn key_display_name_bb_major() {
513        let key = KeySignature {
514            fifths: -2,
515            mode: "major".into(),
516        };
517        assert_eq!(key.display_name(), "Bb major");
518    }
519
520    #[test]
521    fn key_display_name_fsharp_minor() {
522        let key = KeySignature {
523            fifths: 3,
524            mode: "minor".into(),
525        };
526        assert_eq!(key.display_name(), "F# minor");
527    }
528
529    #[test]
530    fn key_tonic_d_major() {
531        let key = KeySignature {
532            fifths: 2,
533            mode: "major".into(),
534        };
535        let (step, alter) = key.tonic();
536        assert_eq!(step, Step::D);
537        assert_eq!(alter, 0);
538    }
539
540    #[test]
541    fn key_tonic_a_minor() {
542        // A minor = relative minor of C major (fifths=0)
543        let key = KeySignature {
544            fifths: 0,
545            mode: "minor".into(),
546        };
547        let (step, alter) = key.tonic();
548        assert_eq!(step, Step::A);
549        assert_eq!(alter, 0);
550    }
551
552    #[test]
553    fn chord_display_major() {
554        let c = ChordSymbol {
555            root: "C".into(),
556            kind: "major".into(),
557            bass: None,
558        };
559        assert_eq!(c.display_text(), "C");
560    }
561
562    #[test]
563    fn chord_display_minor_seventh_slash() {
564        let c = ChordSymbol {
565            root: "D".into(),
566            kind: "minor-seventh".into(),
567            bass: Some("F".into()),
568        };
569        assert_eq!(c.display_text(), "Dm7/F");
570    }
571
572    #[test]
573    fn time_sig_total_beats_three_four() {
574        let ts = TimeSignature {
575            numerator: 3,
576            denominator: 4,
577        };
578        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
579    }
580
581    #[test]
582    fn time_sig_total_beats_six_eight() {
583        let ts = TimeSignature {
584            numerator: 6,
585            denominator: 8,
586        };
587        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
588    }
589
590    #[test]
591    fn clef_treble_middle_b4() {
592        assert_eq!(Clef::Treble.middle_line_midi(), 71);
593    }
594
595    #[test]
596    fn clef_bass_middle_d3() {
597        assert_eq!(Clef::Bass.middle_line_midi(), 50);
598    }
599
600    #[test]
601    fn clef_alto_middle_c4() {
602        assert_eq!(Clef::Alto.middle_line_midi(), 60);
603    }
604
605    #[test]
606    fn clef_tenor_middle_a3() {
607        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
608    }
609}