use super::{FunctionContext, FunctionExecutor};
use crate::language::syntax::ast::nodes::Value;
use anyhow::{Result, anyhow};
pub struct ChordFunction;
impl FunctionExecutor for ChordFunction {
fn name(&self) -> &str {
"chord"
}
fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
if args.is_empty() {
return Err(anyhow!(
"chord() requires at least 1 argument (array of notes or chord name)"
));
}
let notes = match &args[0] {
Value::Array(arr) => arr
.iter()
.filter_map(|v| match v {
Value::String(s) | Value::Identifier(s) => Some(s.clone()),
_ => None,
})
.collect::<Vec<_>>(),
Value::String(chord_name) | Value::Identifier(chord_name) => {
parse_chord_notation(chord_name)?
}
_ => {
return Err(anyhow!(
"chord() first argument must be an array of notes or chord name"
));
}
};
if notes.is_empty() {
return Err(anyhow!("chord() requires at least one note"));
}
context.set(
"notes",
Value::Array(notes.iter().map(|n| Value::String(n.clone())).collect()),
);
Ok(())
}
}
#[allow(dead_code)]
pub fn generate_chord(root: &str, chord_type: &str) -> Result<Vec<String>> {
use super::note::parse_note_to_midi;
let root_midi = parse_note_to_midi(root)?;
let intervals = match chord_type.to_lowercase().as_str() {
"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)),
};
let notes = intervals
.iter()
.map(|interval| midi_to_note(root_midi + interval))
.collect::<Result<Vec<_>>>()?;
Ok(notes)
}
#[allow(dead_code)]
fn midi_to_note(midi: u8) -> Result<String> {
if midi > 127 {
return Err(anyhow!("MIDI note out of range: {}", midi));
}
let octave = (midi / 12) as i32 - 1;
let note_index = (midi % 12) as usize;
let note_names = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
let note_name = note_names[note_index];
Ok(format!("{}{}", note_name, octave))
}
fn parse_chord_notation(chord_name: &str) -> Result<Vec<String>> {
let chord_name = chord_name.trim();
let mut root_end = 1;
if let Some(second_char) = chord_name.chars().nth(1) {
if matches!(second_char, '#' | 'b') {
root_end = 2;
}
}
let rest = &chord_name[root_end..];
let is_chord_type = rest.starts_with("maj")
|| rest.starts_with("min")
|| rest.starts_with("dim")
|| rest.starts_with("aug")
|| rest.starts_with("sus")
|| rest == "7"
|| rest == "m"
|| rest.starts_with("m7")
|| rest == "+";
let (root, chord_type) = if is_chord_type {
(format!("{}4", &chord_name[..root_end]), rest)
} else {
if let Some(first_char) = rest.chars().next() {
if first_char.is_ascii_digit() {
let octave_char = first_char;
let type_rest = &rest[1..];
(
format!("{}{}", &chord_name[..root_end], octave_char),
type_rest,
)
} else {
(format!("{}4", &chord_name[..root_end]), rest)
}
} else {
(format!("{}4", &chord_name[..root_end]), "")
}
};
let normalized_type = match chord_type.trim().to_lowercase().as_str() {
"maj" | "major" | "" => "major",
"min" | "minor" | "m" => "minor",
"dim" | "diminished" => "diminished",
"aug" | "augmented" | "+" => "augmented",
"maj7" | "major7" => "maj7",
"min7" | "minor7" | "m7" => "min7",
"7" | "dom7" => "dom7",
"sus2" => "sus2",
"sus4" => "sus4",
other => return Err(anyhow!("Unknown chord type: {}", other)),
};
generate_chord(&root, normalized_type)
}
#[cfg(test)]
#[path = "test_chord.rs"]
mod tests;