Skip to main content

vibelang_rhai/
engine.rs

1//! Script engine - the main entry point for executing VibeLang scripts.
2//!
3//! The [`ScriptEngine`] wraps a Rhai engine with all VibeLang API functions registered.
4
5use 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// ============================================================================
15// In-memory module resolver for WASM
16// ============================================================================
17
18#[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    /// In-memory module resolver for WASM.
26    ///
27    /// This resolver looks up module source code from an in-memory HashMap
28    /// instead of the filesystem.
29    #[derive(Clone)]
30    pub struct InMemoryModuleResolver {
31        /// Map of module path -> source code
32        modules: Arc<HashMap<String, String>>,
33        /// File extension to add (default: "vibe")
34        extension: String,
35    }
36
37    impl InMemoryModuleResolver {
38        /// Create a new in-memory resolver with the given modules.
39        pub fn new(modules: HashMap<String, String>) -> Self {
40            Self {
41                modules: Arc::new(modules),
42                extension: "vibe".to_string(),
43            }
44        }
45
46        /// Normalize a module path for lookup.
47        fn normalize_path(&self, path: &str) -> String {
48            let mut normalized = path.to_string();
49
50            // Remove leading "./" or "/"
51            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            // Remove "stdlib/" prefix if present (we store without it)
58            if normalized.starts_with("stdlib/") {
59                normalized = normalized[7..].to_string();
60            }
61
62            // Add extension if not present
63            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            // Look up the module source
82            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            // Compile and create module
90            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            // Create module from AST
99            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            // Look up the module source
116            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            // Compile the source
127            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
141/// Script engine for executing VibeLang scripts.
142///
143/// The ScriptEngine:
144/// 1. Creates a Rhai engine with all VibeLang API registered
145/// 2. Executes scripts that call the API functions
146/// 3. Collects the resulting ScriptState
147///
148/// # Example
149///
150/// ```ignore
151/// let mut engine = ScriptEngine::new();
152/// let state = engine.execute_file("song.vibe")?;
153/// // Apply state to runtime via reload system
154/// ```
155pub struct ScriptEngine {
156    engine: Engine,
157    #[cfg(not(target_arch = "wasm32"))]
158    import_paths: Vec<PathBuf>,
159}
160
161impl ScriptEngine {
162    /// Create a new script engine with all VibeLang API registered.
163    pub fn new() -> Self {
164        let mut engine = Engine::new();
165
166        // Set appropriate limits for complex scripts
167        engine.set_max_expr_depths(4096, 4096);
168        engine.set_max_call_levels(4096);
169
170        // Override print() to route through the log system
171        engine.on_print(|text| {
172            log::info!("[script] {}", text);
173        });
174
175        // Override debug() similarly
176        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        // Register VibeLang API
187        api::register_api(&mut engine);
188
189        // Register vibelang-dsp API for define_synthdef
190        vibelang_dsp::register_dsp_api(&mut engine);
191
192        // Set up stdlib module resolver for WASM
193        #[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    /// Add import paths for module resolution (native only).
208    #[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    /// Execute a script from a string.
214    ///
215    /// Returns the collected ScriptState that can be applied to a runtime.
216    pub fn execute(&mut self, script: &str) -> Result<ScriptState> {
217        // Clear object registries before each execution
218        api::clear_all_registries();
219
220        // Reset exit code before execution
221        crate::reset_exit_code();
222
223        // Initialize context
224        context::init_context();
225
226        // Execute script
227        let result = self.engine.run(script).map_err(Error::from);
228
229        // Take state regardless of result (to clean up context)
230        let state = context::take_state();
231        context::clear_context();
232
233        // Check if script exited via exit() - this is not an error
234        if crate::get_exit_code().is_some() {
235            // Script requested exit, return state normally
236            return Ok(state);
237        }
238
239        // Return error if script failed for other reasons
240        result?;
241
242        Ok(state)
243    }
244
245    /// Execute a script from a file (native only).
246    ///
247    /// Returns the collected ScriptState that can be applied to a runtime.
248    #[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        // Read script
253        let script = std::fs::read_to_string(path)?;
254
255        // Set up module resolver
256        let base_path = path.parent().unwrap_or(Path::new(".")).to_path_buf();
257        self.setup_module_resolver(base_path);
258
259        // Clear object registries before each execution
260        api::clear_all_registries();
261
262        // Reset exit code before execution
263        crate::reset_exit_code();
264
265        // Initialize context
266        context::init_context();
267        context::set_current_file(Some(path.to_path_buf()));
268        context::set_import_paths(self.import_paths.clone());
269
270        // Execute script
271        let result = self.engine.run(&script).map_err(Error::from);
272
273        // Take state regardless of result
274        let state = context::take_state();
275        context::clear_context();
276
277        // Debug: log melody state after script execution
278        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        // Check if script exited via exit() - this is not an error
299        if crate::get_exit_code().is_some() {
300            // Script requested exit, return state normally
301            return Ok(state);
302        }
303
304        // Return error if script failed for other reasons
305        result?;
306
307        Ok(state)
308    }
309
310    /// Execute an AST (pre-compiled script).
311    pub fn execute_ast(&mut self, ast: &rhai::AST) -> Result<ScriptState> {
312        // Clear object registries before each execution
313        api::clear_all_registries();
314
315        // Initialize context
316        context::init_context();
317
318        // Execute
319        let result = self.engine.run_ast(ast).map_err(Error::from);
320
321        // Take state
322        let state = context::take_state();
323        context::clear_context();
324
325        result?;
326
327        Ok(state)
328    }
329
330    /// Compile a script to AST for repeated execution.
331    pub fn compile(&self, script: &str) -> Result<rhai::AST> {
332        self.engine.compile(script).map_err(Error::from)
333    }
334
335    /// Compile a script file to AST (native only).
336    #[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    /// Set up module resolver for import statements (native only).
345    #[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        // 1. Source-relative resolver (highest priority)
350        let mut source_resolver = rhai::module_resolvers::FileModuleResolver::new();
351        source_resolver.set_extension("vibe");
352        collection.push(source_resolver);
353
354        // 2. Base path resolver
355        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        // 3. Additional import paths
361        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    /// Get a reference to the underlying Rhai engine.
372    pub fn engine(&self) -> &Engine {
373        &self.engine
374    }
375
376    /// Get a mutable reference to the underlying Rhai engine.
377    pub fn engine_mut(&mut self) -> &mut Engine {
378        &mut self.engine
379    }
380
381    /// Register optional extensions with the script engine.
382    ///
383    /// Extensions provide additional capabilities like filesystem access,
384    /// shell command execution, and networking. These are disabled by default
385    /// and must be explicitly enabled.
386    ///
387    /// # Example
388    ///
389    /// ```ignore
390    /// use vibelang_rhai::{ScriptEngine, ExtensionConfig};
391    ///
392    /// let mut engine = ScriptEngine::new();
393    ///
394    /// // Enable specific extensions
395    /// let config = ExtensionConfig::new()
396    ///     .with_filesystem()
397    ///     .with_exec();
398    /// engine.register_extensions(&config);
399    /// ```
400    ///
401    /// # Security
402    ///
403    /// These extensions provide powerful capabilities. Only enable them
404    /// in trusted environments where script authors are trusted.
405    #[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    /// Register all available extensions.
411    ///
412    /// This is a convenience method that enables all compiled-in extensions.
413    /// Use with caution in production environments.
414    #[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        // Check melody has notes
530        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        // Verify all components were created
662        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    // ==================== Phase 1 Tests: Voice Mute/Solo ====================
672
673    #[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    // ==================== Phase 1 Tests: Group Mute/Solo ====================
816
817    #[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        // Find the group (not the main group)
833        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    // ==================== Phase 1 Tests: Pattern/Melody/Sequence is_playing ====================
973
974    #[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        // launch() should start the pattern
1037        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    // ==================== Phase 1 Tests: Quantization ====================
1201
1202    #[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        // Default quantization should be 0 (no quantization)
1232        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        // Test various quantization values
1243        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    // ==================== Phase 1 Tests: Chord and Scale Functions ====================
1266
1267    #[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        // C major chord has 3 notes
1290        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        // C major scale has 7 notes
1319        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        // This test verifies scale_degree can be used in scripts
1330        // We can't easily check the actual note values, but we can ensure it doesn't error
1331        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    // ==================== Phase 1 Tests: Melody Array Input ====================
1346
1347    #[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        // Each note in the chord becomes a separate note event
1400        assert_eq!(
1401            melody_config.notes.len(),
1402            3,
1403            "Melody should have 3 notes (C major chord)"
1404        );
1405    }
1406
1407    // ==================== Per-Note Parameters Tests ====================
1408
1409    #[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        // C4: velocity=100 (MIDI) → ~0.787
1436        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        // D4: vel=50 (MIDI) → ~0.394
1444        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        // E4: no override → default 1.0
1452        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        // F4: vel=0.3 → 0.3
1460        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        // C4: cutoff + resonance
1495        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        // D4: no params
1506        assert!(
1507            melody_config.notes[1].params.is_empty(),
1508            "D4 should have no per-note params"
1509        );
1510
1511        // E4: pan
1512        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        // Degree 1: velocity override
1548        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        // Degree 3: cutoff param
1555        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        // Degree 5: no params
1562        assert!(melody_config.notes[2].params.is_empty());
1563
1564        // Degree 7: velocity + pan
1565        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        // C4 in bar 1: cutoff=2000
1603        assert!(
1604            (melody_config.notes[0].params["cutoff"] - 2000.0).abs() < 0.01,
1605            "C4 should have cutoff=2000"
1606        );
1607
1608        // G4 in bar 2: cutoff=500
1609        assert!(
1610            (melody_config.notes[4].params["cutoff"] - 500.0).abs() < 0.01,
1611            "G4 should have cutoff=500"
1612        );
1613
1614        // Other notes: no params
1615        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        // Verify that normal melodies (no per-note params) still work exactly the same
1627        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        // All notes should have empty params and default velocity
1652        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    // ==================== Phase 1 Tests: Edge Cases and Error Handling ====================
1667
1668    #[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        // apply() should NOT start the pattern
1730        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        // apply() should NOT start the melody
1753        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        // Verify everything was set up correctly
1822        assert_eq!(state.tempo, 130.0);
1823        assert!((state.quantization - 4.0).abs() < 0.001);
1824
1825        // Check groups
1826        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        // Check voices
1832        assert_eq!(state.voices.len(), 4);
1833
1834        // Check patterns and melodies are playing
1835        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        // Check MIDI routing was created
1871        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        // Check the voice has MIDI output configured
1910        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    // === Phase 3: Sample API tests ===
1920
1921    #[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        // 12 semitones = 2^(12/12) = 2.0 (octave up)
2051        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}