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 { fifths: 0, mode: "major".to_string() }
58    }
59}
60
61impl KeySignature {
62    // Order of sharps added one by one as fifths increases: F C G D A E B
63    const SHARP_ORDER: [Step; 7] = [Step::F, Step::C, Step::G, Step::D, Step::A, Step::E, Step::B];
64    // Order of flats: B E A D G C F
65    const FLAT_ORDER: [Step; 7]  = [Step::B, Step::E, Step::A, Step::D, Step::G, Step::C, Step::F];
66
67    /// Accidental alter (`-1`, `0`, or `+1`) applied to `step` in this key signature.
68    pub fn alter_for_step(&self, step: &Step) -> i8 {
69        if self.fifths > 0 {
70            let count = self.fifths.min(7) as usize;
71            if Self::SHARP_ORDER[..count].contains(step) { 1 } else { 0 }
72        } else if self.fifths < 0 {
73            let count = (-self.fifths).min(7) as usize;
74            if Self::FLAT_ORDER[..count].contains(step) { -1 } else { 0 }
75        } else {
76            0
77        }
78    }
79
80    /// True if `pitch` is diatonic to this key (octave-independent, checks step + alter).
81    pub fn contains_pitch(&self, pitch: &Pitch) -> bool {
82        pitch.alter == self.alter_for_step(&pitch.step)
83    }
84
85    /// Tonic step and alter for this key.
86    ///
87    /// Examples: G major → `(Step::G, 0)`, Bb major → `(Step::B, -1)`, F# minor → `(Step::F, 1)`.
88    pub fn tonic(&self) -> (Step, i8) {
89        if self.mode == "minor" {
90            let (maj_step, maj_alter) = Self::major_tonic_from_fifths(self.fifths);
91            // Relative minor tonic = major tonic - 3 semitones.
92            let major_midi = Pitch::with_alter(maj_step, 4, maj_alter).to_midi();
93            let minor_midi = (major_midi - 3).clamp(0, 127) as u8;
94            let p = Pitch::from_midi(minor_midi, self.fifths < 0);
95            (p.step, p.alter)
96        } else {
97            Self::major_tonic_from_fifths(self.fifths)
98        }
99    }
100
101    /// Human-readable key name: `"C major"`, `"G major"`, `"F# minor"`, `"Bb major"` etc.
102    pub fn display_name(&self) -> String {
103        let (step, alter) = self.tonic();
104        let acc = match alter { 1 => "#", -1 => "b", _ => "" };
105        format!("{}{} {}", step.to_char(), acc, self.mode)
106    }
107
108    fn major_tonic_from_fifths(fifths: i8) -> (Step, i8) {
109        // Index = fifths + 7 (range 0..=14).
110        // -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#
111        const TONICS: [(Step, i8); 15] = [
112            (Step::C, -1), // -7: Cb
113            (Step::G, -1), // -6: Gb
114            (Step::D, -1), // -5: Db
115            (Step::A, -1), // -4: Ab
116            (Step::E, -1), // -3: Eb
117            (Step::B, -1), // -2: Bb
118            (Step::F,  0), // -1: F
119            (Step::C,  0), //  0: C
120            (Step::G,  0), // +1: G
121            (Step::D,  0), // +2: D
122            (Step::A,  0), // +3: A
123            (Step::E,  0), // +4: E
124            (Step::B,  0), // +5: B
125            (Step::F,  1), // +6: F#
126            (Step::C,  1), // +7: C#
127        ];
128        let idx = (fifths.clamp(-7, 7) + 7) as usize;
129        TONICS[idx].clone()
130    }
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct TimeSignature {
135    pub numerator: u8,
136    pub denominator: u8,
137}
138
139impl Default for TimeSignature {
140    fn default() -> Self {
141        Self { numerator: 4, denominator: 4 }
142    }
143}
144
145impl TimeSignature {
146    pub fn beats_per_measure(&self) -> f64 {
147        self.numerator as f64
148    }
149
150    pub fn beat_unit_beats(&self) -> f64 {
151        4.0 / self.denominator as f64
152    }
153
154    pub fn total_beats(&self) -> f64 {
155        self.beats_per_measure() * self.beat_unit_beats()
156    }
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub enum Dynamic {
161    Pppp, Ppp, Pp, P, Mp, Mf, F, Ff, Fff, Ffff,
162    Sfz, Rfz, Fz, Sf,
163}
164
165impl Dynamic {
166    pub fn to_musicxml_str(&self) -> &'static str {
167        match self {
168            Dynamic::Pppp => "pppp",
169            Dynamic::Ppp  => "ppp",
170            Dynamic::Pp   => "pp",
171            Dynamic::P    => "p",
172            Dynamic::Mp   => "mp",
173            Dynamic::Mf   => "mf",
174            Dynamic::F    => "f",
175            Dynamic::Ff   => "ff",
176            Dynamic::Fff  => "fff",
177            Dynamic::Ffff => "ffff",
178            Dynamic::Sfz  => "sfz",
179            Dynamic::Rfz  => "rfz",
180            Dynamic::Fz   => "fz",
181            Dynamic::Sf   => "sf",
182        }
183    }
184
185    pub fn to_velocity(&self) -> u8 {
186        match self {
187            Dynamic::Pppp => 16,
188            Dynamic::Ppp  => 24,
189            Dynamic::Pp   => 36,
190            Dynamic::P    => 48,
191            Dynamic::Mp   => 60,
192            Dynamic::Mf   => 72,
193            Dynamic::F    => 84,
194            Dynamic::Ff   => 96,
195            Dynamic::Fff  => 108,
196            Dynamic::Ffff => 120,
197            Dynamic::Sfz  => 112,
198            Dynamic::Rfz  => 104,
199            Dynamic::Fz   => 100,
200            Dynamic::Sf   => 96,
201        }
202    }
203}
204
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub enum Articulation {
207    Staccato,
208    Staccatissimo,
209    Accent,
210    Tenuto,
211    Marcato,
212    Fermata,
213    Trill,
214    Mordent,
215    InvertedMordent,
216    Turn,
217    InvertedTurn,
218    Shake,
219    Tremolo(u8),
220    BreathMark,
221    Caesura,
222}
223
224/// Guitar-specific playing technique attached to a note.
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(rename_all = "kebab-case")]
227pub enum GuitarTechnique {
228    Bend,
229    Slide,
230    HammerOn,
231    PullOff,
232}
233
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
235pub enum Barline {
236    #[default]
237    Normal,
238    Double,
239    Final,
240    RepeatStart,
241    RepeatEnd,
242    RepeatBoth,
243    Dashed,
244    Dotted,
245    Invisible,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
249pub enum HairpinKind {
250    Crescendo,
251    Decrescendo,
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255pub struct TupletInfo {
256    /// Notes in the tuplet group (e.g. 3 for triplet).
257    pub actual_notes: u8,
258    /// Normal notes displaced (e.g. 2 for triplet = 3-in-2).
259    pub normal_notes: u8,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
263pub enum BeamState {
264    #[default]
265    None,
266    Begin,
267    Continue,
268    End,
269    BeginEnd,
270    BackwardHook,
271    ForwardHook,
272}
273
274/// Ottava (octave transposition bracket).
275#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
276pub enum OttavaKind {
277    /// 8va — sounds one octave higher than written.
278    Va8,
279    /// 8vb — sounds one octave lower than written.
280    Vb8,
281    /// 15ma — sounds two octaves higher than written.
282    Ma15,
283    /// 15mb — sounds two octaves lower than written.
284    Mb15,
285}
286
287impl OttavaKind {
288    pub fn musicxml_type(&self) -> &'static str {
289        match self {
290            OttavaKind::Va8 | OttavaKind::Ma15 => "up",
291            OttavaKind::Vb8 | OttavaKind::Mb15 => "down",
292        }
293    }
294
295    pub fn musicxml_size(&self) -> u8 {
296        match self {
297            OttavaKind::Va8 | OttavaKind::Vb8   => 8,
298            OttavaKind::Ma15 | OttavaKind::Mb15 => 15,
299        }
300    }
301}
302
303/// Note head shape.
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
305pub enum NoteHead {
306    #[default]
307    Normal,
308    Diamond,  // natural harmonics
309    X,        // muted / dead note
310    Slash,    // ghost note
311    Cross,    // percussion special
312    Triangle, // tap harmonics
313}
314
315/// Lyric syllable attached to a note.
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
317pub struct Lyric {
318    /// The syllable text.
319    pub text: String,
320    /// Syllabic position: "single" | "begin" | "middle" | "end"
321    pub syllabic: String,
322}
323
324/// Structured chord symbol attached to a note.
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326pub struct ChordSymbol {
327    /// Root note name: "C", "F#", "Bb", etc.
328    pub root: String,
329    /// MusicXML harmony kind: "major", "minor", "dominant", "major-seventh", etc.
330    pub kind: String,
331    /// Slash-chord bass note.
332    pub bass: Option<String>,
333}
334
335impl ChordSymbol {
336    pub fn display_text(&self) -> String {
337        let kind_str = match self.kind.as_str() {
338            "major" | "" => "",
339            "minor"            => "m",
340            "dominant"         => "7",
341            "major-seventh"    => "maj7",
342            "minor-seventh"    => "m7",
343            "diminished"       => "dim",
344            "diminished-seventh" => "dim7",
345            "augmented"        => "aug",
346            "suspended-second" => "sus2",
347            "suspended-fourth" => "sus4",
348            "half-diminished"  => "m7b5",
349            "major-sixth"      => "6",
350            "minor-sixth"      => "m6",
351            other              => other,
352        };
353        let bass_str = match &self.bass {
354            Some(b) => format!("/{}", b),
355            None => String::new(),
356        };
357        format!("{}{}{}", self.root, kind_str, bass_str)
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn key_alter_c_major_all_natural() {
367        let key = KeySignature { fifths: 0, mode: "major".into() };
368        for step in [Step::C, Step::D, Step::E, Step::F, Step::G, Step::A, Step::B] {
369            assert_eq!(key.alter_for_step(&step), 0, "step {:?}", step);
370        }
371    }
372
373    #[test]
374    fn key_alter_g_major_fsharp() {
375        let key = KeySignature { fifths: 1, mode: "major".into() };
376        assert_eq!(key.alter_for_step(&Step::F), 1);
377        assert_eq!(key.alter_for_step(&Step::G), 0);
378    }
379
380    #[test]
381    fn key_alter_f_major_bflat() {
382        let key = KeySignature { fifths: -1, mode: "major".into() };
383        assert_eq!(key.alter_for_step(&Step::B), -1);
384        assert_eq!(key.alter_for_step(&Step::C), 0);
385    }
386
387    #[test]
388    fn key_alter_bb_major() {
389        let key = KeySignature { fifths: -2, mode: "major".into() };
390        assert_eq!(key.alter_for_step(&Step::B), -1);
391        assert_eq!(key.alter_for_step(&Step::E), -1);
392        assert_eq!(key.alter_for_step(&Step::A), 0);
393    }
394
395    #[test]
396    fn key_contains_pitch_g_major() {
397        let key = KeySignature { fifths: 1, mode: "major".into() };
398        // In-key: G, A, B, C, D, E, F# (alter=1)
399        assert!(key.contains_pitch(&Pitch::new(Step::G, 4)));
400        assert!(key.contains_pitch(&Pitch::new(Step::D, 4)));
401        assert!(key.contains_pitch(&Pitch::with_alter(Step::F, 4, 1))); // F#
402        // Out-of-key: F natural
403        assert!(!key.contains_pitch(&Pitch::new(Step::F, 4)));
404    }
405
406    #[test]
407    fn key_display_name_c_major() {
408        let key = KeySignature { fifths: 0, mode: "major".into() };
409        assert_eq!(key.display_name(), "C major");
410    }
411
412    #[test]
413    fn key_display_name_bb_major() {
414        let key = KeySignature { fifths: -2, mode: "major".into() };
415        assert_eq!(key.display_name(), "Bb major");
416    }
417
418    #[test]
419    fn key_display_name_fsharp_minor() {
420        let key = KeySignature { fifths: 3, mode: "minor".into() };
421        assert_eq!(key.display_name(), "F# minor");
422    }
423
424    #[test]
425    fn key_tonic_d_major() {
426        let key = KeySignature { fifths: 2, mode: "major".into() };
427        let (step, alter) = key.tonic();
428        assert_eq!(step, Step::D);
429        assert_eq!(alter, 0);
430    }
431
432    #[test]
433    fn key_tonic_a_minor() {
434        // A minor = relative minor of C major (fifths=0)
435        let key = KeySignature { fifths: 0, mode: "minor".into() };
436        let (step, alter) = key.tonic();
437        assert_eq!(step, Step::A);
438        assert_eq!(alter, 0);
439    }
440
441    #[test]
442    fn chord_display_major() {
443        let c = ChordSymbol { root: "C".into(), kind: "major".into(), bass: None };
444        assert_eq!(c.display_text(), "C");
445    }
446
447    #[test]
448    fn chord_display_minor_seventh_slash() {
449        let c = ChordSymbol { root: "D".into(), kind: "minor-seventh".into(), bass: Some("F".into()) };
450        assert_eq!(c.display_text(), "Dm7/F");
451    }
452
453    #[test]
454    fn time_sig_total_beats_three_four() {
455        let ts = TimeSignature { numerator: 3, denominator: 4 };
456        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
457    }
458
459    #[test]
460    fn time_sig_total_beats_six_eight() {
461        let ts = TimeSignature { numerator: 6, denominator: 8 };
462        assert!((ts.total_beats() - 3.0).abs() < 1e-9);
463    }
464
465    #[test]
466    fn clef_treble_middle_b4() {
467        assert_eq!(Clef::Treble.middle_line_midi(), 71);
468    }
469
470    #[test]
471    fn clef_bass_middle_d3() {
472        assert_eq!(Clef::Bass.middle_line_midi(), 50);
473    }
474
475    #[test]
476    fn clef_alto_middle_c4() {
477        assert_eq!(Clef::Alto.middle_line_midi(), 60);
478    }
479
480    #[test]
481    fn clef_tenor_middle_a3() {
482        assert_eq!(Clef::Tenor.middle_line_midi(), 57);
483    }
484}