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

1// Full handler implementation copied from top-level module
2use anyhow::Result;
3use std::collections::HashMap;
4
5use crate::language::syntax::ast::{Statement, StatementKind, Value};
6
7use super::AudioInterpreter;
8
9pub fn handle_let(interpreter: &mut AudioInterpreter, name: &str, value: &Value) -> Result<()> {
10    // Check if this is a synth definition (has waveform parameter OR _plugin_ref)
11    if let Value::Map(map) = value {
12        if map.contains_key("waveform") || map.contains_key("_plugin_ref") {
13            // plugin handling simplified: reuse existing logic
14            let mut is_plugin = false;
15            let mut plugin_author: Option<String> = None;
16            let mut plugin_name: Option<String> = None;
17            let mut plugin_export: Option<String> = None;
18
19            if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
20                let parts: Vec<&str> = plugin_ref.split('.').collect();
21                if parts.len() == 2 {
22                    let (var_name, prop_name) = (parts[0], parts[1]);
23                    if let Some(var_value) = interpreter.variables.get(var_name) {
24                        if let Value::Map(var_map) = var_value {
25                            if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
26                                if resolved_plugin.starts_with("plugin:") {
27                                    let ref_parts: Vec<&str> =
28                                        resolved_plugin["plugin:".len()..].split(':').collect();
29                                    if ref_parts.len() == 2 {
30                                        let full_plugin_name = ref_parts[0];
31                                        let export_name = ref_parts[1];
32                                        let plugin_parts: Vec<&str> =
33                                            full_plugin_name.split('.').collect();
34                                        if plugin_parts.len() == 2 {
35                                            plugin_author = Some(plugin_parts[0].to_string());
36                                            plugin_name = Some(plugin_parts[1].to_string());
37                                            plugin_export = Some(export_name.to_string());
38                                            is_plugin = true;
39                                        }
40                                    }
41                                }
42                            } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
43                                if let (
44                                    Some(Value::String(author)),
45                                    Some(Value::String(name)),
46                                    Some(Value::String(export)),
47                                ) = (
48                                    export_map.get("_plugin_author"),
49                                    export_map.get("_plugin_name"),
50                                    export_map.get("_export_name"),
51                                ) {
52                                    plugin_author = Some(author.clone());
53                                    plugin_name = Some(name.clone());
54                                    plugin_export = Some(export.clone());
55                                    is_plugin = true;
56                                }
57                            }
58                        }
59                    }
60                }
61            }
62
63            let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
64            let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
65            let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
66            let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
67            let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
68
69            let synth_type = if let Some(Value::String(t)) = map.get("type") {
70                let clean = t.trim_matches('"').trim_matches('\'');
71                if clean.is_empty() || clean == "synth" {
72                    None
73                } else {
74                    Some(clean.to_string())
75                }
76            } else {
77                None
78            };
79
80            let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
81                crate::engine::audio::events::extract_filters(filters_arr)
82            } else {
83                Vec::new()
84            };
85
86            // Extract LFO configuration if present
87            let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
88                use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
89
90                // Parse rate (Hz or tempo-synced like "1/4")
91                let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
92                    n.to_string()
93                } else if let Some(Value::String(s)) = lfo_map.get("rate") {
94                    s.clone()
95                } else {
96                    "5.0".to_string() // Default rate
97                };
98                let rate = LfoRate::from_value(&rate_str);
99
100                // Parse depth (0-1)
101                let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
102                    (*n).clamp(0.0, 1.0)
103                } else {
104                    0.5 // Default depth
105                };
106
107                // Parse waveform (sine, triangle, square, saw)
108                let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
109                    s.clone()
110                } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
111                    s.clone()
112                } else {
113                    "sine".to_string() // Default waveform
114                };
115                let waveform = LfoWaveform::from_str(&waveform_str);
116
117                // Parse target (volume, pitch, filter, pan)
118                let target = if let Some(Value::String(s)) = lfo_map.get("target") {
119                    LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
120                } else {
121                    LfoTarget::Volume // Default target
122                };
123
124                // Parse initial phase (0-1)
125                let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
126                    (*n).fract().abs() // Ensure 0-1 range
127                } else {
128                    0.0 // Default phase
129                };
130
131                Some(LfoParams {
132                    rate,
133                    depth,
134                    waveform,
135                    target,
136                    phase,
137                })
138            } else {
139                None
140            };
141
142            let mut options = std::collections::HashMap::new();
143            let reserved_keys = if is_plugin {
144                vec![
145                    "attack",
146                    "decay",
147                    "sustain",
148                    "release",
149                    "type",
150                    "filters",
151                    "_plugin_ref",
152                    "lfo",
153                ]
154            } else {
155                vec![
156                    "waveform",
157                    "attack",
158                    "decay",
159                    "sustain",
160                    "release",
161                    "type",
162                    "filters",
163                    "_plugin_ref",
164                    "lfo",
165                ]
166            };
167
168            for (key, val) in map.iter() {
169                if !reserved_keys.contains(&key.as_str()) {
170                    match val {
171                        Value::Number(n) => {
172                            options.insert(key.clone(), *n);
173                        }
174                        Value::String(s) => {
175                            if is_plugin && key == "waveform" {
176                                let waveform_id = match s
177                                    .trim_matches('"')
178                                    .trim_matches('\'')
179                                    .to_lowercase()
180                                    .as_str()
181                                {
182                                    "sine" => 0.0,
183                                    "saw" => 1.0,
184                                    "square" => 2.0,
185                                    "triangle" => 3.0,
186                                    _ => 1.0,
187                                };
188                                options.insert(key.clone(), waveform_id);
189                            }
190                        }
191                        _ => {}
192                    }
193                }
194            }
195
196            if is_plugin && map.contains_key("decay") {
197                options.insert("decay".to_string(), decay);
198            }
199
200            let final_waveform = if is_plugin {
201                "plugin".to_string()
202            } else {
203                waveform
204            };
205
206            let synth_def = crate::engine::audio::events::SynthDefinition {
207                waveform: final_waveform,
208                attack,
209                decay,
210                sustain,
211                release,
212                synth_type,
213                filters,
214                options,
215                plugin_author,
216                plugin_name,
217                plugin_export,
218                lfo,
219            };
220
221            interpreter.events.add_synth(name.to_string(), synth_def);
222        }
223    }
224
225    interpreter
226        .variables
227        .insert(name.to_string(), value.clone());
228    Ok(())
229}
230
231pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str) -> Result<()> {
232    // ============================================================================
233    // CALL EXECUTION (Sequential)
234    // ============================================================================
235    // Call accepts:
236    // - Patterns (defined with `pattern name with trigger = "x---"`)
237    // - Groups (defined with `group name:`)
238    // ============================================================================
239
240    // Check inline pattern or pattern variable or group call
241    // Clone the variable value first to avoid holding an immutable borrow across a mutable call
242    if let Some(pattern_value) = interpreter.variables.get(name).cloned() {
243        if let Value::Statement(stmt_box) = pattern_value {
244            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
245                if let Some(tgt) = target.as_ref() {
246                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
247                    if let Some(pat) = pattern_str {
248                        interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
249                        return Ok(());
250                    }
251                }
252            }
253        }
254    }
255
256    if let Some(body) = interpreter.groups.get(name).cloned() {
257        super::collector::collect_events(interpreter, &body)?;
258    } else {
259        println!("⚠️  Warning: Group or pattern '{}' not found", name);
260    }
261
262    Ok(())
263}
264
265pub fn execute_print(interpreter: &AudioInterpreter, value: &Value) -> Result<()> {
266    let message = match value {
267        Value::String(s) => {
268            if s.contains('{') && s.contains('}') {
269                interpreter.interpolate_string(s)
270            } else {
271                s.clone()
272            }
273        }
274        Value::Identifier(id) => {
275            // Resolve variable from interpreter.variables
276            if let Some(v) = interpreter.variables.get(id) {
277                match v {
278                    Value::String(s) => s.clone(),
279                    Value::Number(n) => n.to_string(),
280                    Value::Boolean(b) => b.to_string(),
281                    Value::Array(arr) => format!("{:?}", arr),
282                    Value::Map(map) => format!("{:?}", map),
283                    _ => format!("{:?}", v),
284                }
285            } else {
286                format!("Identifier(\"{}\")", id)
287            }
288        }
289        Value::Number(n) => n.to_string(),
290        Value::Boolean(b) => b.to_string(),
291        Value::Array(arr) => format!("{:?}", arr),
292        Value::Map(map) => format!("{:?}", map),
293        _ => format!("{:?}", value),
294    };
295
296    println!("💬 {}", message);
297    Ok(())
298}
299
300pub fn execute_if(
301    interpreter: &mut AudioInterpreter,
302    condition: &Value,
303    body: &[Statement],
304    else_body: &Option<Vec<Statement>>,
305) -> Result<()> {
306    let condition_result = interpreter.evaluate_condition(condition)?;
307
308    if condition_result {
309        super::collector::collect_events(interpreter, body)?;
310    } else if let Some(else_stmts) = else_body {
311        super::collector::collect_events(interpreter, else_stmts)?;
312    }
313
314    Ok(())
315}
316
317pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
318    let handlers = interpreter.event_registry.get_handlers_matching(event_name);
319
320    for (index, handler) in handlers.iter().enumerate() {
321        if handler.once
322            && !interpreter
323                .event_registry
324                .should_execute_once(event_name, index)
325        {
326            continue;
327        }
328
329        let body_clone = handler.body.clone();
330        super::collector::collect_events(interpreter, &body_clone)?;
331    }
332
333    Ok(())
334}
335
336pub fn handle_assign(
337    interpreter: &mut AudioInterpreter,
338    target: &str,
339    property: &str,
340    value: &Value,
341) -> Result<()> {
342    if let Some(var) = interpreter.variables.get_mut(target) {
343        if let Value::Map(map) = var {
344            map.insert(property.to_string(), value.clone());
345
346            if interpreter.events.synths.contains_key(target) {
347                let map_clone = map.clone();
348                let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
349                interpreter
350                    .events
351                    .synths
352                    .insert(target.to_string(), updated_def);
353            }
354        } else {
355            return Err(anyhow::anyhow!(
356                "Cannot assign property '{}' to non-map variable '{}'",
357                property,
358                target
359            ));
360        }
361    } else {
362        return Err(anyhow::anyhow!("Variable '{}' not found", target));
363    }
364
365    Ok(())
366}
367
368pub fn extract_synth_def_from_map(
369    _interpreter: &AudioInterpreter,
370    map: &HashMap<String, Value>,
371) -> Result<crate::engine::audio::events::SynthDefinition> {
372    use crate::engine::audio::events::extract_filters;
373    use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
374
375    let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
376    let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
377    let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
378    let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
379    let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
380
381    let synth_type = if let Some(Value::String(t)) = map.get("type") {
382        let clean = t.trim_matches('"').trim_matches('\'');
383        if clean.is_empty() || clean == "synth" {
384            None
385        } else {
386            Some(clean.to_string())
387        }
388    } else {
389        None
390    };
391
392    let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
393        extract_filters(filters_arr)
394    } else {
395        Vec::new()
396    };
397
398    let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
399        Some(s.clone())
400    } else {
401        None
402    };
403    let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
404        Some(s.clone())
405    } else {
406        None
407    };
408    let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
409        Some(s.clone())
410    } else {
411        None
412    };
413
414    // Extract LFO configuration if present
415    let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
416        // Parse rate (Hz or tempo-synced like "1/4")
417        let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
418            n.to_string()
419        } else if let Some(Value::String(s)) = lfo_map.get("rate") {
420            s.clone()
421        } else {
422            "5.0".to_string() // Default rate
423        };
424        let rate = LfoRate::from_value(&rate_str);
425
426        // Parse depth (0-1)
427        let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
428            (*n).clamp(0.0, 1.0)
429        } else {
430            0.5 // Default depth
431        };
432
433        // Parse waveform (sine, triangle, square, saw)
434        let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
435            s.clone()
436        } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
437            s.clone()
438        } else {
439            "sine".to_string() // Default waveform
440        };
441        let waveform = LfoWaveform::from_str(&waveform_str);
442
443        // Parse target (volume, pitch, filter, pan)
444        let target = if let Some(Value::String(s)) = lfo_map.get("target") {
445            LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
446        } else {
447            LfoTarget::Volume // Default target
448        };
449
450        // Parse initial phase (0-1)
451        let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
452            (*n).fract().abs() // Ensure 0-1 range
453        } else {
454            0.0 // Default phase
455        };
456
457        Some(LfoParams {
458            rate,
459            depth,
460            waveform,
461            target,
462            phase,
463        })
464    } else {
465        None
466    };
467
468    let mut options = HashMap::new();
469    for (key, val) in map.iter() {
470        if ![
471            "waveform",
472            "attack",
473            "decay",
474            "sustain",
475            "release",
476            "type",
477            "filters",
478            "plugin_author",
479            "plugin_name",
480            "plugin_export",
481            "lfo",
482        ]
483        .contains(&key.as_str())
484        {
485            if let Value::Number(n) = val {
486                options.insert(key.clone(), *n);
487            } else if let Value::String(s) = val {
488                if key == "waveform" || key.starts_with("_") {
489                    continue;
490                }
491                if let Ok(n) = s.parse::<f32>() {
492                    options.insert(key.clone(), n);
493                }
494            }
495        }
496    }
497
498    Ok(crate::engine::audio::events::SynthDefinition {
499        waveform,
500        attack,
501        decay,
502        sustain,
503        release,
504        synth_type,
505        filters,
506        options,
507        plugin_author,
508        plugin_name,
509        plugin_export,
510        lfo,
511    })
512}
513
514pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
515    use std::path::Path;
516
517    let path = Path::new(source);
518    // Determine extension
519    if let Some(ext) = path
520        .extension()
521        .and_then(|s| s.to_str())
522        .map(|s| s.to_lowercase())
523    {
524        match ext.as_str() {
525            "mid" | "midi" => {
526                use crate::engine::audio::midi::load_midi_file;
527                let midi_data = load_midi_file(path)?;
528                interpreter.variables.insert(alias.to_string(), midi_data);
529                // MIDI file loaded (silent)
530                Ok(())
531            }
532            "wav" | "flac" | "mp3" | "ogg" => {
533                // For native/CLI builds, register sample via the samples subsystem.
534                #[cfg(feature = "cli")]
535                {
536                    use crate::engine::audio::samples;
537                    let registered = samples::register_sample_from_path(path)?;
538                    // Record the sample URI under the alias variable as a string (consistent with triggers)
539                    interpreter
540                        .variables
541                        .insert(alias.to_string(), Value::String(registered.clone()));
542                    // Sample file loaded (silent)
543                    return Ok(());
544                }
545
546                // For non-CLI builds (WASM/plugins), fallback to storing the original path as a string.
547                #[cfg(not(feature = "cli"))]
548                {
549                    interpreter
550                        .variables
551                        .insert(alias.to_string(), Value::String(source.to_string()));
552                    return Ok(());
553                }
554            }
555            _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
556        }
557    } else {
558        Err(anyhow::anyhow!(
559            "Cannot determine file extension for {}",
560            source
561        ))
562    }
563}
564
565pub fn handle_bind(
566    interpreter: &mut AudioInterpreter,
567    source: &str,
568    target: &str,
569    options: &Value,
570) -> Result<()> {
571    use std::collections::HashMap as StdHashMap;
572
573    // Support bindings that reference runtime MIDI device mappings like:
574    //   bind myKickPattern -> mapping.out.myDeviceA with { port: 1, channel: 10 }
575    //   bind mapping.in.myDeviceB with { port: 2, channel: 10 } -> mySynth
576    // When a 'mapping.*' path is used, register a lightweight mapping entry in the
577    // interpreter variables and expose convenience variables:
578    //   mapping.<in|out>.<device>.<noteOn|noteOff|rest>
579    if source.starts_with("mapping.") || target.starts_with("mapping.") {
580        // extract options if present
581        let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
582            m.clone()
583        } else {
584            StdHashMap::new()
585        };
586
587        // Helper function to create mapping variables and bookkeeping
588        fn create_and_insert(
589            path: &str,
590            opts_map: &StdHashMap<String, Value>,
591            interpreter: &mut AudioInterpreter,
592        ) -> Option<(String, String)> {
593            let parts: Vec<&str> = path.split('.').collect();
594            if parts.len() >= 3 {
595                let direction = parts[1]; // "in" or "out"
596                let device = parts[2];
597
598                let mut map = StdHashMap::new();
599                map.insert(
600                    "_type".to_string(),
601                    Value::String("midi_mapping".to_string()),
602                );
603                map.insert(
604                    "direction".to_string(),
605                    Value::String(direction.to_string()),
606                );
607                map.insert("device".to_string(), Value::String(device.to_string()));
608
609                // merge provided options
610                for (k, v) in opts_map.iter() {
611                    map.insert(k.clone(), v.clone());
612                }
613
614                interpreter
615                    .variables
616                    .insert(path.to_string(), Value::Map(map.clone()));
617
618                // Expose event variables for convenience
619                let note_on = format!("mapping.{}.{}.noteOn", direction, device);
620                let note_off = format!("mapping.{}.{}.noteOff", direction, device);
621                let rest = format!("mapping.{}.{}.rest", direction, device);
622                interpreter
623                    .variables
624                    .insert(note_on.clone(), Value::String(note_on.clone()));
625                interpreter
626                    .variables
627                    .insert(note_off.clone(), Value::String(note_off.clone()));
628                interpreter
629                    .variables
630                    .insert(rest.clone(), Value::String(rest.clone()));
631
632                return Some((direction.to_string(), device.to_string()));
633            }
634            None
635        }
636
637        // If source is mapping.* (incoming mapping binds to target instrument)
638        if source.starts_with("mapping.") {
639            if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
640                // Record association when target is not also mapping.*
641                if !target.starts_with("mapping.") {
642                    let mut bmap = StdHashMap::new();
643                    bmap.insert("instrument".to_string(), Value::String(target.to_string()));
644                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
645                    bmap.insert("device".to_string(), Value::String(device.clone()));
646                    for (k, v) in opts_map.iter() {
647                        bmap.insert(k.clone(), v.clone());
648                    }
649                    interpreter
650                        .variables
651                        .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
652
653                    // If we have a midi_manager, try to open the input port (for incoming mappings)
654                    #[cfg(feature = "cli")]
655                    if let Some(manager) = &mut interpreter.midi_manager {
656                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
657                            let idx = *port_num as usize;
658                            if let Ok(mut mgr) = manager.lock() {
659                                // use device as name for identification
660                                let _ = mgr.open_input_by_index(idx, &device);
661                            }
662                        }
663                    }
664                }
665            }
666        }
667
668        // If target is mapping.* (binding a sequence/instrument to an external device)
669        if target.starts_with("mapping.") {
670            if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
671                if !source.starts_with("mapping.") {
672                    let mut bmap = StdHashMap::new();
673                    bmap.insert("source".to_string(), Value::String(source.to_string()));
674                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
675                    bmap.insert("device".to_string(), Value::String(device.clone()));
676                    for (k, v) in opts_map.iter() {
677                        bmap.insert(k.clone(), v.clone());
678                    }
679                    interpreter
680                        .variables
681                        .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
682
683                    // If we have a midi_manager, try to open the output port (for outgoing mappings)
684                    #[cfg(feature = "cli")]
685                    if let Some(manager) = &mut interpreter.midi_manager {
686                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
687                            let idx = *port_num as usize;
688                            if let Ok(mut mgr) = manager.lock() {
689                                let _ = mgr.open_output_by_name(&device, idx);
690                            }
691                        }
692                    }
693                }
694            }
695        }
696
697        // Nothing more to schedule here at audio event level; actual MIDI I/O handlers
698        // will be responsible for reacting to incoming messages and emitting events into
699        // the interpreter event registry, and for flushing outgoing bound sequences to
700        // MIDI device ports when appropriate.
701        return Ok(());
702    }
703
704    // Fallback: existing behaviour (binding MIDI file data to a synth)
705    let midi_data = interpreter
706        .variables
707        .get(source)
708        .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
709        .clone();
710
711    if let Value::Map(midi_map) = &midi_data {
712        let notes = midi_map
713            .get("notes")
714            .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
715
716        if let Value::Array(notes_array) = notes {
717            let _synth_def = interpreter
718                .events
719                .synths
720                .get(target)
721                .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
722                .clone();
723
724            let default_velocity = 100;
725            let mut velocity = default_velocity;
726
727            if let Value::Map(opts) = options {
728                if let Some(Value::Number(v)) = opts.get("velocity") {
729                    velocity = *v as u8;
730                }
731            }
732
733            // Determine MIDI file BPM (if present) so we can rescale times to interpreter BPM
734            // Default to interpreter.bpm when the MIDI file has no BPM metadata
735            let midi_bpm =
736                crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
737
738            for note_val in notes_array {
739                if let Value::Map(note_map) = note_val {
740                    let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
741                    let note =
742                        crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
743                    let note_velocity = crate::engine::audio::events::extract_number(
744                        note_map,
745                        "velocity",
746                        velocity as f32,
747                    ) as u8;
748                    // Duration may be present (ms) from MIDI loader; fallback to 500 ms
749                    let duration_ms =
750                        crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
751
752                    use crate::engine::audio::events::AudioEvent;
753                    let synth_def = interpreter
754                        .events
755                        .get_synth(target)
756                        .cloned()
757                        .unwrap_or_default();
758                    // Rescale times according to interpreter BPM vs MIDI file BPM.
759                    // If midi_bpm == interpreter.bpm this is a no-op. We compute factor = midi_bpm / interpreter.bpm
760                    let interp_bpm = interpreter.bpm;
761                    let factor = if interp_bpm > 0.0 {
762                        midi_bpm / interp_bpm
763                    } else {
764                        1.0
765                    };
766
767                    let start_time_s = (time / 1000.0) * factor;
768                    let duration_s = (duration_ms / 1000.0) * factor;
769
770                    let event = AudioEvent::Note {
771                        midi: note,
772                        start_time: start_time_s,
773                        duration: duration_s,
774                        velocity: note_velocity as f32,
775                        synth_id: target.to_string(),
776                        synth_def,
777                        pan: 0.0,
778                        detune: 0.0,
779                        gain: 1.0,
780                        attack: None,
781                        release: None,
782                        delay_time: None,
783                        delay_feedback: None,
784                        delay_mix: None,
785                        reverb_amount: None,
786                        drive_amount: None,
787                        drive_color: None,
788                        use_per_note_automation: false,
789                    };
790
791                    // Diagnostic: log each scheduled note from bind (midi, time ms, start_time sec)
792                    // bound note scheduled
793                    interpreter.events.events.push(event);
794                }
795            }
796
797            // Bound notes from source to target
798        }
799    }
800
801    Ok(())
802}
803
804#[cfg(feature = "cli")]
805pub fn handle_use_plugin(
806    interpreter: &mut AudioInterpreter,
807    author: &str,
808    name: &str,
809    alias: &str,
810) -> Result<()> {
811    use crate::engine::plugin::loader::load_plugin;
812
813    match load_plugin(author, name) {
814        Ok((info, _wasm_bytes)) => {
815            let mut plugin_map = HashMap::new();
816            plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
817            plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
818            plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
819
820            if let Some(version) = &info.version {
821                plugin_map.insert("_version".to_string(), Value::String(version.clone()));
822            }
823
824            for export in &info.exports {
825                let mut export_map = HashMap::new();
826                export_map.insert(
827                    "_plugin_author".to_string(),
828                    Value::String(info.author.clone()),
829                );
830                export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
831                export_map.insert(
832                    "_export_name".to_string(),
833                    Value::String(export.name.clone()),
834                );
835                export_map.insert(
836                    "_export_kind".to_string(),
837                    Value::String(export.kind.clone()),
838                );
839
840                plugin_map.insert(export.name.clone(), Value::Map(export_map));
841            }
842
843            interpreter
844                .variables
845                .insert(alias.to_string(), Value::Map(plugin_map));
846        }
847        Err(e) => {
848            eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
849            return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
850        }
851    }
852
853    Ok(())
854}
855
856#[cfg(not(feature = "cli"))]
857pub fn handle_use_plugin(
858    interpreter: &mut AudioInterpreter,
859    author: &str,
860    name: &str,
861    alias: &str,
862) -> Result<()> {
863    // Plugin loading not supported in this build (WASM/plugin builds). Insert a minimal placeholder so scripts can still reference the alias.
864    let mut plugin_map = HashMap::new();
865    plugin_map.insert(
866        "_type".to_string(),
867        Value::String("plugin_stub".to_string()),
868    );
869    plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
870    plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
871    interpreter
872        .variables
873        .insert(alias.to_string(), Value::Map(plugin_map));
874    Ok(())
875}
876
877pub fn handle_bank(
878    interpreter: &mut AudioInterpreter,
879    name: &str,
880    alias: &Option<String>,
881) -> Result<()> {
882    let target_alias = alias
883        .clone()
884        .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
885
886    if let Some(existing_value) = interpreter.variables.get(name) {
887        interpreter
888            .variables
889            .insert(target_alias.clone(), existing_value.clone());
890    } else {
891        #[cfg(feature = "wasm")]
892        {
893            use crate::web::registry::banks::REGISTERED_BANKS;
894            REGISTERED_BANKS.with(|banks| {
895                for bank in banks.borrow().iter() {
896                    if bank.full_name == *name {
897                        if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
898                            interpreter
899                                .variables
900                                .insert(target_alias.clone(), Value::Map(bank_map.clone()));
901                        }
902                    }
903                }
904            });
905        }
906
907        #[cfg(not(feature = "wasm"))]
908        {
909            if let Ok(current_dir) = std::env::current_dir() {
910                match interpreter.banks.register_bank(
911                    target_alias.clone(),
912                    &name,
913                    &current_dir,
914                    &current_dir,
915                ) {
916                    Ok(_) => {
917                        let mut bank_map = HashMap::new();
918                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
919                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
920                        interpreter
921                            .variables
922                            .insert(target_alias.clone(), Value::Map(bank_map));
923                    }
924                    Err(e) => {
925                        eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
926                        let mut bank_map = HashMap::new();
927                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
928                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
929                        interpreter
930                            .variables
931                            .insert(target_alias.clone(), Value::Map(bank_map));
932                    }
933                }
934            } else {
935                let mut bank_map = HashMap::new();
936                bank_map.insert("_name".to_string(), Value::String(name.to_string()));
937                bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
938                interpreter
939                    .variables
940                    .insert(target_alias.clone(), Value::Map(bank_map));
941                eprintln!(
942                    "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
943                    name
944                );
945            }
946        }
947    }
948
949    Ok(())
950}
951
952pub fn handle_trigger(interpreter: &mut AudioInterpreter, entity: &str) -> Result<()> {
953    let resolved_entity = if entity.starts_with('.') {
954        &entity[1..]
955    } else {
956        entity
957    };
958
959    if resolved_entity.contains('.') {
960        let parts: Vec<&str> = resolved_entity.split('.').collect();
961        if parts.len() == 2 {
962            let (var_name, property) = (parts[0], parts[1]);
963
964            if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
965                if let Some(Value::String(sample_uri)) = map.get(property) {
966                    let uri = sample_uri.trim_matches('"').trim_matches('\'');
967                    interpreter
968                        .events
969                        .add_sample_event(uri, interpreter.cursor_time, 1.0);
970                    let beat_duration = interpreter.beat_duration();
971                    interpreter.cursor_time += beat_duration;
972                } else {
973                    #[cfg(not(feature = "wasm"))]
974                    {
975                        // First try to produce an internal devalang://bank URI (preferred, supports lazy loading)
976                        let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
977                        if resolved_uri != resolved_entity {
978                            interpreter.events.add_sample_event(
979                                &resolved_uri,
980                                interpreter.cursor_time,
981                                1.0,
982                            );
983                            let beat_duration = interpreter.beat_duration();
984                            interpreter.cursor_time += beat_duration;
985                        } else if let Some(pathbuf) =
986                            interpreter.banks.resolve_trigger(var_name, property)
987                        {
988                            if let Some(path_str) = pathbuf.to_str() {
989                                interpreter.events.add_sample_event(
990                                    path_str,
991                                    interpreter.cursor_time,
992                                    1.0,
993                                );
994                                let beat_duration = interpreter.beat_duration();
995                                interpreter.cursor_time += beat_duration;
996                            } else {
997                                println!(
998                                    "⚠️ Resolution failed for {}.{} (invalid path)",
999                                    var_name, property
1000                                );
1001                            }
1002                        } else {
1003                            println!("⚠️ No path found for {} via BankRegistry", resolved_entity);
1004                        }
1005                    }
1006                }
1007            }
1008        }
1009    } else {
1010        if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
1011            let uri = sample_uri.trim_matches('"').trim_matches('\'');
1012            interpreter
1013                .events
1014                .add_sample_event(uri, interpreter.cursor_time, 1.0);
1015            let beat_duration = interpreter.beat_duration();
1016            interpreter.cursor_time += beat_duration;
1017        }
1018    }
1019
1020    // Note: do not call interpreter.render_audio() here - rendering is handled by the build pipeline.
1021    // Trigger queued for rendering (events collected)
1022
1023    Ok(())
1024}
1025
1026pub fn extract_pattern_data(
1027    _interpreter: &AudioInterpreter,
1028    value: &Value,
1029) -> (Option<String>, Option<HashMap<String, f32>>) {
1030    match value {
1031        Value::String(pattern) => (Some(pattern.clone()), None),
1032        Value::Map(map) => {
1033            let pattern = map.get("pattern").and_then(|v| {
1034                if let Value::String(s) = v {
1035                    Some(s.clone())
1036                } else {
1037                    None
1038                }
1039            });
1040
1041            let mut options = HashMap::new();
1042            for (key, val) in map.iter() {
1043                if key != "pattern" {
1044                    if let Value::Number(num) = val {
1045                        options.insert(key.clone(), *num);
1046                    }
1047                }
1048            }
1049
1050            let opts = if options.is_empty() {
1051                None
1052            } else {
1053                Some(options)
1054            };
1055            (pattern, opts)
1056        }
1057        _ => (None, None),
1058    }
1059}
1060
1061pub fn execute_pattern(
1062    interpreter: &mut AudioInterpreter,
1063    target: &str,
1064    pattern: &str,
1065    options: Option<HashMap<String, f32>>,
1066) -> Result<()> {
1067    use crate::engine::audio::events::AudioEvent;
1068
1069    let swing = options
1070        .as_ref()
1071        .and_then(|o| o.get("swing").copied())
1072        .unwrap_or(0.0);
1073    let humanize = options
1074        .as_ref()
1075        .and_then(|o| o.get("humanize").copied())
1076        .unwrap_or(0.0);
1077    let velocity_mult = options
1078        .as_ref()
1079        .and_then(|o| o.get("velocity").copied())
1080        .unwrap_or(1.0);
1081    let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
1082
1083    let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
1084
1085    let resolved_uri = resolve_sample_uri(interpreter, target);
1086
1087    let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
1088    let step_count = pattern_chars.len() as f32;
1089    if step_count == 0.0 {
1090        return Ok(());
1091    }
1092
1093    let bar_duration = (60.0 / effective_bpm) * 4.0;
1094    let step_duration = bar_duration / step_count;
1095
1096    for (i, &ch) in pattern_chars.iter().enumerate() {
1097        if ch == 'x' || ch == 'X' {
1098            let mut time = interpreter.cursor_time + (i as f32 * step_duration);
1099            if swing > 0.0 && i % 2 == 1 {
1100                time += step_duration * swing;
1101            }
1102
1103            #[cfg(any(feature = "cli", feature = "wasm"))]
1104            if humanize > 0.0 {
1105                use rand::Rng;
1106                let mut rng = rand::thread_rng();
1107                let offset = rng.gen_range(-humanize..humanize);
1108                time += offset;
1109            }
1110
1111            let event = AudioEvent::Sample {
1112                uri: resolved_uri.clone(),
1113                start_time: time,
1114                velocity: velocity_mult, // Already in 0-1 range, not MIDI 0-127
1115            };
1116            interpreter.events.events.push(event);
1117        }
1118    }
1119
1120    interpreter.cursor_time += bar_duration;
1121    Ok(())
1122}
1123
1124pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
1125    if let Some(dot_pos) = target.find('.') {
1126        let bank_alias = &target[..dot_pos];
1127        let trigger_name = &target[dot_pos + 1..];
1128        if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
1129            if let Some(Value::String(bank_name)) = bank_map.get("_name") {
1130                return format!("devalang://bank/{}/{}", bank_name, trigger_name);
1131            }
1132        }
1133    }
1134    target.to_string()
1135}