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;
9use super::patterns::execute_pattern_with_source;
10
11pub fn handle_let(interpreter: &mut AudioInterpreter, name: &str, value: &Value) -> Result<()> {
12 if let Value::Map(orig_map) = value {
15 let is_synth_like = orig_map.contains_key("waveform")
16 || orig_map.contains_key("_plugin_ref")
17 || orig_map.contains_key("synth_type")
18 || matches!(
19 orig_map.get("type"),
20 Some(Value::String(s)) if s.trim_matches('"').trim_matches('\'') == "synth"
21 )
22 || matches!(
23 orig_map.get("type"),
24 Some(Value::Identifier(s)) if s.trim_matches('"').trim_matches('\'') == "synth"
25 );
26
27 let mut map = orig_map.clone();
29
30 if let Some(synth_val) = map.get("synth_type").cloned() {
33 let should_replace = match map.get("type") {
34 Some(Value::String(s)) => {
35 let clean = s.trim_matches('"').trim_matches('\'');
36 clean == "synth" || clean.is_empty()
37 }
38 Some(Value::Identifier(id)) => {
39 let clean = id.trim_matches('"').trim_matches('\'');
40 clean == "synth" || clean.is_empty()
41 }
42 None => true,
43 _ => false,
44 };
45 if should_replace {
46 map.insert("type".to_string(), synth_val.clone());
47 map.remove("synth_type");
48 } else {
49 }
51 }
52
53 let chain_keys = ["params", "adsr", "envelope"];
55 for key in &chain_keys {
56 if let Some(Value::Map(submap)) = map.get(*key) {
57 let to_insert: Vec<(String, Value)> =
59 submap.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
60 for (k, v) in to_insert {
61 map.insert(k, v);
62 }
63 map.remove(*key);
64 }
65 }
66
67 map.entry("volume".to_string())
71 .or_insert(Value::Number(1.0));
72 map.entry("gain".to_string()).or_insert(Value::Number(1.0));
73 map.entry("pan".to_string()).or_insert(Value::Number(0.0));
74 map.entry("detune".to_string())
75 .or_insert(Value::Number(0.0));
76 map.entry("type".to_string())
78 .or_insert(Value::String("synth".to_string()));
79
80 crate::utils::props::ensure_default_properties(&mut map, Some("synth"));
82
83 if is_synth_like {
84 let mut is_plugin = false;
86 let mut plugin_author: Option<String> = None;
87 let mut plugin_name: Option<String> = None;
88 let mut plugin_export: Option<String> = None;
89
90 if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
91 let parts: Vec<&str> = plugin_ref.split('.').collect();
92 if parts.len() == 2 {
93 let (var_name, prop_name_raw) = (parts[0], parts[1]);
94 let prop_name = prop_name_raw.trim_end_matches(';'); if let Some(var_value) = interpreter.variables.get(var_name) {
96 if let Value::Map(var_map) = var_value {
97 if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
98 if resolved_plugin.starts_with("plugin:") {
99 let ref_parts: Vec<&str> =
100 resolved_plugin["plugin:".len()..].split(':').collect();
101 if ref_parts.len() == 2 {
102 let full_plugin_name = ref_parts[0];
103 let export_name = ref_parts[1];
104 let plugin_parts: Vec<&str> =
105 full_plugin_name.split('.').collect();
106 if plugin_parts.len() == 2 {
107 plugin_author = Some(plugin_parts[0].to_string());
108 plugin_name = Some(plugin_parts[1].to_string());
109 plugin_export = Some(export_name.to_string());
110 is_plugin = true;
111 }
112 }
113 }
114 } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
115 if let (
116 Some(Value::String(author)),
117 Some(Value::String(name)),
118 Some(Value::String(export)),
119 ) = (
120 export_map.get("_plugin_author"),
121 export_map.get("_plugin_name"),
122 export_map.get("_export_name"),
123 ) {
124 plugin_author = Some(author.clone());
125 plugin_name = Some(name.clone());
126 plugin_export = Some(export.clone());
127 is_plugin = true;
128 }
129 }
130 }
131 }
132 }
133 }
134
135 let waveform = crate::engine::audio::events::extract_string(&map, "waveform", "sine");
136 let attack = crate::engine::audio::events::extract_number(&map, "attack", 0.01);
137 let decay = crate::engine::audio::events::extract_number(&map, "decay", 0.1);
138 let sustain = crate::engine::audio::events::extract_number(&map, "sustain", 0.7);
139 let release = crate::engine::audio::events::extract_number(&map, "release", 0.2);
140
141 let synth_type = if let Some(v) = map.get("type") {
143 match v {
144 Value::String(t) => {
145 let clean = t.trim_matches('"').trim_matches('\'');
146 if clean.is_empty() || clean == "synth" {
147 None
148 } else {
149 Some(clean.to_string())
150 }
151 }
152 Value::Identifier(id) => {
153 let clean = id.trim_matches('"').trim_matches('\'');
154 if clean.is_empty() || clean == "synth" {
155 None
156 } else {
157 Some(clean.to_string())
158 }
159 }
160 _ => None,
161 }
162 } else {
163 None
164 };
165
166 let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
167 crate::engine::audio::events::extract_filters(filters_arr)
168 } else {
169 Vec::new()
170 };
171
172 let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
174 use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
175
176 let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
178 n.to_string()
179 } else if let Some(Value::String(s)) = lfo_map.get("rate") {
180 s.clone()
181 } else {
182 "5.0".to_string() };
184 let rate = LfoRate::from_value(&rate_str);
185
186 let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
188 (*n).clamp(0.0, 1.0)
189 } else {
190 0.5 };
192
193 let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
195 s.clone()
196 } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
197 s.clone()
198 } else {
199 "sine".to_string() };
201 let lfo_waveform = LfoWaveform::from_str(&waveform_str);
202
203 let target = if let Some(Value::String(s)) = lfo_map.get("target") {
205 LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
206 } else {
207 LfoTarget::Volume };
209
210 let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
212 (*n).fract().abs() } else {
214 0.0 };
216
217 Some(LfoParams {
218 rate,
219 depth,
220 waveform: lfo_waveform,
221 target,
222 phase,
223 })
224 } else {
225 None
226 };
227
228 let mut options = std::collections::HashMap::new();
229 let reserved_keys = if is_plugin {
230 vec![
231 "attack",
232 "decay",
233 "sustain",
234 "release",
235 "type",
236 "filters",
237 "_plugin_ref",
238 "lfo",
239 "waveform", "pan",
242 "gain",
243 "volume",
244 "detune",
245 "spread",
246 "synth_type", "chain", "effects", ]
251 } else {
252 vec![
253 "waveform",
254 "attack",
255 "decay",
256 "sustain",
257 "release",
258 "type",
259 "filters",
260 "_plugin_ref",
261 "lfo",
262 "pan",
264 "gain",
265 "volume",
266 "detune",
267 "spread",
268 "synth_type", "chain", "effects", ]
273 };
274
275 for (key, val) in map.iter() {
276 if !reserved_keys.contains(&key.as_str()) {
277 match val {
278 Value::Number(n) => {
279 options.insert(key.clone(), *n);
280 }
281 Value::String(s) => {
282 if is_plugin && key == "waveform" {
283 let waveform_id = match s
284 .trim_matches('"')
285 .trim_matches('\'')
286 .to_lowercase()
287 .as_str()
288 {
289 "sine" => 0.0,
290 "saw" => 1.0,
291 "square" => 2.0,
292 "triangle" => 3.0,
293 _ => 1.0,
294 };
295 options.insert(key.clone(), waveform_id);
296 }
297 }
298 _ => {}
299 }
300 }
301 }
302
303 if is_plugin && map.contains_key("decay") {
304 options.insert("decay".to_string(), decay);
305 }
306
307 if let Some(Value::Array(chain_arr)) = map.get("chain") {
309 for chain_item in chain_arr {
310 if let Value::Map(chain_map) = chain_item {
311 if let Some(Value::String(method)) = chain_map.get("type") {
313 if let Some(Value::String(str_val)) = chain_map.get("value") {
315 if method == "decay" && str_val == "auto" {
316 options.insert("decay_mode".to_string(), 1.0); }
318 }
319 else if let Some(Value::Number(val)) = chain_map.get("value") {
321 options.insert(method.clone(), *val);
322 }
323 }
324 }
325 }
326 }
327
328 let final_waveform = if is_plugin {
329 "plugin".to_string()
330 } else {
331 waveform
332 };
333
334 let synth_def = crate::engine::audio::events::SynthDefinition {
335 waveform: final_waveform,
336 attack,
337 decay,
338 sustain,
339 release,
340 synth_type,
341 filters,
342 options,
343 plugin_author,
344 plugin_name,
345 plugin_export,
346 lfo,
347 bpm: interpreter.bpm,
348 };
349
350 interpreter.events.add_synth(name.to_string(), synth_def);
351 }
352
353 interpreter
356 .variables
357 .insert(name.to_string(), Value::Map(map.clone()));
358 return Ok(());
359 }
360
361 if let Value::Statement(stmt_box) = value {
366 if let StatementKind::Trigger {
367 entity,
368 duration,
369 effects,
370 } = &stmt_box.kind
371 {
372 let mut map = HashMap::new();
373 map.insert("kind".to_string(), Value::String("Trigger".to_string()));
374 map.insert("entity".to_string(), Value::String(entity.clone()));
375 map.insert("duration".to_string(), Value::Duration(duration.clone()));
376 if let Some(eff) = effects {
377 map.insert("effects".to_string(), eff.clone());
378 }
379
380 crate::utils::props::ensure_default_properties(&mut map, Some("trigger"));
382
383 interpreter
384 .variables
385 .insert(name.to_string(), Value::Map(map));
386 return Ok(());
387 }
388 }
389
390 interpreter
391 .variables
392 .insert(name.to_string(), value.clone());
393 Ok(())
394}
395
396pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str, args: &[Value]) -> Result<()> {
397 if let Some(var_val) = interpreter.variables.get(name).cloned() {
407 if let Value::Statement(stmt_box) = var_val {
408 if let StatementKind::Function {
409 name: _fname,
410 parameters,
411 body,
412 } = &stmt_box.kind
413 {
414 let vars_snapshot = interpreter.variables.clone();
416
417 for (i, param) in parameters.iter().enumerate() {
419 let bound = args.get(i).cloned().unwrap_or(Value::Null);
420 let bound_val = match bound {
422 Value::Identifier(ref id) => {
423 interpreter.resolve_value(&Value::Identifier(id.clone()))?
424 }
425 other => other,
426 };
427 interpreter.variables.insert(param.clone(), bound_val);
428 }
429
430 interpreter.function_call_depth += 1;
433 let exec_result = super::collector::collect_events(interpreter, body);
434 interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
436 exec_result?;
437
438 let mut captured_return: Option<Value> = None;
440 if interpreter.returning_flag {
441 captured_return = interpreter.return_value.clone();
442 interpreter.returning_flag = false;
444 interpreter.return_value = None;
445 }
446
447 interpreter.variables = vars_snapshot;
450
451 if let Some(rv) = captured_return {
455 interpreter.variables.insert("__return".to_string(), rv);
456 }
457
458 return Ok(());
459 }
460 if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
462 if let Some(tgt) = target.as_ref() {
463 let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
464 if let Some(pat) = pattern_str {
465 let previous = interpreter.current_event_node.clone();
467 interpreter.current_event_node =
468 interpreter.routed_node_for_alias(name).or(previous.clone());
469 execute_pattern_with_source(
470 interpreter,
471 tgt.as_str(),
472 &pat,
473 options,
474 Some(name),
475 )?;
476 interpreter.current_event_node = previous;
477 return Ok(());
478 }
479 }
480 }
481 }
482 }
483
484 if let Some(body) = interpreter.groups.get(name).cloned() {
486 let node_id = interpreter.routed_node_for_alias(name);
487 interpreter.collect_with_event_node(node_id, &body)?;
488 return Ok(());
489 }
490
491 println!(
492 "⚠️ Warning: Group, pattern or function '{}' not found",
493 name
494 );
495 Ok(())
496}
497
498pub fn call_function(
502 interpreter: &mut AudioInterpreter,
503 name: &str,
504 args: &[Value],
505) -> Result<Value> {
506 if let Some(var_val) = interpreter.variables.get(name).cloned() {
508 if let Value::Statement(stmt_box) = var_val {
509 if let StatementKind::Function {
510 name: _fname,
511 parameters,
512 body,
513 } = &stmt_box.kind
514 {
515 let vars_snapshot = interpreter.variables.clone();
517
518 for (i, param) in parameters.iter().enumerate() {
520 let bound = args.get(i).cloned().unwrap_or(Value::Null);
521 let bound_val = match bound {
523 Value::Identifier(ref id) => {
524 interpreter.resolve_value(&Value::Identifier(id.clone()))?
525 }
526 other => other,
527 };
528 interpreter.variables.insert(param.clone(), bound_val);
529 }
530
531 interpreter.function_call_depth += 1;
533
534 let exec_result = super::collector::collect_events(interpreter, body);
535 interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
536 exec_result?;
537
538 let mut captured_return: Option<Value> = None;
540 if interpreter.returning_flag {
541 captured_return = interpreter.return_value.clone();
542 interpreter.returning_flag = false;
543 interpreter.return_value = None;
544 }
545
546 interpreter.variables = vars_snapshot;
548
549 if let Some(rv) = captured_return {
550 return Ok(rv);
551 }
552
553 return Ok(Value::Null);
554 }
555 if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
557 if let Some(tgt) = target.as_ref() {
558 let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
559 if let Some(pat) = pattern_str {
560 let previous = interpreter.current_event_node.clone();
561 interpreter.current_event_node =
562 interpreter.routed_node_for_alias(name).or(previous.clone());
563 interpreter.execute_pattern_with_source(
564 tgt.as_str(),
565 &pat,
566 options,
567 Some(name),
568 )?;
569 interpreter.current_event_node = previous;
570 return Ok(Value::Null);
571 }
572 }
573 }
574 }
575 }
576
577 if let Some(body) = interpreter.groups.get(name).cloned() {
579 let node_id = interpreter.routed_node_for_alias(name);
580 interpreter.collect_with_event_node(node_id, &body)?;
581 return Ok(Value::Null);
582 }
583
584 println!(
585 "⚠️ Warning: Group, pattern or function '{}' not found",
586 name
587 );
588 Ok(Value::Null)
589}
590
591pub fn execute_print(interpreter: &mut AudioInterpreter, value: &Value) -> Result<()> {
592 let message = match value {
593 Value::Call { name: _, args: _ } => {
594 let resolved = interpreter.resolve_value(value)?;
596 interpreter.value_to_string(&resolved)
597 }
598
599 Value::String(s) => {
600 if s.contains('{') && s.contains('}') {
601 interpreter.interpolate_string(s)
602 } else {
603 s.clone()
604 }
605 }
606 Value::Identifier(id) => {
607 if id.ends_with("++") {
610 let varname = id[..id.len() - 2].trim();
611 let cur = match interpreter.variables.get(varname) {
613 Some(Value::Number(n)) => *n as isize,
614 _ => 0,
615 };
616 interpreter
618 .variables
619 .insert(varname.to_string(), Value::Number((cur + 1) as f32));
620 cur.to_string()
621 } else {
622 let resolved = interpreter.resolve_value(&Value::Identifier(id.clone()))?;
625 match resolved {
626 Value::String(s) => s.clone(),
627 Value::Number(n) => n.to_string(),
628 Value::Boolean(b) => b.to_string(),
629 Value::Array(arr) => format!("{:?}", arr),
630 Value::Map(map) => format!("{:?}", map),
631 Value::Null => format!("Identifier(\"{}\")", id),
632 other => format!("{:?}", other),
633 }
634 }
635 }
636 Value::Number(n) => n.to_string(),
637 Value::Boolean(b) => b.to_string(),
638 Value::Array(arr) => {
639 let mut parts = Vec::new();
641 for v in arr.iter() {
642 if let Value::Identifier(idtok) = v {
644 if idtok.ends_with("++") {
645 let varname = idtok[..idtok.len() - 2].trim();
646 let cur = match interpreter.variables.get(varname) {
647 Some(Value::Number(n)) => *n as isize,
648 _ => 0,
649 };
650 interpreter
651 .variables
652 .insert(varname.to_string(), Value::Number((cur + 1) as f32));
653 parts.push(cur.to_string());
654 continue;
655 }
656 }
657 let resolved = interpreter.resolve_value(v)?;
658 parts.push(interpreter.value_to_string(&resolved));
659 }
660 parts.join("")
661 }
662 Value::Map(map) => format!("{:?}", map),
663 _ => format!("{:?}", value),
664 };
665 let log_message = message.clone();
668 interpreter
670 .events
671 .add_log_event(log_message.clone(), interpreter.cursor_time);
672 if !interpreter.suppress_print {
677 #[cfg(feature = "cli")]
681 {
682 crate::tools::logger::Logger::new().print(message.clone());
683 }
684
685 #[cfg(not(feature = "cli"))]
688 {
689 if let Some(tx) = &interpreter.realtime_print_tx {
690 let _ = tx.send((interpreter.cursor_time, log_message.clone()));
691 }
692 }
693 } else {
694 if let Some(tx) = &interpreter.realtime_print_tx {
699 let _ = tx.send((interpreter.cursor_time, log_message.clone()));
701 }
702 }
703 Ok(())
704}
705
706pub fn execute_if(
707 interpreter: &mut AudioInterpreter,
708 condition: &Value,
709 body: &[Statement],
710 else_body: &Option<Vec<Statement>>,
711) -> Result<()> {
712 let condition_result = interpreter.evaluate_condition(condition)?;
713
714 if condition_result {
715 super::collector::collect_events(interpreter, body)?;
716 } else if let Some(else_stmts) = else_body {
717 super::collector::collect_events(interpreter, else_stmts)?;
718 }
719
720 Ok(())
721}
722
723pub fn execute_while(
724 interpreter: &mut AudioInterpreter,
725 condition: &Value,
726 body: &[Statement],
727) -> Result<()> {
728 let mut iter_count: usize = 0;
729 let hard_iter_cap: usize = 100_000; loop {
732 let condition_result = interpreter.evaluate_condition(condition)?;
733
734 if !condition_result {
735 break;
736 }
737
738 super::collector::collect_events(interpreter, body)?;
739
740 if interpreter.break_flag {
742 interpreter.break_flag = false;
743 break;
744 }
745
746 if let Some(deadline) = interpreter.schedule_deadline {
748 if interpreter.cursor_time >= deadline {
749 break;
750 }
751 }
752
753 iter_count = iter_count.saturating_add(1);
754 if iter_count > hard_iter_cap {
755 break;
756 }
757 }
758
759 Ok(())
760}
761
762pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
763 let handlers = interpreter.event_registry.get_handlers_matching(event_name);
764
765 for (index, handler) in handlers.iter().enumerate() {
766 if let Some(args) = &handler.args {
768 if let Some(num_val) = args.iter().find(|v| matches!(v, Value::Number(_))) {
769 if let Value::Number(n) = num_val {
770 let interval = (*n as usize).max(1);
771 if event_name == "beat" {
772 let cur = interpreter.special_vars.current_beat.floor() as usize;
773 if cur % interval != 0 {
774 continue;
775 }
776 } else if event_name == "bar" {
777 let cur = interpreter.special_vars.current_bar.floor() as usize;
778 if cur % interval != 0 {
779 continue;
780 }
781 }
782 }
783 }
784 }
785
786 if handler.once
787 && !interpreter
788 .event_registry
789 .should_execute_once(event_name, index)
790 {
791 continue;
792 }
793
794 let body_clone = handler.body.clone();
795 super::collector::collect_events(interpreter, &body_clone)?;
796 }
797
798 Ok(())
799}
800
801pub fn handle_assign(
802 interpreter: &mut AudioInterpreter,
803 target: &str,
804 property: &str,
805 value: &Value,
806) -> Result<()> {
807 if target.contains('.') {
810 let parts: Vec<&str> = target.split('.').collect();
811 let root = parts[0];
812 if let Some(root_val) = interpreter.variables.get_mut(root) {
813 let mut current = root_val;
815 for seg in parts.iter().skip(1) {
816 match current {
817 Value::Map(map) => {
818 if !map.contains_key(*seg) {
819 map.insert((*seg).to_string(), Value::Map(HashMap::new()));
821 }
822 current = map.get_mut(*seg).ok_or_else(|| {
823 anyhow::anyhow!(
824 "Failed to access nested segment '{}' while assigning to '{}'",
825 seg,
826 target
827 )
828 })?;
829 }
830 _ => {
831 return Err(anyhow::anyhow!(
832 "Cannot traverse into non-map segment '{}' when assigning to '{}'",
833 seg,
834 target
835 ));
836 }
837 }
838 }
839
840 if let Value::Map(map) = current {
842 map.insert(property.to_string(), value.clone());
843
844 if interpreter.events.synths.contains_key(root) {
846 if let Some(Value::Map(root_map)) = interpreter.variables.get(root) {
847 let map_clone = root_map.clone();
848 let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
849 interpreter
850 .events
851 .synths
852 .insert(root.to_string(), updated_def);
853 }
854 }
855 } else {
856 return Err(anyhow::anyhow!(
857 "Cannot assign property '{}' to non-map target '{}'",
858 property,
859 target
860 ));
861 }
862 } else {
863 return Err(anyhow::anyhow!("Variable '{}' not found", root));
864 }
865 } else {
866 if let Some(var) = interpreter.variables.get_mut(target) {
867 if let Value::Map(map) = var {
868 map.insert(property.to_string(), value.clone());
869
870 if interpreter.events.synths.contains_key(target) {
871 let map_clone = map.clone();
872 let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
873 interpreter
874 .events
875 .synths
876 .insert(target.to_string(), updated_def);
877 }
878 } else {
879 return Err(anyhow::anyhow!(
880 "Cannot assign property '{}' to non-map variable '{}'",
881 property,
882 target
883 ));
884 }
885 } else {
886 return Err(anyhow::anyhow!("Variable '{}' not found", target));
887 }
888 }
889
890 Ok(())
891}
892
893pub fn extract_synth_def_from_map(
894 interpreter: &AudioInterpreter,
895 map: &HashMap<String, Value>,
896) -> Result<crate::engine::audio::events::SynthDefinition> {
897 use crate::engine::audio::events::extract_filters;
898 use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
899
900 if let Some(Value::Boolean(true)) = map.get("_deprecated_shorthand") {
902 eprintln!(
903 "⚠️ WARNING: synth <waveform> shorthand is deprecated in v0.2.0.\n \
904 Use arrow chain instead: synth -> wave(\"<waveform>\") -> preset(...)\n \
905 Example: synth -> wave(\"saw\") -> preset(\"bass\")"
906 );
907 }
908
909 let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
910 let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
911 let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
912 let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
913 let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
914
915 let synth_type = if let Some(v) = map.get("type") {
917 match v {
918 Value::String(t) => {
919 let clean = t.trim_matches('"').trim_matches('\'');
920 if clean.is_empty() || clean == "synth" {
921 None
922 } else {
923 Some(clean.to_string())
924 }
925 }
926 Value::Identifier(id) => {
927 let clean = id.trim_matches('"').trim_matches('\'');
928 if clean.is_empty() || clean == "synth" {
929 None
930 } else {
931 Some(clean.to_string())
932 }
933 }
934 _ => None,
935 }
936 } else if let Some(v2) = map.get("synth_type") {
937 match v2 {
938 Value::String(t2) => {
939 let clean = t2.trim_matches('"').trim_matches('\'');
940 if clean.is_empty() || clean == "synth" {
941 None
942 } else {
943 Some(clean.to_string())
944 }
945 }
946 Value::Identifier(id2) => {
947 let clean = id2.trim_matches('"').trim_matches('\'');
948 if clean.is_empty() || clean == "synth" {
949 None
950 } else {
951 Some(clean.to_string())
952 }
953 }
954 _ => None,
955 }
956 } else {
957 None
958 };
959
960 let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
961 extract_filters(filters_arr)
962 } else {
963 Vec::new()
964 };
965
966 let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
967 Some(s.clone())
968 } else {
969 None
970 };
971 let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
972 Some(s.clone())
973 } else {
974 None
975 };
976 let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
977 Some(s.clone())
978 } else {
979 None
980 };
981
982 let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
984 let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
986 n.to_string()
987 } else if let Some(Value::String(s)) = lfo_map.get("rate") {
988 s.clone()
989 } else {
990 "5.0".to_string() };
992 let rate = LfoRate::from_value(&rate_str);
993
994 let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
996 (*n).clamp(0.0, 1.0)
997 } else {
998 0.5 };
1000
1001 let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
1003 s.clone()
1004 } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
1005 s.clone()
1006 } else {
1007 "sine".to_string() };
1009 let waveform = LfoWaveform::from_str(&waveform_str);
1010
1011 let target = if let Some(Value::String(s)) = lfo_map.get("target") {
1013 LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
1014 } else {
1015 LfoTarget::Volume };
1017
1018 let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
1020 (*n).fract().abs() } else {
1022 0.0 };
1024
1025 Some(LfoParams {
1026 rate,
1027 depth,
1028 waveform,
1029 target,
1030 phase,
1031 })
1032 } else {
1033 None
1034 };
1035
1036 let mut options = HashMap::new();
1037
1038 let synth_keys = [
1040 "waveform",
1041 "attack",
1042 "decay",
1043 "sustain",
1044 "release",
1045 "type",
1046 "filters",
1047 "plugin_author",
1048 "plugin_name",
1049 "plugin_export",
1050 "lfo",
1051 ];
1052
1053 let audio_keys = [
1055 "pan",
1056 "gain",
1057 "volume",
1058 "detune",
1059 "spread",
1060 "synth_type", "chain", "effects", ];
1064
1065 for (key, val) in map.iter() {
1066 if synth_keys.contains(&key.as_str()) || audio_keys.contains(&key.as_str()) {
1068 continue;
1069 }
1070
1071 if key.starts_with("_") {
1073 continue;
1074 }
1075
1076 if let Value::Number(n) = val {
1078 options.insert(key.clone(), *n);
1079 } else if let Value::String(s) = val {
1080 if let Ok(n) = s.parse::<f32>() {
1082 options.insert(key.clone(), n);
1083 }
1084 }
1085 }
1086
1087 Ok(crate::engine::audio::events::SynthDefinition {
1088 waveform,
1089 attack,
1090 decay,
1091 sustain,
1092 release,
1093 synth_type,
1094 filters,
1095 options,
1096 plugin_author,
1097 plugin_name,
1098 plugin_export,
1099 lfo,
1100 bpm: interpreter.bpm,
1101 })
1102}
1103
1104pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
1105 use std::path::Path;
1106
1107 let path = Path::new(source);
1108 if let Some(ext) = path
1110 .extension()
1111 .and_then(|s| s.to_str())
1112 .map(|s| s.to_lowercase())
1113 {
1114 match ext.as_str() {
1115 "mid" | "midi" => {
1116 use crate::engine::audio::midi::load_midi_file;
1117 let midi_data = load_midi_file(path)?;
1118 interpreter.variables.insert(alias.to_string(), midi_data);
1119 Ok(())
1121 }
1122 "wav" | "flac" | "mp3" | "ogg" => {
1123 #[cfg(feature = "cli")]
1125 {
1126 use crate::engine::audio::samples;
1127 let registered = samples::register_sample_from_path(path)?;
1128 interpreter
1130 .variables
1131 .insert(alias.to_string(), Value::String(registered.clone()));
1132 return Ok(());
1134 }
1135
1136 #[cfg(not(feature = "cli"))]
1138 {
1139 interpreter
1140 .variables
1141 .insert(alias.to_string(), Value::String(source.to_string()));
1142 return Ok(());
1143 }
1144 }
1145 _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
1146 }
1147 } else {
1148 Err(anyhow::anyhow!(
1149 "Cannot determine file extension for {}",
1150 source
1151 ))
1152 }
1153}
1154
1155pub fn handle_bind(
1156 interpreter: &mut AudioInterpreter,
1157 source: &str,
1158 target: &str,
1159 options: &Value,
1160) -> Result<()> {
1161 use std::collections::HashMap as StdHashMap;
1162
1163 if source.starts_with("mapping.") || target.starts_with("mapping.") {
1170 let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
1172 m.clone()
1173 } else {
1174 StdHashMap::new()
1175 };
1176
1177 fn create_and_insert(
1179 path: &str,
1180 opts_map: &StdHashMap<String, Value>,
1181 interpreter: &mut AudioInterpreter,
1182 ) -> Option<(String, String)> {
1183 let parts: Vec<&str> = path.split('.').collect();
1184 if parts.len() >= 3 {
1185 let direction = parts[1]; let device = parts[2];
1187
1188 let mut map = StdHashMap::new();
1189 map.insert(
1190 "_type".to_string(),
1191 Value::String("midi_mapping".to_string()),
1192 );
1193 map.insert(
1194 "direction".to_string(),
1195 Value::String(direction.to_string()),
1196 );
1197 map.insert("device".to_string(), Value::String(device.to_string()));
1198
1199 for (k, v) in opts_map.iter() {
1201 map.insert(k.clone(), v.clone());
1202 }
1203
1204 crate::utils::props::ensure_default_properties(&mut map, Some("mapping"));
1206
1207 interpreter
1208 .variables
1209 .insert(path.to_string(), Value::Map(map.clone()));
1210
1211 let note_on = format!("mapping.{}.{}.noteOn", direction, device);
1213 let note_off = format!("mapping.{}.{}.noteOff", direction, device);
1214 let rest = format!("mapping.{}.{}.rest", direction, device);
1215 interpreter
1216 .variables
1217 .insert(note_on.clone(), Value::String(note_on.clone()));
1218 interpreter
1219 .variables
1220 .insert(note_off.clone(), Value::String(note_off.clone()));
1221 interpreter
1222 .variables
1223 .insert(rest.clone(), Value::String(rest.clone()));
1224
1225 return Some((direction.to_string(), device.to_string()));
1226 }
1227 None
1228 }
1229
1230 if source.starts_with("mapping.") {
1232 if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
1233 if !target.starts_with("mapping.") {
1235 let mut bmap = StdHashMap::new();
1236 bmap.insert("instrument".to_string(), Value::String(target.to_string()));
1237 bmap.insert("direction".to_string(), Value::String(direction.clone()));
1238 bmap.insert("device".to_string(), Value::String(device.clone()));
1239 for (k, v) in opts_map.iter() {
1240 bmap.insert(k.clone(), v.clone());
1241 }
1242 interpreter
1243 .variables
1244 .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
1245
1246 #[cfg(feature = "cli")]
1248 if let Some(manager) = &mut interpreter.midi_manager {
1249 if let Some(Value::Number(port_num)) = opts_map.get("port") {
1250 let idx = *port_num as usize;
1251 if let Ok(mut mgr) = manager.lock() {
1252 let _ = mgr.open_input_by_index(idx, &device);
1254 }
1255 }
1256 }
1257 }
1258 }
1259 }
1260
1261 if target.starts_with("mapping.") {
1263 if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
1264 if !source.starts_with("mapping.") {
1265 let mut bmap = StdHashMap::new();
1266 bmap.insert("source".to_string(), Value::String(source.to_string()));
1267 bmap.insert("direction".to_string(), Value::String(direction.clone()));
1268 bmap.insert("device".to_string(), Value::String(device.clone()));
1269 for (k, v) in opts_map.iter() {
1270 bmap.insert(k.clone(), v.clone());
1271 }
1272 interpreter
1273 .variables
1274 .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
1275
1276 #[cfg(feature = "cli")]
1278 if let Some(manager) = &mut interpreter.midi_manager {
1279 if let Some(Value::Number(port_num)) = opts_map.get("port") {
1280 let idx = *port_num as usize;
1281 if let Ok(mut mgr) = manager.lock() {
1282 let _ = mgr.open_output_by_name(&device, idx);
1283 }
1284 }
1285 }
1286 }
1287 }
1288 }
1289
1290 return Ok(());
1295 }
1296
1297 let midi_data = interpreter
1299 .variables
1300 .get(source)
1301 .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
1302 .clone();
1303
1304 if let Value::Map(midi_map) = &midi_data {
1305 let notes = midi_map
1306 .get("notes")
1307 .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
1308
1309 if let Value::Array(notes_array) = notes {
1310 let _synth_def = interpreter
1311 .events
1312 .synths
1313 .get(target)
1314 .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
1315 .clone();
1316
1317 let default_velocity = 100;
1318 let mut velocity = default_velocity;
1319
1320 if let Value::Map(opts) = options {
1321 if let Some(Value::Number(v)) = opts.get("velocity") {
1322 velocity = *v as u8;
1323 }
1324 }
1325
1326 let midi_bpm =
1329 crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
1330
1331 for note_val in notes_array {
1332 if let Value::Map(note_map) = note_val {
1333 let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
1334 let note =
1335 crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
1336 let note_velocity = crate::engine::audio::events::extract_number(
1337 note_map,
1338 "velocity",
1339 velocity as f32,
1340 ) as u8;
1341 let duration_ms =
1343 crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
1344
1345 use crate::engine::audio::events::AudioEvent;
1346 let synth_def = interpreter
1347 .events
1348 .get_synth(target)
1349 .cloned()
1350 .unwrap_or_default();
1351 let interp_bpm = interpreter.bpm;
1354 let factor = if interp_bpm > 0.0 {
1355 midi_bpm / interp_bpm
1356 } else {
1357 1.0
1358 };
1359
1360 let start_time_s = (time / 1000.0) * factor;
1361 let duration_s = (duration_ms / 1000.0) * factor;
1362
1363 let event = AudioEvent::Note {
1364 node_id: interpreter.resolve_event_node_id(target),
1365 midi: note,
1366 start_time: start_time_s,
1367 duration: duration_s,
1368 velocity: note_velocity as f32,
1369 synth_id: target.to_string(),
1370 synth_def,
1371 pan: 0.0,
1372 detune: 0.0,
1373 gain: 1.0,
1374 attack: None,
1375 release: None,
1376 delay_time: None,
1377 delay_feedback: None,
1378 delay_mix: None,
1379 reverb_amount: None,
1380 drive_amount: None,
1381 drive_color: None,
1382 effects: None,
1383 use_per_note_automation: false,
1384 };
1385
1386 interpreter.events.events.push(event);
1388 }
1389 }
1390
1391 }
1393 }
1394
1395 Ok(())
1396}
1397
1398#[cfg(feature = "cli")]
1399pub fn handle_use_plugin(
1400 interpreter: &mut AudioInterpreter,
1401 author: &str,
1402 name: &str,
1403 alias: &str,
1404) -> Result<()> {
1405 use crate::engine::plugin::loader::load_plugin;
1406
1407 match load_plugin(author, name) {
1408 Ok((info, _wasm_bytes)) => {
1409 let mut plugin_map = HashMap::new();
1410 plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
1411 plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
1412 plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
1413
1414 if let Some(version) = &info.version {
1415 plugin_map.insert("_version".to_string(), Value::String(version.clone()));
1416 }
1417
1418 for export in &info.exports {
1419 let mut export_map = HashMap::new();
1420 export_map.insert(
1421 "_plugin_author".to_string(),
1422 Value::String(info.author.clone()),
1423 );
1424 export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
1425 export_map.insert(
1426 "_export_name".to_string(),
1427 Value::String(export.name.clone()),
1428 );
1429 export_map.insert(
1430 "_export_kind".to_string(),
1431 Value::String(export.kind.clone()),
1432 );
1433
1434 plugin_map.insert(export.name.clone(), Value::Map(export_map));
1435 }
1436 crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1438
1439 interpreter
1440 .variables
1441 .insert(alias.to_string(), Value::Map(plugin_map));
1442 }
1443 Err(e) => {
1444 eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
1445 return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
1446 }
1447 }
1448
1449 Ok(())
1450}
1451
1452#[cfg(not(feature = "cli"))]
1453pub fn handle_use_plugin(
1454 interpreter: &mut AudioInterpreter,
1455 author: &str,
1456 name: &str,
1457 alias: &str,
1458) -> Result<()> {
1459 let mut plugin_map = HashMap::new();
1461 plugin_map.insert(
1462 "_type".to_string(),
1463 Value::String("plugin_stub".to_string()),
1464 );
1465 plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
1466 plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
1467 crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1469
1470 interpreter
1471 .variables
1472 .insert(alias.to_string(), Value::Map(plugin_map));
1473 Ok(())
1474}
1475
1476pub fn handle_bank(
1477 interpreter: &mut AudioInterpreter,
1478 name: &str,
1479 alias: &Option<String>,
1480) -> Result<()> {
1481 let target_alias = alias
1482 .clone()
1483 .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
1484
1485 if let Some(existing_value) = interpreter.variables.get(name) {
1486 interpreter
1487 .variables
1488 .insert(target_alias.clone(), existing_value.clone());
1489 } else {
1490 #[cfg(feature = "wasm")]
1491 {
1492 use crate::web::registry::banks::REGISTERED_BANKS;
1493 REGISTERED_BANKS.with(|banks| {
1494 for bank in banks.borrow().iter() {
1495 if bank.full_name == *name {
1496 if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
1497 interpreter
1498 .variables
1499 .insert(target_alias.clone(), Value::Map(bank_map.clone()));
1500 }
1501 }
1502 }
1503 });
1504 }
1505
1506 #[cfg(not(feature = "wasm"))]
1507 {
1508 if let Ok(current_dir) = std::env::current_dir() {
1509 match interpreter.banks.register_bank(
1510 target_alias.clone(),
1511 &name,
1512 ¤t_dir,
1513 ¤t_dir,
1514 ) {
1515 Ok(_) => {
1516 let mut bank_map = HashMap::new();
1517 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1518 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1519 interpreter
1520 .variables
1521 .insert(target_alias.clone(), Value::Map(bank_map));
1522 }
1523 Err(e) => {
1524 eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
1525 let mut bank_map = HashMap::new();
1526 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1527 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1528 interpreter
1529 .variables
1530 .insert(target_alias.clone(), Value::Map(bank_map));
1531 }
1532 }
1533 } else {
1534 let mut bank_map = HashMap::new();
1535 bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1536 bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1537 interpreter
1538 .variables
1539 .insert(target_alias.clone(), Value::Map(bank_map));
1540 eprintln!(
1541 "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
1542 name
1543 );
1544 }
1545 }
1546 }
1547
1548 Ok(())
1549}
1550
1551pub fn handle_trigger(
1552 interpreter: &mut AudioInterpreter,
1553 entity: &str,
1554 effects: Option<&crate::language::syntax::ast::Value>,
1555) -> Result<()> {
1556 let resolved_entity = if entity.starts_with('.') {
1557 &entity[1..]
1558 } else {
1559 entity
1560 };
1561
1562 if let Some(var_val) = interpreter.variables.get(resolved_entity).cloned() {
1565 if let crate::language::syntax::ast::Value::Statement(stmt_box) = var_val {
1566 if let crate::language::syntax::ast::StatementKind::Trigger {
1567 entity: inner_entity,
1568 duration: _,
1569 effects: stored_effects,
1570 } = &stmt_box.kind
1571 {
1572 if inner_entity != resolved_entity {
1574 let chosen_effects = stored_effects.as_ref().or(effects);
1576 return handle_trigger(interpreter, inner_entity, chosen_effects);
1577 } else {
1578 return Ok(());
1579 }
1580 }
1581 }
1582 }
1583
1584 if resolved_entity.contains('.') {
1585 let parts: Vec<&str> = resolved_entity.split('.').collect();
1586 if parts.len() == 2 {
1587 let (var_name, property) = (parts[0], parts[1]);
1588
1589 if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
1590 if let Some(Value::String(sample_uri)) = map.get(property) {
1591 let uri = sample_uri.trim_matches('"').trim_matches('\'');
1592 interpreter.events.add_sample_event_with_effects(
1594 uri,
1595 interpreter.cursor_time,
1596 1.0,
1597 effects.cloned(),
1598 );
1599 let beat_duration = interpreter.beat_duration();
1600 interpreter.cursor_time += beat_duration;
1601 } else {
1602 #[cfg(not(feature = "wasm"))]
1603 {
1604 let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
1606 if resolved_uri != resolved_entity {
1607 interpreter.events.add_sample_event_with_effects(
1609 &resolved_uri,
1610 interpreter.cursor_time,
1611 1.0,
1612 effects.cloned(),
1613 );
1614 let beat_duration = interpreter.beat_duration();
1615 interpreter.cursor_time += beat_duration;
1616 } else if let Some(pathbuf) =
1617 interpreter.banks.resolve_trigger(var_name, property)
1618 {
1619 if let Some(path_str) = pathbuf.to_str() {
1620 interpreter.events.add_sample_event_with_effects(
1622 path_str,
1623 interpreter.cursor_time,
1624 1.0,
1625 effects.cloned(),
1626 );
1627 let beat_duration = interpreter.beat_duration();
1628 interpreter.cursor_time += beat_duration;
1629 } else {
1630 println!(
1631 "⚠️ Resolution failed for {}.{} (invalid path)",
1632 var_name, property
1633 );
1634 }
1635 } else {
1636 }
1638 }
1639 }
1640 }
1641 }
1642 } else {
1643 if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
1644 let uri = sample_uri.trim_matches('"').trim_matches('\'');
1645 interpreter.events.add_sample_event_with_effects(
1646 uri,
1647 interpreter.cursor_time,
1648 1.0,
1649 effects.cloned(),
1650 );
1651 let beat_duration = interpreter.beat_duration();
1652 interpreter.cursor_time += beat_duration;
1653 }
1654 }
1655
1656 Ok(())
1660}
1661
1662pub fn handle_fade(
1664 interpreter: &mut AudioInterpreter,
1665 direction: &str,
1666 target: &str,
1667 duration: &crate::language::syntax::ast::DurationValue,
1668) -> Result<()> {
1669 let normalized_target = if target.starts_with('.') {
1671 &target[1..] } else {
1673 target
1674 };
1675
1676 let parts: Vec<&str> = normalized_target.split('.').collect();
1677 if parts.len() != 2 {
1678 return Err(anyhow::anyhow!(
1679 "Fade target must be in format 'entity.property', got '{}'",
1680 target
1681 ));
1682 }
1683
1684 let entity = parts[0];
1685 let property = parts[1];
1686
1687 let dur_secs = interpreter.duration_to_seconds(duration)?;
1688
1689 let (from_value, to_value) = match direction.to_lowercase().as_str() {
1691 "in" => (0.0, 1.0), "out" => (1.0, 0.0), _ => {
1694 return Err(anyhow::anyhow!(
1695 "Fade direction must be 'in' or 'out', got '{}'",
1696 direction
1697 ));
1698 }
1699 };
1700
1701 let mut fade_envelope = crate::engine::audio::automation::AutomationEnvelope::new(format!(
1704 "{}.{}",
1705 entity, property
1706 ));
1707
1708 use crate::engine::audio::automation::AutomationCurve;
1710 fade_envelope.add_param(crate::engine::audio::automation::AutomationParam {
1711 param_name: property.to_string(),
1712 from_value,
1713 to_value,
1714 start_time: interpreter.cursor_time,
1715 duration: dur_secs,
1716 curve: AutomationCurve::Linear,
1717 });
1718
1719 interpreter.automation_registry.register(fade_envelope);
1720
1721 interpreter.cursor_time += dur_secs;
1723
1724 Ok(())
1725}
1726
1727pub fn handle_stop(
1729 interpreter: &mut AudioInterpreter,
1730 target: Option<&str>,
1731 delay: f32,
1732) -> Result<()> {
1733 let stop_time = delay;
1736
1737 if let Some(entity_name) = target {
1738 let stop_event = AudioEvent::Stop {
1740 target: Some(entity_name.to_string()),
1741 time: stop_time,
1742 };
1743 interpreter.events.events.push(stop_event);
1744 } else {
1745 let stop_event = AudioEvent::Stop {
1747 target: None,
1748 time: stop_time,
1749 };
1750 interpreter.events.events.push(stop_event);
1751 }
1752
1753 Ok(())
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758 use super::*;
1759
1760 #[test]
1761 fn test_bare_synth_with_preset_registers_synth() {
1762 let mut interpreter = AudioInterpreter::new(44_100);
1763 let mut map = HashMap::new();
1764 map.insert("type".to_string(), Value::String("synth".to_string()));
1765 map.insert("synth_type".to_string(), Value::String("bass".to_string()));
1766
1767 handle_let(&mut interpreter, "bass", &Value::Map(map)).unwrap();
1768
1769 let synth = interpreter
1770 .events
1771 .get_synth("bass")
1772 .expect("bare synth with preset should be registered");
1773 assert_eq!(synth.waveform, "sine");
1774 assert_eq!(synth.synth_type.as_deref(), Some("bass"));
1775 }
1776}