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/// A tablature string/fret position attached to a pitched note.
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
399pub struct TabPosition {
400    /// One-based string number, matching MusicXML `<string>`.
401    pub string: u8,
402    pub fret: u8,
403}
404
405/// Tablature staff metadata. Tuning MIDI values are ordered by string number.
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
407pub struct TablatureConfig {
408    pub lines: u8,
409    pub tuning_midi: Vec<i16>,
410    #[serde(default)]
411    pub capo: u8,
412}
413
414/// Structured chord symbol attached to a note.
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
416pub struct ChordSymbol {
417    /// Root note name: "C", "F#", "Bb", etc.
418    pub root: String,
419    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
420    pub kind: String,
421    /// Slash-chord bass note.
422    pub bass: Option<String>,
423}
424
425impl ChordSymbol {
426    pub fn display_text(&self) -> String {
427        let kind_str = match self.kind.as_str() {
428            "major" | "" => "",
429            "minor" => "m",
430            "dominant" => "7",
431            "major-seventh" => "maj7",
432            "minor-seventh" => "m7",
433            "diminished" => "dim",
434            "diminished-seventh" => "dim7",
435            "augmented" => "aug",
436            "suspended-second" => "sus2",
437            "suspended-fourth" => "sus4",
438            "half-diminished" => "m7b5",
439            "major-sixth" => "6",
440            "minor-sixth" => "m6",
441            other => other,
442        };
443        let bass_str = match &self.bass {
444            Some(b) => format!("/{}", b),
445            None => String::new(),
446        };
447        format!("{}{}{}", self.root, kind_str, bass_str)
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn key_alter_c_major_all_natural() {
457        let key = KeySignature {
458            fifths: 0,
459            mode: "major".into(),
460        };
461        for step in [
462            Step::C,
463            Step::D,
464            Step::E,
465            Step::F,
466            Step::G,
467            Step::A,
468            Step::B,
469        ] {
470            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
471        }
472    }
473
474    #[test]
475    fn key_alter_g_major_fsharp() {
476        let key = KeySignature {
477            fifths: 1,
478            mode: "major".into(),
479        };
480        assert_eq!(key.alter_for_step(&Step::F), 1);
481        assert_eq!(key.alter_for_step(&Step::G), 0);
482    }
483
484    #[test]
485    fn key_alter_f_major_bflat() {
486        let key = KeySignature {
487            fifths: -1,
488            mode: "major".into(),
489        };
490        assert_eq!(key.alter_for_step(&Step::B), -1);
491        assert_eq!(key.alter_for_step(&Step::C), 0);
492    }
493
494    #[test]
495    fn key_alter_bb_major() {
496        let key = KeySignature {
497            fifths: -2,
498            mode: "major".into(),
499        };
500        assert_eq!(key.alter_for_step(&Step::B), -1);
501        assert_eq!(key.alter_for_step(&Step::E), -1);
502        assert_eq!(key.alter_for_step(&Step::A), 0);
503    }
504
505    #[test]
506    fn key_contains_pitch_g_major() {
507        let key = KeySignature {
508            fifths: 1,
509            mode: "major".into(),
510        };
511        // In-key: G, A, B, C, D, E, F# (alter=1)
512        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
513        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
514        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
515        // Out-of-key: F natural
516        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
517    }
518
519    #[test]
520    fn key_display_name_c_major() {
521        let key = KeySignature {
522            fifths: 0,
523            mode: "major".into(),
524        };
525        assert_eq!(key.display_name(), "C major");
526    }
527
528    #[test]
529    fn key_display_name_bb_major() {
530        let key = KeySignature {
531            fifths: -2,
532            mode: "major".into(),
533        };
534        assert_eq!(key.display_name(), "Bb major");
535    }
536
537    #[test]
538    fn key_display_name_fsharp_minor() {
539        let key = KeySignature {
540            fifths: 3,
541            mode: "minor".into(),
542        };
543        assert_eq!(key.display_name(), "F# minor");
544    }
545
546    #[test]
547    fn key_tonic_d_major() {
548        let key = KeySignature {
549            fifths: 2,
550            mode: "major".into(),
551        };
552        let (step, alter) = key.tonic();
553        assert_eq!(step, Step::D);
554        assert_eq!(alter, 0);
555    }
556
557    #[test]
558    fn key_tonic_a_minor() {
559        // A minor = relative minor of C major (fifths=0)
560        let key = KeySignature {
561            fifths: 0,
562            mode: "minor".into(),
563        };
564        let (step, alter) = key.tonic();
565        assert_eq!(step, Step::A);
566        assert_eq!(alter, 0);
567    }
568
569    #[test]
570    fn chord_display_major() {
571        let c = ChordSymbol {
572            root: "C".into(),
573            kind: "major".into(),
574            bass: None,
575        };
576        assert_eq!(c.display_text(), "C");
577    }
578
579    #[test]
580    fn chord_display_minor_seventh_slash() {
581        let c = ChordSymbol {
582            root: "D".into(),
583            kind: "minor-seventh".into(),
584            bass: Some("F".into()),
585        };
586        assert_eq!(c.display_text(), "Dm7/F");
587    }
588
589    #[test]
590    fn time_sig_total_beats_three_four() {
591        let ts = TimeSignature {
592            numerator: 3,
593            denominator: 4,
594        };
595        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
596    }
597
598    #[test]
599    fn time_sig_total_beats_six_eight() {
600        let ts = TimeSignature {
601            numerator: 6,
602            denominator: 8,
603        };
604        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
605    }
606
607    #[test]
608    fn clef_treble_middle_b4() {
609        assert_eq!(Clef::Treble.middle_line_midi(), 71);
610    }
611
612    #[test]
613    fn clef_bass_middle_d3() {
614        assert_eq!(Clef::Bass.middle_line_midi(), 50);
615    }
616
617    #[test]
618    fn clef_alto_middle_c4() {
619        assert_eq!(Clef::Alto.middle_line_midi(), 60);
620    }
621
622    #[test]
623    fn clef_tenor_middle_a3() {
624        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
625    }
626}