re: recover the .slb leading segment — mono, and scoped by measurement

to_xma_riffs now emits the leading headerless segment when it sits at a whole
number of XMA1 packets and carries a non-zero byte. VOICE_D_453 goes from a
0.14 s trailing fragment to a 45116-byte leading sub-wave that dominates it.

I withdrew this exact change earlier for two reasons. Both are now answered
rather than argued away:

* "It recovers no audio" -- it used the STEREO format. At two channels every
  bank yields exactly 1792 bytes, one frame, whatever its size. Mono yields up
  to 113x more.
* "It matches 1524 of 8021 RIFF-bearing entries" -- the byte-level reach is
  still 1524, but the audible reach is not. Across the 84 movie-bound banks
  the segment adds >1 s to exactly 7, the hokyu_*_H tankers on D_453/D_454 --
  precisely the broken ones -- and <=0.25 s to 66 of the rest. The largest
  non-resupply addition is S04A at +0.66 s on a 256 s movie.

The safety oracle is recorded with its limits: 8 of the 84 banks ALREADY
exceed their movie's duration before the change, by hundredths of a second,
so it cannot resolve differences at that scale. It establishes scoping, not
correctness. Callers clamp to the movie length regardless.

VOICE_D_451's all-zero leading region is skipped by the non-zero guard, so
the rule cannot prepend silence to a bank that does not need it. Pinned, as
is the packet arithmetic (n = 8, 1, 7, 22, 29) which has no tunable.

slb_disc, movie_subtitle_disc and movie_manifest_disc all still pass.

NOT verified by ear -- that needs a human.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-26 00:37:06 +00:00
parent ea0eedda86
commit a32c00057e
4 changed files with 152 additions and 20 deletions

View File

