use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] #[command(name = "soundgen")] #[command(version = "0.1.0")] #[command(about = "8-bit sound synthesizer — generate SFX/UI/ambient sounds from JSON presets")] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Generate a sound from a named preset. Gen { /// Preset name (e.g., "jump", "explosion", "click"). preset: String, /// Output WAV path. #[arg(short, long)] out: PathBuf, /// Override parameters (e.g., --param volume=0.9). #[arg(short, long)] param: Vec, /// Presets directory (defaults to bundled presets/). #[arg(long, default_value = "presets")] presets_dir: PathBuf, }, /// Render a sound from a JSON spec file. Render { /// Input JSON spec file. spec: PathBuf, /// Output WAV path. #[arg(short, long)] out: PathBuf, }, /// Render a song from a JSON song file (sequencer). RenderSong { /// Input JSON song file. song: PathBuf, /// Output WAV path. #[arg(short, long)] out: PathBuf, }, /// List available presets. ListPresets { /// Filter by category: sfx, ui, or ambient. #[arg(short, long)] category: Option, /// Presets directory. #[arg(long, default_value = "presets")] presets_dir: PathBuf, }, } fn main() { let cli = Cli::parse(); if let Err(e) = run(cli) { eprintln!("error: {}", e); std::process::exit(1); } } fn run(cli: Cli) -> Result<(), String> { match cli.command { Commands::Gen { preset, out, param, presets_dir, } => cmd_gen(&preset, &out, ¶m, &presets_dir), Commands::Render { spec, out } => cmd_render(&spec, &out), Commands::RenderSong { song, out } => cmd_render_song(&song, &out), Commands::ListPresets { category, presets_dir, } => cmd_list_presets(category.as_deref(), &presets_dir), } } fn cmd_gen( preset_name: &str, out: &std::path::Path, params: &[String], presets_dir: &std::path::Path, ) -> Result<(), String> { let registry = soundgen_fmt::PresetRegistry::load_dir(presets_dir) .map_err(|e| format!("loading presets from {}: {}", presets_dir.display(), e))?; let entry = registry.get(preset_name).ok_or_else(|| { let available = registry.names().join(", "); format!( "preset '{}' not found. Available: {}", preset_name, available ) })?; let mut spec = entry.spec.clone(); // Apply parameter overrides for p in params { let (key, value) = p .split_once('=') .ok_or_else(|| format!("invalid --param format: '{}' (expected key=value)", p))?; apply_param(&mut spec, key, value)?; } render_and_write(&spec, out) } fn cmd_render(spec_path: &std::path::Path, out: &std::path::Path) -> Result<(), String> { let content = std::fs::read_to_string(spec_path) .map_err(|e| format!("reading spec {}: {}", spec_path.display(), e))?; let spec: soundgen_fmt::SoundSpec = serde_json::from_str(&content).map_err(|e| format!("parsing spec: {}", e))?; render_and_write(&spec, out) } fn cmd_render_song(song_path: &std::path::Path, out: &std::path::Path) -> Result<(), String> { let content = std::fs::read_to_string(song_path) .map_err(|e| format!("reading song {}: {}", song_path.display(), e))?; let song: soundgen_seq::Song = serde_json::from_str(&content).map_err(|e| format!("parsing song: {}", e))?; let samples = soundgen_seq::render_song(&song); soundgen_io::write_wav(out, &samples, song.sample_rate)?; eprintln!( "Wrote {} ({} samples, {:.1}s, {} Hz)", out.display(), samples.len() / 2, song.duration(), song.sample_rate ); Ok(()) } fn cmd_list_presets(category: Option<&str>, presets_dir: &std::path::Path) -> Result<(), String> { let registry = soundgen_fmt::PresetRegistry::load_dir(presets_dir) .map_err(|e| format!("loading presets: {}", e))?; let cat = category.and_then(soundgen_fmt::PresetCategory::from_str); let presets = registry.list(cat); if presets.is_empty() { println!("No presets found in {}", presets_dir.display()); return Ok(()); } println!( "{:<20} {:<10} {:<10} {}", "NAME", "CATEGORY", "DURATION", "CHANNELS" ); println!("{}", "-".repeat(60)); for p in &presets { println!( "{:<20} {:<10} {:<10.2} {}", p.name, p.category.as_str(), p.spec.duration, p.spec.channels.len() ); } Ok(()) } fn render_and_write(spec: &soundgen_fmt::SoundSpec, out: &std::path::Path) -> Result<(), String> { let samples = soundgen_fmt::render_spec(spec); soundgen_io::write_wav(out, &samples, spec.sample_rate)?; eprintln!( "Wrote {} ({} samples, {:.2}s, {} Hz)", out.display(), samples.len() / 2, spec.duration, spec.sample_rate ); Ok(()) } fn apply_param(spec: &mut soundgen_fmt::SoundSpec, key: &str, value: &str) -> Result<(), String> { let v: f32 = value .parse() .map_err(|e| format!("invalid param value '{}': {}", value, e))?; match key { "duration" => spec.duration = v, "volume" => { for ch in &mut spec.channels { match ch { soundgen_fmt::ChannelSpec::Pulse { volume, .. } => *volume = v, soundgen_fmt::ChannelSpec::Triangle { volume, .. } => *volume = v, soundgen_fmt::ChannelSpec::Noise { volume, .. } => *volume = v, } } } _ => { return Err(format!( "unknown param '{}' (supported: duration, volume)", key )) } } Ok(()) }