devalang_wasm/engine/functions/
effects.rs

1/// Additional arrow call functions: velocity, duration, pan, detune, spread, gain, attack, release, delay, reverb, drive
2use super::{FunctionContext, FunctionExecutor};
3use crate::language::syntax::ast::nodes::Value;
4use anyhow::{Result, anyhow};
5
6/// Velocity function: modifies note velocity (0-127)
7pub struct VelocityFunction;
8
9impl FunctionExecutor for VelocityFunction {
10    fn name(&self) -> &str {
11        "velocity"
12    }
13
14    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
15        if args.is_empty() {
16            return Err(anyhow!(
17                "velocity() requires 1 argument (velocity value 0-127)"
18            ));
19        }
20
21        let velocity = match &args[0] {
22            Value::Number(v) => *v,
23            _ => return Err(anyhow!("velocity() argument must be a number")),
24        };
25
26        context.set("velocity", Value::Number(velocity));
27        Ok(())
28    }
29}
30
31/// Duration function: modifies note duration
32pub struct DurationFunction;
33
34impl FunctionExecutor for DurationFunction {
35    fn name(&self) -> &str {
36        "duration"
37    }
38
39    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
40        if args.is_empty() {
41            return Err(anyhow!(
42                "duration() requires 1 argument (duration in ms or beats fraction like 1/4)"
43            ));
44        }
45
46        let duration_seconds = match &args[0] {
47            // Number: interpret as milliseconds
48            Value::Number(d) => *d / 1000.0,
49
50            // String or Identifier: parse as fraction (e.g., "1/4" or 1/4) and convert to seconds using tempo
51            Value::String(s) | Value::Identifier(s) => {
52                let frac_str = s.trim().trim_matches('"').trim_matches('\'');
53
54                // Try parsing as fraction (e.g., "1/4")
55                if let Some(beats) = parse_fraction_beats(frac_str) {
56                    // Convert beats to seconds using tempo from context
57                    beats * (60.0 / context.tempo)
58                } else {
59                    // Try parsing as number with optional "ms" suffix
60                    let num_str = frac_str.trim_end_matches("ms");
61                    match num_str.parse::<f32>() {
62                        Ok(ms) => ms / 1000.0,
63                        Err(_) => {
64                            return Err(anyhow!(
65                                "duration() argument must be a number (ms) or fraction (e.g., 1/4): got '{}'",
66                                s
67                            ));
68                        }
69                    }
70                }
71            }
72
73            _ => {
74                return Err(anyhow!(
75                    "duration() argument must be a number, string, or identifier"
76                ));
77            }
78        };
79
80        // Store duration in ms for consistency with other code
81        let duration_ms = duration_seconds * 1000.0;
82        context.set("duration", Value::Number(duration_ms));
83        context.duration = duration_seconds;
84        Ok(())
85    }
86}
87
88/// Parse fraction string like "1/4" to beats as f32
89fn parse_fraction_beats(s: &str) -> Option<f32> {
90    let mut parts = s.split('/');
91    let numerator: f32 = parts.next()?.trim().parse().ok()?;
92    let denominator: f32 = parts.next()?.trim().parse().ok()?;
93    if denominator.abs() < f32::EPSILON {
94        return None;
95    }
96    Some(numerator / denominator)
97}
98
99/// Pan function: stereo positioning (-1.0 = left, 0.0 = center, 1.0 = right)
100pub struct PanFunction;
101
102impl FunctionExecutor for PanFunction {
103    fn name(&self) -> &str {
104        "pan"
105    }
106
107    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
108        if args.is_empty() {
109            return Err(anyhow!("pan() requires 1 argument (pan value -1.0 to 1.0)"));
110        }
111
112        let pan = match &args[0] {
113            Value::Number(p) => *p,
114            _ => return Err(anyhow!("pan() argument must be a number")),
115        };
116
117        context.set("pan", Value::Number(pan));
118        Ok(())
119    }
120}
121
122/// Detune function: pitch adjustment in cents
123pub struct DetuneFunction;
124
125impl FunctionExecutor for DetuneFunction {
126    fn name(&self) -> &str {
127        "detune"
128    }
129
130    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
131        if args.is_empty() {
132            return Err(anyhow!("detune() requires 1 argument (cents)"));
133        }
134
135        let detune = match &args[0] {
136            Value::Number(d) => *d,
137            _ => return Err(anyhow!("detune() argument must be a number")),
138        };
139
140        context.set("detune", Value::Number(detune));
141        Ok(())
142    }
143}
144
145/// Spread function: stereo spread for chords (0.0 = mono, 1.0 = full stereo)
146pub struct SpreadFunction;
147
148impl FunctionExecutor for SpreadFunction {
149    fn name(&self) -> &str {
150        "spread"
151    }
152
153    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
154        if args.is_empty() {
155            return Err(anyhow!(
156                "spread() requires 1 argument (spread value 0.0 to 1.0)"
157            ));
158        }
159
160        let spread = match &args[0] {
161            Value::Number(s) => *s,
162            _ => return Err(anyhow!("spread() argument must be a number")),
163        };
164
165        context.set("spread", Value::Number(spread));
166        Ok(())
167    }
168}
169
170/// Gain function: volume multiplier
171pub struct GainFunction;
172
173impl FunctionExecutor for GainFunction {
174    fn name(&self) -> &str {
175        "gain"
176    }
177
178    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
179        if args.is_empty() {
180            return Err(anyhow!("gain() requires 1 argument (gain multiplier)"));
181        }
182
183        let gain = match &args[0] {
184            Value::Number(g) => *g,
185            _ => return Err(anyhow!("gain() argument must be a number")),
186        };
187
188        context.set("gain", Value::Number(gain));
189        Ok(())
190    }
191}
192
193/// Attack function: envelope attack time override in ms
194pub struct AttackFunction;
195
196impl FunctionExecutor for AttackFunction {
197    fn name(&self) -> &str {
198        "attack"
199    }
200
201    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
202        if args.is_empty() {
203            return Err(anyhow!("attack() requires 1 argument (attack time in ms)"));
204        }
205
206        let attack = match &args[0] {
207            Value::Number(a) => *a,
208            _ => return Err(anyhow!("attack() argument must be a number")),
209        };
210
211        context.set("attack", Value::Number(attack));
212        Ok(())
213    }
214}
215
216/// Release function: envelope release time override in ms
217pub struct ReleaseFunction;
218
219impl FunctionExecutor for ReleaseFunction {
220    fn name(&self) -> &str {
221        "release"
222    }
223
224    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
225        if args.is_empty() {
226            return Err(anyhow!(
227                "release() requires 1 argument (release time in ms)"
228            ));
229        }
230
231        let release = match &args[0] {
232            Value::Number(r) => *r,
233            _ => return Err(anyhow!("release() argument must be a number")),
234        };
235
236        context.set("release", Value::Number(release));
237        Ok(())
238    }
239}
240
241/// Delay function: echo effect with time in ms (default feedback: 0.3, mix: 0.5)
242/// Usage: -> delay(400) or -> delay(400, 0.5) or -> delay(400, 0.5, 0.7)
243pub struct DelayFunction;
244
245impl FunctionExecutor for DelayFunction {
246    fn name(&self) -> &str {
247        "delay"
248    }
249
250    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
251        if args.is_empty() {
252            return Err(anyhow!(
253                "delay() requires at least 1 argument (delay time in ms)"
254            ));
255        }
256        // Support both positional args: delay(time, feedback?, mix?)
257        // and map-style: delay({ time: 300, feedback: 0.3, mix: 0.5 })
258        let mut time_val: Option<f32> = None;
259        let mut feedback: f32 = 0.3;
260        let mut mix: f32 = 0.5;
261
262        if let Some(Value::Map(params)) = args.first() {
263            if let Some(Value::Number(t)) = params.get("time") {
264                time_val = Some(*t);
265            }
266            if let Some(Value::Number(f)) = params.get("feedback") {
267                feedback = *f;
268            }
269            if let Some(Value::Number(m)) = params.get("mix") {
270                mix = *m;
271            }
272        } else {
273            // Positional
274            // Time in ms (required)
275            time_val = match &args[0] {
276                Value::Number(t) => Some(*t),
277                _ => None,
278            };
279
280            if args.len() > 1 {
281                if let Value::Number(f) = &args[1] {
282                    feedback = *f;
283                } else {
284                    return Err(anyhow!(
285                        "delay() second argument must be a number (feedback)"
286                    ));
287                }
288            }
289
290            if args.len() > 2 {
291                if let Value::Number(m) = &args[2] {
292                    mix = *m;
293                } else {
294                    return Err(anyhow!("delay() third argument must be a number (mix)"));
295                }
296            }
297        }
298
299        let time = if let Some(t) = time_val {
300            t
301        } else {
302            return Err(anyhow!(
303                "delay() requires a time parameter either as first numeric arg or as {{ time: ... }}"
304            ));
305        };
306
307        context.set("delay_time", Value::Number(time));
308        context.set("delay_feedback", Value::Number(feedback));
309        context.set("delay_mix", Value::Number(mix));
310
311        Ok(())
312    }
313}
314
315/// Reverb function: reverb effect with amount (0.0-1.0, default: 0.5)
316/// Usage: -> reverb(0.3) for 30% reverb
317pub struct ReverbFunction;
318
319impl FunctionExecutor for ReverbFunction {
320    fn name(&self) -> &str {
321        "reverb"
322    }
323
324    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
325        // Accept either reverb(0.3) or reverb({ size: 0.9 })
326        let mut amount = 0.5; // default
327
328        if let Some(Value::Map(params)) = args.first() {
329            if let Some(Value::Number(s)) = params.get("size") {
330                amount = *s;
331            } else if let Some(Value::Number(a)) = params.get("amount") {
332                amount = *a;
333            }
334        } else if let Some(Value::Number(a)) = args.get(0) {
335            amount = *a;
336        } else {
337            return Err(anyhow!(
338                "reverb() requires either a numeric argument or a parameter map {{ size: ... }}"
339            ));
340        }
341
342        context.set("reverb_amount", Value::Number(amount));
343        context.set("reverb_size", Value::Number(amount)); // Keep compatibility
344
345        Ok(())
346    }
347}
348
349/// Drive function: tube-style saturation (amount: 0.0-1.0, color: 0.0-1.0 default 0.5)
350/// Usage: -> drive(0.6) or -> drive(0.6, 0.7)
351pub struct DriveFunction;
352
353impl FunctionExecutor for DriveFunction {
354    fn name(&self) -> &str {
355        "drive"
356    }
357
358    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
359        if args.is_empty() {
360            return Err(anyhow!(
361                "drive() requires at least 1 argument (drive amount 0.0-1.0)"
362            ));
363        }
364        // Support both positional: drive(amount, color?) and map: drive({ gain: 2.0, color: 0.5 })
365        let mut amount = 0.5;
366        let mut color = 0.5;
367
368        if let Some(Value::Map(params)) = args.first() {
369            if let Some(Value::Number(g)) = params.get("gain") {
370                amount = *g;
371            } else if let Some(Value::Number(a)) = params.get("amount") {
372                amount = *a;
373            }
374
375            if let Some(Value::Number(c)) = params.get("color") {
376                color = *c;
377            }
378        } else {
379            // Positional
380            amount = match &args[0] {
381                Value::Number(a) => *a,
382                _ => return Err(anyhow!("drive() first argument must be a number (amount)")),
383            };
384
385            if args.len() > 1 {
386                color = match &args[1] {
387                    Value::Number(c) => *c,
388                    _ => return Err(anyhow!("drive() second argument must be a number (color)")),
389                };
390            }
391        }
392
393        context.set("drive_amount", Value::Number(amount));
394        context.set("drive_amp", Value::Number(amount)); // Keep compatibility
395        context.set("drive_color", Value::Number(color));
396
397        Ok(())
398    }
399}
400
401/// Chorus function: multiple detuned voices for richness
402pub struct ChorusFunction;
403
404impl FunctionExecutor for ChorusFunction {
405    fn name(&self) -> &str {
406        "chorus"
407    }
408
409    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
410        // Default params
411        let mut depth = 0.5; // Amount of pitch modulation
412        let mut rate = 1.5; // LFO rate in Hz
413        let mut mix = 0.5; // Dry/wet mix
414
415        // Parse params from map { depth: 0.5, rate: 1.5, mix: 0.5 }
416        if let Some(Value::Map(params)) = args.first() {
417            if let Some(Value::Number(d)) = params.get("depth") {
418                depth = *d;
419            }
420            if let Some(Value::Number(r)) = params.get("rate") {
421                rate = *r;
422            }
423            if let Some(Value::Number(m)) = params.get("mix") {
424                mix = *m;
425            }
426        }
427
428        context.set("chorus_depth", Value::Number(depth));
429        context.set("chorus_rate", Value::Number(rate));
430        context.set("chorus_mix", Value::Number(mix));
431
432        Ok(())
433    }
434}
435
436/// Flanger function: sweeping comb filter effect
437pub struct FlangerFunction;
438
439impl FunctionExecutor for FlangerFunction {
440    fn name(&self) -> &str {
441        "flanger"
442    }
443
444    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
445        // Default params
446        let mut depth = 0.7; // Delay modulation depth
447        let mut rate = 0.5; // LFO rate in Hz
448        let mut feedback = 0.5; // Feedback amount
449        let mut mix = 0.5; // Dry/wet mix
450
451        // Parse params from map
452        if let Some(Value::Map(params)) = args.first() {
453            if let Some(Value::Number(d)) = params.get("depth") {
454                depth = *d;
455            }
456            if let Some(Value::Number(r)) = params.get("rate") {
457                rate = *r;
458            }
459            if let Some(Value::Number(f)) = params.get("feedback") {
460                feedback = *f;
461            }
462            if let Some(Value::Number(m)) = params.get("mix") {
463                mix = *m;
464            }
465        }
466
467        context.set("flanger_depth", Value::Number(depth));
468        context.set("flanger_rate", Value::Number(rate));
469        context.set("flanger_feedback", Value::Number(feedback));
470        context.set("flanger_mix", Value::Number(mix));
471
472        Ok(())
473    }
474}
475
476/// Phaser function: phase shifting effect
477pub struct PhaserFunction;
478
479impl FunctionExecutor for PhaserFunction {
480    fn name(&self) -> &str {
481        "phaser"
482    }
483
484    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
485        // Default params
486        let mut stages = 4.0; // Number of allpass stages (2-12)
487        let mut rate = 0.5; // LFO rate in Hz
488        let mut depth = 0.7; // Modulation depth
489        let mut feedback = 0.5; // Feedback amount
490        let mut mix = 0.5; // Dry/wet mix
491
492        // Parse params from map
493        if let Some(Value::Map(params)) = args.first() {
494            if let Some(Value::Number(s)) = params.get("stages") {
495                stages = *s;
496            }
497            if let Some(Value::Number(r)) = params.get("rate") {
498                rate = *r;
499            }
500            if let Some(Value::Number(d)) = params.get("depth") {
501                depth = *d;
502            }
503            if let Some(Value::Number(f)) = params.get("feedback") {
504                feedback = *f;
505            }
506            if let Some(Value::Number(m)) = params.get("mix") {
507                mix = *m;
508            }
509        }
510
511        context.set("phaser_stages", Value::Number(stages));
512        context.set("phaser_rate", Value::Number(rate));
513        context.set("phaser_depth", Value::Number(depth));
514        context.set("phaser_feedback", Value::Number(feedback));
515        context.set("phaser_mix", Value::Number(mix));
516
517        Ok(())
518    }
519}
520
521/// Compressor function: dynamic range compression
522pub struct CompressorFunction;
523
524impl FunctionExecutor for CompressorFunction {
525    fn name(&self) -> &str {
526        "compressor"
527    }
528
529    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
530        // Default params
531        let mut threshold = -20.0; // Threshold in dB
532        let mut ratio = 4.0; // Compression ratio (1:1 to 20:1)
533        let mut attack = 5.0; // Attack time in ms
534        let mut release = 50.0; // Release time in ms
535        let mut makeup = 0.0; // Makeup gain in dB
536
537        // Parse params from map
538        if let Some(Value::Map(params)) = args.first() {
539            if let Some(Value::Number(t)) = params.get("threshold") {
540                threshold = *t;
541            }
542            if let Some(Value::Number(r)) = params.get("ratio") {
543                ratio = *r;
544            }
545            if let Some(Value::Number(a)) = params.get("attack") {
546                attack = *a;
547            }
548            if let Some(Value::Number(r)) = params.get("release") {
549                release = *r;
550            }
551            if let Some(Value::Number(m)) = params.get("makeup") {
552                makeup = *m;
553            }
554        }
555
556        context.set("compressor_threshold", Value::Number(threshold));
557        context.set("compressor_ratio", Value::Number(ratio));
558        context.set("compressor_attack", Value::Number(attack));
559        context.set("compressor_release", Value::Number(release));
560        context.set("compressor_makeup", Value::Number(makeup));
561
562        Ok(())
563    }
564}
565
566/// Distortion function: enhanced distortion/saturation
567pub struct DistortionFunction;
568
569impl FunctionExecutor for DistortionFunction {
570    fn name(&self) -> &str {
571        "distortion"
572    }
573
574    fn execute(&self, context: &mut FunctionContext, args: &[Value]) -> Result<()> {
575        // Default params
576        let mut amount = 0.5; // Distortion amount 0-1
577        let mut tone = 0.5; // Tone control (brightness)
578        let mut mix = 1.0; // Dry/wet mix
579
580        // Parse params from map
581        if let Some(Value::Map(params)) = args.first() {
582            if let Some(Value::Number(a)) = params.get("amount") {
583                amount = *a;
584            }
585            if let Some(Value::Number(t)) = params.get("tone") {
586                tone = *t;
587            }
588            if let Some(Value::Number(m)) = params.get("mix") {
589                mix = *m;
590            }
591        }
592
593        context.set("distortion_amount", Value::Number(amount));
594        context.set("distortion_tone", Value::Number(tone));
595        context.set("distortion_mix", Value::Number(mix));
596
597        Ok(())
598    }
599}