# doux-sova Integration layer connecting Doux audio engine to Sova's AudioEngineProxy. ## Quick Start with DouxManager ```rust use doux_sova::{DouxManager, DouxConfig, audio}; // 1. List available devices (for UI) let devices = audio::list_output_devices(); for dev in &devices { let default_marker = if dev.is_default { " [default]" } else { "" }; println!("{}: {} (max {} ch){}", dev.index, dev.name, dev.max_channels, default_marker); } // 2. Create configuration let config = DouxConfig::default() .with_output_device("Built-in Output") .with_channels(2) .with_buffer_size(512) .with_sample_path("/path/to/samples"); // 3. Create and start manager let mut manager = DouxManager::new(config)?; let proxy = manager.start(clock.micros())?; // 4. Register with Sova device_map.connect_audio_engine("doux", proxy)?; // 5. Control during runtime manager.hush(); // Release all voices manager.panic(); // Immediately stop all voices manager.add_sample_path(path); // Add more samples manager.rescan_samples(); // Rescan all sample directories let state = manager.state(); // Get AudioEngineState (voices, cpu, etc) let scope = manager.scope_capture(); // Get oscilloscope buffer // 6. Stop or restart manager.stop(); // Stop audio streams let new_config = DouxConfig::default().with_channels(4); let new_proxy = manager.restart(new_config, clock.micros())?; ``` ## Low-Level API For direct engine access without lifecycle management: ```rust use std::sync::{Arc, Mutex}; use doux::Engine; use doux_sova::create_integration; // 1. Create the doux engine let engine = Arc::new(Mutex::new(Engine::new(44100.0))); // 2. Get initial sync time from Sova's clock let initial_time = clock.micros(); // 3. Create integration - returns thread handle and proxy let (handle, proxy) = create_integration(engine.clone(), initial_time); // 4. Register proxy with Sova's device map device_map.connect_audio_engine("doux", proxy)?; // 5. Start your audio output loop using the engine // (see doux/src/main.rs for cpal example) ``` ## Architecture ``` Sova Scheduler │ ▼ AudioEngineProxy::send(AudioEnginePayload) │ ▼ (crossbeam channel) │ SovaReceiver thread │ converts HashMap → command string ▼ Engine::evaluate("/s/sine/freq/440/gain/0.5/...") ``` ## Time Synchronization Pass `clock.micros()` at engine startup. Sova's timetags (microseconds) are converted to engine time (seconds) relative to this initial value. Events with timetags go to Doux's scheduler for sample-accurate playback. ## DouxConfig Methods | Method | Description | |--------|-------------| | `with_output_device(name)` | Set output device by name | | `with_input_device(name)` | Set input device by name | | `with_channels(n)` | Set number of output channels | | `with_buffer_size(n)` | Set audio buffer size in frames | | `with_sample_path(path)` | Add a single sample directory | | `with_sample_paths(paths)` | Add multiple sample directories | ## DouxManager Methods | Method | Description | |--------|-------------| | `start(time)` | Start audio streams, returns proxy | | `stop()` | Stop audio streams | | `restart(config, time)` | Restart with new configuration | | `hush()` | Release all voices gracefully | | `panic()` | Immediately stop all voices | | `add_sample_path(path)` | Add sample directory | | `rescan_samples()` | Rescan all sample directories | | `clear_samples()` | Clear sample pool | | `state()` | Returns `AudioEngineState` | | `is_running()` | Check if streams are active | | `sample_rate()` | Get current sample rate | | `channels()` | Get number of channels | | `config()` | Get current configuration | | `engine_handle()` | Access engine for telemetry | | `scope_capture()` | Get oscilloscope buffer | ## Re-exported Types ### AudioEngineState ```rust pub struct AudioEngineState { pub active_voices: u32, pub cpu_percent: f32, pub output_level: f32, } ``` ### DouxError Error type for manager operations (device not found, stream errors, etc). ### ScopeCapture Buffer containing oscilloscope data from `scope_capture()`. ## Parameters All Sova Dirt parameters map directly to Doux event fields via `Event::parse()`. No manual mapping required - add new parameters to Doux and they work automatically.