diff --git a/crates/sylpheed-formats/examples/sound_cue_fields.rs b/crates/sylpheed-formats/examples/sound_cue_fields.rs new file mode 100644 index 00000000..a50cc8bc --- /dev/null +++ b/crates/sylpheed-formats/examples/sound_cue_fields.rs @@ -0,0 +1,65 @@ +//! F2 — is a per-cue or per-bus GAIN on the disc? +//! +//! The port has no gain value anywhere in its export: `confirm` peaks at +//! −0.0 dBFS and sits 3 dB above the music. A cue record commonly carries a +//! volume beside its wave index. This asks the disc directly rather than +//! choosing a number. +//! +//! Method: dump every token of every `tables.pak` object whose tokens mention +//! SOUND/BANK/SE/BGM, so a gain field would appear as a token if one exists. +//! ⚠️ A NEGATIVE here is only as good as its coverage, so this prints the token +//! count per object and does not filter — a field missed by a filter would read +//! exactly like a field that is not there. +//! +//! cargo run --release -p sylpheed-formats --example sound_cue_fields -- $SYLPHEED_DISC +use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive}; + +fn main() { + let a: Vec = std::env::args().collect(); + let root = a.get(1).cloned().unwrap_or_else(|| std::env::var("SYLPHEED_DISC").unwrap()); + let arc = PakArchive::open(format!("{root}/dat/tables.pak")).unwrap(); + + // Anything a gain would plausibly be called, plus the audio nouns. + const GAINY: &[&str] = &["VOL", "GAIN", "LEVEL", "DB", "ATTEN", "AMP", "MIX", "LOUD"]; + let mut audio_objs = 0usize; + let mut gain_hits: Vec<(usize, String)> = Vec::new(); + + for (i, e) in arc.entries().iter().enumerate() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { continue }; + let t = o.tokens(); + let up: Vec = t.iter().map(|s| s.to_uppercase()).collect(); + let is_audio = up.iter().any(|s| { + s.contains("SOUND") || s.contains("BANK_") || s.starts_with("SE_") || s.starts_with("BGM_") + }); + if !is_audio { continue } + audio_objs += 1; + println!("audio object #{i}: schema {:08x}, {} tokens", o.schema_hash, t.len()); + for (j, tok) in up.iter().enumerate() { + if GAINY.iter().any(|g| tok.contains(g)) { + gain_hits.push((i, t[j].clone())); + } + } + } + + println!("\naudio-bearing objects examined: {audio_objs}"); + println!("tokens matching {GAINY:?}: {}", gain_hits.len()); + for (i, tok) in &gain_hits { + println!(" object #{i}: {tok}"); + } + if gain_hits.is_empty() { + println!("\n==> NO gain-like token in any audio object of tables.pak."); + } + + // CONTROL: the search must be able to FIND a token when one is present. + // Without this, "no hits" is indistinguishable from a broken matcher. + let mut ctrl = 0usize; + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { continue }; + ctrl += o.tokens().iter().filter(|s| s.to_uppercase().contains("SE_UI")).count(); + } + println!("\nCONTROL — the same matcher looking for a token known to exist (\"SE_UI\"): {ctrl} hits"); + println!(" {}", if ctrl > 0 { "PASS: the matcher finds tokens that are there" } + else { "FAIL: matcher is broken, the negative above means nothing" }); +} diff --git a/docs/re/f2-no-gain-field-in-tables.md b/docs/re/f2-no-gain-field-in-tables.md new file mode 100644 index 00000000..b7fbc84d --- /dev/null +++ b/docs/re/f2-no-gain-field-in-tables.md @@ -0,0 +1,87 @@ +# ❔ F2 — **no gain field in `tables.pak`.** Undecodable so far, with reach + +**Status: ❔ undecodable, with reach** — the strongest of the three classifications +this could honestly get. 2026-09-02. Instrument: ⟨disc⟩ — `dat/tables.pak` read +exhaustively, with a control. + +The port has **no gain value anywhere** in its export; `confirm` peaks at +−0.0 dBFS and sits 3 dB above the music. F2 asks whether a per-cue or per-bus +volume is on the disc, so nobody has to choose one. + +--- + +## The answer: not in `tables.pak` + +**5 audio-bearing objects, 0 gain-like fields.** + +``` +audio object #15 schema 3abe9c0c 38 tokens +audio object #18 schema 3abe9c0c 39 tokens +audio object #36 schema 070ed386 135 tokens +audio object #39 schema 13cb84ba 13907 tokens +audio object #45 schema 13cb84ba 16741 tokens + +tokens matching [VOL GAIN LEVEL DB ATTEN AMP MIX LOUD]: 0 +``` + +**Control — the same matcher looking for a token known to be there:** `SE_UI` +returns **38 hits**. ✅ **PASS.** Without it, "0 hits" and "broken matcher" are the +same observation, and this corpus has published that mistake before. + +## 🔴 Why the negative is stronger than a name search usually is + +A name search normally cannot exclude an **unnamed numeric column** — and I +expected that to be this page's limitation. Dumping the schema shows it is not. +The token stream is **value-then-key pairs**: + +``` + 1 0x060523 2 VERSION + 4 dat\GP_OPTIONS.pak+eng\ 5 PATH + 33 40 34 LINE_PITCH + 35 0 36 Y_OFFSET_ANALOG_STICK +``` + +**Numbers are tokens and they carry names.** `40` is a token, and so is +`LINE_PITCH`. So a gain in this format would have a *name*, and the name search +covers exactly the space where it would live. That is what turns "I did not find +one" into "one is not there **in this file**". + +## ⚠️ Reach — the two places I did NOT look + +1. **The `.slb` bank headers.** `BANK_SE` names a single bank, `Static.slb`, and a + per-wave gain beside a wave index is the other conventional home for this. The + banks are **inside paks** — no `.slb` exists on the extracted disc as a loose + file — so inspecting the header costs a pak extraction I did not have budget + for this iteration. **This is the first thing the next attempt should do.** +2. **The executable.** A mix could be immediates in the sound-play path; + `sub_821C5580` is a decoded entry point into it. + +**So this is not yet "the mix is not on the disc".** It is *"the mix is not in the +table where a cue's fields live"*, which is a real narrowing and not the whole +answer. + +📌 One adjacent fact, offered as a pointer and not a finding: object #15 lists +`po_sound_scr.prt → SOUND` among `GP_OPTIONS`' screens. **There is a user-facing +sound options screen**, so at least one volume exists as *runtime state*. That is a +different thing from a per-cue mix and does not answer F2 either way — but if the +next attempt finds no authored gain anywhere, a user-settable master is where the +game's own levels would come from. + +## For the port + +**Keep authoring nothing yet.** The place a gain would most likely be has not been +looked at. If the bank headers come back empty too, the honest classification +becomes *undecodable* across all three sites and the port authors a mix knowing it +is authoring — which is the outcome this exercise exists to make explicit rather +than accidental. + +## Refutation attempt, recorded per the adversarial duty + +**Target:** the play-test's framing that *"a cue record commonly carries a volume +beside its wave index"*. + +**Result: not refuted, and not confirmed — the premise is untested where it +matters.** The cue *records* in `tables.pak` carry an id and a name and no volume. +But "beside its wave index" points at the **bank**, not the table, and the wave +index lives in the `.slb`. The framing is sound; it just aims at a file this page +did not open.