devalang_wasm/engine/functions/
chord.rs

1/// Chord function: plays multiple notes simultaneously
2///
3/// Usage: `synth -> chord([C4, E4, G4]) -> duration(800) -> velocity(100)`
4/// Or: `synth -> chord(Cmaj7) -> duration(800)`
5///
6/// Arguments:
7/// - notes: Array of note names or chord notation (e.g., "Cmaj7", "Dmin")
8///
9/// Chainable functions:
10/// - duration(ms): Chord duration in milliseconds
11/// - velocity(0-127): Chord velocity/volume
12/// - pan(-1.0 to 1.0): Stereo panning (left to right)
13/// - detune(-100 to 100): Pitch adjustment in cents
14/// - spread(0.0-1.0): Stereo spread for chord notes
15/// - gain(0.0-2.0): Volume multiplier
16/// - attack(seconds): Attack time override
17/// - release(seconds): Release time override
18/// - strum(ms): Strum delay between notes
19use 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        // Parse notes - can be array or identifier (chord name like "Cmaj7")
38        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                // Try to parse as chord notation (e.g., "Cmaj7", "Dmin")
48                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        // Store chord information - use "notes" key for clarity
62        context.set(
63            "notes",
64            Value::Array(notes.iter().map(|n| Value::String(n.clone())).collect()),
65        );
66
67        Ok(())
68    }
69}
70
71/// Helper to generate common chord types
72#[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    // Define intervals for different chord types
79    let intervals = match chord_type.to_lowercase().as_str() {
80        "major" | "maj" | "" => vec![0, 4, 7],  // Major triad
81        "minor" | "min" | "m" => vec![0, 3, 7], // Minor triad
82        "diminished" | "dim" => vec![0, 3, 6],  // Diminished
83        "augmented" | "aug" => vec![0, 4, 8],   // Augmented
84        "sus2" => vec![0, 2, 7],                // Suspended 2nd
85        "sus4" => vec![0, 5, 7],                // Suspended 4th
86        "7" | "dom7" => vec![0, 4, 7, 10],      // Dominant 7th
87        "maj7" => vec![0, 4, 7, 11],            // Major 7th
88        "min7" | "m7" => vec![0, 3, 7, 10],     // Minor 7th
89        _ => return Err(anyhow!("Unknown chord type: {}", chord_type)),
90    };
91
92    // Generate notes
93    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
118/// Parse chord notation like "Cmaj7", "Dmin", "F#dim"
119fn parse_chord_notation(chord_name: &str) -> Result<Vec<String>> {
120    // Extract root note and chord type
121    let chord_name = chord_name.trim();
122
123    // Find where the note ends (after potential # or b)
124    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    // Extract chord type and octave
133    // First check if rest looks like a chord type (to handle "G7" = dominant 7th, not G octave 7)
134    let rest = &chord_name[root_end..];
135
136    // Common chord type patterns: 7, maj7, min7, m7, dim, aug, sus2, sus4, etc.
137    // If it starts with known chord type patterns, treat everything as chord type
138    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        // Everything after note is chord type, use default octave 4
150        (format!("{}4", &chord_name[..root_end]), rest)
151    } else {
152        // Check for explicit octave
153        if let Some(first_char) = rest.chars().next() {
154            if first_char.is_ascii_digit() {
155                // Has octave, extract it
156                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                // No octave, use default 4
164                (format!("{}4", &chord_name[..root_end]), rest)
165            }
166        } else {
167            // Empty rest, default octave and major
168            (format!("{}4", &chord_name[..root_end]), "")
169        }
170    };
171
172    // Normalize chord type
173    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;