devalang_wasm/engine/audio/interpreter/driver/
extractor.rs

1use crate::engine::audio::events::AudioEvent;
2use crate::language::syntax::ast::Value;
3use anyhow::Result;
4
5use super::AudioInterpreter;
6
7/// Apply automation overrides to a parameter value
8/// Returns the final value after applying global automation, then note-mode templates
9/// For note-mode templates, calculates progress based on the note's start_time relative to the automation block
10fn apply_automation_param(
11    interpreter: &AudioInterpreter,
12    target: &str,
13    param_names: &[&str], // e.g., ["pan"] or ["pitch", "detune"]
14    base_value: f32,
15    note_start_time: f32, // Time when the note starts (used for note-mode progress calculation)
16) -> f32 {
17    let mut result = base_value;
18
19    // 1. Try global automation first
20    for name in param_names {
21        if let Some(v) =
22            interpreter
23                .automation_registry
24                .get_value(target, name, interpreter.cursor_time)
25        {
26            result = v;
27            break; // Use first matching param name
28        }
29    }
30
31    // 2. Apply note-mode templates
32    if let Some(ctx) = interpreter.note_automation_templates.get(target) {
33        // Calculate progress based on note's position in the automation block
34        let note_progress = ctx.progress_at_time(note_start_time);
35
36        for tpl in ctx.templates.iter() {
37            for name in param_names {
38                if tpl.param_name == *name {
39                    // Evaluate template at the note's progress position
40                    result =
41                        crate::engine::audio::automation::evaluate_template_at(tpl, note_progress);
42                    return result; // Use first matching template
43                }
44            }
45        }
46    }
47
48    result
49}
50
51/// Extract plugin-specific parameters from effects array
52/// Plugin parameters should be applied BEFORE generation as synth options, not AFTER as audio effects.
53/// Strategy: Any effect that's NOT a known audio effect is treated as a plugin parameter.
54/// Returns remaining effects (plugin params removed)
55fn extract_plugin_parameters(
56    effects: &Option<Value>,
57    synth_def: &mut crate::engine::audio::events::SynthDefinition,
58) -> Option<Value> {
59    if effects.is_none() {
60        return None;
61    }
62
63    // Known audio effect names - everything else is treated as plugin parameter
64    let audio_effect_names = [
65        // Main effects (Both synth + trigger)
66        "reverb",
67        "delay",
68        "distortion",
69        "bitcrush",
70        "lowpass",
71        "lpf",
72        "highpass",
73        "hpf",
74        "bandpass",
75        "bpf",
76        "tremolo",
77        "vibrato",
78        "chorus",
79        "flanger",
80        "phaser",
81        "compressor",
82        "comp",
83        "drive",
84        "gate",
85        "lfo",
86        // Trigger-only effects
87        "reverse",
88        "speed",
89        "slice",
90        "stretch",
91        "roll",
92        // Sample processing
93        "mono",
94        "monoizer",
95        "stereo",
96        // Special/control effects
97        "freeze",
98        "adsr",
99        "eq",
100        "equalizer",
101        // Aliases for distortion
102        "dist",
103    ];
104
105    if let Some(Value::Array(effects_arr)) = effects {
106        let mut remaining_effects = Vec::new();
107        let mut found_params = false;
108
109        for effect in effects_arr {
110            if let Value::Map(effect_map) = effect {
111                // Check if "type" field is a known audio effect
112                if let Some(Value::String(type_name)) = effect_map.get("type") {
113                    let type_lower = type_name.to_lowercase();
114
115                    if !audio_effect_names.contains(&type_lower.as_str()) {
116                        // This is NOT a known audio effect - treat it as a plugin parameter
117                        found_params = true;
118
119                        // Extract value and add to synth options
120                        // Priority: "value" key > any numeric value > type name itself
121                        if let Some(Value::Number(val)) = effect_map.get("value") {
122                            synth_def.options.insert(type_name.clone(), *val);
123                        } else {
124                            // Look for any numeric parameter
125                            let mut found_numeric = false;
126                            for (key, val) in effect_map.iter() {
127                                if key != "type" {
128                                    if let Value::Number(n) = val {
129                                        synth_def.options.insert(key.clone(), *n);
130                                        found_numeric = true;
131                                    }
132                                }
133                            }
134                            // If no numeric params found, skip this effect
135                            if !found_numeric {
136                                continue;
137                            }
138                        }
139                        continue; // Don't add this effect to remaining_effects
140                    }
141                }
142            }
143            // Keep known audio effects for audio chain
144            remaining_effects.push(effect.clone());
145        }
146
147        if found_params {
148            if remaining_effects.is_empty() {
149                return None; // All effects were plugin params
150            } else {
151                return Some(Value::Array(remaining_effects)); // Return non-plugin effects
152            }
153        }
154    }
155
156    effects.clone() // Return effects unchanged if no plugin params found
157}
158
159pub fn extract_audio_event(
160    interpreter: &mut AudioInterpreter,
161    target: &str,
162    context: &crate::engine::functions::FunctionContext,
163) -> Result<()> {
164    // Implementation simplified: reuse existing driver logic
165    // Try single note first
166    if let Some(Value::String(note_name)) = context.get("note") {
167        // Prepare placeholder for merged effects (will be computed after collecting synth/note effects)
168        let mut event_effects: Option<crate::language::syntax::ast::Value> = None;
169        let midi = crate::engine::functions::note::parse_note_to_midi(note_name)?;
170
171        // Duration: prefer context.duration if set (execute_arrow_call may set it), otherwise read "duration" value (ms)
172        let duration = if context.duration > 0.0 {
173            context.duration
174        } else if let Some(Value::Number(d)) = context.get("duration") {
175            d / 1000.0
176        } else {
177            0.5
178        };
179
180        // Velocity: accept either 0.0-1.0 or 0-127 (if > 2.0 treat as MIDI scale)
181        let velocity = if let Some(Value::Number(v)) = context.get("velocity") {
182            if *v > 2.0 {
183                (*v) / 127.0
184            } else if *v > 1.0 {
185                (*v) / 100.0
186            } else {
187                *v
188            }
189        } else {
190            0.8
191        };
192
193        // Pan: preference order: explicit context > automation > default
194        let base_pan = if let Some(Value::Number(p)) = context.get("pan") {
195            *p
196        } else {
197            0.0
198        };
199
200        // Check if this note should use per-note automation
201        let use_per_note_automation = interpreter.note_automation_templates.contains_key(target);
202
203        // If using per-note automation, don't apply templates here - apply at render time
204        let pan = if use_per_note_automation {
205            base_pan
206        } else {
207            apply_automation_param(
208                interpreter,
209                target,
210                &["pan"],
211                base_pan,
212                interpreter.cursor_time,
213            )
214        };
215
216        // Detune/Pitch: automation may come from either "pitch" or "detune" param name
217        let base_detune = if let Some(Value::Number(d)) = context.get("detune") {
218            *d
219        } else {
220            0.0
221        };
222        let detune = if use_per_note_automation {
223            base_detune
224        } else {
225            apply_automation_param(
226                interpreter,
227                target,
228                &["pitch", "detune"],
229                base_detune,
230                interpreter.cursor_time,
231            )
232        };
233
234        // Gain/Volume: automation may come from either "volume" or "gain" param name
235        let base_gain = if let Some(Value::Number(g)) = context.get("gain") {
236            *g
237        } else {
238            1.0
239        };
240        let gain = if use_per_note_automation {
241            base_gain
242        } else {
243            apply_automation_param(
244                interpreter,
245                target,
246                &["volume", "gain"],
247                base_gain,
248                interpreter.cursor_time,
249            )
250        };
251
252        // Use the provided target as synth id so the synth definition (including plugin info)
253        // is correctly snapshotted at event creation time. Fall back to "default" if empty.
254        let synth_id = if target.is_empty() { "default" } else { target };
255
256        // Build a synth definition snapshot and apply automation overrides so that the
257
258        let mut synth_def = interpreter
259            .events
260            .get_synth(synth_id)
261            .cloned()
262            .unwrap_or_default();
263
264        // Apply automation overrides to synth options (cutoff, resonance, etc.)
265        // Note: cutoff and resonance are in filters, not options
266        for filter in &mut synth_def.filters {
267            // Apply cutoff automation
268            let current_cutoff = filter.cutoff;
269            let automated_cutoff = apply_automation_param(
270                interpreter,
271                synth_id,
272                &["cutoff"],
273                current_cutoff,
274                interpreter.cursor_time,
275            );
276            if (automated_cutoff - current_cutoff).abs() > 0.0001 {
277                filter.cutoff = automated_cutoff;
278            }
279
280            // Apply resonance automation
281            let current_resonance = filter.resonance;
282            let automated_resonance = apply_automation_param(
283                interpreter,
284                synth_id,
285                &["resonance"],
286                current_resonance,
287                interpreter.cursor_time,
288            );
289            if (automated_resonance - current_resonance).abs() > 0.0001 {
290                filter.resonance = automated_resonance;
291            }
292        }
293
294        // Apply automation overrides to synth type options (drive, tone, decay, etc.)
295        let synth_params = ["filter_type", "drive", "tone", "decay"];
296        for param in &synth_params {
297            let current_val = synth_def.options.get(*param).copied().unwrap_or(0.0);
298            let automated_val = apply_automation_param(
299                interpreter,
300                synth_id,
301                &[param],
302                current_val,
303                interpreter.cursor_time,
304            );
305            if (automated_val - current_val).abs() > 0.0001 {
306                synth_def.options.insert(param.to_string(), automated_val);
307            }
308        }
309
310        let mut synth_effects_vec: Vec<crate::language::syntax::ast::Value> = Vec::new();
311        if let Some(var_val) = interpreter.variables.get(synth_id) {
312            match var_val {
313                crate::language::syntax::ast::Value::Map(m) => {
314                    if let Some(chain_v) = m.get("chain") {
315                        if let crate::language::syntax::ast::Value::Array(arr) = chain_v {
316                            synth_effects_vec.extend(arr.clone());
317                        }
318                    } else if let Some(effs) = m.get("effects") {
319                        // Deprecated map-style: normalize into individual effect maps
320                        eprintln!(
321                            "DEPRECATION: synth-level effect param map support is deprecated — use chained params instead."
322                        );
323                        let normalized =
324                            crate::engine::audio::effects::normalize_effects(&Some(effs.clone()));
325                        for (k, v) in normalized.into_iter() {
326                            let mut map = std::collections::HashMap::new();
327                            map.insert(
328                                "type".to_string(),
329                                crate::language::syntax::ast::Value::String(k),
330                            );
331                            for (pk, pv) in v.into_iter() {
332                                map.insert(pk, pv);
333                            }
334                            synth_effects_vec.push(crate::language::syntax::ast::Value::Map(map));
335                        }
336                    }
337                }
338                crate::language::syntax::ast::Value::Statement(stmt_box) => {
339                    if let crate::language::syntax::ast::StatementKind::ArrowCall { .. } =
340                        &stmt_box.kind
341                    {
342                        if let crate::language::syntax::ast::Value::Map(m) = &stmt_box.value {
343                            if let Some(crate::language::syntax::ast::Value::Array(arr)) =
344                                m.get("chain")
345                            {
346                                synth_effects_vec.extend(arr.clone());
347                            }
348                        }
349                    }
350                }
351                _ => {}
352            }
353        }
354
355        // Collect note-level effects from FunctionContext when invoking note/chord/sample
356        let mut note_effects_vec: Vec<crate::language::syntax::ast::Value> = Vec::new();
357        if let Some(crate::language::syntax::ast::Value::String(method_name)) =
358            context.get("method")
359        {
360            let m = method_name.as_str();
361            if m == "note" || m == "chord" || m == "sample" {
362                if let Some(eff_val) = context.get("effects") {
363                    match eff_val {
364                        crate::language::syntax::ast::Value::Array(arr) => {
365                            note_effects_vec.extend(arr.clone());
366                        }
367                        crate::language::syntax::ast::Value::Map(map_v) => {
368                            // normalize map into per-effect entries
369                            let normalized = crate::engine::audio::effects::normalize_effects(
370                                &Some(crate::language::syntax::ast::Value::Map(map_v.clone())),
371                            );
372                            for (k, v) in normalized.into_iter() {
373                                let mut map = std::collections::HashMap::new();
374                                map.insert(
375                                    "type".to_string(),
376                                    crate::language::syntax::ast::Value::String(k),
377                                );
378                                for (pk, pv) in v.into_iter() {
379                                    map.insert(pk, pv);
380                                }
381                                note_effects_vec
382                                    .push(crate::language::syntax::ast::Value::Map(map));
383                            }
384                        }
385                        _ => {}
386                    }
387                }
388            }
389        }
390
391        // As a last resort, if no synth variable chain and context.effects exists (non-note invocation), treat context.effects as synth-level
392        if synth_effects_vec.is_empty() {
393            if let Some(eff_val) = context.get("effects") {
394                if let crate::language::syntax::ast::Value::Array(arr) = eff_val {
395                    synth_effects_vec.extend(arr.clone());
396                } else if let crate::language::syntax::ast::Value::Map(map_v) = eff_val {
397                    let normalized = crate::engine::audio::effects::normalize_effects(&Some(
398                        crate::language::syntax::ast::Value::Map(map_v.clone()),
399                    ));
400                    for (k, v) in normalized.into_iter() {
401                        let mut map = std::collections::HashMap::new();
402                        map.insert(
403                            "type".to_string(),
404                            crate::language::syntax::ast::Value::String(k),
405                        );
406                        for (pk, pv) in v.into_iter() {
407                            map.insert(pk, pv);
408                        }
409                        synth_effects_vec.push(crate::language::syntax::ast::Value::Map(map));
410                    }
411                }
412            }
413        }
414
415        // Merge synth-level then note-level
416        if !synth_effects_vec.is_empty() || !note_effects_vec.is_empty() {
417            let mut merged: Vec<crate::language::syntax::ast::Value> = Vec::new();
418            merged.extend(synth_effects_vec);
419            merged.extend(note_effects_vec);
420            event_effects = Some(crate::language::syntax::ast::Value::Array(merged));
421        }
422
423        // Extract plugin-specific parameters from effects and apply to synth options
424        event_effects = extract_plugin_parameters(&event_effects, &mut synth_def);
425
426        // CRITICAL: Save the updated synth_def back to the interpreter so subsequent notes
427        // on the same synth reuse these parameters (accent, resonance, cutoff, etc.)
428        // This ensures parameters persist across multiple notes in a sequence/group
429        interpreter
430            .events
431            .synths
432            .insert(synth_id.to_string(), synth_def.clone());
433
434        interpreter.events.events.push(AudioEvent::Note {
435            midi,
436            start_time: interpreter.cursor_time,
437            duration,
438            velocity,
439            synth_id: synth_id.to_string(),
440            synth_def,
441            pan,
442            detune,
443            gain,
444            attack: None,
445            release: None,
446            delay_time: None,
447            delay_feedback: None,
448            delay_mix: None,
449            reverb_amount: None,
450            drive_amount: None,
451            drive_color: None,
452            effects: event_effects,
453            use_per_note_automation,
454        });
455        return Ok(());
456    }
457
458    // Handle chords (notes array)
459    if let Some(Value::Array(notes_arr)) = context.get("notes") {
460        let mut midis: Vec<u8> = Vec::new();
461        for n in notes_arr {
462            if let Value::String(s) = n {
463                if let Ok(m) = crate::engine::functions::note::parse_note_to_midi(s) {
464                    midis.push(m);
465                }
466            }
467        }
468
469        if !midis.is_empty() {
470            let duration = if context.duration > 0.0 {
471                context.duration
472            } else if let Some(Value::Number(d)) = context.get("duration") {
473                d / 1000.0
474            } else {
475                0.5
476            };
477
478            let velocity = if let Some(Value::Number(v)) = context.get("velocity") {
479                if *v > 2.0 {
480                    (*v) / 127.0
481                } else if *v > 1.0 {
482                    (*v) / 100.0
483                } else {
484                    *v
485                }
486            } else {
487                0.8
488            };
489
490            let pan = if let Some(Value::Number(p)) = context.get("pan") {
491                *p
492            } else {
493                0.0
494            };
495
496            // Check if this chord should use per-note automation
497            let use_per_note_automation =
498                interpreter.note_automation_templates.contains_key(target);
499
500            let pan = if use_per_note_automation {
501                pan
502            } else {
503                apply_automation_param(interpreter, target, &["pan"], pan, interpreter.cursor_time)
504            };
505
506            let detune = if let Some(Value::Number(d)) = context.get("detune") {
507                *d
508            } else {
509                0.0
510            };
511            let detune = if use_per_note_automation {
512                detune
513            } else {
514                apply_automation_param(
515                    interpreter,
516                    target,
517                    &["pitch", "detune"],
518                    detune,
519                    interpreter.cursor_time,
520                )
521            };
522
523            let spread = if let Some(Value::Number(s)) = context.get("spread") {
524                *s
525            } else {
526                0.0
527            };
528
529            let gain = if let Some(Value::Number(g)) = context.get("gain") {
530                *g
531            } else {
532                1.0
533            };
534            let gain = if use_per_note_automation {
535                gain
536            } else {
537                apply_automation_param(
538                    interpreter,
539                    target,
540                    &["volume", "gain"],
541                    gain,
542                    interpreter.cursor_time,
543                )
544            };
545
546            // optional envelope overrides
547            let attack = context.get("attack").and_then(|v| {
548                if let Value::Number(n) = v {
549                    Some(*n)
550                } else {
551                    None
552                }
553            });
554            let release = context.get("release").and_then(|v| {
555                if let Value::Number(n) = v {
556                    Some(*n)
557                } else {
558                    None
559                }
560            });
561
562            // Effects
563            let delay_time = context.get("delay_time").and_then(|v| {
564                if let Value::Number(n) = v {
565                    Some(*n)
566                } else {
567                    None
568                }
569            });
570            let delay_feedback = context.get("delay_feedback").and_then(|v| {
571                if let Value::Number(n) = v {
572                    Some(*n)
573                } else {
574                    None
575                }
576            });
577            let delay_mix = context.get("delay_mix").and_then(|v| {
578                if let Value::Number(n) = v {
579                    Some(*n)
580                } else {
581                    None
582                }
583            });
584            let reverb_amount = context.get("reverb_amount").and_then(|v| {
585                if let Value::Number(n) = v {
586                    Some(*n)
587                } else {
588                    None
589                }
590            });
591            let drive_amount = context.get("drive_amount").and_then(|v| {
592                if let Value::Number(n) = v {
593                    Some(*n)
594                } else {
595                    None
596                }
597            });
598            let drive_color = context.get("drive_color").and_then(|v| {
599                if let Value::Number(n) = v {
600                    Some(*n)
601                } else {
602                    None
603                }
604            });
605
606            let synth_id = if target.is_empty() { "default" } else { target };
607
608            // Create chord event directly with per-note automation flag
609            let mut synth_def = interpreter
610                .events
611                .get_synth(synth_id)
612                .cloned()
613                .unwrap_or_default();
614            // Determine effects for this chord event by merging synth-level and any chord-level effects
615            let mut event_effects: Option<crate::language::syntax::ast::Value> = None;
616
617            // Collect synth-level effects if present in variable
618            let mut synth_effects_vec: Vec<crate::language::syntax::ast::Value> = Vec::new();
619            if let Some(var_val) = interpreter.variables.get(synth_id) {
620                match var_val {
621                    crate::language::syntax::ast::Value::Map(m) => {
622                        if let Some(chain_v) = m.get("chain") {
623                            if let crate::language::syntax::ast::Value::Array(arr) = chain_v {
624                                synth_effects_vec.extend(arr.clone());
625                            }
626                        } else if let Some(effs) = m.get("effects") {
627                            eprintln!(
628                                "DEPRECATION: synth-level effect param map support is deprecated — use chained params instead."
629                            );
630                            let normalized = crate::engine::audio::effects::normalize_effects(
631                                &Some(effs.clone()),
632                            );
633                            for (k, v) in normalized.into_iter() {
634                                let mut map = std::collections::HashMap::new();
635                                map.insert(
636                                    "type".to_string(),
637                                    crate::language::syntax::ast::Value::String(k),
638                                );
639                                for (pk, pv) in v.into_iter() {
640                                    map.insert(pk, pv);
641                                }
642                                synth_effects_vec
643                                    .push(crate::language::syntax::ast::Value::Map(map));
644                            }
645                        }
646                    }
647                    crate::language::syntax::ast::Value::Statement(stmt_box) => {
648                        if let crate::language::syntax::ast::StatementKind::ArrowCall { .. } =
649                            &stmt_box.kind
650                        {
651                            if let crate::language::syntax::ast::Value::Map(m) = &stmt_box.value {
652                                if let Some(crate::language::syntax::ast::Value::Array(arr)) =
653                                    m.get("chain")
654                                {
655                                    synth_effects_vec.extend(arr.clone());
656                                }
657                            }
658                        }
659                    }
660                    _ => {}
661                }
662            }
663
664            // Collect chord-level (note-level) effects from FunctionContext when invoking chord
665            let mut chord_effects_vec: Vec<crate::language::syntax::ast::Value> = Vec::new();
666            if let Some(crate::language::syntax::ast::Value::String(method_name)) =
667                context.get("method")
668            {
669                let m = method_name.as_str();
670                if m == "chord" {
671                    if let Some(eff_val) = context.get("effects") {
672                        match eff_val {
673                            crate::language::syntax::ast::Value::Array(arr) => {
674                                chord_effects_vec.extend(arr.clone());
675                            }
676                            crate::language::syntax::ast::Value::Map(map_v) => {
677                                let normalized = crate::engine::audio::effects::normalize_effects(
678                                    &Some(crate::language::syntax::ast::Value::Map(map_v.clone())),
679                                );
680                                for (k, v) in normalized.into_iter() {
681                                    let mut map = std::collections::HashMap::new();
682                                    map.insert(
683                                        "type".to_string(),
684                                        crate::language::syntax::ast::Value::String(k),
685                                    );
686                                    for (pk, pv) in v.into_iter() {
687                                        map.insert(pk, pv);
688                                    }
689                                    chord_effects_vec
690                                        .push(crate::language::syntax::ast::Value::Map(map));
691                                }
692                            }
693                            _ => {}
694                        }
695                    }
696                }
697            }
698
699            if !synth_effects_vec.is_empty() || !chord_effects_vec.is_empty() {
700                let mut merged: Vec<crate::language::syntax::ast::Value> = Vec::new();
701                merged.extend(synth_effects_vec);
702                merged.extend(chord_effects_vec);
703                event_effects = Some(crate::language::syntax::ast::Value::Array(merged));
704            }
705
706            // Extract plugin-specific parameters from effects and apply to synth options
707            event_effects = extract_plugin_parameters(&event_effects, &mut synth_def);
708
709            // CRITICAL: Save the updated synth_def back to the interpreter so subsequent chords/notes
710            // on the same synth reuse these parameters (accent, resonance, cutoff, etc.)
711            interpreter
712                .events
713                .synths
714                .insert(synth_id.to_string(), synth_def.clone());
715
716            interpreter.events.events.push(AudioEvent::Chord {
717                midis,
718                start_time: interpreter.cursor_time,
719                duration,
720                velocity,
721                synth_id: synth_id.to_string(),
722                synth_def,
723                pan,
724                detune,
725                spread,
726                gain,
727                attack,
728                release,
729                delay_time,
730                delay_feedback,
731                delay_mix,
732                reverb_amount,
733                drive_amount,
734                drive_color,
735                effects: event_effects,
736                use_per_note_automation: false,
737            });
738            // Apply note-mode/global automation to synth-specific options (cutoff, resonance, etc.)
739            // Collect automated values first to avoid borrowing conflicts
740            let synth_params = [
741                "cutoff",
742                "resonance",
743                "filter_type",
744                "drive",
745                "tone",
746                "decay",
747            ];
748            let mut param_updates = Vec::new();
749
750            // Snapshot current values and compute automations
751            if let Some(synth_def) = interpreter.events.synths.get(synth_id) {
752                for param in &synth_params {
753                    let current_val = synth_def.options.get(*param).copied().unwrap_or(0.0);
754                    let automated_val = apply_automation_param(
755                        interpreter,
756                        synth_id,
757                        &[param],
758                        current_val,
759                        interpreter.cursor_time,
760                    );
761                    if (automated_val - current_val).abs() > 0.0001 {
762                        param_updates.push((param.to_string(), automated_val));
763                    }
764                }
765            }
766
767            // Now apply the updates
768            if let Some(synth_def) = interpreter.events.synths.get_mut(synth_id) {
769                for (param, val) in param_updates {
770                    synth_def.options.insert(param, val);
771                }
772            }
773        }
774    }
775    Ok(())
776}