use std::fs; use std::io::{Read, Seek, SeekFrom}; use std::process::Command; use sylpheed_formats::{hash::name_hash, movie_manifest, movie_voice, slb, PakArchive}; fn rg(disc: &str, g: u64, n: usize) -> Vec { let mut segs = vec![]; let mut cum = 0u64; for i in 0..5 { let p = format!("{disc}/dat/sound.p{i:02}"); if let Ok(m) = fs::metadata(&p) { segs.push((cum, m.len(), p)); cum += m.len(); } } let mut out = vec![]; let (mut need, mut pos) = (n, g); for (base, len, path) in &segs { if need == 0 || pos >= base + len || pos < *base { continue; } let local = pos - base; let take = need.min((len - local) as usize); let mut f = fs::File::open(path).unwrap(); f.seek(SeekFrom::Start(local)).unwrap(); let mut b = vec![0u8; take]; f.read_exact(&mut b).unwrap(); out.extend_from_slice(&b); need -= take; pos += take as u64; } out } fn contains(h: &[u8], n: &[u8]) -> bool { h.windows(n.len()).any(|w| w == n) } fn dur(w: &str) -> String { let o = Command::new("ffprobe") .args([ "-v", "error", "-show_entries", "format=duration", "-of", "default=nw=1:nk=1", w, ]) .output() .unwrap(); String::from_utf8_lossy(&o.stdout).trim().to_string() } fn hokyu_fallback(m: &str) -> Option { if !m.starts_with("hokyu_") { return None; } let ls = m.contains("_LS_"); let ds = m.contains("_DS_"); let a = m.ends_with('A'); let h = m.ends_with('H'); Some( match (ls, ds, a, h) { (true, _, true, _) => "VOICE_D_450", (true, _, _, true) => "VOICE_D_453", (_, true, true, _) => "VOICE_D_452", (_, true, _, true) => "VOICE_D_454", _ => return None, } .into(), ) } fn main() { let disc = std::env::var("SYLPHEED_DISC").unwrap(); let tpak = PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); let manifest = tpak .entries() .iter() .find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b))) .unwrap(); let code = "eng"; let registry = tpak .entries() .iter() .find_map(|e| { tpak.read(e) .ok() .filter(|b| contains(b, format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes())) }) .unwrap(); let ids = movie_voice::registry_voice_ids(®istry); let stoc = fs::read(format!("{disc}/dat/sound.pak")).unwrap(); let entries = PakArchive::parse_toc(&stoc).unwrap(); let hokyu: Vec = movie_manifest::parse(&manifest) .into_iter() .filter(|m| m.movie.starts_with("hokyu_")) .map(|m| m.movie) .collect(); for movie in &hokyu { let bound = movie_manifest::voice_token(&manifest, movie); let token = bound.clone().or_else(|| hokyu_fallback(movie)); let Some(token) = token else { println!("{movie:16} NO TOKEN"); continue; }; let src = if bound.is_some() { "bound" } else { "fallback" }; let Some(&id) = ids.get(&token) else { println!("{movie:16} {token} not in registry"); continue; }; let Some(anchor) = ["Movie", "etc", "Voice"].iter().find_map(|d| { let h = name_hash(&format!("{code}\\{d}\\{token}.slb")); entries .binary_search_by_key(&h, |e| e.name_hash) .ok() .map(|i| entries[i].offset as u64) }) else { println!("{movie:16} no anchor"); continue; }; let win_start = (anchor.saturating_sub(2 * 1024 * 1024)) & !3; let window = rg(&disc, win_start, 8 * 1024 * 1024); let Some(el) = movie_voice::find_descriptor(&window, id) else { println!("{movie:16} desc not found"); continue; }; let end = win_start + el as u64; let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1)) .or_else(|| movie_voice::find_descriptor_before(&window, el)) .map(|o| win_start + o as u64) .filter(|&s| s < end && end - s < 1_500_000) .unwrap_or(anchor); let region = rg(&disc, start, (end - start) as usize); let mut riffs = slb::to_xma_riffs(®ion); if riffs.is_empty() { riffs = slb::to_xma_riff_best(®ion).into_iter().collect(); } let mut d = "?".into(); if let Some(r) = riffs.first() { let xp = format!("/tmp/hf_{movie}.xma.wav"); let wp = format!("/tmp/hf_{movie}.wav"); fs::write(&xp, r).unwrap(); let _ = Command::new("ffmpeg") .args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp]) .status(); d = dur(&wp); } let mv = dur(&format!("{disc}/dat/movie/{movie}.wmv")); println!("{movie:16} {src:8} {token:12} id={id} voice={d}s movie={mv}s"); } }