Files
Sylpheed/crates/sylpheed-formats/examples/slb_fmt_probe.rs
Sylpheed RE agent 58ee6ac52c re: the .slb leading region is XMA1 MONO — 113x more audio than stereo
Retried the format probe with the fmt chunk built to synth_xma1_fmt's exact
byte layout, and with the bank's own RIFF sub-wave decoded through the same
pipe as a CONTROL so a broken harness cannot masquerade as a result.

The channel count is the whole story:

  bank         lead B   channels=2   channels=1
  VOICE_D_450   16384         1792        46756
  VOICE_D_451    2048         1792          896   (all-zero region: control)
  VOICE_D_452   14336         1792        30154
  VOICE_D_453   45056         1792       203648
  VOICE_D_454   59392         1792       294440

channels=2 yields EXACTLY 1792 bytes for every bank regardless of size -- one
frame, then it stops. That constant is the tell. At channels=1 the same data
yields up to 113x more, and the control sub-wave decodes to 13568, so the
pipe works.

Why the previous probe got 0 bytes everywhere is now named: I read
synth_xma1_fmt(2, 2, 48000)'s second argument as a STREAM COUNT when it is a
CHANNEL MASK, and built the WAVEFORMATEX around that misreading.

Also recorded as a refutation, because it was tempting: solving for the
sample rate as decoded-samples / last-subtitle-cue does NOT converge. D_453
implies 21665 Hz -- close enough to 22050 that I nearly wrote it down -- but
D_450 implies 5844 Hz. No single rate explains both, and the decodes are
visibly partial (samples per input byte ranges 2.10-4.96 where a clean decode
would be near-constant).

So the container is identified and the duration is not. Next step recorded:
find why FFmpeg stops early, likely the hardcoded packet/subframe fields.

Artifact: examples/slb_fmt_probe.rs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-25 23:53:10 +00:00

114 lines
4.8 KiB
Rust

//! Does the `.slb` leading region decode as XMA1 under ANY plausible format?
//!
//! A first attempt at this hand-rolled the `fmt ` chunk and produced 0 PCM bytes
//! for all 36 combinations — including ones that should have matched the crate's
//! own working format. So it tested the chunk construction, not the hypothesis.
//! This version replicates `slb::synth_xma1_fmt`'s exact byte layout and starts
//! by reproducing its known result as a CONTROL; if the control does not match,
//! nothing below it means anything.
use std::io::Write;
use std::process::{Command, Stdio};
use sylpheed_formats::{movie_subtitle, slb, PakArchive};
/// Byte-for-byte `slb::synth_xma1_fmt` (private there). Note the second
/// parameter is a **channel mask**, not a stream count — mistaking it is what
/// made the first probe meaningless.
fn xma1_fmt(channels: u8, channel_mask: u16, rate: u32) -> Vec<u8> {
let mut fmt = Vec::with_capacity(40);
fmt.extend_from_slice(b"fmt ");
fmt.extend_from_slice(&32u32.to_le_bytes());
fmt.extend_from_slice(&0x0165u16.to_le_bytes()); // XMA1
fmt.extend_from_slice(&16u16.to_le_bytes()); // BitsPerSample
fmt.extend_from_slice(&0u16.to_le_bytes()); // EncodeOptions
fmt.extend_from_slice(&0u16.to_le_bytes()); // LargestSkip
fmt.extend_from_slice(&1u16.to_le_bytes()); // NumStreams
fmt.push(0); // LoopCount
fmt.push(3); // Version
fmt.extend_from_slice(&(rate * channels as u32 * 2).to_le_bytes());
fmt.extend_from_slice(&rate.to_le_bytes());
fmt.extend_from_slice(&0u32.to_le_bytes());
fmt.extend_from_slice(&0u32.to_le_bytes());
fmt.push(4); // SubframeData
fmt.push(channels);
fmt.extend_from_slice(&channel_mask.to_le_bytes());
fmt
}
fn riff(fmt: &[u8], data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(12 + fmt.len() + 8 + data.len());
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&((4 + fmt.len() + 8 + data.len()) as u32).to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(fmt);
out.extend_from_slice(b"data");
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
out.extend_from_slice(data);
out
}
fn decode_bytes(r: &[u8]) -> usize {
let Ok(mut c) = Command::new("ffmpeg")
.args(["-v", "error", "-i", "pipe:0", "-f", "s16le", "pipe:1"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
else {
return 0;
};
let buf = r.to_vec();
let mut stdin = c.stdin.take().unwrap();
std::thread::spawn(move || {
let _ = stdin.write_all(&buf);
});
c.wait_with_output().map(|o| o.stdout.len()).unwrap_or(0)
}
fn main() {
let disc = std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC");
let snd = PakArchive::open(format!("{disc}/dat/sound.pak")).expect("sound.pak");
let lang = PakArchive::open(format!("{disc}/dat/movie/eng.pak")).expect("eng.pak");
// The leading region decodes only as MONO. At channels=2 it yields 1792
// bytes; at channels=1, 203648 for VOICE_D_453. The decoded SAMPLE COUNT is
// independent of the declared rate (the rate only sets playback speed), so
// the subtitle cue can be used to solve for the real rate instead.
let bound = [
("hokyu_LS_s02A", 450u32),
("hokyu_LS_s09A", 451),
("hokyu_DS_s13A", 452),
("hokyu_LS_s02H", 453),
("hokyu_DS_s07H", 454),
];
println!(
"{:<16} {:>6} {:>9} {:>9} {:>10} {:>9} {:>12}",
"movie", "bank", "stereo B", "mono B", "samples", "cue s", "implied Hz"
);
for (movie, n) in bound {
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
let Some(entry) = snd.find_by_name(&path) else { continue };
let bytes = snd.read(entry).expect("read");
let Some(first_riff) = bytes.windows(4).position(|w| w == b"RIFF") else { continue };
if first_riff <= slb::HEADERLESS_DATA_OFFSET {
println!("{movie:<16} {:>6} (no leading region)", format!("D_{n}"));
continue;
}
let lead = &bytes[slb::HEADERLESS_DATA_OFFSET..first_riff];
let stereo = decode_bytes(&riff(&xma1_fmt(2, 2, 48000), lead));
let mono = decode_bytes(&riff(&xma1_fmt(1, 0, 48000), lead));
let samples = mono / 2; // 16-bit mono
let cue = movie_subtitle::track_voice_cues(&lang, movie)
.iter()
.map(|(_, t)| *t)
.fold(0.0f32, f32::max);
let implied = if cue > 0.0 { samples as f32 / cue } else { f32::NAN };
println!(
"{movie:<16} {:>6} {stereo:>9} {mono:>9} {samples:>10} {cue:>9.2} {implied:>12.0}",
format!("D_{n}")
);
}
println!("\n'implied Hz' = decoded samples / the movie's last subtitle cue.");
println!("A consistent value near a standard rate is the real sample rate.");
}