devalang_wasm/engine/functions/
chord.rs1use super::{FunctionContext, FunctionExecutor};
20use crate::language::syntax::ast::nodes::Value;
21use anyhow::{Result, anyhow};
22
23pub struct ChordFunction;
24
25impl FunctionExecutor for ChordFunction {
26 fn name(&self) -> &str {
27 "chord"
28 }
29
30 fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
31 if args.is_empty() {
32 return Err(anyhow!(
33 "chord() requires at least 1 argument (array of notes or chord name)"
34 ));
35 }
36
37 let notes = match &args[0] {
39 Value::Array(arr) => arr
40 .iter()
41 .filter_map(|v| match v {
42 Value::String(s) | Value::Identifier(s) => Some(s.clone()),
43 _ => None,
44 })
45 .collect::<Vec<_>>(),
46 Value::String(chord_name) | Value::Identifier(chord_name) => {
47 parse_chord_notation(chord_name)?
49 }
50 _ => {
51 return Err(anyhow!(
52 "chord() first argument must be an array of notes or chord name"
53 ));
54 }
55 };
56
57 if notes.is_empty() {
58 return Err(anyhow!("chord() requires at least one note"));
59 }
60
61 context.set(
63 "notes",
64 Value::Array(notes.iter().map(|n| Value::String(n.clone())).collect()),
65 );
66
67 Ok(())
68 }
69}
70
71#[allow(dead_code)]
73pub fn generate_chord(root: &str, chord_type: &str) -> Result<Vec<String>> {
74 use super::note::parse_note_to_midi;
75
76 let root_midi = parse_note_to_midi(root)?;
77
78 let intervals = match chord_type.to_lowercase().as_str() {
80 "major" | "maj" | "" => vec![0, 4, 7], "minor" | "min" | "m" => vec![0, 3, 7], "diminished" | "dim" => vec![0, 3, 6], "augmented" | "aug" => vec![0, 4, 8], "sus2" => vec![0, 2, 7], "sus4" => vec![0, 5, 7], "7" | "dom7" => vec![0, 4, 7, 10], "maj7" => vec![0, 4, 7, 11], "min7" | "m7" => vec![0, 3, 7, 10], _ => return Err(anyhow!("Unknown chord type: {}", chord_type)),
90 };
91
92 let notes = intervals
94 .iter()
95 .map(|interval| midi_to_note(root_midi + interval))
96 .collect::<Result<Vec<_>>>()?;
97
98 Ok(notes)
99}
100
101#[allow(dead_code)]
102fn midi_to_note(midi: u8) -> Result<String> {
103 if midi > 127 {
104 return Err(anyhow!("MIDI note out of range: {}", midi));
105 }
106
107 let octave = (midi / 12) as i32 - 1;
108 let note_index = (midi % 12) as usize;
109
110 let note_names = [
111 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
112 ];
113 let note_name = note_names[note_index];
114
115 Ok(format!("{}{}", note_name, octave))
116}
117
118fn parse_chord_notation(chord_name: &str) -> Result<Vec<String>> {
120 let chord_name = chord_name.trim();
122
123 let mut root_end = 1;
125 if chord_name.len() > 1 {
126 let second_char = chord_name.chars().nth(1).unwrap();
127 if second_char == '#' || second_char == 'b' {
128 root_end = 2;
129 }
130 }
131
132 let rest = &chord_name[root_end..];
135
136 let is_chord_type = rest.starts_with("maj")
139 || rest.starts_with("min")
140 || rest.starts_with("dim")
141 || rest.starts_with("aug")
142 || rest.starts_with("sus")
143 || rest == "7"
144 || rest == "m"
145 || rest.starts_with("m7")
146 || rest == "+";
147
148 let (root, chord_type) = if is_chord_type {
149 (format!("{}4", &chord_name[..root_end]), rest)
151 } else {
152 if let Some(first_char) = rest.chars().next() {
154 if first_char.is_ascii_digit() {
155 let octave_char = first_char;
157 let type_rest = &rest[1..];
158 (
159 format!("{}{}", &chord_name[..root_end], octave_char),
160 type_rest,
161 )
162 } else {
163 (format!("{}4", &chord_name[..root_end]), rest)
165 }
166 } else {
167 (format!("{}4", &chord_name[..root_end]), "")
169 }
170 };
171
172 let normalized_type = match chord_type.trim().to_lowercase().as_str() {
174 "maj" | "major" | "" => "major",
175 "min" | "minor" | "m" => "minor",
176 "dim" | "diminished" => "diminished",
177 "aug" | "augmented" | "+" => "augmented",
178 "maj7" | "major7" => "maj7",
179 "min7" | "minor7" | "m7" => "min7",
180 "7" | "dom7" => "dom7",
181 "sus2" => "sus2",
182 "sus4" => "sus4",
183 other => return Err(anyhow!("Unknown chord type: {}", other)),
184 };
185
186 generate_chord(&root, normalized_type)
187}
188
189#[cfg(test)]
190#[path = "test_chord.rs"]
191mod tests;