@@ -133,7 +133,8 @@ fn parse_voice_clip(name: &str) -> VoiceClip {
/// tracing confirmed the movie→voice binding; this fixes the *decode* of `RT*`.) /// tracing confirmed the movie→voice binding; this fixes the *decode* of `RT*`.)
pub fn to_xma_riffs(slb: &[u8]) -> Vec<Vec<u8>> { pub fn to_xma_riffs(slb: &[u8]) -> Vec<Vec<u8>> {
let mut out = Vec::new(); let mut out = Vec::new();
if find(slb, b"RIFF", 0).is_none() { let first_riff = find(slb, b"RIFF", 0);
if first_riff.is_none() {
// Headerless single-stream bank. // Headerless single-stream bank.
if let Some(data) = slb.get(HEADERLESS_DATA_OFFSET..) { if let Some(data) = slb.get(HEADERLESS_DATA_OFFSET..) {
if !data.is_empty() { if !data.is_empty() {
@@ -142,22 +143,33 @@ pub fn to_xma_riffs(slb: &[u8]) -> Vec<Vec<u8>> {
} }
return out; return out;
} }
// ❌ A "leading headerless stream" rule was tried here and WITHDRAWN. // HYBRID banks: a headerless packet stream followed by RIFF sub-waves, two
// SEQUENTIAL SEGMENTS of one clip. The branch above only fires when there is
// no `RIFF` at all, so the leading segment used to be dropped — which is why
// `VOICE_D_453` decoded to 0.14 s: its line is in that segment and only the
// trailing fragment survived.
// //
// The structure is real: in all five resupply banks the first `RIFF` sits at // The boundary is arithmetic, not a magic: XMA1 packets are 2048 bytes, so a
// exactly `HEADERLESS_DATA_OFFSET + n*XMA1_PACKET` (n = 8, 1, 7, 22, 29), and // leading stream occupies exactly `HEADERLESS_DATA_OFFSET + n*XMA1_PACKET`.
// 87 % of `VOICE_D_453` lies in front of it. Emitting that region as a // It decodes as **mono** — at two channels every bank yields exactly 1792
// sub-wave raised byte coverage from 5.4 % to 89.9 %. // bytes, one frame, whatever its size.
// //
// But byte coverage was the wrong success metric. The emitted streams decode // An earlier version of this was withdrawn for two good reasons, both now
// to **1792 PCM bytes** — silence — through the same FFmpeg path that decodes // answered: it recovered no audio (it used the stereo format), and it
// the RIFF sub-waves fine, so the region is not XMA1 under the synthesised // matched 1524 of the 8021 RIFF-bearing entries. The byte-level reach is
// format. And the rule is not narrow: it matches **1524 of the 8021** // still 1524, but the *audible* reach is not: across the 84 movie-bound
// RIFF-bearing entries in `sound.pak`, including `RT*` banks that decode // banks the segment adds >1 s to exactly **7** — the `hokyu_*_H` tankers
// correctly today. Landing it would have risked a large regression to fix // bound to `VOICE_D_453`/`454`, i.e. precisely the broken ones — and
// five banks it does not actually fix. // ≤0.25 s to 66 of the rest. Callers clamp to the movie length anyway.
// if let Some(ri) = first_riff {
// See docs/re/voice-bank-leading-region.md. if ri > HEADERLESS_DATA_OFFSET && (ri - HEADERLESS_DATA_OFFSET) % XMA1_PACKET == 0 {
if let Some(data) = slb.get(HEADERLESS_DATA_OFFSET..ri) {
if data.iter().any(|b| *b != 0) {
out.push(build_riff(&synth_xma1_fmt(1, 0, 48000), data));
}
}
}
}
let mut pos = 0usize; let mut pos = 0usize;
while let Some(ri) = find(slb, b"RIFF", pos) { while let Some(ri) = find(slb, b"RIFF", pos) {
// Parse this sub-wave's fmt + data (declared size is honest per sub-wave). // Parse this sub-wave's fmt + data (declared size is honest per sub-wave).

View File

@@ -0,0 +1,86 @@
//! The `.slb` leading segment — recovered, and scoped.
//!
//! `VOICE_D_453` used to decode to 0.14 s because its line lives in a headerless
//! packet stream *before* the first `RIFF`, and the decoder started at the
//! `RIFF`. The banks that looked fine were the ones whose leading segment is
//! silence. One rule, two outcomes.
use std::path::{Path, PathBuf};
use sylpheed_formats::{slb, PakArchive};
fn disc_root() -> Option<PathBuf> {
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
let p = PathBuf::from(p);
if p.join("dat").is_dir() {
return Some(p);
}
}
let default = Path::new(
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
);
default.join("dat").is_dir().then(|| default.to_path_buf())
}
macro_rules! skip_without_disc {
($root:ident) => {
let Some($root) = disc_root() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
};
}
fn bank(root: &Path, n: u32) -> Vec<u8> {
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
let entry = snd.find_by_name(&path).expect("bank present");
snd.read(entry).expect("read")
}
/// The boundary is arithmetic and has no tunable: the first `RIFF` sits at
/// exactly `HEADERLESS_DATA_OFFSET + n*XMA1_PACKET` in every resupply bank.
#[test]
fn leading_segment_is_a_whole_number_of_packets() {
skip_without_disc!(root);
for (n, packets) in [(450u32, 8usize), (451, 1), (452, 7), (453, 22), (454, 29)] {
let b = bank(&root, n);
let ri = b.windows(4).position(|w| w == b"RIFF").expect("has a RIFF");
assert!(ri > slb::HEADERLESS_DATA_OFFSET, "VOICE_D_{n}");
let lead = ri - slb::HEADERLESS_DATA_OFFSET;
assert_eq!(lead % slb::XMA1_PACKET, 0, "VOICE_D_{n} not a whole packet count");
assert_eq!(lead / slb::XMA1_PACKET, packets, "VOICE_D_{n} packet count");
}
}
/// The two banks whose line lives in the leading segment now yield it.
#[test]
fn broken_banks_recover_their_line() {
skip_without_disc!(root);
// (bank, sub-waves expected, payload of the leading one)
for (n, waves, lead_len) in [(453u32, 2usize, 45116usize), (454, 2, 59452)] {
let riffs = slb::to_xma_riffs(&bank(&root, n));
assert_eq!(riffs.len(), waves, "VOICE_D_{n} sub-wave count");
assert_eq!(riffs[0].len(), lead_len, "VOICE_D_{n} leading segment");
// It must be the LARGER part: that is the whole point.
assert!(
riffs[0].len() > riffs[1].len() * 5,
"VOICE_D_{n}: leading segment should dominate"
);
}
}
/// `VOICE_D_451`'s leading region is all zeros — the guard must skip it, so the
/// rule cannot prepend silence to a bank that does not need it.
#[test]
fn all_zero_leading_region_is_skipped() {
skip_without_disc!(root);
let b = bank(&root, 451);
let ri = b.windows(4).position(|w| w == b"RIFF").unwrap();
assert!(
b[slb::HEADERLESS_DATA_OFFSET..ri].iter().all(|x| *x == 0),
"expected an all-zero leading region"
);
// Two sub-waves, both from the RIFF section — no synthesised third.
assert_eq!(slb::to_xma_riffs(&b).len(), 2);
}

View File

@@ -1057,9 +1057,18 @@ premise was wrong.**
first `RIFF`. For the first three that discards only silence, so they looked first `RIFF`. For the first three that discards only silence, so they looked
fine (2.8 / 1.6 / 2.2 s); for the last two it discards **the line itself**, fine (2.8 / 1.6 / 2.2 s); for the last two it discards **the line itself**,
leaving 0.14 s and 0.43 s. One rule, two outcomes, depending on which segment leaving 0.14 s and 0.43 s. One rule, two outcomes, depending on which segment
holds the speech. ▶️ The fix is now well-posed: emit the leading region **only holds the speech. ✅ **(same day) LANDED.** `to_xma_riffs` emits the leading segment
when it carries signal**, which also avoids the 1524-bank blast radius that wrapped **mono** when it is a whole number of packets and carries a non-zero
sank the earlier attempt. byte. Both reasons the first attempt was withdrawn are answered: it used the
stereo format (mono yields up to 113× more), and while the byte-level reach is
still 1524 entries the **audible** reach is not — across the 84 movie-bound
banks it adds >1 s to exactly **7**, the `hokyu_*_H` tankers on
`VOICE_D_453`/`454`, and ≤0.25 s to 66 of the rest. ⚠️ The safety oracle is
weak and says so: **8 of the 84 already exceed their movie duration before the
change**, by hundredths of a second, so it establishes scoping rather than
correctness. Pinned by `tests/slb_leading_segment_disc.rs`, including that the
all-zero `VOICE_D_451` region stays skipped. ❔ Not verified by ear — that
needs a human.
* ❌ **(2026-08-25) The `.slb` "multi-subwave" guess is REFUTED, and the voice * ❌ **(2026-08-25) The `.slb` "multi-subwave" guess is REFUTED, and the voice
decoder is discarding up to 87 % of a bank.** The record table gives a decoder is discarding up to 87 % of a bank.** The record table gives a
**direct** binding `hokyu_DS_s13A -> VOICE_D_452` where the corpus records the **direct** binding `hokyu_DS_s13A -> VOICE_D_452` where the corpus records the

View File

@@ -1,7 +1,8 @@
# The resupply voice banks — the decoder discards up to 87 % of them # The resupply voice banks — the decoder discards up to 87 % of them
Status: ❌ the recorded "multi-subwave / not cleanly sliced" explanation is Status: ✅ **FIXED** — `to_xma_riffs` now emits the leading segment as mono, and
**REFUTED**. ✅ each shared bank is **one generic line**, which explains the `VOICE_D_453`/`454` recover their line (`tests/slb_leading_segment_disc.rs`).
❌ the recorded "multi-subwave / not cleanly sliced" explanation is **REFUTED**. ✅ each shared bank is **one generic line**, which explains the
in-game verdict that rejected the `hokyu_DS_s13A` binding — the line really is in-game verdict that rejected the `hokyu_DS_s13A` binding — the line really is
generic. ❌ **my own "audio is missing" conclusion is RETRACTED**: a subtitle cue generic. ❌ **my own "audio is missing" conclusion is RETRACTED**: a subtitle cue
is a START time, not a point inside the clip, and under the correct reading every is a START time, not a point inside the clip, and under the correct reading every
@@ -178,6 +179,30 @@ 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 tells us nothing in either direction. So **something is genuinely missing from
these banks** — independent of anything above, and now measured rather than felt. these banks** — independent of anything above, and now measured rather than felt.
## ✅ Landed — and why it is safe this time
`to_xma_riffs` now emits the leading segment, wrapped **mono**, when it sits at a
whole number of packets and carries a non-zero byte. The first attempt at this
was withdrawn for two good reasons, and both are answered:
* *"It recovers no audio."* It used the **stereo** format. Mono yields up to 113×
more, and `VOICE_D_453`'s 45 116-byte segment is now its largest sub-wave.
* *"It matches 1524 of 8021 entries."* The byte-level reach is still 1524, but
the **audible** reach is not. Across the 84 movie-bound banks the segment adds
**more than 1 s to exactly 7** — the `hokyu_*_H` tankers on `VOICE_D_453`/`454`,
precisely the broken ones — and ≤0.25 s to 66 of the rest. The largest
non-resupply addition is `S04A` at +0.66 s on a **256 s** movie.
The safety oracle is honest about its own limits: 8 of the 84 banks *already*
exceed their movie's duration before the change, by hundredths of a second, so it
cannot resolve differences at that scale. What it does establish is the
**scoping** — the change is material only where it is meant to be. Callers clamp
to the movie length regardless.
`VOICE_D_451`'s all-zero leading region is skipped by the non-zero guard, so the
rule cannot prepend silence to a bank that does not need it; that is pinned by a
test.
## ✅ The two parts are SEQUENTIAL SEGMENTS — and that closes the original mystery ## ✅ The two parts are SEQUENTIAL SEGMENTS — and that closes the original mystery
The last open question was whether the leading mono region duplicates the RIFF The last open question was whether the leading mono region duplicates the RIFF