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(orig_map) = value {
12        // Clone la map pour modification
13        let mut map = orig_map.clone();
14
15        // Normalize older key `synth_type` into `type` so downstream logic reads the same key.
16        // Prefer chained synth_type over default "synth" placeholder when present.
17        if let Some(synth_val) = map.get("synth_type").cloned() {
18            let should_replace = match map.get("type") {
19                Some(Value::String(s)) => {
20                    let clean = s.trim_matches('"').trim_matches('\'');
21                    clean == "synth" || clean.is_empty()
22                }
23                Some(Value::Identifier(id)) => {
24                    let clean = id.trim_matches('"').trim_matches('\'');
25                    clean == "synth" || clean.is_empty()
26                }
27                None => true,
28                _ => false,
29            };
30            if should_replace {
31                map.insert("type".to_string(), synth_val.clone());
32                map.remove("synth_type");
33            } else {
34                // If we didn't replace, still keep synth_type (back-compat) but don't remove it
35            }
36        }
37
38        // Merge chained parameter sub-maps (e.g. "params", "adsr", "envelope")
39        let chain_keys = ["params", "adsr", "envelope"];
40        for key in &chain_keys {
41            if let Some(Value::Map(submap)) = map.get(*key) {
42                // Temporary buffer to avoid mutable/immutable borrow conflict
43                let to_insert: Vec<(String, Value)> =
44                    submap.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
45                for (k, v) in to_insert {
46                    map.insert(k, v);
47                }
48                map.remove(*key);
49            }
50        }
51
52        // Ensure default properties are present on instantiated objects (synths/plugins)
53        // so that dotted access like `mySynth.volume` resolves to a concrete Value.
54        // These defaults are safe no-ops for objects that don't use them.
55        map.entry("volume".to_string())
56            .or_insert(Value::Number(1.0));
57        map.entry("gain".to_string()).or_insert(Value::Number(1.0));
58        map.entry("pan".to_string()).or_insert(Value::Number(0.0));
59        map.entry("detune".to_string())
60            .or_insert(Value::Number(0.0));
61        // Ensure a visible type key exists for prints; prefer existing value if present
62        map.entry("type".to_string())
63            .or_insert(Value::String("synth".to_string()));
64
65        // Ensure defaults for synth-like objects
66        crate::utils::props::ensure_default_properties(&mut map, Some("synth"));
67
68        if map.contains_key("waveform") || map.contains_key("_plugin_ref") {
69            // plugin handling simplified: reuse existing logic
70            let mut is_plugin = false;
71            let mut plugin_author: Option<String> = None;
72            let mut plugin_name: Option<String> = None;
73            let mut plugin_export: Option<String> = None;
74
75            if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
76                let parts: Vec<&str> = plugin_ref.split('.').collect();
77                if parts.len() == 2 {
78                    let (var_name, prop_name) = (parts[0], parts[1]);
79                    if let Some(var_value) = interpreter.variables.get(var_name) {
80                        if let Value::Map(var_map) = var_value {
81                            if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
82                                if resolved_plugin.starts_with("plugin:") {
83                                    let ref_parts: Vec<&str> =
84                                        resolved_plugin["plugin:".len()..].split(':').collect();
85                                    if ref_parts.len() == 2 {
86                                        let full_plugin_name = ref_parts[0];
87                                        let export_name = ref_parts[1];
88                                        let plugin_parts: Vec<&str> =
89                                            full_plugin_name.split('.').collect();
90                                        if plugin_parts.len() == 2 {
91                                            plugin_author = Some(plugin_parts[0].to_string());
92                                            plugin_name = Some(plugin_parts[1].to_string());
93                                            plugin_export = Some(export_name.to_string());
94                                            is_plugin = true;
95                                        }
96                                    }
97                                }
98                            } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
99                                if let (
100                                    Some(Value::String(author)),
101                                    Some(Value::String(name)),
102                                    Some(Value::String(export)),
103                                ) = (
104                                    export_map.get("_plugin_author"),
105                                    export_map.get("_plugin_name"),
106                                    export_map.get("_export_name"),
107                                ) {
108                                    plugin_author = Some(author.clone());
109                                    plugin_name = Some(name.clone());
110                                    plugin_export = Some(export.clone());
111                                    is_plugin = true;
112                                }
113                            }
114                        }
115                    }
116                }
117            }
118
119            let waveform = crate::engine::audio::events::extract_string(&map, "waveform", "sine");
120            let attack = crate::engine::audio::events::extract_number(&map, "attack", 0.01);
121            let decay = crate::engine::audio::events::extract_number(&map, "decay", 0.1);
122            let sustain = crate::engine::audio::events::extract_number(&map, "sustain", 0.7);
123            let release = crate::engine::audio::events::extract_number(&map, "release", 0.2);
124
125            // Accept both String and Identifier for type (parser may emit Identifier for bare words)
126            let synth_type = if let Some(v) = map.get("type") {
127                match v {
128                    Value::String(t) => {
129                        let clean = t.trim_matches('"').trim_matches('\'');
130                        if clean.is_empty() || clean == "synth" {
131                            None
132                        } else {
133                            Some(clean.to_string())
134                        }
135                    }
136                    Value::Identifier(id) => {
137                        let clean = id.trim_matches('"').trim_matches('\'');
138                        if clean.is_empty() || clean == "synth" {
139                            None
140                        } else {
141                            Some(clean.to_string())
142                        }
143                    }
144                    _ => None,
145                }
146            } else {
147                None
148            };
149
150            let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
151                crate::engine::audio::events::extract_filters(filters_arr)
152            } else {
153                Vec::new()
154            };
155
156            // Extract LFO configuration if present
157            let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
158                use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
159
160                // Parse rate (Hz or tempo-synced like "1/4")
161                let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
162                    n.to_string()
163                } else if let Some(Value::String(s)) = lfo_map.get("rate") {
164                    s.clone()
165                } else {
166                    "5.0".to_string() // Default rate
167                };
168                let rate = LfoRate::from_value(&rate_str);
169
170                // Parse depth (0-1)
171                let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
172                    (*n).clamp(0.0, 1.0)
173                } else {
174                    0.5 // Default depth
175                };
176
177                // Parse waveform (sine, triangle, square, saw)
178                let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
179                    s.clone()
180                } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
181                    s.clone()
182                } else {
183                    "sine".to_string() // Default waveform
184                };
185                let waveform = LfoWaveform::from_str(&waveform_str);
186
187                // Parse target (volume, pitch, filter, pan)
188                let target = if let Some(Value::String(s)) = lfo_map.get("target") {
189                    LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
190                } else {
191                    LfoTarget::Volume // Default target
192                };
193
194                // Parse initial phase (0-1)
195                let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
196                    (*n).fract().abs() // Ensure 0-1 range
197                } else {
198                    0.0 // Default phase
199                };
200
201                Some(LfoParams {
202                    rate,
203                    depth,
204                    waveform,
205                    target,
206                    phase,
207                })
208            } else {
209                None
210            };
211
212            let mut options = std::collections::HashMap::new();
213            let reserved_keys = if is_plugin {
214                vec![
215                    "attack",
216                    "decay",
217                    "sustain",
218                    "release",
219                    "type",
220                    "filters",
221                    "_plugin_ref",
222                    "lfo",
223                ]
224            } else {
225                vec![
226                    "waveform",
227                    "attack",
228                    "decay",
229                    "sustain",
230                    "release",
231                    "type",
232                    "filters",
233                    "_plugin_ref",
234                    "lfo",
235                ]
236            };
237
238            for (key, val) in map.iter() {
239                if !reserved_keys.contains(&key.as_str()) {
240                    match val {
241                        Value::Number(n) => {
242                            options.insert(key.clone(), *n);
243                        }
244                        Value::String(s) => {
245                            if is_plugin && key == "waveform" {
246                                let waveform_id = match s
247                                    .trim_matches('"')
248                                    .trim_matches('\'')
249                                    .to_lowercase()
250                                    .as_str()
251                                {
252                                    "sine" => 0.0,
253                                    "saw" => 1.0,
254                                    "square" => 2.0,
255                                    "triangle" => 3.0,
256                                    _ => 1.0,
257                                };
258                                options.insert(key.clone(), waveform_id);
259                            }
260                        }
261                        _ => {}
262                    }
263                }
264            }
265
266            if is_plugin && map.contains_key("decay") {
267                options.insert("decay".to_string(), decay);
268            }
269
270            let final_waveform = if is_plugin {
271                "plugin".to_string()
272            } else {
273                waveform
274            };
275
276            let synth_def = crate::engine::audio::events::SynthDefinition {
277                waveform: final_waveform,
278                attack,
279                decay,
280                sustain,
281                release,
282                synth_type,
283                filters,
284                options,
285                plugin_author,
286                plugin_name,
287                plugin_export,
288                lfo,
289            };
290
291            interpreter.events.add_synth(name.to_string(), synth_def);
292        }
293
294        // Insert the normalized/augmented map into variables so dotted-property reads
295        // (e.g., `mySynth.volume`) return concrete values instead of Identifier("...").
296        interpreter
297            .variables
298            .insert(name.to_string(), Value::Map(map.clone()));
299        return Ok(());
300    }
301
302    // Non-map values: fall back to storing the original value
303    // If the value is a stored Trigger statement, normalize it into a Map so dotted
304    // property access like `myTrigger.effects.reverb.size` works and `print myTrigger`
305    // displays a clean map rather than a Statement debug dump.
306    if let Value::Statement(stmt_box) = value {
307        if let StatementKind::Trigger {
308            entity,
309            duration,
310            effects,
311        } = &stmt_box.kind
312        {
313            let mut map = HashMap::new();
314            map.insert("kind".to_string(), Value::String("Trigger".to_string()));
315            map.insert("entity".to_string(), Value::String(entity.clone()));
316            map.insert("duration".to_string(), Value::Duration(duration.clone()));
317            if let Some(eff) = effects {
318                map.insert("effects".to_string(), eff.clone());
319            }
320
321            // Ensure defaults for trigger objects
322            crate::utils::props::ensure_default_properties(&mut map, Some("trigger"));
323
324            interpreter
325                .variables
326                .insert(name.to_string(), Value::Map(map));
327            return Ok(());
328        }
329    }
330
331    interpreter
332        .variables
333        .insert(name.to_string(), value.clone());
334    Ok(())
335}
336
337pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str, args: &[Value]) -> Result<()> {
338    // ============================================================================
339    // CALL EXECUTION (Sequential)
340    // ============================================================================
341    // Call accepts:
342    // - Patterns (defined with `pattern name with trigger = "x---"`)
343    // - Groups (defined with `group name:`)
344    // ============================================================================
345
346    // Check for user-defined function stored as a variable
347    if let Some(var_val) = interpreter.variables.get(name).cloned() {
348        if let Value::Statement(stmt_box) = var_val {
349            if let StatementKind::Function {
350                name: _fname,
351                parameters,
352                body,
353            } = &stmt_box.kind
354            {
355                // Create a local variable scope for function execution
356                let vars_snapshot = interpreter.variables.clone();
357
358                // Bind parameters: resolve passed args into actual values
359                for (i, param) in parameters.iter().enumerate() {
360                    let bound = args.get(i).cloned().unwrap_or(Value::Null);
361                    // If the bound value is an Identifier, resolve it to its actual value
362                    let bound_val = match bound {
363                        Value::Identifier(ref id) => {
364                            interpreter.resolve_value(&Value::Identifier(id.clone()))?
365                        }
366                        other => other,
367                    };
368                    interpreter.variables.insert(param.clone(), bound_val);
369                }
370
371                // Execute the function body. Track function call depth so 'return'
372                // statements are only valid within a function context.
373                interpreter.function_call_depth += 1;
374                let exec_result = super::collector::collect_events(interpreter, body);
375                // decrement depth regardless of success or error
376                interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
377                exec_result?;
378
379                // Capture return value if the function executed a `return` statement.
380                let mut captured_return: Option<Value> = None;
381                if interpreter.returning_flag {
382                    captured_return = interpreter.return_value.clone();
383                    // clear the interpreter return state now that we've captured it
384                    interpreter.returning_flag = false;
385                    interpreter.return_value = None;
386                }
387
388                // Restore variables (local scope ends). We deliberately do not touch interpreter.events
389                // so synth definitions or other global registrations performed by the function persist.
390                interpreter.variables = vars_snapshot;
391
392                // If there was a returned value, expose it to the caller scope via a special variable
393                // named "__return" so callers can inspect the result. This is a simple mechanism
394                // for now; higher-level expression support can be added later.
395                if let Some(rv) = captured_return {
396                    interpreter.variables.insert("__return".to_string(), rv);
397                }
398
399                return Ok(());
400            }
401            // If it's a stored pattern (inline pattern stored as Statement), handle below
402            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
403                if let Some(tgt) = target.as_ref() {
404                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
405                    if let Some(pat) = pattern_str {
406                        interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
407                        return Ok(());
408                    }
409                }
410            }
411        }
412    }
413
414    // If it's a group call, execute the group body
415    if let Some(body) = interpreter.groups.get(name).cloned() {
416        super::collector::collect_events(interpreter, &body)?;
417        return Ok(());
418    }
419
420    println!(
421        "⚠️  Warning: Group, pattern or function '{}' not found",
422        name
423    );
424    Ok(())
425}
426
427/// Execute a call as an expression and return its resulting Value.
428/// This is similar to `handle_call` but returns the captured `return` value
429/// from a function when present. Groups and patterns return `Value::Null`.
430pub fn call_function(
431    interpreter: &mut AudioInterpreter,
432    name: &str,
433    args: &[Value],
434) -> Result<Value> {
435    // If it's a stored variable that is a function statement, execute and capture return
436    if let Some(var_val) = interpreter.variables.get(name).cloned() {
437        if let Value::Statement(stmt_box) = var_val {
438            if let StatementKind::Function {
439                name: _fname,
440                parameters,
441                body,
442            } = &stmt_box.kind
443            {
444                // create local variable snapshot
445                let vars_snapshot = interpreter.variables.clone();
446
447                // Bind parameters: use provided args (they are already resolved by caller)
448                for (i, param) in parameters.iter().enumerate() {
449                    let bound = args.get(i).cloned().unwrap_or(Value::Null);
450                    // If the bound value is an Identifier, resolve it to its actual value
451                    let bound_val = match bound {
452                        Value::Identifier(ref id) => {
453                            interpreter.resolve_value(&Value::Identifier(id.clone()))?
454                        }
455                        other => other,
456                    };
457                    interpreter.variables.insert(param.clone(), bound_val);
458                }
459
460                // Execute body in function context
461                interpreter.function_call_depth += 1;
462
463                let exec_result = super::collector::collect_events(interpreter, body);
464                interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
465                exec_result?;
466
467                // Capture return value if present
468                let mut captured_return: Option<Value> = None;
469                if interpreter.returning_flag {
470                    captured_return = interpreter.return_value.clone();
471                    interpreter.returning_flag = false;
472                    interpreter.return_value = None;
473                }
474
475                // Restore variables (local scope ends)
476                interpreter.variables = vars_snapshot;
477
478                if let Some(rv) = captured_return {
479                    return Ok(rv);
480                }
481
482                return Ok(Value::Null);
483            }
484            // Patterns fallthrough to below handling
485            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
486                if let Some(tgt) = target.as_ref() {
487                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
488                    if let Some(pat) = pattern_str {
489                        interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
490                        return Ok(Value::Null);
491                    }
492                }
493            }
494        }
495    }
496
497    // If it's a group call, execute and return null
498    if let Some(body) = interpreter.groups.get(name).cloned() {
499        super::collector::collect_events(interpreter, &body)?;
500        return Ok(Value::Null);
501    }
502
503    println!(
504        "⚠️  Warning: Group, pattern or function '{}' not found",
505        name
506    );
507    Ok(Value::Null)
508}
509
510pub fn execute_print(interpreter: &mut AudioInterpreter, value: &Value) -> Result<()> {
511    let message = match value {
512        Value::Call { name: _, args: _ } => {
513            // Evaluate the call expression and convert the returned value to string
514            let resolved = interpreter.resolve_value(value)?;
515            interpreter.value_to_string(&resolved)
516        }
517
518        Value::String(s) => {
519            if s.contains('{') && s.contains('}') {
520                interpreter.interpolate_string(s)
521            } else {
522                s.clone()
523            }
524        }
525        Value::Identifier(id) => {
526            // Resolve identifier values (supports post-increment shorthand)
527            // Support post-increment shorthand in prints: `i++` should mutate the variable.
528            if id.ends_with("++") {
529                let varname = id[..id.len() - 2].trim();
530                // read current
531                let cur = match interpreter.variables.get(varname) {
532                    Some(Value::Number(n)) => *n as isize,
533                    _ => 0,
534                };
535                // set new value
536                interpreter
537                    .variables
538                    .insert(varname.to_string(), Value::Number((cur + 1) as f32));
539                cur.to_string()
540            } else {
541                // Resolve the identifier using the interpreter resolver so dotted paths
542                // (e.g., mySynth.volume) are properly traversed.
543                let resolved = interpreter.resolve_value(&Value::Identifier(id.clone()))?;
544                match resolved {
545                    Value::String(s) => s.clone(),
546                    Value::Number(n) => n.to_string(),
547                    Value::Boolean(b) => b.to_string(),
548                    Value::Array(arr) => format!("{:?}", arr),
549                    Value::Map(map) => format!("{:?}", map),
550                    Value::Null => format!("Identifier(\"{}\")", id),
551                    other => format!("{:?}", other),
552                }
553            }
554        }
555        Value::Number(n) => n.to_string(),
556        Value::Boolean(b) => b.to_string(),
557        Value::Array(arr) => {
558            // Treat arrays used in print as concatenation parts: resolve each part and join
559            let mut parts = Vec::new();
560            for v in arr.iter() {
561                // If identifier with ++, handle mutation here
562                if let Value::Identifier(idtok) = v {
563                    if idtok.ends_with("++") {
564                        let varname = idtok[..idtok.len() - 2].trim();
565                        let cur = match interpreter.variables.get(varname) {
566                            Some(Value::Number(n)) => *n as isize,
567                            _ => 0,
568                        };
569                        interpreter
570                            .variables
571                            .insert(varname.to_string(), Value::Number((cur + 1) as f32));
572                        parts.push(cur.to_string());
573                        continue;
574                    }
575                }
576                let resolved = interpreter.resolve_value(v)?;
577                parts.push(interpreter.value_to_string(&resolved));
578            }
579            parts.join("")
580        }
581        Value::Map(map) => format!("{:?}", map),
582        _ => format!("{:?}", value),
583    };
584    // Always record prints as scheduled log events so they can be replayed at playback time.
585    // Record the scheduled log message (no debug-origin annotation in production)
586    let log_message = message.clone();
587    // Use the interpreter cursor_time as the event time.
588    interpreter
589        .events
590        .add_log_event(log_message.clone(), interpreter.cursor_time);
591    // If the interpreter is explicitly allowed to print (runtime interpreter), print immediately.
592    // For offline renders (`suppress_print == true`) we do NOT print now; prints are scheduled
593    // into `interpreter.events.logs` and written to a `.printlog` sidecar during the build.
594    // The live playback engine replays that sidecar so prints appear in real time during playback.
595    if !interpreter.suppress_print {
596        // Use the global CLI logger formatting for prints so they appear as [PRINT]
597        // Guard CLI-only logger behind the `cli` feature so WASM builds don't
598        // reference the `crate::tools` module (which is only available for native builds).
599        #[cfg(feature = "cli")]
600        {
601            crate::tools::logger::Logger::new().print(message.clone());
602        }
603
604        // For non-CLI builds (wasm/plugins) try forwarding to the realtime print
605        // channel if provided so prints are surfaced in UIs that consume them.
606        #[cfg(not(feature = "cli"))]
607        {
608            if let Some(tx) = &interpreter.realtime_print_tx {
609                let _ = tx.send((interpreter.cursor_time, log_message.clone()));
610            }
611        }
612    } else {
613        // If printing is suppressed (offline render) but a realtime replay channel was provided,
614        // forward the scheduled print to the replay thread so it can be displayed in real-time
615        // while the offline render proceeds. This is a best-effort path used by some callers
616        // that pipe scheduled prints directly into a playback session (when set).
617        if let Some(tx) = &interpreter.realtime_print_tx {
618            // forward the scheduled log message to realtime replayers (best-effort)
619            let _ = tx.send((interpreter.cursor_time, log_message.clone()));
620        }
621    }
622    Ok(())
623}
624
625pub fn execute_if(
626    interpreter: &mut AudioInterpreter,
627    condition: &Value,
628    body: &[Statement],
629    else_body: &Option<Vec<Statement>>,
630) -> Result<()> {
631    let condition_result = interpreter.evaluate_condition(condition)?;
632
633    if condition_result {
634        super::collector::collect_events(interpreter, body)?;
635    } else if let Some(else_stmts) = else_body {
636        super::collector::collect_events(interpreter, else_stmts)?;
637    }
638
639    Ok(())
640}
641
642pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
643    let handlers = interpreter.event_registry.get_handlers_matching(event_name);
644
645    for (index, handler) in handlers.iter().enumerate() {
646        // If handler has args, allow numeric interval to gate execution for 'beat'/'bar'
647        if let Some(args) = &handler.args {
648            if let Some(num_val) = args.iter().find(|v| matches!(v, Value::Number(_))) {
649                if let Value::Number(n) = num_val {
650                    let interval = (*n as usize).max(1);
651                    if event_name == "beat" {
652                        let cur = interpreter.special_vars.current_beat.floor() as usize;
653                        if cur % interval != 0 {
654                            continue;
655                        }
656                    } else if event_name == "bar" {
657                        let cur = interpreter.special_vars.current_bar.floor() as usize;
658                        if cur % interval != 0 {
659                            continue;
660                        }
661                    }
662                }
663            }
664        }
665
666        if handler.once
667            && !interpreter
668                .event_registry
669                .should_execute_once(event_name, index)
670        {
671            continue;
672        }
673
674        let body_clone = handler.body.clone();
675        super::collector::collect_events(interpreter, &body_clone)?;
676    }
677
678    Ok(())
679}
680
681pub fn handle_assign(
682    interpreter: &mut AudioInterpreter,
683    target: &str,
684    property: &str,
685    value: &Value,
686) -> Result<()> {
687    // Support dotted targets like "myTrigger.effects.reverb" as the target string.
688    // The parser may pass the full path as `target` and the last path segment as `property`.
689    if target.contains('.') {
690        let parts: Vec<&str> = target.split('.').collect();
691        let root = parts[0];
692        if let Some(root_val) = interpreter.variables.get_mut(root) {
693            // Traverse into nested maps to reach the parent map where to insert `property`.
694            let mut current = root_val;
695            for seg in parts.iter().skip(1) {
696                match current {
697                    Value::Map(map) => {
698                        if !map.contains_key(*seg) {
699                            // create nested map if missing
700                            map.insert((*seg).to_string(), Value::Map(HashMap::new()));
701                        }
702                        current = map.get_mut(*seg).unwrap();
703                    }
704                    _ => {
705                        return Err(anyhow::anyhow!(
706                            "Cannot traverse into non-map segment '{}' when assigning to '{}'",
707                            seg,
708                            target
709                        ));
710                    }
711                }
712            }
713
714            // Now `current` should be a Value::Map where we insert `property`.
715            if let Value::Map(map) = current {
716                map.insert(property.to_string(), value.clone());
717
718                // If the root object is a synth, update its synth definition
719                if interpreter.events.synths.contains_key(root) {
720                    if let Some(Value::Map(root_map)) = interpreter.variables.get(root) {
721                        let map_clone = root_map.clone();
722                        let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
723                        interpreter
724                            .events
725                            .synths
726                            .insert(root.to_string(), updated_def);
727                    }
728                }
729            } else {
730                return Err(anyhow::anyhow!(
731                    "Cannot assign property '{}' to non-map target '{}'",
732                    property,
733                    target
734                ));
735            }
736        } else {
737            return Err(anyhow::anyhow!("Variable '{}' not found", root));
738        }
739    } else {
740        if let Some(var) = interpreter.variables.get_mut(target) {
741            if let Value::Map(map) = var {
742                map.insert(property.to_string(), value.clone());
743
744                if interpreter.events.synths.contains_key(target) {
745                    let map_clone = map.clone();
746                    let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
747                    interpreter
748                        .events
749                        .synths
750                        .insert(target.to_string(), updated_def);
751                }
752            } else {
753                return Err(anyhow::anyhow!(
754                    "Cannot assign property '{}' to non-map variable '{}'",
755                    property,
756                    target
757                ));
758            }
759        } else {
760            return Err(anyhow::anyhow!("Variable '{}' not found", target));
761        }
762    }
763
764    Ok(())
765}
766
767pub fn extract_synth_def_from_map(
768    _interpreter: &AudioInterpreter,
769    map: &HashMap<String, Value>,
770) -> Result<crate::engine::audio::events::SynthDefinition> {
771    use crate::engine::audio::events::extract_filters;
772    use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
773
774    let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
775    let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
776    let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
777    let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
778    let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
779
780    // Accept both String and Identifier for type (and synth_type alias)
781    let synth_type = if let Some(v) = map.get("type") {
782        match v {
783            Value::String(t) => {
784                let clean = t.trim_matches('"').trim_matches('\'');
785                if clean.is_empty() || clean == "synth" {
786                    None
787                } else {
788                    Some(clean.to_string())
789                }
790            }
791            Value::Identifier(id) => {
792                let clean = id.trim_matches('"').trim_matches('\'');
793                if clean.is_empty() || clean == "synth" {
794                    None
795                } else {
796                    Some(clean.to_string())
797                }
798            }
799            _ => None,
800        }
801    } else if let Some(v2) = map.get("synth_type") {
802        match v2 {
803            Value::String(t2) => {
804                let clean = t2.trim_matches('"').trim_matches('\'');
805                if clean.is_empty() || clean == "synth" {
806                    None
807                } else {
808                    Some(clean.to_string())
809                }
810            }
811            Value::Identifier(id2) => {
812                let clean = id2.trim_matches('"').trim_matches('\'');
813                if clean.is_empty() || clean == "synth" {
814                    None
815                } else {
816                    Some(clean.to_string())
817                }
818            }
819            _ => None,
820        }
821    } else {
822        None
823    };
824
825    let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
826        extract_filters(filters_arr)
827    } else {
828        Vec::new()
829    };
830
831    let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
832        Some(s.clone())
833    } else {
834        None
835    };
836    let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
837        Some(s.clone())
838    } else {
839        None
840    };
841    let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
842        Some(s.clone())
843    } else {
844        None
845    };
846
847    // Extract LFO configuration if present
848    let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
849        // Parse rate (Hz or tempo-synced like "1/4")
850        let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
851            n.to_string()
852        } else if let Some(Value::String(s)) = lfo_map.get("rate") {
853            s.clone()
854        } else {
855            "5.0".to_string() // Default rate
856        };
857        let rate = LfoRate::from_value(&rate_str);
858
859        // Parse depth (0-1)
860        let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
861            (*n).clamp(0.0, 1.0)
862        } else {
863            0.5 // Default depth
864        };
865
866        // Parse waveform (sine, triangle, square, saw)
867        let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
868            s.clone()
869        } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
870            s.clone()
871        } else {
872            "sine".to_string() // Default waveform
873        };
874        let waveform = LfoWaveform::from_str(&waveform_str);
875
876        // Parse target (volume, pitch, filter, pan)
877        let target = if let Some(Value::String(s)) = lfo_map.get("target") {
878            LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
879        } else {
880            LfoTarget::Volume // Default target
881        };
882
883        // Parse initial phase (0-1)
884        let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
885            (*n).fract().abs() // Ensure 0-1 range
886        } else {
887            0.0 // Default phase
888        };
889
890        Some(LfoParams {
891            rate,
892            depth,
893            waveform,
894            target,
895            phase,
896        })
897    } else {
898        None
899    };
900
901    let mut options = HashMap::new();
902    for (key, val) in map.iter() {
903        if ![
904            "waveform",
905            "attack",
906            "decay",
907            "sustain",
908            "release",
909            "type",
910            "filters",
911            "plugin_author",
912            "plugin_name",
913            "plugin_export",
914            "lfo",
915        ]
916        .contains(&key.as_str())
917        {
918            if let Value::Number(n) = val {
919                options.insert(key.clone(), *n);
920            } else if let Value::String(s) = val {
921                if key == "waveform" || key.starts_with("_") {
922                    continue;
923                }
924                if let Ok(n) = s.parse::<f32>() {
925                    options.insert(key.clone(), n);
926                }
927            }
928        }
929    }
930
931    Ok(crate::engine::audio::events::SynthDefinition {
932        waveform,
933        attack,
934        decay,
935        sustain,
936        release,
937        synth_type,
938        filters,
939        options,
940        plugin_author,
941        plugin_name,
942        plugin_export,
943        lfo,
944    })
945}
946
947pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
948    use std::path::Path;
949
950    let path = Path::new(source);
951    // Determine extension
952    if let Some(ext) = path
953        .extension()
954        .and_then(|s| s.to_str())
955        .map(|s| s.to_lowercase())
956    {
957        match ext.as_str() {
958            "mid" | "midi" => {
959                use crate::engine::audio::midi::load_midi_file;
960                let midi_data = load_midi_file(path)?;
961                interpreter.variables.insert(alias.to_string(), midi_data);
962                // MIDI file loaded (silent)
963                Ok(())
964            }
965            "wav" | "flac" | "mp3" | "ogg" => {
966                // For native/CLI builds, register sample via the samples subsystem.
967                #[cfg(feature = "cli")]
968                {
969                    use crate::engine::audio::samples;
970                    let registered = samples::register_sample_from_path(path)?;
971                    // Record the sample URI under the alias variable as a string (consistent with triggers)
972                    interpreter
973                        .variables
974                        .insert(alias.to_string(), Value::String(registered.clone()));
975                    // Sample file loaded (silent)
976                    return Ok(());
977                }
978
979                // For non-CLI builds (WASM/plugins), fallback to storing the original path as a string.
980                #[cfg(not(feature = "cli"))]
981                {
982                    interpreter
983                        .variables
984                        .insert(alias.to_string(), Value::String(source.to_string()));
985                    return Ok(());
986                }
987            }
988            _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
989        }
990    } else {
991        Err(anyhow::anyhow!(
992            "Cannot determine file extension for {}",
993            source
994        ))
995    }
996}
997
998pub fn handle_bind(
999    interpreter: &mut AudioInterpreter,
1000    source: &str,
1001    target: &str,
1002    options: &Value,
1003) -> Result<()> {
1004    use std::collections::HashMap as StdHashMap;
1005
1006    // Support bindings that reference runtime MIDI device mappings like:
1007    //   bind myKickPattern -> mapping.out.myDeviceA with { port: 1, channel: 10 }
1008    //   bind mapping.in.myDeviceB with { port: 2, channel: 10 } -> mySynth
1009    // When a 'mapping.*' path is used, register a lightweight mapping entry in the
1010    // interpreter variables and expose convenience variables:
1011    //   mapping.<in|out>.<device>.<noteOn|noteOff|rest>
1012    if source.starts_with("mapping.") || target.starts_with("mapping.") {
1013        // extract options if present
1014        let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
1015            m.clone()
1016        } else {
1017            StdHashMap::new()
1018        };
1019
1020        // Helper function to create mapping variables and bookkeeping
1021        fn create_and_insert(
1022            path: &str,
1023            opts_map: &StdHashMap<String, Value>,
1024            interpreter: &mut AudioInterpreter,
1025        ) -> Option<(String, String)> {
1026            let parts: Vec<&str> = path.split('.').collect();
1027            if parts.len() >= 3 {
1028                let direction = parts[1]; // "in" or "out"
1029                let device = parts[2];
1030
1031                let mut map = StdHashMap::new();
1032                map.insert(
1033                    "_type".to_string(),
1034                    Value::String("midi_mapping".to_string()),
1035                );
1036                map.insert(
1037                    "direction".to_string(),
1038                    Value::String(direction.to_string()),
1039                );
1040                map.insert("device".to_string(), Value::String(device.to_string()));
1041
1042                // merge provided options
1043                for (k, v) in opts_map.iter() {
1044                    map.insert(k.clone(), v.clone());
1045                }
1046
1047                // Ensure mapping defaults
1048                crate::utils::props::ensure_default_properties(&mut map, Some("mapping"));
1049
1050                interpreter
1051                    .variables
1052                    .insert(path.to_string(), Value::Map(map.clone()));
1053
1054                // Expose event variables for convenience
1055                let note_on = format!("mapping.{}.{}.noteOn", direction, device);
1056                let note_off = format!("mapping.{}.{}.noteOff", direction, device);
1057                let rest = format!("mapping.{}.{}.rest", direction, device);
1058                interpreter
1059                    .variables
1060                    .insert(note_on.clone(), Value::String(note_on.clone()));
1061                interpreter
1062                    .variables
1063                    .insert(note_off.clone(), Value::String(note_off.clone()));
1064                interpreter
1065                    .variables
1066                    .insert(rest.clone(), Value::String(rest.clone()));
1067
1068                return Some((direction.to_string(), device.to_string()));
1069            }
1070            None
1071        }
1072
1073        // If source is mapping.* (incoming mapping binds to target instrument)
1074        if source.starts_with("mapping.") {
1075            if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
1076                // Record association when target is not also mapping.*
1077                if !target.starts_with("mapping.") {
1078                    let mut bmap = StdHashMap::new();
1079                    bmap.insert("instrument".to_string(), Value::String(target.to_string()));
1080                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
1081                    bmap.insert("device".to_string(), Value::String(device.clone()));
1082                    for (k, v) in opts_map.iter() {
1083                        bmap.insert(k.clone(), v.clone());
1084                    }
1085                    interpreter
1086                        .variables
1087                        .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
1088
1089                    // If we have a midi_manager, try to open the input port (for incoming mappings)
1090                    #[cfg(feature = "cli")]
1091                    if let Some(manager) = &mut interpreter.midi_manager {
1092                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
1093                            let idx = *port_num as usize;
1094                            if let Ok(mut mgr) = manager.lock() {
1095                                // use device as name for identification
1096                                let _ = mgr.open_input_by_index(idx, &device);
1097                            }
1098                        }
1099                    }
1100                }
1101            }
1102        }
1103
1104        // If target is mapping.* (binding a sequence/instrument to an external device)
1105        if target.starts_with("mapping.") {
1106            if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
1107                if !source.starts_with("mapping.") {
1108                    let mut bmap = StdHashMap::new();
1109                    bmap.insert("source".to_string(), Value::String(source.to_string()));
1110                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
1111                    bmap.insert("device".to_string(), Value::String(device.clone()));
1112                    for (k, v) in opts_map.iter() {
1113                        bmap.insert(k.clone(), v.clone());
1114                    }
1115                    interpreter
1116                        .variables
1117                        .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
1118
1119                    // If we have a midi_manager, try to open the output port (for outgoing mappings)
1120                    #[cfg(feature = "cli")]
1121                    if let Some(manager) = &mut interpreter.midi_manager {
1122                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
1123                            let idx = *port_num as usize;
1124                            if let Ok(mut mgr) = manager.lock() {
1125                                let _ = mgr.open_output_by_name(&device, idx);
1126                            }
1127                        }
1128                    }
1129                }
1130            }
1131        }
1132
1133        // Nothing more to schedule here at audio event level; actual MIDI I/O handlers
1134        // will be responsible for reacting to incoming messages and emitting events into
1135        // the interpreter event registry, and for flushing outgoing bound sequences to
1136        // MIDI device ports when appropriate.
1137        return Ok(());
1138    }
1139
1140    // Fallback: existing behaviour (binding MIDI file data to a synth)
1141    let midi_data = interpreter
1142        .variables
1143        .get(source)
1144        .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
1145        .clone();
1146
1147    if let Value::Map(midi_map) = &midi_data {
1148        let notes = midi_map
1149            .get("notes")
1150            .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
1151
1152        if let Value::Array(notes_array) = notes {
1153            let _synth_def = interpreter
1154                .events
1155                .synths
1156                .get(target)
1157                .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
1158                .clone();
1159
1160            let default_velocity = 100;
1161            let mut velocity = default_velocity;
1162
1163            if let Value::Map(opts) = options {
1164                if let Some(Value::Number(v)) = opts.get("velocity") {
1165                    velocity = *v as u8;
1166                }
1167            }
1168
1169            // Determine MIDI file BPM (if present) so we can rescale times to interpreter BPM
1170            // Default to interpreter.bpm when the MIDI file has no BPM metadata
1171            let midi_bpm =
1172                crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
1173
1174            for note_val in notes_array {
1175                if let Value::Map(note_map) = note_val {
1176                    let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
1177                    let note =
1178                        crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
1179                    let note_velocity = crate::engine::audio::events::extract_number(
1180                        note_map,
1181                        "velocity",
1182                        velocity as f32,
1183                    ) as u8;
1184                    // Duration may be present (ms) from MIDI loader; fallback to 500 ms
1185                    let duration_ms =
1186                        crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
1187
1188                    use crate::engine::audio::events::AudioEvent;
1189                    let synth_def = interpreter
1190                        .events
1191                        .get_synth(target)
1192                        .cloned()
1193                        .unwrap_or_default();
1194                    // Rescale times according to interpreter BPM vs MIDI file BPM.
1195                    // If midi_bpm == interpreter.bpm this is a no-op. We compute factor = midi_bpm / interpreter.bpm
1196                    let interp_bpm = interpreter.bpm;
1197                    let factor = if interp_bpm > 0.0 {
1198                        midi_bpm / interp_bpm
1199                    } else {
1200                        1.0
1201                    };
1202
1203                    let start_time_s = (time / 1000.0) * factor;
1204                    let duration_s = (duration_ms / 1000.0) * factor;
1205
1206                    let event = AudioEvent::Note {
1207                        midi: note,
1208                        start_time: start_time_s,
1209                        duration: duration_s,
1210                        velocity: note_velocity as f32,
1211                        synth_id: target.to_string(),
1212                        synth_def,
1213                        pan: 0.0,
1214                        detune: 0.0,
1215                        gain: 1.0,
1216                        attack: None,
1217                        release: None,
1218                        delay_time: None,
1219                        delay_feedback: None,
1220                        delay_mix: None,
1221                        reverb_amount: None,
1222                        drive_amount: None,
1223                        drive_color: None,
1224                        effects: None,
1225                        use_per_note_automation: false,
1226                    };
1227
1228                    // bound note scheduled
1229                    interpreter.events.events.push(event);
1230                }
1231            }
1232
1233            // Bound notes from source to target
1234        }
1235    }
1236
1237    Ok(())
1238}
1239
1240#[cfg(feature = "cli")]
1241pub fn handle_use_plugin(
1242    interpreter: &mut AudioInterpreter,
1243    author: &str,
1244    name: &str,
1245    alias: &str,
1246) -> Result<()> {
1247    use crate::engine::plugin::loader::load_plugin;
1248
1249    match load_plugin(author, name) {
1250        Ok((info, _wasm_bytes)) => {
1251            let mut plugin_map = HashMap::new();
1252            plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
1253            plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
1254            plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
1255
1256            if let Some(version) = &info.version {
1257                plugin_map.insert("_version".to_string(), Value::String(version.clone()));
1258            }
1259
1260            for export in &info.exports {
1261                let mut export_map = HashMap::new();
1262                export_map.insert(
1263                    "_plugin_author".to_string(),
1264                    Value::String(info.author.clone()),
1265                );
1266                export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
1267                export_map.insert(
1268                    "_export_name".to_string(),
1269                    Value::String(export.name.clone()),
1270                );
1271                export_map.insert(
1272                    "_export_kind".to_string(),
1273                    Value::String(export.kind.clone()),
1274                );
1275
1276                plugin_map.insert(export.name.clone(), Value::Map(export_map));
1277            }
1278            // Ensure plugin map has default properties available for dotted access
1279            crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1280
1281            interpreter
1282                .variables
1283                .insert(alias.to_string(), Value::Map(plugin_map));
1284        }
1285        Err(e) => {
1286            eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
1287            return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
1288        }
1289    }
1290
1291    Ok(())
1292}
1293
1294#[cfg(not(feature = "cli"))]
1295pub fn handle_use_plugin(
1296    interpreter: &mut AudioInterpreter,
1297    author: &str,
1298    name: &str,
1299    alias: &str,
1300) -> Result<()> {
1301    // Plugin loading not supported in this build (WASM/plugin builds). Insert a minimal placeholder so scripts can still reference the alias.
1302    let mut plugin_map = HashMap::new();
1303    plugin_map.insert(
1304        "_type".to_string(),
1305        Value::String("plugin_stub".to_string()),
1306    );
1307    plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
1308    plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
1309    // Ensure stubs also expose defaults for dotted access
1310    crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1311
1312    interpreter
1313        .variables
1314        .insert(alias.to_string(), Value::Map(plugin_map));
1315    Ok(())
1316}
1317
1318pub fn handle_bank(
1319    interpreter: &mut AudioInterpreter,
1320    name: &str,
1321    alias: &Option<String>,
1322) -> Result<()> {
1323    let target_alias = alias
1324        .clone()
1325        .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
1326
1327    if let Some(existing_value) = interpreter.variables.get(name) {
1328        interpreter
1329            .variables
1330            .insert(target_alias.clone(), existing_value.clone());
1331    } else {
1332        #[cfg(feature = "wasm")]
1333        {
1334            use crate::web::registry::banks::REGISTERED_BANKS;
1335            REGISTERED_BANKS.with(|banks| {
1336                for bank in banks.borrow().iter() {
1337                    if bank.full_name == *name {
1338                        if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
1339                            interpreter
1340                                .variables
1341                                .insert(target_alias.clone(), Value::Map(bank_map.clone()));
1342                        }
1343                    }
1344                }
1345            });
1346        }
1347
1348        #[cfg(not(feature = "wasm"))]
1349        {
1350            if let Ok(current_dir) = std::env::current_dir() {
1351                match interpreter.banks.register_bank(
1352                    target_alias.clone(),
1353                    &name,
1354                    &current_dir,
1355                    &current_dir,
1356                ) {
1357                    Ok(_) => {
1358                        let mut bank_map = HashMap::new();
1359                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1360                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1361                        interpreter
1362                            .variables
1363                            .insert(target_alias.clone(), Value::Map(bank_map));
1364                    }
1365                    Err(e) => {
1366                        eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
1367                        let mut bank_map = HashMap::new();
1368                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1369                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1370                        interpreter
1371                            .variables
1372                            .insert(target_alias.clone(), Value::Map(bank_map));
1373                    }
1374                }
1375            } else {
1376                let mut bank_map = HashMap::new();
1377                bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1378                bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1379                interpreter
1380                    .variables
1381                    .insert(target_alias.clone(), Value::Map(bank_map));
1382                eprintln!(
1383                    "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
1384                    name
1385                );
1386            }
1387        }
1388    }
1389
1390    Ok(())
1391}
1392
1393pub fn handle_trigger(
1394    interpreter: &mut AudioInterpreter,
1395    entity: &str,
1396    effects: Option<&crate::language::syntax::ast::Value>,
1397) -> Result<()> {
1398    let resolved_entity = if entity.starts_with('.') {
1399        &entity[1..]
1400    } else {
1401        entity
1402    };
1403
1404    // If this resolved entity refers to a variable that contains a Trigger statement,
1405    // execute that stored trigger instead (supports: let t = .bank.kick -> reverse(true); .t )
1406    if let Some(var_val) = interpreter.variables.get(resolved_entity).cloned() {
1407        if let crate::language::syntax::ast::Value::Statement(stmt_box) = var_val {
1408            if let crate::language::syntax::ast::StatementKind::Trigger {
1409                entity: inner_entity,
1410                duration: _,
1411                effects: stored_effects,
1412            } = &stmt_box.kind
1413            {
1414                // Avoid direct recursion if someone stored a trigger that points to itself
1415                if inner_entity != resolved_entity {
1416                    // Prefer stored effects when present, otherwise fall back to effects passed in
1417                    let chosen_effects = stored_effects.as_ref().or(effects);
1418                    return handle_trigger(interpreter, inner_entity, chosen_effects);
1419                } else {
1420                    return Ok(());
1421                }
1422            }
1423        }
1424    }
1425
1426    if resolved_entity.contains('.') {
1427        let parts: Vec<&str> = resolved_entity.split('.').collect();
1428        if parts.len() == 2 {
1429            let (var_name, property) = (parts[0], parts[1]);
1430
1431            if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
1432                if let Some(Value::String(sample_uri)) = map.get(property) {
1433                    let uri = sample_uri.trim_matches('"').trim_matches('\'');
1434                    // scheduling sample at current cursor_time
1435                    interpreter.events.add_sample_event_with_effects(
1436                        uri,
1437                        interpreter.cursor_time,
1438                        1.0,
1439                        effects.cloned(),
1440                    );
1441                    let beat_duration = interpreter.beat_duration();
1442                    interpreter.cursor_time += beat_duration;
1443                } else {
1444                    #[cfg(not(feature = "wasm"))]
1445                    {
1446                        // First try to produce an internal devalang://bank URI (preferred, supports lazy loading)
1447                        let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
1448                        if resolved_uri != resolved_entity {
1449                            // scheduling resolved sample
1450                            interpreter.events.add_sample_event_with_effects(
1451                                &resolved_uri,
1452                                interpreter.cursor_time,
1453                                1.0,
1454                                effects.cloned(),
1455                            );
1456                            let beat_duration = interpreter.beat_duration();
1457                            interpreter.cursor_time += beat_duration;
1458                        } else if let Some(pathbuf) =
1459                            interpreter.banks.resolve_trigger(var_name, property)
1460                        {
1461                            if let Some(path_str) = pathbuf.to_str() {
1462                                // scheduling sample via bank path
1463                                interpreter.events.add_sample_event_with_effects(
1464                                    path_str,
1465                                    interpreter.cursor_time,
1466                                    1.0,
1467                                    effects.cloned(),
1468                                );
1469                                let beat_duration = interpreter.beat_duration();
1470                                interpreter.cursor_time += beat_duration;
1471                            } else {
1472                                println!(
1473                                    "⚠️ Resolution failed for {}.{} (invalid path)",
1474                                    var_name, property
1475                                );
1476                            }
1477                        } else {
1478                            // no path found in BankRegistry
1479                        }
1480                    }
1481                }
1482            }
1483        }
1484    } else {
1485        if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
1486            let uri = sample_uri.trim_matches('"').trim_matches('\'');
1487            interpreter.events.add_sample_event_with_effects(
1488                uri,
1489                interpreter.cursor_time,
1490                1.0,
1491                effects.cloned(),
1492            );
1493            let beat_duration = interpreter.beat_duration();
1494            interpreter.cursor_time += beat_duration;
1495        }
1496    }
1497
1498    // Note: do not call interpreter.render_audio() here - rendering is handled by the build pipeline.
1499    // Trigger queued for rendering (events collected)
1500
1501    Ok(())
1502}
1503
1504pub fn extract_pattern_data(
1505    _interpreter: &AudioInterpreter,
1506    value: &Value,
1507) -> (Option<String>, Option<HashMap<String, f32>>) {
1508    match value {
1509        Value::String(pattern) => (Some(pattern.clone()), None),
1510        Value::Map(map) => {
1511            let pattern = map.get("pattern").and_then(|v| {
1512                if let Value::String(s) = v {
1513                    Some(s.clone())
1514                } else {
1515                    None
1516                }
1517            });
1518
1519            let mut options = HashMap::new();
1520            for (key, val) in map.iter() {
1521                if key != "pattern" {
1522                    if let Value::Number(num) = val {
1523                        options.insert(key.clone(), *num);
1524                    }
1525                }
1526            }
1527
1528            let opts = if options.is_empty() {
1529                None
1530            } else {
1531                Some(options)
1532            };
1533            (pattern, opts)
1534        }
1535        _ => (None, None),
1536    }
1537}
1538
1539pub fn execute_pattern(
1540    interpreter: &mut AudioInterpreter,
1541    target: &str,
1542    pattern: &str,
1543    options: Option<HashMap<String, f32>>,
1544) -> Result<()> {
1545    use crate::engine::audio::events::AudioEvent;
1546
1547    let swing = options
1548        .as_ref()
1549        .and_then(|o| o.get("swing").copied())
1550        .unwrap_or(0.0);
1551    let humanize = options
1552        .as_ref()
1553        .and_then(|o| o.get("humanize").copied())
1554        .unwrap_or(0.0);
1555    let velocity_mult = options
1556        .as_ref()
1557        .and_then(|o| o.get("velocity").copied())
1558        .unwrap_or(1.0);
1559    let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
1560
1561    let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
1562
1563    let resolved_uri = resolve_sample_uri(interpreter, target);
1564
1565    let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
1566    let step_count = pattern_chars.len() as f32;
1567    if step_count == 0.0 {
1568        return Ok(());
1569    }
1570
1571    let bar_duration = (60.0 / effective_bpm) * 4.0;
1572    let step_duration = bar_duration / step_count;
1573
1574    for (i, &ch) in pattern_chars.iter().enumerate() {
1575        if ch == 'x' || ch == 'X' {
1576            let mut time = interpreter.cursor_time + (i as f32 * step_duration);
1577            if swing > 0.0 && i % 2 == 1 {
1578                time += step_duration * swing;
1579            }
1580
1581            #[cfg(any(feature = "cli", feature = "wasm"))]
1582            if humanize > 0.0 {
1583                use rand::Rng;
1584                let mut rng = rand::thread_rng();
1585                let offset = rng.gen_range(-humanize..humanize);
1586                time += offset;
1587            }
1588
1589            let event = AudioEvent::Sample {
1590                uri: resolved_uri.clone(),
1591                start_time: time,
1592                velocity: velocity_mult, // Already in 0-1 range, not MIDI 0-127
1593                effects: None,
1594            };
1595            interpreter.events.events.push(event);
1596        }
1597    }
1598
1599    interpreter.cursor_time += bar_duration;
1600    Ok(())
1601}
1602
1603pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
1604    if let Some(dot_pos) = target.find('.') {
1605        let bank_alias = &target[..dot_pos];
1606        let trigger_name = &target[dot_pos + 1..];
1607        if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
1608            if let Some(Value::String(bank_name)) = bank_map.get("_name") {
1609                return format!("devalang://bank/{}/{}", bank_name, trigger_name);
1610            }
1611        }
1612    }
1613    target.to_string()
1614}