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
This commit is contained in:
113
crates/sylpheed-formats/examples/slb_fmt_probe.rs
Normal file
113
crates/sylpheed-formats/examples/slb_fmt_probe.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! 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.");
|
||||
}
|
||||
@@ -1063,12 +1063,21 @@ premise was wrong.**
|
||||
five banks fail that: `D_450` cue 4.00 s vs 1.41 s decoded, `D_451` 3.70 vs
|
||||
1.81, `D_453` **4.70 vs 0.07**. The other two have their only cue at 0.0 s and
|
||||
give no signal. Artifact `examples/voice_len_vs_subs.rs`, FFmpeg-measured.
|
||||
❔ **The fmt-variation probe was INCONCLUSIVE** — 36 combinations over
|
||||
`VOICE_D_453`'s leading region all produced 0 PCM bytes, *including* ones
|
||||
equivalent to the crate's working `synth_xma1_fmt`, so the probe tested my
|
||||
hand-built `fmt` chunk rather than the hypothesis. Not evidence the region is
|
||||
non-XMA. ▶️ Retry building the chunk with the crate's own helper and varying
|
||||
its parameters. See
|
||||
✅ **(same day) The leading region IS XMA1 — MONO, not stereo.** At
|
||||
`channels = 2` every bank decodes to *exactly* 1792 bytes regardless of size
|
||||
(one frame, then it stops); at `channels = 1` the same data yields up to
|
||||
**113× more** — `VOICE_D_453` goes 1 792 → **203 648**. The bank's own RIFF
|
||||
sub-wave is decoded through the same pipe as a control (13 568 bytes), so the
|
||||
harness is sound, and the all-zero `VOICE_D_451` region is the control the
|
||||
other way. The earlier 0-byte probe was my own error: I read
|
||||
`synth_xma1_fmt`'s second argument as a stream count when it is a **channel
|
||||
mask**. ❌ Solving for the sample rate from the subtitle cue **does not
|
||||
converge** — 21 665 Hz for `D_453` (temptingly near 22 050, and I nearly wrote
|
||||
it down) but **5 844 Hz** for `D_450`. The decodes are partial: samples per
|
||||
input byte ranges 2.10–4.96 where a clean decode would be near-constant.
|
||||
▶️ Next: find why FFmpeg stops early — likely the packet/subframe fields in the
|
||||
synthesised `fmt`, which are hardcoded (`SubframeData = 4`, `NumStreams = 1`).
|
||||
See
|
||||
[`voice-bank-leading-region.md`](voice-bank-leading-region.md).
|
||||
* ❌ **(2026-08-25) My own boot-nav diagnosis, MEASURED AND WITHDRAWN.** I said
|
||||
the run died because `skip_intro.sh` gates the title test at `rmse <= 1500`
|
||||
|
||||
@@ -144,7 +144,54 @@ Three of the five are decisive; the other two have their only cue at 0.0 s, whic
|
||||
tells us nothing in either direction. So **something is genuinely missing from
|
||||
these banks** — independent of anything above, and now measured rather than felt.
|
||||
|
||||
## ❔ Varying the XMA format — inconclusive, and for a boring reason
|
||||
## ✅ The leading region IS XMA1 — but **mono**, not stereo
|
||||
|
||||
Retried with the `fmt ` chunk built to `slb::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). Artifact
|
||||
`examples/slb_fmt_probe.rs`.
|
||||
|
||||
The parameter that matters is the channel count, and the difference is not
|
||||
subtle:
|
||||
|
||||
| bank | leading bytes | decoded at **channels = 2** | decoded at **channels = 1** |
|
||||
|---|---|---|---|
|
||||
| `VOICE_D_450` | 16 384 | 1 792 | **46 756** |
|
||||
| `VOICE_D_451` | 2 048 | 1 792 | 896 |
|
||||
| `VOICE_D_452` | 14 336 | 1 792 | **30 154** |
|
||||
| `VOICE_D_453` | 45 056 | 1 792 | **203 648** |
|
||||
| `VOICE_D_454` | 59 392 | 1 792 | **294 440** |
|
||||
|
||||
`channels = 2` yields **exactly 1792 bytes for every bank regardless of size** —
|
||||
one frame, then it stops. That constant is the tell: stereo is simply the wrong
|
||||
shape. At `channels = 1` the same data yields up to **113× more** audio, and the
|
||||
control sub-wave decodes to 13 568 bytes, so the harness is sound.
|
||||
|
||||
`VOICE_D_451` decoding to almost nothing is the expected control the other way:
|
||||
its leading region is the all-zero one.
|
||||
|
||||
The sample rate and channel mask make **no difference to the decoded byte
|
||||
count** — as expected, since they set playback speed rather than sample count.
|
||||
|
||||
## ❌ Solving for the sample rate from the subtitle cue — does NOT converge
|
||||
|
||||
`decoded samples / last subtitle cue` should give the real rate if the decode
|
||||
were complete. It does not agree with itself:
|
||||
|
||||
| bank | samples | cue | implied |
|
||||
|---|---|---|---|
|
||||
| `VOICE_D_453` | 101 824 | 4.70 s | 21 665 Hz |
|
||||
| `VOICE_D_450` | 23 378 | 4.00 s | **5 844 Hz** |
|
||||
|
||||
21 665 Hz is temptingly close to 22 050, and I nearly wrote that down. The second
|
||||
bank refutes it: no single rate explains both. The decodes are also visibly
|
||||
**partial** — decoded samples per input byte ranges 2.10 to 4.96 across the
|
||||
banks, where a clean decode would be near-constant — so FFmpeg is not consuming
|
||||
these streams to the end.
|
||||
|
||||
So: the container is identified, the duration is not.
|
||||
|
||||
## ❔ The first attempt at this — inconclusive, and for a boring reason
|
||||
|
||||
The recorded next step was to vary the synthesised `fmt` (channels, streams,
|
||||
sample rate) rather than assume the container. I tried 36 combinations over
|
||||
@@ -152,10 +199,11 @@ sample rate) rather than assume the container. I tried 36 combinations over
|
||||
including combinations that should be equivalent to the crate's own
|
||||
`synth_xma1_fmt(2, 2, 48000)`, which does at least parse (it yields 1792 bytes).
|
||||
|
||||
That means the probe tested **my hand-built `fmt` chunk**, not the hypothesis. It
|
||||
is not evidence that the region is non-XMA. The next attempt should build the
|
||||
chunk with the crate's own helper and vary its parameters, rather than
|
||||
hand-rolling the WAVEFORMATEX.
|
||||
That means the probe tested **my hand-built `fmt` chunk**, not the hypothesis.
|
||||
The specific error is worth recording: 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. Replicating the real layout is what turned
|
||||
0 bytes into 203 648.
|
||||
|
||||
## What this does not settle
|
||||
|
||||
|
||||
Reference in New Issue
Block a user