//! `cargo run -p benilla-app --example stream_probe -- […]` — does a streamed track //! decode as many frames as it declares? //! //! The question behind the zone-music dropouts. A streaming sound's transport runs to the frame //! count the container *declares* (`codec_params.n_frames`), while the audio it can actually play //! is whatever the decoder yields. When the declaration overstates the content, the stream spends //! the difference audible with nothing to play: `third_party/kira`'s decode scheduler asks //! `frame_at_index` for a frame past the real end, which decodes chunk after chunk looking for an //! index that will never arrive. The renderer's frame ring drains, the position stops advancing, //! and — because no error was raised and the transport never reached its end — the sound stays //! `Playing`, so benilla's reap-and-schedule-the-next-track pass never fires //! (`sound/zone.rs`'s music transport). That is a silent music slot that only a zone change clears. //! //! Reads every copy through the chain, so it answers for the bytes the client actually plays //! ([`benilla_formats::Chain`]), not a copy on disk. With no argument it probes the tracks named //! on stdin, one chain path per line. //! //! Prints, per track: the declared frame count, the frames actually decoded, and the number of //! chunks that came back empty on the way. `declared > decoded` is the defect; the empty-chunk //! tally says whether the decoder stalls before it reports EOF. use std::io::BufRead; use symphonia::core::codecs::Decoder; use symphonia::core::formats::FormatReader; use symphonia::core::io::MediaSourceStream; use symphonia::core::probe::Hint; /// The decoder's own frame count for one buffer, matching kira's `load_frames_from_buffer_ref`. fn frames_in(buf: &symphonia::core::audio::AudioBufferRef) -> usize { use symphonia::core::audio::Signal; match buf { symphonia::core::audio::AudioBufferRef::U8(b) => b.frames(), symphonia::core::audio::AudioBufferRef::U16(b) => b.frames(), symphonia::core::audio::AudioBufferRef::U24(b) => b.frames(), symphonia::core::audio::AudioBufferRef::U32(b) => b.frames(), symphonia::core::audio::AudioBufferRef::S8(b) => b.frames(), symphonia::core::audio::AudioBufferRef::S16(b) => b.frames(), symphonia::core::audio::AudioBufferRef::S24(b) => b.frames(), symphonia::core::audio::AudioBufferRef::S32(b) => b.frames(), symphonia::core::audio::AudioBufferRef::F32(b) => b.frames(), symphonia::core::audio::AudioBufferRef::F64(b) => b.frames(), } } fn main() { let Some(data) = benilla_formats::wow_data() else { eprintln!("no install"); std::process::exit(1); }; let chain = benilla_formats::Chain::open(&data).expect("chain opens"); let args: Vec = std::env::args().skip(1).collect(); let names: Vec = if args.is_empty() { std::io::stdin() .lock() .lines() .map_while(Result::ok) .map(|l| l.trim().to_string()) .filter(|l| !l.is_empty()) .collect() } else { args }; println!( "{:<58} {:>10} {:>10} {:>9} {:>6} verdict", "track", "declared", "decoded", "short by", "empty" ); let mut bad = 0usize; for name in &names { let path = name.replace('/', "\\"); let Ok(bytes) = chain.read(&path) else { println!("{:<58} not in the chain", short(&path)); continue; }; match probe(bytes) { Err(e) => println!("{:<58} {e}", short(&path)), Ok((declared, decoded, empty)) => { let verdict = if declared > decoded { bad += 1; "DECLARES MORE THAN IT HAS" } else { "ok" }; println!( "{:<58} {declared:>10} {decoded:>10} {:>9} {empty:>6} {verdict}", short(&path), declared.saturating_sub(decoded), ); } } } println!( "\n{bad} of {} track(s) declare more frames than they decode", names.len() ); } fn short(p: &str) -> String { p.rsplit('\\').next().unwrap_or(p).to_string() } /// `(declared, decoded, empty_chunks)` for one track, decoded exactly as kira's decoder does. fn probe(bytes: Vec) -> Result<(usize, usize, usize), String> { let source = std::io::Cursor::new(bytes); let mss = MediaSourceStream::new(Box::new(source), Default::default()); let probed = symphonia::default::get_probe() .format( &Hint::default(), mss, &Default::default(), &Default::default(), ) .map_err(|e| format!("probe: {e}"))?; let mut format_reader: Box = probed.format; let track = format_reader.default_track().ok_or("no default track")?; let declared: usize = track .codec_params .n_frames .ok_or("no declared frame count")? .try_into() .map_err(|_| "frame count does not fit usize")?; let track_id = track.id; let mut decoder: Box = symphonia::default::get_codecs() .make(&track.codec_params, &Default::default()) .map_err(|e| format!("codec: {e}"))?; let mut decoded = 0usize; let mut empty = 0usize; // The read ends where kira's decoder would report an error, which is what stops the sound. while let Ok(packet) = format_reader.next_packet() { if packet.track_id() != track_id { continue; } match decoder.decode(&packet) { Ok(buf) => { let n = frames_in(&buf); if n == 0 { empty += 1; } decoded += n; } Err(_) => empty += 1, } } Ok((declared, decoded, empty)) }