1use 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 if let Value::Map(map) = value {
12 if map.contains_key("waveform") || map.contains_key("_plugin_ref") {
13 let mut is_plugin = false;
15 let mut plugin_author: Option<String> = None;
16 let mut plugin_name: Option<String> = None;
17 let mut plugin_export: Option<String> = None;
18
19 if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
20 let parts: Vec<&str> = plugin_ref.split('.').collect();
21 if parts.len() == 2 {
22 let (var_name, prop_name) = (parts[0], parts[1]);
23 if let Some(var_value) = interpreter.variables.get(var_name) {
24 if let Value::Map(var_map) = var_value {
25 if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
26 if resolved_plugin.starts_with("plugin:") {
27 let ref_parts: Vec<&str> =
28 resolved_plugin["plugin:".len()..].split(':').collect();
29 if ref_parts.len() == 2 {
30 let full_plugin_name = ref_parts[0];
31 let export_name = ref_parts[1];
32 let plugin_parts: Vec<&str> =
33 full_plugin_name.split('.').collect();
34 if plugin_parts.len() == 2 {
35 plugin_author = Some(plugin_parts[0].to_string());
36 plugin_name = Some(plugin_parts[1].to_string());
37 plugin_export = Some(export_name.to_string());
38 is_plugin = true;
39 }
40 }
41 }
42 } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
43 if let (
44 Some(Value::String(author)),
45 Some(Value::String(name)),
46 Some(Value::String(export)),
47 ) = (
48 export_map.get("_plugin_author"),
49 export_map.get("_plugin_name"),
50 export_map.get("_export_name"),
51 ) {
52 plugin_author = Some(author.clone());
53 plugin_name = Some(name.clone());
54 plugin_export = Some(export.clone());
55 is_plugin = true;
56 }
57 }
58 }
59 }
60 }
61 }
62
63 let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
64 let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
65 let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
66 let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
67 let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
68
69 let synth_type = if let Some(Value::String(t)) = map.get("type") {
70 let clean = t.trim_matches('"').trim_matches('\'');
71 if clean.is_empty() || clean == "synth" {
72 None
73 } else {
74 Some(clean.to_string())
75 }
76 } else {
77 None
78 };
79
80 let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
81 crate::engine::audio::events::extract_filters(filters_arr)
82 } else {
83 Vec::new()
84 };
85
86 let mut options = std::collections::HashMap::new();
87 let reserved_keys = if is_plugin {
88 vec![
89 "attack",
90 "decay",
91 "sustain",
92 "release",
93 "type",
94 "filters",
95 "_plugin_ref",
96 ]
97 } else {
98 vec![
99 "waveform",
100 "attack",
101 "decay",
102 "sustain",
103 "release",
104 "type",
105 "filters",
106 "_plugin_ref",
107 ]
108 };
109
110 for (key, val) in map.iter() {
111 if !reserved_keys.contains(&key.as_str()) {
112 match val {
113 Value::Number(n) => {
114 options.insert(key.clone(), *n);
115 }
116 Value::String(s) => {
117 if is_plugin && key == "waveform" {
118 let waveform_id = match s
119 .trim_matches('"')
120 .trim_matches('\'')
121 .to_lowercase()
122 .as_str()
123 {
124 "sine" => 0.0,
125 "saw" => 1.0,
126 "square" => 2.0,
127 "triangle" => 3.0,
128 _ => 1.0,
129 };
130 options.insert(key.clone(), waveform_id);
131 }
132 }
133 _ => {}
134 }
135 }
136 }
137
138 if is_plugin && map.contains_key("decay") {
139 options.insert("decay".to_string(), decay);
140 }
141
142 let final_waveform = if is_plugin {
143 "plugin".to_string()
144 } else {
145 waveform
146 };
147
148 let synth_def = crate::engine::audio::events::SynthDefinition {
149 waveform: final_waveform,
150 attack,
151 decay,
152 sustain,
153 release,
154 synth_type,
155 filters,
156 options,
157 plugin_author,
158 plugin_name,
159 plugin_export,
160 };
161
162 interpreter.events.add_synth(name.to_string(), synth_def);
163 }
164 }
165
166 interpreter
167 .variables
168 .insert(name.to_string(), value.clone());
169 Ok(())
170}
171
172pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str) -> Result<()> {
173 if let Some(pattern_value) = interpreter.variables.get(name).cloned() {
184 if let Value::Statement(stmt_box) = pattern_value {
185 if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
186 if let Some(tgt) = target.as_ref() {
187 let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
188 if let Some(pat) = pattern_str {
189 interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
190 return Ok(());
191 }
192 }
193 }
194 }
195 }
196
197 if let Some(body) = interpreter.groups.get(name).cloned() {
198 super::collector::collect_events(interpreter, &body)?;
199 } else {
200 println!("⚠️ Warning: Group or pattern '{}' not found", name);
201 }
202
203 Ok(())
204}
205
206pub fn execute_print(interpreter: &AudioInterpreter, value: &Value) -> Result<()> {
207 let message = match value {
208 Value::String(s) => {
209 if s.contains('{') && s.contains('}') {
210 interpreter.interpolate_string(s)
211 } else {
212 s.clone()
213 }
214 }
215 Value::Identifier(id) => {
216 if let Some(v) = interpreter.variables.get(id) {
218 match v {
219 Value::String(s) => s.clone(),
220 Value::Number(n) => n.to_string(),
221 Value::Boolean(b) => b.to_string(),
222 Value::Array(arr) => format!("{:?}", arr),
223 Value::Map(map) => format!("{:?}", map),
224 _ => format!("{:?}", v),
225 }
226 } else {
227 format!("Identifier(\"{}\")", id)
228 }
229 }
230 Value::Number(n) => n.to_string(),
231 Value::Boolean(b) => b.to_string(),
232 Value::Array(arr) => format!("{:?}", arr),
233 Value::Map(map) => format!("{:?}", map),
234 _ => format!("{:?}", value),
235 };
236
237 println!("💬 {}", message);
238 Ok(())
239}
240
241pub fn execute_if(
242 interpreter: &mut AudioInterpreter,
243 condition: &Value,
244 body: &[Statement],
245 else_body: &Option<Vec<Statement>>,
246) -> Result<()> {
247 let condition_result = interpreter.evaluate_condition(condition)?;
248
249 if condition_result {
250 super::collector::collect_events(interpreter, body)?;
251 } else if let Some(else_stmts) = else_body {
252 super::collector::collect_events(interpreter, else_stmts)?;
253 }
254
255 Ok(())
256}
257
258pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
259 let handlers = interpreter.event_registry.get_handlers_matching(event_name);
260
261 for (index, handler) in handlers.iter().enumerate() {
262 if handler.once
263 && !interpreter
264 .event_registry
265 .should_execute_once(event_name, index)
266 {
267 continue;
268 }
269
270 let body_clone = handler.body.clone();
271 super::collector::collect_events(interpreter, &body_clone)?;
272 }
273
274 Ok(())
275}
276
277pub fn handle_assign(
278 interpreter: &mut AudioInterpreter,
279 target: &str,
280 property: &str,
281 value: &Value,
282) -> Result<()> {
283 if let Some(var) = interpreter.variables.get_mut(target) {
284 if let Value::Map(map) = var {
285 map.insert(property.to_string(), value.clone());
286
287 if interpreter.events.synths.contains_key(target) {
288 let map_clone = map.clone();
289 let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
290 interpreter
291 .events
292 .synths
293 .insert(target.to_string(), updated_def);
294 }
295 } else {
296 return Err(anyhow::anyhow!(
297 "Cannot assign property '{}' to non-map variable '{}'",
298 property,
299 target
300 ));
301 }
302 } else {
303 return Err(anyhow::anyhow!("Variable '{}' not found", target));
304 }
305
306 Ok(())
307}
308
309pub fn extract_synth_def_from_map(
310 _interpreter: &AudioInterpreter,
311 map: &HashMap<String, Value>,
312) -> Result<crate::engine::audio::events::SynthDefinition> {
313 use crate::engine::audio::events::extract_filters;
314
315 let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
316 let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
317 let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
318 let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
319 let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
320
321 let synth_type = if let Some(Value::String(t)) = map.get("type") {
322 let clean = t.trim_matches('"').trim_matches('\'');
323 if clean.is_empty() || clean == "synth" {
324 None
325 } else {
326 Some(clean.to_string())
327 }
328 } else {
329 None
330 };
331
332 let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
333 extract_filters(filters_arr)
334 } else {
335 Vec::new()
336 };
337
338 let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
339 Some(s.clone())
340 } else {
341 None
342 };
343 let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
344 Some(s.clone())
345 } else {
346 None
347 };
348 let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
349 Some(s.clone())
350 } else {
351 None
352 };
353
354 let mut options = HashMap::new();
355 for (key, val) in map.iter() {
356 if ![
357 "waveform",
358 "attack",
359 "decay",
360 "sustain",
361 "release",
362 "type",
363 "filters",
364 "plugin_author",
365 "plugin_name",
366 "plugin_export",
367 ]
368 .contains(&key.as_str())
369 {
370 if let Value::Number(n) = val {
371 options.insert(key.clone(), *n);
372 } else if let Value::String(s) = val {
373 if key == "waveform" || key.starts_with("_") {
374 continue;
375 }
376 if let Ok(n) = s.parse::<f32>() {
377 options.insert(key.clone(), n);
378 }
379 }
380 }
381 }
382
383 Ok(crate::engine::audio::events::SynthDefinition {
384 waveform,
385 attack,
386 decay,
387 sustain,
388 release,
389 synth_type,
390 filters,
391 options,
392 plugin_author,
393 plugin_name,
394 plugin_export,
395 })
396}
397
398pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
399 use std::path::Path;
400
401 let path = Path::new(source);
402 if let Some(ext) = path
404 .extension()
405 .and_then(|s| s.to_str())
406 .map(|s| s.to_lowercase())
407 {
408 match ext.as_str() {
409 "mid" | "midi" => {
410 use crate::engine::audio::midi::load_midi_file;
411 let midi_data = load_midi_file(path)?;
412 interpreter.variables.insert(alias.to_string(), midi_data);
413 Ok(())
415 }
416 "wav" | "flac" | "mp3" | "ogg" => {
417 #[cfg(feature = "cli")]
419 {
420 use crate::engine::audio::samples;
421 let registered = samples::register_sample_from_path(path)?;
422 interpreter
424 .variables
425 .insert(alias.to_string(), Value::String(registered.clone()));
426 return Ok(());
428 }
429
430 #[cfg(not(feature = "cli"))]
432 {
433 interpreter
434 .variables
435 .insert(alias.to_string(), Value::String(source.to_string()));
436 return Ok(());
437 }
438 }
439 _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
440 }
441 } else {
442 Err(anyhow::anyhow!(
443 "Cannot determine file extension for {}",
444 source
445 ))
446 }
447}
448
449pub fn handle_bind(
450 interpreter: &mut AudioInterpreter,
451 source: &str,
452 target: &str,
453 options: &Value,
454) -> Result<()> {
455 use std::collections::HashMap as StdHashMap;
456
457 if source.starts_with("mapping.") || target.starts_with("mapping.") {
464 let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
466 m.clone()
467 } else {
468 StdHashMap::new()
469 };
470
471 fn create_and_insert(
473 path: &str,
474 opts_map: &StdHashMap<String, Value>,
475 interpreter: &mut AudioInterpreter,
476 ) -> Option<(String, String)> {
477 let parts: Vec<&str> = path.split('.').collect();
478 if parts.len() >= 3 {
479 let direction = parts[1]; let device = parts[2];
481
482 let mut map = StdHashMap::new();
483 map.insert(
484 "_type".to_string(),
485 Value::String("midi_mapping".to_string()),
486 );
487 map.insert(
488 "direction".to_string(),
489 Value::String(direction.to_string()),
490 );
491 map.insert("device".to_string(), Value::String(device.to_string()));
492
493 for (k, v) in opts_map.iter() {
495 map.insert(k.clone(), v.clone());
496 }
497
498 interpreter
499 .variables
500 .insert(path.to_string(), Value::Map(map.clone()));
501
502 let note_on = format!("mapping.{}.{}.noteOn", direction, device);
504 let note_off = format!("mapping.{}.{}.noteOff", direction, device);
505 let rest = format!("mapping.{}.{}.rest", direction, device);
506 interpreter
507 .variables
508 .insert(note_on.clone(), Value::String(note_on.clone()));
509 interpreter
510 .variables
511 .insert(note_off.clone(), Value::String(note_off.clone()));
512 interpreter
513 .variables
514 .insert(rest.clone(), Value::String(rest.clone()));
515
516 return Some((direction.to_string(), device.to_string()));
517 }
518 None
519 }
520
521 if source.starts_with("mapping.") {
523 if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
524 if !target.starts_with("mapping.") {
526 let mut bmap = StdHashMap::new();
527 bmap.insert("instrument".to_string(), Value::String(target.to_string()));
528 bmap.insert("direction".to_string(), Value::String(direction.clone()));
529 bmap.insert("device".to_string(), Value::String(device.clone()));
530 for (k, v) in opts_map.iter() {
531 bmap.insert(k.clone(), v.clone());
532 }
533 interpreter
534 .variables
535 .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
536
537 #[cfg(feature = "cli")]
539 if let Some(manager) = &mut interpreter.midi_manager {
540 if let Some(Value::Number(port_num)) = opts_map.get("port") {
541 let idx = *port_num as usize;
542 if let Ok(mut mgr) = manager.lock() {
543 let _ = mgr.open_input_by_index(idx, &device);
545 }
546 }
547 }
548 }
549 }
550 }
551
552 if target.starts_with("mapping.") {
554 if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
555 if !source.starts_with("mapping.") {
556 let mut bmap = StdHashMap::new();
557 bmap.insert("source".to_string(), Value::String(source.to_string()));
558 bmap.insert("direction".to_string(), Value::String(direction.clone()));
559 bmap.insert("device".to_string(), Value::String(device.clone()));
560 for (k, v) in opts_map.iter() {
561 bmap.insert(k.clone(), v.clone());
562 }
563 interpreter
564 .variables
565 .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
566
567 #[cfg(feature = "cli")]
569 if let Some(manager) = &mut interpreter.midi_manager {
570 if let Some(Value::Number(port_num)) = opts_map.get("port") {
571 let idx = *port_num as usize;
572 if let Ok(mut mgr) = manager.lock() {
573 let _ = mgr.open_output_by_name(&device, idx);
574 }
575 }
576 }
577 }
578 }
579 }
580
581 return Ok(());
586 }
587
588 let midi_data = interpreter
590 .variables
591 .get(source)
592 .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
593 .clone();
594
595 if let Value::Map(midi_map) = &midi_data {
596 let notes = midi_map
597 .get("notes")
598 .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
599
600 if let Value::Array(notes_array) = notes {
601 let _synth_def = interpreter
602 .events
603 .synths
604 .get(target)
605 .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
606 .clone();
607
608 let default_velocity = 100;
609 let mut velocity = default_velocity;
610
611 if let Value::Map(opts) = options {
612 if let Some(Value::Number(v)) = opts.get("velocity") {
613 velocity = *v as u8;
614 }
615 }
616
617 let midi_bpm =
620 crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
621
622 for note_val in notes_array {
623 if let Value::Map(note_map) = note_val {
624 let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
625 let note =
626 crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
627 let note_velocity = crate::engine::audio::events::extract_number(
628 note_map,
629 "velocity",
630 velocity as f32,
631 ) as u8;
632 let duration_ms =
634 crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
635
636 use crate::engine::audio::events::AudioEvent;
637 let synth_def = interpreter
638 .events
639 .get_synth(target)
640 .cloned()
641 .unwrap_or_default();
642 let interp_bpm = interpreter.bpm;
645 let factor = if interp_bpm > 0.0 {
646 midi_bpm / interp_bpm
647 } else {
648 1.0
649 };
650
651 let start_time_s = (time / 1000.0) * factor;
652 let duration_s = (duration_ms / 1000.0) * factor;
653
654 let event = AudioEvent::Note {
655 midi: note,
656 start_time: start_time_s,
657 duration: duration_s,
658 velocity: note_velocity as f32,
659 synth_id: target.to_string(),
660 synth_def,
661 pan: 0.0,
662 detune: 0.0,
663 gain: 1.0,
664 attack: None,
665 release: None,
666 delay_time: None,
667 delay_feedback: None,
668 delay_mix: None,
669 reverb_amount: None,
670 drive_amount: None,
671 drive_color: None,
672 };
673
674 interpreter.events.events.push(event);
677 }
678 }
679
680 }
682 }
683
684 Ok(())
685}
686
687#[cfg(feature = "cli")]
688pub fn handle_use_plugin(
689 interpreter: &mut AudioInterpreter,
690 author: &str,
691 name: &str,
692 alias: &str,
693) -> Result<()> {
694 use crate::engine::plugin::loader::load_plugin;
695
696 match load_plugin(author, name) {
697 Ok((info, _wasm_bytes)) => {
698 let mut plugin_map = HashMap::new();
699 plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
700 plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
701 plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
702
703 if let Some(version) = &info.version {
704 plugin_map.insert("_version".to_string(), Value::String(version.clone()));
705 }
706
707 for export in &info.exports {
708 let mut export_map = HashMap::new();
709 export_map.insert(
710 "_plugin_author".to_string(),
711 Value::String(info.author.clone()),
712 );
713 export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
714 export_map.insert(
715 "_export_name".to_string(),
716 Value::String(export.name.clone()),
717 );
718 export_map.insert(
719 "_export_kind".to_string(),
720 Value::String(export.kind.clone()),
721 );
722
723 plugin_map.insert(export.name.clone(), Value::Map(export_map));
724 }
725
726 interpreter
727 .variables
728 .insert(alias.to_string(), Value::Map(plugin_map));
729 }
730 Err(e) => {
731 eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
732 return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
733 }
734 }
735
736 Ok(())
737}
738
739#[cfg(not(feature = "cli"))]
740pub fn handle_use_plugin(
741 interpreter: &mut AudioInterpreter,
742 author: &str,
743 name: &str,
744 alias: &str,
745) -> Result<()> {
746 let mut plugin_map = HashMap::new();
748 plugin_map.insert(
749 "_type".to_string(),
750 Value::String("plugin_stub".to_string()),
751 );
752 plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
753 plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
754 interpreter
755 .variables
756 .insert(alias.to_string(), Value::Map(plugin_map));
757 Ok(())
758}
759
760pub fn handle_bank(
761 interpreter: &mut AudioInterpreter,
762 name: &str,
763 alias: &Option<String>,
764) -> Result<()> {
765 let target_alias = alias
766 .clone()
767 .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
768
769 if let Some(existing_value) = interpreter.variables.get(name) {
770 interpreter
771 .variables
772 .insert(target_alias.clone(), existing_value.clone());
773 } else {
774 #[cfg(feature = "wasm")]
775 {
776 use crate::web::registry::banks::REGISTERED_BANKS;
777 REGISTERED_BANKS.with(|banks| {
778 for bank in banks.borrow().iter() {
779 if bank.full_name == *name {
780 if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
781 interpreter
782 .variables
783 .insert(target_alias.clone(), Value::Map(bank_map.clone()));
784 }
785 }
786 }
787 });
788 }
789
790 #[cfg(not(feature = "wasm"))]
791 {
792 if let Ok(current_dir) = std::env::current_dir() {
793 match interpreter.banks.register_bank(
794 target_alias.clone(),
795 &name,
796 ¤t_dir,
797 ¤t_dir,
798 ) {
799 Ok(_) => {
800 let mut bank_map = HashMap::new();
801 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
802 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
803 interpreter
804 .variables
805 .insert(target_alias.clone(), Value::Map(bank_map));
806 }
807 Err(e) => {
808 eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
809 let mut bank_map = HashMap::new();
810 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
811 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
812 interpreter
813 .variables
814 .insert(target_alias.clone(), Value::Map(bank_map));
815 }
816 }
817 } else {
818 let mut bank_map = HashMap::new();
819 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
820 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
821 interpreter
822 .variables
823 .insert(target_alias.clone(), Value::Map(bank_map));
824 eprintln!(
825 "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
826 name
827 );
828 }
829 }
830 }
831
832 Ok(())
833}
834
835pub fn handle_trigger(interpreter: &mut AudioInterpreter, entity: &str) -> Result<()> {
836 let resolved_entity = if entity.starts_with('.') {
837 &entity[1..]
838 } else {
839 entity
840 };
841
842 if resolved_entity.contains('.') {
843 let parts: Vec<&str> = resolved_entity.split('.').collect();
844 if parts.len() == 2 {
845 let (var_name, property) = (parts[0], parts[1]);
846
847 if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
848 if let Some(Value::String(sample_uri)) = map.get(property) {
849 let uri = sample_uri.trim_matches('"').trim_matches('\'');
850 interpreter
851 .events
852 .add_sample_event(uri, interpreter.cursor_time, 1.0);
853 let beat_duration = interpreter.beat_duration();
854 interpreter.cursor_time += beat_duration;
855 } else {
856 #[cfg(not(feature = "wasm"))]
857 {
858 let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
860 if resolved_uri != resolved_entity {
861 interpreter.events.add_sample_event(
862 &resolved_uri,
863 interpreter.cursor_time,
864 1.0,
865 );
866 let beat_duration = interpreter.beat_duration();
867 interpreter.cursor_time += beat_duration;
868 } else if let Some(pathbuf) =
869 interpreter.banks.resolve_trigger(var_name, property)
870 {
871 if let Some(path_str) = pathbuf.to_str() {
872 interpreter.events.add_sample_event(
873 path_str,
874 interpreter.cursor_time,
875 1.0,
876 );
877 let beat_duration = interpreter.beat_duration();
878 interpreter.cursor_time += beat_duration;
879 } else {
880 println!(
881 "⚠️ Resolution failed for {}.{} (invalid path)",
882 var_name, property
883 );
884 }
885 } else {
886 println!("⚠️ No path found for {} via BankRegistry", resolved_entity);
887 }
888 }
889 }
890 }
891 }
892 } else {
893 if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
894 let uri = sample_uri.trim_matches('"').trim_matches('\'');
895 interpreter
896 .events
897 .add_sample_event(uri, interpreter.cursor_time, 1.0);
898 let beat_duration = interpreter.beat_duration();
899 interpreter.cursor_time += beat_duration;
900 }
901 }
902
903 Ok(())
907}
908
909pub fn extract_pattern_data(
910 _interpreter: &AudioInterpreter,
911 value: &Value,
912) -> (Option<String>, Option<HashMap<String, f32>>) {
913 match value {
914 Value::String(pattern) => (Some(pattern.clone()), None),
915 Value::Map(map) => {
916 let pattern = map.get("pattern").and_then(|v| {
917 if let Value::String(s) = v {
918 Some(s.clone())
919 } else {
920 None
921 }
922 });
923
924 let mut options = HashMap::new();
925 for (key, val) in map.iter() {
926 if key != "pattern" {
927 if let Value::Number(num) = val {
928 options.insert(key.clone(), *num);
929 }
930 }
931 }
932
933 let opts = if options.is_empty() {
934 None
935 } else {
936 Some(options)
937 };
938 (pattern, opts)
939 }
940 _ => (None, None),
941 }
942}
943
944pub fn execute_pattern(
945 interpreter: &mut AudioInterpreter,
946 target: &str,
947 pattern: &str,
948 options: Option<HashMap<String, f32>>,
949) -> Result<()> {
950 use crate::engine::audio::events::AudioEvent;
951
952 let swing = options
953 .as_ref()
954 .and_then(|o| o.get("swing").copied())
955 .unwrap_or(0.0);
956 let humanize = options
957 .as_ref()
958 .and_then(|o| o.get("humanize").copied())
959 .unwrap_or(0.0);
960 let velocity_mult = options
961 .as_ref()
962 .and_then(|o| o.get("velocity").copied())
963 .unwrap_or(1.0);
964 let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
965
966 let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
967
968 let resolved_uri = resolve_sample_uri(interpreter, target);
969
970 let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
971 let step_count = pattern_chars.len() as f32;
972 if step_count == 0.0 {
973 return Ok(());
974 }
975
976 let bar_duration = (60.0 / effective_bpm) * 4.0;
977 let step_duration = bar_duration / step_count;
978
979 for (i, &ch) in pattern_chars.iter().enumerate() {
980 if ch == 'x' || ch == 'X' {
981 let mut time = interpreter.cursor_time + (i as f32 * step_duration);
982 if swing > 0.0 && i % 2 == 1 {
983 time += step_duration * swing;
984 }
985
986 #[cfg(any(feature = "cli", feature = "wasm"))]
987 if humanize > 0.0 {
988 use rand::Rng;
989 let mut rng = rand::thread_rng();
990 let offset = rng.gen_range(-humanize..humanize);
991 time += offset;
992 }
993
994 let event = AudioEvent::Sample {
995 uri: resolved_uri.clone(),
996 start_time: time,
997 velocity: velocity_mult, };
999 interpreter.events.events.push(event);
1000 }
1001 }
1002
1003 interpreter.cursor_time += bar_duration;
1004 Ok(())
1005}
1006
1007pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
1008 if let Some(dot_pos) = target.find('.') {
1009 let bank_alias = &target[..dot_pos];
1010 let trigger_name = &target[dot_pos + 1..];
1011 if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
1012 if let Some(Value::String(bank_name)) = bank_map.get("_name") {
1013 return format!("devalang://bank/{}/{}", bank_name, trigger_name);
1014 }
1015 }
1016 }
1017 target.to_string()
1018}