//! AVA — Advanced Visualization Agent · native scaffold (Rust + wgpu). //! //! This binary wires the modules together (the same engine stages as //! `src/lib/ava/engine.ts`): //! //! ```text //! cpal capture thread → ring buffer → band analysis (spectrum.rs) //! → beat tracker (beat.rs) → script engine (script/, driven by engine.rs) //! → wgpu renderer (render/) → screen //! ``` //! //! main.rs stays orchestration-only: CLI, window, wgpu bootstrap, event loop. //! The event loop uses winit 0.30's `ApplicationHandler` trait with //! continuous rendering (`about_to_wait` → `request_redraw` → //! `RedrawRequested` → `Engine::tick`). mod audio; mod beat; mod engine; mod render; mod script; mod spectrum; use std::path::PathBuf; use anyhow::{bail, Context, Result}; use winit::application::ApplicationHandler; use winit::event::{ElementState, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::keyboard::{KeyCode, PhysicalKey}; use winit::window::{Window, WindowId}; use engine::Engine; use render::Renderer; // ------------------------------------------------------------ CLI parsing struct Options { /// Path to a .ava preset script (optional). preset: Option, /// Input device index (`--input 2`); None = system default. input: Option, } fn print_usage() { println!("AVA — Advanced Visualization Agent (native scaffold)"); println!(); println!("USAGE:"); println!(" ava-agent [--preset ] [--input ]"); println!(); println!("OPTIONS:"); println!(" --preset load a .ava visualization script"); println!(" --input audio input device (default: system default)"); println!(" --list-devices list audio input devices and exit"); println!(); println!("CONTROLS:"); println!(" Space — beat tap tempo (the planned --bpm-tap key)"); println!(" Esc — quit"); } fn parse_args() -> Result { let mut opts = Options { preset: None, input: None }; let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { "-h" | "--help" => { print_usage(); std::process::exit(0); } "--list-devices" => { println!("audio input devices:\n{}", audio::list_default_host_input_devices()); std::process::exit(0); } "--preset" => { let v = args.next().context("--preset expects a path to a .ava script")?; opts.preset = Some(PathBuf::from(v)); } "--input" => { let v = args.next().context("--input expects ")?; if v != "default" { opts.input = Some(v.parse().with_context(|| format!("bad device index '{v}'"))?); } } other => bail!("unknown argument '{other}' — see --help"), } } Ok(opts) } // ------------------------------------------------------------------- app /// Window + renderer + engine state. The native twin of the web app's /// `AvaEngine` singleton attached to its canvas. struct App { window: Option, renderer: Option, engine: Engine, } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.window.is_some() { return; // already initialized (resumed can re-fire on some platforms) } let attrs = Window::default_attributes() .with_title("AVA — Advanced Visualization Agent") .with_inner_size(winit::dpi::PhysicalSize::new(1280u32, 720u32)); let window = match event_loop.create_window(attrs) { Ok(w) => w, Err(err) => { eprintln!("[ava] window creation failed: {err}"); event_loop.exit(); return; } }; let size = window.inner_size(); let mut renderer = match pollster::block_on(Renderer::init(&window, size.width, size.height)) { Ok(r) => r, Err(err) => { eprintln!("[ava] renderer init failed: {err:#}"); event_loop.exit(); return; } }; if let Some(s) = &self.engine.script { renderer.set_mode(s.meta.mode); } self.renderer = Some(renderer); self.window = Some(window); } fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::Resized(size) => { if let Some(r) = &mut self.renderer { r.resize(size.width, size.height); } } WindowEvent::KeyboardInput { event, .. } => { if event.state == ElementState::Pressed { if let PhysicalKey::Code(code) = event.physical_key { match code { // "Set Beat" tap tempo (the planned --bpm-tap key) KeyCode::Space => self.engine.tap(), KeyCode::Escape => event_loop.exit(), _ => {} } } } } WindowEvent::RedrawRequested => { let renderer = self.renderer.as_mut(); self.engine.tick(renderer); } _ => {} } } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { // continuous render loop: keep the redraw requests flowing if let Some(window) = &self.window { window.request_redraw(); } } } fn main() -> Result<()> { let opts = parse_args()?; // ---- preset script (optional) ------------------------------------------ let script = match &opts.preset { Some(path) => { let text = std::fs::read_to_string(path) .with_context(|| format!("failed to read preset {}", path.display()))?; match script::parse_script(&text) { Ok(s) => { println!( "[ava] preset '{}' by {} — mode {:?}, blend {:?}, {} point statement(s)", s.meta.name, s.meta.author, s.meta.mode, s.meta.blend, s.point_prog.as_ref().map(|p| p.statement_count()).unwrap_or(0) ); Some(s) } Err(err) => { eprintln!("[ava] preset error: {err}"); bail!("failed to parse {}", path.display()); } } } None => { println!( "[ava] no --preset given — rendering the background only \ (try --preset presets/starter.ava)" ); None } }; // ---- audio input --------------------------------------------------------- let audio = match audio::AudioProcessor::start(opts.input) { Ok(a) => Some(a), Err(err) => { eprintln!("[ava] audio capture failed: {err:#}"); eprintln!( "[ava] available input devices:\n{}", audio::list_default_host_input_devices() ); return Err(err.context("no usable audio input — pick one with --input ")); } }; println!("[ava] starting window (Space = tap tempo, Esc = quit)"); let event_loop = EventLoop::new().context("failed to create the winit event loop")?; let mut app = App { window: None, renderer: None, engine: Engine::new(script, audio), }; event_loop .run_app(&mut app) .map_err(|err| anyhow::anyhow!("event loop terminated with error: {err}"))?; Ok(()) }