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 let Some(second_char) = chord_name.chars().nth(1) {
126 if matches!(second_char, '#' | 'b') {
127 root_end = 2;
128 }
129 }
130
131 let rest = &chord_name[root_end..];
134
135 let is_chord_type = rest.starts_with("maj")
138 || rest.starts_with("min")
139 || rest.starts_with("dim")
140 || rest.starts_with("aug")
141 || rest.starts_with("sus")
142 || rest == "7"
143 || rest == "m"
144 || rest.starts_with("m7")
145 || rest == "+";
146
147 let (root, chord_type) = if is_chord_type {
148 (format!("{}4", &chord_name[..root_end]), rest)
150 } else {
151 if let Some(first_char) = rest.chars().next() {
153 if first_char.is_ascii_digit() {
154 let octave_char = first_char;
156 let type_rest = &rest[1..];
157 (
158 format!("{}{}", &chord_name[..root_end], octave_char),
159 type_rest,
160 )
161 } else {
162 (format!("{}4", &chord_name[..root_end]), rest)
164 }
165 } else {
166 (format!("{}4", &chord_name[..root_end]), "")
168 }
169 };
170
171 let normalized_type = match chord_type.trim().to_lowercase().as_str() {
173 "maj" | "major" | "" => "major",
174 "min" | "minor" | "m" => "minor",
175 "dim" | "diminished" => "diminished",
176 "aug" | "augmented" | "+" => "augmented",
177 "maj7" | "major7" => "maj7",
178 "min7" | "minor7" | "m7" => "min7",
179 "7" | "dom7" => "dom7",
180 "sus2" => "sus2",
181 "sus4" => "sus4",
182 other => return Err(anyhow!("Unknown chord type: {}", other)),
183 };
184
185 generate_chord(&root, normalized_type)
186}
187
188#[cfg(test)]
189#[path = "test_chord.rs"]
190mod tests;