1use acorde_core::{ChordSymbol, NoteAddr, Score, detect_chord, roman_numeral};
4use serde::{Deserialize, Serialize};
5
6pub const ANALYSIS_SCHEMA_VERSION: u32 = 2;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct ChordLabel {
12 pub address: NoteAddr,
13 pub chord: ChordSymbol,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub roman_numeral: Option<String>,
16 pub confidence: u8,
17 pub rule_id: String,
18 pub evidence: Vec<NoteAddr>,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct AnalysisResult {
24 pub schema_version: u32,
25 pub chords: Vec<ChordLabel>,
26 pub intervals: Vec<IntervalObservation>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct IntervalObservation {
32 pub from: NoteAddr,
33 pub to: NoteAddr,
34 pub semitones: u8,
35 pub diatonic_steps: i8,
36 pub rule_id: String,
37 pub evidence: Vec<NoteAddr>,
38}
39
40pub fn analyze_chords(score: &Score) -> AnalysisResult {
42 let mut chords = Vec::new();
43 for (part_index, part) in score.parts.iter().enumerate() {
44 for (staff_index, staff) in part.staves.iter().enumerate() {
45 for (measure_index, measure) in staff.measures.iter().enumerate() {
46 let key = measure
47 .key_sig
48 .as_ref()
49 .unwrap_or(&score.settings.key_signature);
50 for (voice_index, voice) in measure.voices.iter().enumerate() {
51 let pitched: Vec<_> = voice
52 .iter()
53 .enumerate()
54 .filter(|(_, note)| !note.is_rest && !note.pitches.is_empty())
55 .collect();
56 let pitches: Vec<_> = pitched
57 .iter()
58 .flat_map(|(_, note)| note.pitches.iter().cloned())
59 .collect();
60 let Some(chord) = detect_chord(&pitches) else {
61 continue;
62 };
63 let evidence = pitched
64 .iter()
65 .map(|(note_index, _)| NoteAddr {
66 part: part_index,
67 staff: staff_index,
68 measure: measure_index,
69 voice: voice_index,
70 note: *note_index,
71 })
72 .collect();
73 chords.push(ChordLabel {
74 address: NoteAddr {
75 part: part_index,
76 staff: staff_index,
77 measure: measure_index,
78 voice: voice_index,
79 note: pitched[0].0,
80 },
81 roman_numeral: roman_numeral(&chord, key),
82 chord,
83 confidence: 100,
84 rule_id: "pitch-class-template".to_string(),
85 evidence,
86 });
87 }
88 }
89 }
90 }
91 let intervals = analyze_intervals(score);
92 AnalysisResult {
93 schema_version: ANALYSIS_SCHEMA_VERSION,
94 chords,
95 intervals,
96 }
97}
98
99pub fn analyze_intervals(score: &Score) -> Vec<IntervalObservation> {
101 let mut observations = Vec::new();
102 for (part_index, part) in score.parts.iter().enumerate() {
103 for (staff_index, staff) in part.staves.iter().enumerate() {
104 for (measure_index, measure) in staff.measures.iter().enumerate() {
105 for (voice_index, voice) in measure.voices.iter().enumerate() {
106 let notes: Vec<_> = voice
107 .iter()
108 .enumerate()
109 .filter_map(|(note_index, note)| {
110 if note.is_rest {
111 None
112 } else {
113 note.pitches.first().map(|pitch| (note_index, pitch))
114 }
115 })
116 .collect();
117 for pair in notes.windows(2) {
118 let (from_index, from) = pair[0];
119 let (to_index, to) = pair[1];
120 let from_addr = NoteAddr {
121 part: part_index,
122 staff: staff_index,
123 measure: measure_index,
124 voice: voice_index,
125 note: from_index,
126 };
127 let to_addr = NoteAddr {
128 part: part_index,
129 staff: staff_index,
130 measure: measure_index,
131 voice: voice_index,
132 note: to_index,
133 };
134 observations.push(IntervalObservation {
135 from: from_addr.clone(),
136 to: to_addr.clone(),
137 semitones: (to.to_midi() - from.to_midi()).unsigned_abs() as u8,
138 diatonic_steps: diatonic_distance(from, to),
139 rule_id: "adjacent-melodic-interval".to_string(),
140 evidence: vec![from_addr, to_addr],
141 });
142 }
143 }
144 }
145 }
146 }
147 observations
148}
149
150fn diatonic_distance(from: &acorde_core::Pitch, to: &acorde_core::Pitch) -> i8 {
151 let step_index = |step: &acorde_core::Step| match step {
152 acorde_core::Step::C => 0i16,
153 acorde_core::Step::D => 1,
154 acorde_core::Step::E => 2,
155 acorde_core::Step::F => 3,
156 acorde_core::Step::G => 4,
157 acorde_core::Step::A => 5,
158 acorde_core::Step::B => 6,
159 };
160 (i16::from(to.octave) * 7 + step_index(&to.step)
161 - (i16::from(from.octave) * 7 + step_index(&from.step))) as i8
162}
163
164pub fn chord_name(chord: &ChordSymbol) -> String {
166 let suffix = match chord.kind.as_str() {
167 "major" => "",
168 "minor" => "m",
169 "dominant" => "7",
170 "major-seventh" => "maj7",
171 "minor-seventh" => "m7",
172 "diminished" => "dim",
173 "diminished-seventh" => "dim7",
174 "half-diminished" => "ΓΈ7",
175 "augmented" => "+",
176 _ => chord.kind.as_str(),
177 };
178 let bass = chord
179 .bass
180 .as_deref()
181 .map_or(String::new(), |bass| format!("/{bass}"));
182 format!("{}{suffix}{bass}", chord.root)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use acorde_core::{Duration, Note, Pitch, Score, Step};
189
190 #[test]
191 fn labels_chord_with_note_addresses_and_roman_numeral() {
192 let mut score = Score::default();
193 let voice = &mut score.parts[0].staves[0].measures[0].voices[0];
194 voice.clear();
195 for step in [Step::C, Step::E, Step::G] {
196 voice.push(Note::new(Pitch::new(step, 4), Duration::Quarter));
197 }
198 let result = analyze_chords(&score);
199 assert_eq!(result.schema_version, ANALYSIS_SCHEMA_VERSION);
200 assert_eq!(result.chords.len(), 1);
201 assert_eq!(result.intervals.len(), 2);
202 assert_eq!(result.chords[0].address.note, 0);
203 assert_eq!(result.chords[0].evidence.len(), 3);
204 assert_eq!(result.chords[0].roman_numeral.as_deref(), Some("I"));
205 assert_eq!(chord_name(&result.chords[0].chord), "C");
206 }
207
208 #[test]
209 fn interval_observation_preserves_direction_and_evidence() {
210 let mut score = Score::default();
211 let voice = &mut score.parts[0].staves[0].measures[0].voices[0];
212 voice.clear();
213 voice.push(Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
214 voice.push(Note::new(Pitch::new(Step::G, 4), Duration::Quarter));
215 let intervals = analyze_intervals(&score);
216 assert_eq!(intervals.len(), 1);
217 assert_eq!(intervals[0].semitones, 7);
218 assert_eq!(intervals[0].diatonic_steps, 4);
219 assert_eq!(intervals[0].evidence.len(), 2);
220 }
221
222 #[test]
223 fn does_not_invent_label_for_unknown_pitch_set() {
224 let mut score = Score::default();
225 let voice = &mut score.parts[0].staves[0].measures[0].voices[0];
226 voice.clear();
227 for step in [Step::C, Step::C] {
228 voice.push(Note::new(Pitch::new(step, 4), Duration::Quarter));
229 }
230 assert!(analyze_chords(&score).chords.is_empty());
231 }
232}