//! Ask the REAL decoder what it can open. //! //! The app carries several hand-maintained lists of "audio extensions", and a //! list is a claim, not a capability — the only thing that settles whether a //! file plays is building the same decoder the player builds. //! //! cargo run -p player --example probe_formats -- [more...] //! //! A folder is walked and one file per extension is probed, which is what makes //! this cheap to point at a whole library. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; fn main() { let args: Vec = std::env::args().skip(1).collect(); if args.is_empty() { eprintln!("usage: probe_formats ..."); std::process::exit(2); } // One representative per extension: the question is "can this FORMAT be // decoded", and a thousand MP3s answer it no better than one. let mut sample: BTreeMap = BTreeMap::new(); let mut counts: BTreeMap = BTreeMap::new(); for a in &args { collect(Path::new(a), &mut sample, &mut counts, 0); } println!("{:<8} {:>7} {:<9} {}", "EXT", "FILES", "VERDICT", "DETAIL"); for (ext, path) in &sample { let (verdict, detail) = probe(path); println!( "{:<8} {:>7} {:<9} {}", ext, counts.get(ext).copied().unwrap_or(0), verdict, detail ); } } fn collect( p: &Path, sample: &mut BTreeMap, counts: &mut BTreeMap, depth: u8, ) { if depth > 8 { return; } if p.is_dir() { let Ok(entries) = std::fs::read_dir(p) else { return }; for e in entries.flatten() { collect(&e.path(), sample, counts, depth + 1); } return; } let Some(ext) = p.extension().and_then(|e| e.to_str()) else { return }; let ext = ext.to_ascii_lowercase(); // Anything that is plausibly audio, INCLUDING formats no list mentions — // the point is to discover the gap, not to confirm the lists. const PLAUSIBLE: &[&str] = &[ "flac", "mp3", "mp2", "mp1", "m4a", "m4b", "mp4", "aac", "alac", "ogg", "oga", "opus", "wav", "wave", "aiff", "aif", "aifc", "caf", "mka", "webm", "wma", "ape", "wv", "mpc", "tta", "shn", "dsf", "dff", "ra", "au", "amr", "3gp", "spx", ]; if !PLAUSIBLE.contains(&ext.as_str()) { return; } *counts.entry(ext.clone()).or_insert(0) += 1; sample.entry(ext).or_insert_with(|| p.to_path_buf()); } /// Build exactly what `player` builds for a local file. fn probe(path: &Path) -> (&'static str, String) { use rodio::Decoder; let file = match std::fs::File::open(path) { Ok(f) => f, Err(e) => return ("ERROR", format!("open failed: {e}")), }; let len = file.metadata().map(|m| m.len()).unwrap_or(0); let reader = std::io::BufReader::new(file); let mut builder = Decoder::builder().with_data(reader).with_seekable(true); if len > 0 { builder = builder.with_byte_len(len); } // What did symphonia actually RECOGNISE? A verdict of "plays" is worth // little if the container was mistaken for something else, so name the // format and codec rather than inferring capability from the extension. let detected = detect(path).unwrap_or_else(|| "?".into()); match builder.build() { Ok(d) => { // Opening is not playing. A container can parse while its codec is // missing, and a truncated decode looks like success if you only // pull a few thousand samples — so decode the WHOLE thing and check // the duration against the file. A 30 MB lossless track that yields // two seconds of audio has not been decoded, it has been guessed at. use rodio::Source as _; let rate = d.sample_rate(); let ch = d.channels(); let mut src = d; // Amplitude tells apart "decoded" from "mis-identified". Compressed // bytes read as raw PCM are full-scale noise: near-100% of samples // land loud, where real music spends most of its time quiet. let (mut n, mut sumsq, mut hot, mut bad) = (0u64, 0f64, 0u64, 0u64); for s in src.by_ref() { let v = s as f64; // A non-finite sample is proof on its own: no decoder emits // NaN from real audio. It comes from compressed bytes being // reinterpreted as floats, and it poisons every mixer and // normaliser stage downstream of here. if !v.is_finite() { bad += 1; n += 1; continue; } sumsq += v * v; if v.abs() > 0.5 { hot += 1; } n += 1; } let rms = if n > 0 { (sumsq / n as f64).sqrt() } else { 0.0 }; let hot_pct = if n > 0 { hot as f64 * 100.0 / n as f64 } else { 0.0 }; let frames = n as f64 / (ch.max(1) as f64); let secs = frames / rate.max(1) as f64; let mb = len as f64 / 1_048_576.0; if secs < 1.0 { return ("NO AUDIO", format!("{rate} Hz, {ch}ch — {secs:.2}s from {mb:.1} MB")); } // Even the densest lossless runs well under 10 MB per second. if mb / secs > 10.0 { return ( "PARTIAL", format!("{rate} Hz, {ch}ch — only {secs:.1}s from {mb:.1} MB"), ); } // Half the samples above half scale is not music. This is the // WavPack case: a RIFF header inside the wrapper makes symphonia // read the compressed body as PCM, so it "succeeds" into noise — // which is worse than refusing, because nothing reports an error. if bad > 0 { return ( "GARBAGE", format!( "{detected} mis-identified — {} of {n} samples are NaN/Inf", bad ), ); } if hot_pct > 40.0 && rms > 0.35 { return ( "NOISE", format!("{detected} mis-identified — rms {rms:.2}, {hot_pct:.0}% of samples above half scale"), ); } ( "PLAYS", format!("{detected} {rate} Hz, {ch}ch, {secs:.0}s, rms {rms:.2}"), ) } Err(e) => ("CANNOT", format!("{detected} {e}")), } } /// The codec symphonia identifies, straight from the probe — the ground truth /// behind a "plays" verdict. fn detect(path: &Path) -> Option { use symphonia::core::io::MediaSourceStream; use symphonia::core::probe::Hint; let file = std::fs::File::open(path).ok()?; let mss = MediaSourceStream::new(Box::new(file), Default::default()); let mut hint = Hint::new(); if let Some(ext) = path.extension().and_then(|e| e.to_str()) { hint.with_extension(ext); } let probed = symphonia::default::get_probe() .format(&hint, mss, &Default::default(), &Default::default()) .ok()?; let track = probed.format.default_track()?; let codec = track.codec_params.codec; let name = symphonia::default::get_codecs() .get_codec(codec) .map(|d| d.short_name.to_string()) .unwrap_or_else(|| format!("codec:{codec:?}")); Some(name) }