re: the two unexplained XMA streams are BGM_102, and BGM_103 s sizes survive
Closes the open question left by the take-2 audio capture, where the probe logged five streams on one boot when only ADV s three were accounted for. Both unexplained sizes are whole packet counts, 562 and 620. Searching every inter-descriptor span of the voice stream and every sound.pak entry large enough finds zero hits in the voice stream and ONE entry carrying both -- hash 9799c546, which candidate enumeration recovers as BGM_102.slb, two streams of 1150976 and 1269760 B. One entry holding both sizes is the two-stem shape rather than two coincidental matches. So the boot s five streams were ADV s three voice streams plus one music bank s two stems, and nothing is unaccounted for. What it does not establish is which screen it belongs to. The window ran from launch to t=253 s with the title arriving at 262, so BGM_102 was decoded somewhere inside a launch-to-just-before-title window -- but the probe fires on first decode and its lines carry a thread id rather than a timestamp, so a title BGM decoded moments before the title appears is equally consistent with the evidence. Cue 1103 is already the main menu, which makes 1102 as the title at least suggestive. The settling experiment is written down and not done. Refutation attempt on HANDOFF s BGM_103 wave sizes: exact match on both (3876864 / 3930112). The claim survives unchanged. Also a third route to two-stems-of-identical-duration, from the XMA1 header alone now that PsuedoBytesPerSec is read correctly: BGM_102 37.487/37.487, BGM_103 87.750/87.749, BGM_001 173.821/173.821. The one apparent disagreement resolves in the corpus s favour -- BGM_001 reads 173.821 here against the port s decoded 167.663, a gap of 6.158 s, and HANDOFF already records 6.15 s of trailing silence after its fade-out. Declared duration covers the encoded stream including that silence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
40
crates/sylpheed-formats/examples/bank_streams.rs
Normal file
40
crates/sylpheed-formats/examples/bank_streams.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
//! List a sound bank's streams and their declared rates.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example bank_streams -- <disc> BGM_102.slb …
|
||||
use sylpheed_formats::media::{self, DirectorySource};
|
||||
use sylpheed_formats::{hash::name_hash, slb};
|
||||
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let disc = a.next().expect("usage: bank_streams <disc> NAME.slb…");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
for name in a {
|
||||
let h = name_hash(&name);
|
||||
match media::read_sound_bank(&src, h) {
|
||||
Ok(bytes) => {
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
println!(
|
||||
"{name} (hash {h:08x}, {} B on disc) header {:?} -> {} stream(s)",
|
||||
bytes.len(),
|
||||
slb::bank_header_len(&bytes),
|
||||
riffs.len()
|
||||
);
|
||||
for (i, r) in riffs.iter().enumerate() {
|
||||
let rate = if r.len() >= 0x28 {
|
||||
u32::from_le_bytes(r[0x20..0x24].try_into().unwrap())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let payload = r.len() - 60;
|
||||
println!(
|
||||
" stream {i}: payload {payload} B ({} packets) declared {rate} B/s \
|
||||
=> {:.3} s",
|
||||
payload / 2048,
|
||||
if rate > 0 { payload as f64 / rate as f64 } else { 0.0 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{name}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
123
crates/sylpheed-formats/examples/find_stream_by_size.rs
Normal file
123
crates/sylpheed-formats/examples/find_stream_by_size.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! Which cue owns an XMA stream of a given payload size?
|
||||
//!
|
||||
//! A boot with `--xma_param_probe` logs each decoded stream's `byte_size`. Three
|
||||
//! of the five on the take-2 `ADV` boot are that movie's own streams; two —
|
||||
//! 1 150 976 and 1 269 760 B — belong to something unidentified. The probe gives
|
||||
//! a size and nothing else, so the disc has to be asked which cue has a stream
|
||||
//! that long.
|
||||
//!
|
||||
//! Searches every inter-descriptor span of the continuous voice stream, and
|
||||
//! every `sound.pak` entry, for a stream whose payload matches.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example find_stream_by_size -- <disc> <bytes>…
|
||||
use sylpheed_formats::media::{DirectorySource, DiscSource};
|
||||
use sylpheed_formats::{slb, PakArchive};
|
||||
|
||||
const DESC_MARK: u32 = 0x11;
|
||||
const DESC_REPEAT: usize = 0x800;
|
||||
const ID_MAX: u32 = 0x1_0000;
|
||||
|
||||
fn all_descriptors(buf: &[u8]) -> Vec<(usize, u32)> {
|
||||
let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]);
|
||||
let mut out = Vec::new();
|
||||
if buf.len() < DESC_REPEAT + 8 {
|
||||
return out;
|
||||
}
|
||||
let end = buf.len() - (DESC_REPEAT + 4);
|
||||
let mut o = 0;
|
||||
while o <= end {
|
||||
let id = be(o);
|
||||
if id >= 1 && id < ID_MAX && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id {
|
||||
out.push((o, id));
|
||||
}
|
||||
o += 4;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let disc = args.next().expect("usage: find_stream_by_size <disc> <bytes>…");
|
||||
let wanted: Vec<usize> = args.filter_map(|a| a.parse().ok()).collect();
|
||||
assert!(!wanted.is_empty(), "give at least one payload size");
|
||||
// A `to_xma_riffs` chunk is the payload plus a 60-byte RIFF wrapper.
|
||||
let want_riff: Vec<usize> = wanted.iter().map(|w| w + 60).collect();
|
||||
println!("looking for payloads {wanted:?} (riff sizes {want_riff:?})\n");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
|
||||
// --- 1. the continuous movie-voice stream, span by span
|
||||
let tpak = src.open_pak("dat/tables.pak").expect("tables.pak");
|
||||
let marker = "eng\\Movie\\VOICE_ADV.slb";
|
||||
let registry = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| {
|
||||
tpak.read(e)
|
||||
.ok()
|
||||
.filter(|b| b.windows(marker.len()).any(|w| w == marker.as_bytes()))
|
||||
})
|
||||
.expect("registry");
|
||||
let ids = sylpheed_formats::movie_voice::registry_voice_ids(®istry);
|
||||
let name_of: std::collections::HashMap<u32, String> =
|
||||
ids.iter().map(|(n, &i)| (i, n.clone())).collect();
|
||||
let win_start: u64 = 421_739_888 & !3;
|
||||
let buf = src
|
||||
.read_segment_range("dat/sound", win_start, 116_300_000)
|
||||
.expect("window");
|
||||
let descs = all_descriptors(&buf);
|
||||
println!("voice stream: {} descriptors", descs.len());
|
||||
let mut hits = 0;
|
||||
for w in descs.windows(2) {
|
||||
let (a, b) = (w[0].0, w[1].0);
|
||||
if b <= a || b - a < 4096 {
|
||||
continue;
|
||||
}
|
||||
for (i, r) in slb::to_xma_riffs(&buf[a..b]).iter().enumerate() {
|
||||
if want_riff.contains(&r.len()) {
|
||||
let name = name_of
|
||||
.get(&w[1].1)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("id{}", w[1].1));
|
||||
println!(
|
||||
" ✅ cue {name} (id {}) stream {i}: payload {} B",
|
||||
w[1].1,
|
||||
r.len() - 60
|
||||
);
|
||||
hits += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" {hits} hit(s) in the voice stream\n");
|
||||
|
||||
// --- 2. every sound.pak entry
|
||||
let stoc = src.read_file("dat/sound.pak").expect("sound.pak toc");
|
||||
let entries = PakArchive::parse_toc(&stoc).expect("toc");
|
||||
println!("sound.pak: {} entries", entries.len());
|
||||
let mut phits = 0;
|
||||
let mut scanned = 0usize;
|
||||
for e in &entries {
|
||||
// Only entries big enough to hold the target.
|
||||
let need = wanted.iter().copied().min().unwrap_or(0) as u32;
|
||||
if e.comp_size < need {
|
||||
continue;
|
||||
}
|
||||
let Ok(bytes) = src.read_segment_range("dat/sound", e.offset as u64, e.comp_size as usize)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
scanned += 1;
|
||||
for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() {
|
||||
if want_riff.contains(&r.len()) {
|
||||
println!(
|
||||
" ✅ sound.pak entry hash {:08x} offset {} size {} — stream {i}: payload {} B",
|
||||
e.name_hash,
|
||||
e.offset,
|
||||
e.comp_size,
|
||||
r.len() - 60
|
||||
);
|
||||
phits += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" scanned {scanned} entries large enough; {phits} hit(s)");
|
||||
}
|
||||
46
crates/sylpheed-formats/examples/name_from_hash.rs
Normal file
46
crates/sylpheed-formats/examples/name_from_hash.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Recover a `sound.pak` TOC name from its hash by generating candidates.
|
||||
//!
|
||||
//! The hash is a Barrett-reduction over the uppercased path, so it cannot be
|
||||
//! inverted — but the naming is regular enough to enumerate. Tries the shapes
|
||||
//! this disc actually uses for sound entries.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example name_from_hash -- <hex-hash>…
|
||||
use sylpheed_formats::hash::name_hash;
|
||||
|
||||
fn main() {
|
||||
let wanted: Vec<u32> = std::env::args()
|
||||
.skip(1)
|
||||
.filter_map(|a| u32::from_str_radix(a.trim_start_matches("0x"), 16).ok())
|
||||
.collect();
|
||||
assert!(!wanted.is_empty(), "give hashes in hex");
|
||||
let langs = ["eng", "jpn", ""];
|
||||
let dirs = ["", "Movie", "etc", "Voice", "Sound", "BGM", "bgm", "se", "SE"];
|
||||
let mut tried = 0usize;
|
||||
let mut check = |name: String, tried: &mut usize| {
|
||||
*tried += 1;
|
||||
let h = name_hash(&name);
|
||||
if wanted.contains(&h) {
|
||||
println!(" ✅ {h:08x} {name}");
|
||||
}
|
||||
};
|
||||
for l in langs {
|
||||
for d in dirs {
|
||||
let pre = match (l.is_empty(), d.is_empty()) {
|
||||
(true, true) => String::new(),
|
||||
(true, false) => format!("{d}\\"),
|
||||
(false, true) => format!("{l}\\"),
|
||||
(false, false) => format!("{l}\\{d}\\"),
|
||||
};
|
||||
for stem in ["BGM", "bgm", "JNGL", "jngl", "SE", "Static", "VOICE"] {
|
||||
for n in 0..1200u32 {
|
||||
check(format!("{pre}{stem}_{n:03}.slb"), &mut tried);
|
||||
check(format!("{pre}{stem}{n:03}.slb"), &mut tried);
|
||||
}
|
||||
}
|
||||
for bare in ["Static.slb", "static.slb", "SE.slb", "BGM.slb"] {
|
||||
check(format!("{pre}{bare}"), &mut tried);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("tried {tried} candidate names");
|
||||
}
|
||||
90
docs/re/bgm-102-decoded-during-boot.md
Normal file
90
docs/re/bgm-102-decoded-during-boot.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# ✅ The two unexplained XMA streams are `BGM_102.slb` — and the corpus's `BGM_103` sizes survive a check
|
||||
|
||||
**Classification: decoded** for the identification (the bank, plus a disc-wide
|
||||
search); **measured** for the fact that it was decoded during a boot.
|
||||
|
||||
Closes the ❔ left by the take-2 audio capture, where
|
||||
[`audio-capture-channel-map-trap.md`](audio-capture-channel-map-trap.md) recorded
|
||||
that `--xma_param_probe` logged **five** distinct streams on one boot when only
|
||||
`ADV`'s three were accounted for.
|
||||
|
||||
## The identification
|
||||
|
||||
The probe gives a `byte_size` and nothing else, so the disc was asked which cue
|
||||
owns a stream that long. Both unexplained sizes are whole packet counts —
|
||||
1 150 976 = 562 packets, 1 269 760 = 620 — and
|
||||
`--example find_stream_by_size` searched every inter-descriptor span of the
|
||||
continuous voice stream **and** every `sound.pak` entry large enough:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| hits in the movie-voice stream | **0** |
|
||||
| hits in `sound.pak` | one entry carrying **both**: hash `9799c546` |
|
||||
|
||||
One entry holding both sizes is the two-stem shape, not a coincidence of two
|
||||
separate matches. The hash recovers by candidate enumeration
|
||||
(`--example name_from_hash`) to **`BGM_102.slb`**.
|
||||
|
||||
```
|
||||
BGM_102.slb 2 445 760 B on disc, header 10 240 -> 2 streams
|
||||
stream 0: 1 150 976 B (562 packets) declared 30 703 B/s => 37.487 s
|
||||
stream 1: 1 269 760 B (620 packets) declared 33 872 B/s => 37.487 s
|
||||
```
|
||||
|
||||
✅ So the boot's five streams were **`ADV`'s three voice streams plus one music
|
||||
bank's two stems**, and nothing is unaccounted for.
|
||||
|
||||
## 🟡 What it does NOT establish: which screen it belongs to
|
||||
|
||||
The capture window ran from process launch to **t = 253 s**, and its screen log
|
||||
reads movie/attract throughout, with the title arriving at t = 262 s — *after*
|
||||
the recording ended. So `BGM_102` was decoded somewhere inside a
|
||||
launch-to-just-before-title window.
|
||||
|
||||
⚠️ **That is not enough to call it the attract music.** The probe fires on *first
|
||||
decode* and its log lines carry a thread id, not a timestamp, so nothing here
|
||||
says *when* in those 253 s it started — and a title BGM being decoded moments
|
||||
before the title appears is exactly as consistent. The numbering makes that a
|
||||
live hypothesis rather than a remote one: the corpus already has the **main
|
||||
menu** on cue **1103** → `BGM_103`, so **1102** sitting one below it is at least
|
||||
suggestive of the title.
|
||||
|
||||
**The experiment that would settle it** is cheap and is not done: put a
|
||||
wall-clock timestamp on the probe line (or bound the run so it stops before the
|
||||
title) and compare against the screen log the capture already produces.
|
||||
|
||||
## 🟢 Refutation attempt — HANDOFF's `BGM_103` wave sizes. It SURVIVED.
|
||||
|
||||
HANDOFF asserts the menu's music is `BGM_103` partly on *"`BGM_103.slb`'s two
|
||||
declared waves (3 876 864 / 3 930 112 B)"*. Read off the disc:
|
||||
|
||||
```
|
||||
BGM_103.slb 7 841 292 B, header 10 240 -> 2 streams
|
||||
stream 0: 3 876 864 B declared 44 181 B/s => 87.750 s
|
||||
stream 1: 3 930 112 B declared 44 788 B/s => 87.749 s
|
||||
```
|
||||
|
||||
**Exact, both.** The claim stands unchanged.
|
||||
|
||||
## ✅ And a third route to "two stems of identical duration"
|
||||
|
||||
[`bgm-two-stems`](structures/bgm-two-stems.md) established equal duration by
|
||||
decoding. The XMA1 `PsuedoBytesPerSec` fix
|
||||
([`voice-region-leading-chunk.md`](structures/voice-region-leading-chunk.md))
|
||||
gives the same answer from the header alone, on three banks:
|
||||
|
||||
| bank | stem 0 | stem 1 |
|
||||
|---|---|---|
|
||||
| `BGM_102` | 37.487 s | 37.487 s |
|
||||
| `BGM_103` | 87.750 s | 87.749 s |
|
||||
| `BGM_001` | 173.821 s | 173.821 s |
|
||||
|
||||
⚠️ **And the one apparent disagreement resolves in the corpus's favour.**
|
||||
`BGM_001` reads **173.821 s** here, against the **167.663 s** the port measured by
|
||||
decoding. The gap is **6.158 s** — and HANDOFF already records that `BGM_001`
|
||||
"fades out at 167.663 s and is followed by **6.15 s of silence**". The declared
|
||||
duration covers the encoded stream *including* its trailing silence; the decoded
|
||||
figure is where the audio stops. The two are consistent, and neither is wrong.
|
||||
|
||||
That is a genuine cross-check of the declared-rate method against an independent
|
||||
decode, and it is the second one (the first was `ADV`, agreeing to 0.02 %).
|
||||
Reference in New Issue
Block a user