devalang_wasm/engine/functions/
note.rs

1/// Note function: plays a single note
2///
3/// Usage: `synth -> note(C4) -> velocity(100) -> duration(400)`
4///
5/// Arguments:
6/// - note: String or Identifier (e.g., "C4", "D#5")
7///
8/// Chainable functions:
9/// - duration(ms): Note duration in milliseconds
10/// - velocity(0-127): Note velocity/volume
11/// - pan(-1.0 to 1.0): Stereo panning (left to right)
12/// - detune(-100 to 100): Pitch adjustment in cents
13/// - gain(0.0-2.0): Volume multiplier
14/// - attack(seconds): Attack time override
15/// - release(seconds): Release time override
16use super::{FunctionContext, FunctionExecutor};
17use crate::language::syntax::ast::nodes::Value;
18use anyhow::{Result, anyhow};
19
20pub struct NoteFunction;
21
22impl FunctionExecutor for NoteFunction {
23    fn name(&self) -> &str {
24        "note"
25    }
26
27    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
28        if args.is_empty() {
29            return Err(anyhow!("note() requires at least 1 argument (note name)"));
30        }
31
32        // Parse note name
33        let note_name = match &args[0] {
34            Value::String(s) | Value::Identifier(s) => s.clone(),
35            _ => return Err(anyhow!("note() first argument must be a note name")),
36        };
37
38        // Store note information in context
39        context.set("note", Value::String(note_name.clone()));
40
41        Ok(())
42    }
43}
44
45/// Parse note name to MIDI number
46/// C4 = 60, C#4 = 61, D4 = 62, etc.
47pub fn parse_note_to_midi(note: &str) -> Result<u8> {
48    let note = note.to_uppercase();
49
50    // Extract base note (C, D, E, F, G, A, B)
51    let base_char = note.chars().next().ok_or_else(|| anyhow!("Empty note"))?;
52    let base_note = match base_char {
53        'C' => 0,
54        'D' => 2,
55        'E' => 4,
56        'F' => 5,
57        'G' => 7,
58        'A' => 9,
59        'B' => 11,
60        _ => return Err(anyhow!("Invalid note: {}", base_char)),
61    };
62
63    // Extract sharp/flat
64    let mut offset = 0;
65    let mut octave_start = 1;
66
67    if note.len() > 1 {
68        match note.chars().nth(1) {
69            Some('#') => {
70                offset = 1;
71                octave_start = 2;
72            }
73            Some('b') | Some('B') => {
74                offset = -1;
75                octave_start = 2;
76            }
77            _ => {}
78        }
79    }
80
81    // Extract octave
82    let octave_str = &note[octave_start..];
83    let octave: i32 = octave_str
84        .parse()
85        .map_err(|_| anyhow!("Invalid octave: {}", octave_str))?;
86
87    // Calculate MIDI number: (octave + 1) * 12 + base_note + offset
88    let midi = ((octave + 1) * 12) + base_note + offset;
89
90    if midi < 0 || midi > 127 {
91        return Err(anyhow!("MIDI note out of range: {}", midi));
92    }
93
94    Ok(midi as u8)
95}
96
97#[cfg(test)]
98#[path = "test_note.rs"]
99mod tests;