| 1 | //! `analyze_audio`: format, levels, loudness, and tempo/key estimates for a local audio file. |
| 2 | |
| 3 | use anyhow::{Result, bail}; |
| 4 | use serde::Deserialize; |
| 5 | use serde_json::{Value, json}; |
| 6 | |
| 7 | use super::{ToolCtx, ToolOutcome, absolutize, function_def}; |
| 8 | use crate::audio::analysis; |
| 9 | |
| 10 | pub const NAME: &str = "analyze_audio"; |
| 11 | const MAX_FILE_BYTES: u64 = 1 << 30; |
| 12 | |
| 13 | pub fn definition() -> Value { |
| 14 | function_def( |
| 15 | NAME, |
| 16 | "Analyze a local audio file (WAV, FLAC, MP3, OGG, M4A/AAC, AIFF): duration, sample rate, channels, peak/RMS, integrated loudness (LUFS), and estimated tempo (with half/double-time readings) and key. Tempo and key are estimates; say so.", |
| 17 | json!({ |
| 18 | "type": "object", |
| 19 | "properties": { |
| 20 | "path": { "type": "string", "description": "Audio file path; relative paths resolve against the working directory" }, |
| 21 | "root": { "type": "string", "description": "Workspace root to resolve a relative path against" } |
| 22 | }, |
| 23 | "required": ["path"] |
| 24 | }), |
| 25 | ) |
| 26 | } |
| 27 | |
| 28 | #[derive(Deserialize)] |
| 29 | struct Args { |
| 30 | path: String, |
| 31 | root: Option<String>, |
| 32 | } |
| 33 | |
| 34 | pub async fn execute(ctx: &ToolCtx, args: Value) -> Result<ToolOutcome> { |
| 35 | let a: Args = serde_json::from_value(args)?; |
| 36 | let path = absolutize(&ctx.resolve_with(a.root.as_deref(), &a.path)?); |
| 37 | let meta = tokio::fs::metadata(&path).await; |
| 38 | match meta { |
| 39 | Ok(m) if m.is_file() && m.len() <= MAX_FILE_BYTES => {} |
| 40 | Ok(m) if m.is_file() => bail!("{} is larger than 1 GiB", path.display()), |
| 41 | Ok(_) => bail!("{} is not a file", path.display()), |
| 42 | Err(e) => bail!("cannot read {}: {e}", path.display()), |
| 43 | } |
| 44 | let work = { |
| 45 | let path = path.clone(); |
| 46 | tokio::task::spawn_blocking(move || analysis::analyze_and_report(&path)) |
| 47 | }; |
| 48 | let text = tokio::select! { |
| 49 | r = work => r??, |
| 50 | () = ctx.cancel.cancelled() => return Ok(ToolOutcome::err("Analysis cancelled.")), |
| 51 | }; |
| 52 | let mut out = ToolOutcome::ok(text); |
| 53 | out.locations.push(path); |
| 54 | Ok(out) |
| 55 | } |