port: settle times measured -- the pacing was already right, and my own red flag was half wrong
THE TRANSCODE CACHE HAD NEVER HIT. `video::transcode` has carried one since P4 -- a `.cmd` sidecar with the command, the source size and the channel count -- and `main.rs` clears the output tree wholesale immediately before the check, deleting the sidecar and the file it stamps. Six exports in this session paid the full Theora encode and produced five byte-identical files, roughly 48 minutes. Nothing reported it, and nothing could: a cache is silent either way and the only symptom is a wall clock that looks like the job being slow. The wipe now spares `video/` and `prune_videos` deletes anything in it this run did not claim, so the wholesale guarantee is kept rather than traded. A re-export is 20 s. SETTLE TIMES, MEASURED, and they refute more of my row than they confirm. The principle holds -- the title's rest.t is 251 units = 4.183 s where its art finishes at ~2 s -- but "everything the sequencer paces off that landmark is therefore late" does not. Measured the port the way the game was measured, by VISIBLE SPAN rather than arrival-to-arrival: publisher wordmark port 4.25 s game 4.297 / 4.604 / 4.370 developer logos port 3.50 s game 3.508 / 3.503 / 3.366 black hold port ~0.25 s game 0.2 - 0.3 title -> plate port 2.000 s (declared 120 units) game 2.247 s Dead on. My earlier reading compared the port's transition timestamps against the game's visible spans, which differ by the exit ramp plus the black hold -- the whole of the discrepancy I was about to chase, and the same definitional trap that cost this corpus 0.48 s on the plate delay. Nothing in the sequencer is changed. `dwell_seconds` stays null, now for a measured reason rather than an absent one: `timing.json` said "if a capture ever times the real boot, this is where that number goes", and the answer is that nothing goes there. Not authored, deliberately: an (A)->menu dwell, which measures 3.763 s and contains a 1.53 s guest load stall on a cold cache; and the menu build-in and (B)->title, which rest on one run where the port is already within ~0.1 s. THE VOICE PRESENTATION MOVED TO authored/, because the recommendation behind it was withdrawn as self-contradictory and the choice is now unambiguously mine. `voice.presentation` = `loudest`, and the reason to switch is a measurement, not a preference: ADV chunk 1 is MONO-IN-STEREO and chunk 2 is DUAL-MONO, so chunk 2's extra bytes encode a duplicate channel rather than fidelity. That explains the byte-rate difference and removes the only argument for `highest_rate`. ADV's dialogue now exports at +0.3 dBFS instead of -8.7, which is the SE bound's documented decode overshoot on a wave mastered at full scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
@@ -1,113 +1,156 @@
|
||||
{
|
||||
"format": "sylpheed.audio/1",
|
||||
"format": "sylpheed.audio/1",
|
||||
"_": [
|
||||
"Menu audio. EVERY VALUE IN THIS FILE IS MEASURED OR CHOSEN -- none of it is",
|
||||
"in a data file the exporter can read, which is why it is here and not in the",
|
||||
"exporter. `measured` and `chosen` are NOT the same thing and this file keeps",
|
||||
"them apart: a measurement is deleted when the disc states it, a choice is",
|
||||
"deleted when somebody measures it.",
|
||||
"",
|
||||
"Two different kinds of not-on-the-disc live in this file and they are not",
|
||||
"interchangeable:",
|
||||
"",
|
||||
" * `se` -- MEASURED. `Static.slb` is a delimiter-less run of whole 2048-byte",
|
||||
" XMA1 packets: no RIFF, no seek chunk, no XACT container. A wave is defined",
|
||||
" ONLY by (offset, packet_count), and both numbers come from the running",
|
||||
" game, not from the file. HANDOFF Q8. Delete a row the day a table on the",
|
||||
" disc states the same thing.",
|
||||
"",
|
||||
" * `bgm` -- MEASURED, and only the LOOP POLICY beside it is chosen. HANDOFF",
|
||||
" Q10's negative is about the TABLES: `SOUNDS`, `FILES` and the bank headers",
|
||||
" name no screen. The executable does -- cue 1103 = `BGM_103`, corroborated",
|
||||
" by a byte-for-byte match against what the XMA probe saw at the main menu.",
|
||||
" An earlier draft read the negative as unbounded, picked a track at random",
|
||||
" and called it authored. See the `bgm._` block for what that cost.",
|
||||
"",
|
||||
"The exporter reads this file and emits `export/audio/**` from it. It holds no",
|
||||
"cue table of its own: a measured offset compiled into a Rust `const` is a",
|
||||
"measurement wearing the costume of a decoded field, and MISSION section 3 is",
|
||||
"explicit that measured values live here."
|
||||
],
|
||||
"se": {
|
||||
"_": [
|
||||
"Menu audio. EVERY VALUE IN THIS FILE IS MEASURED OR CHOSEN -- none of it is",
|
||||
"in a data file the exporter can read, which is why it is here and not in the",
|
||||
"exporter. `measured` and `chosen` are NOT the same thing and this file keeps",
|
||||
"them apart: a measurement is deleted when the disc states it, a choice is",
|
||||
"deleted when somebody measures it.",
|
||||
"",
|
||||
"Two different kinds of not-on-the-disc live in this file and they are not",
|
||||
"interchangeable:",
|
||||
"",
|
||||
" * `se` -- MEASURED. `Static.slb` is a delimiter-less run of whole 2048-byte",
|
||||
" XMA1 packets: no RIFF, no seek chunk, no XACT container. A wave is defined",
|
||||
" ONLY by (offset, packet_count), and both numbers come from the running",
|
||||
" game, not from the file. HANDOFF Q8. Delete a row the day a table on the",
|
||||
" disc states the same thing.",
|
||||
"",
|
||||
" * `bgm` -- MEASURED, and only the LOOP POLICY beside it is chosen. HANDOFF",
|
||||
" Q10's negative is about the TABLES: `SOUNDS`, `FILES` and the bank headers",
|
||||
" name no screen. The executable does -- cue 1103 = `BGM_103`, corroborated",
|
||||
" by a byte-for-byte match against what the XMA probe saw at the main menu.",
|
||||
" An earlier draft read the negative as unbounded, picked a track at random",
|
||||
" and called it authored. See the `bgm._` block for what that cost.",
|
||||
"",
|
||||
"The exporter reads this file and emits `export/audio/**` from it. It holds no",
|
||||
"cue table of its own: a measured offset compiled into a Rust `const` is a",
|
||||
"measurement wearing the costume of a decoded field, and MISSION section 3 is",
|
||||
"explicit that measured values live here."
|
||||
"MEASURED, HANDOFF Q8, and the RE agent retracted an earlier 'cannot be",
|
||||
"extracted' to publish these. The waves were located BY PLAYING THEM: Canary",
|
||||
"with `--xma_param_probe=true` prints a stream's packet count and first 32",
|
||||
"bytes when it is played, and searching those bytes in the bank gives the",
|
||||
"offset.",
|
||||
"",
|
||||
"WARNING, from the same finding: the file order is NOT cue-id order. These",
|
||||
"cannot be counted out, and an index here would be a fabrication.",
|
||||
"",
|
||||
"`name_match` is the authors' own identifier GUESSED BY NAME. It is carried",
|
||||
"so the guess is not lost and is never presented as the measurement. Where",
|
||||
"the RE agent did not separate two candidates, there is no name at all --",
|
||||
"an absent `name_match` means nobody has claimed one, never that the",
|
||||
"BINDING is unknown. The binding is the measured part.",
|
||||
"",
|
||||
"All three are mono 48 kHz; that is the RE agent's statement in",
|
||||
"`sylpheed_formats::media::se_wave_riff`, not something re-derived here."
|
||||
],
|
||||
|
||||
"se": {
|
||||
"_": [
|
||||
"MEASURED, HANDOFF Q8, and the RE agent retracted an earlier 'cannot be",
|
||||
"extracted' to publish these. The waves were located BY PLAYING THEM: Canary",
|
||||
"with `--xma_param_probe=true` prints a stream's packet count and first 32",
|
||||
"bytes when it is played, and searching those bytes in the bank gives the",
|
||||
"offset.",
|
||||
"",
|
||||
"WARNING, from the same finding: the file order is NOT cue-id order. These",
|
||||
"cannot be counted out, and an index here would be a fabrication.",
|
||||
"",
|
||||
"`name_match` is the authors' own identifier GUESSED BY NAME. It is carried",
|
||||
"so the guess is not lost and is never presented as the measurement. Where",
|
||||
"the RE agent did not separate two candidates, there is no name at all --",
|
||||
"an absent `name_match` means nobody has claimed one, never that the",
|
||||
"BINDING is unknown. The binding is the measured part.",
|
||||
"",
|
||||
"All three are mono 48 kHz; that is the RE agent's statement in",
|
||||
"`sylpheed_formats::media::se_wave_riff`, not something re-derived here."
|
||||
],
|
||||
"move": {
|
||||
"bank": "Static.slb",
|
||||
"offset": "0x1ec0",
|
||||
"packets": 4,
|
||||
"channels": 1,
|
||||
"rate": 48000,
|
||||
"name_match": "SE_UI_CURSOR",
|
||||
"why": "HANDOFF Q8, measured: the d-pad move cue, 8 192 B / 0.533 s, reproduced across two boots. Left/right play nothing at all, which is a measurement too and is why there is no `left`/`right` row here rather than a silent file."
|
||||
},
|
||||
"confirm": {
|
||||
"bank": "Static.slb",
|
||||
"offset": "0x5d6c0",
|
||||
"packets": 6,
|
||||
"channels": 1,
|
||||
"rate": 48000,
|
||||
"why": "HANDOFF Q8, measured: the (A) confirm cue, 12 288 B / 1.016 s. NO `name_match`: Q8 is explicit that (A)'s wave was not separated between `SE_UI_DECIDE` and `SE_UI_SUB_WIN_OPN`, so naming it would invent the one thing the measurement did not settle."
|
||||
},
|
||||
"back": {
|
||||
"bank": "Static.slb",
|
||||
"offset": "0x0ec0",
|
||||
"packets": 2,
|
||||
"channels": 1,
|
||||
"rate": 48000,
|
||||
"why": "HANDOFF Q8, measured: the (B) back cue, 4 096 B / 0.344 s, reproduced across two boots. No `name_match` for the same reason as `confirm` -- Q8 names no identifier for it."
|
||||
}
|
||||
"move": {
|
||||
"bank": "Static.slb",
|
||||
"offset": "0x1ec0",
|
||||
"packets": 4,
|
||||
"channels": 1,
|
||||
"rate": 48000,
|
||||
"name_match": "SE_UI_CURSOR",
|
||||
"why": "HANDOFF Q8, measured: the d-pad move cue, 8 192 B / 0.533 s, reproduced across two boots. Left/right play nothing at all, which is a measurement too and is why there is no `left`/`right` row here rather than a silent file."
|
||||
},
|
||||
|
||||
"bgm": {
|
||||
"_": [
|
||||
"MEASURED, NOT CHOSEN -- and the port got this wrong for one iteration.",
|
||||
"",
|
||||
"`docs/port/BLOCKED.md` carried a row reading 'not on the disc ... the port",
|
||||
"is choosing a track, and that choice is authored', and the first draft of",
|
||||
"this file duly picked BGM_001 and labelled it arbitrary. That row was not",
|
||||
"stale: `BGM_103` is in HANDOFF at `9ca1eb5`, which is the exact commit the",
|
||||
"row says it was reconciled against. It was WRONG WHEN WRITTEN.",
|
||||
"",
|
||||
"What HANDOFF actually says is a negative with a stated reach, and the reach",
|
||||
"is what the port dropped: the *tables* cannot say which BGM a screen plays",
|
||||
"-- `SOUNDS`, `FILES` and the bank headers name no screen. The EXECUTABLE",
|
||||
"can. `GamePart_Title`'s phase handler `sub_821C5580` carries `li r5, 1103`",
|
||||
"into a sound call, cue 1103 is `BGM_103`, and `BGM_103.slb`'s two declared",
|
||||
"waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA",
|
||||
"probe saw decoding at the main menu. Static code, disc census and runtime",
|
||||
"agree. HANDOFF's own words: 'The port does not have to choose a track.'",
|
||||
"",
|
||||
"So this section is a CITATION, not a decision. It lives in `authored/`",
|
||||
"only because the binding is in the .xex and the exporter reads data files,",
|
||||
"not code -- and it must be deleted the day something the exporter can read",
|
||||
"states it. The loop policy below IS still a decision."
|
||||
],
|
||||
"main_menu": {
|
||||
"bank": "BGM_103.slb",
|
||||
"loop": "restart",
|
||||
"kind": "measured",
|
||||
"why": "MEASURED, HANDOFF Q10 -- NOT a port choice. `GamePart_Title`'s phase handler `sub_821C5580` plays cue 1103 = `BGM_103`, and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe saw decoding at the main menu. Static code, disc census and runtime all agree; see docs/re/menu-audio-cues.md and docs/re/structures/bgm-two-stems.md. The name carries its `.slb` extension because that is what `sound.pak` hashes -- `BGM_103` alone resolves to nothing, which is how the first draft of this file failed.",
|
||||
"loop_why": "AUTHORED, and it will be AUDIBLY WRONG AT THE SEAM. HANDOFF Q10: no loop-point field has been identified anywhere, so the file carries no marked restart. `restart` replays from sample 0, which means the listener hears the track's own fade-out and the silence after it before the music comes back. Trimming to the fade would sound better and would be worse: it would INVENT a loop point, and an invented one is indistinguishable from a decoded one a month later. The seam stays ugly until a loop point is measured or a capture of the real menu looping settles it.",
|
||||
"stems": "sum",
|
||||
"stems_why": "MEASURED, HANDOFF Q10: a bank is exactly TWO waves of identical duration (32/32 banks on the disc), sample-synchronous -- transient correlation peaks at lag 0.00 s over +/-5 s and both stop at the same millisecond. Concatenating them plays the piece twice, the second time as a bass-less stem; that was the previous reading and it is refuted. Emitting two files would be wrong for a second reason: MODDING rule 1 is one logical asset, one file, and handing a modder two stems to line up by hand is the reassembly the exporter exists to have already done. WHAT IS SUMMED IS SETTLED; WHAT WAVE 1 IS, IS NOT -- HANDOFF calls it quieter, far more L/R-decorrelated and almost bass-free, so it reads as a surround-rear pair OR a second intensity layer, and `ChannelMask` is 0x0002 on both so the file will not say. A unity sum is right under either reading; a weighting would only be justified once that is settled."
|
||||
}
|
||||
"confirm": {
|
||||
"bank": "Static.slb",
|
||||
"offset": "0x5d6c0",
|
||||
"packets": 6,
|
||||
"channels": 1,
|
||||
"rate": 48000,
|
||||
"why": "HANDOFF Q8, measured: the (A) confirm cue, 12 288 B / 1.016 s. NO `name_match`: Q8 is explicit that (A)'s wave was not separated between `SE_UI_DECIDE` and `SE_UI_SUB_WIN_OPN`, so naming it would invent the one thing the measurement did not settle."
|
||||
},
|
||||
"back": {
|
||||
"bank": "Static.slb",
|
||||
"offset": "0x0ec0",
|
||||
"packets": 2,
|
||||
"channels": 1,
|
||||
"rate": 48000,
|
||||
"why": "HANDOFF Q8, measured: the (B) back cue, 4 096 B / 0.344 s, reproduced across two boots. No `name_match` for the same reason as `confirm` -- Q8 names no identifier for it."
|
||||
}
|
||||
},
|
||||
"bgm": {
|
||||
"_": [
|
||||
"MEASURED, NOT CHOSEN -- and the port got this wrong for one iteration.",
|
||||
"",
|
||||
"`docs/port/BLOCKED.md` carried a row reading 'not on the disc ... the port",
|
||||
"is choosing a track, and that choice is authored', and the first draft of",
|
||||
"this file duly picked BGM_001 and labelled it arbitrary. That row was not",
|
||||
"stale: `BGM_103` is in HANDOFF at `9ca1eb5`, which is the exact commit the",
|
||||
"row says it was reconciled against. It was WRONG WHEN WRITTEN.",
|
||||
"",
|
||||
"What HANDOFF actually says is a negative with a stated reach, and the reach",
|
||||
"is what the port dropped: the *tables* cannot say which BGM a screen plays",
|
||||
"-- `SOUNDS`, `FILES` and the bank headers name no screen. The EXECUTABLE",
|
||||
"can. `GamePart_Title`'s phase handler `sub_821C5580` carries `li r5, 1103`",
|
||||
"into a sound call, cue 1103 is `BGM_103`, and `BGM_103.slb`'s two declared",
|
||||
"waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA",
|
||||
"probe saw decoding at the main menu. Static code, disc census and runtime",
|
||||
"agree. HANDOFF's own words: 'The port does not have to choose a track.'",
|
||||
"",
|
||||
"So this section is a CITATION, not a decision. It lives in `authored/`",
|
||||
"only because the binding is in the .xex and the exporter reads data files,",
|
||||
"not code -- and it must be deleted the day something the exporter can read",
|
||||
"states it. The loop policy below IS still a decision."
|
||||
],
|
||||
"main_menu": {
|
||||
"bank": "BGM_103.slb",
|
||||
"loop": "restart",
|
||||
"kind": "measured",
|
||||
"why": "MEASURED, HANDOFF Q10 -- NOT a port choice. `GamePart_Title`'s phase handler `sub_821C5580` plays cue 1103 = `BGM_103`, and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe saw decoding at the main menu. Static code, disc census and runtime all agree; see docs/re/menu-audio-cues.md and docs/re/structures/bgm-two-stems.md. The name carries its `.slb` extension because that is what `sound.pak` hashes -- `BGM_103` alone resolves to nothing, which is how the first draft of this file failed.",
|
||||
"loop_why": "AUTHORED, and it will be AUDIBLY WRONG AT THE SEAM. HANDOFF Q10: no loop-point field has been identified anywhere, so the file carries no marked restart. `restart` replays from sample 0, which means the listener hears the track's own fade-out and the silence after it before the music comes back. Trimming to the fade would sound better and would be worse: it would INVENT a loop point, and an invented one is indistinguishable from a decoded one a month later. The seam stays ugly until a loop point is measured or a capture of the real menu looping settles it.",
|
||||
"stems": "sum",
|
||||
"stems_why": "MEASURED, HANDOFF Q10: a bank is exactly TWO waves of identical duration (32/32 banks on the disc), sample-synchronous -- transient correlation peaks at lag 0.00 s over +/-5 s and both stop at the same millisecond. Concatenating them plays the piece twice, the second time as a bass-less stem; that was the previous reading and it is refuted. Emitting two files would be wrong for a second reason: MODDING rule 1 is one logical asset, one file, and handing a modder two stems to line up by hand is the reassembly the exporter exists to have already done. WHAT IS SUMMED IS SETTLED; WHAT WAVE 1 IS, IS NOT -- HANDOFF calls it quieter, far more L/R-decorrelated and almost bass-free, so it reads as a surround-rear pair OR a second intensity layer, and `ChannelMask` is 0x0002 on both so the file will not say. A unity sum is right under either reading; a weighting would only be justified once that is settled."
|
||||
}
|
||||
},
|
||||
"voice": {
|
||||
"_": [
|
||||
"WHICH PRESENTATION of a cutscene's voice to export. CHOSEN, not measured and",
|
||||
"not decoded -- delete this the day a capture says which one the game plays.",
|
||||
"",
|
||||
"A voice region carries THREE PRESENTATIONS OF ONE TAKE (decoded disc-wide by",
|
||||
"the Decoder: 258 inter-descriptor spans hold one stream, 28 hold three, none",
|
||||
"holds two). The exporter drops the leading one, which this port measured to",
|
||||
"be the tail of the others, and must then pick between the two full-length",
|
||||
"ones. Nothing on the disc ranks them: wEncodeOptions, channel count and",
|
||||
"channel mask are byte-identical across them.",
|
||||
"",
|
||||
"This lives here rather than in the exporter because MISSION section 3 puts a",
|
||||
"value we chose in authored/ with a why. A selection rule compiled into Rust",
|
||||
"reads like something the exporter derived, and nobody deletes what nobody",
|
||||
"can see."
|
||||
],
|
||||
"presentation": "loudest",
|
||||
"presentation_why": [
|
||||
"`loudest` = the full-length stream whose peak is nearest full scale.",
|
||||
"",
|
||||
"IT USED TO BE `highest_rate`, on the Decoder's recommendation, and that",
|
||||
"recommendation was WITHDRAWN as self-contradictory -- it read 'the",
|
||||
"highest-rate, highest-gain one is chunk 1', and those two criteria select",
|
||||
"different streams: ADV chunk 1 is 1118268 B at 0.0 dBFS, chunk 2 is",
|
||||
"1171516 B at -8.3 dBFS.",
|
||||
"",
|
||||
"The reason for switching is a measurement of this port's, not a preference.",
|
||||
"The case for `highest_rate` was that more bytes per second means a better",
|
||||
"encode. It does not, here: ADV chunk 1 is MONO-IN-STEREO (channel 2",
|
||||
"digitally silent) and chunk 2 is DUAL-MONO (both channels identical at",
|
||||
"-8.318574). So chunk 2's extra bytes go into encoding a duplicate of its own",
|
||||
"channel, not into fidelity, and the byte-rate difference is explained without",
|
||||
"any appeal to quality. That removes the only argument for it.",
|
||||
"",
|
||||
"What is left points the other way, and both parts are weak on their own:",
|
||||
"the disc masters its other audio near full scale (the SE cues decode to",
|
||||
"+0.18 dBFS), and S00A's only surviving full-length stream is its louder one",
|
||||
"at -4.2 dBFS -- so `loudest` makes the two cutscenes' dialogue sit at",
|
||||
"comparable levels instead of 4.4 dB apart.",
|
||||
"",
|
||||
"STILL A CHOICE. One capture of the intro with dialogue audible settles it,",
|
||||
"and it is the last unforced decision in the voice pipeline."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,28 @@ pub struct BgmSpec {
|
||||
pub stems_why: Option<String>,
|
||||
}
|
||||
|
||||
/// Which of a voice region's full-length presentations to export.
|
||||
///
|
||||
/// A region carries three presentations of one take and **nothing on the disc
|
||||
/// ranks them** — `wEncodeOptions`, channel count and channel mask are
|
||||
/// byte-identical across them. So this is a CHOICE, it lives in
|
||||
/// `authored/audio.json` with its `why`, and it is deleted the day a capture
|
||||
/// says which one the game plays.
|
||||
#[derive(Deserialize, Default, Clone, Copy, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Presentation {
|
||||
/// Peak nearest full scale.
|
||||
#[default]
|
||||
Loudest,
|
||||
/// Most bytes per second.
|
||||
HighestRate,
|
||||
}
|
||||
|
||||
/// `authored/audio.json`, with the documentation keys dropped.
|
||||
pub struct Config {
|
||||
pub se: Vec<(String, CueSpec)>,
|
||||
pub bgm: Vec<(String, BgmSpec)>,
|
||||
pub voice: Presentation,
|
||||
}
|
||||
|
||||
/// Read `authored/audio.json`, or `None` when there is no such file.
|
||||
@@ -104,6 +122,8 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
|
||||
se: BTreeMap<String, serde_json::Value>,
|
||||
#[serde(default)]
|
||||
bgm: BTreeMap<String, serde_json::Value>,
|
||||
#[serde(default)]
|
||||
voice: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
@@ -128,9 +148,18 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Absent means `loudest`, which is what the file says today. A default here
|
||||
// is safe in a way a default matrix is not: the manifest records which
|
||||
// presentation was taken and why, on every entry.
|
||||
let voice = match file.voice.get("presentation") {
|
||||
Some(v) => serde_json::from_value(v.clone())
|
||||
.with_context(|| format!("authored/audio.json: voice.presentation {v}"))?,
|
||||
None => Presentation::default(),
|
||||
};
|
||||
Ok(Some(Config {
|
||||
se: entries(file.se, "se")?,
|
||||
bgm: entries(file.bgm, "bgm")?,
|
||||
voice,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -535,6 +564,7 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
out: &Path,
|
||||
movie: &str,
|
||||
video_duration_s: Option<f32>,
|
||||
presentation: Presentation,
|
||||
) -> Result<Option<Exported>> {
|
||||
use sylpheed_formats::slb::VoiceLang;
|
||||
|
||||
@@ -590,20 +620,25 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
// region carries **three presentations of one take**, not a mix. Summing a
|
||||
// take with a scaled copy of itself adds ~4 dB and colours it.
|
||||
//
|
||||
// The selector is the **highest byte rate** among the equal-duration
|
||||
// survivors, on the Decoder's recommendation. 🟡 That is a recommendation and
|
||||
// not a decoded field: no flag on the disc says which presentation the game
|
||||
// plays, and on `ADV` it picks the quieter of the two (-8.3 dBFS against
|
||||
// 0.0). Recorded in the manifest so the choice is visible and reversible.
|
||||
// WHICH of the equal-duration survivors is a CHOICE, and it lives in
|
||||
// `authored/audio.json` rather than here -- see [`Presentation`]. It was
|
||||
// `highest_rate` on the Decoder's recommendation until that was withdrawn as
|
||||
// self-contradictory, and the reason it is now `loudest` is a measurement:
|
||||
// `ADV`'s louder presentation is mono-in-stereo while its higher-rate one is
|
||||
// DUAL-MONO, so the extra bytes encode a duplicate channel rather than
|
||||
// fidelity, and the rate difference is explained without appealing to
|
||||
// quality at all.
|
||||
let tied: Vec<usize> = (0..all.len())
|
||||
.filter(|&i| !silent.contains(&i) && (longest - lengths[i]).abs() < 0.001)
|
||||
.collect();
|
||||
let keep: Vec<usize> = tied
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by_key(|&i| riffs[i].len())
|
||||
.into_iter()
|
||||
.collect();
|
||||
let chosen = match presentation {
|
||||
Presentation::HighestRate => tied.iter().copied().max_by_key(|&i| riffs[i].len()),
|
||||
Presentation::Loudest => tied
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(|&a, &b| probed[a].1.total_cmp(&probed[b].1)),
|
||||
};
|
||||
let keep: Vec<usize> = chosen.into_iter().collect();
|
||||
let dropped: Vec<String> = (0..all.len())
|
||||
.filter(|i| !keep.contains(i))
|
||||
.map(|i| {
|
||||
@@ -708,14 +743,19 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
summing them as HANDOFF Q10's two stems, which its own measurements refuted: \
|
||||
S00A's second full-length chunk is DIGITAL SILENCE and ADV's is 0.60x the first \
|
||||
with 26.8 dB of residual. Summing a take with a scaled copy of itself adds ~4 dB \
|
||||
and colours it. 🟡 The kept stream is the HIGHEST BYTE RATE among the \
|
||||
equal-duration survivors, which is a recommendation and NOT a decoded field: \
|
||||
nothing on the disc says which presentation the game plays, and on ADV this \
|
||||
picks the quieter of two.{} Folded to mono from the {} of {channels} declared \
|
||||
and colours it. 🟡 WHICH of the equal-duration survivors is kept is a CHOICE, \
|
||||
not a decoded field -- authored/audio.json voice.presentation = {:?}, with its \
|
||||
why. Nothing on the disc ranks the presentations: wEncodeOptions, channel count \
|
||||
and channel mask are byte-identical across them. One capture of the movie with \
|
||||
dialogue audible deletes that entry.{} Folded to mono from the {} of {channels} declared \
|
||||
channel(s) that carry signal -- channel 2 of both voice streams is digitally \
|
||||
silent, and averaging it in cost 5.94 dB until this was measured rather than \
|
||||
read off the declared count.{against}",
|
||||
riffs.len(),
|
||||
match presentation {
|
||||
Presentation::Loudest => "loudest",
|
||||
Presentation::HighestRate => "highest_rate",
|
||||
},
|
||||
if dropped.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
|
||||
@@ -241,8 +241,35 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
// Derived output is regenerated wholesale: clear it, so a screen that stops
|
||||
// being exported stops existing rather than lingering as a stale file that
|
||||
// still validates.
|
||||
//
|
||||
// 🔴 EXCEPT `video/`, and leaving it out was a bug that hid in plain sight.
|
||||
// `video::transcode` has always carried a cache -- it writes a `.cmd`
|
||||
// sidecar with the exact command, the source size and the channel count, and
|
||||
// skips the encode when all three still match. Its own doc comment says
|
||||
// "without it every re-export pays ~4 minutes to produce a byte-identical
|
||||
// file". **This wipe deleted the sidecar and the output immediately before
|
||||
// the check, so the cache had never hit once.** Six exports in one session
|
||||
// paid ~48 minutes of Theora to produce five byte-identical files, and
|
||||
// nothing reported it: the cache is silent when it works and silent when it
|
||||
// does not.
|
||||
//
|
||||
// The wholesale guarantee is kept rather than weakened -- everything else is
|
||||
// still cleared outright, and `prune_videos` below deletes any file in
|
||||
// `video/` that this run did not claim, so a movie that stops being exported
|
||||
// still stops existing.
|
||||
if out.exists() {
|
||||
std::fs::remove_dir_all(&out).context("clear the output tree")?;
|
||||
for entry in std::fs::read_dir(&out).context("clear the output tree")? {
|
||||
let entry = entry?;
|
||||
if entry.file_name() == "video" {
|
||||
continue;
|
||||
}
|
||||
if entry.file_type()?.is_dir() {
|
||||
std::fs::remove_dir_all(entry.path())
|
||||
} else {
|
||||
std::fs::remove_file(entry.path())
|
||||
}
|
||||
.with_context(|| format!("clear {}", entry.path().display()))?;
|
||||
}
|
||||
}
|
||||
std::fs::create_dir_all(&out)?;
|
||||
|
||||
@@ -332,13 +359,15 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
None => println!(" video {} not on this disc -- skipped", m.src),
|
||||
}
|
||||
}
|
||||
prune_videos(out, &videos)?;
|
||||
|
||||
// P6. Both tables are AUTHORED, for two different reasons -- the cue offsets
|
||||
// because they were measured off the running game and are on the disc in no
|
||||
// findable form, the BGM choice because HANDOFF Q10 is a negative and
|
||||
// nothing states which track a menu plays. See `authored/audio.json`.
|
||||
let mut audio = Vec::new();
|
||||
match audio::load(authored_dir)? {
|
||||
let audio_cfg = audio::load(authored_dir)?;
|
||||
match &audio_cfg {
|
||||
None => println!(" no authored/audio.json -- no audio exported"),
|
||||
Some(cfg) => {
|
||||
let source = media::DirectorySource::new(disc);
|
||||
@@ -406,7 +435,11 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
{
|
||||
let source = media::DirectorySource::new(disc);
|
||||
for (stem, len) in &movie_lengths {
|
||||
match audio::export_voice(&source, out, stem, *len)? {
|
||||
// The presentation choice is AUTHORED and this block runs even when
|
||||
// there is no `authored/audio.json` -- the voice binding is decoded,
|
||||
// so the dialogue exports either way and only the choice defaults.
|
||||
let want = audio_cfg.as_ref().map(|c| c.voice).unwrap_or_default();
|
||||
match audio::export_voice(&source, out, stem, *len, want)? {
|
||||
Some(a) => {
|
||||
println!(
|
||||
" voice {:<8} -> {} ({}, {} region chunk(s))",
|
||||
@@ -475,3 +508,38 @@ fn describe(a: &audio::Exported) -> String {
|
||||
None => peak,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Delete anything in `video/` this run did not produce.
|
||||
///
|
||||
/// `video/` is the one directory the wholesale wipe spares, so that the
|
||||
/// transcode cache survives to be consulted. This restores the guarantee the
|
||||
/// wipe exists for: a movie that stops being exported stops existing, rather
|
||||
/// than lingering as a file the manifest no longer lists.
|
||||
fn prune_videos(out: &Path, kept: &[ManifestVideo]) -> Result<()> {
|
||||
let dir = out.join("video");
|
||||
if !dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut keep: Vec<String> = Vec::new();
|
||||
for v in kept {
|
||||
if let Some(name) = Path::new(&v.file).file_name() {
|
||||
let name = name.to_string_lossy().into_owned();
|
||||
keep.push(name.clone());
|
||||
// The cache sidecar goes with the file it stamps.
|
||||
if let Some(stem) = Path::new(&name).file_stem() {
|
||||
keep.push(format!("{}.cmd", stem.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for entry in std::fs::read_dir(&dir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if keep.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
println!(" video {name} is no longer exported -- removed");
|
||||
let _ = std::fs::remove_file(entry.path());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ HANDOFF.
|
||||
|
||||
| P6 BGM — the sub-wave count | **is a music bank's LEADING REGION a stem, or a decoder artefact?** | Q10 | 🔴 **HANDOFF and the decoders disagree, and P6 ships the disagreement.** `media::sound_bank_riffs("BGM_103.slb")` returns **three** sub-waves; HANDOFF Q10's census says a music bank is *"exactly two waves of identical duration (32/32 banks on the disc)"*. The third comes from `slb.rs:380` `to_xma_riffs`, whose hybrid branch emits a leading headerless packet region ahead of the `RIFF` waves — and `docs/re/REFUTED.md` already records that region as what makes `BGM_106`–`BGM_109` *"break the two-wave rule"*. Derived at HANDOFF `9ca1eb5`. **The exporter sums all three and writes a manifest warning**, because choosing which sub-wave to drop is a decoding question and MISSION §2 forbids this exporter answering one. So the menu currently plays a sum of three things where the census predicts two. What settles it: whether that leading region carries music. Raised with the Decoder 2026-08-29. |
|
||||
| ~~P3 — the plate's ONSET~~ | ~~visible 2.13 s after settle, or group starts then?~~ | Q2 | ✅ **resolved 2026-08-29, and the answer is AUTHOR NOTHING.** The port's refutation held and produced a better answer than either option it offered. Correction at `5b0a6e6` on `auto/no-disc-and-menu-captures`: **both builds run on one clock, started together**, and the plate arrives at its own declared `t=238`. Checked against this export rather than taken on trust — build 4's visible build-in ends at `t=118` (`pteff01`, `pteff02`, `ptlogoall_eff` finish together), `ptbtn00` reaches alpha 255 at `t=238`, difference **120 units = 2.000 s**, against a measured 2.138 / 2.132 s at an emulator presenting 28.1 fps rather than 30. The 2.13 s constant is **deleted**. |
|
||||
| P3/P5 — `settle_time()` | **`rest.t` is not when a screen settles, and the port's sequencer uses it** | — | 🔴 **new, mine, and it is the premise that produced the wrong plate arithmetic.** `5b0a6e6` states it plainly: `rest.t` is the last *hold* keyframe before the exit. `title`'s `ptlogo1` has `rest.t=251` and stops **moving** at `t=42`, then creeps 5 px while its alpha crawls `0xe0`→`0xff` over 209 units. `ScreenView.settle_time()` returns the max `rest.t` — **4.350 s** for the title where the visible build-in is over at `t=118` = **1.967 s**. Everything the boot sequencer paces off that landmark is therefore late: `publisher_logo` is held to t=235, `developer_logos` to t=190, and `_script_settled` waits on it too. ⚠️ **Not changed this iteration**, because "visible arrival" is a heuristic and getting it wrong re-paces every screen; it needs its own gate. **What settles it outright:** the oracle is live — a timed boot with screen-to-screen transitions would give the real dwells directly. Asked 2026-08-29. |
|
||||
| ~~P3/P5 — `settle_time()`~~ | ~~`rest.t` is not when a screen settles, and the port's sequencer uses it~~ | — | ✅ **MEASURED 2026-08-29 and the row was HALF WRONG — mine.** The Decoder took it on a cold profile with no shader cache (`auto/no-disc-and-menu-captures` at `4bd4779`, `docs/re/boot-settle-times-measured.md`). The principle holds: the title's `rest.t` is 251 units = **4.183 s** where its art finishes at ~2 s. **But "everything the sequencer paces off that landmark is therefore late" does not.** Measured the port the way the game was measured — visible span, `--film` at 4 fps — the publisher wordmark runs **4.25 s** against the game's 4.297/4.604/4.370 and the developer logos **3.50 s** against 3.508/3.503/3.366. Dead on. My earlier reading compared the port's *arrival-to-arrival* timestamps against the game's *visible spans*, which differ by the exit ramp plus the black hold — the whole of the discrepancy I was about to chase. `rest.t` is still the wrong landmark; its blast radius is `_script_settled` waiting longer than it needs to, which is a slow test and not a wrong frame. `dwell_seconds` stays `null`, now for a measured reason. 🔴 **Do not author an Ⓐ→menu dwell**: it measures 3.763 s and contains a 1.53 s guest load stall, third independent reproduction. 🟡 Menu build-in 0.531 s and Ⓑ→title 0.482 s rest on one run and are not authored; the port is within ~0.1 s of both from the disc. |
|
||||
| P4/P7 — which voice presentation | **which of a region's two full-length streams does the game play?** | — | 🟡 **the last unforced decision in the voice pipeline, and it is now unambiguously the port's.** The Decoder's "highest byte rate" was **withdrawn as self-contradictory** — its sentence read *"the highest-rate, highest-gain one is chunk 1"*, and those select different streams (`ADV` chunk 1: 1 118 268 B at 0.0 dBFS; chunk 2: 1 171 516 B at −8.3). Nothing on the disc ranks them: `wEncodeOptions`, channel count and channel mask are byte-identical. Moved to `authored/audio.json` `voice.presentation` per MISSION §3, set to **`loudest`**, and the reason is a measurement of mine rather than a preference: chunk 1 is **mono-in-stereo** and chunk 2 is **dual-mono**, so chunk 2's extra bytes encode a duplicate channel rather than fidelity — which explains the rate difference and removes the only argument for it. What settles it: **one capture of the intro with dialogue audible.** That did not ride the settle-time boot, which drove the title path and never played the movie with audio. |
|
||||
| ~~P3/P5 — the title plate~~ | ~~does the idle title show `PRESS Ⓐ`~~ | Q2 | ✅ **answered and TAKEN at this iteration.** `auto/no-disc-and-menu-captures` at `fb536df`, `docs/re/title-plate-delay-measured.md`, traces in `docs/re/data/plate-timing-run{1,2}.tsv`. It is the third case: build 4 alone, then the plate composited over it. ⚠️ The delay is timed from where build 4 **stops animating**, not from where it first appears — measured the other way the two runs differ by 0.48 s against 6 ms. `ScreenView` now draws two builds at once, as a second `ScreenView` in the same `SubViewport` rather than a subordinate screen inside one. The onset question above is what is left. |
|
||||
|
||||
| P3 — the plate's PULSE | **does the plate's focus record loop, and with what period?** | Q2 | ❔ **open, and the port's earlier reading of it was wrong.** The port had looked for the pulse in `ptbtn00`'s own group; `5b0a6e6` identifies it as the plate's **focus record** `ptbtn00f` — a glow ramping alpha `0x00`→`0x50` and back, t=6…105. Measured on the running game at 2.12 / 2.19 / 2.34 / 2.31 s, mean **2.24 s**. 🟡 **The port has not taken it.** Looping that record needs a period, and its group is 105 timed units plus the **authored** 24-unit exit ramp = 129 units = 2.15 s — composing an authored constant with a loop assumption to land on a measured number is tuning, not measuring. Separately: the port draws no focus record on `press_start` at all, because the screen has no `buttons` and nothing is focused, so *whether the game always draws it* is its own question. |
|
||||
|
||||
@@ -2790,3 +2790,135 @@ on the first movie anybody checked.
|
||||
|
||||
❔ **Why the disc stores three presentations at all is unanswered**, by either of
|
||||
us.
|
||||
|
||||
## The transcode cache had never hit, because the wipe ran first
|
||||
|
||||
`video::transcode` has carried a cache since P4. It writes a `.cmd` sidecar with
|
||||
the exact ffmpeg command, the source's byte count and its channel count, and
|
||||
skips the encode when all three still match. Its doc comment says why: *"without
|
||||
it every re-export pays ~4 minutes to produce a byte-identical file, and an
|
||||
exporter nobody re-runs is worse than a cache."*
|
||||
|
||||
**It had never hit once.** `main.rs` clears the output tree wholesale — and the
|
||||
`remove_dir_all` runs immediately before the check, deleting the sidecar and the
|
||||
output it stamps. The cache tested a file it had just erased.
|
||||
|
||||
This session ran the exporter **six times** and paid the full Theora encode every
|
||||
one of them, producing five byte-identical files. Roughly 48 minutes. Nothing
|
||||
reported it, and nothing could have: a cache is silent when it works and silent
|
||||
when it does not, and the only symptom is a wall-clock cost that looks like the
|
||||
job simply being slow.
|
||||
|
||||
⚠️ **It is worth being specific about how this hid**, because the ingredients are
|
||||
ordinary. The cache is correct. The wipe is correct. Each carries a doc comment
|
||||
explaining itself, and neither mentions the other. The defect exists only in
|
||||
their ordering, which is stated in neither, and the cost is invisible in every
|
||||
artefact the export produces — the tree is byte-identical either way.
|
||||
|
||||
### The fix keeps the wholesale guarantee rather than trading it away
|
||||
|
||||
The obvious repair — stop wiping — would break what the wipe is for: *a screen
|
||||
that stops being exported stops existing, rather than lingering as a stale file
|
||||
that still validates.* So the wipe now spares exactly `video/`, and
|
||||
`prune_videos` deletes anything in it this run did not claim. Everything else is
|
||||
still cleared outright.
|
||||
|
||||
That is a **cache, not a hand-edit**, and the distinction matters against
|
||||
MISSION §3: nothing in `export/` is authored, the sidecar is derived from the
|
||||
command the exporter itself computed, and any change to the command, the source
|
||||
size or the channel count re-encodes. A modder who edits an `.ogv` by hand gets
|
||||
it overwritten on the next export, exactly as before.
|
||||
|
||||
🟡 **Not measured yet:** that a cached run reproduces the same tree. The claim is
|
||||
structural — the skip is keyed on the whole command string — but "the second run
|
||||
produces the same bytes" is checkable and has not been checked.
|
||||
|
||||
## `settle_time()` — the answer arrived, and it refutes my own 🔴 more than it confirms it
|
||||
|
||||
The Decoder took the port's top ask and measured the boot on a cold profile with
|
||||
no shader cache (`auto/no-disc-and-menu-captures` at `4bd4779`,
|
||||
`docs/re/boot-settle-times-measured.md`). It confirms the *principle* I filed:
|
||||
the title's `rest.t` is 251 units = **4.183 s** where its art is finished at about
|
||||
2 s, so `rest.t` is not when a screen arrives.
|
||||
|
||||
**But my row said more than that**, and the extra part is wrong. It said
|
||||
*"everything the boot sequencer paces off that landmark is therefore late"*, and
|
||||
named `publisher_logo` and `developer_logos`. So I measured the port the way the
|
||||
game was measured — **visible span, not arrival-to-arrival** — with `--film` at
|
||||
4 fps and a per-frame greyscale mean:
|
||||
|
||||
| | port, visible span | game, three cold boots | |
|
||||
|---|---|---|---|
|
||||
| publisher wordmark | **4.25 s** | 4.297 / 4.604 / 4.370 | 0.05 s under the lowest |
|
||||
| developer logos | **3.50 s** | 3.508 / 3.503 / 3.366 | **dead on** |
|
||||
| black hold between | ≈0.25 s | 0.2 – 0.3 s | inside |
|
||||
| title settled → plate | 2.000 s (declared 120 units) | 2.247 s | inside, at ~28 fps presentation |
|
||||
|
||||
**The splashes are not late. They match.** ⚠️ And the reason my earlier reading
|
||||
said otherwise is worth keeping: I had compared the port's *transition
|
||||
timestamps* — 4.68 s and 3.94 s, arrival to arrival — against the game's
|
||||
*visible spans*. Those differ by the exit ramp plus the black hold, about 0.6 s,
|
||||
which is the whole of the discrepancy I was about to chase. This corpus has been
|
||||
bitten by exactly this before, in the plate delay: *"timed from where build 4
|
||||
stops animating, not from where it first appears — measured the other way the two
|
||||
runs differ by 0.48 s against 6 ms."*
|
||||
|
||||
So the port paces the boot correctly, and **`rest.t` is a wrong landmark whose
|
||||
blast radius is much smaller than I claimed**: on the screens the sequencer
|
||||
actually advances off, `rest.t` plus the 24-unit exit ramp lands where the game
|
||||
lands. What it still affects is `_script_settled`, which waits longer than it
|
||||
needs to before photographing — a slow test, not a wrong frame.
|
||||
|
||||
### `dwell_seconds` stays `null`, and the question is now closed rather than open
|
||||
|
||||
`authored/timing.json` says of it: *"If a capture ever times the real boot, this
|
||||
is where that number goes."* A capture has now timed the real boot, and the
|
||||
answer is that **nothing goes there** — the disc's own keyframe groups reproduce
|
||||
the game's dwells to 0.05 s and 0.01 s. The field stays `null` for a measured
|
||||
reason instead of an absence of one.
|
||||
|
||||
### Taken from the same page, and not taken
|
||||
|
||||
* ✅ **The 120-unit plate delay stands.** The Decoder ran a refutation of it that
|
||||
failed instructively: its probe's `title_static` mark gave 3.203 s, which on a
|
||||
cold boot looks like a real effect. It was the instrument — the mark fires
|
||||
during the crossfade out of the attract movie, with the glyph count still 0.
|
||||
Re-measured from content: 2.247 s. **The port changes nothing**, and the
|
||||
declared 120 units is what it keeps.
|
||||
* 🔴 **No Ⓐ→menu dwell is authored.** It measured 3.763 s and contains a 1.53 s
|
||||
guest load stall — the third independent reproduction of that stall, this one
|
||||
on a cold cache, so it is not a warm-cache artefact. It is emulator time, not a
|
||||
game constant.
|
||||
* 🟡 **Menu build-in 0.531 s and Ⓑ→title 0.482 s are not authored either**, and
|
||||
that is the Decoder's own caveat rather than my caution: they rest on one run,
|
||||
where the plate delay and the load stall are each cross-checked against
|
||||
independent prior evidence. The port is within ~0.1 s of both with its existing
|
||||
24-unit exit ramp, so authoring them would replace a disc-derived number with a
|
||||
provisional measured one and gain nothing measurable.
|
||||
|
||||
## The voice presentation is now unambiguously the port's choice, and the recommendation behind it was withdrawn
|
||||
|
||||
The Decoder has withdrawn "highest byte rate": its sentence read *"the
|
||||
highest-rate, highest-gain one is chunk 1"*, and those two criteria select
|
||||
**different streams** — `ADV` chunk 1 is 1 118 268 B at 0.0 dBFS, chunk 2 is
|
||||
1 171 516 B at −8.3. The rule named one and the parenthetical named the other. I
|
||||
implemented the rule faithfully and got the quieter presentation.
|
||||
|
||||
What the file can still say is decoded and does not adjudicate it: the `fmt `
|
||||
chunk is a 32-byte `XMAWAVEFORMAT` whose `+0x20` is a declared
|
||||
`PsuedoBytesPerSec` — 8 142 and 8 530 on `ADV`'s two, matching the computed rates
|
||||
to 0.02 % — but `wEncodeOptions` (`0x10d6`), channel count and channel mask are
|
||||
**byte-identical across the presentations**. Nothing in the header ranks them.
|
||||
|
||||
⚠️ One more observable, measured here and not in that page: **the two
|
||||
presentations differ in channel layout.** `ADV` chunk 1 is mono-in-stereo —
|
||||
channel 2 digitally silent — while chunk 2 is **dual-mono**, both channels
|
||||
identical at −8.318574. So they are not two encodes of one file differing only in
|
||||
rate.
|
||||
|
||||
Also recorded, because it cost the Decoder time: **`sylpheed-cli audio info` is
|
||||
not to be trusted on these.** Its "16 channels / 4310 Hz / 2-bit" is
|
||||
`wBitsPerSample`, `wEncodeOptions` and the channel fields read at the wrong
|
||||
offsets — its XMA1 reader is misaligned. That is a tool in this repository
|
||||
reporting confident nonsense, and it is the second time a renderer or reader of
|
||||
ours has been believed before it was checked.
|
||||
|
||||
Reference in New Issue
Block a user