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::{Value, Statement, StatementKind};
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> = resolved_plugin["plugin:".len()..].split(':').collect();
28                                    if ref_parts.len() == 2 {
29                                        let full_plugin_name = ref_parts[0];
30                                        let export_name = ref_parts[1];
31                                        let plugin_parts: Vec<&str> = full_plugin_name.split('.').collect();
32                                        if plugin_parts.len() == 2 {
33                                            plugin_author = Some(plugin_parts[0].to_string());
34                                            plugin_name = Some(plugin_parts[1].to_string());
35                                            plugin_export = Some(export_name.to_string());
36                                            is_plugin = true;
37                                        }
38                                    }
39                                }
40                            } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
41                                if let (Some(Value::String(author)), Some(Value::String(name)), Some(Value::String(export))) = (
42                                    export_map.get("_plugin_author"),
43                                    export_map.get("_plugin_name"),
44                                    export_map.get("_export_name")
45                                ) {
46                                    plugin_author = Some(author.clone());
47                                    plugin_name = Some(name.clone());
48                                    plugin_export = Some(export.clone());
49                                    is_plugin = true;
50                                }
51                            }
52                        }
53                    }
54                }
55            }
56
57            let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
58            let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
59            let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
60            let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
61            let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
62
63            let synth_type = if let Some(Value::String(t)) = map.get("type") {
64                let clean = t.trim_matches('"').trim_matches('\'');
65                if clean.is_empty() || clean == "synth" { None } else { Some(clean.to_string()) }
66            } else { None };
67
68            let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
69                crate::engine::audio::events::extract_filters(filters_arr)
70            } else { Vec::new() };
71
72            let mut options = std::collections::HashMap::new();
73            let reserved_keys = if is_plugin {
74                vec!["attack", "decay", "sustain", "release", "type", "filters", "_plugin_ref"]
75            } else {
76                vec!["waveform", "attack", "decay", "sustain", "release", "type", "filters", "_plugin_ref"]
77            };
78
79            for (key, val) in map.iter() {
80                if !reserved_keys.contains(&key.as_str()) {
81                    match val {
82                        Value::Number(n) => { options.insert(key.clone(), *n); }
83                        Value::String(s) => {
84                            if is_plugin && key == "waveform" {
85                                let waveform_id = match s.trim_matches('"').trim_matches('\'').to_lowercase().as_str() {
86                                    "sine" => 0.0,
87                                    "saw" => 1.0,
88                                    "square" => 2.0,
89                                    "triangle" => 3.0,
90                                    _ => 1.0,
91                                };
92                                options.insert(key.clone(), waveform_id);
93                            }
94                        }
95                        _ => {}
96                    }
97                }
98            }
99
100            if is_plugin && map.contains_key("decay") {
101                options.insert("decay".to_string(), decay);
102            }
103
104            let final_waveform = if is_plugin { "plugin".to_string() } else { waveform };
105
106            let synth_def = crate::engine::audio::events::SynthDefinition {
107                waveform: final_waveform,
108                attack,
109                decay,
110                sustain,
111                release,
112                synth_type,
113                filters,
114                options,
115                plugin_author,
116                plugin_name,
117                plugin_export,
118            };
119
120            // Log plugin info for diagnostics: why plugin path may not be used later
121            println!("🎹 Synth registered: {} -> plugin_author={:?}, plugin_name={:?}, plugin_export={:?}", name, synth_def.plugin_author, synth_def.plugin_name, synth_def.plugin_export);
122            interpreter.events.add_synth(name.to_string(), synth_def);
123        }
124    }
125
126    interpreter.variables.insert(name.to_string(), value.clone());
127    Ok(())
128}
129
130pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str) -> Result<()> {
131    // Check inline pattern or pattern variable or group call
132    // Clone the variable value first to avoid holding an immutable borrow across a mutable call
133    if let Some(pattern_value) = interpreter.variables.get(name).cloned() {
134        if let Value::Statement(stmt_box) = pattern_value {
135            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
136                if let Some(tgt) = target.as_ref() {
137                        let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
138                        if let Some(pat) = pattern_str {
139                            println!("🎵 Call pattern: {} with {}", name, tgt);
140                            interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
141                            return Ok(());
142                        }
143                    }
144            }
145        }
146    }
147
148    if let Some(body) = interpreter.groups.get(name).cloned() {
149        println!("📞 Call group: {}", name);
150        super::collector::collect_events(interpreter, &body)?;
151    } else {
152        println!("⚠️  Warning: Group or pattern '{}' not found", name);
153    }
154
155    Ok(())
156}
157
158pub fn execute_print(interpreter: &AudioInterpreter, value: &Value) -> Result<()> {
159    let message = match value {
160        Value::String(s) => {
161            if s.contains('{') && s.contains('}') { interpreter.interpolate_string(s) } else { s.clone() }
162        }
163        Value::Identifier(id) => {
164            // Resolve variable from interpreter.variables
165            if let Some(v) = interpreter.variables.get(id) {
166                match v {
167                    Value::String(s) => s.clone(),
168                    Value::Number(n) => n.to_string(),
169                    Value::Boolean(b) => b.to_string(),
170                    Value::Array(arr) => format!("{:?}", arr),
171                    Value::Map(map) => format!("{:?}", map),
172                    _ => format!("{:?}", v),
173                }
174            } else {
175                format!("Identifier(\"{}\")", id)
176            }
177        }
178        Value::Number(n) => n.to_string(),
179        Value::Boolean(b) => b.to_string(),
180        Value::Array(arr) => format!("{:?}", arr),
181        Value::Map(map) => format!("{:?}", map),
182        _ => format!("{:?}", value),
183    };
184
185    println!("💬 {}", message);
186    Ok(())
187}
188
189pub fn execute_if(
190    interpreter: &mut AudioInterpreter,
191    condition: &Value,
192    body: &[Statement],
193    else_body: &Option<Vec<Statement>>,
194) -> Result<()> {
195    let condition_result = interpreter.evaluate_condition(condition)?;
196
197    if condition_result {
198        super::collector::collect_events(interpreter, body)?;
199    } else if let Some(else_stmts) = else_body {
200        super::collector::collect_events(interpreter, else_stmts)?;
201    }
202
203    Ok(())
204}
205
206pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
207    let handlers = interpreter.event_registry.get_handlers_matching(event_name);
208
209    for (index, handler) in handlers.iter().enumerate() {
210        if handler.once && !interpreter.event_registry.should_execute_once(event_name, index) {
211            continue;
212        }
213
214        let body_clone = handler.body.clone();
215        super::collector::collect_events(interpreter, &body_clone)?;
216    }
217
218    Ok(())
219}
220
221pub fn handle_assign(interpreter: &mut AudioInterpreter, target: &str, property: &str, value: &Value) -> Result<()> {
222    if let Some(var) = interpreter.variables.get_mut(target) {
223        if let Value::Map(map) = var {
224            map.insert(property.to_string(), value.clone());
225
226            if interpreter.events.synths.contains_key(target) {
227                let map_clone = map.clone();
228                let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
229                interpreter.events.synths.insert(target.to_string(), updated_def);
230                println!("🔧 Updated {}.{} = {:?}", target, property, value);
231            }
232        } else {
233            return Err(anyhow::anyhow!("Cannot assign property '{}' to non-map variable '{}'", property, target));
234        }
235    } else {
236        return Err(anyhow::anyhow!("Variable '{}' not found", target));
237    }
238
239    Ok(())
240}
241
242pub fn extract_synth_def_from_map(_interpreter: &AudioInterpreter, map: &HashMap<String, Value>) -> Result<crate::engine::audio::events::SynthDefinition> {
243    use crate::engine::audio::events::extract_filters;
244
245    let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
246    let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
247    let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
248    let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
249    let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
250
251    let synth_type = if let Some(Value::String(t)) = map.get("type") {
252        let clean = t.trim_matches('"').trim_matches('\'');
253        if clean.is_empty() || clean == "synth" { None } else { Some(clean.to_string()) }
254    } else { None };
255
256    let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") { extract_filters(filters_arr) } else { Vec::new() };
257
258    let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") { Some(s.clone()) } else { None };
259    let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") { Some(s.clone()) } else { None };
260    let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") { Some(s.clone()) } else { None };
261
262    let mut options = HashMap::new();
263    for (key, val) in map.iter() {
264        if ![
265            "waveform", "attack", "decay", "sustain", "release", "type", "filters",
266            "plugin_author", "plugin_name", "plugin_export",
267        ].contains(&key.as_str())
268        {
269            if let Value::Number(n) = val {
270                options.insert(key.clone(), *n);
271            } else if let Value::String(s) = val {
272                if key == "waveform" || key.starts_with("_") { continue; }
273                if let Ok(n) = s.parse::<f32>() { options.insert(key.clone(), n); }
274            }
275        }
276    }
277
278    Ok(crate::engine::audio::events::SynthDefinition {
279        waveform,
280        attack,
281        decay,
282        sustain,
283        release,
284        synth_type,
285        filters,
286        options,
287        plugin_author,
288        plugin_name,
289        plugin_export,
290    })
291}
292
293pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
294    use std::path::Path;
295
296    let path = Path::new(source);
297    // Determine extension
298    if let Some(ext) = path.extension().and_then(|s| s.to_str()).map(|s| s.to_lowercase()) {
299        match ext.as_str() {
300            "mid" | "midi" => {
301                use crate::engine::audio::midi::load_midi_file;
302                let midi_data = load_midi_file(path)?;
303                interpreter.variables.insert(alias.to_string(), midi_data);
304                        // MIDI file loaded (silent)
305                Ok(())
306            }
307            "wav" | "flac" | "mp3" | "ogg" => {
308                // For now support WAV via existing parser; other formats may be supported later.
309                use crate::engine::audio::samples;
310                let registered = samples::register_sample_from_path(path)?;
311                // Record the sample URI under the alias variable as a string (consistent with triggers)
312                interpreter.variables.insert(alias.to_string(), Value::String(registered.clone()));
313                        // Sample file loaded (silent)
314                Ok(())
315            }
316            _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
317        }
318    } else {
319        Err(anyhow::anyhow!("Cannot determine file extension for {}", source))
320    }
321}
322
323pub fn handle_bind(interpreter: &mut AudioInterpreter, source: &str, target: &str, options: &Value) -> Result<()> {
324    let midi_data = interpreter.variables.get(source).ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?.clone();
325
326    if let Value::Map(midi_map) = &midi_data {
327        let notes = midi_map.get("notes").ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
328
329        if let Value::Array(notes_array) = notes {
330            let _synth_def = interpreter.events.synths.get(target).ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?.clone();
331
332            let default_velocity = 100;
333            let mut velocity = default_velocity;
334
335            if let Value::Map(opts) = options {
336                if let Some(Value::Number(v)) = opts.get("velocity") { velocity = *v as u8; }
337            }
338
339            // Determine MIDI file BPM (if present) so we can rescale times to interpreter BPM
340            // Default to interpreter.bpm when the MIDI file has no BPM metadata
341            let midi_bpm = crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
342
343            for note_val in notes_array {
344                if let Value::Map(note_map) = note_val {
345                    let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
346                    let note = crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
347                    let note_velocity = crate::engine::audio::events::extract_number(note_map, "velocity", velocity as f32) as u8;
348                    // Duration may be present (ms) from MIDI loader; fallback to 500 ms
349                    let duration_ms = crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
350
351                    use crate::engine::audio::events::AudioEvent;
352                    let synth_def = interpreter.events.get_synth(target).cloned().unwrap_or_default();
353                    // Rescale times according to interpreter BPM vs MIDI file BPM.
354                    // If midi_bpm == interpreter.bpm this is a no-op. We compute factor = midi_bpm / interpreter.bpm
355                    let interp_bpm = interpreter.bpm;
356                    let factor = if interp_bpm > 0.0 { midi_bpm / interp_bpm } else { 1.0 };
357
358                    let start_time_s = (time / 1000.0) * factor;
359                    let duration_s = (duration_ms / 1000.0) * factor;
360
361                    let event = AudioEvent::Note {
362                        midi: note,
363                        start_time: start_time_s,
364                        duration: duration_s,
365                        velocity: note_velocity as f32,
366                        synth_id: target.to_string(),
367                        synth_def,
368                        pan: 0.0,
369                        detune: 0.0,
370                        gain: 1.0,
371                        attack: None,
372                        release: None,
373                        delay_time: None,
374                        delay_feedback: None,
375                        delay_mix: None,
376                        reverb_amount: None,
377                        drive_amount: None,
378                        drive_color: None,
379                    };
380
381                    // Diagnostic: log each scheduled note from bind (midi, time ms, start_time sec)
382                    // bound note scheduled
383                    interpreter.events.events.push(event);
384                }
385            }
386
387            // Bound notes from source to target
388        }
389    }
390
391    Ok(())
392}
393
394pub fn handle_use_plugin(interpreter: &mut AudioInterpreter, author: &str, name: &str, alias: &str) -> Result<()> {
395    use crate::engine::plugin::loader::load_plugin;
396
397    match load_plugin(author, name) {
398        Ok((info, _wasm_bytes)) => {
399            let mut plugin_map = HashMap::new();
400            plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
401            plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
402            plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
403
404            if let Some(version) = &info.version { plugin_map.insert("_version".to_string(), Value::String(version.clone())); }
405
406            for export in &info.exports {
407                let mut export_map = HashMap::new();
408                export_map.insert("_plugin_author".to_string(), Value::String(info.author.clone()));
409                export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
410                export_map.insert("_export_name".to_string(), Value::String(export.name.clone()));
411                export_map.insert("_export_kind".to_string(), Value::String(export.kind.clone()));
412
413                plugin_map.insert(export.name.clone(), Value::Map(export_map));
414            }
415
416            interpreter.variables.insert(alias.to_string(), Value::Map(plugin_map));
417            println!("🔌 Plugin loaded: {}.{} as {}", author, name, alias);
418        }
419        Err(e) => {
420            eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
421            return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
422        }
423    }
424
425    Ok(())
426}
427
428pub fn handle_bank(interpreter: &mut AudioInterpreter, name: &str, alias: &Option<String>) -> Result<()> {
429    let target_alias = alias.clone().unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
430
431    if let Some(existing_value) = interpreter.variables.get(name) {
432        interpreter.variables.insert(target_alias.clone(), existing_value.clone());
433        #[cfg(not(feature = "wasm"))]
434        println!("🏦 Bank alias created: {} -> {}", name, target_alias);
435    } else {
436        #[cfg(feature = "wasm")]
437        {
438            use crate::web::registry::banks::REGISTERED_BANKS;
439            REGISTERED_BANKS.with(|banks| {
440                for bank in banks.borrow().iter() {
441                            if bank.full_name == *name {
442                        if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
443                            interpreter.variables.insert(target_alias.clone(), Value::Map(bank_map.clone()));
444                        }
445                    }
446                }
447            });
448        }
449
450        #[cfg(not(feature = "wasm"))]
451        {
452            if let Ok(current_dir) = std::env::current_dir() {
453                match interpreter.banks.register_bank(target_alias.clone(), &name, &current_dir, &current_dir) {
454                    Ok(_) => {
455                        let mut bank_map = HashMap::new();
456                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
457                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
458                        interpreter.variables.insert(target_alias.clone(), Value::Map(bank_map));
459                        println!("🏦 Bank registered: {} as {}", name, target_alias);
460                    }
461                    Err(e) => {
462                        eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
463                        let mut bank_map = HashMap::new();
464                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
465                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
466                        interpreter.variables.insert(target_alias.clone(), Value::Map(bank_map));
467                    }
468                }
469            } else {
470                let mut bank_map = HashMap::new();
471                bank_map.insert("_name".to_string(), Value::String(name.to_string()));
472                bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
473                interpreter.variables.insert(target_alias.clone(), Value::Map(bank_map));
474                eprintln!("⚠️ Could not determine cwd to register bank '{}', registered minimal alias.", name);
475            }
476        }
477    }
478
479    #[cfg(not(feature = "wasm"))]
480            // Bank handling completed
481
482    Ok(())
483}
484
485pub fn handle_trigger(interpreter: &mut AudioInterpreter, entity: &str) -> Result<()> {
486    let resolved_entity = if entity.starts_with('.') { &entity[1..] } else { entity };
487
488    if resolved_entity.contains('.') {
489        let parts: Vec<&str> = resolved_entity.split('.').collect();
490        if parts.len() == 2 {
491            let (var_name, property) = (parts[0], parts[1]);
492
493            if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
494                if let Some(Value::String(sample_uri)) = map.get(property) {
495                    let uri = sample_uri.trim_matches('"').trim_matches('\'');
496                    println!("🎵 Trigger: {}.{} -> {} at {:.3}s", var_name, property, uri, interpreter.cursor_time);
497                    interpreter.events.add_sample_event(uri, interpreter.cursor_time, 1.0);
498                    let beat_duration = interpreter.beat_duration();
499                    interpreter.cursor_time += beat_duration;
500                        } else {
501                    #[cfg(not(feature = "wasm"))]
502                    {
503                        println!("🔍 Attempting to resolve trigger: {}", resolved_entity);
504                        // First try to produce an internal devalang://bank URI (preferred, supports lazy loading)
505                        let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
506                        if resolved_uri != resolved_entity {
507                            println!("🎵 Resolved via variable: {} -> {}", resolved_entity, resolved_uri);
508                            interpreter.events.add_sample_event(&resolved_uri, interpreter.cursor_time, 1.0);
509                            let beat_duration = interpreter.beat_duration();
510                            interpreter.cursor_time += beat_duration;
511                        } else if let Some(pathbuf) = interpreter.banks.resolve_trigger(var_name, property) {
512                            if let Some(path_str) = pathbuf.to_str() {
513                                println!("🎵 Resolved (file): {}.{} -> {}", var_name, property, path_str);
514                                interpreter.events.add_sample_event(path_str, interpreter.cursor_time, 1.0);
515                                let beat_duration = interpreter.beat_duration();
516                                interpreter.cursor_time += beat_duration;
517                            } else {
518                                println!("⚠️ Resolution failed for {}.{} (invalid path)", var_name, property);
519                            }
520                        } else {
521                            println!("⚠️ No path found for {} via BankRegistry", resolved_entity);
522                        }
523                    }
524                }
525            }
526        }
527    } else {
528        if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
529            let uri = sample_uri.trim_matches('"').trim_matches('\'');
530            println!("🎵 Trigger: {} -> {} at {:.3}s", resolved_entity, uri, interpreter.cursor_time);
531            interpreter.events.add_sample_event(uri, interpreter.cursor_time, 1.0);
532            let beat_duration = interpreter.beat_duration();
533            interpreter.cursor_time += beat_duration;
534        }
535    }
536
537    println!("🔄 Trigger interpreted: {}", entity);
538    #[cfg(not(feature = "wasm"))]
539    {
540        println!("🔍 Déclencheurs disponibles dans BankRegistry:");
541        for (bank_name, bank) in interpreter.banks.list_banks() {
542            println!("   Banque: {}", bank_name);
543            for trigger in bank.list_triggers() {
544                println!("      Déclencheur: {}", trigger);
545            }
546        }
547    }
548
549    // Note: do not call interpreter.render_audio() here - rendering is handled by the build pipeline.
550    // Trigger queued for rendering (events collected)
551
552    Ok(())
553}
554
555pub fn extract_pattern_data(_interpreter: &AudioInterpreter, value: &Value) -> (Option<String>, Option<HashMap<String, f32>>) {
556    match value {
557        Value::String(pattern) => (Some(pattern.clone()), None),
558        Value::Map(map) => {
559            let pattern = map.get("pattern").and_then(|v| {
560                if let Value::String(s) = v { Some(s.clone()) } else { None }
561            });
562
563            let mut options = HashMap::new();
564            for (key, val) in map.iter() {
565                if key != "pattern" {
566                    if let Value::Number(num) = val { options.insert(key.clone(), *num); }
567                }
568            }
569
570            let opts = if options.is_empty() { None } else { Some(options) };
571            (pattern, opts)
572        }
573        _ => (None, None),
574    }
575}
576
577pub fn execute_pattern(interpreter: &mut AudioInterpreter, target: &str, pattern: &str, options: Option<HashMap<String, f32>>) -> Result<()> {
578    use crate::engine::audio::events::AudioEvent;
579
580    let swing = options.as_ref().and_then(|o| o.get("swing").copied()).unwrap_or(0.0);
581    let humanize = options.as_ref().and_then(|o| o.get("humanize").copied()).unwrap_or(0.0);
582    let velocity_mult = options.as_ref().and_then(|o| o.get("velocity").copied()).unwrap_or(1.0);
583    let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
584
585    let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
586
587    let resolved_uri = resolve_sample_uri(interpreter, target);
588
589    let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
590    let step_count = pattern_chars.len() as f32;
591    if step_count == 0.0 { return Ok(()); }
592
593    let bar_duration = (60.0 / effective_bpm) * 4.0;
594    let step_duration = bar_duration / step_count;
595
596    for (i, &ch) in pattern_chars.iter().enumerate() {
597        if ch == 'x' || ch == 'X' {
598            let mut time = interpreter.cursor_time + (i as f32 * step_duration);
599            if swing > 0.0 && i % 2 == 1 { time += step_duration * swing; }
600
601            #[cfg(any(feature = "cli", feature = "wasm"))]
602            if humanize > 0.0 {
603                use rand::Rng;
604                let mut rng = rand::thread_rng();
605                let offset = rng.gen_range(-humanize..humanize);
606                time += offset;
607            }
608
609            let event = AudioEvent::Sample { uri: resolved_uri.clone(), start_time: time, velocity: 100.0 * velocity_mult };
610            interpreter.events.events.push(event);
611        }
612    }
613
614    interpreter.cursor_time += bar_duration;
615    Ok(())
616}
617
618pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
619    if let Some(dot_pos) = target.find('.') {
620        let bank_alias = &target[..dot_pos];
621        let trigger_name = &target[dot_pos + 1..];
622        if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
623            if let Some(Value::String(bank_name)) = bank_map.get("_name") {
624                return format!("devalang://bank/{}/{}", bank_name, trigger_name);
625            }
626        }
627    }
628    target.to_string()
629}