Skip to main content

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