openvisengine/rust-agent/src/main.rs
2026-10-22 03:29:51 +00:00

229 lines
8 KiB
Rust
Executable file

//! 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<PathBuf>,
/// Input device index (`--input 2`); None = system default.
input: Option<usize>,
}
fn print_usage() {
println!("AVA — Advanced Visualization Agent (native scaffold)");
println!();
println!("USAGE:");
println!(" ava-agent [--preset <path.ava>] [--input <device-index|default>]");
println!();
println!("OPTIONS:");
println!(" --preset <path> load a .ava visualization script");
println!(" --input <index> 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<Options> {
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 <device-index|default>")?;
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<Window>,
renderer: Option<Renderer>,
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 <index>"));
}
};
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(())
}