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 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(); for movie in [ "ADV", "RT01A", "RT01B", "RT01C_1", "S00A", "S01A", "hokyu_LS_s02A", "hokyu_LS_s09A", "hokyu_LS_s02H", "hokyu_DS_s07H", ] { let Some(token) = movie_manifest::voice_token(&manifest, movie) else { println!("{movie:16} no token"); continue; }; let Some(&id) = ids.get(&token) else { println!("{movie:16} token {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 for {token}"); continue; }; let win_start = (anchor.saturating_sub(2 * 1024 * 1024)) & !3; let window = rg(&disc, win_start, 8 * 1024 * 1024); let Some(end_local) = movie_voice::find_descriptor(&window, id) else { println!("{movie:16} id {id}: desc NOT FOUND"); continue; }; let end = win_start + end_local as u64; let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1)) .or_else(|| movie_voice::find_descriptor_before(&window, end_local)) .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/ra2_{movie}.xma.wav"); let wp = format!("/tmp/ra2_{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} id={id:5} region={}KB riffs={} voice={d}s movie={mv}s", (end - start) / 1024, riffs.len() ); } }