1use rhai::Engine;
6#[cfg(not(target_arch = "wasm32"))]
7use std::path::{Path, PathBuf};
8use vibelang_core::reload::ScriptState;
9
10use crate::api;
11use crate::context;
12use crate::error::{Error, Result};
13
14#[cfg(target_arch = "wasm32")]
19mod wasm_resolver {
20 use rhai::module_resolvers::ModuleResolver;
21 use rhai::{Engine, Module, Position, Scope, AST};
22 use std::collections::HashMap;
23 use std::sync::Arc;
24
25 #[derive(Clone)]
30 pub struct InMemoryModuleResolver {
31 modules: Arc<HashMap<String, String>>,
33 extension: String,
35 }
36
37 impl InMemoryModuleResolver {
38 pub fn new(modules: HashMap<String, String>) -> Self {
40 Self {
41 modules: Arc::new(modules),
42 extension: "vibe".to_string(),
43 }
44 }
45
46 fn normalize_path(&self, path: &str) -> String {
48 let mut normalized = path.to_string();
49
50 if normalized.starts_with("./") {
52 normalized = normalized[2..].to_string();
53 } else if normalized.starts_with('/') {
54 normalized = normalized[1..].to_string();
55 }
56
57 if normalized.starts_with("stdlib/") {
59 normalized = normalized[7..].to_string();
60 }
61
62 if !normalized.ends_with(&format!(".{}", self.extension)) {
64 normalized = format!("{}.{}", normalized, self.extension);
65 }
66
67 normalized
68 }
69 }
70
71 impl ModuleResolver for InMemoryModuleResolver {
72 fn resolve(
73 &self,
74 engine: &Engine,
75 _source: Option<&str>,
76 path: &str,
77 pos: Position,
78 ) -> Result<rhai::Shared<Module>, Box<rhai::EvalAltResult>> {
79 let normalized = self.normalize_path(path);
80
81 let source = self.modules.get(&normalized).ok_or_else(|| {
83 Box::new(rhai::EvalAltResult::ErrorModuleNotFound(
84 format!("Module not found: {} (looked for: {})", path, normalized),
85 pos,
86 ))
87 })?;
88
89 let ast = engine.compile(source).map_err(|e| {
91 Box::new(rhai::EvalAltResult::ErrorInModule(
92 path.to_string(),
93 e.into(),
94 pos,
95 ))
96 })?;
97
98 let module = Module::eval_ast_as_new(Scope::new(), &ast, engine).map_err(|e| {
100 Box::new(rhai::EvalAltResult::ErrorInModule(path.to_string(), e, pos))
101 })?;
102
103 Ok(rhai::Shared::new(module))
104 }
105
106 fn resolve_ast(
107 &self,
108 engine: &Engine,
109 _source: Option<&str>,
110 path: &str,
111 pos: Position,
112 ) -> Option<Result<AST, Box<rhai::EvalAltResult>>> {
113 let normalized = self.normalize_path(path);
114
115 let source = match self.modules.get(&normalized) {
117 Some(s) => s,
118 None => {
119 return Some(Err(Box::new(rhai::EvalAltResult::ErrorModuleNotFound(
120 format!("Module not found: {} (looked for: {})", path, normalized),
121 pos,
122 ))))
123 }
124 };
125
126 Some(engine.compile(source).map_err(|e| {
128 Box::new(rhai::EvalAltResult::ErrorInModule(
129 path.to_string(),
130 e.into(),
131 pos,
132 ))
133 }))
134 }
135 }
136}
137
138#[cfg(target_arch = "wasm32")]
139pub use wasm_resolver::InMemoryModuleResolver;
140
141pub struct ScriptEngine {
156 engine: Engine,
157 #[cfg(not(target_arch = "wasm32"))]
158 import_paths: Vec<PathBuf>,
159}
160
161impl ScriptEngine {
162 pub fn new() -> Self {
164 let mut engine = Engine::new();
165
166 engine.set_max_expr_depths(4096, 4096);
168 engine.set_max_call_levels(4096);
169
170 engine.on_print(|text| {
172 log::info!("[script] {}", text);
173 });
174
175 engine.on_debug(|text, source, pos| {
177 let loc = match (source, pos) {
178 (Some(src), pos) if !pos.is_none() => format!(" ({}:{})", src, pos),
179 (Some(src), _) => format!(" ({})", src),
180 (None, pos) if !pos.is_none() => format!(" ({})", pos),
181 _ => String::new(),
182 };
183 log::debug!("[script]{} {}", loc, text);
184 });
185
186 api::register_api(&mut engine);
188
189 vibelang_dsp::register_dsp_api(&mut engine);
191
192 #[cfg(target_arch = "wasm32")]
194 {
195 let stdlib_files = vibelang_std::get_stdlib_files();
196 let resolver = InMemoryModuleResolver::new(stdlib_files);
197 engine.set_module_resolver(resolver);
198 }
199
200 Self {
201 engine,
202 #[cfg(not(target_arch = "wasm32"))]
203 import_paths: Vec::new(),
204 }
205 }
206
207 #[cfg(not(target_arch = "wasm32"))]
209 pub fn add_import_path(&mut self, path: impl Into<PathBuf>) {
210 self.import_paths.push(path.into());
211 }
212
213 pub fn execute(&mut self, script: &str) -> Result<ScriptState> {
217 api::clear_all_registries();
219
220 crate::reset_exit_code();
222
223 context::init_context();
225
226 let result = self.engine.run(script).map_err(Error::from);
228
229 let state = context::take_state();
231 context::clear_context();
232
233 if crate::get_exit_code().is_some() {
235 return Ok(state);
237 }
238
239 result?;
241
242 Ok(state)
243 }
244
245 #[cfg(not(target_arch = "wasm32"))]
249 pub fn execute_file(&mut self, path: impl AsRef<Path>) -> Result<ScriptState> {
250 let path = path.as_ref();
251
252 let script = std::fs::read_to_string(path)?;
254
255 let base_path = path.parent().unwrap_or(Path::new(".")).to_path_buf();
257 self.setup_module_resolver(base_path);
258
259 api::clear_all_registries();
261
262 crate::reset_exit_code();
264
265 context::init_context();
267 context::set_current_file(Some(path.to_path_buf()));
268 context::set_import_paths(self.import_paths.clone());
269
270 let result = self.engine.run(&script).map_err(Error::from);
272
273 let state = context::take_state();
275 context::clear_context();
276
277 tracing::debug!(
279 "Script execution complete: {} melodies, {} playing_melodies, {} voices",
280 state.melodies.len(),
281 state.playing_melodies.len(),
282 state.voices.len()
283 );
284 for (id, config) in &state.melodies {
285 tracing::debug!(
286 " Melody {:?} '{}': voice={:?}, notes={}, length={:.2}",
287 id,
288 config.name,
289 config.voice,
290 config.notes.len(),
291 config.length.to_f64()
292 );
293 }
294 for id in &state.playing_melodies {
295 tracing::debug!(" Playing melody: {:?}", id);
296 }
297
298 if crate::get_exit_code().is_some() {
300 return Ok(state);
302 }
303
304 result?;
306
307 Ok(state)
308 }
309
310 pub fn execute_ast(&mut self, ast: &rhai::AST) -> Result<ScriptState> {
312 api::clear_all_registries();
314
315 context::init_context();
317
318 let result = self.engine.run_ast(ast).map_err(Error::from);
320
321 let state = context::take_state();
323 context::clear_context();
324
325 result?;
326
327 Ok(state)
328 }
329
330 pub fn compile(&self, script: &str) -> Result<rhai::AST> {
332 self.engine.compile(script).map_err(Error::from)
333 }
334
335 #[cfg(not(target_arch = "wasm32"))]
337 pub fn compile_file(&self, path: impl AsRef<Path>) -> Result<rhai::AST> {
338 let path = path.as_ref();
339 self.engine
340 .compile_file(path.to_path_buf())
341 .map_err(Error::from)
342 }
343
344 #[cfg(not(target_arch = "wasm32"))]
346 fn setup_module_resolver(&mut self, base_path: PathBuf) {
347 let mut collection = rhai::module_resolvers::ModuleResolversCollection::new();
348
349 let mut source_resolver = rhai::module_resolvers::FileModuleResolver::new();
351 source_resolver.set_extension("vibe");
352 collection.push(source_resolver);
353
354 let mut base_resolver = rhai::module_resolvers::FileModuleResolver::new();
356 base_resolver.set_base_path(base_path);
357 base_resolver.set_extension("vibe");
358 collection.push(base_resolver);
359
360 for import_path in &self.import_paths {
362 let mut resolver = rhai::module_resolvers::FileModuleResolver::new();
363 resolver.set_base_path(import_path.clone());
364 resolver.set_extension("vibe");
365 collection.push(resolver);
366 }
367
368 self.engine.set_module_resolver(collection);
369 }
370
371 pub fn engine(&self) -> &Engine {
373 &self.engine
374 }
375
376 pub fn engine_mut(&mut self) -> &mut Engine {
378 &mut self.engine
379 }
380
381 #[cfg(any(feature = "ext-fs", feature = "ext-exec", feature = "ext-net"))]
406 pub fn register_extensions(&mut self, config: &crate::extensions::ExtensionConfig) {
407 crate::extensions::register_extensions(&mut self.engine, config);
408 }
409
410 #[cfg(any(feature = "ext-fs", feature = "ext-exec", feature = "ext-net"))]
415 pub fn register_all_extensions(&mut self) {
416 let config = crate::extensions::ExtensionConfig::enable_all();
417 crate::extensions::register_extensions(&mut self.engine, &config);
418 }
419}
420
421impl Default for ScriptEngine {
422 fn default() -> Self {
423 Self::new()
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_execute_simple_script() {
433 let mut engine = ScriptEngine::new();
434 let state = engine.execute("set_tempo(140);").unwrap();
435 assert_eq!(state.tempo, 140.0);
436 }
437
438 #[test]
439 fn test_execute_with_group() {
440 let mut engine = ScriptEngine::new();
441 let state = engine
442 .execute(
443 r#"
444 set_tempo(128);
445 define_group("Drums", || {
446 let kick = voice("kick").synth("kick_synth").gain(db(-6));
447 });
448 "#,
449 )
450 .unwrap();
451
452 assert_eq!(state.tempo, 128.0);
453 assert!(!state.groups.is_empty());
454 assert!(!state.voices.is_empty());
455 }
456
457 #[test]
458 fn test_helper_functions() {
459 let mut engine = ScriptEngine::new();
460 let state = engine
461 .execute(
462 r#"
463 let amp = db(-6);
464 let midi = note("C4");
465 let beats = bars(2);
466 set_tempo(120);
467 "#,
468 )
469 .unwrap();
470
471 assert_eq!(state.tempo, 120.0);
472 }
473
474 #[test]
475 fn test_pattern_api() {
476 let mut engine = ScriptEngine::new();
477 let state = engine
478 .execute(
479 r#"
480 set_tempo(130);
481 define_group("Drums", || {
482 let kick = voice("kick").synth("kick_909").gain(db(-6));
483 let hihat = voice("hihat").synth("hihat_909").gain(db(-10));
484
485 let kick_ptn = pattern("kick_main")
486 .on(kick)
487 .step("x... x... x... x...")
488 .apply();
489
490 let hihat_ptn = pattern("hihat_main")
491 .on(hihat)
492 .step("..x. ..x. ..x. ..x.")
493 .apply();
494 });
495 "#,
496 )
497 .unwrap();
498
499 assert_eq!(state.tempo, 130.0);
500 assert!(!state.groups.is_empty(), "Should have groups");
501 assert_eq!(state.voices.len(), 2, "Should have 2 voices");
502 assert_eq!(state.patterns.len(), 2, "Should have 2 patterns");
503 }
504
505 #[test]
506 fn test_melody_api() {
507 let mut engine = ScriptEngine::new();
508 let state = engine
509 .execute(
510 r#"
511 set_tempo(120);
512 define_group("Synth", || {
513 let lead = voice("lead").synth("saw_lead").gain(db(-8));
514
515 let melody = melody("main_melody")
516 .on(lead)
517 .notes("C4 D4 E4 F4 | G4 A4 B4 C5")
518 .transpose(0)
519 .apply();
520 });
521 "#,
522 )
523 .unwrap();
524
525 assert_eq!(state.tempo, 120.0);
526 assert_eq!(state.voices.len(), 1, "Should have 1 voice");
527 assert_eq!(state.melodies.len(), 1, "Should have 1 melody");
528
529 let melody_id = state.melodies.keys().next().unwrap();
531 let melody_config = state.melodies.get(melody_id).unwrap();
532 assert!(
533 melody_config.notes.len() >= 8,
534 "Should have at least 8 notes"
535 );
536 }
537
538 #[test]
539 fn test_sequence_api() {
540 let mut engine = ScriptEngine::new();
541 let state = engine
542 .execute(
543 r#"
544 set_tempo(128);
545
546 define_group("Drums", || {
547 let kick = voice("kick").synth("kick_909");
548 let kick_ptn = pattern("kick_main")
549 .on(kick)
550 .step("x... x... x... x...")
551 .apply();
552 });
553
554 let main_seq = sequence("arrangement")
555 .loop_bars(8)
556 .apply();
557 "#,
558 )
559 .unwrap();
560
561 assert_eq!(state.tempo, 128.0);
562 assert_eq!(state.sequences.len(), 1, "Should have 1 sequence");
563 }
564
565 #[test]
566 fn test_euclidean_pattern() {
567 let mut engine = ScriptEngine::new();
568 let state = engine
569 .execute(
570 r#"
571 set_tempo(120);
572 define_group("Drums", || {
573 let perc = voice("perc").synth("perc_synth");
574
575 // Euclidean rhythm: 5 hits in 16 steps
576 let euclid_ptn = pattern("euclid_5_16")
577 .on(perc)
578 .euclid(5, 16)
579 .apply();
580 });
581 "#,
582 )
583 .unwrap();
584
585 assert_eq!(state.patterns.len(), 1, "Should have 1 pattern");
586 let pattern_id = state.patterns.keys().next().unwrap();
587 let pattern_config = state.patterns.get(pattern_id).unwrap();
588 assert_eq!(pattern_config.steps.len(), 5, "Should have 5 steps (hits)");
589 }
590
591 #[test]
592 fn test_fx_api() {
593 let mut engine = ScriptEngine::new();
594 let state = engine
595 .execute(
596 r#"
597 set_tempo(120);
598 define_group("Synth", || {
599 let lead = voice("lead").synth("saw_lead");
600
601 // Add reverb effect
602 fx("reverb")
603 .synth("reverb_fx")
604 .param("room", 0.8)
605 .param("mix", 0.3)
606 .apply();
607 });
608 "#,
609 )
610 .unwrap();
611
612 assert_eq!(state.effects.len(), 1, "Should have 1 effect");
613 }
614
615 #[test]
616 fn test_comprehensive_script() {
617 let mut engine = ScriptEngine::new();
618 let state = engine
619 .execute(
620 r#"
621 // Full minimal techno-style script
622 set_tempo(130);
623 set_time_signature(4, 4);
624
625 // Drums group
626 define_group("Drums", || {
627 let kick = voice("kick").synth("kick_909").gain(db(-6));
628 let hihat = voice("hihat").synth("hihat_909").gain(db(-10));
629 let clap = voice("clap").synth("clap_909").gain(db(-8));
630
631 pattern("kick_main").on(kick).step("x... .... x... ....").apply();
632 pattern("hihat_main").on(hihat).step("..x. ..x. ..x. ..x.").apply();
633 pattern("clap_main").on(clap).step(".... x... .... x...").apply();
634 });
635
636 // Bass group
637 define_group("Bass", || {
638 let bass = voice("bass").synth("acid_bass").gain(db(-4));
639 melody("bassline").on(bass).notes("C2 . C2 . | C2 . E2 F2").apply();
640 });
641
642 // Lead group with effect
643 define_group("Lead", || {
644 let lead = voice("lead").synth("saw_lead").gain(db(-8));
645 melody("lead_melody").on(lead).notes("G4 - - . | A4 - G4 F4").apply();
646
647 fx("lead_reverb")
648 .synth("reverb_fx")
649 .param("room", 0.6)
650 .apply();
651 });
652
653 // Main arrangement
654 sequence("main")
655 .loop_bars(16)
656 .apply();
657 "#,
658 )
659 .unwrap();
660
661 assert_eq!(state.tempo, 130.0);
663 assert!(state.groups.len() >= 3, "Should have at least 3 groups");
664 assert_eq!(state.voices.len(), 5, "Should have 5 voices");
665 assert_eq!(state.patterns.len(), 3, "Should have 3 patterns");
666 assert_eq!(state.melodies.len(), 2, "Should have 2 melodies");
667 assert_eq!(state.effects.len(), 1, "Should have 1 effect");
668 assert_eq!(state.sequences.len(), 1, "Should have 1 sequence");
669 }
670
671 #[test]
674 fn test_voice_mute() {
675 let mut engine = ScriptEngine::new();
676 let state = engine
677 .execute(
678 r#"
679 set_tempo(120);
680 define_group("Test", || {
681 let v = voice("test_voice").synth("test_synth").mute();
682 });
683 "#,
684 )
685 .unwrap();
686
687 let voice_id = state.voices.keys().next().unwrap();
688 let voice_config = state.voices.get(voice_id).unwrap();
689 assert!(voice_config.muted, "Voice should be muted");
690 assert!(!voice_config.soloed, "Voice should not be soloed");
691 }
692
693 #[test]
694 fn test_voice_unmute() {
695 let mut engine = ScriptEngine::new();
696 let state = engine
697 .execute(
698 r#"
699 set_tempo(120);
700 define_group("Test", || {
701 let v = voice("test_voice").synth("test_synth").mute().unmute();
702 });
703 "#,
704 )
705 .unwrap();
706
707 let voice_id = state.voices.keys().next().unwrap();
708 let voice_config = state.voices.get(voice_id).unwrap();
709 assert!(
710 !voice_config.muted,
711 "Voice should not be muted after unmute"
712 );
713 }
714
715 #[test]
716 fn test_voice_solo() {
717 let mut engine = ScriptEngine::new();
718 let state = engine
719 .execute(
720 r#"
721 set_tempo(120);
722 define_group("Test", || {
723 let v = voice("test_voice").synth("test_synth").solo();
724 });
725 "#,
726 )
727 .unwrap();
728
729 let voice_id = state.voices.keys().next().unwrap();
730 let voice_config = state.voices.get(voice_id).unwrap();
731 assert!(voice_config.soloed, "Voice should be soloed");
732 assert!(!voice_config.muted, "Voice should not be muted");
733 }
734
735 #[test]
736 fn test_voice_unsolo() {
737 let mut engine = ScriptEngine::new();
738 let state = engine
739 .execute(
740 r#"
741 set_tempo(120);
742 define_group("Test", || {
743 let v = voice("test_voice").synth("test_synth").solo().unsolo();
744 });
745 "#,
746 )
747 .unwrap();
748
749 let voice_id = state.voices.keys().next().unwrap();
750 let voice_config = state.voices.get(voice_id).unwrap();
751 assert!(
752 !voice_config.soloed,
753 "Voice should not be soloed after unsolo"
754 );
755 }
756
757 #[test]
758 fn test_voice_mute_solo_chain() {
759 let mut engine = ScriptEngine::new();
760 let state = engine
761 .execute(
762 r#"
763 set_tempo(120);
764 define_group("Test", || {
765 // Mute, then solo, then unmute - should be soloed but not muted
766 let v = voice("test_voice").synth("test_synth").mute().solo().unmute();
767 });
768 "#,
769 )
770 .unwrap();
771
772 let voice_id = state.voices.keys().next().unwrap();
773 let voice_config = state.voices.get(voice_id).unwrap();
774 assert!(!voice_config.muted, "Voice should not be muted");
775 assert!(voice_config.soloed, "Voice should be soloed");
776 }
777
778 #[test]
779 fn test_multiple_voices_mute_solo() {
780 let mut engine = ScriptEngine::new();
781 let state = engine
782 .execute(
783 r#"
784 set_tempo(120);
785 define_group("Test", || {
786 let v1 = voice("voice1").synth("synth").mute();
787 let v2 = voice("voice2").synth("synth").solo();
788 let v3 = voice("voice3").synth("synth"); // Default: not muted, not soloed
789 });
790 "#,
791 )
792 .unwrap();
793
794 assert_eq!(state.voices.len(), 3);
795
796 let mut muted_count = 0;
797 let mut soloed_count = 0;
798 let mut default_count = 0;
799
800 for voice_config in state.voices.values() {
801 if voice_config.muted && !voice_config.soloed {
802 muted_count += 1;
803 } else if voice_config.soloed && !voice_config.muted {
804 soloed_count += 1;
805 } else if !voice_config.muted && !voice_config.soloed {
806 default_count += 1;
807 }
808 }
809
810 assert_eq!(muted_count, 1, "Should have 1 muted voice");
811 assert_eq!(soloed_count, 1, "Should have 1 soloed voice");
812 assert_eq!(default_count, 1, "Should have 1 default voice");
813 }
814
815 #[test]
818 fn test_group_mute() {
819 let mut engine = ScriptEngine::new();
820 let state = engine
821 .execute(
822 r#"
823 set_tempo(120);
824 define_group("MutedGroup", || {
825 let v = voice("test_voice").synth("test_synth");
826 });
827 group("MutedGroup").mute();
828 "#,
829 )
830 .unwrap();
831
832 let group = state
834 .groups
835 .values()
836 .find(|g| g.name == "MutedGroup")
837 .expect("Should find MutedGroup");
838
839 assert!(group.muted, "Group should be muted");
840 assert!(!group.soloed, "Group should not be soloed");
841 }
842
843 #[test]
844 fn test_group_unmute() {
845 let mut engine = ScriptEngine::new();
846 let state = engine
847 .execute(
848 r#"
849 set_tempo(120);
850 define_group("TestGroup", || {
851 let v = voice("test_voice").synth("test_synth");
852 });
853 group("TestGroup").mute().unmute();
854 "#,
855 )
856 .unwrap();
857
858 let group = state
859 .groups
860 .values()
861 .find(|g| g.name == "TestGroup")
862 .expect("Should find TestGroup");
863
864 assert!(!group.muted, "Group should not be muted after unmute");
865 }
866
867 #[test]
868 fn test_group_solo() {
869 let mut engine = ScriptEngine::new();
870 let state = engine
871 .execute(
872 r#"
873 set_tempo(120);
874 define_group("SoloGroup", || {
875 let v = voice("test_voice").synth("test_synth");
876 });
877 group("SoloGroup").solo(true);
878 "#,
879 )
880 .unwrap();
881
882 let group = state
883 .groups
884 .values()
885 .find(|g| g.name == "SoloGroup")
886 .expect("Should find SoloGroup");
887
888 assert!(group.soloed, "Group should be soloed");
889 assert!(!group.muted, "Group should not be muted");
890 }
891
892 #[test]
893 fn test_group_solo_false() {
894 let mut engine = ScriptEngine::new();
895 let state = engine
896 .execute(
897 r#"
898 set_tempo(120);
899 define_group("TestGroup", || {
900 let v = voice("test_voice").synth("test_synth");
901 });
902 group("TestGroup").solo(true).solo(false);
903 "#,
904 )
905 .unwrap();
906
907 let group = state
908 .groups
909 .values()
910 .find(|g| g.name == "TestGroup")
911 .expect("Should find TestGroup");
912
913 assert!(
914 !group.soloed,
915 "Group should not be soloed after solo(false)"
916 );
917 }
918
919 #[test]
920 fn test_group_set_param() {
921 let mut engine = ScriptEngine::new();
922 let state = engine
923 .execute(
924 r#"
925 set_tempo(120);
926 define_group("TestGroup", || {
927 let v = voice("test_voice").synth("test_synth");
928 });
929 group("TestGroup").set_param("filter_cutoff", 0.75);
930 "#,
931 )
932 .unwrap();
933
934 let group = state
935 .groups
936 .values()
937 .find(|g| g.name == "TestGroup")
938 .expect("Should find TestGroup");
939
940 let param = group
941 .params
942 .get("filter_cutoff")
943 .expect("Should have filter_cutoff param");
944 assert!((param - 0.75).abs() < 0.001, "filter_cutoff should be 0.75");
945 }
946
947 #[test]
948 fn test_group_gain() {
949 let mut engine = ScriptEngine::new();
950 let state = engine
951 .execute(
952 r#"
953 set_tempo(120);
954 define_group("TestGroup", || {
955 let v = voice("test_voice").synth("test_synth");
956 });
957 group("TestGroup").gain(0.5);
958 "#,
959 )
960 .unwrap();
961
962 let group = state
963 .groups
964 .values()
965 .find(|g| g.name == "TestGroup")
966 .expect("Should find TestGroup");
967
968 let amp = group.params.get("amp").expect("Should have amp param");
969 assert!((amp - 0.5).abs() < 0.001, "amp should be 0.5");
970 }
971
972 #[test]
975 fn test_pattern_start_is_playing() {
976 let mut engine = ScriptEngine::new();
977 let state = engine
978 .execute(
979 r#"
980 set_tempo(120);
981 define_group("Test", || {
982 let v = voice("test_voice").synth("test_synth");
983 let p = pattern("test_pattern").on(v).step("x...").start();
984 });
985 "#,
986 )
987 .unwrap();
988
989 assert_eq!(
990 state.playing_patterns.len(),
991 1,
992 "Should have 1 playing pattern"
993 );
994 }
995
996 #[test]
997 fn test_pattern_stop_not_playing() {
998 let mut engine = ScriptEngine::new();
999 let state = engine
1000 .execute(
1001 r#"
1002 set_tempo(120);
1003 define_group("Test", || {
1004 let v = voice("test_voice").synth("test_synth");
1005 let p = pattern("test_pattern").on(v).step("x...");
1006 p.start();
1007 p.stop();
1008 });
1009 "#,
1010 )
1011 .unwrap();
1012
1013 assert_eq!(
1014 state.playing_patterns.len(),
1015 0,
1016 "Should have 0 playing patterns after stop"
1017 );
1018 }
1019
1020 #[test]
1021 fn test_pattern_launch() {
1022 let mut engine = ScriptEngine::new();
1023 let state = engine
1024 .execute(
1025 r#"
1026 set_tempo(120);
1027 set_quantization(4.0); // Quantize to 1 bar
1028 define_group("Test", || {
1029 let v = voice("test_voice").synth("test_synth");
1030 let p = pattern("test_pattern").on(v).step("x...").launch();
1031 });
1032 "#,
1033 )
1034 .unwrap();
1035
1036 assert_eq!(
1038 state.playing_patterns.len(),
1039 1,
1040 "Should have 1 playing pattern after launch"
1041 );
1042 }
1043
1044 #[test]
1045 fn test_melody_start_is_playing() {
1046 let mut engine = ScriptEngine::new();
1047 let state = engine
1048 .execute(
1049 r#"
1050 set_tempo(120);
1051 define_group("Test", || {
1052 let v = voice("test_voice").synth("test_synth");
1053 let m = melody("test_melody").on(v).notes("C4 D4 E4 F4").start();
1054 });
1055 "#,
1056 )
1057 .unwrap();
1058
1059 assert_eq!(
1060 state.playing_melodies.len(),
1061 1,
1062 "Should have 1 playing melody"
1063 );
1064 }
1065
1066 #[test]
1067 fn test_melody_stop_not_playing() {
1068 let mut engine = ScriptEngine::new();
1069 let state = engine
1070 .execute(
1071 r#"
1072 set_tempo(120);
1073 define_group("Test", || {
1074 let v = voice("test_voice").synth("test_synth");
1075 let m = melody("test_melody").on(v).notes("C4 D4 E4 F4");
1076 m.start();
1077 m.stop();
1078 });
1079 "#,
1080 )
1081 .unwrap();
1082
1083 assert_eq!(
1084 state.playing_melodies.len(),
1085 0,
1086 "Should have 0 playing melodies after stop"
1087 );
1088 }
1089
1090 #[test]
1091 fn test_melody_launch() {
1092 let mut engine = ScriptEngine::new();
1093 let state = engine
1094 .execute(
1095 r#"
1096 set_tempo(120);
1097 set_quantization(4.0);
1098 define_group("Test", || {
1099 let v = voice("test_voice").synth("test_synth");
1100 let m = melody("test_melody").on(v).notes("C4 D4 E4 F4").launch();
1101 });
1102 "#,
1103 )
1104 .unwrap();
1105
1106 assert_eq!(
1107 state.playing_melodies.len(),
1108 1,
1109 "Should have 1 playing melody after launch"
1110 );
1111 }
1112
1113 #[test]
1114 fn test_sequence_start_is_playing() {
1115 let mut engine = ScriptEngine::new();
1116 let state = engine
1117 .execute(
1118 r#"
1119 set_tempo(120);
1120 let seq = sequence("test_sequence").loop_bars(4).apply();
1121 seq.start();
1122 "#,
1123 )
1124 .unwrap();
1125
1126 assert_eq!(
1127 state.playing_sequences.len(),
1128 1,
1129 "Should have 1 playing sequence"
1130 );
1131 }
1132
1133 #[test]
1134 fn test_sequence_stop_not_playing() {
1135 let mut engine = ScriptEngine::new();
1136 let state = engine
1137 .execute(
1138 r#"
1139 set_tempo(120);
1140 let seq = sequence("test_sequence").loop_bars(4).apply();
1141 seq.start();
1142 seq.stop();
1143 "#,
1144 )
1145 .unwrap();
1146
1147 assert_eq!(
1148 state.playing_sequences.len(),
1149 0,
1150 "Should have 0 playing sequences after stop"
1151 );
1152 }
1153
1154 #[test]
1155 fn test_sequence_launch() {
1156 let mut engine = ScriptEngine::new();
1157 let state = engine
1158 .execute(
1159 r#"
1160 set_tempo(120);
1161 set_quantization(4.0);
1162 let seq = sequence("test_sequence").loop_bars(4).apply();
1163 seq.launch();
1164 "#,
1165 )
1166 .unwrap();
1167
1168 assert_eq!(
1169 state.playing_sequences.len(),
1170 1,
1171 "Should have 1 playing sequence after launch"
1172 );
1173 }
1174
1175 #[test]
1176 fn test_multiple_patterns_playing() {
1177 let mut engine = ScriptEngine::new();
1178 let state = engine
1179 .execute(
1180 r#"
1181 set_tempo(120);
1182 define_group("Test", || {
1183 let v = voice("test_voice").synth("test_synth");
1184 pattern("p1").on(v).step("x...").start();
1185 pattern("p2").on(v).step(".x..").start();
1186 pattern("p3").on(v).step("..x.").apply(); // apply only, not start
1187 });
1188 "#,
1189 )
1190 .unwrap();
1191
1192 assert_eq!(state.patterns.len(), 3, "Should have 3 patterns defined");
1193 assert_eq!(
1194 state.playing_patterns.len(),
1195 2,
1196 "Should have 2 playing patterns"
1197 );
1198 }
1199
1200 #[test]
1203 fn test_set_quantization() {
1204 let mut engine = ScriptEngine::new();
1205 let state = engine
1206 .execute(
1207 r#"
1208 set_tempo(120);
1209 set_quantization(4.0); // Quantize to 1 bar (4 beats)
1210 "#,
1211 )
1212 .unwrap();
1213
1214 assert!(
1215 (state.quantization - 4.0).abs() < 0.001,
1216 "Quantization should be 4.0"
1217 );
1218 }
1219
1220 #[test]
1221 fn test_quantization_default() {
1222 let mut engine = ScriptEngine::new();
1223 let state = engine
1224 .execute(
1225 r#"
1226 set_tempo(120);
1227 "#,
1228 )
1229 .unwrap();
1230
1231 assert!(
1233 (state.quantization - 0.0).abs() < 0.001,
1234 "Default quantization should be 0.0"
1235 );
1236 }
1237
1238 #[test]
1239 fn test_quantization_various_values() {
1240 let mut engine = ScriptEngine::new();
1241
1242 let test_cases = vec![
1244 (0.25, "16th note"),
1245 (0.5, "8th note"),
1246 (1.0, "quarter note"),
1247 (2.0, "half note"),
1248 (4.0, "1 bar"),
1249 (8.0, "2 bars"),
1250 (16.0, "4 bars"),
1251 ];
1252
1253 for (value, description) in test_cases {
1254 let script = format!("set_tempo(120); set_quantization({});", value);
1255 let state = engine.execute(&script).unwrap();
1256 assert!(
1257 (state.quantization - value).abs() < 0.001,
1258 "Quantization should be {} for {}",
1259 value,
1260 description
1261 );
1262 }
1263 }
1264
1265 #[test]
1268 fn test_chord_function_in_script() {
1269 let mut engine = ScriptEngine::new();
1270 let state = engine
1271 .execute(
1272 r#"
1273 set_tempo(120);
1274 define_group("Test", || {
1275 let v = voice("test_voice").synth("test_synth");
1276 // Use chord function to get notes
1277 let c_major = chord("C");
1278 let m = melody("chord_melody").on(v).notes(c_major).apply();
1279 });
1280 "#,
1281 )
1282 .unwrap();
1283
1284 let melody_config = state
1285 .melodies
1286 .values()
1287 .next()
1288 .expect("Should have a melody");
1289 assert!(
1291 melody_config.notes.len() >= 3,
1292 "Melody should have at least 3 notes from chord"
1293 );
1294 }
1295
1296 #[test]
1297 fn test_scale_function_in_script() {
1298 let mut engine = ScriptEngine::new();
1299 let state = engine
1300 .execute(
1301 r#"
1302 set_tempo(120);
1303 define_group("Test", || {
1304 let v = voice("test_voice").synth("test_synth");
1305 // Use scale function to get notes
1306 let c_scale = scale("C", "major");
1307 let m = melody("scale_melody").on(v).notes(c_scale).apply();
1308 });
1309 "#,
1310 )
1311 .unwrap();
1312
1313 let melody_config = state
1314 .melodies
1315 .values()
1316 .next()
1317 .expect("Should have a melody");
1318 assert!(
1320 melody_config.notes.len() >= 7,
1321 "Melody should have at least 7 notes from scale"
1322 );
1323 }
1324
1325 #[test]
1326 fn test_scale_degree_in_script() {
1327 let mut engine = ScriptEngine::new();
1328
1329 let result = engine.execute(
1332 r#"
1333 set_tempo(120);
1334 let degree_1 = scale_degree("C", "major", 1);
1335 let degree_5 = scale_degree("C", "major", 5);
1336 "#,
1337 );
1338
1339 assert!(
1340 result.is_ok(),
1341 "Script with scale_degree should execute without errors"
1342 );
1343 }
1344
1345 #[test]
1348 fn test_melody_add_note() {
1349 let mut engine = ScriptEngine::new();
1350 let state = engine
1351 .execute(
1352 r#"
1353 set_tempo(120);
1354 define_group("Test", || {
1355 let v = voice("test_voice").synth("test_synth");
1356 let m = melody("note_melody")
1357 .on(v)
1358 .add_note(0.0, 60, 1.0, 0.5) // C4 at beat 0
1359 .add_note(1.0, 62, 0.8, 0.5) // D4 at beat 1
1360 .add_note(2.0, 64, 0.9, 0.5) // E4 at beat 2
1361 .apply();
1362 });
1363 "#,
1364 )
1365 .unwrap();
1366
1367 let melody_config = state
1368 .melodies
1369 .values()
1370 .next()
1371 .expect("Should have a melody");
1372 assert_eq!(melody_config.notes.len(), 3, "Melody should have 3 notes");
1373 }
1374
1375 #[test]
1376 fn test_melody_add_chord_at_beat() {
1377 let mut engine = ScriptEngine::new();
1378 let state = engine
1379 .execute(
1380 r#"
1381 set_tempo(120);
1382 define_group("Test", || {
1383 let v = voice("test_voice").synth("test_synth");
1384 let c_chord = chord("C"); // [60, 64, 67]
1385 let m = melody("chord_at_beat")
1386 .on(v)
1387 .add_chord(0.0, c_chord, 1.0, 2.0)
1388 .apply();
1389 });
1390 "#,
1391 )
1392 .unwrap();
1393
1394 let melody_config = state
1395 .melodies
1396 .values()
1397 .next()
1398 .expect("Should have a melody");
1399 assert_eq!(
1401 melody_config.notes.len(),
1402 3,
1403 "Melody should have 3 notes (C major chord)"
1404 );
1405 }
1406
1407 #[test]
1410 fn test_melody_with_per_note_velocity() {
1411 let mut engine = ScriptEngine::new();
1412 let state = engine
1413 .execute(
1414 r#"
1415 set_tempo(120);
1416 define_group("Test", || {
1417 let v = voice("test_voice").synth("test_synth");
1418 melody("vel_test")
1419 .on(v)
1420 .notes("C4[velocity=100] D4[vel=50] E4 F4[vel=0.3]")
1421 .apply();
1422 });
1423 "#,
1424 )
1425 .unwrap();
1426
1427 let melody_config = state
1428 .melodies
1429 .values()
1430 .next()
1431 .expect("Should have a melody");
1432
1433 assert_eq!(melody_config.notes.len(), 4, "Should have 4 notes");
1434
1435 let vel_c4 = melody_config.notes[0].velocity;
1437 assert!(
1438 (vel_c4 - 100.0 / 127.0).abs() < 0.01,
1439 "C4 velocity should be 100/127, got {}",
1440 vel_c4
1441 );
1442
1443 let vel_d4 = melody_config.notes[1].velocity;
1445 assert!(
1446 (vel_d4 - 50.0 / 127.0).abs() < 0.01,
1447 "D4 velocity should be 50/127, got {}",
1448 vel_d4
1449 );
1450
1451 let vel_e4 = melody_config.notes[2].velocity;
1453 assert!(
1454 (vel_e4 - 1.0).abs() < 0.01,
1455 "E4 velocity should be 1.0, got {}",
1456 vel_e4
1457 );
1458
1459 let vel_f4 = melody_config.notes[3].velocity;
1461 assert!(
1462 (vel_f4 - 0.3).abs() < 0.01,
1463 "F4 velocity should be 0.3, got {}",
1464 vel_f4
1465 );
1466 }
1467
1468 #[test]
1469 fn test_melody_with_per_note_synth_params() {
1470 let mut engine = ScriptEngine::new();
1471 let state = engine
1472 .execute(
1473 r#"
1474 set_tempo(120);
1475 define_group("Test", || {
1476 let v = voice("test_voice").synth("test_synth");
1477 melody("params_test")
1478 .on(v)
1479 .notes("C4[cutoff=2000,resonance=0.8] D4 E4[pan=-0.5]")
1480 .apply();
1481 });
1482 "#,
1483 )
1484 .unwrap();
1485
1486 let melody_config = state
1487 .melodies
1488 .values()
1489 .next()
1490 .expect("Should have a melody");
1491
1492 assert_eq!(melody_config.notes.len(), 3, "Should have 3 notes");
1493
1494 assert_eq!(melody_config.notes[0].params.len(), 2);
1496 assert!(
1497 (melody_config.notes[0].params["cutoff"] - 2000.0).abs() < 0.01,
1498 "C4 should have cutoff=2000"
1499 );
1500 assert!(
1501 (melody_config.notes[0].params["resonance"] - 0.8).abs() < 0.01,
1502 "C4 should have resonance=0.8"
1503 );
1504
1505 assert!(
1507 melody_config.notes[1].params.is_empty(),
1508 "D4 should have no per-note params"
1509 );
1510
1511 assert_eq!(melody_config.notes[2].params.len(), 1);
1513 assert!(
1514 (melody_config.notes[2].params["pan"] - (-0.5)).abs() < 0.01,
1515 "E4 should have pan=-0.5"
1516 );
1517 }
1518
1519 #[test]
1520 fn test_melody_with_per_note_params_and_scale_degrees() {
1521 let mut engine = ScriptEngine::new();
1522 let state = engine
1523 .execute(
1524 r#"
1525 set_tempo(120);
1526 define_group("Test", || {
1527 let v = voice("test_voice").synth("test_synth");
1528 melody("scale_params")
1529 .on(v)
1530 .root("C4")
1531 .scale("minor")
1532 .notes("1[vel=100] 3[cutoff=2000] 5 7[vel=50,pan=0.5]")
1533 .apply();
1534 });
1535 "#,
1536 )
1537 .unwrap();
1538
1539 let melody_config = state
1540 .melodies
1541 .values()
1542 .next()
1543 .expect("Should have a melody");
1544
1545 assert_eq!(melody_config.notes.len(), 4, "Should have 4 notes");
1546
1547 let vel_1 = melody_config.notes[0].velocity;
1549 assert!(
1550 (vel_1 - 100.0 / 127.0).abs() < 0.01,
1551 "Degree 1 velocity should be 100/127"
1552 );
1553
1554 assert!(!melody_config.notes[1].params.is_empty());
1556 assert!(
1557 (melody_config.notes[1].params["cutoff"] - 2000.0).abs() < 0.01,
1558 "Degree 3 should have cutoff=2000"
1559 );
1560
1561 assert!(melody_config.notes[2].params.is_empty());
1563
1564 let vel_7 = melody_config.notes[3].velocity;
1566 assert!(
1567 (vel_7 - 50.0 / 127.0).abs() < 0.01,
1568 "Degree 7 velocity should be 50/127"
1569 );
1570 assert!(
1571 (melody_config.notes[3].params["pan"] - 0.5).abs() < 0.01,
1572 "Degree 7 should have pan=0.5"
1573 );
1574 }
1575
1576 #[test]
1577 fn test_melody_with_per_note_params_multi_bar() {
1578 let mut engine = ScriptEngine::new();
1579 let state = engine
1580 .execute(
1581 r#"
1582 set_tempo(120);
1583 define_group("Test", || {
1584 let v = voice("test_voice").synth("test_synth");
1585 melody("multi_bar_params")
1586 .on(v)
1587 .notes("C4[cutoff=2000] D4 E4 F4 | G4[cutoff=500] A4 B4 C5")
1588 .apply();
1589 });
1590 "#,
1591 )
1592 .unwrap();
1593
1594 let melody_config = state
1595 .melodies
1596 .values()
1597 .next()
1598 .expect("Should have a melody");
1599
1600 assert_eq!(melody_config.notes.len(), 8, "Should have 8 notes");
1601
1602 assert!(
1604 (melody_config.notes[0].params["cutoff"] - 2000.0).abs() < 0.01,
1605 "C4 should have cutoff=2000"
1606 );
1607
1608 assert!(
1610 (melody_config.notes[4].params["cutoff"] - 500.0).abs() < 0.01,
1611 "G4 should have cutoff=500"
1612 );
1613
1614 for i in [1, 2, 3, 5, 6, 7] {
1616 assert!(
1617 melody_config.notes[i].params.is_empty(),
1618 "Note at index {} should have no params",
1619 i
1620 );
1621 }
1622 }
1623
1624 #[test]
1625 fn test_melody_without_params_backwards_compatible() {
1626 let mut engine = ScriptEngine::new();
1628 let state = engine
1629 .execute(
1630 r#"
1631 set_tempo(120);
1632 define_group("Test", || {
1633 let v = voice("test_voice").synth("test_synth");
1634 melody("compat_test")
1635 .on(v)
1636 .notes("C4 D4 E4 F4 | G4 A4 B4 C5")
1637 .apply();
1638 });
1639 "#,
1640 )
1641 .unwrap();
1642
1643 let melody_config = state
1644 .melodies
1645 .values()
1646 .next()
1647 .expect("Should have a melody");
1648
1649 assert_eq!(melody_config.notes.len(), 8, "Should have 8 notes");
1650
1651 for (i, note) in melody_config.notes.iter().enumerate() {
1653 assert!(
1654 note.params.is_empty(),
1655 "Note {} should have no params",
1656 i
1657 );
1658 assert!(
1659 (note.velocity - 1.0).abs() < 0.01,
1660 "Note {} should have default velocity 1.0",
1661 i
1662 );
1663 }
1664 }
1665
1666 #[test]
1669 fn test_voice_default_state() {
1670 let mut engine = ScriptEngine::new();
1671 let state = engine
1672 .execute(
1673 r#"
1674 set_tempo(120);
1675 define_group("Test", || {
1676 let v = voice("test_voice").synth("test_synth").apply();
1677 });
1678 "#,
1679 )
1680 .unwrap();
1681
1682 let voice_config = state.voices.values().next().expect("Should have a voice");
1683 assert!(!voice_config.muted, "Voice should not be muted by default");
1684 assert!(
1685 !voice_config.soloed,
1686 "Voice should not be soloed by default"
1687 );
1688 }
1689
1690 #[test]
1691 fn test_group_default_state() {
1692 let mut engine = ScriptEngine::new();
1693 let state = engine
1694 .execute(
1695 r#"
1696 set_tempo(120);
1697 define_group("TestGroup", || {
1698 let v = voice("test_voice").synth("test_synth");
1699 });
1700 "#,
1701 )
1702 .unwrap();
1703
1704 let group = state
1705 .groups
1706 .values()
1707 .find(|g| g.name == "TestGroup")
1708 .expect("Should find TestGroup");
1709
1710 assert!(!group.muted, "Group should not be muted by default");
1711 assert!(!group.soloed, "Group should not be soloed by default");
1712 }
1713
1714 #[test]
1715 fn test_pattern_apply_not_playing() {
1716 let mut engine = ScriptEngine::new();
1717 let state = engine
1718 .execute(
1719 r#"
1720 set_tempo(120);
1721 define_group("Test", || {
1722 let v = voice("test_voice").synth("test_synth");
1723 let p = pattern("test_pattern").on(v).step("x...").apply();
1724 });
1725 "#,
1726 )
1727 .unwrap();
1728
1729 assert_eq!(
1731 state.playing_patterns.len(),
1732 0,
1733 "Pattern should not be playing after apply()"
1734 );
1735 }
1736
1737 #[test]
1738 fn test_melody_apply_not_playing() {
1739 let mut engine = ScriptEngine::new();
1740 let state = engine
1741 .execute(
1742 r#"
1743 set_tempo(120);
1744 define_group("Test", || {
1745 let v = voice("test_voice").synth("test_synth");
1746 let m = melody("test_melody").on(v).notes("C4 D4 E4").apply();
1747 });
1748 "#,
1749 )
1750 .unwrap();
1751
1752 assert_eq!(
1754 state.playing_melodies.len(),
1755 0,
1756 "Melody should not be playing after apply()"
1757 );
1758 }
1759
1760 #[test]
1761 fn test_negative_quantization_clamped() {
1762 let mut engine = ScriptEngine::new();
1763 let state = engine
1764 .execute(
1765 r#"
1766 set_tempo(120);
1767 set_quantization(-5.0); // Negative should be clamped to 0
1768 "#,
1769 )
1770 .unwrap();
1771
1772 assert!(state.quantization >= 0.0, "Quantization should be >= 0");
1773 }
1774
1775 #[test]
1776 fn test_combined_phase1_features() {
1777 let mut engine = ScriptEngine::new();
1778 let state = engine
1779 .execute(
1780 r#"
1781 // Test all Phase 1 features together
1782 set_tempo(130);
1783 set_time_signature(4, 4);
1784 set_quantization(4.0);
1785
1786 // Drums group - muted
1787 define_group("Drums", || {
1788 let kick = voice("kick").synth("kick_909").gain(db(-6));
1789 let hihat = voice("hihat").synth("hihat_909").mute(); // Muted
1790
1791 pattern("kick_main").on(kick).step("x... x...").start();
1792 pattern("hihat_main").on(hihat).step("..x. ..x.").launch();
1793 });
1794 group("Drums").set_param("compressor", 0.6);
1795
1796 // Bass group - soloed
1797 define_group("Bass", || {
1798 let bass = voice("bass").synth("acid_bass").solo(); // Soloed
1799
1800 // Use scale for the bassline
1801 let notes = scale("C", "minor");
1802 melody("bassline").on(bass).notes(notes).start();
1803 });
1804 group("Bass").solo(true);
1805
1806 // Lead group
1807 define_group("Lead", || {
1808 let lead = voice("lead").synth("saw_lead");
1809
1810 // Use add_note for precise control
1811 melody("lead_melody")
1812 .on(lead)
1813 .add_note(0.0, 60, 1.0, 2.0)
1814 .add_note(2.0, 67, 0.8, 1.0)
1815 .launch();
1816 });
1817 "#,
1818 )
1819 .unwrap();
1820
1821 assert_eq!(state.tempo, 130.0);
1823 assert!((state.quantization - 4.0).abs() < 0.001);
1824
1825 assert!(state.groups.len() >= 3);
1827 let bass_group = state.groups.values().find(|g| g.name == "Bass");
1828 assert!(bass_group.is_some());
1829 assert!(bass_group.unwrap().soloed);
1830
1831 assert_eq!(state.voices.len(), 4);
1833
1834 assert_eq!(state.playing_patterns.len(), 2);
1836 assert_eq!(state.playing_melodies.len(), 2);
1837 }
1838
1839 #[cfg(feature = "midi")]
1840 #[test]
1841 fn test_midi_api() {
1842 let mut engine = ScriptEngine::new();
1843 let state = engine
1844 .execute(
1845 r#"
1846 set_tempo(120);
1847
1848 // Get a MIDI device (by index for testing)
1849 let keyboard = midi_device(0);
1850
1851 define_group("Synth", || {
1852 let lead = voice("lead").synth("saw_lead").gain(db(-8));
1853
1854 // Route keyboard to voice
1855 keyboard.route_to(lead);
1856
1857 // Route CC 1 (mod wheel) to filter cutoff
1858 keyboard.route_cc(1, lead, "filter_cutoff", 0.0, 1.0);
1859 });
1860
1861 // Open the device for input
1862 keyboard.open_input();
1863 "#,
1864 )
1865 .unwrap();
1866
1867 assert_eq!(state.tempo, 120.0);
1868 assert_eq!(state.voices.len(), 1, "Should have 1 voice");
1869
1870 assert_eq!(
1872 state.midi_keyboard_routes.len(),
1873 1,
1874 "Should have 1 keyboard route"
1875 );
1876 assert_eq!(state.midi_cc_routes.len(), 1, "Should have 1 CC route");
1877 assert_eq!(state.midi_inputs.len(), 1, "Should have 1 MIDI input");
1878 }
1879
1880 #[cfg(feature = "midi")]
1881 #[test]
1882 fn test_midi_output_voice() {
1883 let mut engine = ScriptEngine::new();
1884 let state = engine
1885 .execute(
1886 r#"
1887 set_tempo(120);
1888
1889 // Get a MIDI device for output
1890 let synth = midi_device(0);
1891
1892 define_group("External", || {
1893 // Create a voice that outputs to MIDI instead of audio
1894 let ext_synth = voice("external_synth")
1895 .on(synth) // Routes to MIDI device
1896 .channel(2) // MIDI channel 3 (0-indexed)
1897 .gain(db(0));
1898
1899 // Create a melody that will be sent as MIDI
1900 melody("ext_melody").on(ext_synth).notes("C3 D3 E3 F3").apply();
1901 });
1902 "#,
1903 )
1904 .unwrap();
1905
1906 assert_eq!(state.tempo, 120.0);
1907 assert_eq!(state.voices.len(), 1, "Should have 1 voice");
1908
1909 let voice_id = state.voices.keys().next().unwrap();
1911 let voice_config = state.voices.get(voice_id).unwrap();
1912 assert!(
1913 voice_config.midi_output.is_some(),
1914 "Voice should have MIDI output"
1915 );
1916 assert_eq!(voice_config.midi_channel, 2, "Voice should be on channel 2");
1917 }
1918
1919 #[test]
1922 fn test_sample_envelope_methods() {
1923 let mut engine = ScriptEngine::new();
1924 let state = engine
1925 .execute(
1926 r#"
1927 set_tempo(120);
1928
1929 // Test sample with envelope settings
1930 let kick = sample("kick", "samples/kick.wav")
1931 .attack(0.01)
1932 .sustain(0.8)
1933 .release(0.2);
1934 "#,
1935 )
1936 .unwrap();
1937
1938 assert_eq!(state.samples.len(), 1, "Should have 1 sample");
1939 let sample_id = state.samples.keys().next().unwrap();
1940 let sample_config = state.samples.get(sample_id).unwrap();
1941 assert!(
1942 (sample_config.attack - 0.01).abs() < 0.001,
1943 "Attack should be 0.01"
1944 );
1945 assert!(
1946 (sample_config.sustain - 0.8).abs() < 0.001,
1947 "Sustain should be 0.8"
1948 );
1949 assert!(
1950 (sample_config.release - 0.2).abs() < 0.001,
1951 "Release should be 0.2"
1952 );
1953 }
1954
1955 #[test]
1956 fn test_sample_playback_methods() {
1957 let mut engine = ScriptEngine::new();
1958 let state = engine
1959 .execute(
1960 r#"
1961 set_tempo(120);
1962
1963 // Test sample with playback settings
1964 let loop_sample = sample("loop", "samples/loop.wav")
1965 .amp(0.7)
1966 .rate(1.5)
1967 .loop_mode(true)
1968 .offset(0.5)
1969 .length(2.0);
1970 "#,
1971 )
1972 .unwrap();
1973
1974 assert_eq!(state.samples.len(), 1, "Should have 1 sample");
1975 let sample_id = state.samples.keys().next().unwrap();
1976 let sample_config = state.samples.get(sample_id).unwrap();
1977 assert!((sample_config.amp - 0.7).abs() < 0.001, "Amp should be 0.7");
1978 assert!(
1979 (sample_config.rate - 1.5).abs() < 0.001,
1980 "Rate should be 1.5"
1981 );
1982 assert!(sample_config.loop_mode, "Loop mode should be true");
1983 assert!(
1984 (sample_config.offset - 0.5).abs() < 0.001,
1985 "Offset should be 0.5"
1986 );
1987 assert_eq!(sample_config.length, Some(2.0), "Length should be 2.0");
1988 }
1989
1990 #[test]
1991 fn test_sample_warp_methods() {
1992 let mut engine = ScriptEngine::new();
1993 let state = engine
1994 .execute(
1995 r#"
1996 set_tempo(120);
1997
1998 // Test sample with warp/time-stretch settings
1999 let warped = sample("warped", "samples/loop.wav")
2000 .warp(true)
2001 .speed(0.5)
2002 .pitch(1.2)
2003 .window_size(0.15)
2004 .overlaps(12.0);
2005 "#,
2006 )
2007 .unwrap();
2008
2009 assert_eq!(state.samples.len(), 1, "Should have 1 sample");
2010 let sample_id = state.samples.keys().next().unwrap();
2011 let sample_config = state.samples.get(sample_id).unwrap();
2012 assert!(sample_config.warp, "Warp should be true");
2013 assert!(
2014 (sample_config.speed - 0.5).abs() < 0.001,
2015 "Speed should be 0.5"
2016 );
2017 assert!(
2018 (sample_config.pitch - 1.2).abs() < 0.001,
2019 "Pitch should be 1.2"
2020 );
2021 assert!(
2022 (sample_config.window_size - 0.15).abs() < 0.001,
2023 "Window size should be 0.15"
2024 );
2025 assert!(
2026 (sample_config.overlaps - 12.0).abs() < 0.001,
2027 "Overlaps should be 12.0"
2028 );
2029 }
2030
2031 #[test]
2032 fn test_sample_semitones() {
2033 let mut engine = ScriptEngine::new();
2034 let state = engine
2035 .execute(
2036 r#"
2037 set_tempo(120);
2038
2039 // Test pitch shift by semitones
2040 let shifted = sample("shifted", "samples/loop.wav")
2041 .semitones(12.0); // One octave up
2042 "#,
2043 )
2044 .unwrap();
2045
2046 assert_eq!(state.samples.len(), 1, "Should have 1 sample");
2047 let sample_id = state.samples.keys().next().unwrap();
2048 let sample_config = state.samples.get(sample_id).unwrap();
2049 assert!(sample_config.warp, "Warp should be auto-enabled");
2050 assert!(
2052 (sample_config.pitch - 2.0).abs() < 0.001,
2053 "Pitch should be 2.0 for +12 semitones"
2054 );
2055 }
2056
2057 #[test]
2058 fn test_sample_warp_to_bpm() {
2059 let mut engine = ScriptEngine::new();
2060 let state = engine
2061 .execute(
2062 r#"
2063 set_tempo(120);
2064
2065 // Test warp to BPM
2066 let tempo_matched = sample("tempo", "samples/loop.wav")
2067 .warp_to_bpm(140.0);
2068 "#,
2069 )
2070 .unwrap();
2071
2072 assert_eq!(state.samples.len(), 1, "Should have 1 sample");
2073 let sample_id = state.samples.keys().next().unwrap();
2074 let sample_config = state.samples.get(sample_id).unwrap();
2075 assert!(sample_config.warp, "Warp should be auto-enabled");
2076 assert_eq!(
2077 sample_config.target_bpm,
2078 Some(140.0),
2079 "Target BPM should be 140"
2080 );
2081 }
2082
2083 #[test]
2084 fn test_sample_slice() {
2085 let mut engine = ScriptEngine::new();
2086 let state = engine
2087 .execute(
2088 r#"
2089 set_tempo(120);
2090
2091 // Test sample slicing
2092 let sliced = sample("sliced", "samples/loop.wav")
2093 .slice(1.0, 3.0); // 2-second slice starting at 1 second
2094 "#,
2095 )
2096 .unwrap();
2097
2098 assert_eq!(state.samples.len(), 1, "Should have 1 sample");
2099 let sample_id = state.samples.keys().next().unwrap();
2100 let sample_config = state.samples.get(sample_id).unwrap();
2101 assert!(
2102 (sample_config.offset - 1.0).abs() < 0.001,
2103 "Offset should be 1.0"
2104 );
2105 assert_eq!(
2106 sample_config.length,
2107 Some(2.0),
2108 "Length should be 2.0 (3.0 - 1.0)"
2109 );
2110 }
2111
2112 #[test]
2113 fn test_sample_chained_methods() {
2114 let mut engine = ScriptEngine::new();
2115 let state = engine
2116 .execute(
2117 r#"
2118 set_tempo(120);
2119
2120 // Test chaining multiple sample methods
2121 let full_sample = sample("full", "samples/loop.wav")
2122 .attack(0.005)
2123 .release(0.1)
2124 .amp(0.9)
2125 .warp(true)
2126 .speed(0.75)
2127 .pitch(1.1)
2128 .loop_mode(true);
2129 "#,
2130 )
2131 .unwrap();
2132
2133 assert_eq!(state.samples.len(), 1, "Should have 1 sample");
2134 let sample_id = state.samples.keys().next().unwrap();
2135 let sample_config = state.samples.get(sample_id).unwrap();
2136 assert!((sample_config.attack - 0.005).abs() < 0.001);
2137 assert!((sample_config.release - 0.1).abs() < 0.001);
2138 assert!((sample_config.amp - 0.9).abs() < 0.001);
2139 assert!(sample_config.warp);
2140 assert!((sample_config.speed - 0.75).abs() < 0.001);
2141 assert!((sample_config.pitch - 1.1).abs() < 0.001);
2142 assert!(sample_config.loop_mode);
2143 }
2144}