1use anyhow::Result;
3use std::collections::HashMap;
4
5use crate::engine::audio::events::AudioEvent;
6use crate::language::syntax::ast::{Statement, StatementKind, Value};
7
8use super::AudioInterpreter;
9
10pub fn handle_let(interpreter: &mut AudioInterpreter, name: &str, value: &Value) -> Result<()> {
11 if let Value::Map(orig_map) = value {
13 let mut map = orig_map.clone();
15
16 if let Some(synth_val) = map.get("synth_type").cloned() {
19 let should_replace = match map.get("type") {
20 Some(Value::String(s)) => {
21 let clean = s.trim_matches('"').trim_matches('\'');
22 clean == "synth" || clean.is_empty()
23 }
24 Some(Value::Identifier(id)) => {
25 let clean = id.trim_matches('"').trim_matches('\'');
26 clean == "synth" || clean.is_empty()
27 }
28 None => true,
29 _ => false,
30 };
31 if should_replace {
32 map.insert("type".to_string(), synth_val.clone());
33 map.remove("synth_type");
34 } else {
35 }
37 }
38
39 let chain_keys = ["params", "adsr", "envelope"];
41 for key in &chain_keys {
42 if let Some(Value::Map(submap)) = map.get(*key) {
43 let to_insert: Vec<(String, Value)> =
45 submap.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
46 for (k, v) in to_insert {
47 map.insert(k, v);
48 }
49 map.remove(*key);
50 }
51 }
52
53 map.entry("volume".to_string())
57 .or_insert(Value::Number(1.0));
58 map.entry("gain".to_string()).or_insert(Value::Number(1.0));
59 map.entry("pan".to_string()).or_insert(Value::Number(0.0));
60 map.entry("detune".to_string())
61 .or_insert(Value::Number(0.0));
62 map.entry("type".to_string())
64 .or_insert(Value::String("synth".to_string()));
65
66 crate::utils::props::ensure_default_properties(&mut map, Some("synth"));
68
69 if map.contains_key("waveform") || map.contains_key("_plugin_ref") {
70 let mut is_plugin = false;
72 let mut plugin_author: Option<String> = None;
73 let mut plugin_name: Option<String> = None;
74 let mut plugin_export: Option<String> = None;
75
76 if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
77 let parts: Vec<&str> = plugin_ref.split('.').collect();
78 if parts.len() == 2 {
79 let (var_name, prop_name_raw) = (parts[0], parts[1]);
80 let prop_name = prop_name_raw.trim_end_matches(';'); if let Some(var_value) = interpreter.variables.get(var_name) {
82 if let Value::Map(var_map) = var_value {
83 if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
84 if resolved_plugin.starts_with("plugin:") {
85 let ref_parts: Vec<&str> =
86 resolved_plugin["plugin:".len()..].split(':').collect();
87 if ref_parts.len() == 2 {
88 let full_plugin_name = ref_parts[0];
89 let export_name = ref_parts[1];
90 let plugin_parts: Vec<&str> =
91 full_plugin_name.split('.').collect();
92 if plugin_parts.len() == 2 {
93 plugin_author = Some(plugin_parts[0].to_string());
94 plugin_name = Some(plugin_parts[1].to_string());
95 plugin_export = Some(export_name.to_string());
96 is_plugin = true;
97 }
98 }
99 }
100 } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
101 if let (
102 Some(Value::String(author)),
103 Some(Value::String(name)),
104 Some(Value::String(export)),
105 ) = (
106 export_map.get("_plugin_author"),
107 export_map.get("_plugin_name"),
108 export_map.get("_export_name"),
109 ) {
110 plugin_author = Some(author.clone());
111 plugin_name = Some(name.clone());
112 plugin_export = Some(export.clone());
113 is_plugin = true;
114 }
115 }
116 }
117 }
118 }
119 }
120
121 let waveform = crate::engine::audio::events::extract_string(&map, "waveform", "sine");
122 let attack = crate::engine::audio::events::extract_number(&map, "attack", 0.01);
123 let decay = crate::engine::audio::events::extract_number(&map, "decay", 0.1);
124 let sustain = crate::engine::audio::events::extract_number(&map, "sustain", 0.7);
125 let release = crate::engine::audio::events::extract_number(&map, "release", 0.2);
126
127 let synth_type = if let Some(v) = map.get("type") {
129 match v {
130 Value::String(t) => {
131 let clean = t.trim_matches('"').trim_matches('\'');
132 if clean.is_empty() || clean == "synth" {
133 None
134 } else {
135 Some(clean.to_string())
136 }
137 }
138 Value::Identifier(id) => {
139 let clean = id.trim_matches('"').trim_matches('\'');
140 if clean.is_empty() || clean == "synth" {
141 None
142 } else {
143 Some(clean.to_string())
144 }
145 }
146 _ => None,
147 }
148 } else {
149 None
150 };
151
152 let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
153 crate::engine::audio::events::extract_filters(filters_arr)
154 } else {
155 Vec::new()
156 };
157
158 let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
160 use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
161
162 let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
164 n.to_string()
165 } else if let Some(Value::String(s)) = lfo_map.get("rate") {
166 s.clone()
167 } else {
168 "5.0".to_string() };
170 let rate = LfoRate::from_value(&rate_str);
171
172 let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
174 (*n).clamp(0.0, 1.0)
175 } else {
176 0.5 };
178
179 let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
181 s.clone()
182 } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
183 s.clone()
184 } else {
185 "sine".to_string() };
187 let lfo_waveform = LfoWaveform::from_str(&waveform_str);
188
189 let target = if let Some(Value::String(s)) = lfo_map.get("target") {
191 LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
192 } else {
193 LfoTarget::Volume };
195
196 let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
198 (*n).fract().abs() } else {
200 0.0 };
202
203 Some(LfoParams {
204 rate,
205 depth,
206 waveform: lfo_waveform,
207 target,
208 phase,
209 })
210 } else {
211 None
212 };
213
214 let mut options = std::collections::HashMap::new();
215 let reserved_keys = if is_plugin {
216 vec![
217 "attack",
218 "decay",
219 "sustain",
220 "release",
221 "type",
222 "filters",
223 "_plugin_ref",
224 "lfo",
225 "waveform", "pan",
228 "gain",
229 "volume",
230 "detune",
231 "spread",
232 "synth_type", "chain", "effects", ]
237 } else {
238 vec![
239 "waveform",
240 "attack",
241 "decay",
242 "sustain",
243 "release",
244 "type",
245 "filters",
246 "_plugin_ref",
247 "lfo",
248 "pan",
250 "gain",
251 "volume",
252 "detune",
253 "spread",
254 "synth_type", "chain", "effects", ]
259 };
260
261 for (key, val) in map.iter() {
262 if !reserved_keys.contains(&key.as_str()) {
263 match val {
264 Value::Number(n) => {
265 options.insert(key.clone(), *n);
266 }
267 Value::String(s) => {
268 if is_plugin && key == "waveform" {
269 let waveform_id = match s
270 .trim_matches('"')
271 .trim_matches('\'')
272 .to_lowercase()
273 .as_str()
274 {
275 "sine" => 0.0,
276 "saw" => 1.0,
277 "square" => 2.0,
278 "triangle" => 3.0,
279 _ => 1.0,
280 };
281 options.insert(key.clone(), waveform_id);
282 }
283 }
284 _ => {}
285 }
286 }
287 }
288
289 if is_plugin && map.contains_key("decay") {
290 options.insert("decay".to_string(), decay);
291 }
292
293 if let Some(Value::Array(chain_arr)) = map.get("chain") {
295 for chain_item in chain_arr {
296 if let Value::Map(chain_map) = chain_item {
297 if let Some(Value::String(method)) = chain_map.get("type") {
299 if let Some(Value::String(str_val)) = chain_map.get("value") {
301 if method == "decay" && str_val == "auto" {
302 options.insert("decay_mode".to_string(), 1.0); }
304 }
305 else if let Some(Value::Number(val)) = chain_map.get("value") {
307 options.insert(method.clone(), *val);
308 }
309 }
310 }
311 }
312 }
313
314 let final_waveform = if is_plugin {
315 "plugin".to_string()
316 } else {
317 waveform
318 };
319
320 let synth_def = crate::engine::audio::events::SynthDefinition {
321 waveform: final_waveform,
322 attack,
323 decay,
324 sustain,
325 release,
326 synth_type,
327 filters,
328 options,
329 plugin_author,
330 plugin_name,
331 plugin_export,
332 lfo,
333 };
334
335 interpreter.events.add_synth(name.to_string(), synth_def);
336 }
337
338 interpreter
341 .variables
342 .insert(name.to_string(), Value::Map(map.clone()));
343 return Ok(());
344 }
345
346 if let Value::Statement(stmt_box) = value {
351 if let StatementKind::Trigger {
352 entity,
353 duration,
354 effects,
355 } = &stmt_box.kind
356 {
357 let mut map = HashMap::new();
358 map.insert("kind".to_string(), Value::String("Trigger".to_string()));
359 map.insert("entity".to_string(), Value::String(entity.clone()));
360 map.insert("duration".to_string(), Value::Duration(duration.clone()));
361 if let Some(eff) = effects {
362 map.insert("effects".to_string(), eff.clone());
363 }
364
365 crate::utils::props::ensure_default_properties(&mut map, Some("trigger"));
367
368 interpreter
369 .variables
370 .insert(name.to_string(), Value::Map(map));
371 return Ok(());
372 }
373 }
374
375 interpreter
376 .variables
377 .insert(name.to_string(), value.clone());
378 Ok(())
379}
380
381pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str, args: &[Value]) -> Result<()> {
382 if let Some(var_val) = interpreter.variables.get(name).cloned() {
392 if let Value::Statement(stmt_box) = var_val {
393 if let StatementKind::Function {
394 name: _fname,
395 parameters,
396 body,
397 } = &stmt_box.kind
398 {
399 let vars_snapshot = interpreter.variables.clone();
401
402 for (i, param) in parameters.iter().enumerate() {
404 let bound = args.get(i).cloned().unwrap_or(Value::Null);
405 let bound_val = match bound {
407 Value::Identifier(ref id) => {
408 interpreter.resolve_value(&Value::Identifier(id.clone()))?
409 }
410 other => other,
411 };
412 interpreter.variables.insert(param.clone(), bound_val);
413 }
414
415 interpreter.function_call_depth += 1;
418 let exec_result = super::collector::collect_events(interpreter, body);
419 interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
421 exec_result?;
422
423 let mut captured_return: Option<Value> = None;
425 if interpreter.returning_flag {
426 captured_return = interpreter.return_value.clone();
427 interpreter.returning_flag = false;
429 interpreter.return_value = None;
430 }
431
432 interpreter.variables = vars_snapshot;
435
436 if let Some(rv) = captured_return {
440 interpreter.variables.insert("__return".to_string(), rv);
441 }
442
443 return Ok(());
444 }
445 if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
447 if let Some(tgt) = target.as_ref() {
448 let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
449 if let Some(pat) = pattern_str {
450 execute_pattern_with_source(
452 interpreter,
453 tgt.as_str(),
454 &pat,
455 options,
456 Some(name),
457 )?;
458 return Ok(());
459 }
460 }
461 }
462 }
463 }
464
465 if let Some(body) = interpreter.groups.get(name).cloned() {
467 super::collector::collect_events(interpreter, &body)?;
468 return Ok(());
469 }
470
471 println!(
472 "⚠️ Warning: Group, pattern or function '{}' not found",
473 name
474 );
475 Ok(())
476}
477
478pub fn call_function(
482 interpreter: &mut AudioInterpreter,
483 name: &str,
484 args: &[Value],
485) -> Result<Value> {
486 if let Some(var_val) = interpreter.variables.get(name).cloned() {
488 if let Value::Statement(stmt_box) = var_val {
489 if let StatementKind::Function {
490 name: _fname,
491 parameters,
492 body,
493 } = &stmt_box.kind
494 {
495 let vars_snapshot = interpreter.variables.clone();
497
498 for (i, param) in parameters.iter().enumerate() {
500 let bound = args.get(i).cloned().unwrap_or(Value::Null);
501 let bound_val = match bound {
503 Value::Identifier(ref id) => {
504 interpreter.resolve_value(&Value::Identifier(id.clone()))?
505 }
506 other => other,
507 };
508 interpreter.variables.insert(param.clone(), bound_val);
509 }
510
511 interpreter.function_call_depth += 1;
513
514 let exec_result = super::collector::collect_events(interpreter, body);
515 interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
516 exec_result?;
517
518 let mut captured_return: Option<Value> = None;
520 if interpreter.returning_flag {
521 captured_return = interpreter.return_value.clone();
522 interpreter.returning_flag = false;
523 interpreter.return_value = None;
524 }
525
526 interpreter.variables = vars_snapshot;
528
529 if let Some(rv) = captured_return {
530 return Ok(rv);
531 }
532
533 return Ok(Value::Null);
534 }
535 if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
537 if let Some(tgt) = target.as_ref() {
538 let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
539 if let Some(pat) = pattern_str {
540 interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
541 return Ok(Value::Null);
542 }
543 }
544 }
545 }
546 }
547
548 if let Some(body) = interpreter.groups.get(name).cloned() {
550 super::collector::collect_events(interpreter, &body)?;
551 return Ok(Value::Null);
552 }
553
554 println!(
555 "⚠️ Warning: Group, pattern or function '{}' not found",
556 name
557 );
558 Ok(Value::Null)
559}
560
561pub fn execute_print(interpreter: &mut AudioInterpreter, value: &Value) -> Result<()> {
562 let message = match value {
563 Value::Call { name: _, args: _ } => {
564 let resolved = interpreter.resolve_value(value)?;
566 interpreter.value_to_string(&resolved)
567 }
568
569 Value::String(s) => {
570 if s.contains('{') && s.contains('}') {
571 interpreter.interpolate_string(s)
572 } else {
573 s.clone()
574 }
575 }
576 Value::Identifier(id) => {
577 if id.ends_with("++") {
580 let varname = id[..id.len() - 2].trim();
581 let cur = match interpreter.variables.get(varname) {
583 Some(Value::Number(n)) => *n as isize,
584 _ => 0,
585 };
586 interpreter
588 .variables
589 .insert(varname.to_string(), Value::Number((cur + 1) as f32));
590 cur.to_string()
591 } else {
592 let resolved = interpreter.resolve_value(&Value::Identifier(id.clone()))?;
595 match resolved {
596 Value::String(s) => s.clone(),
597 Value::Number(n) => n.to_string(),
598 Value::Boolean(b) => b.to_string(),
599 Value::Array(arr) => format!("{:?}", arr),
600 Value::Map(map) => format!("{:?}", map),
601 Value::Null => format!("Identifier(\"{}\")", id),
602 other => format!("{:?}", other),
603 }
604 }
605 }
606 Value::Number(n) => n.to_string(),
607 Value::Boolean(b) => b.to_string(),
608 Value::Array(arr) => {
609 let mut parts = Vec::new();
611 for v in arr.iter() {
612 if let Value::Identifier(idtok) = v {
614 if idtok.ends_with("++") {
615 let varname = idtok[..idtok.len() - 2].trim();
616 let cur = match interpreter.variables.get(varname) {
617 Some(Value::Number(n)) => *n as isize,
618 _ => 0,
619 };
620 interpreter
621 .variables
622 .insert(varname.to_string(), Value::Number((cur + 1) as f32));
623 parts.push(cur.to_string());
624 continue;
625 }
626 }
627 let resolved = interpreter.resolve_value(v)?;
628 parts.push(interpreter.value_to_string(&resolved));
629 }
630 parts.join("")
631 }
632 Value::Map(map) => format!("{:?}", map),
633 _ => format!("{:?}", value),
634 };
635 let log_message = message.clone();
638 interpreter
640 .events
641 .add_log_event(log_message.clone(), interpreter.cursor_time);
642 if !interpreter.suppress_print {
647 #[cfg(feature = "cli")]
651 {
652 crate::tools::logger::Logger::new().print(message.clone());
653 }
654
655 #[cfg(not(feature = "cli"))]
658 {
659 if let Some(tx) = &interpreter.realtime_print_tx {
660 let _ = tx.send((interpreter.cursor_time, log_message.clone()));
661 }
662 }
663 } else {
664 if let Some(tx) = &interpreter.realtime_print_tx {
669 let _ = tx.send((interpreter.cursor_time, log_message.clone()));
671 }
672 }
673 Ok(())
674}
675
676pub fn execute_if(
677 interpreter: &mut AudioInterpreter,
678 condition: &Value,
679 body: &[Statement],
680 else_body: &Option<Vec<Statement>>,
681) -> Result<()> {
682 let condition_result = interpreter.evaluate_condition(condition)?;
683
684 if condition_result {
685 super::collector::collect_events(interpreter, body)?;
686 } else if let Some(else_stmts) = else_body {
687 super::collector::collect_events(interpreter, else_stmts)?;
688 }
689
690 Ok(())
691}
692
693pub fn execute_while(
694 interpreter: &mut AudioInterpreter,
695 condition: &Value,
696 body: &[Statement],
697) -> Result<()> {
698 let mut iter_count: usize = 0;
699 let hard_iter_cap: usize = 100_000; loop {
702 let condition_result = interpreter.evaluate_condition(condition)?;
703
704 if !condition_result {
705 break;
706 }
707
708 super::collector::collect_events(interpreter, body)?;
709
710 if interpreter.break_flag {
712 interpreter.break_flag = false;
713 break;
714 }
715
716 if let Some(deadline) = interpreter.schedule_deadline {
718 if interpreter.cursor_time >= deadline {
719 break;
720 }
721 }
722
723 iter_count = iter_count.saturating_add(1);
724 if iter_count > hard_iter_cap {
725 break;
726 }
727 }
728
729 Ok(())
730}
731
732pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
733 let handlers = interpreter.event_registry.get_handlers_matching(event_name);
734
735 for (index, handler) in handlers.iter().enumerate() {
736 if let Some(args) = &handler.args {
738 if let Some(num_val) = args.iter().find(|v| matches!(v, Value::Number(_))) {
739 if let Value::Number(n) = num_val {
740 let interval = (*n as usize).max(1);
741 if event_name == "beat" {
742 let cur = interpreter.special_vars.current_beat.floor() as usize;
743 if cur % interval != 0 {
744 continue;
745 }
746 } else if event_name == "bar" {
747 let cur = interpreter.special_vars.current_bar.floor() as usize;
748 if cur % interval != 0 {
749 continue;
750 }
751 }
752 }
753 }
754 }
755
756 if handler.once
757 && !interpreter
758 .event_registry
759 .should_execute_once(event_name, index)
760 {
761 continue;
762 }
763
764 let body_clone = handler.body.clone();
765 super::collector::collect_events(interpreter, &body_clone)?;
766 }
767
768 Ok(())
769}
770
771pub fn handle_assign(
772 interpreter: &mut AudioInterpreter,
773 target: &str,
774 property: &str,
775 value: &Value,
776) -> Result<()> {
777 if target.contains('.') {
780 let parts: Vec<&str> = target.split('.').collect();
781 let root = parts[0];
782 if let Some(root_val) = interpreter.variables.get_mut(root) {
783 let mut current = root_val;
785 for seg in parts.iter().skip(1) {
786 match current {
787 Value::Map(map) => {
788 if !map.contains_key(*seg) {
789 map.insert((*seg).to_string(), Value::Map(HashMap::new()));
791 }
792 current = map.get_mut(*seg).unwrap();
793 }
794 _ => {
795 return Err(anyhow::anyhow!(
796 "Cannot traverse into non-map segment '{}' when assigning to '{}'",
797 seg,
798 target
799 ));
800 }
801 }
802 }
803
804 if let Value::Map(map) = current {
806 map.insert(property.to_string(), value.clone());
807
808 if interpreter.events.synths.contains_key(root) {
810 if let Some(Value::Map(root_map)) = interpreter.variables.get(root) {
811 let map_clone = root_map.clone();
812 let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
813 interpreter
814 .events
815 .synths
816 .insert(root.to_string(), updated_def);
817 }
818 }
819 } else {
820 return Err(anyhow::anyhow!(
821 "Cannot assign property '{}' to non-map target '{}'",
822 property,
823 target
824 ));
825 }
826 } else {
827 return Err(anyhow::anyhow!("Variable '{}' not found", root));
828 }
829 } else {
830 if let Some(var) = interpreter.variables.get_mut(target) {
831 if let Value::Map(map) = var {
832 map.insert(property.to_string(), value.clone());
833
834 if interpreter.events.synths.contains_key(target) {
835 let map_clone = map.clone();
836 let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
837 interpreter
838 .events
839 .synths
840 .insert(target.to_string(), updated_def);
841 }
842 } else {
843 return Err(anyhow::anyhow!(
844 "Cannot assign property '{}' to non-map variable '{}'",
845 property,
846 target
847 ));
848 }
849 } else {
850 return Err(anyhow::anyhow!("Variable '{}' not found", target));
851 }
852 }
853
854 Ok(())
855}
856
857pub fn extract_synth_def_from_map(
858 _interpreter: &AudioInterpreter,
859 map: &HashMap<String, Value>,
860) -> Result<crate::engine::audio::events::SynthDefinition> {
861 use crate::engine::audio::events::extract_filters;
862 use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
863
864 let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
865 let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
866 let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
867 let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
868 let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
869
870 let synth_type = if let Some(v) = map.get("type") {
872 match v {
873 Value::String(t) => {
874 let clean = t.trim_matches('"').trim_matches('\'');
875 if clean.is_empty() || clean == "synth" {
876 None
877 } else {
878 Some(clean.to_string())
879 }
880 }
881 Value::Identifier(id) => {
882 let clean = id.trim_matches('"').trim_matches('\'');
883 if clean.is_empty() || clean == "synth" {
884 None
885 } else {
886 Some(clean.to_string())
887 }
888 }
889 _ => None,
890 }
891 } else if let Some(v2) = map.get("synth_type") {
892 match v2 {
893 Value::String(t2) => {
894 let clean = t2.trim_matches('"').trim_matches('\'');
895 if clean.is_empty() || clean == "synth" {
896 None
897 } else {
898 Some(clean.to_string())
899 }
900 }
901 Value::Identifier(id2) => {
902 let clean = id2.trim_matches('"').trim_matches('\'');
903 if clean.is_empty() || clean == "synth" {
904 None
905 } else {
906 Some(clean.to_string())
907 }
908 }
909 _ => None,
910 }
911 } else {
912 None
913 };
914
915 let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
916 extract_filters(filters_arr)
917 } else {
918 Vec::new()
919 };
920
921 let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
922 Some(s.clone())
923 } else {
924 None
925 };
926 let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
927 Some(s.clone())
928 } else {
929 None
930 };
931 let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
932 Some(s.clone())
933 } else {
934 None
935 };
936
937 let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
939 let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
941 n.to_string()
942 } else if let Some(Value::String(s)) = lfo_map.get("rate") {
943 s.clone()
944 } else {
945 "5.0".to_string() };
947 let rate = LfoRate::from_value(&rate_str);
948
949 let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
951 (*n).clamp(0.0, 1.0)
952 } else {
953 0.5 };
955
956 let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
958 s.clone()
959 } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
960 s.clone()
961 } else {
962 "sine".to_string() };
964 let waveform = LfoWaveform::from_str(&waveform_str);
965
966 let target = if let Some(Value::String(s)) = lfo_map.get("target") {
968 LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
969 } else {
970 LfoTarget::Volume };
972
973 let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
975 (*n).fract().abs() } else {
977 0.0 };
979
980 Some(LfoParams {
981 rate,
982 depth,
983 waveform,
984 target,
985 phase,
986 })
987 } else {
988 None
989 };
990
991 let mut options = HashMap::new();
992
993 let synth_keys = [
995 "waveform",
996 "attack",
997 "decay",
998 "sustain",
999 "release",
1000 "type",
1001 "filters",
1002 "plugin_author",
1003 "plugin_name",
1004 "plugin_export",
1005 "lfo",
1006 ];
1007
1008 let audio_keys = [
1010 "pan",
1011 "gain",
1012 "volume",
1013 "detune",
1014 "spread",
1015 "synth_type", "chain", "effects", ];
1019
1020 for (key, val) in map.iter() {
1021 if synth_keys.contains(&key.as_str()) || audio_keys.contains(&key.as_str()) {
1023 continue;
1024 }
1025
1026 if key.starts_with("_") {
1028 continue;
1029 }
1030
1031 if let Value::Number(n) = val {
1033 options.insert(key.clone(), *n);
1034 } else if let Value::String(s) = val {
1035 if let Ok(n) = s.parse::<f32>() {
1037 options.insert(key.clone(), n);
1038 }
1039 }
1040 }
1041
1042 Ok(crate::engine::audio::events::SynthDefinition {
1043 waveform,
1044 attack,
1045 decay,
1046 sustain,
1047 release,
1048 synth_type,
1049 filters,
1050 options,
1051 plugin_author,
1052 plugin_name,
1053 plugin_export,
1054 lfo,
1055 })
1056}
1057
1058pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
1059 use std::path::Path;
1060
1061 let path = Path::new(source);
1062 if let Some(ext) = path
1064 .extension()
1065 .and_then(|s| s.to_str())
1066 .map(|s| s.to_lowercase())
1067 {
1068 match ext.as_str() {
1069 "mid" | "midi" => {
1070 use crate::engine::audio::midi::load_midi_file;
1071 let midi_data = load_midi_file(path)?;
1072 interpreter.variables.insert(alias.to_string(), midi_data);
1073 Ok(())
1075 }
1076 "wav" | "flac" | "mp3" | "ogg" => {
1077 #[cfg(feature = "cli")]
1079 {
1080 use crate::engine::audio::samples;
1081 let registered = samples::register_sample_from_path(path)?;
1082 interpreter
1084 .variables
1085 .insert(alias.to_string(), Value::String(registered.clone()));
1086 return Ok(());
1088 }
1089
1090 #[cfg(not(feature = "cli"))]
1092 {
1093 interpreter
1094 .variables
1095 .insert(alias.to_string(), Value::String(source.to_string()));
1096 return Ok(());
1097 }
1098 }
1099 _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
1100 }
1101 } else {
1102 Err(anyhow::anyhow!(
1103 "Cannot determine file extension for {}",
1104 source
1105 ))
1106 }
1107}
1108
1109pub fn handle_bind(
1110 interpreter: &mut AudioInterpreter,
1111 source: &str,
1112 target: &str,
1113 options: &Value,
1114) -> Result<()> {
1115 use std::collections::HashMap as StdHashMap;
1116
1117 if source.starts_with("mapping.") || target.starts_with("mapping.") {
1124 let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
1126 m.clone()
1127 } else {
1128 StdHashMap::new()
1129 };
1130
1131 fn create_and_insert(
1133 path: &str,
1134 opts_map: &StdHashMap<String, Value>,
1135 interpreter: &mut AudioInterpreter,
1136 ) -> Option<(String, String)> {
1137 let parts: Vec<&str> = path.split('.').collect();
1138 if parts.len() >= 3 {
1139 let direction = parts[1]; let device = parts[2];
1141
1142 let mut map = StdHashMap::new();
1143 map.insert(
1144 "_type".to_string(),
1145 Value::String("midi_mapping".to_string()),
1146 );
1147 map.insert(
1148 "direction".to_string(),
1149 Value::String(direction.to_string()),
1150 );
1151 map.insert("device".to_string(), Value::String(device.to_string()));
1152
1153 for (k, v) in opts_map.iter() {
1155 map.insert(k.clone(), v.clone());
1156 }
1157
1158 crate::utils::props::ensure_default_properties(&mut map, Some("mapping"));
1160
1161 interpreter
1162 .variables
1163 .insert(path.to_string(), Value::Map(map.clone()));
1164
1165 let note_on = format!("mapping.{}.{}.noteOn", direction, device);
1167 let note_off = format!("mapping.{}.{}.noteOff", direction, device);
1168 let rest = format!("mapping.{}.{}.rest", direction, device);
1169 interpreter
1170 .variables
1171 .insert(note_on.clone(), Value::String(note_on.clone()));
1172 interpreter
1173 .variables
1174 .insert(note_off.clone(), Value::String(note_off.clone()));
1175 interpreter
1176 .variables
1177 .insert(rest.clone(), Value::String(rest.clone()));
1178
1179 return Some((direction.to_string(), device.to_string()));
1180 }
1181 None
1182 }
1183
1184 if source.starts_with("mapping.") {
1186 if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
1187 if !target.starts_with("mapping.") {
1189 let mut bmap = StdHashMap::new();
1190 bmap.insert("instrument".to_string(), Value::String(target.to_string()));
1191 bmap.insert("direction".to_string(), Value::String(direction.clone()));
1192 bmap.insert("device".to_string(), Value::String(device.clone()));
1193 for (k, v) in opts_map.iter() {
1194 bmap.insert(k.clone(), v.clone());
1195 }
1196 interpreter
1197 .variables
1198 .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
1199
1200 #[cfg(feature = "cli")]
1202 if let Some(manager) = &mut interpreter.midi_manager {
1203 if let Some(Value::Number(port_num)) = opts_map.get("port") {
1204 let idx = *port_num as usize;
1205 if let Ok(mut mgr) = manager.lock() {
1206 let _ = mgr.open_input_by_index(idx, &device);
1208 }
1209 }
1210 }
1211 }
1212 }
1213 }
1214
1215 if target.starts_with("mapping.") {
1217 if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
1218 if !source.starts_with("mapping.") {
1219 let mut bmap = StdHashMap::new();
1220 bmap.insert("source".to_string(), Value::String(source.to_string()));
1221 bmap.insert("direction".to_string(), Value::String(direction.clone()));
1222 bmap.insert("device".to_string(), Value::String(device.clone()));
1223 for (k, v) in opts_map.iter() {
1224 bmap.insert(k.clone(), v.clone());
1225 }
1226 interpreter
1227 .variables
1228 .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
1229
1230 #[cfg(feature = "cli")]
1232 if let Some(manager) = &mut interpreter.midi_manager {
1233 if let Some(Value::Number(port_num)) = opts_map.get("port") {
1234 let idx = *port_num as usize;
1235 if let Ok(mut mgr) = manager.lock() {
1236 let _ = mgr.open_output_by_name(&device, idx);
1237 }
1238 }
1239 }
1240 }
1241 }
1242 }
1243
1244 return Ok(());
1249 }
1250
1251 let midi_data = interpreter
1253 .variables
1254 .get(source)
1255 .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
1256 .clone();
1257
1258 if let Value::Map(midi_map) = &midi_data {
1259 let notes = midi_map
1260 .get("notes")
1261 .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
1262
1263 if let Value::Array(notes_array) = notes {
1264 let _synth_def = interpreter
1265 .events
1266 .synths
1267 .get(target)
1268 .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
1269 .clone();
1270
1271 let default_velocity = 100;
1272 let mut velocity = default_velocity;
1273
1274 if let Value::Map(opts) = options {
1275 if let Some(Value::Number(v)) = opts.get("velocity") {
1276 velocity = *v as u8;
1277 }
1278 }
1279
1280 let midi_bpm =
1283 crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
1284
1285 for note_val in notes_array {
1286 if let Value::Map(note_map) = note_val {
1287 let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
1288 let note =
1289 crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
1290 let note_velocity = crate::engine::audio::events::extract_number(
1291 note_map,
1292 "velocity",
1293 velocity as f32,
1294 ) as u8;
1295 let duration_ms =
1297 crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
1298
1299 use crate::engine::audio::events::AudioEvent;
1300 let synth_def = interpreter
1301 .events
1302 .get_synth(target)
1303 .cloned()
1304 .unwrap_or_default();
1305 let interp_bpm = interpreter.bpm;
1308 let factor = if interp_bpm > 0.0 {
1309 midi_bpm / interp_bpm
1310 } else {
1311 1.0
1312 };
1313
1314 let start_time_s = (time / 1000.0) * factor;
1315 let duration_s = (duration_ms / 1000.0) * factor;
1316
1317 let event = AudioEvent::Note {
1318 midi: note,
1319 start_time: start_time_s,
1320 duration: duration_s,
1321 velocity: note_velocity as f32,
1322 synth_id: target.to_string(),
1323 synth_def,
1324 pan: 0.0,
1325 detune: 0.0,
1326 gain: 1.0,
1327 attack: None,
1328 release: None,
1329 delay_time: None,
1330 delay_feedback: None,
1331 delay_mix: None,
1332 reverb_amount: None,
1333 drive_amount: None,
1334 drive_color: None,
1335 effects: None,
1336 use_per_note_automation: false,
1337 };
1338
1339 interpreter.events.events.push(event);
1341 }
1342 }
1343
1344 }
1346 }
1347
1348 Ok(())
1349}
1350
1351#[cfg(feature = "cli")]
1352pub fn handle_use_plugin(
1353 interpreter: &mut AudioInterpreter,
1354 author: &str,
1355 name: &str,
1356 alias: &str,
1357) -> Result<()> {
1358 use crate::engine::plugin::loader::load_plugin;
1359
1360 match load_plugin(author, name) {
1361 Ok((info, _wasm_bytes)) => {
1362 let mut plugin_map = HashMap::new();
1363 plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
1364 plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
1365 plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
1366
1367 if let Some(version) = &info.version {
1368 plugin_map.insert("_version".to_string(), Value::String(version.clone()));
1369 }
1370
1371 for export in &info.exports {
1372 let mut export_map = HashMap::new();
1373 export_map.insert(
1374 "_plugin_author".to_string(),
1375 Value::String(info.author.clone()),
1376 );
1377 export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
1378 export_map.insert(
1379 "_export_name".to_string(),
1380 Value::String(export.name.clone()),
1381 );
1382 export_map.insert(
1383 "_export_kind".to_string(),
1384 Value::String(export.kind.clone()),
1385 );
1386
1387 plugin_map.insert(export.name.clone(), Value::Map(export_map));
1388 }
1389 crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1391
1392 interpreter
1393 .variables
1394 .insert(alias.to_string(), Value::Map(plugin_map));
1395 }
1396 Err(e) => {
1397 eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
1398 return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
1399 }
1400 }
1401
1402 Ok(())
1403}
1404
1405#[cfg(not(feature = "cli"))]
1406pub fn handle_use_plugin(
1407 interpreter: &mut AudioInterpreter,
1408 author: &str,
1409 name: &str,
1410 alias: &str,
1411) -> Result<()> {
1412 let mut plugin_map = HashMap::new();
1414 plugin_map.insert(
1415 "_type".to_string(),
1416 Value::String("plugin_stub".to_string()),
1417 );
1418 plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
1419 plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
1420 crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1422
1423 interpreter
1424 .variables
1425 .insert(alias.to_string(), Value::Map(plugin_map));
1426 Ok(())
1427}
1428
1429pub fn handle_bank(
1430 interpreter: &mut AudioInterpreter,
1431 name: &str,
1432 alias: &Option<String>,
1433) -> Result<()> {
1434 let target_alias = alias
1435 .clone()
1436 .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
1437
1438 if let Some(existing_value) = interpreter.variables.get(name) {
1439 interpreter
1440 .variables
1441 .insert(target_alias.clone(), existing_value.clone());
1442 } else {
1443 #[cfg(feature = "wasm")]
1444 {
1445 use crate::web::registry::banks::REGISTERED_BANKS;
1446 REGISTERED_BANKS.with(|banks| {
1447 for bank in banks.borrow().iter() {
1448 if bank.full_name == *name {
1449 if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
1450 interpreter
1451 .variables
1452 .insert(target_alias.clone(), Value::Map(bank_map.clone()));
1453 }
1454 }
1455 }
1456 });
1457 }
1458
1459 #[cfg(not(feature = "wasm"))]
1460 {
1461 if let Ok(current_dir) = std::env::current_dir() {
1462 match interpreter.banks.register_bank(
1463 target_alias.clone(),
1464 &name,
1465 ¤t_dir,
1466 ¤t_dir,
1467 ) {
1468 Ok(_) => {
1469 let mut bank_map = HashMap::new();
1470 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1471 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1472 interpreter
1473 .variables
1474 .insert(target_alias.clone(), Value::Map(bank_map));
1475 }
1476 Err(e) => {
1477 eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
1478 let mut bank_map = HashMap::new();
1479 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1480 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1481 interpreter
1482 .variables
1483 .insert(target_alias.clone(), Value::Map(bank_map));
1484 }
1485 }
1486 } else {
1487 let mut bank_map = HashMap::new();
1488 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1489 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1490 interpreter
1491 .variables
1492 .insert(target_alias.clone(), Value::Map(bank_map));
1493 eprintln!(
1494 "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
1495 name
1496 );
1497 }
1498 }
1499 }
1500
1501 Ok(())
1502}
1503
1504pub fn handle_trigger(
1505 interpreter: &mut AudioInterpreter,
1506 entity: &str,
1507 effects: Option<&crate::language::syntax::ast::Value>,
1508) -> Result<()> {
1509 let resolved_entity = if entity.starts_with('.') {
1510 &entity[1..]
1511 } else {
1512 entity
1513 };
1514
1515 if let Some(var_val) = interpreter.variables.get(resolved_entity).cloned() {
1518 if let crate::language::syntax::ast::Value::Statement(stmt_box) = var_val {
1519 if let crate::language::syntax::ast::StatementKind::Trigger {
1520 entity: inner_entity,
1521 duration: _,
1522 effects: stored_effects,
1523 } = &stmt_box.kind
1524 {
1525 if inner_entity != resolved_entity {
1527 let chosen_effects = stored_effects.as_ref().or(effects);
1529 return handle_trigger(interpreter, inner_entity, chosen_effects);
1530 } else {
1531 return Ok(());
1532 }
1533 }
1534 }
1535 }
1536
1537 if resolved_entity.contains('.') {
1538 let parts: Vec<&str> = resolved_entity.split('.').collect();
1539 if parts.len() == 2 {
1540 let (var_name, property) = (parts[0], parts[1]);
1541
1542 if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
1543 if let Some(Value::String(sample_uri)) = map.get(property) {
1544 let uri = sample_uri.trim_matches('"').trim_matches('\'');
1545 interpreter.events.add_sample_event_with_effects(
1547 uri,
1548 interpreter.cursor_time,
1549 1.0,
1550 effects.cloned(),
1551 );
1552 let beat_duration = interpreter.beat_duration();
1553 interpreter.cursor_time += beat_duration;
1554 } else {
1555 #[cfg(not(feature = "wasm"))]
1556 {
1557 let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
1559 if resolved_uri != resolved_entity {
1560 interpreter.events.add_sample_event_with_effects(
1562 &resolved_uri,
1563 interpreter.cursor_time,
1564 1.0,
1565 effects.cloned(),
1566 );
1567 let beat_duration = interpreter.beat_duration();
1568 interpreter.cursor_time += beat_duration;
1569 } else if let Some(pathbuf) =
1570 interpreter.banks.resolve_trigger(var_name, property)
1571 {
1572 if let Some(path_str) = pathbuf.to_str() {
1573 interpreter.events.add_sample_event_with_effects(
1575 path_str,
1576 interpreter.cursor_time,
1577 1.0,
1578 effects.cloned(),
1579 );
1580 let beat_duration = interpreter.beat_duration();
1581 interpreter.cursor_time += beat_duration;
1582 } else {
1583 println!(
1584 "⚠️ Resolution failed for {}.{} (invalid path)",
1585 var_name, property
1586 );
1587 }
1588 } else {
1589 }
1591 }
1592 }
1593 }
1594 }
1595 } else {
1596 if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
1597 let uri = sample_uri.trim_matches('"').trim_matches('\'');
1598 interpreter.events.add_sample_event_with_effects(
1599 uri,
1600 interpreter.cursor_time,
1601 1.0,
1602 effects.cloned(),
1603 );
1604 let beat_duration = interpreter.beat_duration();
1605 interpreter.cursor_time += beat_duration;
1606 }
1607 }
1608
1609 Ok(())
1613}
1614
1615pub fn extract_pattern_data(
1616 _interpreter: &AudioInterpreter,
1617 value: &Value,
1618) -> (Option<String>, Option<HashMap<String, f32>>) {
1619 match value {
1620 Value::String(pattern) => (Some(pattern.clone()), None),
1621 Value::Map(map) => {
1622 let pattern = map.get("pattern").and_then(|v| {
1623 if let Value::String(s) = v {
1624 Some(s.clone())
1625 } else {
1626 None
1627 }
1628 });
1629
1630 let mut options = HashMap::new();
1631 for (key, val) in map.iter() {
1632 if key != "pattern" {
1633 if let Value::Number(num) = val {
1634 options.insert(key.clone(), *num);
1635 }
1636 }
1637 }
1638
1639 let opts = if options.is_empty() {
1640 None
1641 } else {
1642 Some(options)
1643 };
1644 (pattern, opts)
1645 }
1646 _ => (None, None),
1647 }
1648}
1649
1650pub fn execute_pattern(
1651 interpreter: &mut AudioInterpreter,
1652 target: &str,
1653 pattern: &str,
1654 options: Option<HashMap<String, f32>>,
1655) -> Result<()> {
1656 execute_pattern_with_source(interpreter, target, pattern, options, None)
1657}
1658
1659pub fn execute_pattern_with_source(
1660 interpreter: &mut AudioInterpreter,
1661 target: &str,
1662 pattern: &str,
1663 options: Option<HashMap<String, f32>>,
1664 source: Option<&str>,
1665) -> Result<()> {
1666 use crate::engine::audio::events::AudioEvent;
1667
1668 let swing = options
1669 .as_ref()
1670 .and_then(|o| o.get("swing").copied())
1671 .unwrap_or(0.0);
1672 let humanize = options
1673 .as_ref()
1674 .and_then(|o| o.get("humanize").copied())
1675 .unwrap_or(0.0);
1676 let velocity_mult = options
1677 .as_ref()
1678 .and_then(|o| o.get("velocity").copied())
1679 .unwrap_or(1.0);
1680 let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
1681
1682 let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
1683
1684 let resolved_uri = resolve_sample_uri(interpreter, target);
1685
1686 let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
1687 let step_count = pattern_chars.len() as f32;
1688 if step_count == 0.0 {
1689 return Ok(());
1690 }
1691
1692 let bar_duration = (60.0 / effective_bpm) * 4.0;
1693 let step_duration = bar_duration / step_count;
1694
1695 for (i, &ch) in pattern_chars.iter().enumerate() {
1696 if ch == 'x' || ch == 'X' {
1697 let mut time = interpreter.cursor_time + (i as f32 * step_duration);
1698 if swing > 0.0 && i % 2 == 1 {
1699 time += step_duration * swing;
1700 }
1701
1702 #[cfg(any(feature = "cli", feature = "wasm"))]
1703 if humanize > 0.0 {
1704 let offset = crate::engine::special_vars::gen_range_f32(-humanize, humanize);
1705 time += offset;
1706 }
1707
1708 let event = AudioEvent::Sample {
1709 uri: resolved_uri.clone(),
1710 start_time: time,
1711 velocity: velocity_mult, effects: None,
1713 source: source.map(|s| s.to_string()),
1714 };
1715 interpreter.events.events.push(event);
1716 }
1717 }
1718
1719 interpreter.cursor_time += bar_duration;
1720 Ok(())
1721}
1722
1723pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
1724 if let Some(dot_pos) = target.find('.') {
1725 let bank_alias = &target[..dot_pos];
1726 let trigger_name = &target[dot_pos + 1..];
1727 if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
1728 if let Some(Value::String(bank_name)) = bank_map.get("_name") {
1729 return format!("devalang://bank/{}/{}", bank_name, trigger_name);
1730 }
1731 }
1732 }
1733 target.to_string()
1734}
1735
1736pub fn handle_fade(
1738 interpreter: &mut AudioInterpreter,
1739 direction: &str,
1740 target: &str,
1741 duration: &crate::language::syntax::ast::DurationValue,
1742) -> Result<()> {
1743 let normalized_target = if target.starts_with('.') {
1745 &target[1..] } else {
1747 target
1748 };
1749
1750 let parts: Vec<&str> = normalized_target.split('.').collect();
1751 if parts.len() != 2 {
1752 return Err(anyhow::anyhow!(
1753 "Fade target must be in format 'entity.property', got '{}'",
1754 target
1755 ));
1756 }
1757
1758 let entity = parts[0];
1759 let property = parts[1];
1760
1761 let dur_secs = match duration {
1763 crate::language::syntax::ast::DurationValue::Milliseconds(ms) => ms / 1000.0,
1764 crate::language::syntax::ast::DurationValue::Beats(b) => b * (60.0 / interpreter.bpm),
1765 crate::language::syntax::ast::DurationValue::Beat(s) => {
1766 if let Some((num, den)) = {
1768 let mut sp = s.split('/');
1769 if let (Some(a), Some(b)) = (sp.next(), sp.next()) {
1770 if let (Ok(an), Ok(bn)) = (a.trim().parse::<f32>(), b.trim().parse::<f32>()) {
1771 Some((an, bn))
1772 } else {
1773 None
1774 }
1775 } else {
1776 None
1777 }
1778 } {
1779 if den.abs() > f32::EPSILON {
1780 (num / den) * (60.0 / interpreter.bpm)
1781 } else {
1782 0.0
1783 }
1784 } else {
1785 0.0
1786 }
1787 }
1788 crate::language::syntax::ast::DurationValue::Number(n) => n / 1000.0,
1789 _ => 0.0,
1790 };
1791
1792 let (from_value, to_value) = match direction.to_lowercase().as_str() {
1794 "in" => (0.0, 1.0), "out" => (1.0, 0.0), _ => {
1797 return Err(anyhow::anyhow!(
1798 "Fade direction must be 'in' or 'out', got '{}'",
1799 direction
1800 ));
1801 }
1802 };
1803
1804 let mut fade_envelope = crate::engine::audio::automation::AutomationEnvelope::new(format!(
1807 "{}.{}",
1808 entity, property
1809 ));
1810
1811 use crate::engine::audio::automation::AutomationCurve;
1813 fade_envelope.add_param(crate::engine::audio::automation::AutomationParam {
1814 param_name: property.to_string(),
1815 from_value,
1816 to_value,
1817 start_time: interpreter.cursor_time,
1818 duration: dur_secs,
1819 curve: AutomationCurve::Linear,
1820 });
1821
1822 interpreter.automation_registry.register(fade_envelope);
1823
1824 interpreter.cursor_time += dur_secs;
1826
1827 Ok(())
1828}
1829
1830pub fn handle_stop(
1832 interpreter: &mut AudioInterpreter,
1833 target: Option<&str>,
1834 delay: f32,
1835) -> Result<()> {
1836 let stop_time = delay;
1839
1840 if let Some(entity_name) = target {
1841 let stop_event = AudioEvent::Stop {
1843 target: Some(entity_name.to_string()),
1844 time: stop_time,
1845 };
1846 interpreter.events.events.push(stop_event);
1847 } else {
1848 let stop_event = AudioEvent::Stop {
1850 target: None,
1851 time: stop_time,
1852 };
1853 interpreter.events.events.push(stop_event);
1854 }
1855
1856 Ok(())
1857}