229 lines
8 KiB
Rust
Executable file
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(())
|
|
}
|